An overview of the
Recently, I have been studying the Matrix framework and found many related blogs. I found that most of them only described the general framework process, but the details were not in-depth enough, or they were based on the earlier version, so I decided to compare the latest version with one article by myself. Students who are not familiar with Matrix Trace Canary are advised to have a look at the official document first, which is helpful for understanding this article. Without further ado, let’s begin.
Framework for parsing
Trace is based on ASM bytecode petting, which modiates the function bytecode in all class files at compile time, before and after the function is executed.
Custom MatrixTraceTransform
Transform edits the class bytecode before the class file is packaged into a dex file. Pile insertion part is not the focus of this article, there are many related articles on the Internet, do not overintroduce. Take a look at the key code
@Override
protected void onMethodEnter() {
TraceMethod traceMethod = collectedMethodMap.get(methodName);
if (traceMethod != null) {
traceMethodCount.incrementAndGet();
mv.visitLdcInsn(traceMethod.id);
mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "i", "(I)V", false);
}
}
Copy the code
Insert the AppMethodBeat.i() method before the execution of the function that requires the peg
@Override protected void onMethodExit(int opcode) { TraceMethod traceMethod = collectedMethodMap.get(methodName); if (traceMethod ! = null) { if (hasWindowFocusMethod && isActivityOrSubClass && isNeedTrace) { 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)) { traceWindowFocusChangeMethod(mv, className); } } traceMethodCount.incrementAndGet(); mv.visitLdcInsn(traceMethod.id); mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "o", "(I)V", false); }}}Copy the code
Insert the AppMethodBeat.o() method after the execution of the function that requires staking
private void traceWindowFocusChangeMethod(MethodVisitor mv, String classname) { mv.visitVarInsn(Opcodes.ALOAD, 0); mv.visitVarInsn(Opcodes.ILOAD, 1); mv.visitMethodInsn(Opcodes.INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "at", "(Landroid/app/Activity; Z)V", false); } private void insertWindowFocusChangeMethod(ClassVisitor cv, String classname) { 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, classname); methodVisitor.visitInsn(Opcodes.RETURN); methodVisitor.visitMaxs(2, 2); methodVisitor.visitEnd(); }Copy the code
Insert the AppMethodBeat.at() method in the Activity’s onWindowFocusChanged function
TracePlugin
The start () method of the TracePlugin is the starting location of the entire project
@Override public void start() { super.start(); if (! isSupported()) { MatrixLog.w(TAG, "[start] Plugin is unSupported!" ); return; } MatrixLog.w(TAG, "start!" ); Runnable runnable = new Runnable() { @Override public void run() { if (! UIThreadMonitor.getMonitor().isInit()) { try { UIThreadMonitor.getMonitor().init(traceConfig); } catch (java.lang.RuntimeException e) { MatrixLog.e(TAG, "[start] RuntimeException:%s", e); return; } } AppMethodBeat.getInstance().onStart(); UIThreadMonitor.getMonitor().onStart(); anrTracer.onStartTrace(); frameTracer.onStartTrace(); evilMethodTracer.onStartTrace(); startupTracer.onStartTrace(); }}; if (Thread.currentThread() == Looper.getMainLooper().getThread()) { runnable.run(); } else { MatrixLog.w(TAG, "start TracePlugin in Thread[%s] but not in mainThread!" , Thread.currentThread().getId()); MatrixHandlerThread.getDefaultMainHandler().post(runnable); }}Copy the code
AppMethodBeat
The logic for hook functions is in AppMethodBeat
AppMethodBeat.i()/AppMethodBeat.o()
The previous piling function starts with I () and ends with o();
public static void i(int methodId) { if (status <= STATUS_STOPPED) { return; } if (methodId >= METHOD_ID_MAX) { //1 return; } if (status == STATUS_DEFAULT) { synchronized (statusLock) { if (status == STATUS_DEFAULT) { realExecute(); //2 status = STATUS_READY; } } } long threadId = Thread.currentThread().getId(); if (sMethodEnterListener ! = null) { sMethodEnterListener.enter(methodId, threadId); } if (threadId == sMainThreadId) { if (assertIn) { android.util.Log.e(TAG, "ERROR!!! AppMethodBeat.i Recursive calls!!!" ); return; } assertIn = true; if (sIndex < Constants.BUFFER_SIZE) { mergeData(methodId, sIndex, true); //3 } else { sIndex = 0; mergeData(methodId, sIndex, true); } ++sIndex; assertIn = false; }}Copy the code
MethodId was verified in Note 1. MethodId has the following two types
- AppMethodBeat.i(AppMethodBeat.METHOD_ID_DISPATCH); Buy a pit and explain later
- traceMethod.id = methodId.incrementAndGet(); Autoincrement of AtomicInteger during piling
The determination of the state in comment 2 is mainly based on the realExecute() method executed
private static void realExecute() {
MatrixLog.i(TAG, "[realExecute] timestamp:%s", System.currentTimeMillis());
sCurrentDiffTime = SystemClock.uptimeMillis() - sDiffTime;
sHandler.removeCallbacksAndMessages(null);
//2.1 sHandler.postDelayed(sUpdateDiffTimeRunnable, Constants.TIME_UPDATE_CYCLE_MS);
sHandler.postDelayed(checkStartExpiredRunnable = new Runnable() {
@Override
public void run() {
synchronized (statusLock) {
MatrixLog.i(TAG, "[startExpired] timestamp:%s status:%s", System.currentTimeMillis(), status);
if (status == STATUS_DEFAULT || status == STATUS_READY) {
status = STATUS_EXPIRED_START;
}
}
}
}, Constants.DEFAULT_RELEASE_BUFFER_DELAY);
//2.2 ActivityThreadHacker.hackSysHandlerCallback();
LooperMonitor.register(looperMonitorListener);
}
private static Runnable sUpdateDiffTimeRunnable = new Runnable() {
@Override
public void run() {
try {
while (true) {
while (!isPauseUpdateTime && status > STATUS_STOPPED) {
sCurrentDiffTime = SystemClock.uptimeMillis() - sDiffTime;
SystemClock.sleep(Constants.TIME_UPDATE_CYCLE_MS);
}
synchronized (updateTimeLock) {
updateTimeLock.wait();
}
}
} catch (InterruptedException e) {
MatrixLog.e(TAG, "" + e.toString());
}
}
};
Copy the code
SUpdateDiffTimeRunnable updates the sCurrentDiffTime value every 5ms by starting a thread. The official explanation is that each time the peg function obtains time increases the performance cost, after all, it is in the main thread, and the time interval below 5ms can be ignored. SBuffer, sIndex, sBuffer, sIndex, mergeData, sBuffer, sIndex
private static long[] sBuffer = new long[Constants.BUFFER_SIZE];
private static int sIndex = 0;
Copy the code
The running records of a function are stored in sBuffer. SIndexr is the current index record of sBuffe
private static void mergeData(int methodId, int index, boolean isIn) { if (methodId == AppMethodBeat.METHOD_ID_DISPATCH) { sCurrentDiffTime = SystemClock.uptimeMillis() - sDiffTime; } long trueId = 0L; if (isIn) { trueId |= 1L << 63; } trueId |= (long) methodId << 43; trueId |= sCurrentDiffTime & 0x7FFFFFFFFFFL; sBuffer[index] = trueId; checkPileup(index); / / 3.1 sLastIndex = index; } private static void checkPileup(int index) { IndexRecord indexRecord = sIndexRecordHead; while (indexRecord ! = null) { if (indexRecord.index == index || (indexRecord.index == -1 && sLastIndex == Constants.BUFFER_SIZE - 1)) { indexRecord.isValid = false; MatrixLog.w(TAG, "[checkPileup] %s", indexRecord.toString()); sIndexRecordHead = indexRecord = indexRecord.next; } else { break; }}}Copy the code
IndexRecord is a linked list. If the condition is met, the isValid attribute of the header is set first, but the header is deleted.
AppMethodBeat.at()
Appmethodbeat.at () is also a peg function, but it monitors the Activity to start, followed by StartupTracer
OK, now that we have a rough idea of the main data formats, let’s start incorporating the code flow
EvilMethodTracer
EvilMethodTracer focuses on the API first
@Override
public void onAlive() {
super.onAlive();
if (isEvilMethodTraceEnable) {
UIThreadMonitor.getMonitor().addObserver(this);
}
}
public void addObserver(LooperObserver observer) {
if (!isAlive) {
onStart();
}
synchronized (observers) {
observers.add(observer);
}
}
Copy the code
So I’m setting a listener here, and you can see that I’m actually setting a listener in LooperObserver, and I’m following up to see when the implementation function in EvilMethodTracer calls back, Find dispatch(x.char (0) == ‘>’, x) in LooperPrinter; methods
class LooperPrinter implements Printer { public Printer origin; boolean isHasChecked = false; boolean isValid = false; LooperPrinter(Printer printer) { this.origin = printer; } @Override public void println(String x) { if (null ! = origin) { origin.println(x); if (origin == this) { throw new RuntimeException(TAG + " origin == this"); } } if (! isHasChecked) { isValid = x.charAt(0) == '>' || x.charAt(0) == '<'; isHasChecked = true; if (! isValid) { MatrixLog.e(TAG, "[println] Printer is inValid! x:%s", x); } } if (isValid) { dispatch(x.charAt(0) == '>', x); / /}}}Copy the code
ResetPrinter () called when LooperMonitor is initialized
private synchronized void resetPrinter() { Printer originPrinter = null; try { if (! isReflectLoggingError) { originPrinter = ReflectUtils.get(looper.getClass(), "mLogging", looper); if (originPrinter == printer && null ! = printer) { return; } } } catch (Exception e) { isReflectLoggingError = true; Log.e(TAG, "[resetPrinter] %s", e); } if (null ! = printer) { MatrixLog.w(TAG, "maybe thread:%s printer[%s] was replace other[%s]!" , looper.getThread().getName(), printer, originPrinter); } looper.setMessageLogging(printer = new LooperPrinter(originPrinter)); if (null ! = originPrinter) { MatrixLog.i(TAG, "reset printer, originPrinter[%s] in %s", originPrinter, looper.getThread().getName()); }}Copy the code
As you can see, this method gets the mLogging property in Looper by reflection. Let’s look at the code in Looper
for (;;) { Message msg = queue.next(); // might block final Printer logging = me.mLogging; if (logging ! = null) { logging.println(">>>>> Dispatching to " + msg.target + " " + msg.callback + ": " + msg.what); }... // Omit irrelevant code if (logging! = null) { logging.println("<<<<< Finished to " + msg.target + " " + msg.callback); }}Copy the code
It can be found that Looper prints logs when processing the beginning and end of a message. Based on this, LooperMonitor implements whether the main thread timeout occurs when processing a message.
Back to EvilMethodTracer and UIThreadMonitor, their dispatchBegin () is called when a message from the main thread starts dispatch and their dispatchEnd () is called when dispatch ends.
In the UIThreadMonitor
private void dispatchBegin() { token = dispatchTimeMs[0] = SystemClock.uptimeMillis(); dispatchTimeMs[2] = SystemClock.currentThreadTimeMillis(); AppMethodBeat.i(AppMethodBeat.METHOD_ID_DISPATCH); //1 synchronized (observers) { for (LooperObserver observer : observers) { if (! observer.isDispatchBegin()) { observer.dispatchBegin(dispatchTimeMs[0], dispatchTimeMs[2], token); }}}}Copy the code
Methodbeat.METHOD_ID_DISPATCH is used to start and end the main thread’s message. methodbeat. The dispatchEnd () method is similar and will not be introduced
@Override public void dispatchBegin(long beginMs, long cpuBeginMs, long token) { super.dispatchBegin(beginMs, cpuBeginMs, token); indexRecord = AppMethodBeat.getInstance().maskIndex("EvilMethodTracer#dispatchBegin"); } public IndexRecord maskIndex(String source) { if (sIndexRecordHead == null) {//1 sIndexRecordHead = new IndexRecord(sIndex - 1); sIndexRecordHead.source = source; return sIndexRecordHead; } else { IndexRecord indexRecord = new IndexRecord(sIndex - 1); indexRecord.source = source; IndexRecord record = sIndexRecordHead; IndexRecord last = null; while (record ! = null) { if (indexRecord.index <= record.index) { if (null == last) { IndexRecord tmp = sIndexRecordHead; sIndexRecordHead = indexRecord; indexRecord.next = tmp; } else { IndexRecord tmp = last.next; if (null ! = last.next) { last.next = indexRecord; } indexRecord.next = tmp; } return indexRecord; //2 } last = record; record = record.next; } last.next = indexRecord; return indexRecord; / / 3}}Copy the code
The maskIndex method is used to generate an IndexRecord node, which is the operation of adding nodes to a single linked list. The index property of the IndexRecord is determined by the index of the current sBuffer, sIndex, which is equivalent to the index of the peg method in the sBuffer.
- Set to the head node directly at comment 1
- At comment 2, if last==null is inserted before the current sIndexRecordHead, last! =null is inserted in the middle of the sIndexRecordHead, depending on the index size of the node
- Note 3 is added directly to the last node
So if we go back to the Illegitimate MethodTracer where we just said dispatchBegin, we’re definitely going to say dispatchEnd, which is the main subclass as dispatchBegin
@Override public void dispatchEnd(long beginMs, long cpuBeginMs, long endMs, long cpuEndMs, long token, boolean isBelongFrame) { super.dispatchEnd(beginMs, cpuBeginMs, endMs, cpuEndMs, token, isBelongFrame); long start = config.isDevEnv() ? System.currentTimeMillis() : 0; try { long dispatchCost = endMs - beginMs; if (dispatchCost >= evilThresholdMs) { long[] data = AppMethodBeat.getInstance().copyData(indexRecord); long[] queueCosts = new long[3]; System.arraycopy(queueTypeCosts, 0, queueCosts, 0, 3); String scene = AppMethodBeat.getVisibleScene(); MatrixHandlerThread.getDefaultHandler().post(new AnalyseTask(isForeground(), scene, data, queueCosts, cpuEndMs - cpuBeginMs, endMs - beginMs, endMs)); } } finally { indexRecord.release(); if (config.isDevEnv()) { String usage = Utils.calculateCpuUsage(cpuEndMs - cpuBeginMs, endMs - beginMs); MatrixLog.v(TAG, "[dispatchEnd] token:%s cost:%sms cpu:%sms usage:%s innerCost:%s", token, endMs - beginMs, cpuEndMs - cpuBeginMs, usage, System.currentTimeMillis() - start); }}}Copy the code
When dispatchCost exceeds the threshold is a time consuming method, long [] data = AppMethodBeat. GetInstance (). CopyData (indexRecord); Get a record of time-consuming methods in sBuffer
public long[] copyData(IndexRecord startRecord) { return copyData(startRecord, new IndexRecord(sIndex - 1)); } private long[] copyData(IndexRecord startRecord, IndexRecord endRecord) { long current = System.currentTimeMillis(); long[] data = new long[0]; try { if (startRecord.isValid && endRecord.isValid) { int length; int start = Math.max(0, startRecord.index); int end = Math.max(0, endRecord.index); if (end > start) { length = end - start + 1; data = new long[length]; System.arraycopy(sBuffer, start, data, 0, length); } else if (end < start) { length = 1 + end + (sBuffer.length - start); data = new long[length]; System.arraycopy(sBuffer, start, data, 0, sBuffer.length - start); System.arraycopy(sBuffer, 0, data, sBuffer.length - start, end + 1); } return data; } return data; } catch (OutOfMemoryError e) { MatrixLog.e(TAG, e.toString()); return data; } finally { MatrixLog.i(TAG, "[copyData] [%s:%s] length:%s cost:%sms", Math.max(0, startRecord.index), endRecord.index, data.length, System.currentTimeMillis() - current); }}Copy the code
StartRecord is the IndexRecord node generated in dispatchBegin. Index in endRecord corresponds to the current index in sBuffer. This captures all the functions that a message executes during dispatch.
Since the default length of the sBuffer array is 1000000, sIndex will be set to 0 when the maximum value is reached, so end< start occurs, as shown in the following code
if (sIndex < Constants.BUFFER_SIZE) {
mergeData(methodId, sIndex, true);
} else {
sIndex = 0;
mergeData(methodId, sIndex, true);
}
Copy the code
Go back to the dispatchEnd () method
String scene = AppMethodBeat.getVisibleScene(); Scene is actually the current activity
private void updateScene(Activity activity) {
visibleScene = activity.getClass().getName();
}
Copy the code
Finally, analyze and report data in analyse Task ()
TraceDataUtils.structuredDataToStack(data, stack, true, endMs); public static void structuredDataToStack(long[] buffer, LinkedList<MethodItem> result, boolean isStrict, long endTime) { long lastInId = 0L; int depth = 0; LinkedList<Long> rawData = new LinkedList<>(); boolean isBegin = ! isStrict; for (long trueId : buffer) {// 1 if (0 == trueId) { continue; } if (isStrict) { if (isIn(trueId) && AppMethodBeat.METHOD_ID_DISPATCH == getMethodId(trueId)) { isBegin = true; } if (! isBegin) { MatrixLog.d(TAG, "never begin! pass this method[%s]", getMethodId(trueId)); continue; } } if (isIn(trueId)) { lastInId = getMethodId(trueId); if (lastInId == AppMethodBeat.METHOD_ID_DISPATCH) { depth = 0; } depth++; rawData.push(trueId); } else { int outMethodId = getMethodId(trueId); if (! rawData.isEmpty()) { long in = rawData.pop(); depth--; int inMethodId; LinkedList<Long> tmp = new LinkedList<>(); tmp.add(in); while ((inMethodId = getMethodId(in)) ! = outMethodId && ! rawData.isEmpty()) { MatrixLog.w(TAG, "pop inMethodId[%s] to continue match ouMethodId[%s]", inMethodId, outMethodId); in = rawData.pop(); depth--; tmp.add(in); } if (inMethodId ! = outMethodId && inMethodId == AppMethodBeat.METHOD_ID_DISPATCH) { MatrixLog.e(TAG, "inMethodId[%s] ! = outMethodId[%s] throw this outMethodId!" , inMethodId, outMethodId); rawData.addAll(tmp); depth += rawData.size(); continue; } long outTime = getTime(trueId); long inTime = getTime(in); long during = outTime - inTime; if (during < 0) { MatrixLog.e(TAG, "[structuredDataToStack] trace during invalid:%d", during); rawData.clear(); result.clear(); return; } MethodItem methodItem = new MethodItem(outMethodId, (int) during, depth); addMethodItem(result, methodItem); //2 } else { MatrixLog.w(TAG, "[structuredDataToStack] method[%s] not found in! ", outMethodId); } } } while (! rawData.isEmpty() && isStrict) {//3 long trueId = rawData.pop(); int methodId = getMethodId(trueId); boolean isIn = isIn(trueId); long inTime = getTime(trueId) + AppMethodBeat.getDiffTime(); MatrixLog.w(TAG, "[structuredDataToStack] has never out method[%s], isIn:%s, inTime:%s, endTime:%s,rawData size:%s", methodId, isIn, inTime, endTime, rawData.size()); if (! isIn) { MatrixLog.e(TAG, "[structuredDataToStack] why has out Method[%s]? is wrong! ", methodId); continue; } MethodItem methodItem = new MethodItem(methodId, (int) (endTime - inTime), rawData.size()); addMethodItem(result, methodItem); } TreeNode root = new TreeNode(null, null); stackToTree(result, root); //4 result.clear(); treeToStack(root, result); / / 5}Copy the code
Let’s start with the array traversal in comment 1. The main thing here is to convert the methods recorded in the array to a list of MethodItems with a hierarchical attribute depth
Fun1 () {//depth=0 fun2 () {//depth=1 fun3 () {//depth= 2} fun4 () {//depth= 2}}}Copy the code
MethodItemd also has fairly simple properties depth, time, method ID
public MethodItem(int methodId, int durTime, int depth) {
this.methodId = methodId;
this.durTime = durTime;
this.depth = depth;
}
Copy the code
Comment 2 adds the generated methodItem to the head of the list and updates it if there is the same methodId node below
private static int addMethodItem(LinkedList<MethodItem> resultStack, MethodItem item) { if (AppMethodBeat.isDev) { Log.v(TAG, "method:" + item); } MethodItem last = null; if (! resultStack.isEmpty()) { last = resultStack.peek(); } if (null ! = last && last.methodId == item.methodId && last.depth == item.depth && 0 ! = item.depth) { item.durTime = item.durTime == Constants.DEFAULT_ANR ? last.durTime : item.durTime; last.mergeMore(item.durTime); return last.durTime; } else { resultStack.push(item); return item.durTime; }}Copy the code
Note 3 is mainly about the verification operation of abnormal data. Generally, the intercepted sBuffer data I and O functions are one-to-one corresponding.
Note 4 is to transform the linked list data into a tree structure. Note that this is not a binary tree. It can be understood as a multi-level tree data format of provinces and cities
public static final class TreeNode {
MethodItem item;
TreeNode father;
LinkedList<TreeNode> children = new LinkedList<>();
TreeNode(MethodItem item, TreeNode father) {
this.item = item;
this.father = father;
}
private int depth() {
return null == item ? 0 : item.depth;
}
private void add(TreeNode node) {![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e94387b5380b4872adfaa43056a6515c~tplv-k3u1fbpfcp-zoom-1.image)
children.addFirst(node);
}
private boolean isLeaf() {
return children.isEmpty();
}
}
Copy the code
Back to AnalyseTask’s analyse ()
TraceDataUtils.trimStack(stack, Constants.TARGET_EVIL_METHOD_STACK, new TraceDataUtils.IStructuredDataFilter() { @Override public boolean isFilter(long during, int filterCount) { return during < filterCount * Constants.TIME_UPDATE_CYCLE_MS; } @Override public int getFilterMaxCount() { return Constants.FILTER_STACK_MAX_COUNT; } @Override public void fallback(List<MethodItem> stack, int size) { MatrixLog.w(TAG, "[fallback] size:%s targetSize:%s stack:%s", size, Constants.TARGET_EVIL_METHOD_STACK, stack); Iterator iterator = stack.listIterator(Math.min(size, Constants.TARGET_EVIL_METHOD_STACK)); while (iterator.hasNext()) { iterator.next(); iterator.remove(); }}});Copy the code
The focus here is on filtering the collected data
public static void trimStack(List<MethodItem> stack, int targetCount, IStructuredDataFilter filter) { if (0 > targetCount) { stack.clear(); return; } int filterCount = 1; int curStackSize = stack.size(); while (curStackSize > targetCount) {//1 ListIterator<MethodItem> iterator = stack.listIterator(stack.size()); while (iterator.hasPrevious()) { MethodItem item = iterator.previous(); if (filter.isFilter(item.durTime, filterCount)) {//2 iterator.remove(); curStackSize--; if (curStackSize <= targetCount) { return; } } } curStackSize = stack.size(); filterCount++; if (filter.getFilterMaxCount() < filterCount) { break; } } int size = stack.size(); if (size > targetCount) { filter.fallback(stack, size); }}Copy the code
Note 1 sets the threshold value of more than 30 nodes before filtering. Note 2 judges whether filtering is needed for specific time consumption.
Continue to the analyse ()
long stackCost = Math.max(cost, TraceDataUtils.stackToString(stack, reportBuilder, logcatBuilder));
Copy the code
Here to obtain the maximum time, the logic is relatively simple
String stackKey = TraceDataUtils.getTreeKey(stack, stackCost);
Copy the code
Here is a key value required to obtain the time consuming method detection data report. See for details
public static String getTreeKey(List<MethodItem> stack, long stackCost) {
StringBuilder ss = new StringBuilder();
long allLimit = (long) (stackCost * Constants.FILTER_STACK_KEY_ALL_PERCENT);
LinkedList<MethodItem> sortList = new LinkedList<>();
for (MethodItem item : stack) {
if (item.durTime >= allLimit) {
sortList.add(item);
}
}
Collections.sort(sortList, new Comparator<MethodItem>() {
@Override
public int compare(MethodItem o1, MethodItem o2) {
return Integer.compare((o2.depth + 1) * o2.durTime, (o1.depth + 1) * o1.durTime);
}
});
if (sortList.isEmpty() && !stack.isEmpty()) {
MethodItem root = stack.get(0);
sortList.add(root);
} else if (sortList.size() > 1 && sortList.peek().methodId == AppMethodBeat.METHOD_ID_DISPATCH) {
sortList.removeFirst();
}
for (MethodItem item : sortList) {
ss.append(item.methodId + "|");
break;
}
return ss.toString();
}
Copy the code
The logic is not complicated. The data is filtered by durTime first, and then sorted by depth and durTime. Depth is needed because of the call stack hierarchy, not just the time of a single method.
The analyse is followed by the setting and reporting of json data.
So much for EvilMethodTracer logic.
AnrTracer
It’s easy to understand the logic associated with EvilMethodTracer, AnrTracer, because the root cause of ANR is program unresponsiveness due to main thread time
@Override public void dispatchBegin(long beginMs, long cpuBeginMs, long token) { super.dispatchBegin(beginMs, cpuBeginMs, token); anrTask = new AnrHandleTask(AppMethodBeat.getInstance().maskIndex("AnrTracer#dispatchBegin"), token); if (traceConfig.isDevEnv()) { MatrixLog.v(TAG, "* [dispatchBegin] token:%s index:%s", token, anrTask.beginRecord.index); } anrHandler.postDelayed(anrTask, Constants.DEFAULT_ANR - (SystemClock.uptimeMillis() - token)); } @Override public void dispatchEnd(long beginMs, long cpuBeginMs, long endMs, long cpuEndMs, long token, boolean isBelongFrame) { super.dispatchEnd(beginMs, cpuBeginMs, endMs, cpuEndMs, token, isBelongFrame); if (traceConfig.isDevEnv()) { MatrixLog.v(TAG, "[dispatchEnd] token:%s cost:%sms cpu:%sms usage:%s", token, endMs - beginMs, cpuEndMs - cpuBeginMs, Utils.calculateCpuUsage(cpuEndMs - cpuBeginMs, endMs - beginMs)); } if (null ! = anrTask) { anrTask.getBeginRecord().release(); anrHandler.removeCallbacks(anrTask); }}Copy the code
- DispatchBegin delays the sending of a message
- DispatchEnd cancels the delayed message. If it does not cancel, ANR has occurred
- An ANR report occurred. Procedure
StartupTracer
@Override protected void onAlive() { super.onAlive(); MatrixLog.i(TAG, "[onAlive] isStartupEnable:%s", isStartupEnable); if (isStartupEnable) { AppMethodBeat.getInstance().addListener(this); Matrix.with().getApplication().registerActivityLifecycleCallbacks(this); }}Copy the code
There are two listeners registered here
- IAppMethodBeatListener
- ActivityLifecycleCallbacks global listening Activity life cycle
Look at IAppMethodBeatListener and see that his onActivityFocused () method is called from at () in AppMethodBeat, which was inserted at compile time just like the I () o () methods. The insertion location is in the onWindowFocusChange method of the Activity subclass
public static void at(Activity activity, boolean isFocus) {
String activityName = activity.getClass().getName();
if (isFocus) {
if (sFocusActivitySet.add(activityName)) {
synchronized (listeners) {
for (IAppMethodBeatListener listener : listeners) {
listener.onActivityFocused(activityName);
}
}
MatrixLog.i(TAG, "[at] visibleScene[%s] has %s focus!", getVisibleScene(), "attach");
}
} else {
if (sFocusActivitySet.remove(activityName)) {
MatrixLog.i(TAG, "[at] visibleScene[%s] has %s focus!", getVisibleScene(), "detach");
}
}
}
Copy the code
The main callbacks in StartupTracer have been identified, so let’s look at them
@Override public void onActivityFocused(String activity) { if (isColdStartup()) { if (firstScreenCost == 0) { this.firstScreenCost = uptimeMillis() - ActivityThreadHacker.getEggBrokenTime(); } if (hasShowSplashActivity) { coldCost = uptimeMillis() - ActivityThreadHacker.getEggBrokenTime(); } else { if (splashActivities.contains(activity)) { hasShowSplashActivity = true; } else if (splashActivities.isEmpty()) { MatrixLog.i(TAG, "default splash activity[%s]", activity); coldCost = firstScreenCost; } else { MatrixLog.w(TAG, "pass this activity[%s] at duration of start up! splashActivities=%s", activity, splashActivities); } } if (coldCost > 0) { analyse(ActivityThreadHacker.getApplicationCost(), firstScreenCost, coldCost, false); } } else if (isWarmStartUp()) { isWarmStartUp = false; long warmCost = uptimeMillis() - ActivityThreadHacker.getLastLaunchActivityTime(); if (warmCost > 0) { analyse(ActivityThreadHacker.getApplicationCost(), firstScreenCost, warmCost, true); }}}Copy the code
The logic here is not complicated mainly because of the meaning of some variables
- ActivityThreadHacker.getEggBrokenTime(); The first time appMethodBeat.i () is called is the time the program starts
- ActivityThreadHacker.getLastLaunchActivityTime(); Here we listen for msG.what == LAUNCH_ACTIVITY messages by reflecting the mH Handler in the ActivityThread.
The next analyse () method is data analysis and report logic similar to EvilMethodTracer before
private void analyse(long applicationCost, long firstScreenCost, long allCost, boolean isWarmStartUp) { MatrixLog.i(TAG, "[report] applicationCost:%s firstScreenCost:%s allCost:%s isWarmStartUp:%s", applicationCost, firstScreenCost, allCost, isWarmStartUp); long[] data = new long[0]; if (! isWarmStartUp && allCost >= coldStartupThresholdMs) { // for cold startup data = AppMethodBeat.getInstance().copyData(ActivityThreadHacker.sApplicationCreateBeginMethodIndex); ActivityThreadHacker.sApplicationCreateBeginMethodIndex.release(); } else if (isWarmStartUp && allCost >= warmStartupThresholdMs) { data = AppMethodBeat.getInstance().copyData(ActivityThreadHacker.sLastLaunchActivityMethodIndex); ActivityThreadHacker.sLastLaunchActivityMethodIndex.release(); } MatrixHandlerThread.getDefaultHandler().post(new AnalyseTask(data, applicationCost, firstScreenCost, allCost, isWarmStartUp, ActivityThreadHacker.sApplicationCreateScene)); }Copy the code
FrameTracer
The FrameTracer module mainly FPSCollector and FrameDecorator. The main principle is to get VSync VSync related callbacks through Choreographer.
choreographer = Choreographer.getInstance();
callbackQueueLock = reflectObject(choreographer, "mLock");
callbackQueues = reflectObject(choreographer, "mCallbackQueues");
addInputQueue = reflectChoreographerMethod(callbackQueues[CALLBACK_INPUT], ADD_CALLBACK, long.class, Object.class, Object.class);
addAnimationQueue = reflectChoreographerMethod(callbackQueues[CALLBACK_ANIMATION], ADD_CALLBACK, long.class, Object.class, Object.class);
addTraversalQueue = reflectChoreographerMethod(callbackQueues[CALLBACK_TRAVERSAL], ADD_CALLBACK, long.class, Object.class, Object.class);
frameIntervalNanos = reflectObject(choreographer, "mFrameIntervalNanos");
Copy the code
Get Choreographer related instances through reflection
private synchronized void addFrameCallback(int type, Runnable callback, boolean isAddHeader) { if (callbackExist[type]) { MatrixLog.w(TAG, "[addFrameCallback] this type %s callback has exist! isAddHeader:%s", type, isAddHeader); return; } if (! isAlive && type == CALLBACK_INPUT) { MatrixLog.w(TAG, "[addFrameCallback] UIThreadMonitor is not alive!" ); return; } try { synchronized (callbackQueueLock) { Method method = null; switch (type) { case CALLBACK_INPUT: method = addInputQueue; break; case CALLBACK_ANIMATION: method = addAnimationQueue; break; case CALLBACK_TRAVERSAL: method = addTraversalQueue; break; } if (null ! = method) {//1 method.invoke(callbackQueues[type], ! isAddHeader ? SystemClock.uptimeMillis() : -1, callback, null); callbackExist[type] = true; } } } catch (Exception e) { MatrixLog.e(TAG, e.toString()); }}Copy the code
Note 1: Add data to mCallbackQueues in Choreographer, the result of which is returned in callback and time consuming data is set
public void addCallbackLocked(long dueTime, Object action, Object token) { CallbackRecord callback = obtainCallbackLocked(dueTime, action, token); CallbackRecord entry = mHead; if (entry == null) { mHead = callback; return; } if (dueTime < entry.dueTime) { callback.next = entry; mHead = callback; return; } while (entry.next ! = null) { if (dueTime < entry.next.dueTime) { callback.next = entry.next; break; } entry = entry.next; } entry.next = callback; }Copy the code
private void doQueueBegin(int type) { queueStatus[type] = DO_QUEUE_BEGIN; queueCost[type] = System.nanoTime(); } private void doQueueEnd(int type) { queueStatus[type] = DO_QUEUE_END; queueCost[type] = System.nanoTime() - queueCost[type]; synchronized (this) { callbackExist[type] = false; }}Copy the code
This is where the callback sets the data, but queueCost doesn’t use it. Emmm actually choretracer does not use FrameTracer, it still uses the main thread message distribution logic, the same logic as EvilMethodTracer
The core code in FrameTracer is as follows
private void notifyListener(final String visibleScene, final long taskCostMs, final long frameCostMs, final boolean isContainsFrame) {
long start = System.currentTimeMillis();
try {
synchronized (listeners) {
for (final IDoFrameListener listener : listeners) {
if (config.isDevEnv()) {
listener.time = SystemClock.uptimeMillis();
}
final int dropFrame = (int) (taskCostMs / frameIntervalMs);
listener.doFrameSync(visibleScene, taskCostMs, frameCostMs, dropFrame, isContainsFrame);
if (null != listener.getExecutor()) {
listener.getExecutor().execute(new Runnable() {
@Override
public void run() {
listener.doFrameAsync(visibleScene, taskCostMs, frameCostMs, dropFrame, isContainsFrame);//1
}
});
}
}
}
} finally {
long cost = System.currentTimeMillis() - start;
if (config.isDebug() && cost > frameIntervalMs) {
MatrixLog.w(TAG, "[notifyListener] warm! maybe do heavy work in doFrameSync! size:%s cost:%sms", listeners.size(), cost);
}
}
}
Copy the code
Note 1 has specific implementations in FPSCollector and FrameDecorator
Processing and reporting of frame drop data in FPSCollector
void collect(int droppedFrames, boolean isContainsFrame) {
long frameIntervalCost = UIThreadMonitor.getMonitor().getFrameIntervalNanos();
sumFrameCost += (droppedFrames + 1) * frameIntervalCost / Constants.TIME_MILLIS_TO_NANO;
sumDroppedFrames += droppedFrames;
sumFrame++;
if (!isContainsFrame) {
sumTaskFrame++;
}
if (droppedFrames >= frozenThreshold) {
dropLevel[DropStatus.DROPPED_FROZEN.index]++;
dropSum[DropStatus.DROPPED_FROZEN.index] += droppedFrames;
} else if (droppedFrames >= highThreshold) {
dropLevel[DropStatus.DROPPED_HIGH.index]++;
dropSum[DropStatus.DROPPED_HIGH.index] += droppedFrames;
} else if (droppedFrames >= middleThreshold) {
dropLevel[DropStatus.DROPPED_MIDDLE.index]++;
dropSum[DropStatus.DROPPED_MIDDLE.index] += droppedFrames;
} else if (droppedFrames >= normalThreshold) {
dropLevel[DropStatus.DROPPED_NORMAL.index]++;
dropSum[DropStatus.DROPPED_NORMAL.index] += droppedFrames;
} else {
dropLevel[DropStatus.DROPPED_BEST.index]++;
dropSum[DropStatus.DROPPED_BEST.index] += (droppedFrames < 0 ? 0 : droppedFrames);
}
}
Copy the code
- The number of frames collected is calculated by the time the main line becomes a dispatch
- The number of frames dropped is set to 5 levels
FrameDecorator creates a view based on the frame rate. This part of the logic is relatively simple, do not go into too much.
@Override public void doFrameAsync(String visibleScene, long taskCost, long frameCostMs, int droppedFrames, boolean isContainsFrame) { super.doFrameAsync(visibleScene, taskCost, frameCostMs, droppedFrames, isContainsFrame); sumFrameCost += (droppedFrames + 1) * UIThreadMonitor.getMonitor().getFrameIntervalNanos() / Constants.TIME_MILLIS_TO_NANO; sumFrames += 1; long duration = sumFrameCost - lastCost[0]; long collectFrame = sumFrames - lastFrames[0]; if (duration >= 200) { final float fps = Math.min(60.f, 1000.f * collectFrame / duration); updateView(view.fpsView, fps); view.chartView.addFps((int) fps); lastCost[0] = sumFrameCost; lastFrames[0] = sumFrames; mainHandler.removeCallbacks(updateDefaultRunnable); mainHandler.postDelayed(updateDefaultRunnable, 130); } } private void updateView(final TextView view, final float fps) { mainHandler.post(new Runnable() { @Override public void run() { view.setText(String.format("%.2f FPS", fps)); if (fps >= 50) { view.setTextColor(view.getResources().getColor(android.R.color.holo_green_dark)); } else if (fps >= 30) { view.setTextColor(view.getResources().getColor(android.R.color.holo_orange_dark)); } else { view.setTextColor(view.getResources().getColor(android.R.color.holo_red_dark)); }}}); }Copy the code
conclusion
After introducing the framework of Matrix Trace — Canary, the framework structure is relatively clear and not complicated, mainly due to the complicated data processing module. However, this is also one of the advantages of Trace compared with other frameworks, which can completely record the time-consuming function call stack.