Android進(jìn)階Handler應(yīng)用線上卡頓監(jiān)控詳解
引言
在上一篇文章中# Android進(jìn)階寶典 -- KOOM線上APM監(jiān)控最全剖析,我詳細(xì)介紹了對(duì)于線上App內(nèi)存監(jiān)控的方案策略,其實(shí)除了內(nèi)存指標(biāo)之外,經(jīng)常有用戶反饋卡頓問題,其實(shí)這種問題是最難定位的,因?yàn)椴幌馛rash有完整的堆棧信息,而且卡頓問題可能轉(zhuǎn)瞬即逝,那么如何健全完整的線上卡頓監(jiān)控,可能就需要我們對(duì)于Android系統(tǒng)的消息處理有一個(gè)清晰的認(rèn)知。
1 Handler消息機(jī)制
這里我不會(huì)完整的從Handler源碼來分析Android的消息體系,而是從Handler自身的特性引申出線上卡頓監(jiān)控的策略方案。
1.1 方案確認(rèn)
首先當(dāng)我們啟動(dòng)一個(gè)App的時(shí)候,是由AMS通知zygote進(jìn)程fork出主進(jìn)程,其中主進(jìn)程的入口就是ActivityThread的main方法,在這個(gè)方法中開啟Loop死循環(huán)來處理系統(tǒng)消息。
Looper.loop();
在ActivityThread中,有一個(gè)內(nèi)部類ApplicationThread,這個(gè)類是system_server的一個(gè)代理對(duì)象,負(fù)責(zé)App主進(jìn)程與system_server進(jìn)程的通信(如果對(duì)這塊有疑問的,可以看之前的文章都有詳細(xì)的介紹)。
private class ApplicationThread extends IApplicationThread.Stub { private static final String DB_INFO_FORMAT = " %8s %8s %14s %14s %s"; @Override public final void bindApplication(String processName, ApplicationInfo appInfo, ProviderInfoList providerList, ComponentName instrumentationName, ProfilerInfo profilerInfo, Bundle instrumentationArgs, IInstrumentationWatcher instrumentationWatcher, IUiAutomationConnection instrumentationUiConnection, int debugMode, boolean enableBinderTracking, boolean trackAllocation, boolean isRestrictedBackupMode, boolean persistent, Configuration config, CompatibilityInfo compatInfo, Map services, Bundle coreSettings, String buildSerial, AutofillOptions autofillOptions, ContentCaptureOptions contentCaptureOptions, long[] disabledCompatChanges, SharedMemory serializedSystemFontMap) { if (services != null) { if (false) { // Test code to make sure the app could see the passed-in services. for (Object oname : services.keySet()) { if (services.get(oname) == null) { continue; // AM just passed in a null service. } String name = (String) oname; // See b/79378449 about the following exemption. switch (name) { case "package": case Context.WINDOW_SERVICE: continue; } if (ServiceManager.getService(name) == null) { Log.wtf(TAG, "Service " + name + " should be accessible by this app"); } } } // Setup the service cache in the ServiceManager ServiceManager.initServiceCache(services); } setCoreSettings(coreSettings); AppBindData data = new AppBindData(); data.processName = processName; data.appInfo = appInfo; data.providers = providerList.getList(); data.instrumentationName = instrumentationName; data.instrumentationArgs = instrumentationArgs; data.instrumentationWatcher = instrumentationWatcher; data.instrumentationUiAutomationConnection = instrumentationUiConnection; data.debugMode = debugMode; data.enableBinderTracking = enableBinderTracking; data.trackAllocation = trackAllocation; data.restrictedBackupMode = isRestrictedBackupMode; data.persistent = persistent; data.config = config; data.compatInfo = compatInfo; data.initProfilerInfo = profilerInfo; data.buildSerial = buildSerial; data.autofillOptions = autofillOptions; data.contentCaptureOptions = contentCaptureOptions; data.disabledCompatChanges = disabledCompatChanges; data.mSerializedSystemFontMap = serializedSystemFontMap; sendMessage(H.BIND_APPLICATION, data); } }
我們可以看到,每個(gè)方法的最后,其實(shí)都是調(diào)用了sendMessage方法,通過Handler發(fā)送消息;為啥會(huì)用到Handler呢,是因?yàn)锳pp進(jìn)程與system_server進(jìn)程通信是通過Binder實(shí)現(xiàn)的,Binder會(huì)開辟Binder線程池,那么此時(shí)這個(gè)方法的調(diào)用是在子線程中完成,像bindApplication最終需要調(diào)用Application的onCreate方法,但這個(gè)方法是在主線程中,因此需要Handler完成線程切換。
所以整個(gè)App消息體系都是通過Handler來支持起來的,看下圖
因?yàn)锳ndroid對(duì)于消息的時(shí)效性要求非常高,需要一個(gè)高速執(zhí)行的狀態(tài),一旦有消息執(zhí)行耗時(shí)造成阻塞就會(huì)產(chǎn)生卡頓,所以通過Handler來監(jiān)聽消息的執(zhí)行速度,通過設(shè)定閾值判斷是否產(chǎn)生卡頓,從而獲取堆棧消息來定位問題。
1.2 Looper源碼
我們先去看下Looper源碼,看如何處理分發(fā)消息的
public static void loop() { final Looper me = myLooper(); if (me == null) { throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread."); } if (me.mInLoop) { Slog.w(TAG, "Loop again would have the queued messages be executed" + " before this one completed."); } me.mInLoop = true; // Make sure the identity of this thread is that of the local process, // and keep track of what that identity token actually is. Binder.clearCallingIdentity(); final long ident = Binder.clearCallingIdentity(); // Allow overriding a threshold with a system prop. e.g. // adb shell 'setprop log.looper.1000.main.slow 1 && stop && start' final int thresholdOverride = SystemProperties.getInt("log.looper." + Process.myUid() + "." + Thread.currentThread().getName() + ".slow", 0); me.mSlowDeliveryDetected = false; /**在這里開啟死循環(huán)*/ for (;;) { if (!loopOnce(me, ident, thresholdOverride)) { return; } } }
在Looper的loop方法中,開啟一個(gè)死循環(huán),然后調(diào)用了loopOnce方法
private static boolean loopOnce(final Looper me, final long ident, final int thresholdOverride) { /**第一步,從MessagQueue中取出消息*/ Message msg = me.mQueue.next(); // might block if (msg == null) { // No message indicates that the message queue is quitting. return false; } // This must be in a local variable, in case a UI event sets the logger /**這里關(guān)注下這個(gè)打點(diǎn)信息*/ final Printer logging = me.mLogging; if (logging != null) { logging.println(">>>>> Dispatching to " + msg.target + " " + msg.callback + ": " + msg.what); } try { /**第二步,調(diào)用Handler的dispatchMessage方法*/ msg.target.dispatchMessage(msg); if (observer != null) { observer.messageDispatched(token, msg); } dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0; } catch (Exception exception) { if (observer != null) { observer.dispatchingThrewException(token, msg, exception); } throw exception; } finally { ThreadLocalWorkSource.restore(origWorkSource); if (traceTag != 0) { Trace.traceEnd(traceTag); } } //...... /**消息執(zhí)行完成的打點(diǎn)*/ if (logging != null) { logging.println("<<<<< Finished to " + msg.target + " " + msg.callback); } return true; }
這里我們需要關(guān)注的有兩個(gè)點(diǎn):
(1)看消息是如何被分發(fā)執(zhí)行的,在注釋中,我標(biāo)注了關(guān)鍵的二步;
(2)從消息被執(zhí)行之前,到消息執(zhí)行之后,有兩處打點(diǎn)信息分別為:Dispatching to和Finished to,這個(gè)就是代表消息執(zhí)行的整個(gè)過程,如果我們能夠拿到這兩段之間的耗時(shí),是不是就可以完成我們的方案策略。
通過源碼我們可以看到,這個(gè)Printer是我們可以自定義傳入的,那也就是說,我們可以在我們自定義的Printer中插入計(jì)時(shí)的代碼,就可以監(jiān)控每個(gè)消息執(zhí)行的耗時(shí)了。
public void setMessageLogging(@Nullable Printer printer) { mLogging = printer; }
1.3 Blockcanary原理分析
所以根據(jù)上面的源碼分析,業(yè)內(nèi)有一款適用于卡頓監(jiān)控的組件 - Blockcanary
implementation 'com.github.markzhai:blockcanary-android:1.5.0'
使用方式:
BlockCanary.install(this, BlockCanaryContext()).start()
所以我們看一下Blockcanary的源碼,它的思想就是我們提到的通過setMessageLogging方法注入自己的代碼。
public void start() { if (!mMonitorStarted) { mMonitorStarted = true; Looper.getMainLooper().setMessageLogging(mBlockCanaryCore.monitor); } }
在start方法中,就是調(diào)用了setMessageLogging方法,傳入了一個(gè)Printer對(duì)象,這個(gè)實(shí)現(xiàn)類就是LooperMonitor,其中需要實(shí)現(xiàn)println方法.
class LooperMonitor implements Printer { @Override public void println(String x) { if (mStopWhenDebugging && Debug.isDebuggerConnected()) { return; } /** mPrintingStarted 默認(rèn)false */ if (!mPrintingStarted) { mStartTimestamp = System.currentTimeMillis(); mStartThreadTimestamp = SystemClock.currentThreadTimeMillis(); mPrintingStarted = true; startDump(); } else { final long endTime = System.currentTimeMillis(); mPrintingStarted = false; if (isBlock(endTime)) { notifyBlockEvent(endTime); } stopDump(); } } private boolean isBlock(long endTime) { return endTime - mStartTimestamp > mBlockThresholdMillis; } }
我們知道,在Looper的loop方法中,會(huì)調(diào)用兩次print方法,所以在第一次調(diào)用println方法的時(shí)候,會(huì)記錄一個(gè)系統(tǒng)時(shí)間;第二次進(jìn)入的時(shí)候,會(huì)再次記一次系統(tǒng)時(shí)間,前后兩次時(shí)間差如果超過一個(gè)閾值mBlockThresholdMillis,那么認(rèn)為是發(fā)生了卡頓。
private void notifyBlockEvent(final long endTime) { final long startTime = mStartTimestamp; final long startThreadTime = mStartThreadTimestamp; final long endThreadTime = SystemClock.currentThreadTimeMillis(); HandlerThreadFactory.getWriteLogThreadHandler().post(new Runnable() { @Override public void run() { mBlockListener.onBlockEvent(startTime, endTime, startThreadTime, endThreadTime); } }); }
如果發(fā)生了卡頓,那么就會(huì)將堆棧信息記錄到文件當(dāng)中,但是這樣處理真的能夠幫助到我們嗎?
1.4 Handler監(jiān)控的缺陷
當(dāng)然Blockcanary確實(shí)能夠幫助我們確認(rèn)卡頓發(fā)生的一個(gè)大致范圍,但是我們看下面的圖
當(dāng)方法B執(zhí)行完成之后觸發(fā)了卡頓閾值,這個(gè)時(shí)候堆棧當(dāng)中存在方法A的堆棧信息和方法B的堆棧信息,那么我們會(huì)認(rèn)為因?yàn)榉椒˙的原因產(chǎn)生了卡頓嗎?其實(shí)不然,如果堆棧信息中也包含了其他方法,那么Handler監(jiān)控其實(shí)也只是給出了一個(gè)大粒度的范圍,分析起來還是會(huì)有問題。
2 字節(jié)碼插樁實(shí)現(xiàn)方法耗時(shí)監(jiān)控
基于前面我們對(duì)于Blockcanary的分析,其存在的一個(gè)重大弊端就是無法獲取細(xì)顆粒度的數(shù)據(jù),例如每個(gè)方法執(zhí)行的耗時(shí),當(dāng)打印出堆棧信息之后,附加上每個(gè)方法的耗時(shí),這樣就能準(zhǔn)確地定位出耗時(shí)方法的存在。
private fun funcA() { funcB() } private fun funcB() { Thread.sleep(400) funcC() } private fun funcC() { funcD() } private fun funcD() { Thread.sleep(100) }
例如還是以500ms為卡頓閾值,那么當(dāng)執(zhí)行方法A的時(shí)候,系統(tǒng)檢測(cè)到了卡頓的發(fā)生,如果給到一個(gè)堆棧信息如下:
D方法 耗時(shí)100ms
C方法 耗時(shí)100ms
B方法 耗時(shí)400ms
A方法 耗時(shí)500ms
這樣是不是就一目了然了,顯然是方法B中有一個(gè)非常耗時(shí)的操作,那么如何獲取每個(gè)方法執(zhí)行的時(shí)間呢?
private fun funcA() { val startTime = System.currentTimeMillis() funcB() val deltaTime = System.currentTimeMillis() - startTime }
上述這種方式可以獲取方法耗時(shí),如果我們僅在測(cè)試階段想測(cè)試某個(gè)方法耗時(shí)可以這么做,但是工程中成千上萬的方法,如果靠自己手動(dòng)這么添加豈不是要累死,所以就需要字節(jié)碼插樁來幫忙在每個(gè)方法中加入上述代碼邏輯。
2.1 字節(jié)碼插樁流程
如果有看過Android進(jìn)階寶典 -- 從字節(jié)碼插樁技術(shù)了解美團(tuán)熱修復(fù)這篇文章的伙伴,可能對(duì)于字節(jié)碼插樁有些了解了。其實(shí)字節(jié)碼插樁,就是在class文件中寫代碼。
因?yàn)椴还苁荍ava還是Kotlin最終都會(huì)編譯成class字節(jié)碼,而我們?nèi)粘i_發(fā)中肯定是在Java(Kotlin)層上寫代碼,而字節(jié)碼插樁則是在class文件上寫代碼。
因此整個(gè)字節(jié)碼插樁的流程就是
其中難點(diǎn)就在于解析出class文件中包含的信息之后,需要嚴(yán)格按照class字節(jié)碼的規(guī)則來進(jìn)行修改,只要有一個(gè)地方改錯(cuò)了,那么生成的.class文件就無法使用,所以如果要我們自己修改顯然是很難,因此各路Android大佬考慮到這個(gè)問題,就開源出很多框架提供給我們使用。
2.2 引入ASM實(shí)現(xiàn)字節(jié)碼插樁
首先,我們先引入ASM依賴
implementation 'org.ow2.asm:asm:9.1' implementation 'org.ow2.asm:asm-util:9.1' implementation 'org.ow2.asm:asm-commons:9.1'
我們可以根據(jù)2.1小節(jié)的這個(gè)流程圖,利用ASM中的工具完成字節(jié)碼插樁。
public class TestFunctionRunTime { public TestFunctionRunTime() { } public void funA() throws InterruptedException { Thread.sleep(2000); } }
例如,我們想在funA中插入計(jì)算耗時(shí)的方法,那么首先需要得到這個(gè)類的class文件
fun transform() { //IO操作,獲取文件流 val fis = FileInputStream("/storage/emulated/0/TestFunctionRunTime.class") //用于讀取class文件中信息 val cr = ClassReader(fis) val cw = ClassWriter(ClassWriter.COMPUTE_MAXS) //開始分析字節(jié)碼 cr.accept( MyClassVisitor(Opcodes.ASM9, cw), ClassReader.SKIP_FRAMES or ClassReader.SKIP_DEBUG ) }
首先,獲取class文件這里我作為示例直接通過IO加載某個(gè)路徑下的class文件,通過ASM中提供的ClassReader和ClassWriter來讀取class中的文件信息,然后調(diào)用ClassReader的accept方法,開始分析class文件。
class MyClassVisitor(api: Int, classVisitor: ClassVisitor) : ClassVisitor(api, classVisitor) { override fun visitMethod( access: Int, name: String?, descriptor: String?, signature: String?, exceptions: Array<out String>? ): MethodVisitor { /**這里假設(shè)就對(duì)一個(gè)方法插樁*/ return if (name == "funA") { val methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions) MyMethodVisitor(api, methodVisitor, access, name, descriptor) } else { super.visitMethod(access, name, descriptor, signature, exceptions) } } override fun visitField( access: Int, name: String?, descriptor: String?, signature: String?, value: Any? ): FieldVisitor { return super.visitField(access, name, descriptor, signature, value) } override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor { return super.visitAnnotation(descriptor, visible) } }
因?yàn)樵谝粋€(gè)類中,會(huì)存在很多屬性,例如變量、方法、注解等等,所以在ASM中的ClassVisitor類中,提供了這些屬性的訪問權(quán)利,例如visitMethod可以訪問方法,假如我們想要對(duì)funA進(jìn)行插樁,那么就需要做一些自定義的操作,這里就可以使用ASM提供的AdviceAdapter來完成方法執(zhí)行過程中代碼的插入。
class MyMethodVisitor( val api: Int, val methodVisitor: MethodVisitor, val mAccess: Int, val methodName: String, val descriptor: String? ) : AdviceAdapter(api, methodVisitor, mAccess, methodName, descriptor) { /**當(dāng)方法開始執(zhí)行的時(shí)候*/ override fun onMethodEnter() { super.onMethodEnter() } /**當(dāng)方法執(zhí)行結(jié)束的時(shí)候*/ override fun onMethodExit(opcode: Int) { super.onMethodExit(opcode) } }
假設(shè)我們對(duì)于每個(gè)方法,都插入以下兩行代碼,那么我們?cè)诓僮髯止?jié)碼的時(shí)候,需要看一下當(dāng)這個(gè)方法被編譯成字節(jié)碼之后,是什么樣的。
public void funA() throws InterruptedException { Long startTime = System.currentTimeMillis(); Thread.sleep(2000L); Log.e("TestFunctionRunTime", "duration=>" + (System.currentTimeMillis() - startTime)); }
插入代碼之前的字節(jié)碼如下:
public funA()V throws java/lang/InterruptedException L0 LINENUMBER 18 L0 LDC 2000 INVOKESTATIC java/lang/Thread.sleep (J)V L1 LINENUMBER 19 L1 RETURN L2 LOCALVARIABLE this Lcom/lay/mvi/net/TestFunctionRunTime; L0 L2 0 MAXSTACK = 2 MAXLOCALS = 1
插入代碼之后的字節(jié)碼如下:
public funA()V throws java/lang/InterruptedException L0 LINENUMBER 17 L0 INVOKESTATIC java/lang/System.currentTimeMillis ()J INVOKESTATIC java/lang/Long.valueOf (J)Ljava/lang/Long; ASTORE 1 L1 LINENUMBER 18 L1 LDC 2000 INVOKESTATIC java/lang/Thread.sleep (J)V L2 LINENUMBER 19 L2 LDC "TestFunctionRunTime" NEW java/lang/StringBuilder DUP INVOKESPECIAL java/lang/StringBuilder.<init> ()V LDC "duration=>" INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; INVOKESTATIC java/lang/System.currentTimeMillis ()J ALOAD 1 INVOKEVIRTUAL java/lang/Long.longValue ()J LSUB INVOKEVIRTUAL java/lang/StringBuilder.append (J)Ljava/lang/StringBuilder; INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; INVOKESTATIC android/util/Log.e (Ljava/lang/String;Ljava/lang/String;)I POP L3 LINENUMBER 20 L3 RETURN L4 LOCALVARIABLE this Lcom/lay/mvi/net/TestFunctionRunTime; L0 L4 0 LOCALVARIABLE startTime Ljava/lang/Long; L1 L4 1 MAXSTACK = 6 MAXLOCALS = 2 }
首先我們看如果按照我們這種加代碼的方式,當(dāng)然沒問題,但是在進(jìn)行插樁的時(shí)候,將會(huì)寫很多的字節(jié)碼指令,看下面的代碼,我僅僅貼出L2代碼塊就需要這么多,寫的多通常就會(huì)出問題。
visitLdcInsn(methodName) visitTypeInsn(NEW, "java/lang/StringBuilder") visitInsn(DUP) visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false) visitLdcInsn(""duration=>"") visitMethodInsn( INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder", false ) visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false) visitVarInsn(ALOAD, 1) visitMethodInsn(INVOKEVIRTUAL, "java/lang/Long", "longValue", "()J", false) visitInsn(LSUB) visitMethodInsn( INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder", false ) visitMethodInsn( INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String", false ) visitMethodInsn( INVOKEVIRTUAL, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false ) visitInsn(POP)
所以簡(jiǎn)單一點(diǎn)就是封裝一個(gè)方法,因?yàn)檫@個(gè)插樁是在編譯時(shí)將代碼插入,所以不影響
object AppMethodTrace { private var startTime: Long = 0L fun start() { startTime = System.currentTimeMillis() } fun end(funcName: String) { val endTime = System.currentTimeMillis() Log.e("AppMethodTrace", "$funcName 耗時(shí)為${endTime - startTime}") } }
看這樣就變得非常簡(jiǎn)便了,而且寫起來也是非常清晰
public funA()V throws java/lang/InterruptedException L0 LINENUMBER 17 L0 GETSTATIC com/lay/mvi/net/AppMethodTrace.INSTANCE : Lcom/lay/mvi/net/AppMethodTrace; INVOKEVIRTUAL com/lay/mvi/net/AppMethodTrace.start ()V L1 LINENUMBER 18 L1 LDC 2000 INVOKESTATIC java/lang/Thread.sleep (J)V L2 LINENUMBER 19 L2 GETSTATIC com/lay/mvi/net/AppMethodTrace.INSTANCE : Lcom/lay/mvi/net/AppMethodTrace; LDC "funA" INVOKEVIRTUAL com/lay/mvi/net/AppMethodTrace.end (Ljava/lang/String;)V L3 LINENUMBER 20 L3 RETURN L4 LOCALVARIABLE this Lcom/lay/mvi/net/TestFunctionRunTime; L0 L4 0 MAXSTACK = 2 MAXLOCALS = 1
那么通過onMethodEnter和onMethodExit兩個(gè)方法的處理,就可以完成對(duì)字節(jié)碼插入的操作。
class MyMethodVisitor( val api: Int, val methodVisitor: MethodVisitor, val mAccess: Int, val methodName: String, val descriptor: String? ) : AdviceAdapter(api, methodVisitor, mAccess, methodName, descriptor) { /**當(dāng)方法開始執(zhí)行的時(shí)候*/ override fun onMethodEnter() { super.onMethodEnter() visitFieldInsn( GETSTATIC, "com/lay/mvi/net/AppMethodTrace", "INSTANCE", "Lcom/lay/mvi/net/AppMethodTrace" ) visitMethodInsn(INVOKEVIRTUAL, "com/lay/mvi/net/AppMethodTrace", "start", "()V", false) } /**當(dāng)方法執(zhí)行結(jié)束的時(shí)候*/ override fun onMethodExit(opcode: Int) { super.onMethodExit(opcode) visitFieldInsn( GETSTATIC, "com/lay/mvi/net/AppMethodTrace", "INSTANCE", "Lcom/lay/mvi/net/AppMethodTrace" ) /**方法名可以動(dòng)態(tài)拿到*/ visitLdcInsn(methodName) visitMethodInsn( INVOKEVIRTUAL, "com/lay/mvi/net/AppMethodTrace", "end", "(Ljava/lang/String;)V", false ) } }
最終,通過分析處理字節(jié)碼之后,將修改后的字節(jié)碼重新輸出到新的文件,在實(shí)際的應(yīng)用開發(fā)中,是需要覆蓋之前的字節(jié)碼文件的。
//輸出結(jié)果 val bytes = cw.toByteArray() val fos = FileOutputStream("/storage/emulated/0/TestFunctionRunTimeTransform.class") fos.write(bytes) fos.flush() fos.close()
如果伙伴們第一次使用,建議還是熟悉所有的字節(jié)碼指令以及ASM的API,這樣我們?cè)趯懙臅r(shí)候就非常迅速了。
2.3 Blockcanary的優(yōu)化策略
通過前面我們對(duì)于Blockcanary的了解,通過Handler雖然能夠獲取卡頓時(shí)的堆棧信息,但是無法獲取到方法的執(zhí)行耗時(shí),所以通過ASM字節(jié)碼插樁統(tǒng)計(jì)方法耗時(shí)配合Handler,就能夠精確地定位到卡頓的方法,有時(shí)間的伙伴們可以去看下騰訊的Matrix。
最后還要啰嗦一下,其實(shí)對(duì)于字節(jié)碼插樁,像美團(tuán)的熱修復(fù)框架采用的字節(jié)碼插樁技術(shù)就是ASM,但方式并不是只有這一種,像Javassist、kotlinpoet/javapoet都具備插樁的能力;我們?cè)谧鼍€上卡頓監(jiān)控的時(shí)候,其實(shí)就是在做一個(gè)系統(tǒng),所以不能從一個(gè)點(diǎn)出發(fā),像運(yùn)用到系統(tǒng)能力之外,同樣也會(huì)使用到三方框架作為輔助手段,目的就是為了能夠達(dá)到快速定位、快速響應(yīng)的能力。
以上就是Android進(jìn)階Handler應(yīng)用線上卡頓監(jiān)控詳解的詳細(xì)內(nèi)容,更多關(guān)于Android Handler線上卡頓監(jiān)控的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android ProgressBar直線進(jìn)度條的實(shí)例代碼
本文通過實(shí)例代碼給大家介紹了android progressbar直線進(jìn)度條的實(shí)現(xiàn)方法,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友參考下吧2017-06-06Android系統(tǒng)添加自定義鼠標(biāo)樣式通過按鍵切換實(shí)例詳解
在本篇文章里小編給大家整理的是關(guān)于Android系統(tǒng)添加自定義鼠標(biāo)樣式通過按鍵切換實(shí)例詳解內(nèi)容,有需要的朋友們可以學(xué)習(xí)下。2019-11-11Android開發(fā)Launcher進(jìn)程啟動(dòng)流程
這篇文章主要為大家介紹了Android開發(fā)Launcher進(jìn)程啟動(dòng)流程示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06Android中SparseArray性能優(yōu)化的使用方法
這篇文章主要為大家詳細(xì)介紹了Android中SparseArray性能優(yōu)化的使用方法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-04-04Android?Jetpack組件ViewModel基本用法詳解
這篇文章主要為大家介紹了Android?Jetpack組件ViewModel基本用法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01Android使用xUtils3.0實(shí)現(xiàn)文件上傳
這篇文章主要為大家詳細(xì)介紹了Android使用xUtils3.0實(shí)現(xiàn)文件上傳的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-11-11