Android 卡頓方案研究

Android 卡頓研究

[TOC]

穩(wěn)定化实檀,不是說說而已

基礎(chǔ)概念

這里主要是根據(jù)張紹文老師的文章做的筆記,根據(jù)張紹文老師的文筆去實(shí)踐具體卡頓監(jiān)控的內(nèi)容

散列知識點(diǎn)

  • JVM中的線程切換大概花費(fèi)CPU 20000個(gè)時(shí)鐘周期

CPU

這里CPU需要單獨(dú)搞出來提一下抓督,卡頓優(yōu)化前需要搞清楚CPU是什么,能干什么束亏,正在干什么铃在,然后才是“什么”這個(gè)區(qū)間里面應(yīng)用程序此時(shí)的參數(shù)是否合理,優(yōu)化的空間又是多少

查看一個(gè)CPU的參數(shù)需要看CPU的頻率碍遍,核心等參數(shù),具體參考 Wiki

這里就僅僅點(diǎn)相對重要的一些參數(shù)含義

  • 時(shí)鐘周期:CPU每秒可以完成幾個(gè)時(shí)鐘周期,如
    2.4GHz = 2.4 * 10^9
    可以完成這么多個(gè)時(shí)鐘周期

  • 機(jī)器周期:主存中讀取一個(gè)指令字的最短時(shí)間(由于CPU內(nèi)部的操作速度較快.而CPU訪問一次主存所花的時(shí)間較長钠惩,因此機(jī)器周期通常用主存中讀取一個(gè)指令字的最短時(shí)間來規(guī)定秕狰。),所以 機(jī)器周期 = 時(shí)鐘周期 * n(n >= 1)

  • 指令周期:完成一個(gè)指令需要的時(shí)間东跪,一般由 幾個(gè)或者一個(gè)機(jī)器周期組成祝沸,相當(dāng)于 指令周期 = 機(jī)器周期 * n(n >= 1)

方法論

指標(biāo)

  • CPU使用率
adb查看CPU.png

如果 CPU 使用率長期大于 60% ,表示系統(tǒng)處于繁忙狀態(tài),就需要進(jìn)一步分析用戶時(shí)間和系統(tǒng)時(shí)間的比例越庇。對于普通應(yīng)用程序罩锐,系統(tǒng)時(shí)間不會長期高于 30%,如果超過這個(gè)值卤唉,就得考慮是否I/O調(diào)用過多或者鎖調(diào)用的過于頻繁的問題涩惑。利用Android Studio的profile也能查看CPU的使用率

  • CPU飽和度

CPU 飽和度反映的是線程排隊(duì)等待 CPU 的情況,也就是 CPU 的負(fù)載情況桑驱。

CPU 飽和度首先會跟應(yīng)用的線程數(shù)有關(guān)竭恬,如果啟動(dòng)的線程過多跛蛋,易導(dǎo)致系統(tǒng)不斷地切換執(zhí)行的線程,把大量的時(shí)間浪費(fèi)在上下文切換,要知道每一次 CPU 上下文切換都需要刷新寄存器和計(jì)數(shù)器,至少需要幾十納秒的時(shí)間痊硕。

可以通過vmstat命令查看CPU上下文切換次數(shù)

proc/self/sched:
    nr_voluntary_switches:主動(dòng)上下文切換次數(shù)赊级,因?yàn)榫€程無法獲取資源導(dǎo)致上下文切換,最普遍的就是IO
    nr_involuntary_switches:被動(dòng)上下文切換次數(shù)岔绸,線程被系統(tǒng)強(qiáng)制調(diào)度導(dǎo)致上下文切換理逊,例如大量線程在搶占CPU
    se.statistics.iowait_count:IO 等待次數(shù)
    se.statistics.iowait_sum:IO 等待時(shí)間

此外也可以通過 uptime 命令可以檢查 CPU 在 1 分鐘、5 分鐘和 15 分鐘內(nèi)的平均負(fù)載盒揉。比如一個(gè) 4 核的 CPU晋被,如果當(dāng)前平均負(fù)載是 8,這意味著每個(gè) CPU 上有一個(gè)線程在運(yùn)行刚盈,還有一個(gè)線程在等待羡洛。一般平均負(fù)載建議控制在“0.7 × 核數(shù)”以內(nèi)。

00:02:39 up 7 days, 46 min,  0 users,  
load average: 13.91, 14.70, 14.32

另外一個(gè)會影響 CPU 飽和度的是線程優(yōu)先級藕漱,線程優(yōu)先級會影響 Android 系統(tǒng)的調(diào)度策略欲侮,它主要由 nice 和 cgroup 類型共同決定。nice 值越低肋联,搶占 CPU 時(shí)間片的能力越強(qiáng)威蕉。當(dāng) CPU 空閑時(shí),線程的優(yōu)先級對執(zhí)行效率的影響并不會特別明顯牺蹄,但在 CPU 繁忙的時(shí)候忘伞,線程調(diào)度會對執(zhí)行效率有非常大的影響。

關(guān)于線程優(yōu)先級沙兰,你需要注意是否存在高優(yōu)先級的線程空等低優(yōu)先級線程氓奈,例如主線程等待某個(gè)后臺線程的鎖。從應(yīng)用程序的角度來看鼎天,無論是用戶時(shí)間舀奶、系統(tǒng)時(shí)間,還是等待 CPU 的調(diào)度斋射,都是程序運(yùn)行花費(fèi)的時(shí)間育勺。

市場調(diào)研

Traceview 和 systrace 都是我們比較熟悉的排查卡頓的工具,從實(shí)現(xiàn)上這些工具分為兩個(gè)流派罗岖。

第一個(gè)流派是 instrument涧至。獲取一段時(shí)間內(nèi)所有函數(shù)的調(diào)用過程,可以通過分析這段時(shí)間內(nèi)的函數(shù)調(diào)用流程桑包,再進(jìn)一步分析待優(yōu)化的點(diǎn)南蓬。

第二個(gè)流派是 sample。有選擇性或者采用抽樣的方式觀察某些函數(shù)調(diào)用過程,可以通過這些有限的信息推測出流程中的可疑點(diǎn)赘方,然后再繼續(xù)細(xì)化分析烧颖。

根據(jù)流派,對目前市場上的性能監(jiān)控工具做一些調(diào)研和使用窄陡,包括但不限于官方提供的性能監(jiān)控工具炕淮,如systrace,Matrix等跳夭,關(guān)于Android上Systrace的使用可以參考我之前寫過的一個(gè)blog里面有提到過如何使用Android 性能優(yōu)化

選擇哪種工具涂圆,需要看具體的場景。如果需要分析 Native 代碼的耗時(shí)优妙,可以選擇 Simpleperf乘综;如果想分析系統(tǒng)調(diào)用憎账,可以選擇 systrace套硼;如果想分析整個(gè)程序執(zhí)行流程的耗時(shí),可以選擇 Traceview 或者插樁版本的 systrace胞皱。

對目前市場上的一些性能監(jiān)控框架做基本調(diào)研邪意,如DoraemonKitMatrix反砌,BlockCanary

BlockCanary & DoraemonKit

這里之所以把兩個(gè)都放到一起雾鬼,是因?yàn)榈蔚蔚亩呃睞夢的卡頓檢測其實(shí)就是blockCanary,實(shí)現(xiàn)很簡單宴树,但是思路很巧妙~

想要檢測卡頓策菜,其實(shí)就是檢測主線程的運(yùn)行情況,為什么這么說呢酒贬,因?yàn)槊恳粠秩緮?shù)據(jù)的創(chuàng)建又憨,就依托于主線程來創(chuàng)建,而想要保證每一幀CPU都能在16.7ms內(nèi)(這里僅限于60幀這種情況锭吨,如果是90或者120蠢莺,可以反推的哈~)完成工作,這樣就不會出現(xiàn)丟幀的現(xiàn)象零如,也就不會造成卡頓躏将,而我們就監(jiān)測每個(gè)而如何監(jiān)測主線程的運(yùn)行情況呢?這里需要知道安卓中的handler機(jī)制考蕾,通過檢測每次處理主線程消息的耗時(shí)情況祸憋,就能夠知道是否產(chǎn)生了卡頓,而在發(fā)生卡頓的時(shí)候肖卧,同時(shí)抓取此時(shí)主線程的堆棧蚯窥,那么就更能方便的定位到需要優(yōu)化的代碼。

BlockCanary核心的地方,主要分為兩個(gè)部分:

  • 檢測handleMessage
  • 主線程抓取堆棧的部分

handleMessage

在主線程Looper每次處理消息的過程中沟沙,通過hook主線程Looper每次處理消息的過程河劝,在處理消息之前記錄一個(gè)時(shí)間戳,處理完消息之后記錄一個(gè)時(shí)間戳矛紫,那么兩個(gè)時(shí)間的差值赎瞎,就是處理一條消息所花費(fèi)的時(shí)間。通過給這個(gè)時(shí)間設(shè)置閾值颊咬,如:處理時(shí)間 > 閾值時(shí)間(430ms > 200ms)那么就認(rèn)為是發(fā)生了卡頓

Looper處理消息.png

這里的hook其實(shí)非常簡單务甥,因?yàn)閒ramework給咱們預(yù)留了這樣的口子,可以看下在handlemessage這里的源碼:

public void loop(){
    for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }

            // This must be in a local variable, in case a UI event sets the logger
            final Printer logging = me.mLogging;
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }
            ......
            final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
            final long dispatchEnd;
            try {
                msg.target.dispatchMessage(msg);
                dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }

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

            msg.recycleUnchecked();
        }
}

......

/**
 * Control logging of messages as they are processed by this Looper.  If
 * enabled, a log message will be written to <var>printer</var>
 * at the beginning and ending of each message dispatch, identifying the
 * target Handler and message contents.
 *
 * @param printer A Printer object that will receive log messages, or
 * null to disable message logging.
 */
public void setMessageLogging(@Nullable Printer printer) {
    mLogging = printer;
}

可見們只需要手動(dòng)調(diào)用Looper.setMessageLogging方法就能給線程looper對象設(shè)置printer對象喳篇,在每次處理消息的時(shí)候敞临,通過監(jiān)聽printer的println回調(diào),解析出內(nèi)容麸澜,就能知道知道分發(fā)消息的開始和結(jié)束了

//Printer 接口
public interface Printer {
    /**
     * Write a line of text to the output.  There is no need to terminate
     * the given string with a newline.
     */
    void println(String x);
}

通過過濾printer.println打印的內(nèi)容挺尿,判斷是否是在消息分發(fā)處理相關(guān)的內(nèi)容,然后進(jìn)行時(shí)間差的計(jì)算炊邦,來判斷卡頓是否發(fā)生

//LooperPrinter
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(MonitorConstants.LOG_TAG + " origin == this");
            }
        }

        if (!isHasChecked) {
            isValid = x.charAt(0) == '>' || x.charAt(0) == '<';
            isHasChecked = true;
            if (!isValid) {
                InsectLogger.ne("[println] Printer is inValid! x:%s", x);
            }
        }

        if (isValid) {
            dispatch(x.charAt(0) == '>', x);
        }

    }
}

那么依據(jù)主線程卡頓的監(jiān)控就已經(jīng)完成了编矾,接下來是對于卡頓問題的定位,也就是對主線程堆棧的抓取

dumpStack

這里不完全參照blockCanary的實(shí)現(xiàn)馁害,但是大家都是為了解決能夠抓取到問題發(fā)生的堆棧窄俏,這里先說一下對于主線程堆棧dump需要關(guān)注的問題。不同的抓取策略也是為了解決這個(gè)問題碘菜,此處先不考慮對性能帶來的影響
假設(shè)此時(shí)發(fā)生了卡頓凹蜈,那么在調(diào)用getStackTrace的時(shí)候,這時(shí)候虛擬機(jī)中所跟蹤的堆棧中會把當(dāng)前記錄的一些堆棧返回忍啸。通過在發(fā)生卡頓的時(shí)候仰坦,dump出當(dāng)前的堆棧,記錄下來吊骤,再追溯問題的時(shí)候直接看存儲下來的堆棧信息缎岗,那么定位問題就會方便很多,而實(shí)際情況下并不能如此理想白粉,因?yàn)閺腣M中取出的堆棧dalvik.system.VMStack#getThreadStackTrace返回的數(shù)據(jù)是未知的传泊,不能保證里面到底有多少內(nèi)容,可能只有一部分鸭巴,這樣就可能會遺漏真正的問題所在眷细,可以參考下圖~

函數(shù)執(zhí)行火圖?.png

可以看到真正有問題的函數(shù)其實(shí)是FunctionA-1,而如果撈出來的堆棧只有FunctionA-2或者A-3的話鹃祖,當(dāng)然可以優(yōu)化A-3溪椎,但是會漏掉真正發(fā)生問題的函數(shù)。所以對于堆棧的抓取,基于VMStack抓取堆棧的方式下校读,筆者思考了兩種方案來解決這樣的問題沼侣,這兩種應(yīng)該也是市面上基于VMStack方式的大概方案,再深入往VM中去研究感覺可以有歉秫,但是不推薦蛾洛,因?yàn)槌杀靖撸一貓?bào)的話不太會有預(yù)期中的高雁芙。

周期性Dump

通過每個(gè)一段時(shí)間從VM中獲取主線程的堆棧轧膘,在發(fā)生卡頓的時(shí)候,過濾出時(shí)間兔甘,然后直接取出這段時(shí)間內(nèi)的堆棧來進(jìn)行問題排查谎碍。


周期Dmp.png

在實(shí)現(xiàn)的時(shí)候需要注意的一些小細(xì)節(jié):

  • 循環(huán)隊(duì)列
  • 堆棧去重
  • 時(shí)間區(qū)間篩選
起止Dump

這里可以“忽略”多線程的特性,因?yàn)槲覀冴P(guān)注的僅僅是主線程洞焙,那么只需要在消息分發(fā)之初dump一次堆棧蟆淀,然后再消息處理之后再dump一次堆棧,這樣既能在dump出來的堆棧中發(fā)現(xiàn)可能存在的問題闽晦,同時(shí)又能自行推斷這中間的執(zhí)行過程來觀測代碼中出現(xiàn)的問題扳碍。當(dāng)然不可缺少一個(gè)代碼耗時(shí)檢測的小工具~

起止Dmp.png

Matrix

關(guān)于matrix-traceCanary原理

關(guān)于matrix解剖提岔,需要先了解定義仙蛉,再根據(jù)具體代碼進(jìn)行分析,最后根據(jù)代碼梳理出實(shí)現(xiàn)的思路

卡頓定義

微信開發(fā)者對于卡頓的定義碱蒙,很簡單荠瘪,很清晰,很明了赛惩,這里就cv過來了哀墓,一定要仔細(xì)讀對卡頓的定義

什么是卡頓,很多人能馬上聯(lián)系到的是幀率 FPS (每秒顯示幀數(shù))喷兼。那么多低的 FPS 才是卡頓呢篮绰?又或者低 FPS 真的就是卡頓嗎?(以下 FPS 默認(rèn)指平均幀率)

其實(shí)并非如此季惯,舉個(gè)例子吠各,游戲玩家通常追求更流暢的游戲畫面體驗(yàn)一般要達(dá)到 60FPS 以上,但我們平時(shí)看到的大部分電影或視頻 FPS 其實(shí)不高勉抓,一般只有 25FPS ~ 30FPS贾漏,而實(shí)際上我們也沒有覺得卡頓。 在人眼結(jié)構(gòu)上看藕筋,當(dāng)一組動(dòng)作在 1 秒內(nèi)有 12 次變化(即 12FPS)纵散,我們會認(rèn)為這組動(dòng)作是連貫的;而當(dāng)大于 60FPS 時(shí),人眼很難區(qū)分出來明顯的變化伍掀,所以 60FPS 也一直作為業(yè)界衡量一個(gè)界面流暢程度的重要指標(biāo)掰茶。一個(gè)穩(wěn)定在 30FPS 的動(dòng)畫,我們不會認(rèn)為是卡頓的蜜笤,但一旦 FPS 很不穩(wěn)定符匾,人眼往往容易感知到。

FPS 低并不意味著卡頓發(fā)生瘩例,而卡頓發(fā)生 FPS 一定不高啊胶。 FPS 可以衡量一個(gè)界面的流程性,但往往不能很直觀的衡量卡頓的發(fā)生垛贤,這里有另一個(gè)指標(biāo)(掉幀程度)可以更直觀地衡量卡頓焰坪。

什么是掉幀(跳幀)? 按照理想幀率 60FPS 這個(gè)指標(biāo)聘惦,計(jì)算出平均每一幀的準(zhǔn)備時(shí)間有 1000ms/60 = 16.6667ms某饰,如果一幀的準(zhǔn)備時(shí)間超出這個(gè)值,則認(rèn)為發(fā)生掉幀善绎,超出的時(shí)間越長黔漂,掉幀程度越嚴(yán)重。假設(shè)每幀準(zhǔn)備時(shí)間約 32ms禀酱,每次只掉一幀炬守,那么 1 秒內(nèi)實(shí)際只刷新 30 幀,即平均幀率只有 30FPS剂跟,但這時(shí)往往不會覺得是卡頓减途。反而如果出現(xiàn)某次嚴(yán)重掉幀(>300ms),那么這一次的變化曹洽,通常很容易感知到鳍置。所以界面的掉幀程度,往往可以更直觀的反映出卡頓送淆。

流暢性

綜上所述税产,其實(shí)可以明白對于卡頓的定義,衡量流暢性的指標(biāo)可以簡單理解為:

  • 在用戶有操作的前提下
  • 平均掉幀率偷崩,只有在某一時(shí)刻發(fā)生的掉幀情況遠(yuǎn)遠(yuǎn)大于其他時(shí)刻辟拷,那么才界定為卡頓,這也是上面說到的界面的掉幀程度环凿,才更直觀的反映出卡頓
Best Normal Middle High Frozen
[0:3) [3:9) [9:24) [24:42) [42:∞)

Code實(shí)現(xiàn)

關(guān)于反射 Choreographer 來做到如何監(jiān)測用戶觸發(fā)后開始計(jì)算平均幀率

關(guān)于Choreographer的知識相關(guān)梧兼,這里不做贅述,只根據(jù)實(shí)現(xiàn)原理來對使用的地方做說明

  1. 用戶觸發(fā)刷新

了解下源碼中callbackType的含義

    /**
     * Callback type: Input callback.  Runs first.
     * @hide
     */
    public static final int CALLBACK_INPUT = 0;

    /**
     * Callback type: Animation callback.  Runs before traversals.
     * @hide
     */
    @TestApi
    public static final int CALLBACK_ANIMATION = 1;

    /**
     * Callback type: Traversal callback.  Handles layout and draw.  Runs
     * after all other asynchronous messages have been handled.
     * @hide
     */
    public static final int CALLBACK_TRAVERSAL = 2;

顯而易見智听,CALLBACK_INPUT都已經(jīng)注釋好了羽杰,首先run的是這個(gè)類型的回調(diào)渡紫,然后我們平時(shí)注冊的又是什么樣子呢?

/**
 * Posts a frame callback to run on the next frame.
 * <p>
 * The callback runs once then is automatically removed.
 * </p>
 *
 * @param callback The frame callback to run during the next frame.
 *
 * @see #postFrameCallbackDelayed
 * @see #removeFrameCallback
 */
public void postFrameCallback(FrameCallback callback) {
    postFrameCallbackDelayed(callback, 0);
}

/**
 * Posts a frame callback to run on the next frame after the specified delay.
 * <p>
 * The callback runs once then is automatically removed.
 * </p>
 *
 * @param callback The frame callback to run during the next frame.
 * @param delayMillis The delay time in milliseconds.
 *
 * @see #postFrameCallback
 * @see #removeFrameCallback
 */
public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
    if (callback == null) {
        throw new IllegalArgumentException("callback must not be null");
    }

    postCallbackDelayedInternal(CALLBACK_ANIMATION,
            callback, FRAME_CALLBACK_TOKEN, delayMillis);
}

注冊類型的callbackType為ANIMATION的考赛,而ANIMATION的type又是什么時(shí)候回調(diào)呢惕澎?

void doFrame(long frameTimeNanos, int frame) {
    final long startNanos;
    synchronized (mLock) {
    ......
    try {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
        AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);
        //優(yōu)先執(zhí)行CALLBACK_INPUT類型鏈表里面的回調(diào)
        mFrameInfo.markInputHandlingStart();
        doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);

        mFrameInfo.markAnimationsStart();
        doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);

        mFrameInfo.markPerformTraversalsStart();
        doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);

        doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
    } finally {
        AnimationUtils.unlockAnimationClock();
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
    ......
}

可見是在執(zhí)行完優(yōu)先級最高的輸入類型的回調(diào)才會回調(diào)ANIMATION的(注意這里提供的兩個(gè)參數(shù),第一個(gè)是執(zhí)行該frame的時(shí)間戳颜骤,第二個(gè)是當(dāng)前幀號唧喉,是native調(diào)用,在DisplayEventReceiver事件中收到后維護(hù)的一個(gè)成員變量忍抽,具體實(shí)現(xiàn)類也在Choreagrapher中)八孝,而顯然不能夠符合我們的要求,我們是期望在用戶有操作的情況下是否發(fā)生丟幀情況

Choreographer類結(jié)構(gòu).png

而如何計(jì)算input時(shí)機(jī)的幀率呢鸠项? 勢必需要在input類型中添加自己實(shí)現(xiàn)的callback干跛,在animation開始的執(zhí)行的時(shí)候,標(biāo)識為input執(zhí)行結(jié)束

Hook時(shí)機(jī).png

好了祟绊,原理分析完畢楼入,接下來看一下在Matrix中帶佬如何實(shí)現(xiàn)的,核心類主要是com.tencent.matrix.trace.core.UIThreadMonitor

初始化中先拿到需要hook的方法牧抽,然后模擬順序進(jìn)行執(zhí)行

public void init(TraceConfig config) {
    ......
    //反射同步鎖對象和對應(yīng)doFrame的所有回調(diào)數(shù)組鏈表對象
    choreographer = Choreographer.getInstance();
    callbackQueueLock = reflectObject(choreographer, "mLock");
    callbackQueues = reflectObject(choreographer, "mCallbackQueues");

    //先拿到添加對應(yīng)回調(diào)的可執(zhí)行反射方法
    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");
        ......
    this.isInit = true;
    ......
}

通過上面分析可得嘉熊,doframe的回調(diào)執(zhí)行是順序執(zhí)行下來,也就是說一個(gè)類型的callback執(zhí)行結(jié)束時(shí)間扬舒,就是下一個(gè)類型的開始時(shí)間阐肤,那么在addCallback的時(shí)機(jī)也是如此,最開始要添加的則是input類型回調(diào)

public void init(TraceConfig config) {
    ......
    choreographer = Choreographer.getInstance();
    callbackQueueLock = ReflectUtils.reflectObject(choreographer, "mLock", new Object());
    //反射獲取回調(diào)的數(shù)組鏈表對象呼巴,可以理解為單object的hashMap  數(shù)組+鏈表實(shí)現(xiàn)
    callbackQueues = ReflectUtils.reflectObject(choreographer, "mCallbackQueues", null);
    if (null != callbackQueues) {
        addInputQueue = ReflectUtils.reflectMethod(callbackQueues[CALLBACK_INPUT], ADD_CALLBACK, long.class, Object.class, Object.class);
        addAnimationQueue = ReflectUtils.reflectMethod(callbackQueues[CALLBACK_ANIMATION], ADD_CALLBACK, long.class, Object.class, Object.class);
        addTraversalQueue = ReflectUtils.reflectMethod(callbackQueues[CALLBACK_TRAVERSAL], ADD_CALLBACK, long.class, Object.class, Object.class);
    }
    //主要是拿vsync回調(diào)上來的信號開始繪制的時(shí)間戳泽腮,可以分析出來丟幀數(shù),源碼也是這么干的
    vsyncReceiver = ReflectUtils.reflectObject(choreographer, "mDisplayEventReceiver", null);
    //產(chǎn)生vsync信號的時(shí)間戳
    frameIntervalNanos = ReflectUtils.reflectObject(choreographer, "mFrameIntervalNanos", Constants.DEFAULT_FRAME_DURATION);

    LooperMonitor.register(new LooperMonitor.LooperDispatchListener() {
        @Override
        public boolean isValid() {
            return isAlive;
        }

        @Override
        public void dispatchStart() {
            super.dispatchStart();
            UIThreadMonitor.this.dispatchBegin();
        }

        @Override
        public void dispatchEnd() {
            super.dispatchEnd();
            UIThreadMonitor.this.dispatchEnd();
        }

    });
    ......
}

public synchronized void onStart() {
    ......
    if (!isAlive) {
        this.isAlive = true;
        synchronized (this) {
            MatrixLog.i(TAG, "[onStart] callbackExist:%s %s", Arrays.toString(callbackExist), Utils.getStack());
            callbackExist = new boolean[CALLBACK_LAST + 1];
        }
        //為三種callback增加狀態(tài)維護(hù)數(shù)組
        queueStatus = new int[CALLBACK_LAST + 1];
        //為三種callback增加耗時(shí)數(shù)組
        queueCost = new long[CALLBACK_LAST + 1];
        //首次添加input類型callback
        addFrameCallback(CALLBACK_INPUT, this, true);
    }
}

可以看到衣赶,在添加input類型的回調(diào)時(shí),傳的是自己厚满,那么來分析一下接下來的run實(shí)現(xiàn)

@Override
public void run() {
    //來自vsync信號開始
    doFrameBegin(token);
    //維護(hù)input類型數(shù)組們的狀態(tài)
    doQueueBegin(CALLBACK_INPUT);
    //animation回調(diào)注冊回調(diào)
    addFrameCallback(CALLBACK_ANIMATION, new Runnable() {

        @Override
        public void run() {
            //animation回調(diào)府瞄,input結(jié)束
            doQueueEnd(CALLBACK_INPUT);
            doQueueBegin(CALLBACK_ANIMATION);
        }
    }, true);

    addFrameCallback(CALLBACK_TRAVERSAL, new Runnable() {

        @Override
        public void run() {
            //traversal類型回調(diào),animation結(jié)束
            doQueueEnd(CALLBACK_ANIMATION);
            doQueueBegin(CALLBACK_TRAVERSAL);
        }
    }, true);
}

可以看到碘箍,如此就能得到一個(gè)Vsync信號過來的輪回遵馆,但是走到這里只能完成一次,matrix如何把每一次串起來的咧丰榴?

還記得上面初始化的時(shí)候注冊looper監(jiān)聽货邓,每次消息的處理開始和結(jié)束都會激活一次dispatchStart和dispatchEnd,start這里就不分析了四濒,其實(shí)就是往外回調(diào)换况,主要是end

private void dispatchEnd() {
    ......
    //在第一次Vsync開始的時(shí)候賦值為true职辨,直接進(jìn)來
    if (isVsyncFrame) {
        doFrameEnd(token);
        intendedFrameTimeNs = getIntendedFrameTimeNs(startNs);
    }
    ......
    //來自一次vsync信號結(jié)束
    this.isVsyncFrame = false;
}

private void doFrameEnd(long token) {
    //下一次input開始,上一次的traversal結(jié)束
    doQueueEnd(CALLBACK_TRAVERSAL);
    ......
    //開啟下一次input輪回
    addFrameCallback(CALLBACK_INPUT, this, true);
}

可以看到戈二,這里才是真正的結(jié)束舒裤,一個(gè)完整的Choreographer循環(huán)~

卡頓策略

Matrix的文檔里面已經(jīng)非常清楚的用文字描述一個(gè)卡頓是如何產(chǎn)生的,以及卡頓的定義

FPS 低并不意味著卡頓發(fā)生觉吭,而卡頓發(fā)生 FPS 一定不高腾供。 FPS 可以衡量一個(gè)界面的流程性,但往往不能很直觀的衡量卡頓的發(fā)生鲜滩,這里有另一個(gè)指標(biāo)(掉幀程度)可以更直觀地衡量卡頓伴鳖。

什么是掉幀(跳幀)? 按照理想幀率 60FPS 這個(gè)指標(biāo)徙硅,計(jì)算出平均每一幀的準(zhǔn)備時(shí)間有 1000ms/60 = 16.6667ms黎侈,如果一幀的準(zhǔn)備時(shí)間超出這個(gè)值,則認(rèn)為發(fā)生掉幀闷游,超出的時(shí)間越長峻汉,掉幀程度越嚴(yán)重。假設(shè)每幀準(zhǔn)備時(shí)間約 32ms脐往,每次只掉一幀休吠,那么 1 秒內(nèi)實(shí)際只刷新 30 幀,即平均幀率只有 30FPS业簿,但這時(shí)往往不會覺得是卡頓瘤礁。反而如果出現(xiàn)某次嚴(yán)重掉幀(>300ms),那么這一次的變化梅尤,通常很容易感知到柜思。所以界面的掉幀程度,往往可以更直觀的反映出卡頓巷燥。

然后分析下關(guān)于瞬時(shí)平均幀率的代碼需要重點(diǎn)關(guān)注的就是com.tencent.matrix.trace.tracer.FrameTracer這個(gè)類

在每一次doFrame的回調(diào)中去分析這個(gè)參數(shù)

@Override
public void doFrame(String focusedActivity, long startNs, long endNs, boolean isVsyncFrame, long intendedFrameTimeNs, long inputCostNs, long animationCostNs, long traversalCostNs) {
    if (isForeground()) {
        notifyListener(focusedActivity, startNs, endNs, isVsyncFrame, intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
    }
}


private void notifyListener(final String focusedActivity, final long startNs, final long endNs, final boolean isVsyncFrame,
                            final long intendedFrameTimeNs, final long inputCostNs, final long animationCostNs, final long traversalCostNs) {
    try {
        //計(jì)算丟幀赡盘,跟源碼的計(jì)算方式一致,上一個(gè)版本局部變量的聲明并沒有如此直觀缰揪,這個(gè)版本改了陨享,清爽許多
        final long jiter = endNs - intendedFrameTimeNs;
        final int dropFrame = (int) (jiter / frameIntervalNs);
       
        synchronized (listeners) {
            //listeners目前注冊進(jìn)來的就倆,一個(gè)內(nèi)部類FPSCollect钝腺,一個(gè)用于UI展示的FrameDecorator
            for (final IDoFrameListener listener : listeners) {
                if (config.isDevEnv()) {
                    listener.time = SystemClock.uptimeMillis();
                }
                if (null != listener.getExecutor()) {
                    if (listener.getIntervalFrameReplay() > 0) {
                        //數(shù)據(jù)收集部分
                        listener.collect(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame,
                                intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
                    } else {
                        //卡頓分析部分
                        listener.getExecutor().execute(new Runnable() {
                            @Override
                            public void run() {
                                listener.doFrameAsync(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame,
                                        intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
                            }
                        });
                    }
                } else {
                    listener.doFrameSync(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame,
                            intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
                }
            }
        }
    } finally {
    }
}

為什么說丟幀計(jì)算和源碼一致呢抛姑? 這里我們可以和源碼對比一下:

final long jiter = endNs - intendedFrameTimeNs;
final int dropFrame = (int) (jiter / frameIntervalNs);

//源碼 android.view.Choreographer#doFrame
final long jitterNanos = startNanos - frameTimeNanos;
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
  Log.i(TAG, "Skipped " + skippedFrames + " frames!  "
        + "The application may be doing too much work on its main thread.");
}

這么看上去,瞬間就友好了很多~

從注釋里面也能看到艳狐,注冊的倆監(jiān)聽定硝,一個(gè)用于記錄,一個(gè)用于展示毫目,記錄其實(shí)就是填充此時(shí)此刻的關(guān)于FPS的快照蔬啡,沒什么可看的诲侮,學(xué)習(xí)而言,展示的要好一些星爪,因?yàn)樗枰治鰯?shù)據(jù)浆西,然后展示到UI上,那么接下來就直接看下Matix中的com.tencent.matrix.trace.view.FrameDecorator#doFrameAsync顽腾,源碼過長近零,這里就一步一步分析代碼是如何體現(xiàn)上面的表和描述

計(jì)算出平均每一幀的準(zhǔn)備時(shí)間有 1000ms/60 = 16.6667ms,如果一幀的準(zhǔn)備時(shí)間超出這個(gè)值抄肖,則認(rèn)為發(fā)生掉幀久信,超出的時(shí)間越長,掉幀程度越嚴(yán)重漓摩。假設(shè)每幀準(zhǔn)備時(shí)間約 32ms裙士,每次只掉一幀,那么 1 秒內(nèi)實(shí)際只刷新 30 幀管毙,即平均幀率只有 30FPS腿椎,但這時(shí)往往不會覺得是卡頓。反而如果出現(xiàn)某次嚴(yán)重掉幀(>300ms)夭咬,那么這一次的變化啃炸,通常很容易感知到

Best Normal Middle High Frozen
[0:3) [3:9) [9:24) [24:42) [42:∞)
/**
 * 流暢指標(biāo),佳0卓舵,正常1南用,中等2,嚴(yán)重3掏湾,凍幀4
 */
public enum DropStatus {
    DROPPED_FROZEN(4), DROPPED_HIGH(3), DROPPED_MIDDLE(2), DROPPED_NORMAL(1), DROPPED_BEST(0);
    public int index;

    DropStatus(int index) {
        this.index = index;
    }
}

在回調(diào)回來的函數(shù)中裹虫,分析流暢指標(biāo)

卡頓分布數(shù)據(jù).png
@Override
public void doFrameAsync(String focusedActivity, long startNs, long endNs, int dropFrame, boolean isVsyncFrame, long intendedFrameTimeNs, long inputCostNs, long animationCostNs, long traversalCostNs) {
    super.doFrameAsync(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame, intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
    ......
    if (dropFrame >= Constants.DEFAULT_DROPPED_FROZEN) { //凍幀
        dropLevel[FrameTracer.DropStatus.DROPPED_FROZEN.index]++;
        sumDropLevel[FrameTracer.DropStatus.DROPPED_FROZEN.index]++;
    } else if (dropFrame >= Constants.DEFAULT_DROPPED_HIGH) { //嚴(yán)重
        dropLevel[FrameTracer.DropStatus.DROPPED_HIGH.index]++;
        sumDropLevel[FrameTracer.DropStatus.DROPPED_HIGH.index]++;
    } else if (dropFrame >= Constants.DEFAULT_DROPPED_MIDDLE) { //中等
        dropLevel[FrameTracer.DropStatus.DROPPED_MIDDLE.index]++;
        sumDropLevel[FrameTracer.DropStatus.DROPPED_MIDDLE.index]++;
    } else if (dropFrame >= Constants.DEFAULT_DROPPED_NORMAL) { //正常
        dropLevel[FrameTracer.DropStatus.DROPPED_NORMAL.index]++;
        sumDropLevel[FrameTracer.DropStatus.DROPPED_NORMAL.index]++;
    } else {
        dropLevel[FrameTracer.DropStatus.DROPPED_BEST.index]++;
        sumDropLevel[FrameTracer.DropStatus.DROPPED_BEST.index]++;
    }

    ......
}

這里的代碼非常簡單,接下來是分析造成嚴(yán)重卡頓的情況融击,也就是嚴(yán)重丟幀的時(shí)候筑公,也是文章中所分析的內(nèi)容:

如果出現(xiàn)某次嚴(yán)重掉幀(>300ms),那么這一次的變化砚嘴,通常很容易感知到

sumFrameCost += (dropFrame + 1) * frameIntervalMs;
sumFrames += 1;
float duration = sumFrameCost - lastCost[0];
long collectFrame = sumFrames - lastFrames[0];
if (duration >= 200) {
    //更新視圖
}

綜上十酣,就是Matrix中對頁面流暢性分析的核心代碼,而對于Matrix中精準(zhǔn)命中堆棧則可以取自凍幀或者所謂的duration >= 200這個(gè)條件下dump一次主線程的堆棧來獲取

慢函數(shù)

其實(shí)關(guān)于上述嚴(yán)重掉幀情況下的抓取堆棧的數(shù)量不多际长,同樣避不開上面提到的漏掉其他耗時(shí)代碼的情況,不過筆者認(rèn)為這樣的情況不會特別多兴泥,因?yàn)榭D發(fā)生的時(shí)候工育,大概率避免不了正在執(zhí)行一個(gè)耗時(shí)操作,那么這個(gè)耗時(shí)操作的堆棧出現(xiàn)在此刻dump出來的堆棧里面的可能性很大搓彻,所以Matrix干脆利落的出了一個(gè)慢函數(shù)檢測如绸,這樣感覺就無孔不入了嘱朽,對所有在主線程上運(yùn)行的函數(shù)耗時(shí)進(jìn)行收集,和BlockCanary檢測卡頓的策略一樣怔接,但是堆棧的抓取就要復(fù)雜一些搪泳,這也是為什么Matrix性能更好的原因,能在灰度下上線的能力扼脐。同時(shí)也能有對應(yīng)的聚合策略岸军,這樣結(jié)合后端的能力方便我們分析代碼運(yùn)行情況,然后做 “狹義” 上的優(yōu)化瓦侮,這里推薦一個(gè)在解決卡頓時(shí)候您可以用到的方法耗時(shí)小插件

Matrix中的慢函數(shù)和BlockCanary的卡頓堆棧獲取時(shí)機(jī)和檢測卡頓或者慢的策略一致艰赞,下面可以簡單看下茎截,關(guān)于Matrix中如何去撈堆棧的~

@Override
public void dispatchEnd(long beginNs, long cpuBeginMs, long endNs, long cpuEndMs, long token, boolean isVsyncFrame) {
    super.dispatchEnd(beginNs, cpuBeginMs, endNs, cpuEndMs, token, isVsyncFrame);
    long start = config.isDevEnv() ? System.currentTimeMillis() : 0;
    long dispatchCost = (endNs - beginNs) / Constants.TIME_MILLIS_TO_NANO;
    try {
        //查過閾值卵凑,和BlockCanary一樣com.tencent.matrix.trace.constants.Constants.DEFAULT_EVIL_METHOD_THRESHOLD_MS = 700
        if (dispatchCost >= evilThresholdMs) {
            long[] data = AppMethodBeat.getInstance().copyData(indexRecord);
            long[] queueCosts = new long[3];
            System.arraycopy(queueTypeCosts, 0, queueCosts, 0, 3);
            //scene拿的是當(dāng)前的activity
            String scene = AppMethodBeat.getVisibleScene();
            MatrixHandlerThread.getDefaultHandler().post(new AnalyseTask(isForeground(), scene, data, queueCosts, cpuEndMs - cpuBeginMs, dispatchCost, endNs / Constants.TIME_MILLIS_TO_NANO));
        }
    } finally {
        indexRecord.release();
    }
}

可以看到赚楚,慢函數(shù)的閾值是700ms洽损,如果超出閾值萍鲸,則會從AppMethod中拿取相關(guān)的堆棧數(shù)據(jù)鲜棠,同時(shí)記錄下當(dāng)前的頁面然后上傳一波谅海,那么關(guān)鍵的地方就在于這個(gè)

AppMethodBeat.getInstance().copyData(indexRecord)

關(guān)于Matrix的堆棧實(shí)現(xiàn)主要分為兩塊

  • ASM插樁記錄方法
  • Java側(cè)關(guān)于記錄方法耗時(shí)以及方法記錄的實(shí)現(xiàn)
編譯期:

通過代理編譯期間的任務(wù) transformClassesWithDexTask倚喂,將全局 class 文件作為輸入斋泄,利用 ASM 工具杯瞻,高效地對所有 class 文件進(jìn)行掃描及插樁。

插樁過程有幾個(gè)關(guān)鍵點(diǎn):

1是己、選擇在該編譯任務(wù)執(zhí)行時(shí)插樁又兵,是因?yàn)?proguard 操作是在該任務(wù)之前就完成的,意味著插樁時(shí)的 class 文件已經(jīng)被混淆過的卒废。而選擇 proguard 之后去插樁沛厨,是因?yàn)槿绻崆安鍢稌斐刹糠址椒ú环蟽?nèi)聯(lián)規(guī)則,沒法在 proguard 時(shí)進(jìn)行優(yōu)化摔认,最終導(dǎo)致程序方法數(shù)無法減少逆皮,從而引發(fā)方法數(shù)過大問題。

2参袱、為了減少插樁量及性能損耗电谣,通過遍歷 class 方法指令集,判斷掃描的函數(shù)是否只含有 PUT/READ FIELD 等簡單的指令抹蚀,來過濾一些默認(rèn)或匿名構(gòu)造函數(shù)剿牺,以及 get/set 等簡單不耗時(shí)函數(shù)。

3环壤、針對界面啟動(dòng)耗時(shí)晒来,因?yàn)橐y(tǒng)計(jì)從 Activity#onCreate 到 Activity#onWindowFocusChange 間的耗時(shí),所以在插樁過程中需要收集應(yīng)用內(nèi)所有 Activity 的實(shí)現(xiàn)類郑现,并覆蓋 onWindowFocusChange 函數(shù)進(jìn)行打點(diǎn)湃崩。

4荧降、為了方便及高效記錄函數(shù)執(zhí)行過程,我們?yōu)槊總€(gè)插樁的函數(shù)分配一個(gè)獨(dú)立 ID攒读,在插樁過程中朵诫,記錄插樁的函數(shù)簽名及分配的 ID,在插樁完成后輸出一份 mapping薄扁,作為數(shù)據(jù)上報(bào)后的解析支持剪返。

插樁流程.png

下面重點(diǎn)介紹一下關(guān)于Matrix中對性能考量以及具體的業(yè)務(wù)插樁代碼

  • 編譯優(yōu)化,內(nèi)聯(lián)規(guī)則

選擇在該編譯任務(wù)執(zhí)行時(shí)插樁泌辫,是因?yàn)?proguard 操作是在該任務(wù)之前就完成的随夸,意味著插樁時(shí)的 class 文件已經(jīng)被混淆過的。而選擇 proguard 之后去插樁震放,是因?yàn)槿绻崆安鍢稌斐刹糠址椒ú环蟽?nèi)聯(lián)規(guī)則宾毒,沒法在 proguard 時(shí)進(jìn)行優(yōu)化,最終導(dǎo)致程序方法數(shù)無法減少殿遂,從而引發(fā)方法數(shù)過大問題诈铛。

解釋一下什么叫做方法內(nèi)聯(lián),說白了其實(shí)就是你寫了兩個(gè)方法墨礁,在編譯時(shí)會將其中一個(gè)方法的實(shí)現(xiàn)直接放到調(diào)用的地方幢竹,這樣就無需去走一遍調(diào)用其他方法,從而達(dá)到優(yōu)化的目的恩静,因?yàn)槊總€(gè)方法會生成一個(gè)棧幀焕毫,然后進(jìn)行壓棧出棧的操作,過程比較反復(fù)驶乾,這里用代碼解釋一下編譯優(yōu)化之一邑飒,方法內(nèi)聯(lián):

//編譯前,source.java
public int doubleNum(int num){
    return num * 2;
}

public void method1(){
    int a = 10;
    int b = doubleNum(2);
    System.out.println(String.format("a:%s, b:%s", a, b));
}

//編譯后级乐, source.class
public void method1(){
    int a = 10;
    int b = a * 2;//doubleNum(2);
    System.out.println(String.format("a:%s, b:%s", a, b));
}

可以看到在編譯之后就沒有了doubleNum這個(gè)方法疙咸,在method1中調(diào)用的時(shí)候直接變成了 a * 2 , 這樣就無需在運(yùn)行的過程中去對doubleNum壓棧操作

這里有一篇介紹jvm中關(guān)于內(nèi)聯(lián)的blog

雖然能保證proguard時(shí)候優(yōu)化能正常進(jìn)行风科,不過優(yōu)化程度上來講撒轮,筆者也沒有統(tǒng)計(jì)過哈,也不曉得從哪里能夠看到贼穆,因?yàn)槟壳霸?.0之后题山,安裝的時(shí)候會進(jìn)行JIT和AOT混合的方式,這個(gè)結(jié)果應(yīng)該不是很好看故痊,在JIT選擇編譯熱代碼的時(shí)候臀蛛,優(yōu)化的那部分內(nèi)聯(lián)又該如何考慮呢?想想太復(fù)雜了崖蜜。浊仆。。大家感興趣的話可以統(tǒng)計(jì)下在打出來的release包中豫领,可以看日志抡柿,優(yōu)化的效果~

那接下里就是Matrix中如何做到在proguard的transform后進(jìn)行插樁呢,核心工程是matrix-gradle-plugin

入口:

//com.tencent.matrix.plugin.MatrixPlugin#apply

/**
 * <p>Adds a closure to be called immediately after this project has been evaluated. The project is passed to the
 * closure as a parameter. Such a listener gets notified when the build file belonging to this project has been
 * executed. A parent project may for example add such a listener to its child project. Such a listener can further
 * configure those child projects based on the state of the child projects after their build files have been
 * run.</p>
 *
 * @param closure The closure to call.
 */
void afterEvaluate(Closure closure);

@Override
void apply(Project project) {
    ......
    //完成所有transform之后執(zhí)行
    project.afterEvaluate {
        def android = project.extensions.android
        def configuration = project.matrix
        android.applicationVariants.all { variant ->

            if (configuration.trace.enable) {
                //代碼插樁入口
                MatrixTraceTransform.inject(project, configuration.trace, variant.getVariantData().getScope())
            }
            ......
        }
    }
}

在afterEvaluate后傳入閉包等恐,開始插樁代碼洲劣,為什么是這個(gè)時(shí)機(jī),可以參考上面的源碼注釋哈~

接下來就是注入代碼课蔬,因?yàn)椴灰蕾囉谧远x的transformTask囱稽,Matrix的實(shí)現(xiàn)是通過往transformTask里面去注入執(zhí)行事件,這里的寫法也是個(gè)新姿勢~

//com.tencent.matrix.trace.transform.MatrixTraceTransform#inject

public static void inject(Project project, MatrixTraceExtension extension, VariantScope variantScope) {
    ......
    try {
        String[] hardTask = getTransformTaskName(extension.getCustomDexTransformName(), variant.getName());
        for (Task task : project.getTasks()) {
            for (String str : hardTask) {
                if (task.getName().equalsIgnoreCase(str) && task instanceof TransformTask) {
                    //如果確實(shí)是Transform的task后進(jìn)來執(zhí)行反射hook注入matrix的transform任務(wù)
                    TransformTask transformTask = (TransformTask) task;
                    Log.i(TAG, "successfully inject task:" + transformTask.getName());
                    Field field = TransformTask.class.getDeclaredField("transform");
                    field.setAccessible(true);
                    //這里就是注入自定義的任務(wù)二跋,在編譯執(zhí)行的時(shí)候
                    field.set(task, new MatrixTraceTransform(config, transformTask.getTransform()));
                    break;
                }
            }
        }
    } catch (Exception e) {
        Log.e(TAG, e.toString());
    }

}

//源碼中可以看到執(zhí)行transformTask的時(shí)候調(diào)用這個(gè)注入的transform執(zhí)行處战惊,com.android.build.gradle.internal.pipeline.TransformTask#transform
@TaskAction
void transform(final IncrementalTaskInputs incrementalTaskInputs)
        throws IOException, TransformException, InterruptedException {
    ......
    ThreadRecorder.get().record(ExecutionType.TASK_TRANSFORM,
        new Recorder.Block<Void>() {
            @Override
            public Void call() throws Exception {
                //這里就是調(diào)用transform.transform的時(shí)機(jī)
                transform.transform(new TransformInvocationBuilder(TransformTask.this)
                        .addInputs(consumedInputs.getValue())
                        .addReferencedInputs(referencedInputs.getValue())
                        .addSecondaryInputs(changedSecondaryInputs.getValue())
                        .addOutputProvider(outputStream != null
                                ? outputStream.asOutput()
                                : null)
                        .setIncrementalMode(isIncremental.getValue())
                        .build());
                return null;
            }
        },
        new Recorder.Property("project", getProject().getName()),
        new Recorder.Property("transform", transform.getName()),
        new Recorder.Property("incremental", Boolean.toString(transform.isIncremental())));
}

[手動(dòng)表情秒啊~],簡直了扎即。吞获。。 不愧是微信的帶佬谚鄙,如果不是對編譯任務(wù)的task有一定了解的話各拷,要做到這里感覺不太可能。闷营。烤黍。 不過還好能站在巨人的肩膀上~

這里注意下關(guān)于MatrixTraceTransform的構(gòu)造,這里也是一個(gè)優(yōu)秀的細(xì)節(jié)處理傻盟,雖然hook了系統(tǒng)transform的task速蕊,但是不會扔棄系統(tǒng)的transformTask,而是會傳遞進(jìn)來莫杈,接下來在代碼中分析這個(gè)偽代理的使用~

@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
    super.transform(transformInvocation);
    long start = System.currentTimeMillis();
    try {
        //在執(zhí)行系統(tǒng)的transform之前互例,執(zhí)行自己的transform
        doTransform(transformInvocation); // hack
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
    long cost = System.currentTimeMillis() - start;
    long begin = System.currentTimeMillis();
    //ok 接下來,執(zhí)行系統(tǒng)原來內(nèi)置的orignTransformTask筝闹, 優(yōu)秀的細(xì)節(jié)媳叨,不過貌似也只能這么干,可以考慮到卡頓hook looer中printer的時(shí)候也可以這么干~
    origTransform.transform(transformInvocation);
}

最后就是插樁的核心代碼了关顷,這里分為兩個(gè)部分來進(jìn)行介紹糊秆,一個(gè)是如何過濾簡單方法,一個(gè)是插樁細(xì)節(jié)

  • 過濾簡單方法
  • 插樁
matrix編譯工作.png

總的來說议双,流程也比較簡單痘番,整個(gè)方法分為三個(gè)過程

首先解析mapping文件,畢竟是對混淆過的代碼插樁,這里要能知道自己到底插的哪個(gè)方法

然后是收集要插樁的方法汞舱,就是過濾伍纫,過濾黑名單中不需要插樁的類或者方法

對收集后的方法開始插樁

精簡一下代碼:

//com.tencent.matrix.trace.transform.MatrixTraceTransform#doTransform
private void doTransform(TransformInvocation transformInvocation) throws ExecutionException, InterruptedException {
    final boolean isIncremental = transformInvocation.isIncremental() && this.isIncremental();

    /**
     * step 1 把編譯后的mapping文件對應(yīng)的混淆方法關(guān)系記錄下來,如:a() -> onCreate()
     */
    List<Future> futures = new LinkedList<>();
    //...干掉亂八七糟的代碼干掉哈昂芜,主要就是生成方法id莹规,然后解析~
    //拿到需要查找的類文件
    for (TransformInput input : inputs) {

        for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
            futures.add(executor.submit(new CollectDirectoryInputTask(dirInputOutMap, directoryInput, isIncremental)));
        }

        for (JarInput inputJar : input.getJarInputs()) {
            futures.add(executor.submit(new CollectJarInputTask(inputJar, isIncremental, jarInputOutMap, dirInputOutMap)));
        }
    }
    //這里調(diào)用get方法就是執(zhí)行
    for (Future future : futures) {
        future.get();
    }
    futures.clear();

    /**
     * step 2 這里就是收集下來需要進(jìn)行插樁的方法,過濾出來黑名單或者是簡單方法泌神,這些方法不需要插樁~  這里這個(gè)判斷再一次[手動(dòng)秒啊~]
     */
    MethodCollector methodCollector = new MethodCollector(executor, mappingCollector, methodId, config, collectedMethodMap);
    methodCollector.collect(dirInputOutMap.keySet(), jarInputOutMap.keySet());

    /**
     * step 3 對收集的方法進(jìn)行插樁~
     */
    MethodTracer methodTracer = new MethodTracer(executor, mappingCollector, config, methodCollector.getCollectedMethodMap(), methodCollector.getCollectedClassExtendMap());
    methodTracer.trace(dirInputOutMap, jarInputOutMap);

}

mapping的解析就是工作量問題良漱,我們可以看下輸入結(jié)果:

1,1,sample.tencent.matrix.listener.TestPluginListener <init> (Landroid.content.Context;)V
2,0,sample.tencent.matrix.trace.TestFpsActivity$4 <init> (Lsample.tencent.matrix.trace.TestFpsActivity;Landroid.content.Context;I[Ljava.lang.Object;)V
3,1,sample.tencent.matrix.trace.TestTraceFragmentActivity <init> ()V
4,1,sample.tencent.matrix.trace.TestTraceFragmentActivity$2 onClick (Landroid.view.View;)V
5,1,sample.tencent.matrix.listener.TestPluginListener onReportIssue (Lcom.tencent.matrix.report.Issue;)V

然后分析函數(shù)是否簡單的地方,先看一下文檔中的定義

為了減少插樁量及性能損耗欢际,通過遍歷 class 方法指令集母市,判斷掃描的函數(shù)是否只含有 PUT/READ FIELD 等簡單的指令,來過濾一些默認(rèn)或匿名構(gòu)造函數(shù)损趋,以及 get/set 等簡單不耗時(shí)函數(shù)患久。

簡單方法過濾

內(nèi)部獲取方法同樣利用ASM那一套邏輯,上面介紹耗時(shí)插件已經(jīng)介紹過了哈~

可以重點(diǎn)關(guān)注核心代碼:

//com.tencent.matrix.trace.MethodCollector.TraceClassAdapter#visitMethod
@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 {
        //判斷該方法是不是onWindowFocus
        if (!hasWindowFocusMethod) {
            hasWindowFocusMethod = isWindowFocusChangeMethod(name, desc);
        }
        //真正核心處理過濾邏輯
        return new CollectMethodNode(className, access, name, desc, signature, exceptions);
    }
}

//com.tencent.matrix.trace.MethodCollector.CollectMethodNode#visitEnd
@Override
public void visitEnd() {
    super.visitEnd();
    TraceMethod traceMethod = TraceMethod.create(0, access, className, name, desc);

    if ("<init>".equals(name)) {
        isConstructor = true;
    }
        //判斷是否是黑名單里面的舶沿,這里和黑名單中進(jìn)行了互斥墙杯,在插樁中也會判斷
    //所以可以不用在意這個(gè)細(xì)節(jié),說白了就是判斷這個(gè)方法不在黑名單中而已
    boolean isNeedTrace = isNeedTrace(configuration, traceMethod.className, mappingCollector);
    // filter simple methods 這里就是過濾簡單方法括荡,我們重點(diǎn)關(guān)注這里
    if ((isEmptyMethod() || isGetSetMethod() || isSingleMethod())
            && isNeedTrace) {
        ignoreCount.incrementAndGet();
        collectedIgnoreMethodMap.put(traceMethod.getMethodName(), traceMethod);
        return;
    }
    ......
}

上面截取出來的額低嗎可見端倪高镐,一個(gè)是判斷構(gòu)造方法,然后就是isEmptyMethod畸冲,isGetSetMethod嫉髓,isSingleMethod,接下來就分析下這個(gè)所謂的過濾簡單方法到底????與否

//com.tencent.matrix.trace.MethodCollector.CollectMethodNode#isEmptyMethod
/**
 * 檢測空方法邑闲,不知道這里為什么這么寫算行。。苫耸。反正我驗(yàn)證這個(gè)方法基本沒有用
 * -1 是F_NEW指令州邢,不應(yīng)該是判斷return指令么?
 *
 * @return
 */
private boolean isEmptyMethod() {
    ListIterator<AbstractInsnNode> iterator = instructions.iterator();
    while (iterator.hasNext()) {
        //邏輯就是過濾掉是new指令褪子?  說白了就是如果指令集不為空量淌,就不是空方法
        AbstractInsnNode insnNode = iterator.next();
        int opcode = insnNode.getOpcode();
        //-1對應(yīng)的opcode是NEW這個(gè)指令
        if (-1 == opcode) {
            continue;
        } else {
            return false;
        }
    }
    return true;
}

/*
 這里是空方法反編譯后的字節(jié)碼
 public void logClueMethod();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 22: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  this   Lcom/done/testlibrary/Utils;

*/

首先空方法的判斷不夠牛批哈~

接下來看第二個(gè)isGetSetMethod

private boolean isGetSetMethod() {
  int ignoreCount = 0;
  ListIterator<AbstractInsnNode> iterator = instructions.iterator();
  while (iterator.hasNext()) {
    AbstractInsnNode insnNode = iterator.next();
    int opcode = insnNode.getOpcode();
    if (-1 == opcode) {
      continue;
    }
    if (opcode != Opcodes.GETFIELD
        && opcode != Opcodes.GETSTATIC
        && opcode != Opcodes.H_GETFIELD
        && opcode != Opcodes.H_GETSTATIC

        && opcode != Opcodes.RETURN
        && opcode != Opcodes.ARETURN
        && opcode != Opcodes.DRETURN
        && opcode != Opcodes.FRETURN
        && opcode != Opcodes.LRETURN
        && opcode != Opcodes.IRETURN

        && opcode != Opcodes.PUTFIELD
        && opcode != Opcodes.PUTSTATIC
        && opcode != Opcodes.H_PUTFIELD
        && opcode != Opcodes.H_PUTSTATIC
        && opcode > Opcodes.SALOAD) {
      if (isConstructor && opcode == Opcodes.INVOKESPECIAL) {
        ignoreCount++;
        if (ignoreCount > 1) {
          return false;
        }
        continue;
      }
      return false;
    }
  }
  return true;
}

最后是判斷是不是簡單方法

private boolean isSingleMethod() {
  ListIterator<AbstractInsnNode> iterator = instructions.iterator();
  while (iterator.hasNext()) {
    AbstractInsnNode insnNode = iterator.next();
    int opcode = insnNode.getOpcode();
    if (-1 == opcode) {
      continue;
      //出現(xiàn)這個(gè)指令區(qū)間內(nèi)嫌褪,都標(biāo)識調(diào)用了其他方法呀枢,會出現(xiàn)壓棧的情況
      // 調(diào)用了別的方法,自然就不是簡單的方法笼痛,不過這里沒有判斷指令的數(shù)量裙秋,感覺也不是一定可靠
    } else if (Opcodes.INVOKEVIRTUAL <= opcode && opcode <= Opcodes.INVOKEDYNAMIC) {
      return false;
    }
  }
  return true;
}

三個(gè)過濾的函數(shù)都看完了琅拌,感覺不一定可取,可以借鑒和參考摘刑,但不一定準(zhǔn)进宝,可能是在下沒有嚴(yán)謹(jǐn)?shù)木幾g看吧。泣侮。即彪。不過有這種思路也還好,可以自己定義所謂的簡單方法吧~

插樁代碼

這里只簡單看一下插樁的代碼活尊,具體計(jì)算耗時(shí)的功能邏輯實(shí)現(xiàn)后面會繼續(xù)介紹,那部分也不在plugin的工程中~

3漏益、針對界面啟動(dòng)耗時(shí)蛹锰,因?yàn)橐y(tǒng)計(jì)從 Activity#onCreate 到 Activity#onWindowFocusChange 間的耗時(shí),所以在插樁過程中需要收集應(yīng)用內(nèi)所有 Activity 的實(shí)現(xiàn)類绰疤,并覆蓋 onWindowFocusChange 函數(shù)進(jìn)行打點(diǎn)铜犬。

4、為了方便及高效記錄函數(shù)執(zhí)行過程轻庆,我們?yōu)槊總€(gè)插樁的函數(shù)分配一個(gè)獨(dú)立 ID癣猾,在插樁過程中,記錄插樁的函數(shù)簽名及分配的 ID余爆,在插樁完成后輸出一份 mapping纷宇,作為數(shù)據(jù)上報(bào)后的解析支持。

插樁對外的類是com.tencent.matrix.trace.MethodTracer蛾方,這里就簡單看一個(gè)插樁source代碼的像捶,因?yàn)榈阶詈蟛还苁莏ar還是source,可都是對class文件進(jìn)行處理~

//com.tencent.matrix.trace.MethodTracer#innerTraceMethodFromSrc
private void innerTraceMethodFromSrc(File input, File output) {
    ArrayList<File> classFileList = new ArrayList<>();
    if (input.isDirectory()) {
        listClassFiles(classFileList, input);
    } else {
        classFileList.add(input);
    }
    for (File classFile : classFileList) {
        InputStream is = null;
        FileOutputStream os = null;
        try {
            ......
            if (MethodCollector.isNeedTraceFile(classFile.getName())) {
                is = new FileInputStream(classFile);
                ClassReader classReader = new ClassReader(is);
                ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
                //關(guān)注這里插樁訪問者訪問類桩砰,然后里面插樁
                ClassVisitor classVisitor = new TraceClassAdapter(Opcodes.ASM5, classWriter);
                //調(diào)用這里后開始插樁
                classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
                ......
            } else {
                FileUtil.copyFileUsingStream(classFile, changedFileOutput);
            }
        } catch (Exception e) {
            ......
        } finally {
            .......
        }
    }
}

代碼里面關(guān)鍵的實(shí)現(xiàn)還是asm的classVisitor拓春,我們只需要關(guān)注這里面的實(shí)現(xiàn)即可~

這里由于封裝的路徑還是有個(gè)兩三層,我就簡單說下調(diào)用關(guān)系亚隅,這里代碼咱們還是看核心實(shí)現(xiàn)哈~

//com.tencent.matrix.trace.MethodTracer.TraceMethodAdapter
//1. 調(diào)用com.tencent.matrix.trace.MethodTracer.TraceClassAdapter#visitMethod
//2. 調(diào)用com.tencent.matrix.trace.MethodTracer.TraceMethodAdapter#TraceMethodAdapter
//3. 利用AdviceAdapter的方法進(jìn)入和退出回調(diào)插樁

public final static String MATRIX_TRACE_CLASS = "com/tencent/matrix/trace/core/AppMethodBeat";

@Override
protected void onMethodEnter() {
    //這里的插樁結(jié)果就是在方法進(jìn)入的時(shí)候插入 AppMethodBeat.i(methodid);
    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);
    }
}


@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)) {
                //如果是onWindowFocusChanged硼莽,那么還會插入AppMethodBeat.at(activity, isFocus)
                traceWindowFocusChangeMethod(mv, className);
            }
        }
        //無論是不是 onWindowFocusChanged,都會插入AppMethodBeat.o(methodid);
        traceMethodCount.incrementAndGet();
        mv.visitLdcInsn(traceMethod.id);
        mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "o", "(I)V", false);
    }
}

插裝部分就是純工作量的事情了煮纵,有了前面的判斷和過濾邏輯懂鸵,這里就非常簡單,主要插入 i 方法和 o 方法就行具體可以看上面的代碼注釋 多余的這里就不展開去看代碼了

?

JavaLib AppMethodBeat 實(shí)現(xiàn)

? 上面就是trace插件在編譯時(shí)所做的工作醉途,可以看到插樁時(shí)期絲毫沒有進(jìn)行任何系統(tǒng)方法的調(diào)用矾瑰,如:SystemClock.time或者System.nanoTime這些獲取時(shí)間戳的native方法,這樣可以理解為一個(gè)小優(yōu)化的點(diǎn)~不通過系統(tǒng)方法獲取時(shí)間隘擎,matrix利用很巧妙的方式來獲取時(shí)間

考慮到每個(gè)方法執(zhí)行前后都獲取系統(tǒng)時(shí)間(System.nanoTime)會對性能影響比較大殴穴,而實(shí)際上,單個(gè)函數(shù)執(zhí)行耗時(shí)小于 5ms 的情況,對卡頓來說不是主要原因采幌,可以忽略不計(jì)劲够,如果是多次調(diào)用的情況,則在它的父級方法中可以反映出來休傍,所以為了減少對性能的影響征绎,通過另一條更新時(shí)間的線程每 5ms 去更新一個(gè)時(shí)間變量,而每個(gè)方法執(zhí)行前后只讀取該變量來減少性能損耗磨取。

具體就在java lib中去實(shí)現(xiàn)人柿,下面我們就分析下java中的實(shí)現(xiàn),其實(shí)上面插樁的時(shí)候就已經(jīng)知道具體的實(shí)現(xiàn)的核心類是哪個(gè)了 => com.tencent.matrix.trace.core.AppMethodBeat

這個(gè)類的邏輯還是有一小丟丟繞忙厌,因?yàn)槠渲胁粌H僅包含了計(jì)算方法耗時(shí)凫岖,還兼顧了查看生命周期相關(guān)的,包括activity逢净、service的生命周期哥放,目測了下,contentProvider的還沒完成爹土,感興趣的同學(xué)可以具體查看下內(nèi)部關(guān)于hook mH相關(guān)的代碼甥雕,這里就貼一下關(guān)鍵性的代碼哈~

//com.tencent.matrix.trace.hacker.ActivityThreadHacker#hackSysHandlerCallback
public static void hackSysHandlerCallback() {
    try {
        Class<?> forName = Class.forName("android.app.ActivityThread");
        Field field = forName.getDeclaredField("sCurrentActivityThread");
        field.setAccessible(true);
        Object activityThreadValue = field.get(forName);
        //hook mH這個(gè)handler
        Field mH = forName.getDeclaredField("mH");
        mH.setAccessible(true);
        Object handler = mH.get(activityThreadValue);
        Class<?> handlerClass = handler.getClass().getSuperclass();
        if (null != handlerClass) {
            //接著hook系統(tǒng)的callback,方便內(nèi)部調(diào)用從而不影響系統(tǒng)調(diào)動(dòng)過程
            Field callbackField = handlerClass.getDeclaredField("mCallback");
            callbackField.setAccessible(true);
            Handler.Callback originalCallback = (Handler.Callback) callbackField.get(handler);
            HackCallback callback = new HackCallback(originalCallback);
            callbackField.set(handler, callback);
        }
    } catch (Exception e) {
    }
}

進(jìn)入正題胀茵,我們先從插樁中的方法入口和方法出口來分析

編譯期已經(jīng)對全局的函數(shù)進(jìn)行插樁社露,在運(yùn)行期間每個(gè)函數(shù)的執(zhí)行前后都會調(diào)用 MethodBeat.i/o 的方法,如果是在主線程中執(zhí)行宰掉,則在函數(shù)的執(zhí)行前后獲取當(dāng)前距離 MethodBeat 模塊初始化的時(shí)間 offset(為了壓縮數(shù)據(jù)呵哨,存進(jìn)一個(gè)long類型變量中),并將當(dāng)前執(zhí)行的是 MethodBeat i或者o轨奄、mehtod id 及時(shí)間 offset孟害,存放到一個(gè) long 類型變量中,記錄到一個(gè)預(yù)先初始化好的數(shù)組 long[] 中 index 的位置(預(yù)先分配記錄數(shù)據(jù)的 buffer 長度為 100w挪拟,內(nèi)存占用約 7.6M)挨务。數(shù)據(jù)存儲如下圖:

![方法數(shù)據(jù)邏輯.png](https://upload-images.jianshu.io/upload_images/2822814-fecb3594fa74d08c.png?imageMogr2/aut
o-orient/strip%7CimageView2/2/w/820)

在搞清楚內(nèi)部的邏輯之前,筆者認(rèn)為直接貼代碼不是很好理解玉组,所以我們先來配個(gè)圖谎柄,然后結(jié)合圖來理解這個(gè)過程:

private static final int STATUS_DEFAULT = Integer.MAX_VALUE;
private static final int STATUS_STARTED = 2;
private static final int STATUS_READY = 1;
private static final int STATUS_STOPPED = -1;
private static final int STATUS_EXPIRED_START = -2;
private static final int STATUS_OUT_RELEASE = -3;


public static void i(int methodId) {
    ......
    //正式開始 step 1
    if (status == STATUS_DEFAULT) {
        synchronized (statusLock) {
            if (status == STATUS_DEFAULT) {
                realExecute();
                status = STATUS_READY;
            }
        }
    }
    long threadId = Thread.currentThread().getId();
    if (threadId == sMainThreadId) {
        //合并方法堆棧 step 2
        if (sIndex < Constants.BUFFER_SIZE) {
            mergeData(methodId, sIndex, true);
        } else {
            sIndex = 0;
            mergeData(methodId, sIndex, true);
        }
        ++sIndex;
    }
}

private static void realExecute() {
    //記錄開始執(zhí)行的時(shí)間戳
    sCurrentDiffTime = SystemClock.uptimeMillis() - sDiffTime;

    sHandler.removeCallbacksAndMessages(null);
    //啟動(dòng)計(jì)時(shí)器
    sHandler.postDelayed(sUpdateDiffTimeRunnable, Constants.TIME_UPDATE_CYCLE_MS);
    //狀態(tài)維護(hù),延遲15s后執(zhí)行惯雳,由上可知有i首次進(jìn)來以后狀態(tài)是STATUS_READY
    //這里得結(jié)合looper的監(jiān)聽來說朝巫,后面分析捕捉細(xì)節(jié)再詳說,這里記得這個(gè)狀態(tài)維護(hù)
    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);
    //注冊監(jiān)聽
    LooperMonitor.register(looperMonitorListener);
}

然后可以看到時(shí)間更新的方法為石景,主要工作其實(shí)就是更新時(shí)間戳劈猿,每次更新后睡5s拙吉,然后掛起自己,等待被主線程分發(fā)消息時(shí)候再喚醒:

/**
 * 計(jì)時(shí)器
 * update time runnable
 */
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 (Exception e) {
            MatrixLog.e(TAG, "" + e.toString());
        }
    }
};

private static void dispatchBegin() {
    sCurrentDiffTime = SystemClock.uptimeMillis() - sDiffTime;
    isPauseUpdateTime = false;

    synchronized (updateTimeLock) {
        updateTimeLock.notify();
    }
}

最后在合并數(shù)據(jù)的時(shí)候把方法進(jìn)入退出和方法id以及時(shí)間戳帶上揪荣,組成一個(gè)long變量:

/**
 * merge trace info as a long data
 *
 * @param methodId
 * @param index
 * @param isIn
 */
private static void mergeData(int methodId, int index, boolean isIn) {
    //如果是分發(fā)的函數(shù)過來的筷黔,更新一下時(shí)刻,以示尊重~  就是更新時(shí)戳仗颈,這個(gè)時(shí)間可不能是簡單的5的倍數(shù)[手動(dòng)摳鼻]
    if (methodId == AppMethodBeat.METHOD_ID_DISPATCH) {
        sCurrentDiffTime = SystemClock.uptimeMillis() - sDiffTime;
    }
    //可見輸入為1輸出為0
    long trueId = 0L;
    if (isIn) {
        trueId |= 1L << 63;
    }
    trueId |= (long) methodId << 43;
    //將方法id和時(shí)間戳合并成一個(gè)long值佛舱,以達(dá)到8字節(jié)存儲的目的
    trueId |= sCurrentDiffTime & 0x7FFFFFFFFFFL;
    sBuffer[index] = trueId;
    checkPileup(index);
    sLastIndex = index;
}

最后就把數(shù)據(jù)合并完成了。matrix的慢函數(shù)監(jiān)控至此結(jié)束~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末挨决,一起剝皮案震驚了整個(gè)濱河市请祖,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌凰棉,老刑警劉巖损拢,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異撒犀,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)掏秩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進(jìn)店門或舞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蒙幻,你說我怎么就攤上這事映凳。” “怎么了邮破?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵诈豌,是天一觀的道長。 經(jīng)常有香客問我抒和,道長矫渔,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任摧莽,我火速辦了婚禮庙洼,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘镊辕。我一直安慰自己油够,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布征懈。 她就那樣靜靜地躺著石咬,像睡著了一般。 火紅的嫁衣襯著肌膚如雪卖哎。 梳的紋絲不亂的頭發(fā)上鬼悠,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天删性,我揣著相機(jī)與錄音,去河邊找鬼厦章。 笑死镇匀,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的袜啃。 我是一名探鬼主播汗侵,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼群发!你這毒婦竟也來了晰韵?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤熟妓,失蹤者是張志新(化名)和其女友劉穎雪猪,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體起愈,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡只恨,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了抬虽。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片官觅。...
    茶點(diǎn)故事閱讀 38,039評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖阐污,靈堂內(nèi)的尸體忽然破棺而出休涤,到底是詐尸還是另有隱情,我是刑警寧澤笛辟,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布功氨,位于F島的核電站,受9級特大地震影響手幢,放射性物質(zhì)發(fā)生泄漏捷凄。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一弯菊、第九天 我趴在偏房一處隱蔽的房頂上張望纵势。 院中可真熱鬧,春花似錦管钳、人聲如沸钦铁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽牛曹。三九已至,卻和暖如春醇滥,著一層夾襖步出監(jiān)牢的瞬間黎比,已是汗流浹背超营。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留阅虫,地道東北人演闭。 一個(gè)月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像颓帝,于是被迫代替她去往敵國和親米碰。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容