Android線上輕量級(jí)APM性能監(jiān)測(cè)方案

Github 鏈接 Collie

App性能如何量化

如何衡量一個(gè)APP性能好壞谊迄?直觀感受就是:?jiǎn)?dòng)快油猫、流暢紊册、不閃退汇鞭、耗電少等感官指標(biāo)矗蕊,反應(yīng)到技術(shù)層面包裝下就是:FPS(幀率)惫谤、界面渲染速度、Crash率、網(wǎng)絡(luò)堂油、CPU使用率、電量損耗速度等碧绞,一般挑其中幾個(gè)關(guān)鍵指標(biāo)作為APP質(zhì)量的標(biāo)尺府框。目前也有多種開(kāi)源APM監(jiān)控方案,但大部分偏向離線檢測(cè)讥邻,對(duì)于線上監(jiān)測(cè)而言顯得太重迫靖,可能會(huì)適得其反,方案簡(jiǎn)單對(duì)比如下:

SDK 現(xiàn)狀與問(wèn)題 是否推薦直接線上使用
騰訊matrix 功能全兴使,但是重系宜,而且運(yùn)行測(cè)試期間經(jīng)常Crash
騰訊GT 2018年之后沒(méi)更新,關(guān)注度低发魄,本身功能挺多盹牧,也挺重性?xún)r(jià)比還不如matrix
網(wǎng)易Emmagee 2018年之后沒(méi)更新,幾乎沒(méi)有關(guān)注度励幼,重
聽(tīng)云App 適合監(jiān)測(cè)網(wǎng)絡(luò)跟啟動(dòng)汰寓,場(chǎng)景受限

還有其他多種APM檢測(cè)工具,功能復(fù)雜多樣苹粟,但其實(shí)很多指標(biāo)并不是特別重要有滑,實(shí)現(xiàn)越復(fù)雜,線上風(fēng)險(xiǎn)越大六水,因此俺孙,并不建議直接使用。而且掷贾,分析多家APP的實(shí)現(xiàn)原理睛榄,其核心思路基本相同,且門(mén)檻也并不是特別高想帅,建議自研一套场靴,在靈活性、安全性上更有保障,更容易做到輕量級(jí)旨剥。本文主旨就是圍繞幾個(gè)關(guān)鍵指標(biāo):FPS咧欣、內(nèi)存(內(nèi)存泄漏)、界面啟動(dòng)轨帜、流量等魄咕,實(shí)現(xiàn)輕量級(jí)的線上監(jiān)測(cè)。

核心性能指標(biāo)拆解

  • 穩(wěn)定性:Crash統(tǒng)計(jì)

Crash統(tǒng)計(jì)與聚合有比較通用的策略蚌父,比如Firebase哮兰、Bugly等,不在本文討論范圍

  • 網(wǎng)絡(luò)請(qǐng)求

每個(gè)APP的網(wǎng)絡(luò)請(qǐng)求一般都存在統(tǒng)一的Hook點(diǎn)苟弛,門(mén)檻很低喝滞,且各家請(qǐng)求協(xié)議與SDK有別,很難實(shí)現(xiàn)統(tǒng)一的網(wǎng)絡(luò)請(qǐng)求監(jiān)測(cè)膏秫,其次右遭,想要真正定位網(wǎng)絡(luò)請(qǐng)求問(wèn)題,可能牽扯整個(gè)請(qǐng)求的鏈路缤削,更適合做一套網(wǎng)絡(luò)全鏈路監(jiān)控APM窘哈,也不在討論范圍。

  • 冷啟動(dòng)時(shí)間及各個(gè)Activity頁(yè)面啟動(dòng)時(shí)間 (存在統(tǒng)一方案)
  • 頁(yè)面FPS僻他、卡頓宵距、ANR (存在統(tǒng)一方案)
  • 內(nèi)存統(tǒng)計(jì)及內(nèi)存泄露偵測(cè) (存在統(tǒng)一方案)
  • 流量消耗 (存在統(tǒng)一方案)
  • 電量 (存在統(tǒng)一方案)
  • CPU使用率(CPU):還沒(méi)想好咋么用,7.0之后實(shí)現(xiàn)機(jī)制也變了吨拗,先不考慮

線上監(jiān)測(cè)的重點(diǎn)就聚焦后面幾個(gè)满哪,下面逐個(gè)拆解如何實(shí)現(xiàn)。

啟動(dòng)耗時(shí)

直觀上說(shuō)界面啟動(dòng)就是:從點(diǎn)擊一個(gè)圖標(biāo)到看到下一個(gè)界面首幀劝篷,如果這個(gè)過(guò)程耗時(shí)較長(zhǎng)哨鸭,用戶(hù)會(huì)會(huì)感受到頓挫,影響體驗(yàn)娇妓。從場(chǎng)景上說(shuō)像鸡,啟動(dòng)耗時(shí)間簡(jiǎn)單分兩種:

  • 冷啟動(dòng)耗時(shí):在APP未啟動(dòng)的情況從,從點(diǎn)擊桌面icon 到看到閃屏Activity的首幀(非默認(rèn)背景)
  • 界面啟動(dòng)耗:APP啟動(dòng)后哈恰,從上一個(gè)界面pause只估,到下一個(gè)界面首幀可見(jiàn),

本文粒度較粗着绷,主要聚焦Activity蛔钙,這里有個(gè)比較核心的時(shí)機(jī):Activity首幀可見(jiàn)點(diǎn),這個(gè)點(diǎn)究竟在什么時(shí)候荠医?經(jīng)分析測(cè)試發(fā)現(xiàn)吁脱,不同版本表現(xiàn)不一桑涎,在Android 10 之前這個(gè)點(diǎn)與onWindowFocusChanged回調(diào)點(diǎn)基本吻合,在Android 10 之后兼贡,系統(tǒng)做了優(yōu)化攻冷,將首幀可見(jiàn)的時(shí)機(jī)提前到onWindowFocusChanged之前,可以簡(jiǎn)單看做onResume(或者onAttachedToWindow)之后遍希,對(duì)于一開(kāi)始點(diǎn)擊icon的點(diǎn)等曼,可以約等于APP進(jìn)程啟動(dòng)的點(diǎn),拿到了上面兩個(gè)時(shí)間點(diǎn)孵班,就可以得到冷啟動(dòng)耗時(shí)涉兽。

APP進(jìn)程啟動(dòng)的點(diǎn)可以通過(guò)加載一個(gè)空的ContentProvider來(lái)記錄,因?yàn)镃ontentProvider的加載時(shí)機(jī)比較靠前篙程,早于Application的onCreate之前,相對(duì)更準(zhǔn)確一點(diǎn)别厘,很多SDK的初始也采用這種方式虱饿,實(shí)現(xiàn)如下:

public class LauncherHelpProvider extends ContentProvider {

    // 用來(lái)記錄啟動(dòng)時(shí)間
    public static long sStartUpTimeStamp = SystemClock.uptimeMillis();
    ...
    
    }

這樣就得到了冷啟動(dòng)的開(kāi)始時(shí)間,如何得到第一個(gè)Activity界面可見(jiàn)的時(shí)間呢触趴?大概回執(zhí)流程如下

image.png

網(wǎng)上有一些認(rèn)為可以監(jiān)聽(tīng)onAttachedToWindow或者OnWindowFocusChange氮发,onAttachedToWindow的問(wèn)題是可能太過(guò)靠前,還沒(méi)有Draw, OnWindowFocusChange的缺點(diǎn)可能是太過(guò)滯后冗懦。其實(shí)可以簡(jiǎn)單認(rèn)為在view draw以后爽冕,View的繪制就算完成,雖然到展示還可能相差一個(gè)VSYNC等待圖層合成披蕉,但是對(duì)于性能監(jiān)測(cè)的評(píng)定颈畸,誤差一個(gè)固定值可以接受:

image.png

在onResume函數(shù)中插入一條消息可以嗎,理論上來(lái)說(shuō)没讲,太過(guò)靠前眯娱,這條消息在執(zhí)行的時(shí)候,還沒(méi)Draw爬凑,因?yàn)檎?qǐng)求VSYNC的同步柵欄是在是在Onresume結(jié)束后才插入的徙缴,無(wú)法攔截之前的Message,但是由于VSYNC可能存在復(fù)用嘁信,Onresume中插入的消息也有可能會(huì)在繪制之后執(zhí)行于样,這個(gè)不是完全一定的,比如點(diǎn)擊MaterialButton啟動(dòng)一個(gè)Activity潘靖,第二個(gè)Activity的setView觸發(fā)的VSYNC就可能復(fù)用MaterialButton的波紋觸發(fā)的VSYNC穿剖,從而導(dǎo)致第二個(gè)Activity的performTraval復(fù)用第一個(gè)VSYNC執(zhí)行,從而發(fā)生在onResume插入消息之前秘豹,如下

柵欄消息

image.png

重繪CallBack包含多個(gè)Activity的重繪

image.png

綜上所述携御,將指標(biāo)定義在第一次View的Draw執(zhí)行可能比較靠譜。具體可以再DecorView上插入一個(gè)透明View,監(jiān)聽(tīng)器onDraw回調(diào)即可啄刹,如果覺(jué)得不夠優(yōu)雅涮坐,就退一步,監(jiān)聽(tīng)OnWindowFocusChange的回調(diào)誓军,也勉強(qiáng)可以接受, OnWindowFocusChange一定是在Draw之后的袱讹。如此就可以檢測(cè)到冷啟動(dòng)耗時(shí)。APP啟動(dòng)后昵时,各Activity啟動(dòng)耗時(shí)計(jì)算邏輯類(lèi)似捷雕,首幀可見(jiàn)點(diǎn)沿用上面方案即可,不過(guò)這里還缺少上一個(gè)界面暫停的點(diǎn)壹甥,經(jīng)分析測(cè)試救巷,錨在上一個(gè)Actiivty pause的時(shí)候比較合理,因此Activity啟動(dòng)耗時(shí)定義如下:

Activity啟動(dòng)耗時(shí) = 當(dāng)前Activity 首幀可見(jiàn) - 上一個(gè)Activity onPause被調(diào)用

同樣為了減輕對(duì)業(yè)務(wù)入侵句柠,也依賴(lài)registerActivityLifecycleCallbacks來(lái)實(shí)現(xiàn):補(bǔ)全上方缺失

   application.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {

       @Override
        public void onActivityPaused(@NonNull Activity activity) {
            super.onActivityPaused(activity);
            <!--記錄上一個(gè)Activity pause節(jié)點(diǎn)-->
            mActivityLauncherTimeStamp = SystemClock.uptimeMillis();
            launcherFlag = 0;
        }
        ...
    @Override
    public void onActivityResumed(@NonNull final Activity activity) {
        super.onActivityResumed(activity);
        launcherFlag |= resumeFlag;
       <!--參考上面獲取首幀的點(diǎn)-->
             ...

到這里就獲取了兩個(gè)比較關(guān)鍵的啟動(dòng)耗時(shí)浦译,不過(guò),時(shí)機(jī)使用中可能存在各種異常場(chǎng)景:比如閃屏頁(yè)在onCreate或者onResume中調(diào)用了finish跳轉(zhuǎn)首頁(yè)溯职,對(duì)于這種場(chǎng)景就需要額外處理精盅,比如在onCreate中調(diào)用了finish,onResume可能不會(huì)被調(diào)用谜酒,這個(gè)時(shí)候就要在 onCreate之后進(jìn)行統(tǒng)計(jì)叹俏,同時(shí)利用用Activity.isFinishing()標(biāo)識(shí)這種場(chǎng)景,其次僻族,啟動(dòng)耗時(shí)對(duì)于不同配置也是不一樣的粘驰,不能用絕對(duì)時(shí)間衡量,只能橫向?qū)Ρ扔ス螅?jiǎn)單線上效果如下:

image.png
image.png
image.png

流暢度及FPS(Frames Per Second)監(jiān)測(cè)

FPS是圖像領(lǐng)域中的定義晴氨,指畫(huà)面每秒傳輸幀數(shù),每秒幀數(shù)越多碉输,顯示的動(dòng)作就越流暢籽前。FPS可以作為衡量流暢度的一個(gè)指標(biāo),但是敷钾,從各廠商的報(bào)告來(lái)看枝哄,僅用FPS來(lái)衡量是否流暢并不科學(xué)。電影或視頻的FPS并不高阻荒,30的FPS即可滿(mǎn)足人眼需求挠锥,穩(wěn)定在30FPS的動(dòng)畫(huà),并不會(huì)讓人感到卡頓侨赡,但如果FPS 很不穩(wěn)定的話蓖租,就很容易感知到卡頓粱侣,注意,這里有個(gè)詞叫穩(wěn)定蓖宦。舉個(gè)極端例子:前500ms刷新了59幀齐婴,后500ms只繪制一幀,即使達(dá)到了60FPS稠茂,仍會(huì)感知卡頓柠偶,這里就突出穩(wěn)定的重要性。不過(guò)FPS也并不是完全沒(méi)用睬关,可以用其上限定義流暢诱担,用其下限可以定義卡頓,對(duì)于中間階段的感知电爹,F(xiàn)PS無(wú)能為力蔫仙,如下示意:

image

上面那個(gè)是極端例子,Android 系統(tǒng)中藐不,VSYNC會(huì)杜絕16ms內(nèi)刷新兩次匀哄,那么在中間的情況下怎么定義流暢?比如雏蛮,F(xiàn)PS降低到50會(huì)卡嗎?答案是不一定阱州。50的FPS如果是均分到各個(gè)節(jié)點(diǎn)挑秉,用戶(hù)是感知不到掉幀的,但苔货,如果丟失的10幀全部在一次繪制點(diǎn)犀概,那就能明顯感知卡頓,這個(gè)時(shí)候夜惭,瞬時(shí)幀率的意義更大姻灶,如下

image

Matrix給的卡頓標(biāo)準(zhǔn):

image

總之,相比1s平均FPS诈茧,瞬時(shí)掉幀程度的嚴(yán)重性更能反應(yīng)界面流暢程度产喉,因此FPS監(jiān)測(cè)的重點(diǎn)是偵測(cè)瞬時(shí)掉幀程度。

image.png

在應(yīng)用中敢会,F(xiàn)PS對(duì)動(dòng)畫(huà)及列表意義較大曾沈,監(jiān)測(cè)開(kāi)始的時(shí)機(jī)放在界面啟動(dòng)并展示第一幀之后,這樣就能跟啟動(dòng)完美銜接起來(lái)鸥昏,

    // 幀率不統(tǒng)計(jì)第一幀
    @Override
    public void onActivityResumed(@NonNull final Activity activity) {
        super.onActivityResumed(activity);
        activity.getWindow().getDecorView().getViewTreeObserver().addOnWindowFocusChangeListener(new ViewTreeObserver.OnWindowFocusChangeListener() {
            @Override
            public void onWindowFocusChanged(boolean b) {
                if (b) {
                <!--界面可見(jiàn)后塞俱,開(kāi)始偵測(cè)FPS-->
                    resumeTrack();
                    activity.getWindow().getDecorView().getViewTreeObserver().removeOnWindowFocusChangeListener(this);
    ...
  }

偵測(cè)停止的時(shí)機(jī)也比較簡(jiǎn)單在onActivityPaused:界面失去焦點(diǎn),無(wú)法與用戶(hù)交互的時(shí)候

    @Override
    public void onActivityPaused(@NonNull Activity activity) {
        super.onActivityPaused(activity);
        pauseTrack(activity.getApplication());
    }

如何偵測(cè)瞬時(shí)FPS吏垮?有兩種常用方式

  • 360 ArgusAPM類(lèi)實(shí)現(xiàn)方式: 監(jiān)測(cè)Choreographer兩次Vsync時(shí)間差
  • BlockCanary的實(shí)現(xiàn)方式:監(jiān)測(cè)UI線程單條Message執(zhí)行時(shí)間

360的實(shí)現(xiàn)依賴(lài)Choreographer VSYNC回調(diào)障涯,具體實(shí)現(xiàn)如下:循環(huán)添加Choreographer.FrameCallback

Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {

    @Override
        public void doFrame(long frameTimeNanos) {
            mFpsCount++;
            mFrameTimeNanos = frameTimeNanos;
            if (isCanWork()) {
                //注冊(cè)下一幀回調(diào)
                Choreographer.getInstance().postFrameCallback(this);
            } else {
                mCurrentCount = 0;
            }
        }
});

這種監(jiān)聽(tīng)有個(gè)問(wèn)題就是罐旗,監(jiān)聽(tīng)過(guò)于頻繁,因?yàn)樵跓o(wú)需界面刷新的時(shí)候Choreographer.FrameCallback還是不斷循環(huán)執(zhí)行唯蝶,浪費(fèi)CPU資源九秀,對(duì)線上運(yùn)行采集并不友好,相比之下BlockCanary的監(jiān)聽(tīng)單個(gè)Message執(zhí)行要友善的多生棍,而且同樣能夠涵蓋UI繪制耗時(shí)颤霎、兩幀之間的耗時(shí),額外執(zhí)行負(fù)擔(dān)較低涂滴,也是本文采取的策略友酱,核心實(shí)現(xiàn)參照Matrix:

  • 監(jiān)聽(tīng)Message執(zhí)行耗時(shí)
  • 通過(guò)反射循環(huán)添加Choreographer.FrameCallback區(qū)分doFrame耗時(shí)

為L(zhǎng)ooper設(shè)置一個(gè)LooperPrinter,根據(jù)回傳信息頭區(qū)分消息執(zhí)行開(kāi)始于結(jié)束柔纵,計(jì)算Message耗時(shí):原理如下

      public static void loop() {
                ...
                if (logging != null) {
                    logging.println(">>>>> Dispatching to " + msg.target + " " +
                            msg.callback + ": " + msg.what);
                }
                 ...
                if (logging != null) {
                    logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
                }

自定義LooperPrinter如下:

        class LooperPrinter implements Printer {
        
        @Override
        public void println(String x) {
           ...
            if (isValid) {
            <!--區(qū)分開(kāi)始結(jié)束缔杉,計(jì)算消息耗時(shí)-->
                dispatch(x.charAt(0) == '>', x);
        }

利用回調(diào)參數(shù)">>>>"與"<<<"的 區(qū)別即可診斷出Message執(zhí)行耗時(shí),從而確定是否導(dǎo)致掉幀搁料。以上實(shí)現(xiàn)針對(duì)所有UI Message或详,原則上UI線程所有的消息都應(yīng)該保持輕量級(jí),任何消息超時(shí)都應(yīng)當(dāng)算作異常行為郭计,所以霸琴,直接拿來(lái)做掉幀監(jiān)測(cè)沒(méi)特大問(wèn)題的。但是昭伸,有些特殊情況可能對(duì)FPS計(jì)算有一些誤判梧乘,比如,在touch時(shí)間里往UI線程塞了很多消息庐杨,單條一般不會(huì)影響滾動(dòng)选调,但多條聚合可能會(huì)帶來(lái)影響,如果沒(méi)跳消息執(zhí)行時(shí)間很短灵份,這種方式就可能統(tǒng)計(jì)不到仁堪,當(dāng)然這種業(yè)務(wù)的寫(xiě)法本身就存在問(wèn)題,所以先不考慮這種場(chǎng)景填渠。

Choreographer有個(gè)方法addCallbackLocked弦聂,通過(guò)這個(gè)方法添加的任務(wù)會(huì)被加入到VSYNC回調(diào),會(huì)跟Input揭蜒、動(dòng)畫(huà)横浑、UI繪制一起執(zhí)行,因此可以用來(lái)作為鑒別是否是UI重繪的Message屉更,看看是不是重繪或者觸摸事件導(dǎo)致的卡頓掉幀徙融。Choreographer源碼如下:

    @UnsupportedAppUsage
    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;
    }

該方法不為外部可見(jiàn),因此需要通過(guò)反射獲取瑰谜,

private synchronized void addFrameCallback(int type, Runnable callback, boolean isAddHeader) {

    try {
         <!--反射獲取方法-->
            addInputQueue = reflectChoreographerMethod(0 “addCallbackLocked”, long.class, Object.class, Object.class);
          <!--添加回調(diào)-->
            if (null != method) {
                method.invoke(callbackQueues[type], !isAddHeader ? SystemClock.uptimeMillis() : -1, callback, null);
            }

然后在每次執(zhí)行結(jié)束后欺冀,重新將callback添加回Choreographer的Queue树绩,監(jiān)聽(tīng)下一次UI繪制。

@Override
public void dispatchEnd() {
    super.dispatchEnd();
    if (mStartTime > 0) {
        long cost = SystemClock.uptimeMillis() - mStartTime;
        <!--計(jì)算耗時(shí)-->
        collectInfoAndDispatch(ActivityStack.getInstance().getTopActivity(), cost, mInDoFrame);
        if (mInDoFrame) {
        <!--監(jiān)聽(tīng)下一次UI繪制-->
            addFrameCallBack();
            mInDoFrame = false;
        }
    }
}

這樣就能檢測(cè)到每次Message執(zhí)行的時(shí)間隐轩,它可以直接用來(lái)計(jì)算瞬時(shí)幀率饺饭,

瞬時(shí)掉幀程度 = Message耗時(shí)/16 -1 (不足1 可看做1)

瞬時(shí)掉幀小于2次可以認(rèn)為沒(méi)有發(fā)生抖動(dòng),如果出現(xiàn)了單個(gè)Message執(zhí)行過(guò)長(zhǎng)职车,可認(rèn)為發(fā)生了掉幀瘫俊,流暢度與瞬時(shí)幀率監(jiān)測(cè)大概就是這樣。不過(guò)悴灵,同啟動(dòng)耗時(shí)類(lèi)似扛芽,不同配置結(jié)果不同,不能用絕對(duì)時(shí)間衡量积瞒,只能橫向?qū)Ρ却猓?jiǎn)單線上效果如下:

image
image

內(nèi)存泄露及內(nèi)存使用偵測(cè)

內(nèi)存泄露有個(gè)比較出名的庫(kù)LeakCanary,實(shí)現(xiàn)原理也比較清晰茫孔,就是利用弱引用+ReferenceQueue叮喳,其實(shí)只用弱引用也可以做,ReferenceQueue只是個(gè)輔助作用缰贝,LeakCanary除了泄露檢測(cè)還有個(gè)堆棧Dump的功能馍悟,雖然很好,但是這個(gè)功能并不適合線上剩晴,而且赋朦,只要能監(jiān)聽(tīng)到Activity泄露,本地分析原因是比較快的李破,沒(méi)必要將堆棧Dump出來(lái)。因此壹将,本文只實(shí)現(xiàn)Activity泄露監(jiān)測(cè)能力嗤攻,不在線上分析原因。而且诽俯,參考LeakCanary妇菱,改用一個(gè)WeakHashMap實(shí)現(xiàn)上述功能,不在主動(dòng)暴露ReferenceQueue這個(gè)對(duì)象暴区。WeakHashMap最大的特點(diǎn)是其key對(duì)象被自動(dòng)弱引用闯团,可以被回收,利用這個(gè)特點(diǎn)仙粱,用其key監(jiān)聽(tīng)Activity回收就能達(dá)到泄露監(jiān)測(cè)的目的房交。核心實(shí)現(xiàn)如下:

   application.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {

        @Override
        public void onActivityDestroyed(@NonNull Activity activity) {
            super.onActivityDestroyed(activity);
            <!--放入map,進(jìn)行監(jiān)聽(tīng)-->
            mActivityStringWeakHashMap.put(activity, activity.getClass().getSimpleName());
        }

        @Override
        public void onActivityStopped(@NonNull final Activity activity) {
            super.onActivityStopped(activity);
            //  退后臺(tái)伐割,GC 找LeakActivity
            if (!ActivityStack.getInstance().isInBackGround()) {
                return;
            }
            Runtime.getRuntime().gc();
            mHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    try {
                        if (!ActivityStack.getInstance().isInBackGround()) {
                            return;
                        }
                        try {
                            //   申請(qǐng)個(gè)稍微大的對(duì)象候味,促進(jìn)GC
                            byte[] leakHelpBytes = new byte[4 * 1024 * 1024];
                            for (int i = 0; i < leakHelpBytes.length; i += 1024) {
                                leakHelpBytes[i] = 1;
                            }
                        } catch (Throwable ignored) {
                        }
                        Runtime.getRuntime().gc();
                        SystemClock.sleep(100);
                        System.runFinalization();
                        HashMap<String, Integer> hashMap = new HashMap<>();
                        for (Map.Entry<Activity, String> activityStringEntry : mActivityStringWeakHashMap.entrySet()) {
                            String name = activityStringEntry.getKey().getClass().getName();
                            Integer value = hashMap.get(name);
                            if (value == null) {
                                hashMap.put(name, 1);
                            } else {
                                hashMap.put(name, value + 1);
                            }
                        }
                        if (mMemoryListeners.size() > 0) {
                            for (Map.Entry<String, Integer> entry : hashMap.entrySet()) {
                                for (ITrackMemoryListener listener : mMemoryListeners) {
                                    listener.onLeakActivity(entry.getKey(), entry.getValue());
                                }
                            }
                        }
                    } catch (Exception ignored) {
                    }
                }
            }, 10000);
        }

線上選擇監(jiān)測(cè)沒(méi)必要實(shí)時(shí)刃唤,將其延后到APP進(jìn)入后臺(tái)的時(shí)候,在APP進(jìn)入后臺(tái)之后主動(dòng)觸發(fā)一次GC白群,然后延時(shí)10s尚胞,進(jìn)行檢查,之所以延時(shí)10s帜慢,是因?yàn)镚C不是同步的笼裳,為了讓GC操作能夠順利執(zhí)行完,這里選擇10s后檢查粱玲。在檢查前分配一個(gè)4M的大內(nèi)存塊躬柬,再次確保GC執(zhí)行,之后就可以根據(jù)WeakHashMap的特性密幔,查找有多少Activity還保留在其中楔脯,這些Activity就是泄露Activity。

關(guān)于內(nèi)存檢測(cè)

內(nèi)存檢測(cè)比較簡(jiǎn)單胯甩,弄清幾個(gè)關(guān)鍵的指標(biāo)就行昧廷,這些指標(biāo)都能通過(guò) Debug.MemoryInfo獲取

        Debug.MemoryInfo debugMemoryInfo = new Debug.MemoryInfo();
        Debug.getMemoryInfo(debugMemoryInfo);
        appMemory.nativePss = debugMemoryInfo.nativePss >> 10;
        appMemory.dalvikPss = debugMemoryInfo.dalvikPss >> 10;
        appMemory.totalPss = debugMemoryInfo.getTotalPss() >> 10;

這里關(guān)心三個(gè)就行,

  • TotalPss(整體內(nèi)存偎箫,native+dalvik+共享)
  • nativePss (native內(nèi)存)
  • dalvikPss (java內(nèi)存 OOM原因)

一般而言total是大于nativ+dalvik的木柬,因?yàn)樗斯蚕韮?nèi)存,理論上我們只關(guān)心native跟dalvik就行淹办,以上就是關(guān)于內(nèi)存的監(jiān)測(cè)能力眉枕,不過(guò)內(nèi)存泄露不是100%正確,暴露明顯問(wèn)題即可怜森,效果如下:

image

流量監(jiān)測(cè)

流量監(jiān)測(cè)的實(shí)現(xiàn)相對(duì)簡(jiǎn)單速挑,利用系統(tǒng)提供的TrafficStats.getUidRxBytes方法,配合Actvity生命周期副硅,即可獲取每個(gè)Activity的流量消耗姥宝。具體做法:在Activity start的時(shí)候記錄起點(diǎn),在pause的時(shí)候累加恐疲,最后在Destroyed的時(shí)候統(tǒng)計(jì)整個(gè)Activity的流量消耗腊满,如果想要做到Fragment維度,就要具體業(yè)務(wù)具體分析了培己,簡(jiǎn)單實(shí)現(xiàn)如下

   application.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {

        @Override
        public void onActivityStarted(@NonNull Activity activity) {
            super.onActivityStarted(activity);
            <!--開(kāi)始記錄-->
            markActivityStart(activity);
        }

        @Override
        public void onActivityPaused(@NonNull Activity activity) {
            super.onActivityPaused(activity);
            <!--累加-->
            markActivityPause(activity);
        }
        
        @Override
        public void onActivityDestroyed(@NonNull Activity activity) {
            super.onActivityDestroyed(activity);
            <!--統(tǒng)計(jì)結(jié)果碳蛋,并通知回調(diào)-->
            markActivityDestroy(activity);
        }
    };
image
image

電量檢測(cè)

Android電量狀態(tài)能通過(guò)一下方法實(shí)時(shí)獲取,只是對(duì)于分析來(lái)說(shuō)有點(diǎn)麻煩省咨,需要根據(jù)不同手機(jī)肃弟、不同配置做聚合,單處采集很簡(jiǎn)單

            IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
            android.content.Intent batteryStatus = application.registerReceiver(null, filter);
            int status = batteryStatus.getIntExtra("status", 0);
            boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
                    status == BatteryManager.BATTERY_STATUS_FULL;
            int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1);

不過(guò)并不能獲取絕對(duì)電量茸炒,只能看百分比愕乎,因?yàn)閷?duì)單個(gè)Activity來(lái)做電量監(jiān)測(cè)并不靠譜阵苇,往往都是0,可以在APP推到后臺(tái)后感论,對(duì)真?zhèn)€在線時(shí)長(zhǎng)的電池消耗做監(jiān)測(cè)绅项,這個(gè)可能還能看出一些電量變化。

CPU使用監(jiān)測(cè)

沒(méi)想好怎么弄比肄,顯不出力

數(shù)據(jù)整合與基線制定

APP端只是完成的數(shù)據(jù)的采集快耿,數(shù)據(jù)的整合及根系還是要依賴(lài)后臺(tái)數(shù)據(jù)分析,根據(jù)不同配置芳绩,不同場(chǎng)景才能制定一套比較合理的基線掀亥,而且,這種基線肯定不是絕對(duì)的妥色,只能是相對(duì)的搪花,這套基線將來(lái)可以作為頁(yè)面性能評(píng)估標(biāo)準(zhǔn),對(duì)Android而言嘹害,挺難撮竿,機(jī)型太多。

總結(jié)

  • 啟動(dòng)有相對(duì)靠譜節(jié)點(diǎn)
  • 瞬時(shí)FPS(瞬時(shí)掉幀程度)意義更大
  • 內(nèi)存泄露可以一個(gè)WeakHashMap簡(jiǎn)單搞定
  • 電量及CPU還不知道怎么用

GITHUB鏈接 Collie

作者:看書(shū)的小蝸牛
原文鏈接:Android輕量級(jí)APM性能監(jiān)測(cè)方案

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末笔呀,一起剝皮案震驚了整個(gè)濱河市幢踏,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌许师,老刑警劉巖房蝉,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異微渠,居然都是意外死亡恼除,警方通過(guò)查閱死者的電腦和手機(jī)鲁森,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)痊末,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)晤揣,“玉大人,你說(shuō)我怎么就攤上這事纳击。” “怎么了攻臀?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵焕数,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我刨啸,道長(zhǎng)堡赔,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任设联,我火速辦了婚禮善已,結(jié)果婚禮上灼捂,老公的妹妹穿的比我還像新娘。我一直安慰自己换团,他們只是感情好悉稠,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著艘包,像睡著了一般的猛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上想虎,一...
    開(kāi)封第一講書(shū)人閱讀 51,631評(píng)論 1 305
  • 那天卦尊,我揣著相機(jī)與錄音,去河邊找鬼舌厨。 笑死岂却,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的裙椭。 我是一名探鬼主播躏哩,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼骇陈!你這毒婦竟也來(lái)了震庭?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤你雌,失蹤者是張志新(化名)和其女友劉穎器联,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體婿崭,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡拨拓,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了氓栈。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片渣磷。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖授瘦,靈堂內(nèi)的尸體忽然破棺而出醋界,到底是詐尸還是另有隱情,我是刑警寧澤提完,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布形纺,位于F島的核電站,受9級(jí)特大地震影響徒欣,放射性物質(zhì)發(fā)生泄漏逐样。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望脂新。 院中可真熱鬧挪捕,春花似錦、人聲如沸争便。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)始花。三九已至妄讯,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間酷宵,已是汗流浹背亥贸。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留浇垦,地道東北人炕置。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像男韧,于是被迫代替她去往敵國(guó)和親朴摊。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355