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