卡頓優(yōu)化②原因分析和監(jiān)控工具

卡頓分析

造成卡頓的原因可能是多種多樣的,但是最終都會(huì)反映在CPU時(shí)間上。Android系統(tǒng)是基于Linux的奋隶,可以CPU時(shí)間分為兩種:

  • 用戶時(shí)間——執(zhí)行用戶態(tài)應(yīng)用程序代碼消耗的時(shí)間苛让。
  • 系統(tǒng)時(shí)間——執(zhí)行內(nèi)核態(tài)系統(tǒng)調(diào)用的時(shí)間火惊。包括I/O、鎖累贤、中斷以及其他系統(tǒng)調(diào)用的執(zhí)行時(shí)間叠穆。

CPU的相關(guān)問(wèn)題可以分為三類:

  1. CPU資源冗余使用
    算法效率太低——主要出現(xiàn)在數(shù)據(jù)的查找、排序臼膏、刪除等環(huán)節(jié)
    沒(méi)有使用緩存——比如bitmap的復(fù)用硼被,圖片的緩存等,圖片的讀取會(huì)涉及文件I/O或者網(wǎng)絡(luò)I/O渗磅,Bitmap的創(chuàng)建涉及解碼祷嘶。這些操作對(duì)于CPU來(lái)說(shuō)都是耗時(shí)操作屎媳,應(yīng)該盡量避免。
    計(jì)算時(shí)使用的數(shù)據(jù)結(jié)構(gòu)不對(duì)——可以用int類型處理的數(shù)據(jù)運(yùn)算论巍,卻使用long或者double烛谊,這會(huì)導(dǎo)致CPU的運(yùn)算負(fù)載多出4倍。
  2. CPU資源搶占
    主線程的CPU資源被搶占——這是最常見(jiàn)的問(wèn)題嘉汰,在Android6.0之前沒(méi)有RenderThread的時(shí)候丹禀,頁(yè)面繪制渲染的工作都是在主線程中完成,主線程處理這些工作需要的CPU資源被子線程搶占鞋怀,就會(huì)導(dǎo)致頁(yè)面繪制渲染工作無(wú)法及時(shí)完成双泪。
    音視頻播放的資源被搶占——音視頻編解碼本身會(huì)消耗大量的CPU資源,并且流暢的視頻播放會(huì)解碼速度是有硬性要求的密似,如果達(dá)不到就可能導(dǎo)致音視頻播放效果不流暢焙矛。
    線程過(guò)多——如果同時(shí)競(jìng)爭(zhēng)CPU資源的線程過(guò)多,就會(huì)導(dǎo)致同一段時(shí)間內(nèi)残腌,每個(gè)線程分配到的CPU資源偏少村斟,從而導(dǎo)致線程執(zhí)行任務(wù)的耗時(shí)增加。所以在使用線程池時(shí)抛猫,要結(jié)合運(yùn)行設(shè)備的CPU的核心數(shù)蟆盹,來(lái)合理設(shè)置線程數(shù)。
  3. CPU使用率低
    當(dāng)系統(tǒng)處理磁盤或者網(wǎng)絡(luò)IO闺金、同步鎖的競(jìng)爭(zhēng)和線程切換逾滥、線程的休眠等操作時(shí),會(huì)降低CPU的使用率败匹。

通過(guò)shell命令寨昙,了解CPU的性能

查看CPU的相關(guān)信息
  //在Android studio的Terminal窗口
//先輸入adb shell進(jìn)入當(dāng)前已連接手機(jī)的shell環(huán)境
adb shell

//獲取手機(jī)CPU的核心數(shù),如下圖所示掀亩,當(dāng)前連接的手機(jī)CPU為8核
cat /sys/devices/system/cpu/possible

//獲取第一個(gè)CPU的最大頻率
cat /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq

//獲取第二個(gè)CPU的最小頻率
cat /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq

執(zhí)行結(jié)果如下圖所示:
image.png
通過(guò)/proc/stat命令舔哪,查看CPU耗時(shí)
//查看整個(gè)系統(tǒng)的CPU使用情況
cat /proc/stat

//查看當(dāng)前正在調(diào)試的app所在進(jìn)程的CPU使用情況
cat /proc/self/stat

//查看指定的某個(gè)進(jìn)程的CPU使用情況
cat /proc/[pid]/stat

/proc/[pid]/stat             // 進(jìn)程CPU使用情況
/proc/[pid]/task/[tid]/stat  // 進(jìn)程下面各個(gè)線程的CPU使用情況
/proc/[pid]/sched            // 進(jìn)程CPU調(diào)度相關(guān)
/proc/loadavg                // 系統(tǒng)平均負(fù)載,uptime命令對(duì)應(yīng)文件

執(zhí)行結(jié)果如下:
image.png
image.png
使用top命令查看各進(jìn)程的CPU使用情況
// 直接使用top命令會(huì)定時(shí)不斷地輸出進(jìn)程的相關(guān)信息
top

// 排除0%的進(jìn)程信息 
top | grep -v '0% S'
image.png
// 獲取指定進(jìn)程的CPU归榕、內(nèi)存消耗尸红,并設(shè)置刷新間隔 
top -d 1 | grep pers.jay.wanandroid
image.png
使用ps命令查看進(jìn)程消耗CPU的時(shí)間占比
// 查看指定進(jìn)程的狀態(tài)信息 
ps -p 6440

上述指令中的6440是進(jìn)程ID,執(zhí)行結(jié)果如下
image.png

上圖中各輸出參數(shù)的含義如下

  • USER:用戶名
  • PID:進(jìn)程ID
  • PPID:父進(jìn)程ID
  • VSZ:虛擬內(nèi)存大小刹泄,以K為單位
  • RSS:常駐內(nèi)存大型饫铩(正在使用的頁(yè))
  • WCHAN:進(jìn)程在內(nèi)核態(tài)的運(yùn)行時(shí)間
  • nstruction pointer:指令指針
  • NAME:進(jìn)程名字
  • S:當(dāng)前進(jìn)程的狀態(tài),總共有10種可能的狀態(tài):R (running) S (sleeping) D (device I/O) T (stopped) t (traced) Z (zombie) X (deader) x (dead) K (wakekill) W (waking)特石。

查看指定進(jìn)程已經(jīng)消耗的CPU時(shí)間占系統(tǒng)總時(shí)間的百分比

// 查看指定進(jìn)程已經(jīng)消耗的CPU時(shí)間占系統(tǒng)總時(shí)間的百分比  
ps -o PCPU -p 6440
image.png
使用dumpsys cpuinfo命令查看CPU使用情況

使用dumpsys cpuinfo命令獲得的信息比起top命令得到的信息要更加精煉盅蝗,可以看到一段時(shí)間內(nèi),系統(tǒng)內(nèi)正在運(yùn)行的所有進(jìn)程姆蘸,以及各進(jìn)程的CPU使用情況墩莫。
image.png

卡頓優(yōu)化的工具

StrictMode

StrictMode芙委,是Android提供一種運(yùn)行時(shí)檢測(cè)機(jī)制,可以幫助開發(fā)人員檢測(cè)代碼中一些不規(guī)范的問(wèn)題狂秦。對(duì)于規(guī)模較大的項(xiàng)目灌侣,代碼量也很大,如果只是肉眼去review代碼裂问,不僅效率非常低侧啼,而且也比較容易出問(wèn)題。使用StrictMode之后堪簿,系統(tǒng)會(huì)自動(dòng)檢測(cè)出主線程中的一些異常情況痊乾。并且按照自定義的配置給出相應(yīng)的反應(yīng)。
StrictMode主要用來(lái)檢測(cè)兩方面的問(wèn)題:

  1. 線程策略
    線程策略的檢測(cè)內(nèi)容椭更,是一些自定義的耗時(shí)操作哪审,磁盤讀取以及網(wǎng)絡(luò)請(qǐng)求等。主要用于檢測(cè)主線程中的耗時(shí)操作虑瀑。
  2. 虛擬機(jī)策略
    虛擬機(jī)策略的檢測(cè)內(nèi)容主要是Activity泄漏湿滓、Sqlite對(duì)象泄漏和檢測(cè)實(shí)例數(shù)量。

StrictMode使用如下:

        //設(shè)置線程策略
        StrictMode.ThreadPolicy threadPolicy = new StrictMode.ThreadPolicy.Builder()
//                .detectCustomSlowCalls()  //API等級(jí)11缴川,使用StrictMode.noteSlowCode
//                .detectDiskReads()
//                .detectDiskWrites()
//                .detectNetwork()
                .detectAll()    //detectAll() for all detectable problems
//                .penaltyDialog()  //可以直接彈出警報(bào)Dialog
//                .penaltyDeath()   //或者直接崩潰
                .penaltyLog()   //在Logcat 中打印違規(guī)異常信息
                .build();


        StrictMode.setThreadPolicy(threadPolicy);


        //虛擬機(jī)策略
        StrictMode.VmPolicy vmPolicy = new StrictMode.VmPolicy.Builder()
                .detectActivityLeaks()  //檢測(cè)Activity對(duì)象的泄漏
                .detectLeakedSqlLiteObjects()   //檢測(cè)Sqlite對(duì)象的泄漏
                .setClassInstanceLimit(Danmakus.class,1)
                .detectAll()
                .penaltyLog()
                .build();


        StrictMode.setVmPolicy(vmPolicy);

在上述代碼中茉稠,我通過(guò)設(shè)置setClassInstanceLimit(Danmakus.class,1)來(lái)限制在程序運(yùn)行時(shí)Danmakus類的對(duì)象只能有一個(gè)描馅,當(dāng)StrictMode檢測(cè)到有多個(gè)Danmakus類的對(duì)象時(shí)把夸,就會(huì)報(bào)出如下提示:

2020-07-24 14:16:06.105 19773-19773/com.zacky.bulletchattest D/StrictMode: StrictMode policy violation: android.os.strictmode.InstanceCountViolation: class master.flame.danmaku.danmaku.model.android.Danmakus; instances=11; limit=1
        at android.os.StrictMode.setClassInstanceLimit(StrictMode.java:1)
2020-07-24 14:16:29.579 19773-19773/com.zacky.bulletchattest D/StrictMode: StrictMode policy violation; ~duration=56 ms: android.os.strictmode.DiskWriteViolation
        at android.os.StrictMode$AndroidBlockGuardPolicy.onWriteToDisk(StrictMode.java:1460)
        at java.io.FileOutputStream.<init>(FileOutputStream.java:236)
        at java.io.FileOutputStream.<init>(FileOutputStream.java:119)
        at java.io.FileWriter.<init>(FileWriter.java:63)
        at com.android.server.am.ActivityManagerServiceInjector.writeToNode(ActivityManagerServiceInjector.java:1726)
        at com.android.server.am.ActivityManagerServiceInjector.setTopAppUIThread(ActivityManagerServiceInjector.java:1716)
        at com.android.server.am.ActivityManagerService.applyOomAdjLocked(ActivityManagerService.java:25240)
        at com.android.server.am.ActivityManagerService.updateOomAdjLocked(ActivityManagerService.java:25943)
        at com.android.server.am.ActivityStack.resumeTopActivityInnerLocked(ActivityStack.java:2871)
        at com.android.server.am.ActivityStack.resumeTopActivityUncheckedLocked(ActivityStack.java:2474)
        at com.android.server.am.ActivityStackSupervisor.resumeFocusedStackTopActivityLocked(ActivityStackSupervisor.java:2352)
        at com.android.server.am.ActivityStack.completePauseLocked(ActivityStack.java:1726)
        at com.android.server.am.ActivityStack.activityPausedLocked(ActivityStack.java:1646)
        at com.android.server.am.ActivityManagerService.activityPaused(ActivityManagerService.java:8523)
        at android.app.IActivityManager$Stub.onTransact(IActivityManager.java:225)
        at com.android.server.am.ActivityManagerService.onTransact(ActivityManagerService.java:3374)
        at android.os.Binder.execTransact(Binder.java:726)
BlockCanary

BlockCanary對(duì)主線程操作進(jìn)行了完全透明的監(jiān)控,并能輸出有效的信息铭污,幫助開發(fā)分析恋日、定位到問(wèn)題所在,迅速優(yōu)化應(yīng)用嘹狞。其特點(diǎn)有:

  • 非侵入式岂膳,簡(jiǎn)單的兩行就打開監(jiān)控,不需要到處打點(diǎn)磅网,破壞代碼優(yōu)雅性谈截。
  • 精準(zhǔn),輸出的信息可以幫助定位到問(wèn)題所在(精確到行)涧偷,不需要像Logcat一樣簸喂,慢慢去找。

BlockCanary的原理:基于Android的消息處理機(jī)制燎潮。熟悉消息處理機(jī)制的同學(xué)都知道喻鳄,一個(gè)線程最多只有一個(gè)Looper對(duì)象與之關(guān)聯(lián)。應(yīng)用程序的主線程也是如此确封,在ActivityThread的main方法中除呵,會(huì)調(diào)用Looper.prepareMainLooper()方法再菊,來(lái)為主線程創(chuàng)建關(guān)聯(lián)的Looper對(duì)象。

private static Looper sMainLooper;  // guarded by Looper.class

...

/**
 * Initialize the current thread as a looper, marking it as an
 * application's main looper. The main looper for your application
 * is created by the Android environment, so you should never need
 * to call this function yourself.  See also: {@link #prepare()}
 */
public static void prepareMainLooper() {
    prepare(false);
    synchronized (Looper.class) {
        if (sMainLooper != null) {
            throw new IllegalStateException("The main Looper has already been prepared.");
        }
        sMainLooper = myLooper();
    }
}

/** Returns the application's main looper, which lives in the main thread of the application.
 */
public static Looper getMainLooper() {
    synchronized (Looper.class) {
        return sMainLooper;
    }
}

從上面的代碼也可以看出颜曾,prepareMainLooper()方法保證了主線程的Looper對(duì)象只會(huì)創(chuàng)建一次纠拔。這樣不管在主線程中,創(chuàng)建了多少個(gè)Handler對(duì)象來(lái)發(fā)送和處理各種消息泛豪,最終都會(huì)通過(guò)這個(gè)Looper對(duì)象來(lái)進(jìn)行消息的分發(fā)绿语,即調(diào)用Handler的dispatchMessage方法
在Looper.loop方法中有這樣一段代碼:

public static void loop() {
    ...

    for (;;) {
        ...

        // This must be in a local variable, in case a UI event sets the logger
        Printer logging = me.mLogging;
        if (logging != null) {
            logging.println(">>>>> Dispatching to " + msg.target + " " +
                    msg.callback + ": " + msg.what);
        }

        msg.target.dispatchMessage(msg);

        if (logging != null) {
            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
        }

        ...
    }
}

可以看到在dispatchMessage方法的前后,都有一個(gè)Printer類型的對(duì)象logging在打印信息候址。也就是說(shuō)吕粹,只需要比較開始和結(jié)束信息的打印時(shí)間,就可以得到dispatchMessage方法的耗時(shí)岗仑。如果主線程中存在耗時(shí)操作匹耕,那肯定會(huì)體現(xiàn)在dispatchMessage的執(zhí)行時(shí)間上。那么我們可以給主線程的Looper對(duì)象設(shè)置一個(gè)自定義的Printer荠雕,來(lái)實(shí)現(xiàn)對(duì)每一次dispatchMessage方法執(zhí)行耗時(shí)的監(jiān)控稳其。

Looper.getMainLooper().setMessageLogging(mainLooperPrinter);

并在mainLooperPrinter中判斷start和end,來(lái)獲取主線程dispatch該message的開始和結(jié)束時(shí)間炸卑,并判定該時(shí)間超過(guò)閾值(如2000毫秒)為主線程卡慢發(fā)生既鞠,此時(shí)就通過(guò)子線程dump出卡慢發(fā)生時(shí)的各種信息,提供開發(fā)者分析性能瓶頸盖文。

...
@Override
public void println(String x) {
    if (!mStartedPrinting) {
        mStartTimeMillis = System.currentTimeMillis();
        mStartThreadTimeMillis = SystemClock.currentThreadTimeMillis();
        mStartedPrinting = true;
    } else {
        final long endTime = System.currentTimeMillis();
        mStartedPrinting = false;
        if (isBlock(endTime)) {
            notifyBlockEvent(endTime);
        }
    }
}

private boolean isBlock(long endTime) {
    return endTime - mStartTimeMillis > mBlockThresholdMillis;
}
...

核心流程圖如下:


image.png

具體的監(jiān)控流程可以歸納為如下步驟:

  1. 首先通過(guò)Looper.getMainLooper().setMessageLogging()為主線程的Looper對(duì)象設(shè)置自定義的Printer實(shí)現(xiàn)類來(lái)打印輸出logging嘱蛋。這樣在每次執(zhí)行dispatchMessage方法前后都會(huì)調(diào)用我們自定義的Printer類。
  2. 在自定義的Printer類的println方法中五续,通過(guò)匹配字符串洒敏,如果匹配到">>>>> Dispatching to ",則開始在子線程去執(zhí)行獲取當(dāng)前主線程堆棧信息的任務(wù)疙驾,這個(gè)任務(wù)同時(shí)會(huì)獲取當(dāng)前的一些場(chǎng)景信息凶伙,比如內(nèi)存、CPU和網(wǎng)絡(luò)狀態(tài)等信息它碎。
  3. 如果在指定的時(shí)間閾值內(nèi)函荣,再次調(diào)用println方法匹配到了“<<<<< Finished to ”,就說(shuō)明dispatchMessage方法的耗時(shí)正常扳肛,沒(méi)有發(fā)生卡頓傻挂,那我們就可以將子線程的任務(wù)取消掉。

應(yīng)用發(fā)生卡頓時(shí)敞峭,BlockCanary除了能在開發(fā)調(diào)試時(shí)提供信息界面讓開發(fā)和測(cè)試人員直接看到卡頓原因之外踊谋,其最大的作用就是在App發(fā)布到線上后,也可以進(jìn)行大范圍的Log采集和分析旋讹,主要是從兩個(gè)維度進(jìn)行分析:一是卡頓時(shí)間殖蚕,二是根據(jù)相同堆棧出現(xiàn)的次數(shù)來(lái)對(duì)卡頓原因進(jìn)行排序和歸類轿衔。下圖演示了實(shí)際開發(fā)調(diào)試時(shí),BlockCanary的卡頓信息提示頁(yè)面


image.png

BlockCanary的優(yōu)點(diǎn):
非侵入式
方便精準(zhǔn)睦疫,能定位到具體的某一行代碼

BlockCanary的缺陷:

BlockCanary監(jiān)控的是dispatchMessage方法的耗時(shí)害驹,而dispatchMessage方法執(zhí)行的是一個(gè)相對(duì)完成的流程,里面可能涉及到多個(gè)方法的串行執(zhí)行蛤育。這樣會(huì)導(dǎo)致在發(fā)生卡頓的周期內(nèi)宛官,應(yīng)用確實(shí)發(fā)生了卡頓,但是獲取到的卡頓信息可能會(huì)不準(zhǔn)確瓦糕。也就是說(shuō)底洗,最后獲取到的堆棧信息只是一個(gè)表象,并不是真正導(dǎo)致卡頓的原因咕娄。先看看如下示意圖:
image.png
假設(shè)在dispatchMessage方法處理過(guò)程中亥揖,順序執(zhí)行了A、B圣勒、C三個(gè)方法费变,當(dāng)整個(gè)執(zhí)行過(guò)程的耗時(shí)超過(guò)指定的閾值(假設(shè)是3秒)時(shí),可以看出圣贸,實(shí)際上A挚歧、B、C三個(gè)方法的耗時(shí)分別是2300ms吁峻、500ms和200ms滑负。但是因?yàn)榉椒ˋ和B已經(jīng)執(zhí)行完畢,我們只能得到正在執(zhí)行的方法C的堆棧信息锡搜。但事實(shí)上它并不是導(dǎo)致卡頓的真正原因橙困。

還有一個(gè)問(wèn)題是瞧掺,調(diào)用Printer的println方法時(shí)耕餐,會(huì)涉及到字符串拼接,所以在短時(shí)間內(nèi)處理大量任務(wù)辟狈,類似快速滑動(dòng)recyclerView導(dǎo)致頁(yè)面刷新等操作時(shí)肠缔,會(huì)增加內(nèi)存消耗,甚至觸發(fā)GC等哼转。

AspectJ

AspectJ是一個(gè)實(shí)現(xiàn)AOP的框架明未。在Android平臺(tái)上,常用的是Hujiang團(tuán)隊(duì)開源的AspectJ插件壹蔓。它的工作原理是:通過(guò)Gradle Transform趟妥,在編譯期間class文件生成后至dex文件生成前,遍歷并匹配所有符合AspectJ定義的切點(diǎn)處佣蓉,插入定義好的代碼披摄,從而處理相應(yīng)的邏輯亲雪。AspectJ常用于打印方法的耗時(shí),權(quán)限檢查疚膊,統(tǒng)計(jì)按鈕事件的點(diǎn)擊次數(shù)等义辕。在Android上的應(yīng)用主要是做性能監(jiān)控,基于注解的數(shù)據(jù)埋點(diǎn)寓盗。具體的使用教程可以參考Android AspectJ詳解AspectJ AOP教程:實(shí)現(xiàn)Android基于注解無(wú)侵入埋點(diǎn)灌砖、性能監(jiān)控
另外傀蚌,常見(jiàn)的基于AspectJ實(shí)現(xiàn)的有大神JakeWharton的Hugo框架基显。
AspectJ的弊端:由于其基于規(guī)則,所以切入點(diǎn)相對(duì)固定善炫,對(duì)于字節(jié)碼文件的操作自由度以及開發(fā)的掌握度都要打一定的折扣续镇。,并且它會(huì)額外生成一些包裝代碼销部,對(duì)性能以及包大小都有一定的負(fù)擔(dān)摸航。
雖然AspectJ非常強(qiáng)大,但是它也只能實(shí)現(xiàn)50%的字節(jié)碼操作場(chǎng)景舅桩,如果要實(shí)現(xiàn)100%的字節(jié)碼操作場(chǎng)景酱虎,就需要使用ASM。

ASM

ASM基本上可以實(shí)現(xiàn)任何對(duì)字節(jié)碼的操作擂涛,也就是自由度和開發(fā)的掌握度很高读串。它提供了訪問(wèn)者模式來(lái)訪問(wèn)字節(jié)碼文件,并且只注入我們想要注入的代碼撒妈。ASM是諸多JVM語(yǔ)言欽定的字節(jié)碼生成庫(kù)恢暖,它在效率和性能方面的優(yōu)勢(shì)要遠(yuǎn)超其他的字節(jié)碼操作庫(kù)如AspectJ。
ASM的優(yōu)點(diǎn):

  • 適宜處理簡(jiǎn)單類的修改
  • 學(xué)習(xí)成本較低
  • 代碼量較少
    ASM的缺點(diǎn):
  • 處理大量信息會(huì)使代碼變得復(fù)雜
  • 代碼難以復(fù)用
Lancet

Lancet是一個(gè)輕量級(jí)的Android AOP框架狰右。由eleme團(tuán)隊(duì)開源分享杰捂。它的特點(diǎn)是:

  • 編譯速度快,并且支持增量編譯棋蚌。
  • 簡(jiǎn)潔的API嫁佳,幾行Java代碼就能完成注入需求。
  • 沒(méi)有任何多余代碼插入APK.
  • 支持用于SDK谷暮,可以在SDK編寫注入代碼來(lái)修改依賴SDK的App蒿往。
DroidAssist

DroidAssist由滴滴團(tuán)隊(duì)開源,是一個(gè)輕量級(jí)的Android字節(jié)碼編輯插件湿弦,基于Javassist對(duì)字節(jié)碼操作瓤漏,根據(jù)xml配置class文件,以達(dá)到對(duì)class文件進(jìn)行動(dòng)態(tài)修改的效果。與其他AOP方案不同蔬充。DroidAssist提供了一種更加輕量俯在,簡(jiǎn)單易用,無(wú)侵入娃惯,可配置化的字節(jié)碼操作方式跷乐。你不需要Java字節(jié)碼的相關(guān)知識(shí),只需要在xml插件配置中添加簡(jiǎn)單的Java代碼即可實(shí)現(xiàn)類似AOP的功能趾浅,同時(shí)不需要引入其他額外的依賴愕提。
DroidAssist的特點(diǎn)

  • 靈活的配置化方式,使得一個(gè)配置就可以處理項(xiàng)目中所有的 class 文件皿哨。
  • 豐富的字節(jié)碼處理功能浅侨,針對(duì) Android 移動(dòng)端的特點(diǎn)提供了例如代碼替換,添加try catch证膨,方法耗時(shí)等功能如输。
  • 簡(jiǎn)單易用,只需要依賴一個(gè)插件央勒,處理過(guò)程以及處理后的代碼中也不需要添加額外的依賴不见。
  • 處理速度較快,只占用較少的編譯時(shí)間崔步。

最后稳吮,卡頓優(yōu)化還會(huì)涉及到布局和繪制方面的優(yōu)化,慢慢把坑補(bǔ)上吧井濒。

本文參考:
Android開發(fā)高手課:06 | 卡頓優(yōu)化(下):如何監(jiān)控應(yīng)用卡頓灶似?
BlockCanary — 輕松找出Android App界面卡頓元兇
深入探索編譯插樁技術(shù)(二、AspectJ)
深入探索編譯插樁技術(shù)(四瑞你、ASM 探秘)
編譯插樁操縱字節(jié)碼酪惭,實(shí)現(xiàn)不可能完成的任務(wù)
DroidAssist
Android AspectJ詳解
AOP在Android中最佳用法
AspectJ AOP教程:實(shí)現(xiàn)Android基于注解無(wú)侵入埋點(diǎn)、性能監(jiān)控

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末者甲,一起剝皮案震驚了整個(gè)濱河市春感,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌过牙,老刑警劉巖甥厦,帶你破解...
    沈念sama閱讀 222,252評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異寇钉,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)舶赔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門扫倡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事撵溃【卫穑” “怎么了?”我有些...
    開封第一講書人閱讀 168,814評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵缘挑,是天一觀的道長(zhǎng)集歇。 經(jīng)常有香客問(wèn)我,道長(zhǎng)语淘,這世上最難降的妖魔是什么诲宇? 我笑而不...
    開封第一講書人閱讀 59,869評(píng)論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮惶翻,結(jié)果婚禮上姑蓝,老公的妹妹穿的比我還像新娘。我一直安慰自己吕粗,他們只是感情好纺荧,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,888評(píng)論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著颅筋,像睡著了一般宙暇。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上议泵,一...
    開封第一講書人閱讀 52,475評(píng)論 1 312
  • 那天客给,我揣著相機(jī)與錄音,去河邊找鬼肢簿。 笑死靶剑,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的池充。 我是一名探鬼主播桩引,決...
    沈念sama閱讀 41,010評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼收夸!你這毒婦竟也來(lái)了坑匠?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,924評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤卧惜,失蹤者是張志新(化名)和其女友劉穎厘灼,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體咽瓷,經(jīng)...
    沈念sama閱讀 46,469評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡设凹,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,552評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了茅姜。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片闪朱。...
    茶點(diǎn)故事閱讀 40,680評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出奋姿,到底是詐尸還是另有隱情锄开,我是刑警寧澤,帶...
    沈念sama閱讀 36,362評(píng)論 5 351
  • 正文 年R本政府宣布称诗,位于F島的核電站萍悴,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏寓免。R本人自食惡果不足惜癣诱,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,037評(píng)論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望再榄。 院中可真熱鬧狡刘,春花似錦、人聲如沸困鸥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)疾就。三九已至澜术,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間猬腰,已是汗流浹背鸟废。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留姑荷,地道東北人盒延。 一個(gè)月前我還...
    沈念sama閱讀 49,099評(píng)論 3 378
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像鼠冕,于是被迫代替她去往敵國(guó)和親添寺。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,691評(píng)論 2 361