堆棧信息采集
進行性能分析的時候耀怜,如檢測到卡頓袜茧,ANR等異常指標時爪瓜,需要還原現(xiàn)場來進行問題的追蹤,因此知道如何獲取當前的程序的調用堆棧信息來還原現(xiàn)場十分的重要梯捕。
記錄調用堆棧信息的方式大致可通過以下兩種方式獲得
定時器抓取堆棧
啟動一個定時器厢呵,一定時間間隔進行堆棧抓取,按照時間戳存儲在堆棧信息列表中傀顾。當發(fā)生問題時襟铭,根據(jù)當前時間戳從堆棧信息列表中截取一段時間段的堆棧信息。
這種方式實現(xiàn)簡單穩(wěn)定短曾,但可能會存在一定的誤差寒砖。
private static final LinkedHashMap<Long, String> stackMap = new LinkedHashMap<>();
...
//抓取堆棧信息
StringBuilder stringBuilder = new StringBuilder(4096);
for (StackTraceElement stackTraceElement : Thread.currentThread().getStackTrace()) {
stringBuilder.append(stackTraceElement.toString())
.append("\r\n");
}
stackMap.put(System.currentTimeMillis(), stringBuilder.toString());
插樁記錄方法隊列
通過Gradle Transfrom API在編譯的過程中去修改字節(jié)碼進行插樁處理,在每個類的每個方法開始和結束位置注入指定代碼用于收集該方法的名稱和耗時并存儲到全局的隊列中嫉拐。當發(fā)生問題時哩都,截取隊列最近的幾條記錄作為堆棧信息。
這種方式能夠準確的定位問題函數(shù)椭岩,但是插樁會增加包的大小和不穩(wěn)定性茅逮。
以下為Tencent Matrix中對于方法插樁的實現(xiàn)
- MatrixTraceTransform#transform()
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
...
//定義方法id和方法名映射文件
final MappingCollector mappingCollector = new MappingCollector()
File mappingFile = new File(traceConfig.getMappingPath());
if (mappingFile.exists() && mappingFile.isFile()) {
MappingReader mappingReader = new MappingReader(mappingFile);
mappingReader.read(mappingCollector)
}
//收集源碼中的類和三方jar包的類
Map<File, File> jarInputMap = new HashMap<>()
Map<File, File> scrInputMap = new HashMap<>()
transformInvocation.inputs.each { TransformInput input ->
input.directoryInputs.each { DirectoryInput dirInput ->
collectAndIdentifyDir(scrInputMap, dirInput, rootOutput, isIncremental)
}
input.jarInputs.each { JarInput jarInput ->
if (jarInput.getStatus() != Status.REMOVED) {
collectAndIdentifyJar(jarInputMap, scrInputMap, jarInput, rootOutput, isIncremental)
}
}
}
//將所有類中的方法收集起來,生成方法id和方法名映射判哥,并保存在mCollectedMethodMap中
MethodCollector methodCollector = new MethodCollector(traceConfig, mappingCollector)
HashMap<String, TraceMethod> mCollectedMethodMap = methodCollector.collect(scrInputMap.keySet().toList(), jarInputMap.keySet().toList())
//處理所有的類對其進行插樁
MethodTracer methodTracer = new MethodTracer(traceConfig, mCollectedMethodMap, methodCollector.getCollectedClassExtendMap())
methodTracer.trace(scrInputMap, jarInputMap)
origTransform.transform(transformInvocation)
Log.i("Matrix." + getName(), "[transform] cost time: %dms", System.currentTimeMillis() - start)
}
- MethodTracer#trace() -> MethodTracer#traceMethodFromSrc() -> MethodTracer#innerTraceMethodFromSrc()
private void innerTraceMethodFromSrc(File input, File output) {
...
// 通過asm庫對類的字節(jié)碼進行修改
is = new FileInputStream(classFile);
ClassReader classReader = new ClassReader(is);
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor classVisitor = new TraceClassAdapter(Opcodes.ASM5, classWriter);
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
is.close();
if (output.isDirectory()) {
os = new FileOutputStream(changedFileOutput);
} else {
os = new FileOutputStream(output);
}
os.write(classWriter.toByteArray());
os.close();
}
- MethodTracer.TraceClassAdapter
private class TraceClassAdapter extends ClassVisitor {
...
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
...
//在類中每個方法的節(jié)點,通過asm庫對方法的字節(jié)碼進行修改
MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);
return new TraceMethodAdapter(api, methodVisitor, access, name, desc, this.className,
hasWindowFocusMethod, isMethodBeatClass);
}
...
}
- MethodTracer.TraceMethodAdapter
private class TraceMethodAdapter extends AdviceAdapter {
...
@Override
protected void onMethodEnter() {
//在方法的開始節(jié)點碉考,通過asm庫插入com.tencent.matrix.trace.core.MethodBeat.i()代碼
TraceMethod traceMethod = mCollectedMethodMap.get(methodName);
if (traceMethod != null) {
traceMethodCount.incrementAndGet();
mv.visitLdcInsn(traceMethod.id);
mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "i", "(I)V", false);
}
}
@Override
protected void onMethodExit(int opcode) {
//在方法的結束節(jié)點塌计,通過asm庫插入com.tencent.matrix.trace.core.MethodBeat.o()代碼
TraceMethod traceMethod = mCollectedMethodMap.get(methodName);
if (traceMethod != null) {
...
traceMethodCount.incrementAndGet();
mv.visitLdcInsn(traceMethod.id);
mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "o", "(I)V", false);
}
}
}
com.tencent.matrix.trace.core.MethodBeat.i()和com.tencent.matrix.trace.core.MethodBeat.o()兩段代碼即用來統(tǒng)計每個方法的信息。
幀率采集
Android提供的幀率采集的方式大致上有兩種侯谁,設置Choreographer.FrameCallback回調和OnFrameMetricsAvailableListener回調锌仅。
OnFrameMetricsAvailableListener回調數(shù)據(jù)源更加豐富章钾,同時要求的API版本也較高。
Choreographer.FrameCallback
通過Choreographer.FrameCallback監(jiān)聽器热芹,監(jiān)聽每一幀回調doFrame()贱傀。一般設備刷新頻率在60HZ,即每幀間隔在16.66ms左右伊脓。
final Choreographer mChoreographer = Choreographer.getInstance();
mChoreographer.postFrameCallback(new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
//統(tǒng)計每幀時間府寒,單位納秒
long interval = frameTimeNanos - lastTime;
lastTime = frameTimeNanos;
mChoreographer.postFrameCallback(this);
}
});
OnFrameMetricsAvailableListener
從Android 7.0(API 24)開始,Android SDK新增OnFrameMetricsAvailableListener接口用于提供幀繪制各階段的耗時报腔,數(shù)據(jù)源與 GPU Profile 相同株搔。能夠描述每幀各階段的耗時。
if(Build.VERSION.SDK_INT >= 24) {
getWindow().addOnFrameMetricsAvailableListener(new Window.OnFrameMetricsAvailableListener() {
@Override
public void onFrameMetricsAvailable(Window window, FrameMetrics metrics, int dropCountSinceLastInvocation) {
Log.d("Metrics", "動畫耗時: " + metrics.getMetric((FrameMetrics.ANIMATION_DURATION)) / Math.pow(10, 6));
Log.d("Metrics", "執(zhí)行 OpenGL 命令和 DisplayList 耗時: " + metrics.getMetric(FrameMetrics.COMMAND_ISSUE_DURATION) / Math.pow(10, 6));
Log.d("Metrics", "創(chuàng)建和更新 DisplayList 耗時: " + metrics.getMetric(FrameMetrics.DRAW_DURATION) / Math.pow(10, 6));
Log.d("Metrics", "布爾值纯蛾,標志該幀是否為此 Window 繪制的第一幀: " + metrics.getMetric(FrameMetrics.FIRST_DRAW_FRAME) / Math.pow(10, 6));
Log.d("Metrics", "處理用戶輸入操作的耗時: " + metrics.getMetric(FrameMetrics.INPUT_HANDLING_DURATION) / Math.pow(10, 6));
Log.d("Metrics", "layout/measure 耗時: " + metrics.getMetric(FrameMetrics.LAYOUT_MEASURE_DURATION) / Math.pow(10, 6));
Log.d("Metrics", "CPU 在等待 GPU 完成渲染的耗時: " + metrics.getMetric(FrameMetrics.SWAP_BUFFERS_DURATION) / Math.pow(10, 6));
Log.d("Metrics", "上傳 bitmap 到 GPU 的耗時: " + metrics.getMetric(FrameMetrics.SYNC_DURATION) / Math.pow(10, 6));
Log.d("Metrics", "整幀渲染耗時: " + metrics.getMetric(FrameMetrics.TOTAL_DURATION) / Math.pow(10, 6));
Log.d("Metrics", "未知延遲: " + metrics.getMetric(FrameMetrics.UNKNOWN_DELAY_DURATION) / Math.pow(10, 6));
// 日志輸出纤房,單位毫秒
// D/Metrics: 動畫耗時: 0.499687
// D/Metrics: 執(zhí)行 OpenGL 命令和 DisplayList 耗時: 1.033854
// D/Metrics: 創(chuàng)建和更新 DisplayList 耗時: 2.467813
// D/Metrics: 布爾值,標志該幀是否為此 Window 繪制的第一幀: 0.0
// D/Metrics: 處理用戶輸入操作的耗時: 0.11
// D/Metrics: layout/measure 耗時: 0.448698
// D/Metrics: CPU 在等待 GPU 完成渲染的耗時: 0.537656
// D/Metrics: 上傳 bitmap 到 GPU 的耗時: 0.068906
// D/Metrics: 整幀渲染耗時: 10.150618
// D/Metrics: 未知延遲: 4.950827
}
}, new Handler());
}
UI卡頓、ANR采集
UI卡頓和ANR的原因都是主線程進行耗時操作導致的幀率丟失求摇,因此可以通過監(jiān)聽幀率回調冈涧,或者監(jiān)聽主線程消息處理兩種方式進行采集。
- 監(jiān)聽幀率回調間隔超過指定閾值
通過Choreographer.FrameCallback回調舒岸,當間隔超過不同的閾值則認定為卡頓或ANR(一般卡頓1s,ANR 5s)拄查,并且抓取堆棧信息進行輸出吁津。
- 主線消息處理時間超過指定閾值
通過android.util.Printer對主線程每條消息處理進行打印,通過計算>>>>>和<<<<<兩條日志之間的時間戳得到主線程處理每條消息的時間堕扶。某條消息處理時間超過不同時長可認定為卡頓或ANR
Looper.getMainLooper().setMessageLogging(new Printer() {
@Override
public void println(String x) {
//主線程Looper每從MessageQueue處理一條msg都會回調兩次碍脏,日志格式 ">>>>> Dispatching to " + msg.target + " " + msg.callback + ": " + msg.what
//>>>>> Dispatching to Handler (android.view.ViewRootImpl$ViewRootHandler) {3fac4cc} android.view.View$UnsetPressedState@b842315: 0
//<<<<< Finished to Handler (android.view.ViewRootImpl$ViewRootHandler) {3fac4cc} android.view.View$UnsetPressedState@b842315
//>>>>> Dispatching to Handler (android.app.ActivityThread$H) {bef393} null: 104
//<<<<< Finished to Handler (android.app.ActivityThread$H) {bef393} null
//>>>>> Dispatching to Handler (android.os.Handler) {9fae164} null: 1
//<<<<< Finished to Handler (android.os.Handler) {9fae164} null
}
});
內存信息采集
Android內存幾個指標的含義如下:
Item | 全稱 | 含義 | 等價 |
---|---|---|---|
USS | Unique Set Size | 物理內存 | 進程獨占的內存 |
PSS | Proportional Set Size | 物理內存 | PSS= USS+ 按比例包含共享庫 |
RSS | Resident Set Size | 物理內存 | RSS= USS+ 包含共享庫 |
VSS | Virtual Set Size | 虛擬內存 | VSS= RSS+ 未分配實際物理內存 |
故內存的大小關系:VSS >= RSS >= PSS >= USS
一般統(tǒng)計應用進程的內存以PSS值為主
Android中獲取內存的幾種不同方法
- ActivityManager.MemoryInfo
主要是用于得到當前系統(tǒng)剩余內存的及判斷是否處于低內存運行
ActivityManager.MemoryInfo info = new ActivityManager.MemoryInfo();
((ActivityManager) getSystemService(ACTIVITY_SERVICE)).getMemoryInfo(info);
- Debug.MemoryInfo
Debug.MemoryInfo[] memInfos = activityManager.getProcessMemoryInfo(new int[]{pid1, pid2});
Debug.MemoryInfo memInfo = new Debug.MemoryInfo(); Debug.getMemoryInfo(memInfo);
描述內存使用情況比較詳細數(shù)據(jù),單位KB稍算。前者獲得指定進程內存使用情況典尾,后者獲得當前進程內存。其數(shù)據(jù)也可通過adb shell "dumpsys meminfo com.xxx.xxx"查看指定進程名得到
- Debug#getNativeHeapSize() getNativeHeapAllocatedSize() getNativeHeapFreeSize()
得到Native的內存大概情況糊探,單位B钾埂。分別是native堆已使用內存,剩余內存和native堆本身內存大小
Android內存警告采集
OnLowMemory是Android提供的API科平,在系統(tǒng)內存不足褥紫,所有后臺程序(優(yōu)先級為background的進程,不是指后臺運行的進程)都被殺死時瞪慧,系統(tǒng)會調用OnLowMemory髓考。
OnTrimMemory是Android 4.0 (API1.6)之后提供的API,系統(tǒng)會根據(jù)不同的內存狀態(tài)來回調弃酌。根據(jù)不同的內存狀態(tài)氨菇,來響應不同的內存釋放策略儡炼。
getApplication().registerComponentCallbacks(new ComponentCallbacks2() {
@Override
public void onTrimMemory(int level) {
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
}
@Override
public void onLowMemory() {
}
});
onTrimMemory(int level) level不同值代表不同狀態(tài):
static final int TRIM_MEMORY_COMPLETE = 80;
static final int TRIM_MEMORY_MODERATE = 60;
static final int TRIM_MEMORY_BACKGROUND = 40;
static final int TRIM_MEMORY_UI_HIDDEN = 20;
static final int TRIM_MEMORY_RUNNING_CRITICAL = 15;
static final int TRIM_MEMORY_RUNNING_LOW = 10;
static final int TRIM_MEMORY_RUNNING_MODERATE = 5;
數(shù)值越大一般表示系統(tǒng)內存狀態(tài)越緊張
5 10 15表示該進程是重要進程,系統(tǒng)處于不同低內存狀態(tài)查蓉,需要應用進行相應回收操作乌询。
20 表示進程用戶界面消失,退到后臺
40 60 80 表示該進程是后臺進程豌研,在低內存狀態(tài)時處于回收LRU列表的位置(即將計入LRU妹田,LRU中部,LRU底部)
CPU的使用率采集
獲取應用CPU使用率的步驟如下
- 讀取設備cpu總使用情況(從開機為止)
/proc/stat 計算totalCpuTime - 讀取當前應用cpu使用情況(從進程開始為止)
/proc/pid/stat 計算processCpuTime - 計算cpu使用率
取一段時間內應用cpu使用時間 / cpu總是用時間
△processCpuTime / △totalCpuTime
android 8(API 26) or higher is inaccessible to /proc/stat
/proc/stat: open failed: EACCES (Permission denied)
只有系統(tǒng)應用才能訪問/proc/stat聂沙,無法獲取設備cpu的總使用情況秆麸,只能夠獲取當前應用的cpu使用情況,目前采取的做法可暫時用簡單的兩次采集時間間隔來替代cpu總時間進行計算及汉。
頁面啟動時間采集
頁面啟動時間即打開頁面到完成加載的時間沮趣,Activity生命周期提供了相應的回調函數(shù),也可通過Activity消息事件進一步獲取坷随。
onCreate和onPostCreate回調時間間隔
LAUNCH_ACTIVITY和ENTER_ANIMATION_COMPLETE兩個事件的時間間隔
onCreate 和 onPostCreate的時間間隔房铭,和用戶主觀感受到頁面打開的時間可能存在著偏差,可能采集時更需要從Activity創(chuàng)建到入場動畫結束的時間
查看ActivityThread的源碼得知温眉,Activity的執(zhí)行事件都通過發(fā)送消息交由ActivityThread.H類處理
消息類型定義如下:
private class H extends Handler {
public static final int LAUNCH_ACTIVITY = 100;
public static final int PAUSE_ACTIVITY = 101;
......
public static final int ENTER_ANIMATION_COMPLETE = 149;
......
}
LAUNCH_ACTIVITY表示開始創(chuàng)建Activity缸匪,ENTER_ANIMATION_COMPLETE則表示Activity入場動畫結束完全呈現(xiàn)Activity。
因此类溢,我們可以通過反射獲取到ActivityThread.H的實例對象并且為其設置代理類凌蔬,在代理類中對 LAUNCH_ACTIVITY 和 ENTER_ANIMATION_COMPLETE 兩個消息進行監(jiān)聽計算時間差得到頁面啟動時間
public static void hackCallback() {
try {
//1. 反射ActivityThread的靜態(tài)變量sCurrentActivityThread獲取ActivityThread實例
Class<?> forName = Class.forName("android.app.ActivityThread");
Field field = forName.getDeclaredField("sCurrentActivityThread");
field.setAccessible(true);
Object activityThreadValue = field.get(forName);
//2. 反射獲取ActivityThread實例的mH,這是一個繼承Hadler的實現(xiàn)類
Field mH = forName.getDeclaredField("mH");
mH.setAccessible(true);
Object handler = mH.get(activityThreadValue);
//3. 反射替換mH的mCallback對象為自己創(chuàng)建的靜態(tài)代理類HackCallback
Class<?> handlerClass = handler.getClass().getSuperclass();
Field callbackField = handlerClass.getDeclaredField("mCallback");
callbackField.setAccessible(true);
Handler.Callback originalCallback = (Handler.Callback) callbackField.get(handler);
HackCallback callback = new HackCallback(originalCallback);
callbackField.set(handler, callback);
} catch (Exception e) {
}
}
public class HackCallback implements Handler.Callback {
@Override
public boolean handleMessage(Message msg) {
if (msg.what == LAUNCH_ACTIVITY) {
// TODO
} else if (msg.what == ENTER_ANIMATION_COMPLETE) {
// TODO
}
}
內存泄露采集
內存泄露主要通過定時器不斷的去觀察隊列里檢測不再使用的對象是否已經(jīng)被回收闯冷,如果多次檢測到某一對象沒有回收則認定為該對象內存泄露砂心。
具體的步驟:
- 在對象不使用時將其加入觀察隊列,以WeakReference形式持有蛇耀。針對Activity的泄露辩诞,可以在onDestroy時將其加入觀察隊列
- 定時器每隔一段時間遍歷觀察隊列里的對象,主動觸發(fā)GC
- 如果觀察對象WeakReference為空則說明沒泄露并將其移除纺涤。
- 如果不為空則判斷計數(shù)是否超過最大次數(shù)译暂,沒超過則計數(shù)加1繼續(xù)保留在隊列,超過則說明已經(jīng)泄露撩炊。
- 如果發(fā)生內存泄露則通過android.os.Debug.dumpHprofData(hprofPath) 生成hpro文件進行輸出外永,可以通過此文件對內存泄露進行分析
過度重繪檢測
過度重繪更多的針對于開發(fā)階段的布局問題追查,Android提供了一系列工具可以進行查看拧咳,同時代碼中也可以對View進行分析象迎,幫助我們采集相關問題。
具體的步驟如下:
- 找一特定時機(如生命周期onActDestroyed回調)獲取activity.getWindow().getDecorView()呛踊,遍歷View Tree的對每個View進行分析
- 檢測ImageView的圖片資源大于ImageView的大小尺寸砾淌,給出優(yōu)化提醒
- 檢測View.getBackground()和getDrawable()同時不為空,可能導致過度重繪谭网,給出優(yōu)化提醒
- 檢測View的子View只有一個且大小相同汪厨,給出優(yōu)化提醒