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í)流程如下
網(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è)固定值可以接受:
在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插入消息之前秘豹,如下
柵欄消息
重繪CallBack包含多個(gè)Activity的重繪
綜上所述携御,將指標(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)單線上效果如下:
流暢度及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ú)能為力蔫仙,如下示意:
上面那個(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í)幀率的意義更大姻灶,如下
Matrix給的卡頓標(biāo)準(zhǔn):
總之,相比1s平均FPS诈茧,瞬時(shí)掉幀程度的嚴(yán)重性更能反應(yīng)界面流暢程度产喉,因此FPS監(jiān)測(cè)的重點(diǎn)是偵測(cè)瞬時(shí)掉幀程度。
在應(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)單線上效果如下:
內(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)題即可怜森,效果如下:
流量監(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);
}
};
電量檢測(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還不知道怎么用
作者:看書(shū)的小蝸牛
原文鏈接:Android輕量級(jí)APM性能監(jiān)測(cè)方案