Android应用开发性能优化完全分析,完美收官
可以看見,上面這些導(dǎo)致卡頓的原因都是我們平時(shí)開發(fā)中非常常見的。有些人可能會覺得自己的應(yīng)用用著還蠻OK的,其實(shí)那是因?yàn)槟銢]進(jìn)行一些瞬時(shí)測試和壓力測試,一旦在這種環(huán)境下運(yùn)行你的App你就會發(fā)現(xiàn)很多性能問題。
2-3 應(yīng)用UI卡頓分析解決方法
分析UI卡頓我們一般都借助工具,通過工具一般都可以直觀的分析出問題原因,從而反推尋求優(yōu)化方案,具體如下細(xì)說各種強(qiáng)大的工具。
2-3-1 使用HierarchyViewer分析UI性能
我們可以通過SDK提供的工具HierarchyViewer來進(jìn)行UI布局復(fù)雜程度及冗余等分析,如下:
xxx@ThinkPad:~$ hierarchyviewer //通過命令啟動HierarchyViewer
選中一個(gè)Window界面item,然后點(diǎn)擊右上方Hierarchy window或者Pixel Perfect window(這里不介紹,主要用來檢查像素屬性的)即可操作。
先看下Hierarchy window,如下:
一個(gè)Activity的View樹,通過這個(gè)樹可以分析出View嵌套的冗余層級,左下角可以輸入View的id直接自動跳轉(zhuǎn)到中間顯示;Save as PNG用來把左側(cè)樹保存為一張圖片;Capture Layers用來保存psd的PhotoShop分層素材;右側(cè)劇中顯示選中View的當(dāng)前屬性狀態(tài);右下角顯示當(dāng)前View在Activity中的位置等;左下角三個(gè)進(jìn)行切換;Load View Hierarchy用來手動刷新變化(不會自動刷新的)。當(dāng)我們選擇一個(gè)View后會如下圖所示:
類似上圖可以很方便的查看到當(dāng)前View的許多信息;上圖最底那三個(gè)彩色原點(diǎn)代表了當(dāng)前View的性能指標(biāo),從左到右依次代表測量、布局、繪制的渲染時(shí)間,紅色和黃色的點(diǎn)代表速度渲染較慢的View(當(dāng)然了,有些時(shí)候較慢不代表有問題,譬如ViewGroup子節(jié)點(diǎn)越多、結(jié)構(gòu)越復(fù)雜,性能就越差)。
當(dāng)然了,在自定義View的性能調(diào)試時(shí),HierarchyViewer上面的invalidate Layout和requestLayout按鈕的功能更加強(qiáng)大,它可以幫助我們debug自定義View執(zhí)行invalidate()和requestLayout()過程,我們只需要在代碼的相關(guān)地方打上斷點(diǎn)就行了,接下來通過它觀察繪制即可。
可以發(fā)現(xiàn),有了HierarchyViewer調(diào)試工具,我們的UI性能分析變得十分容易,這個(gè)工具也是我們開發(fā)中調(diào)試UI的利器,在平時(shí)寫代碼時(shí)會時(shí)常伴隨我們左右。
2-3-2 使用GPU過度繪制分析UI性能
我們對于UI性能的優(yōu)化還可以通過開發(fā)者選項(xiàng)中的GPU過度繪制工具來進(jìn)行分析。在設(shè)置->開發(fā)者選項(xiàng)->調(diào)試GPU過度繪制(不同設(shè)備可能位置或者叫法不同)中打開調(diào)試后可以看見如下圖(對settings當(dāng)前界面過度繪制進(jìn)行分析):
可以發(fā)現(xiàn),開啟后在我們想要調(diào)試的應(yīng)用界面中可以看到各種顏色的區(qū)域,具體含義如下:
| 顏色 | 含義 |
| — | — |
| 無色 | WebView等的渲染區(qū)域 |
| 藍(lán)色 | 1x過度繪制 |
| 綠色 | 2x過度繪制 |
| 淡紅色 | 3x過度繪制 |
| 紅色 | 4x(+)過度繪制 |
由于過度繪制指在屏幕的一個(gè)像素上繪制多次(譬如一個(gè)設(shè)置了背景色的TextView就會被繪制兩次,一次背景一次文本;這里需要強(qiáng)調(diào)的是Activity設(shè)置的Theme主題的背景不被算在過度繪制層級中),所以最理想的就是繪制一次,也就是藍(lán)色(當(dāng)然這在很多絢麗的界面是不現(xiàn)實(shí)的,所以大家有個(gè)度即可,我們的開發(fā)性能優(yōu)化標(biāo)準(zhǔn)要求最極端界面下紅色區(qū)域不能長期持續(xù)超過屏幕三分之一,可見還是比較寬松的規(guī)定),因此我們需要依據(jù)此顏色分布進(jìn)行代碼優(yōu)化,譬如優(yōu)化布局層級、減少沒必要的背景、暫時(shí)不顯示的View設(shè)置為GONE而不是INVISIBLE、自定義View的onDraw方法設(shè)置canvas.clipRect()指定繪制區(qū)域或通過canvas.quickreject()減少繪制區(qū)域等。
2-3-3 使用GPU呈現(xiàn)模式圖及FPS考核UI性能
Android界面流暢度除過視覺感知以外是可以考核的(測試妹子專用),常見的方法就是通過GPU呈現(xiàn)模式圖或者實(shí)時(shí)FPS顯示進(jìn)行考核,這里我們主要針對GPU呈現(xiàn)模式圖進(jìn)行下說明,因?yàn)镕PS考核測試方法有很多(譬如自己寫代碼實(shí)現(xiàn)、第三方App測試、固件支持等),所以不做統(tǒng)一說明。
通過開發(fā)者選項(xiàng)中GPU呈現(xiàn)模式圖工具來進(jìn)行流暢度考量的流程是(注意:如果是在開啟應(yīng)用后才開啟此功能,記得先把應(yīng)用結(jié)束后重新啟動)在設(shè)置->開發(fā)者選項(xiàng)->GPU呈現(xiàn)模式(不同設(shè)備可能位置或者叫法不同)中打開調(diào)試后可以看見如下圖(對settings當(dāng)前界面上下滑動列表后的圖表):
當(dāng)然,也可以在執(zhí)行完UI滑動操作后在命令行輸入如下命令查看命令行打印的GPU渲染數(shù)據(jù)(分析依據(jù):Draw + Process + Execute = 完整的顯示一幀時(shí)間 < 16ms):
adb shell dumpsys gfxinfo [應(yīng)用包名]
打開上圖可視化工具后,我們可以在手機(jī)畫面上看到豐富的GPU繪制圖形信息,分別展示了StatusBar、NavgationBar、Activity區(qū)域等的GPU渲染時(shí)間信息,隨著界面的刷新,界面上會以實(shí)時(shí)柱狀圖來顯示每幀的渲染時(shí)間,柱狀圖越高表示渲染時(shí)間越長,每個(gè)柱狀圖偏上都有一根代表16ms基準(zhǔn)的綠色橫線,每一條豎著的柱狀線都包含三部分(藍(lán)色代表測量繪制Display List的時(shí)間,紅色代表OpenGL渲染Display List所需要的時(shí)間,黃色代表CPU等待GPU處理的時(shí)間),只要我們每一幀的總時(shí)間低于基準(zhǔn)線就不會發(fā)生UI卡頓問題(個(gè)別超出基準(zhǔn)線其實(shí)也不算啥問題的)。
可以發(fā)現(xiàn),這個(gè)工具是有局限性的,他雖然能夠看出來有幀耗時(shí)超過基準(zhǔn)線導(dǎo)致了丟幀卡頓,但卻分析不到造成丟幀的具體原因。所以說為了配合解決分析UI丟幀卡頓問題我們還需要借助traceview和systrace來進(jìn)行原因追蹤,下面我們會介紹這兩種工具的。
2-3-4 使用Lint進(jìn)行資源及冗余UI布局等優(yōu)化
上面說了,冗余資源及邏輯等也可能會導(dǎo)致加載和執(zhí)行緩慢,所以我們就來看看Lint這個(gè)工具是如何發(fā)現(xiàn)優(yōu)化這些問題的(當(dāng)然了,Lint實(shí)際的功能是非常強(qiáng)大的,我們開發(fā)中也是經(jīng)常使用它來發(fā)現(xiàn)一些問題的,這里主要有點(diǎn)針對UI性能的說明了,其他的雷同)。
在Android Studio 1.4版本中使用Lint最簡單的辦法就是將鼠標(biāo)放在代碼區(qū)點(diǎn)擊右鍵->Analyze->Inspect Code–>界面選擇你要檢測的模塊->點(diǎn)擊確認(rèn)開始檢測,等待一下后會發(fā)現(xiàn)如下結(jié)果:
可以看見,Lint檢測完后給了我們很多建議的,我們重點(diǎn)看一個(gè)關(guān)于UI性能的檢測結(jié)果;上圖中高亮的那一行明確說明了存在冗余的UI層級嵌套,所以我們是可以點(diǎn)擊跳進(jìn)去進(jìn)行優(yōu)化處理掉的。
當(dāng)然了,Lint還有很多功能,大家可以自行探索發(fā)揮,這里只是達(dá)到拋磚引玉的作用。
2-3-5 使用Memory監(jiān)測及GC打印與Allocation Tracker進(jìn)行UI卡頓分析
關(guān)于Android的內(nèi)存管理機(jī)制下面的一節(jié)會詳細(xì)介紹,這里我們主要針對GC導(dǎo)致的UI卡頓問題進(jìn)行詳細(xì)說明。
Android系統(tǒng)會依據(jù)內(nèi)存中不同的內(nèi)存數(shù)據(jù)類型分別執(zhí)行不同的GC操作,常見應(yīng)用開發(fā)中導(dǎo)致GC頻繁執(zhí)行的原因主要可能是因?yàn)槎虝r(shí)間內(nèi)有大量頻繁的對象創(chuàng)建與釋放操作,也就是俗稱的內(nèi)存抖動現(xiàn)象,或者短時(shí)間內(nèi)已經(jīng)存在大量內(nèi)存暫用介于閾值邊緣,接著每當(dāng)有新對象創(chuàng)建時(shí)都會導(dǎo)致超越閾值觸發(fā)GC操作。
如下是我工作中一個(gè)項(xiàng)目的一次經(jīng)歷(我將代碼回退特意抓取的),出現(xiàn)這個(gè)問題的場景是一次壓力測試導(dǎo)致整個(gè)系統(tǒng)卡頓,瞬間殺掉應(yīng)用就OK了,究其原因最終查到是一個(gè)API的調(diào)運(yùn)位置寫錯(cuò)了方式,導(dǎo)致一直被狂調(diào),當(dāng)普通使用時(shí)不會有問題,壓力測試必現(xiàn)卡頓。具體內(nèi)存參考圖如下:
與此抖動圖對應(yīng)的LogCat抓取如下:
//截取其中比較密集一段LogCat,與上圖Memory檢測到的抖動圖對應(yīng),其中xxx為應(yīng)用包名
…
10-06 00:59:45.619 xxx I/art: Explicit concurrent mark sweep GC freed 72515(3MB) AllocSpace objects, 65(2028KB) LOS objects, 80% free, 17MB/89MB, paused 3.505ms total 60.958ms
10-06 00:59:45.749 xxx I/art: Explicit concurrent mark sweep GC freed 5396(193KB) AllocSpace objects, 0(0B) LOS objects, 75% free, 23MB/95MB, paused 2.079ms total 100.522ms
…
10-06 00:59:48.059 xxx I/art: Explicit concurrent mark sweep GC freed 4693(172KB) AllocSpace objects, 0(0B) LOS objects, 75% free, 23MB/95MB, paused 2.227ms total 101.692ms
…
我們知道,類似上面logcat打印一樣,觸發(fā)垃圾回收的主要原因有以下幾種:
-
GC_MALLOC——內(nèi)存分配失敗時(shí)觸發(fā);
-
GC_CONCURRENT——當(dāng)分配的對象大小超過一個(gè)限定值(不同系統(tǒng))時(shí)觸發(fā);
-
GC_EXPLICIT——對垃圾收集的顯式調(diào)用(System.gc()) ;
-
GC_EXTERNAL_ALLOC——外部內(nèi)存分配失敗時(shí)觸發(fā);
可以看見,這種不停的大面積打印GC導(dǎo)致所有線程暫停的操作必定會導(dǎo)致UI視覺的卡頓,所以我們要避免此類問題的出現(xiàn),具體的常見優(yōu)化方式如下:
-
檢查代碼,盡量避免有些頻繁觸發(fā)的邏輯方法中存在大量對象分配;
-
盡量避免在多次for循環(huán)中頻繁分配對象;
-
避免在自定義View的onDraw()方法中執(zhí)行復(fù)雜的操作及創(chuàng)建對象(譬如Paint的實(shí)例化操作不要寫在onDraw()方法中等);
-
對于并發(fā)下載等類似邏輯的實(shí)現(xiàn)盡量避免多次創(chuàng)建線程對象,而是交給線程池處理。
當(dāng)然了,有了上面說明GC導(dǎo)致的性能后我們就該定位分析問題了,可以通過運(yùn)行DDMS->Allocation Tracker標(biāo)簽打開一個(gè)新窗口,然后點(diǎn)擊Start Tracing按鈕,接著運(yùn)行你想分析的代碼,運(yùn)行完畢后點(diǎn)擊Get Allocations按鈕就能夠看見一個(gè)已分配對象的列表,如下:
點(diǎn)擊上面第一個(gè)表格中的任何一項(xiàng)就能夠在第二個(gè)表格中看見導(dǎo)致該內(nèi)存分配的棧信息,通過這個(gè)工具我們可以很方便的知道代碼分配了哪類對象、在哪個(gè)線程、哪個(gè)類、哪個(gè)文件的哪一行。譬如我們可以通過Allocation Tracker分別做一次Paint對象實(shí)例化在onDraw與構(gòu)造方法的一個(gè)自定義View的內(nèi)存跟蹤,然后你就明白這個(gè)工具的強(qiáng)大了。
PS一句,Android Studio新版本除過DDMS以外在Memory視圖的左側(cè)已經(jīng)集成了Allocation Tracker功能,只是用起來還是沒有DDMS的方便實(shí)用,如下圖:
2-3-6 使用Traceview和dmtracedump進(jìn)行分析優(yōu)化
關(guān)于UI卡頓問題我們還可以通過運(yùn)行Traceview工具進(jìn)行分析,他是一個(gè)分析器,記錄了應(yīng)用程序中每個(gè)函數(shù)的執(zhí)行時(shí)間;我們可以打開DDMS然后選擇一個(gè)進(jìn)程,接著點(diǎn)擊上面的“Start Method Profiling”按鈕(紅色小點(diǎn)變?yōu)楹谏撮_始運(yùn)行),然后操作我們的卡頓UI(小范圍測試,所以操作最好不要超過5s),完事再點(diǎn)一下剛才按的那個(gè)按鈕,稍等片刻即可出現(xiàn)下圖,如下:
花花綠綠的一幅圖我們怎么分析呢?下面我們解釋下如何通過該工具定位問題:
整個(gè)界面包括上下兩部分,上面是你測試的進(jìn)程中每個(gè)線程運(yùn)行的時(shí)間線,下面是每個(gè)方法(包含parent及child)執(zhí)行的各個(gè)指標(biāo)的值。通過上圖的時(shí)間面板可以直觀發(fā)現(xiàn),整個(gè)trace時(shí)間段main線程做的事情特別多,其他的做的相對較少。當(dāng)我們選擇上面的一個(gè)線程后可以發(fā)現(xiàn)下面的性能面板很復(fù)雜,其實(shí)這才是TraceView的核心圖表,它主要展示了線程中各個(gè)方法的調(diào)用信息(CPU使用時(shí)間、調(diào)用次數(shù)等),這些信息就是我們分析UI性能卡頓的核心關(guān)注點(diǎn),所以我們先看幾個(gè)重要的屬性說明,如下:
| 屬性名 | 含義 |
| — | — |
| name | 線程中調(diào)運(yùn)的方法名; |
| Incl CPU Time | 當(dāng)前方法(包含內(nèi)部調(diào)運(yùn)的子方法)執(zhí)行占用的CPU時(shí)間; |
| Excl CPU Time | 當(dāng)前方法(不包含內(nèi)部調(diào)運(yùn)的子方法)執(zhí)行占用的CPU時(shí)間; |
| Incl Real Time | 當(dāng)前方法(包含內(nèi)部調(diào)運(yùn)的子方法)執(zhí)行的真實(shí)時(shí)間,ms單位; |
| Excl Real Time | 當(dāng)前方法(不包含內(nèi)部調(diào)運(yùn)的子方法)執(zhí)行的真實(shí)時(shí)間,ms單位; |
| Calls+Recur Calls/Total | 當(dāng)前方法被調(diào)運(yùn)的次數(shù)及遞歸調(diào)運(yùn)占總調(diào)運(yùn)次數(shù)百分比; |
| CPU Time/Call | 當(dāng)前方法調(diào)運(yùn)CPU時(shí)間與調(diào)運(yùn)次數(shù)比,即當(dāng)前方法平均執(zhí)行CPU耗時(shí)時(shí)間; |
| Real Time/Call | 當(dāng)前方法調(diào)運(yùn)真實(shí)時(shí)間與調(diào)運(yùn)次數(shù)比,即當(dāng)前方法平均執(zhí)行真實(shí)耗時(shí)時(shí)間;(重點(diǎn)關(guān)注) |
有了對上面Traceview圖表的一個(gè)認(rèn)識之后我們就來看看具體導(dǎo)致UI性能后該如何切入分析,一般Traceview可以定位兩類性能問題:
-
方法調(diào)運(yùn)一次需要耗費(fèi)很長時(shí)間導(dǎo)致卡頓;
-
方法調(diào)運(yùn)一次耗時(shí)不長,但被頻繁調(diào)運(yùn)導(dǎo)致累計(jì)時(shí)長卡頓。
譬如我們來舉個(gè)實(shí)例,有時(shí)候我們寫完App在使用時(shí)不覺得有啥大的影響,但是當(dāng)我們啟動完App后靜止在那卻十分費(fèi)電或者導(dǎo)致設(shè)備發(fā)熱,這種情況我們就可以打開Traceview然后按照Cpu Time/Call或者Real Time/Call進(jìn)行降序排列,然后打開可疑的方法及其child進(jìn)行分析查看,然后再回到代碼定位檢查邏輯優(yōu)化即可;當(dāng)然了,我們也可以通過該工具來trace我們自定義View的一些方法來權(quán)衡性能問題,這里不再一一列舉嘍。
可以看見,Traceview能夠幫助我們分析程序性能,已經(jīng)很方便了,然而Traceview家族還有一個(gè)更加直觀強(qiáng)大的小工具,那就是可以通過dmtracedump生成方法調(diào)用圖。具體做法如下:
dmtracedump -g result.png target.trace //結(jié)果png文件 目標(biāo)trace文件
通過這個(gè)生成的方法調(diào)運(yùn)圖我們可以更加直觀的發(fā)現(xiàn)一些方法的調(diào)運(yùn)異常現(xiàn)象。不過本人優(yōu)化到現(xiàn)在還沒怎么用到它,每次用到Traceview分析就已經(jīng)搞定問題了,所以說dmtracedump自己酌情使用吧。
PS一句,Android Studio新版本除過DDMS以外在CPU視圖的左側(cè)已經(jīng)集成了Traceview(start Method Tracing)功能,只是用起來還是沒有DDMS的方便實(shí)用(這里有一篇AS MT個(gè)人覺得不錯(cuò)的分析文章(引用自網(wǎng)絡(luò),鏈接屬于原作者功勞)),如下圖:
2-3-7 使用Systrace進(jìn)行分析優(yōu)化
Systrace其實(shí)有些類似Traceview,它是對整個(gè)系統(tǒng)進(jìn)行分析(同一時(shí)間軸包含應(yīng)用及SurfaceFlinger、WindowManagerService等模塊、服務(wù)運(yùn)行信息),不過這個(gè)工具需要你的設(shè)備內(nèi)核支持trace(命令行檢查/sys/kernel/debug/tracing)且設(shè)備是eng或userdebug版本才可以,所以使用前麻煩自己確認(rèn)一下。
我們在分析UI性能時(shí)一般只關(guān)注圖形性能(所以必須選擇Graphics和View,其他隨意),同時(shí)一般對于卡頓的抓取都是5s,最多10s。啟動Systrace進(jìn)行數(shù)據(jù)抓取可以通過兩種方式,命令行方式如下:
python systrace.py --time=10 -o mynewtrace.html sched gfx view wm
圖形模式:
打開DDMS->Capture system wide trace using Android systrace->設(shè)置時(shí)間與選項(xiàng)點(diǎn)擊OK就開始了抓取,接著操作APP,完事生成一個(gè)trace.html文件,用Chrome打開即可如下圖:
在Chrome中瀏覽分析該文件我們可以通過鍵盤的W-A-S-D鍵來搞定,由于上面我們在進(jìn)行trace時(shí)選擇了一些選項(xiàng),所以上圖生成了左上方相關(guān)的CPU頻率、負(fù)載、狀態(tài)等信息,其中的CPU N代表了CPU核數(shù),每個(gè)CPU行的柱狀圖表代表了當(dāng)前時(shí)間段當(dāng)前核上的運(yùn)行信息;下面我們再來看看SurfaceFlinger的解釋,如下:
可以看見上面左邊欄的SurfaceFlinger其實(shí)就是負(fù)責(zé)繪制Android程序UI的服務(wù),所以SurfaceFlinger能反應(yīng)出整體繪制情況,可以關(guān)注上圖VSYNC-app一行可以發(fā)現(xiàn)前5s多基本都能夠達(dá)到16ms刷新間隔,5s多開始到7s多大于了15ms,說明此時(shí)存在繪制丟幀卡頓;同時(shí)可以發(fā)現(xiàn)surfaceflinger一行明顯存在類似不規(guī)律間隔,這是因?yàn)橛械牡胤绞遣恍枰匦落秩綰I,所以有大范圍不規(guī)律,有的是因?yàn)樽枞麑?dǎo)致不規(guī)律,明顯可以發(fā)現(xiàn)0到4s間大多是不需要渲染,而5s以后大多是阻塞導(dǎo)致;對應(yīng)這個(gè)時(shí)間點(diǎn)我們放大可以看到每個(gè)部分所使用的時(shí)間和正在執(zhí)行的任務(wù),具體如下:
可以發(fā)現(xiàn)具體的執(zhí)行明顯存在超時(shí)性能卡頓(原點(diǎn)不是綠色的基本都代表存在一定問題,下面和右側(cè)都會提示你選擇的幀相關(guān)詳細(xì)信息或者alert信息),但是遺憾的是通過Systrace只能大體上發(fā)現(xiàn)是否存在性能問題,具體問題還需要通過Traceview或者代碼中嵌入Trace工具類等去繼續(xù)詳細(xì)分析,總之很蛋疼。
PS:如果你想使用Systrace很輕松的分析定位所有問題,看明白所有的行含義,你還需要具備非常扎實(shí)的Android系統(tǒng)框架的原理才可以將該工具使用的得心應(yīng)手。
2-3-8 使用traces.txt文件進(jìn)行ANR分析優(yōu)化
ANR(Application Not Responding)是Android中AMS與WMS監(jiān)測應(yīng)用響應(yīng)超時(shí)的表現(xiàn);之所以把臭名昭著的ANR單獨(dú)作為UI性能卡頓的分析來說明是因?yàn)锳NR是直接卡死UI不動且必須要解掉的Bug,我們必須盡量在開發(fā)時(shí)避免他的出現(xiàn),當(dāng)然了,萬一出現(xiàn)了那就用下面介紹的方法來分析吧。
我們應(yīng)用開發(fā)中常見的ANR主要有如下幾類:
-
按鍵觸摸事件派發(fā)超時(shí)ANR,一般閾值為5s(設(shè)置中開啟ANR彈窗,默認(rèn)有事件派發(fā)才會觸發(fā)彈框ANR);
-
廣播阻塞ANR,一般閾值為10s(設(shè)置中開啟ANR彈窗,默認(rèn)不彈框,只有l(wèi)og提示);
-
服務(wù)超時(shí)ANR,一般閾值為20s(設(shè)置中開啟ANR彈窗,默認(rèn)不彈框,只有l(wèi)og提示);
當(dāng)ANR發(fā)生時(shí)除過logcat可以看見的log以外我們還可以在系統(tǒng)指定目錄下找到traces文件或dropbox文件進(jìn)行分析,發(fā)生ANR后我們可以通過如下命令得到ANR trace文件:
adb pull /data/anr/traces.txt ./
然后我們用txt編輯器打開可以發(fā)現(xiàn)如下結(jié)構(gòu)分析:
//顯示進(jìn)程id、ANR發(fā)生時(shí)間點(diǎn)、ANR發(fā)生進(jìn)程包名
----- pid 19073 at 2015-10-08 17:24:38 -----
Cmd line: com.example.yanbo.myapplication
//一些GC等object信息,通常可以忽略
…
//ANR方法堆棧打印信息!重點(diǎn)!
DALVIK THREADS (18):
“main” prio=5 tid=1 Sleeping
| group=“main” sCount=1 dsCount=0 obj=0x7497dfb8 self=0x7f9d09a000
| sysTid=19073 nice=0 cgrp=default sched=0/0 handle=0x7fa106c0a8
| state=S schedstat=( 125271779 68162762 280 ) utm=11 stm=1 core=0 HZ=100
| stack=0x7fe90d3000-0x7fe90d5000 stackSize=8MB
| held mutexes=
at java.lang.Thread.sleep!(Native method)
- sleeping on <0x0a2ae345> (a java.lang.Object)
at java.lang.Thread.sleep(Thread.java:1031)
- locked <0x0a2ae345> (a java.lang.Object)
//真正導(dǎo)致ANR的問題點(diǎn),可以發(fā)現(xiàn)是onClick中有sleep導(dǎo)致。我們平時(shí)可以類比分析即可,這里不詳細(xì)說明。
at java.lang.Thread.sleep(Thread.java:985)
at com.example.yanbo.myapplication.MainActivity$1.onClick(MainActivity.java:21)
at android.view.View.performClick(View.java:4908)
at android.view.View$PerformClick.run(View.java:20389)
at android.os.Handler.handleCallback(Handler.java:815)
at android.os.Handler.dispatchMessage(Handler.java:104)
at android.os.Looper.loop(Looper.java:194)
at android.app.ActivityThread.main(ActivityThread.java:5743)
at java.lang.reflect.Method.invoke!(Native method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:988)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:783)
…
//省略一些不常關(guān)注堆棧打印
…
至此常見的應(yīng)用開發(fā)中ANR分析定位就可以解決了。
2-4 應(yīng)用UI性能分析解決總結(jié)
可以看見,關(guān)于Android UI卡頓的性能分析還是有很多工具的,上面只是介紹了應(yīng)用開發(fā)中我們經(jīng)常使用的一些而已,還有一些其他的,譬如Oprofile等工具不怎么常用,這里就不再詳細(xì)介紹。
通過上面UI性能的原理、原因、工具分析總結(jié)可以發(fā)現(xiàn),我們在開發(fā)應(yīng)用時(shí)一定要時(shí)刻重視性能問題,如若真的沒留意出現(xiàn)了性能問題,不妨使用上面的一些案例方式進(jìn)行分析。但是那終歸是補(bǔ)救措施,在我們知道上面UI卡頓原理之后我們應(yīng)該盡量從項(xiàng)目代碼架構(gòu)搭建及編寫時(shí)就避免一些UI性能問題,具體項(xiàng)目中常見的注意事項(xiàng)如下:
-
布局優(yōu)化;盡量使用include、merge、ViewStub標(biāo)簽,盡量不存在冗余嵌套及過于復(fù)雜布局(譬如10層就會直接異常),盡量使用GONE替換INVISIBLE,使用weight后盡量將width和heigh設(shè)置為0dp減少運(yùn)算,Item存在非常復(fù)雜的嵌套時(shí)考慮使用自定義Item View來取代,減少measure與layout次數(shù)等。
-
列表及Adapter優(yōu)化;盡量復(fù)用getView方法中的相關(guān)View,不重復(fù)獲取實(shí)例導(dǎo)致卡頓,列表盡量在滑動過程中不進(jìn)行UI元素刷新等。
-
背景和圖片等內(nèi)存分配優(yōu)化;盡量減少不必要的背景設(shè)置,圖片盡量壓縮處理顯示,盡量避免頻繁內(nèi)存抖動等問題出現(xiàn)。
-
自定義View等繪圖與布局優(yōu)化;盡量避免在draw、measure、layout中做過于耗時(shí)及耗內(nèi)存操作,尤其是draw方法中,盡量減少draw、measure、layout等執(zhí)行次數(shù)。
-
避免ANR,不要在UI線程中做耗時(shí)操作,遵守ANR規(guī)避守則,譬如多次數(shù)據(jù)庫操作等。
當(dāng)然了,上面只是列出了我們項(xiàng)目中常見的一些UI性能注意事項(xiàng)而已,相信還有很多其他的情況這里沒有說到,歡迎補(bǔ)充。還有一點(diǎn)就是我們上面所謂的UI性能優(yōu)化分析總結(jié)等都是建議性的,因?yàn)樾阅苓@個(gè)問題是一個(gè)涉及面很廣很泛的問題,有些優(yōu)化不是必需的,有些優(yōu)化是必需的,有些優(yōu)化掉以后又是得不償失的,所以我們一般著手解決那些必須的就可以了。
【工匠若水 http://blog.csdn.net/yanbober 轉(zhuǎn)載請注明出處。點(diǎn)我開始Android技術(shù)交流】
3 應(yīng)用開發(fā)Memory內(nèi)存性能分析優(yōu)化
========================
說完了應(yīng)用開發(fā)中的UI性能問題后我們就該來關(guān)注應(yīng)用開發(fā)中的另一個(gè)重要、嚴(yán)重、非常重要的性能問題了,那就是內(nèi)存性能優(yōu)化分析。Android其實(shí)就是嵌入式設(shè)備,嵌入式設(shè)備核心關(guān)注點(diǎn)之一就是內(nèi)存資源;有人說現(xiàn)在的設(shè)備都在堆硬件配置(譬如國產(chǎn)某米的某兔跑分手機(jī)、盒子等),所以內(nèi)存不會再像以前那么緊張了,其實(shí)這句話聽著沒錯(cuò),但為啥再牛逼配置的Android設(shè)備上有些應(yīng)用還是越用系統(tǒng)越卡呢?這里面的原因有很多,不過相信有了這一章下面的內(nèi)容分析,作為一個(gè)移動開發(fā)者的你就有能力打理好自己應(yīng)用的那一畝三分地內(nèi)存了,能做到這樣就足以了。關(guān)于Android內(nèi)存優(yōu)化,這里有一篇Google的官方指導(dǎo)文檔,但是本文為自己項(xiàng)目摸索,會有很多不一樣的地方。
3-1 Android內(nèi)存管理原理
系統(tǒng)級內(nèi)存管理:
Android系統(tǒng)內(nèi)核是基于Linux,所以說Android的內(nèi)存管理其實(shí)也是Linux的升級版而已。Linux在進(jìn)程停止后就結(jié)束該進(jìn)程,而Android把這些停止的進(jìn)程都保留在內(nèi)存中,直到系統(tǒng)需要更多內(nèi)存時(shí)才選擇性的釋放一些,保留在內(nèi)存中的進(jìn)程默認(rèn)(不包含后臺service與Thread等單獨(dú)UI線程的進(jìn)程)不會影響整體系統(tǒng)的性能(速度與電量等)且當(dāng)再次啟動這些保留在內(nèi)存的進(jìn)程時(shí)可以明顯提高啟動速度,不需要再去加載。
再直白點(diǎn)就是說Android系統(tǒng)級內(nèi)存管理機(jī)制其實(shí)類似于Java的垃圾回收機(jī)制,這下明白了吧;在Android系統(tǒng)中框架會定義如下幾類進(jìn)程、在系統(tǒng)內(nèi)存達(dá)到規(guī)定的不同level閾值時(shí)觸發(fā)清空不同level的進(jìn)程類型。
可以看見,所謂的我們的Service在后臺跑著跑著掛了,或者盒子上有些大型游戲啟動起來就掛(之前我在上家公司做盒子時(shí)遇見過),有一個(gè)直接的原因就是這個(gè)閾值定義的太大,導(dǎo)致系統(tǒng)一直認(rèn)為已經(jīng)達(dá)到閾值,所以進(jìn)行優(yōu)先清除了符合類型的進(jìn)程。所以說,該閾值的設(shè)定是有一些講究的,額,扯多了,我們主要是針對應(yīng)用層內(nèi)存分析的,系統(tǒng)級內(nèi)存回收了解這些就基本夠解釋我們應(yīng)用在設(shè)備上的一些表現(xiàn)特征了。
應(yīng)用級內(nèi)存管理:
在說應(yīng)用級別內(nèi)存管理原理時(shí)大家先想一個(gè)問題,假設(shè)有一個(gè)內(nèi)存為1G的Android設(shè)備,上面運(yùn)行了一個(gè)非常非常吃內(nèi)存的應(yīng)用,如果沒有任何機(jī)制的情況下是不是用著用著整個(gè)設(shè)備會因?yàn)槲覀冞@個(gè)應(yīng)用把1G內(nèi)存吃光然后整個(gè)系統(tǒng)運(yùn)行癱瘓呢?
哈哈,其實(shí)Google的工程師才不會這么傻的把系統(tǒng)設(shè)計(jì)這么差勁。為了使系統(tǒng)不存在我們上面假想情況且能安全快速的運(yùn)行,Android的框架使得每個(gè)應(yīng)用程序都運(yùn)行在單獨(dú)的進(jìn)程中(這些應(yīng)用進(jìn)程都是由Zygote進(jìn)程孵化出來的,每個(gè)應(yīng)用進(jìn)程都對應(yīng)自己唯一的虛擬機(jī)實(shí)例);如果應(yīng)用在運(yùn)行時(shí)再存在上面假想的情況,那么癱瘓的只會是自己的進(jìn)程,不會直接影響系統(tǒng)運(yùn)行及其他進(jìn)程運(yùn)行。
既然每個(gè)Android應(yīng)用程序都執(zhí)行在自己的虛擬機(jī)中,那了解Java的一定明白,每個(gè)虛擬機(jī)必定會有堆內(nèi)存閾值限制(值得一提的是這個(gè)閾值一般都由廠商依據(jù)硬件配置及設(shè)備特性自己設(shè)定,沒有統(tǒng)一標(biāo)準(zhǔn),可以為64M,也可以為128M等;它的配置是在Android的屬性系統(tǒng)的/system/build.prop中配置dalvik.vm.heapsize=128m即可,若存在dalvik.vm.heapstartsize則表示初始申請大小),也即一個(gè)應(yīng)用進(jìn)程同時(shí)存在的對象必須小于閾值規(guī)定的內(nèi)存大小才可以正常運(yùn)行。
接著我們運(yùn)行的App在自己的虛擬機(jī)中內(nèi)存管理基本就是遵循Java的內(nèi)存管理機(jī)制了,系統(tǒng)在特定的情況下主動進(jìn)行垃圾回收。但是要注意的一點(diǎn)就是在Android系統(tǒng)中執(zhí)行垃圾回收(GC)操作時(shí)所有線程(包含UI線程)都必須暫停,等垃圾回收操作完成之后其他線程才能繼續(xù)運(yùn)行。這些GC垃圾回收一般都會有明顯的log打印出回收類型,常見的如下:
-
GC_MALLOC——內(nèi)存分配失敗時(shí)觸發(fā);
-
GC_CONCURRENT——當(dāng)分配的對象大小超過一個(gè)限定值(不同系統(tǒng))時(shí)觸發(fā);
-
GC_EXPLICIT——對垃圾收集的顯式調(diào)用(System.gc()) ;
-
GC_EXTERNAL_ALLOC——外部內(nèi)存分配失敗時(shí)觸發(fā);
通過上面這幾點(diǎn)的分析可以發(fā)現(xiàn),應(yīng)用的內(nèi)存管理其實(shí)就是一個(gè)蘿卜一個(gè)坑,坑都一般大,你在開發(fā)應(yīng)用時(shí)要保證的是內(nèi)存使用同一時(shí)刻不能超過坑的大小,否則就裝不下了。
3-2 Android內(nèi)存泄露性能分析
有了關(guān)于Android的一些內(nèi)存認(rèn)識,接著我們來看看關(guān)于Android應(yīng)用開發(fā)中常出現(xiàn)的一種內(nèi)存問題—-內(nèi)存泄露。
3-2-1 Android應(yīng)用內(nèi)存泄露概念
眾所周知,在Java中有些對象的生命周期是有限的,當(dāng)它們完成了特定的邏輯后將會被垃圾回收;但是,如果在對象的生命周期本來該被垃圾回收時(shí)這個(gè)對象還被別的對象所持有引用,那就會導(dǎo)致內(nèi)存泄漏;這樣的后果就是隨著我們的應(yīng)用被長時(shí)間使用,他所占用的內(nèi)存越來越大。如下就是一個(gè)最常見簡單的泄露例子(其它的泄露不再一一列舉了):
public final class MainActivity extends Activity {
private DbManager mDbManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//DbManager是一個(gè)單例模式類,這樣就持有了MainActivity引用,導(dǎo)致泄露
mDbManager = DbManager.getInstance(this);
}
}
可以看見,上面例子中我們讓一個(gè)單例模式的對象持有了當(dāng)前Activity的強(qiáng)引用,那在當(dāng)前Acvitivy執(zhí)行完onDestroy()后,這個(gè)Activity就無法得到垃圾回收,也就造成了內(nèi)存泄露。
內(nèi)存泄露可以引發(fā)很多的問題,常見的內(nèi)存泄露導(dǎo)致問題如下:
-
應(yīng)用卡頓,響應(yīng)速度慢(內(nèi)存占用高時(shí)JVM虛擬機(jī)會頻繁觸發(fā)GC);
-
應(yīng)用被從后臺進(jìn)程干為空進(jìn)程(上面系統(tǒng)內(nèi)存原理有介紹,也就是超過了閾值);
-
應(yīng)用莫名的崩潰(上面應(yīng)用內(nèi)存原理有介紹,也就是超過了閾值OOM);
造成內(nèi)存泄露泄露的最核心原理就是一個(gè)對象持有了超過自己生命周期以外的對象強(qiáng)引用導(dǎo)致該對象無法被正常垃圾回收;可以發(fā)現(xiàn),應(yīng)用內(nèi)存泄露是個(gè)相當(dāng)棘手重要的問題,我們必須重視。
3-2-2 Android應(yīng)用內(nèi)存泄露察覺手段
知道了內(nèi)存泄露的概念之后肯定就是想辦法來確認(rèn)自己的項(xiàng)目是否存在內(nèi)存泄露了,那該如何察覺自己項(xiàng)目是否存在內(nèi)存泄露呢?如下提供了幾種常用的方式:
| 察覺方式 | 場景 |
| — | — |
| AS的Memory窗口 | 平時(shí)用來直觀了解自己應(yīng)用的全局內(nèi)存情況,大的泄露才能有感知。 |
| DDMS-Heap內(nèi)存監(jiān)測工具 | 同上,大的泄露才能有感知。 |
| dumpsys meminfo命令 | 常用方式,可以很直觀的察覺一些泄露,但不全面且常規(guī)足夠用。 |
| leakcanary神器 | 比較強(qiáng)大,可以感知泄露且定位泄露;實(shí)質(zhì)是MAT原理,只是更加自動化了,當(dāng)現(xiàn)有代碼量已經(jīng)龐大成型,且無法很快察覺掌控全局代碼時(shí)極力推薦;或者是偶現(xiàn)泄露的情況下極力推薦。 |
AS的Memory窗口如下,詳細(xì)的說明這里就不解釋了,很簡單很直觀(使用頻率高):
DDMS-Heap內(nèi)存監(jiān)測工具窗口如下,詳細(xì)的說明這里就不解釋了,很簡單(使用頻率不高):
dumpsys meminfo命令如下(使用頻率非常高,非常高效,我的最愛之一,平時(shí)一般關(guān)注幾個(gè)重要的Object個(gè)數(shù)即可判斷一般的泄露;當(dāng)然了,adb shell dumpsys meminfo不跟參數(shù)直接展示系統(tǒng)所有內(nèi)存狀態(tài)):
leakcanary神器使用這里先不說,下文會專題介紹,你會震撼的一B。有了這些工具的定位我們就能很方便的察覺我們App的內(nèi)存泄露問題,察覺到以后該怎么定位分析呢,繼續(xù)往下看。
3-2-3 Android應(yīng)用內(nèi)存泄露leakcanary工具定位分析
leakcanary是一個(gè)開源項(xiàng)目,一個(gè)內(nèi)存泄露自動檢測工具,是著名的GitHub開源組織Square貢獻(xiàn)的,它的主要優(yōu)勢就在于自動化過早的發(fā)覺內(nèi)存泄露、配置簡單、抓取貼心,缺點(diǎn)在于還存在一些bug,不過正常使用百分之九十情況是OK的,其核心原理與MAT工具類似。
關(guān)于leakcanary工具的配置使用方式這里不再詳細(xì)介紹,因?yàn)檎娴暮芎唵?#xff0c;詳情點(diǎn)我參考官方教程學(xué)習(xí)使用即可。
PS:之前在優(yōu)化性能時(shí)發(fā)現(xiàn)我們有一個(gè)應(yīng)用有兩個(gè)界面退出后Activity沒有被回收(dumpsys meminfo發(fā)現(xiàn)一直在加),所以就懷疑可能存在內(nèi)存泄露。但是問題來了,這兩個(gè)Activity的邏輯十分復(fù)雜,代碼也不是我寫的,相關(guān)聯(lián)的代碼量也十分龐大,更加郁悶的是很難判斷是哪個(gè)版本修改導(dǎo)致的,這時(shí)候只知道有泄露,卻無法定位具體原因,使用MAT分析解決掉了一個(gè)可疑泄露后發(fā)現(xiàn)泄露又變成了概率性的。可以發(fā)現(xiàn),對于這種概率性的泄露用MAT去主動抓取肯定是很耗時(shí)耗力的,所以決定直接引入leakcanary神器來檢測項(xiàng)目,后來很快就徹底解決了項(xiàng)目中所有必現(xiàn)的、偶現(xiàn)的內(nèi)存泄露。
總之一點(diǎn),工具再強(qiáng)大也只是幫我們定位可能的泄露點(diǎn),而最核心的GC ROOT泄露信息推導(dǎo)出泄露問題及如何解決還是需要你把住代碼邏輯及泄露核心概念去推理解決。
3-2-4 Android應(yīng)用內(nèi)存泄露MAT工具定位分析
Eclipse Memory Analysis Tools(點(diǎn)我下載)是一個(gè)專門分析Java堆數(shù)據(jù)內(nèi)存引用的工具,我們可以使用它方便的定位內(nèi)存泄露原因,核心任務(wù)就是找到GC ROOT位置即可,哎呀,關(guān)于這個(gè)工具的使用我是真的不想說了,自己搜索吧,實(shí)在簡單、傳統(tǒng)的不行了。
PS:這是開發(fā)中使用頻率非常高的一個(gè)工具之一,麻煩務(wù)必掌握其核心使用技巧,雖然Android Studio已經(jīng)實(shí)現(xiàn)了部分功能,但是真的很難用,遇到問題目前還是使用Eclipse Memory Analysis Tools吧。
原諒我該小節(jié)的放蕩不羈!!!!(其實(shí)我是困了,嗚嗚!)
3-2-5 Android應(yīng)用開發(fā)規(guī)避內(nèi)存泄露建議
有了上面的原理及案例處理其實(shí)還不夠,因?yàn)樯厦孢@些處理辦法是補(bǔ)救的措施,我們正確的做法應(yīng)該是在開發(fā)過程中就養(yǎng)成良好的習(xí)慣和敏銳的嗅覺才對,所以下面給出一些應(yīng)用開發(fā)中常見的規(guī)避內(nèi)存泄露建議:
-
Context使用不當(dāng)造成內(nèi)存泄露;不要對一個(gè)Activity Context保持長生命周期的引用(譬如上面概念部分給出的示例)。盡量在一切可以使用應(yīng)用ApplicationContext代替Context的地方進(jìn)行替換(原理我前面有一篇關(guān)于Context的文章有解釋)。
-
非靜態(tài)內(nèi)部類的靜態(tài)實(shí)例容易造成內(nèi)存泄漏;即一個(gè)類中如果你不能夠控制它其中內(nèi)部類的生命周期(譬如Activity中的一些特殊Handler等),則盡量使用靜態(tài)類和弱引用來處理(譬如ViewRoot的實(shí)現(xiàn))。
-
警惕線程未終止造成的內(nèi)存泄露;譬如在Activity中關(guān)聯(lián)了一個(gè)生命周期超過Activity的Thread,在退出Activity時(shí)切記結(jié)束線程。一個(gè)典型的例子就是HandlerThread的run方法是一個(gè)死循環(huán),它不會自己結(jié)束,線程的生命周期超過了Activity生命周期,我們必須手動在Activity的銷毀方法中中調(diào)運(yùn)thread.getLooper().quit();才不會泄露。
-
對象的注冊與反注冊沒有成對出現(xiàn)造成的內(nèi)存泄露;譬如注冊廣播接收器、注冊觀察者(典型的譬如數(shù)據(jù)庫的監(jiān)聽)等。
-
創(chuàng)建與關(guān)閉沒有成對出現(xiàn)造成的泄露;譬如Cursor資源必須手動關(guān)閉,WebView必須手動銷毀,流等對象必須手動關(guān)閉等。
-
不要在執(zhí)行頻率很高的方法或者循環(huán)中創(chuàng)建對象,可以使用HashTable等創(chuàng)建一組對象容器從容器中取那些對象,而不用每次new與釋放。
-
避免代碼設(shè)計(jì)模式的錯(cuò)誤造成內(nèi)存泄露。
關(guān)于規(guī)避內(nèi)存泄露上面我只是列出了我在項(xiàng)目中經(jīng)常遇見的一些情況而已,肯定不全面,歡迎拍磚!當(dāng)然了,只有我們做到好的規(guī)避加上強(qiáng)有力的判斷嗅覺泄露才能讓我們的應(yīng)用駕馭好自己的一畝三分地。
3-3 Android內(nèi)存溢出OOM性能分析
上面談?wù)摿薃ndroid應(yīng)用開發(fā)的內(nèi)存泄露,下面談?wù)剝?nèi)存溢出(OOM);其實(shí)可以認(rèn)為內(nèi)存溢出與內(nèi)存泄露是交集關(guān)系,具體如下圖:
下面我們就來看看內(nèi)存溢出(OOM)相關(guān)的東東吧。
3-3-1 Android應(yīng)用內(nèi)存溢出OOM概念
上面我們探討了Android內(nèi)存管理和應(yīng)用開發(fā)中的內(nèi)存泄露問題,可以知道內(nèi)存泄露一般影響就是導(dǎo)致應(yīng)用卡頓,但是極端的影響是使應(yīng)用掛掉。前面也提到過應(yīng)用的內(nèi)存分配是有一個(gè)閾值的,超過閾值就會出問題,這里我們就來看看這個(gè)問題—–內(nèi)存溢出(OOM–OutOfMemoryError)。
內(nèi)存溢出的主要導(dǎo)致原因有如下幾類:
-
應(yīng)用代碼存在內(nèi)存泄露,長時(shí)間積累無法釋放導(dǎo)致OOM;
-
應(yīng)用的某些邏輯操作瘋狂的消耗掉大量內(nèi)存(譬如加載一張不經(jīng)過處理的超大超高清圖片等)導(dǎo)致超過閾值OOM;
可以發(fā)現(xiàn),無論哪種類型,導(dǎo)致內(nèi)存溢出(OutOfMemoryError)的核心原因就是應(yīng)用的內(nèi)存超過閾值了。
3-3-2 Android應(yīng)用內(nèi)存溢出OOM性能分析
通過上面的OOM概念和那幅交集圖可以發(fā)現(xiàn),要想分析OOM原因和避免OOM需要分兩種情況考慮,泄露導(dǎo)致的OOM,申請過大導(dǎo)致的OOM。
內(nèi)存泄露導(dǎo)致的OOM分析:
這種OOM一旦發(fā)生后會在logcat中打印相關(guān)OutOfMemoryError的異常棧信息,不過你別高興太早,這種情況下導(dǎo)致的OOM打印異常信息是沒有太大作用,因?yàn)檫@種OOM的導(dǎo)致一般都如下圖情況(圖示為了說明問題數(shù)據(jù)和場景有夸張,請忽略):
從圖片可以看見,這種OOM我們有時(shí)也遇到,第一反應(yīng)是去分析OOM異常打印棧,可是后來發(fā)現(xiàn)打印棧打印的地方?jīng)]有啥問題,沒有可優(yōu)化的余地了,于是就郁悶了。其實(shí)這時(shí)候你留心觀察幾個(gè)現(xiàn)象即可,如下:
-
留意你執(zhí)行觸發(fā)OOM操作前的界面是否有卡頓或者比較密集的GC打印;
-
使用命令查看下當(dāng)前應(yīng)用占用內(nèi)存情況;
確認(rèn)了以上這些現(xiàn)象你基本可以斷定該OOM的log真的沒用,真正導(dǎo)致問題的原因是內(nèi)存泄露,所以我們應(yīng)該按照上節(jié)介紹的方式去著手排查內(nèi)存泄露問題,解決掉內(nèi)存泄露后紅色空間都能得到釋放,再去顯示一張0.8M的優(yōu)化圖片就不會再報(bào)OOM異常了。
不珍惜內(nèi)存導(dǎo)致的OOM分析:
上面說了內(nèi)存泄露導(dǎo)致的OOM異常,下面我們再來看一幅圖(數(shù)據(jù)和場景描述有夸張,請忽略),如下:
可見,這種類型的OOM就很好定位原因了,一般都可以從OOM后的log中得出分析定位。
如下例子,我們在Activity中的ImageView放置一張未優(yōu)化的特大的(30多M)高清圖片,運(yùn)行直接崩潰如下:
//拋出OOM異常
10-10 09:01:04.873 11703-11703/? E/art: Throwing OutOfMemoryError “Failed to allocate a 743620620 byte allocation with 4194208 free bytes and 239MB until OOM”
10-10 09:01:04.940 11703-11703/? E/art: Throwing OutOfMemoryError “Failed to allocate a 743620620 byte allocation with 4194208 free bytes and 239MB until OOM”
//堆棧打印
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: FATAL EXCEPTION: main
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: Process: com.example.application, PID: 11703
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.application/com.example.myapplication.MainActivity}: android.view.InflateException: Binary XML file line #21: Error inflating class
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2610)
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2684)
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: at android.app.ActivityThread.access$800(ActivityThread.java:177)
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1542)
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: at android.os.Handler.dispatchMessage(Handler.java:111)
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: at android.os.Looper.loop(Looper.java:194)
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: at android.app.ActivityThread.main(ActivityThread.java:5743)
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method)
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: at java.lang.reflect.Method.invoke(Method.java:372)
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:988)
10-10 09:01:04.958 11703-11703/? E/A
《Android學(xué)習(xí)筆記總結(jié)+最新移動架構(gòu)視頻+大廠安卓面試真題+項(xiàng)目實(shí)戰(zhàn)源碼講義》
【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整內(nèi)容開源分享
ndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:783)
//出錯(cuò)地點(diǎn),原因是21行的ImageView設(shè)置的src是一張未優(yōu)化的31M的高清圖片
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: Caused by: android.view.InflateException: Binary XML file line #21: Error inflating class
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: at android.view.LayoutInflater.createView(LayoutInflater.java:633)
通過上面的log可以很方便的看出來問題原因所在地,那接下來的做法就是優(yōu)化唄,降低圖片的相關(guān)規(guī)格即可(譬如使用BitmapFactory的Option類操作等)。
PS:提醒一句的是記得應(yīng)用所屬的內(nèi)存是區(qū)分Java堆和native堆的!
3-3-3 Android應(yīng)用規(guī)避內(nèi)存溢出OOM建議
還是那句話,等待OOM發(fā)生是為時(shí)已晚的事,我們應(yīng)該將其扼殺于萌芽之中,至于如何在開發(fā)中規(guī)避OOM,如下給出一些我們應(yīng)用開發(fā)中的常用的策略建議:
-
時(shí)刻記得不要加載過大的Bitmap對象;譬如對于類似圖片加載我們要通過BitmapFactory.Options設(shè)置圖片的一些采樣比率和復(fù)用等,具體做法點(diǎn)我參考官方文檔,不過過我們一般都用fresco或Glide開源庫進(jìn)行加載。
-
優(yōu)化界面交互過程中頻繁的內(nèi)存使用;譬如在列表等操作中只加載可見區(qū)域的Bitmap、滑動時(shí)不加載、停止滑動后再開始加載。
-
有些地方避免使用強(qiáng)引用,替換為弱引用等操作。
-
避免各種內(nèi)存泄露的存在導(dǎo)致OOM。
-
對批量加載等操作進(jìn)行緩存設(shè)計(jì),譬如列表圖片顯示,Adapter的convertView緩存等。
-
盡可能的復(fù)用資源;譬如系統(tǒng)本身有很多字符串、顏色、圖片、動畫、樣式以及簡單布局等資源可供我們直接使用,我們自己也要盡量復(fù)用style等資源達(dá)到節(jié)約內(nèi)存。
-
對于有緩存等存在的應(yīng)用盡量實(shí)現(xiàn)onLowMemory()和onTrimMemory()方法。
-
盡量使用線程池替代多線程操作,這樣可以節(jié)約內(nèi)存及CPU占用率。
-
盡量管理好自己的Service、Thread等后臺的生命周期,不要浪費(fèi)內(nèi)存占用。
-
盡可能的不要使用依賴注入,中看不中用。
-
盡量在做一些大內(nèi)存分配等可疑內(nèi)存操作時(shí)進(jìn)行try catch操作,避免不必要的應(yīng)用閃退。
-
盡量的優(yōu)化自己的代碼,減少冗余,進(jìn)行編譯打包等優(yōu)化對齊處理,避免類加載時(shí)浪費(fèi)內(nèi)存。
可以發(fā)現(xiàn),上面只是列出了我們開發(fā)中常見的導(dǎo)致OOM異常的一些規(guī)避原則,還有很多相信還沒有列出來,大家可以自行追加參考即可。
3-4 Android內(nèi)存性能優(yōu)化總結(jié)
無論是什么電子設(shè)備的開發(fā),內(nèi)存問題永遠(yuǎn)都是一個(gè)很深?yuàn)W、無底洞的話題,上面的這些內(nèi)存分析建議也單單只是Android應(yīng)用開發(fā)中一些常見的場景而已,真正的達(dá)到合理的優(yōu)化還是需要很多知識和功底的。
合理的應(yīng)用架構(gòu)設(shè)計(jì)、設(shè)計(jì)風(fēng)格選擇、開源Lib選擇、代碼邏輯規(guī)范等都會決定到應(yīng)用的內(nèi)存性能,我們必須時(shí)刻頭腦清醒的意識到這些問題潛在的風(fēng)險(xiǎn)與優(yōu)劣,因?yàn)閮?nèi)存優(yōu)化必須要有一個(gè)度,不能一味的優(yōu)化,亦不能置之不理。
【工匠若水 http://blog.csdn.net/yanbober 轉(zhuǎn)載請注明出處。點(diǎn)我開始Android技術(shù)交流】
4 Android應(yīng)用API使用及代碼邏輯性能分析
=============================
在我們開發(fā)中除過常規(guī)的那些經(jīng)典UI、內(nèi)存性能問題外其實(shí)還存在很多潛在的性能優(yōu)化、這種優(yōu)化不是十分明顯,但是在某些場景下卻是非常有必要的,所以我們簡單列舉一些常見的其他潛在性能優(yōu)化技巧,具體如下探討。
4-1 Android應(yīng)用String/StringBuilder/StringBuffer優(yōu)化建議
字符串操作在Android應(yīng)用開發(fā)中是十分常見的操作,也就是這個(gè)最簡單的字符串操作卻也暗藏很多潛在的性能問題,下面我們實(shí)例來說說。
先看下面這個(gè)關(guān)于String和StringBuffer的對比例子:
//性能差的實(shí)現(xiàn)
String str1 = “Name:”;
String str2 = “GJRS”;
總結(jié)
以上是生活随笔為你收集整理的Android应用开发性能优化完全分析,完美收官的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 计算机交流会活动流程,新老生交流会活动方
- 下一篇: FastJNI导致的Android系统死