概述
年前,微信開源了Matrix專案,提供了Android、ios的APM實現方案。對於Android端實現,主要包括APK Checker
、Resource Canary
、Trace Canary
、SQLite Lint
、IO Canary
五部分。本文主要介紹Trace Canary
的原始碼實現,其他部分的原始碼分析將在後續推出。
程式碼框架分析
Trace Canary通過位元組碼插樁的方式在編譯期預埋了方法進入、方法退出的埋點。執行期,慢函式檢測、FPS檢測、卡頓檢測、啟動檢測使用這些埋點資訊排查具體哪個函式導致的異常。
編譯期方法插樁程式碼分析
程式碼插樁的整體流程如上圖。在打包過程中,hook生成Dex的Task任務,新增方法插樁的邏輯。我們的hook點是在Proguard之後,Class已經被混淆了,所以需要考慮類混淆的問題。
插樁程式碼邏輯大致分為三步:
-
hook原有的Task,執行自己的
MatrixTraceTransform
,並在最後執行原邏輯 -
在方法插樁之前先要讀取ClassMapping檔案,獲取混淆前方法、混淆後方法的對映關係並儲存在MappingCollector中。
-
之後遍歷所有Dir、Jar中的Class檔案,實際程式碼執行的時候遍歷了兩次。
- 第一次遍歷Class,獲取所有待插樁的Method資訊,並將資訊輸出到methodMap檔案中;
- 第二次遍歷Class,利用ASM執行Method插樁邏輯。
hook原生打包流程
將實際執行的Transform換成了MatrixTraceTransform
public static void inject(Project project, def variant) {
//獲取Matrix trace的gradle配置引數
def configuration = project.matrix.trace
//hook的Task名
String hackTransformTaskName = getTransformTaskName(
configuration.hasProperty('customDexTransformName') ? configuration.customDexTransformName : "",
"",variant.name
)
//同上
String hackTransformTaskNameForWrapper = getTransformTaskName(
configuration.hasProperty('customDexTransformName') ? configuration.customDexTransformName : "",
"Builder",variant.name
)
project.logger.info("prepare inject dex transform :" + hackTransformTaskName +" hackTransformTaskNameForWrapper:"+hackTransformTaskNameForWrapper)
project.getGradle().getTaskGraph().addTaskExecutionGraphListener(new TaskExecutionGraphListener() {
@Override
public void graphPopulated(TaskExecutionGraph taskGraph) {
for (Task task : taskGraph.getAllTasks()) {
//找到需要hook的Task名稱
if ((task.name.equalsIgnoreCase(hackTransformTaskName) || task.name.equalsIgnoreCase(hackTransformTaskNameForWrapper))
&& !(((TransformTask) task).getTransform() instanceof MatrixTraceTransform)) {
project.logger.warn("find dex transform. transform class: " + task.transform.getClass() + " . task name: " + task.name)
project.logger.info("variant name: " + variant.name)
Field field = TransformTask.class.getDeclaredField("transform")
field.setAccessible(true)
//反射替換成MatrixTraceTransform,並將原transform傳入,最後執行原transform邏輯
field.set(task, new MatrixTraceTransform(project, variant, task.transform))
project.logger.warn("transform class after hook: " + task.transform.getClass())
break
}
}
}
})
}
複製程式碼
MatrixTraceTransform
主要邏輯在transform
方法中
@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
long start = System.currentTimeMillis()
//是否增量編譯
final boolean isIncremental = transformInvocation.isIncremental() && this.isIncremental()
//transform的結果,重定向輸出到這個目錄
final File rootOutput = new File(project.matrix.output, "classes/${getName()}/")
if (!rootOutput.exists()) {
rootOutput.mkdirs()
}
final TraceBuildConfig traceConfig = initConfig()
Log.i("Matrix." + getName(), "[transform] isIncremental:%s rootOutput:%s", isIncremental, rootOutput.getAbsolutePath())
//獲取Class混淆的mapping資訊,儲存到mappingCollector中
final MappingCollector mappingCollector = new MappingCollector()
File mappingFile = new File(traceConfig.getMappingPath());
if (mappingFile.exists() && mappingFile.isFile()) {
MappingReader mappingReader = new MappingReader(mappingFile);
mappingReader.read(mappingCollector)
}
Map<File, File> jarInputMap = new HashMap<>()
Map<File, File> scrInputMap = new HashMap<>()
transformInvocation.inputs.each { TransformInput input ->
input.directoryInputs.each { DirectoryInput dirInput ->
//收集、重定向目錄中的class
collectAndIdentifyDir(scrInputMap, dirInput, rootOutput, isIncremental)
}
input.jarInputs.each { JarInput jarInput ->
if (jarInput.getStatus() != Status.REMOVED) {
//收集、重定向jar包中的class
collectAndIdentifyJar(jarInputMap, scrInputMap, jarInput, rootOutput, isIncremental)
}
}
}
//收集需要插樁的方法資訊,每個插樁資訊封裝成TraceMethod物件
MethodCollector methodCollector = new MethodCollector(traceConfig, mappingCollector)
HashMap<String, TraceMethod> collectedMethodMap = methodCollector.collect(scrInputMap.keySet().toList(), jarInputMap.keySet().toList())
//執行插樁邏輯,在需要插樁方法的入口、出口新增MethodBeat的i/o邏輯
MethodTracer methodTracer = new MethodTracer(traceConfig, collectedMethodMap, methodCollector.getCollectedClassExtendMap())
methodTracer.trace(scrInputMap, jarInputMap)
//執行原transform的邏輯;預設transformClassesWithDexBuilderForDebug這個task會將Class轉換成Dex
origTransform.transform(transformInvocation)
Log.i("Matrix." + getName(), "[transform] cost time: %dms", System.currentTimeMillis() - start)
}
複製程式碼
收集Dir中的Class資訊
private void collectAndIdentifyDir(Map<File, File> dirInputMap, DirectoryInput input, File rootOutput, boolean isIncremental) {
final File dirInput = input.file
final File dirOutput = new File(rootOutput, input.file.getName())
if (!dirOutput.exists()) {
dirOutput.mkdirs()
}
//增量編譯
if (isIncremental) {
if (!dirInput.exists()) {
dirOutput.deleteDir()
} else {
final Map<File, Status> obfuscatedChangedFiles = new HashMap<>()
final String rootInputFullPath = dirInput.getAbsolutePath()
final String rootOutputFullPath = dirOutput.getAbsolutePath()
input.changedFiles.each { Map.Entry<File, Status> entry ->
final File changedFileInput = entry.getKey()
final String changedFileInputFullPath = changedFileInput.getAbsolutePath()
//增量編譯模式下之前的build輸出已經重定向到dirOutput;替換成output的目錄
final File changedFileOutput = new File(
changedFileInputFullPath.replace(rootInputFullPath, rootOutputFullPath)
)
final Status status = entry.getValue()
switch (status) {
case Status.NOTCHANGED:
break
case Status.ADDED:
case Status.CHANGED:
//新增、修改的Class檔案,此次需要掃描
dirInputMap.put(changedFileInput, changedFileOutput)
break
case Status.REMOVED:
//刪除的Class檔案,將檔案直接刪除
changedFileOutput.delete()
break
}
obfuscatedChangedFiles.put(changedFileOutput, status)
}
replaceChangedFile(input, obfuscatedChangedFiles)
}
} else {
//全量編譯模式下,所有的Class檔案都需要掃描
dirInputMap.put(dirInput, dirOutput)
}
//反射input,將dirOutput設定為其輸出目錄
replaceFile(input, dirOutput)
}
複製程式碼
反射替換輸出目錄的程式碼:
protected void replaceFile(QualifiedContent input, File newFile) {
final Field fileField = ReflectUtil.getDeclaredFieldRecursive(input.getClass(), 'file')
fileField.set(input, newFile
}
複製程式碼
類似的,收集Jar中的Class資訊
private void collectAndIdentifyJar(Map<File, File> jarInputMaps, Map<File, File> dirInputMaps, JarInput input, File rootOutput, boolean isIncremental) {
final File jarInput = input.file
final File jarOutput = new File(rootOutput, getUniqueJarName(jarInput))
if (IOUtil.isRealZipOrJar(jarInput)) {
switch (input.status) {
case Status.NOTCHANGED:
if (isIncremental) {
break
}
case Status.ADDED:
case Status.CHANGED:
jarInputMaps.put(jarInput, jarOutput)
break
case Status.REMOVED:
break
}
} else {
...
//這部分程式碼可忽略,微信AutoDex自定義的檔案結構
}
replaceFile(input, jarOutput)
}
複製程式碼
第一次遍歷Class,收集待插樁method
總體流程都在collect
方法中
public HashMap collect(List<File> srcFolderList, List<File> dependencyJarList) {
mTraceConfig.parseBlackFile(mMappingCollector);
//獲取base模組已經收集到的待插樁方法
File originMethodMapFile = new File(mTraceConfig.getBaseMethodMap());
getMethodFromBaseMethod(originMethodMapFile);
Log.i(TAG, "[collect] %s method from %s", mCollectedMethodMap.size(), mTraceConfig.getBaseMethodMap());
//轉換為混淆後的方法名
retraceMethodMap(mMappingCollector, mCollectedMethodMap);
//僅收集目錄、jar包中的class資訊
collectMethodFromSrc(srcFolderList, true);
collectMethodFromJar(dependencyJarList, true);
//收集目錄、jar包中的method資訊
collectMethodFromSrc(srcFolderList, false);
collectMethodFromJar(dependencyJarList, false);
Log.i(TAG, "[collect] incrementCount:%s ignoreMethodCount:%s", mIncrementCount, mIgnoreCount);
//儲存待插樁的方法資訊到檔案
saveCollectedMethod(mMappingCollector);
//儲存不需要插樁的方法資訊到檔案(包括黑名單中的方法)
saveIgnoreCollectedMethod(mMappingCollector);
//返回待插樁的方法集合
return mCollectedMethodMap;
}
複製程式碼
收集method資訊的邏輯類似,以下面程式碼為例(位元組碼相關操作使用了ASM)
private void innerCollectMethodFromSrc(File srcFile, boolean isSingle) {
ArrayList<File> classFileList = new ArrayList<>();
if (srcFile.isDirectory()) {
listClassFiles(classFileList, srcFile);
} else {
classFileList.add(srcFile);
}
for (File classFile : classFileList) {
InputStream is = null;
try {
is = new FileInputStream(classFile);
ClassReader classReader = new ClassReader(is);
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor visitor;
if (isSingle) {
//僅收集Class資訊
visitor = new SingleTraceClassAdapter(Opcodes.ASM5, classWriter);
} else {
//收集Method資訊
visitor = new TraceClassAdapter(Opcodes.ASM5, classWriter);
}
classReader.accept(visitor, 0);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
is.close();
} catch (Exception e) {
// ignore
}
}
}
}
複製程式碼
個人感覺SingleTraceClassAdapter
好像是多餘的,一個TraceClassAdapter
可以搞定收集Class、Method的資訊
private class TraceClassAdapter extends ClassVisitor {
private String className;
private boolean isABSClass = false;
private boolean hasWindowFocusMethod = false;
TraceClassAdapter(int i, ClassVisitor classVisitor) {
super(i, classVisitor);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
this.className = name;
if ((access & Opcodes.ACC_ABSTRACT) > 0 || (access & Opcodes.ACC_INTERFACE) > 0) {
this.isABSClass = true;
}
//儲存一個 類->父類 的map(用於查詢Activity的子類)
mCollectedClassExtendMap.put(className, superName);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
if (isABSClass) {
return super.visitMethod(access, name, desc, signature, exceptions);
} else {
if (!hasWindowFocusMethod) {
//該方法是否與onWindowFocusChange方法的簽名一致(該類中是否複寫了onWindowFocusChange方法,Activity不用考慮Class混淆)
hasWindowFocusMethod = mTraceConfig.isWindowFocusChangeMethod(name, desc);
}
//CollectMethodNode中執行method收集操作
return new CollectMethodNode(className, access, name, desc, signature, exceptions);
}
}
@Override
public void visitEnd() {
super.visitEnd();
// collect Activity#onWindowFocusChange
//onWindowFocusChange方法統一給一個-1的方法id
TraceMethod traceMethod = TraceMethod.create(-1, Opcodes.ACC_PUBLIC, className,
TraceBuildConstants.MATRIX_TRACE_ON_WINDOW_FOCUS_METHOD, TraceBuildConstants.MATRIX_TRACE_ON_WINDOW_FOCUS_METHOD_ARGS);
//沒有過複寫onWindowFocusChange,後續會在該類中插入一個onWindowFocusChange方法,此處先記錄一下這個會被插樁的方法
if (!hasWindowFocusMethod && mTraceConfig.isActivityOrSubClass(className, mCollectedClassExtendMap) && mTraceConfig.isNeedTrace(traceMethod.className, mMappingCollector)) {
mCollectedMethodMap.put(traceMethod.getMethodName(), traceMethod);
}
}
}
複製程式碼
如果子類Activity複寫了onWindowFocusChange方法,其對應的methodId就不為-1了;這塊邏輯感覺有點問題~~
private class CollectMethodNode extends MethodNode {
private String className;
private boolean isConstructor;
CollectMethodNode(String className, int access, String name, String desc,
String signature, String[] exceptions) {
super(Opcodes.ASM5, access, name, desc, signature, exceptions);
this.className = className;
}
@Override
public void visitEnd() {
super.visitEnd();
TraceMethod traceMethod = TraceMethod.create(0, access, className, name, desc);
if ("<init>".equals(name) /*|| "<clinit>".equals(name)*/) {
isConstructor = true;
}
// filter simple methods
//忽略空方法、get/set方法、沒有區域性變數的簡單方法,
if ((isEmptyMethod() || isGetSetMethod() || isSingleMethod())
&& mTraceConfig.isNeedTrace(traceMethod.className, mMappingCollector)) {
mIgnoreCount++;
mCollectedIgnoreMethodMap.put(traceMethod.getMethodName(), traceMethod);
return;
}
//不在黑名單中的方法加入待插樁的集合;在黑名單中的方法加入ignore插樁的集合
if (mTraceConfig.isNeedTrace(traceMethod.className, mMappingCollector) && !mCollectedMethodMap.containsKey(traceMethod.getMethodName())) {
traceMethod.id = mMethodId.incrementAndGet();
mCollectedMethodMap.put(traceMethod.getMethodName(), traceMethod);
mIncrementCount++;
} else if (!mTraceConfig.isNeedTrace(traceMethod.className, mMappingCollector)
&& !mCollectedBlackMethodMap.containsKey(traceMethod.className)) {
mIgnoreCount++;
mCollectedBlackMethodMap.put(traceMethod.getMethodName(), traceMethod);
}
}
}
複製程式碼
第二次遍歷Class,執行method插樁邏輯
入口是MethodTracer
的trace
方法
public void trace(Map<File, File> srcFolderList, Map<File, File> dependencyJarList) {
traceMethodFromSrc(srcFolderList);
traceMethodFromJar(dependencyJarList);
}
複製程式碼
分別對目錄、jar包插樁
private void innerTraceMethodFromSrc(File input, File output) {
...
if (mTraceConfig.isNeedTraceClass(classFile.getName())) {
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);
...
}
private void innerTraceMethodFromJar(File input, File output) {
...
if (mTraceConfig.isNeedTraceClass(zipEntryName)) {
InputStream inputStream = zipFile.getInputStream(zipEntry);
ClassReader classReader = new ClassReader(inputStream);
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor classVisitor = new TraceClassAdapter(Opcodes.ASM5, classWriter);
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
byte[] data = classWriter.toByteArray();
InputStream byteArrayInputStream = new ByteArrayInputStream(data);
ZipEntry newZipEntry = new ZipEntry(zipEntryName);
FileUtil.addZipEntry(zipOutputStream, newZipEntry, byteArrayInputStream);
...
}
複製程式碼
核心邏輯在TraceClassAdapter
中
private class TraceClassAdapter extends ClassVisitor {
private String className;
private boolean isABSClass = false;
private boolean isMethodBeatClass = false;
private boolean hasWindowFocusMethod = false;
TraceClassAdapter(int i, ClassVisitor classVisitor) {
super(i, classVisitor);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
this.className = name;
//是否是抽象類、介面
if ((access & Opcodes.ACC_ABSTRACT) > 0 || (access & Opcodes.ACC_INTERFACE) > 0) {
this.isABSClass = true;
}
//是否是MethodBeat類
if (mTraceConfig.isMethodBeatClass(className, mCollectedClassExtendMap)) {
isMethodBeatClass = true;
}
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
//抽象類、介面不插樁
if (isABSClass) {
return super.visitMethod(access, name, desc, signature, exceptions);
} else {
if (!hasWindowFocusMethod) {
//是否是onWindowFocusChange方法
hasWindowFocusMethod = mTraceConfig.isWindowFocusChangeMethod(name, desc);
}
MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);
return new TraceMethodAdapter(api, methodVisitor, access, name, desc, this.className,
hasWindowFocusMethod, isMethodBeatClass);
}
}
@Override
public void visitEnd() {
TraceMethod traceMethod = TraceMethod.create(-1, Opcodes.ACC_PUBLIC, className,
TraceBuildConstants.MATRIX_TRACE_ON_WINDOW_FOCUS_METHOD, TraceBuildConstants.MATRIX_TRACE_ON_WINDOW_FOCUS_METHOD_ARGS);
//如果Activity的子類沒有onWindowFocusChange方法,插入一個onWindowFocusChange方法
if (!hasWindowFocusMethod && mTraceConfig.isActivityOrSubClass(className, mCollectedClassExtendMap)
&& mCollectedMethodMap.containsKey(traceMethod.getMethodName())) {
insertWindowFocusChangeMethod(cv);
}
super.visitEnd();
}
}
複製程式碼
在待插樁方法的入口、出口新增對應邏輯
rivate class TraceMethodAdapter extends AdviceAdapter {
private final String methodName;
private final String name;
private final String className;
private final boolean hasWindowFocusMethod;
private final boolean isMethodBeatClass;
protected TraceMethodAdapter(int api, MethodVisitor mv, int access, String name, String desc, String className,
boolean hasWindowFocusMethod, boolean isMethodBeatClass) {
super(api, mv, access, name, desc);
TraceMethod traceMethod = TraceMethod.create(0, access, className, name, desc);
this.methodName = traceMethod.getMethodName();
this.isMethodBeatClass = isMethodBeatClass;
this.hasWindowFocusMethod = hasWindowFocusMethod;
this.className = className;
this.name = name;
}
@Override
protected void onMethodEnter() {
TraceMethod traceMethod = mCollectedMethodMap.get(methodName);
if (traceMethod != null) {
//函式入口處新增邏輯;
//沒有單獨處理onWindowFocusChange,對於已經複寫onWindowFocusChange的Activity子類,會有問題?
traceMethodCount.incrementAndGet();
mv.visitLdcInsn(traceMethod.id);
mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "i", "(I)V", false);
}
}
@Override
protected void onMethodExit(int opcode) {
TraceMethod traceMethod = mCollectedMethodMap.get(methodName);
if (traceMethod != null) {
if (hasWindowFocusMethod && mTraceConfig.isActivityOrSubClass(className, mCollectedClassExtendMap)
&& mCollectedMethodMap.containsKey(traceMethod.getMethodName())) {
TraceMethod windowFocusChangeMethod = TraceMethod.create(-1, Opcodes.ACC_PUBLIC, className,
TraceBuildConstants.MATRIX_TRACE_ON_WINDOW_FOCUS_METHOD, TraceBuildConstants.MATRIX_TRACE_ON_WINDOW_FOCUS_METHOD_ARGS);
if (windowFocusChangeMethod.equals(traceMethod)) {
//onWindowFocusChange方法統一新增method id = -1的邏輯
traceWindowFocusChangeMethod(mv);
}
}
//函式出口處新增邏輯
traceMethodCount.incrementAndGet();
mv.visitLdcInsn(traceMethod.id);
mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "o", "(I)V", false);
}
}
}
複製程式碼
對於沒有復現onWindowFocusChange方法的Activity子類,插入一個onWindowFocusChange方法
private void insertWindowFocusChangeMethod(ClassVisitor cv) {
MethodVisitor methodVisitor = cv.visitMethod(Opcodes.ACC_PUBLIC, TraceBuildConstants.MATRIX_TRACE_ON_WINDOW_FOCUS_METHOD,
TraceBuildConstants.MATRIX_TRACE_ON_WINDOW_FOCUS_METHOD_ARGS, null, null);
methodVisitor.visitCode();
methodVisitor.visitVarInsn(Opcodes.ALOAD, 0);
methodVisitor.visitVarInsn(Opcodes.ILOAD, 1);
methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, TraceBuildConstants.MATRIX_TRACE_ACTIVITY_CLASS, TraceBuildConstants.MATRIX_TRACE_ON_WINDOW_FOCUS_METHOD,
TraceBuildConstants.MATRIX_TRACE_ON_WINDOW_FOCUS_METHOD_ARGS, false);
traceWindowFocusChangeMethod(methodVisitor);
methodVisitor.visitInsn(Opcodes.RETURN);
methodVisitor.visitMaxs(2, 2);
methodVisitor.visitEnd();
}
複製程式碼
至此,編譯期插樁的邏輯就結束了;在執行期,檢測到某個方法異常時,會上報一個method id,後端通過下圖的method id到method name的對映關係,追查到有問題的方法
慢函式檢測
目的:檢測影響主執行緒執行的慢函式。
上文講述了在編譯期,會對每個方法的執行體前後新增上MethodBeat.i(int methodId)
和MethodBeat.o(int methodId)
的方法呼叫,且methodId是在編譯期生成的,在執行時是一個寫死的常量。通過編譯期的這個操作,就能感知到具體每個方法的進入、退出動作。下面來看下這兩個方法的內部實現
/**
* hook method when it's called in.
*
* @param methodId
*/
public static void i(int methodId) {
if (isBackground) {
return;
}
...
isRealTrace = true;
if (isCreated && Thread.currentThread() == sMainThread) {
...
} else if (!isCreated && Thread.currentThread() == sMainThread && sBuffer != null) {
..
}
}
/**
* hook method when it's called out.
*
* @param methodId
*/
public static void o(int methodId) {
if (isBackground || null == sBuffer) {
return;
}
if (isCreated && Thread.currentThread() == sMainThread) {
...
} else if (!isCreated && Thread.currentThread() == sMainThread) {
...
}
}
複製程式碼
統計了當應用處於前臺時,在主執行緒執行的方法的進入、退出。這些資訊最後儲存在MethodBeat
的Buffer
中。當主執行緒有疑似慢函式存在時,讀取Buffer
的資料,分析可能的慢函式,並上報json資料到後端(後端將methodId轉換為具體的方法宣告)。
疑似發生慢函式的實際有兩個:一個是掉幀的場景,一個是類似ANR這樣長時間主執行緒阻塞UI繪製的場景。
- 掉幀的場景
內部FrameBeat
類實現了Choreographer.FrameCallback
,可以感知每一幀的繪製時間。通過前後兩幀的時間差判斷是否有慢函式發生。
@Override
public void doFrame(long lastFrameNanos, long frameNanos) {
if (isIgnoreFrame) {
mActivityCreatedInfoMap.clear();
setIgnoreFrame(false);
getMethodBeat().resetIndex();
return;
}
int index = getMethodBeat().getCurIndex();
//判斷是否有慢函式
if (hasEntered && frameNanos - lastFrameNanos > mTraceConfig.getEvilThresholdNano()) {
MatrixLog.e(TAG, "[doFrame] dropped frame too much! lastIndex:%s index:%s", 0, index);
handleBuffer(Type.NORMAL, 0, index - 1, getMethodBeat().getBuffer(), (frameNanos - lastFrameNanos) / Constants.TIME_MILLIS_TO_NANO);
}
getMethodBeat().resetIndex();
mLazyScheduler.cancel();
mLazyScheduler.setUp(this, false);
}
複製程式碼
- 主執行緒長時間阻塞UI繪製的場景
LazyScheduler
內有一個HandlerThread,呼叫LazyScheduler.setup
方法會向這個HandlerThread的MQ傳送一個延時5s的訊息。若沒有發生類似ANR的場景,在每一幀的doFrame
回撥中取消這個訊息,同時傳送一個新的延時5s的訊息(正常情況下訊息是得不到執行的);若發生類似ANR的情況,doFrame
沒有被回撥,這個延時5s的訊息得到執行,將回撥到onTimeExpire
方法
@Override
public void onTimeExpire() {
// maybe ANR
if (isBackground()) {
MatrixLog.w(TAG, "[onTimeExpire] pass this time, on Background!");
return;
}
long happenedAnrTime = getMethodBeat().getCurrentDiffTime();
MatrixLog.w(TAG, "[onTimeExpire] maybe ANR!");
setIgnoreFrame(true);
getMethodBeat().lockBuffer(false);
//有慢函式
handleBuffer(Type.ANR, 0, getMethodBeat().getCurIndex() - 1, getMethodBeat().getBuffer(), null, Constants.DEFAULT_ANR, happenedAnrTime, -1);
}
複製程式碼
當檢測到慢函式時,會在後臺執行緒完成慢函式的分析
private final class AnalyseTask implements Runnable {
private final long[] buffer;
private final AnalyseExtraInfo analyseExtraInfo;
private AnalyseTask(long[] buffer, AnalyseExtraInfo analyseExtraInfo) {
this.buffer = buffer;
this.analyseExtraInfo = analyseExtraInfo;
}
private long getTime(long trueId) {
return trueId & 0x7FFFFFFFFFFL;
}
private int getMethodId(long trueId) {
return (int) ((trueId >> 43) & 0xFFFFFL);
}
private boolean isIn(long trueId) {
return ((trueId >> 63) & 0x1) == 1;
}
@Override
public void run() {
analyse(buffer);
}
private void analyse(long[] buffer) {
...
//分析邏輯主要是找出最耗時的方法,可自行閱讀
}
複製程式碼
FPS檢測
目的:檢測繪製過程中的FPS數量。
獲取DectorView的ViewTreeObserver,感知UI繪製的開始
private void addDrawListener(final Activity activity) {
activity.getWindow().getDecorView().post(new Runnable() {
@Override
public void run() {
activity.getWindow().getDecorView().getViewTreeObserver().removeOnDrawListener(FPSTracer.this);
activity.getWindow().getDecorView().getViewTreeObserver().addOnDrawListener(FPSTracer.this);
}
});
}
@Override
public void onDraw() {
isDrawing = true;
}
複製程式碼
通過Choreographer.FrameCallback
,感知UI繪製的結束
@Override
public void doFrame(long lastFrameNanos, long frameNanos) {
if (!isInvalid && isDrawing && isEnterAnimationComplete() && mTraceConfig.isTargetScene(getScene())) {
handleDoFrame(lastFrameNanos, frameNanos, getScene());
}
isDrawing = false;
}
複製程式碼
理論上使用者更關心的是繪製過程中FPS過低導致的卡頓(UI靜止的情況下,使用者是感知不到FPS低的)
在doFrame
方法中,記錄每一幀的資料,其中scene
這個欄位標識一個頁面
@Override
public void onChange(final Activity activity, final Fragment fragment) {
this.mScene = TraceConfig.getSceneForString(activity, fragment);
}
複製程式碼
onChange
的預設實現是通過Application的ActivityLifecycleCallbacks
回撥感知Activity的變化
@Override
public void onActivityResumed(final Activity activity) {
...
if (!activityHash.equals(mCurActivityHash)) {
for (IObserver listener : mObservers) {
listener.onChange(activity, null);
}
mCurActivityHash = activityHash;
}
...
}
複製程式碼
FPS資料預設是2分鐘分析一次(前臺情況下),切後臺時後臺輪詢執行緒停止。
/**
* report FPS
*/
private void doReport() {
...
//資料分析邏輯可行閱讀
}
複製程式碼
卡頓檢測
目的:檢測UI繪製過程中的卡頓情況。
卡頓檢測與FPS檢測類似,在每一幀的`doFrame回撥中判斷是否有卡頓發生,如有卡頓將資料傳送到後臺分析執行緒處理。
@Override
public void doFrame(final long lastFrameNanos, final long frameNanos) {
if (!isDrawing) {
return;
}
isDrawing = false;
final int droppedCount = (int) ((frameNanos - lastFrameNanos) / REFRESH_RATE_MS);
if (droppedCount > 1) {
for (final IDoFrameListener listener : mDoFrameListenerList) {
listener.doFrameSync(lastFrameNanos, frameNanos, getScene(), droppedCount);
if (null != listener.getHandler()) {
listener.getHandler().post(new Runnable() {
@Override
public void run() {
listener.getHandler().post(new AsyncDoFrameTask(listener,
lastFrameNanos, frameNanos, getScene(), droppedCount));
}
});
}
}
複製程式碼
啟動檢測
目的:檢測啟動階段耗時
應用啟動時,會直接對ActivityThread類hook
public class Hacker {
private static final String TAG = "Matrix.Hacker";
public static boolean isEnterAnimationComplete = false;
public static long sApplicationCreateBeginTime = 0L;
public static int sApplicationCreateBeginMethodIndex = 0;
public static long sApplicationCreateEndTime = 0L;
public static int sApplicationCreateEndMethodIndex = 0;
public static int sApplicationCreateScene = -100;
public static void hackSysHandlerCallback() {
try {
//這個類被載入的時間,認為是整個App的啟動開始時間
sApplicationCreateBeginTime = System.currentTimeMillis();
sApplicationCreateBeginMethodIndex = MethodBeat.getCurIndex();
Class<?> forName = Class.forName("android.app.ActivityThread");
Field field = forName.getDeclaredField("sCurrentActivityThread");
field.setAccessible(true);
Object activityThreadValue = field.get(forName);
Field mH = forName.getDeclaredField("mH");
mH.setAccessible(true);
Object handler = mH.get(activityThreadValue);
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);
MatrixLog.i(TAG, "hook system handler completed. start:%s", sApplicationCreateBeginTime);
} catch (Exception e) {
MatrixLog.e(TAG, "hook system handler err! %s", e.getCause().toString());
}
}
}
複製程式碼
代理原有的Handler.Callback,感知Application onCreate的結束時間
public class HackCallback implements Handler.Callback {
private static final String TAG = "Matrix.HackCallback";
private static final int LAUNCH_ACTIVITY = 100;
private static final int ENTER_ANIMATION_COMPLETE = 149;
private static final int CREATE_SERVICE = 114;
private static final int RECEIVER = 113;
private static boolean isCreated = false;
private final Handler.Callback mOriginalCallback;
public HackCallback(Handler.Callback callback) {
this.mOriginalCallback = callback;
}
@Override
public boolean handleMessage(Message msg) {
// MatrixLog.i(TAG, "[handleMessage] msg.what:%s begin:%s", msg.what, System.currentTimeMillis());
if (msg.what == LAUNCH_ACTIVITY) {
Hacker.isEnterAnimationComplete = false;
} else if (msg.what == ENTER_ANIMATION_COMPLETE) {
Hacker.isEnterAnimationComplete = true;
}
if (!isCreated) {
if (msg.what == LAUNCH_ACTIVITY || msg.what == CREATE_SERVICE || msg.what == RECEIVER) {
//傳送啟動Activity等訊息,認為是Application onCreate的結束時間
Hacker.sApplicationCreateEndTime = System.currentTimeMillis();
Hacker.sApplicationCreateEndMethodIndex = MethodBeat.getCurIndex();
Hacker.sApplicationCreateScene = msg.what;
isCreated = true;
}
}
if (null == mOriginalCallback) {
return false;
}
return mOriginalCallback.handleMessage(msg);
}
}
複製程式碼
記錄第一個Activity的onCreate時間
@Override
public void onActivityCreated(Activity activity) {
super.onActivityCreated(activity);
if (isFirstActivityCreate && mFirstActivityMap.isEmpty()) {
String activityName = activity.getComponentName().getClassName();
mFirstActivityIndex = getMethodBeat().getCurIndex();
mFirstActivityName = activityName;
mFirstActivityMap.put(activityName, System.currentTimeMillis());
MatrixLog.i(TAG, "[onActivityCreated] first activity:%s index:%s", mFirstActivityName, mFirstActivityIndex);
getMethodBeat().lockBuffer(true);
}
}
複製程式碼
記錄Activity獲取焦點的時間(在編譯期,在Activity子類的onWindowFocusChange方法中插入MethodBeat.at
方法)
public static void at(Activity activity, boolean isFocus) {
MatrixLog.i(TAG, "[AT] activity: %s, isCreated: %b sListener size: %d,isFocus: %b",
activity.getClass().getSimpleName(), isCreated, sListeners.size(), isFocus);
if (isCreated && Thread.currentThread() == sMainThread) {
for (IMethodBeatListener listener : sListeners) {
listener.onActivityEntered(activity, isFocus, sIndex - 1, sBuffer);
}
}
}
複製程式碼
當Activity獲取到焦點時,認為啟動階段結束(若有SplashActivity,則記錄下一個Activity獲取焦點的時間)
@Override
public void onActivityEntered(Activity activity, boolean isFocus, int nowIndex, long[] buffer) {
...啟動資料分析
}
複製程式碼
總結
Matrix Trace檢測巧妙的利用了編譯期位元組碼插樁技術,優化了移動端的FPS、卡頓、啟動的檢測手段;藉助Matrix Trace,開發人員可以從方法級別來做優化。