編輯:Android資訊
其實有點不想寫這篇文章的,但是又想寫,有些矛盾。不想寫的原因是隨便上網一搜一堆關於性能的建議,感覺大家你一總結、我一總結的都說到了很多優化注意事項,但是看過這些文章後大多數存在一個問題就是只給出啥啥啥不能用,啥啥啥該咋用等,卻很少有較為系統的進行真正性能案例分析的,大多數都是嘴上喊喊或者死記住規則而已(當然了,這話我自己聽著都有些刺耳,實在不好意思,其實關於性能優化的優質博文網上也還是有很多的,譬如Google官方都已經推出了優化專題,我這裡只是總結下自的感悟而已,若有得罪歡迎拍磚,我願挨打,因為我之前工作的一半時間都是負責性能優化)。
當然了,本文不會就此編輯這麼一次,因為技術在發展,工具在強大(寫著寫著Android Studio 1.4版本都推送了),自己的經驗也在增加,所以本文自然不會覆蓋所有性能優化及分析;解決的辦法就是該文章會長期維護更新,同時在評論區歡迎你關於性能優化點子的探討。
Android應用的性能問題其實可以劃分為幾個大的模塊的,而且都具有相對不錯的優化調試技巧,下面我們就會依據一個項目常規開發的大類型來進行一些分析講解。
PS:之前呆過一家初創醫療互聯網公司,別提性能優化了,老板立完新項目後一個月就要求見到上線成品,這種壓迫下談何性能優化,純屬扯蛋,所以不到三個月時間我主動選擇撤了,這種現象後來我一打聽發現在很多初創公司都很嚴重,都想速成卻忽略了體驗。
PPPS:本文只是達到拋磚引玉的作用,很多東西細究下去都是值得深入研究的,再加上性能優化本來就是一個需要綜合考量的任務,不是說會了本文哪一點就能做性能分析了,需要面面俱到才可高效定位問題原因。
UI可謂是一個應用的臉,所以每一款應用在開發階段我們的交互、視覺、動畫工程師都拼命的想讓它變得自然大方美麗,可是現實總是不盡人意,動畫和交互總會覺得開發做出來的應用用上去感覺不自然,沒有達到他們心目中的自然流暢細節;這種情況之下就更別提發布給終端用戶使用了,用戶要是能夠感覺出來,少則影響心情,多則卸載應用;所以一個應用的UI顯示性能問題就不得不被開發人員重視。
人類大腦與眼睛對一個畫面的連貫性感知其實是有一個界限的,譬如我們看電影會覺得畫面很自然連貫(幀率為24fps),用手機當然也需要感知屏幕操作的連貫性(尤其是動畫過度),所以Android索性就把達到這種流暢的幀率規定為60fps。
有了上面的背景,我們開發App的幀率性能目標就是保持在60fps,也就是說我們在進行App性能優化時心中要有如下准則:
換算關系:60幀/秒-----------16ms/幀; 准則:盡量保證每次在16ms內處理完所有的CPU與GPU計算、繪制、渲染等操作,否則會造成丟幀卡頓問題。
從上面可以看出來,所謂的卡頓其實是可以量化的,每次是否能夠成功渲染是非常重要的問題,16ms能否完整的做完一次操作直接決定了卡頓性能問題。
當然了,針對Android系統的設計我們還需要知道另一個常識;虛擬機在執行GC垃圾回收操作時所有線程(包括UI線程)都需要暫停,當GC垃圾回收完成之後所有線程才能夠繼續執行(這個細節下面小節會有詳細介紹)。也就是說當在16ms內進行渲染等操作時如果剛好遇上大量GC操作則會導致渲染時間明顯不足,也就從而導致了丟幀卡頓問題。
有了上面這兩個簡單的理論基礎之後我們下面就會探討一些UI卡頓的原因分析及解決方案。
我們在使用App時會發現有些界面啟動卡頓、動畫不流暢、列表等滑動時也會卡頓,究其原因,很多都是丟幀導致的;通過上面卡頓原理的簡單說明我們從應用開發的角度往回推理可以得出常見卡頓原因,如下:
可以看見,上面這些導致卡頓的原因都是我們平時開發中非常常見的。有些人可能會覺得自己的應用用著還蠻OK的,其實那是因為你沒進行一些瞬時測試和壓力測試,一旦在這種環境下運行你的App你就會發現很多性能問題。
分析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的利器,在平時寫代碼時會時常伴隨我們左右。
我們對於UI性能的優化還可以通過開發者選項中的GPU過度繪制工具來進行分析。在設置->開發者選項->調試GPU過度繪制(不同設備可能位置或者叫法不同)中打開調試後可以看見如下圖(對settings當前界面過度繪制進行分析):
可以發現,開啟後在我們想要調試的應用界面中可以看到各種顏色的區域,具體含義如下:
由於過度繪制指在屏幕的一個像素上繪制多次(譬如一個設置了背景色的TextView就會被繪制兩次,一次背景一次文本;這裡需要強調的是Activity設置的Theme主題的背景不被算在過度繪制層級中),所以最理想的就是繪制一次,也就是藍色(當然這在很多絢麗的界面是不現實的,所以大家有個度即可,我們的開發性能優化標准要求最極端界面下紅色區域不能長期持續超過屏幕三分之一,可見還是比較寬松的規定),因此我們需要依據此顏色分布進行代碼優化,譬如優化布局層級、減少沒必要的背景、暫時不顯示的View設置為GONE而不是INVISIBLE、自定義View的onDraw方法設置canvas.clipRect()指定繪制區域或通過canvas.quickreject()減少繪制區域等。
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來進行原因追蹤,下面我們會介紹這兩種工具的。
上面說了,冗余資源及邏輯等也可能會導致加載和執行緩慢,所以我們就來看看Lint這個工具是如何發現優化這些問題的(當然了,Lint實際的功能是非常強大的,我們開發中也是經常使用它來發現一些問題的,這裡主要有點針對UI性能的說明了,其他的雷同)。
在Android Studio 1.4版本中使用Lint最簡單的辦法就是將鼠標放在代碼區點擊右鍵->Analyze->Inspect Code–>界面選擇你要檢測的模塊->點擊確認開始檢測,等待一下後會發現如下結果:
可以看見,Lint檢測完後給了我們很多建議的,我們重點看一個關於UI性能的檢測結果;上圖中高亮的那一行明確說明了存在冗余的UI層級嵌套,所以我們是可以點擊跳進去進行優化處理掉的。
當然了,Lint還有很多功能,大家可以自行探索發揮,這裡只是達到拋磚引玉的作用。
關於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導致所有線程暫停的操作必定會導致UI視覺的卡頓,所以我們要避免此類問題的出現,具體的常見優化方式如下:
當然了,有了上面說明GC導致的性能後我們就該定位分析問題了,可以通過運行DDMS->Allocation Tracker標簽打開一個新窗口,然後點擊Start Tracing按鈕,接著運行你想分析的代碼,運行完畢後點擊Get Allocations按鈕就能夠看見一個已分配對象的列表,如下:
點擊上面第一個表格中的任何一項就能夠在第二個表格中看見導致該內存分配的棧信息,通過這個工具我們可以很方便的知道代碼分配了哪類對象、在哪個線程、哪個類、哪個文件的哪一行。譬如我們可以通過Allocation Tracker分別做一次Paint對象實例化在onDraw與構造方法的一個自定義View的內存跟蹤,然後你就明白這個工具的強大了。
PS一句,Android Studio新版本除過DDMS以外在Memory視圖的左側已經集成了Allocation Tracker功能,只是用起來還是沒有DDMS的方便實用,如下圖:
關於UI卡頓問題我們還可以通過運行Traceview工具進行分析,他是一個分析器,記錄了應用程序中每個函數的執行時間;我們可以打開DDMS然後選擇一個進程,接著點擊上面的“Start Method Profiling”按鈕(紅色小點變為黑色即開始運行),然後操作我們的卡頓UI(小范圍測試,所以操作最好不要超過5s),完事再點一下剛才按的那個按鈕,稍等片刻即可出現下圖,如下:
花花綠綠的一幅圖我們怎麼分析呢?下面我們解釋下如何通過該工具定位問題:
整個界面包括上下兩部分,上面是你測試的進程中每個線程運行的時間線,下面是每個方法(包含parent及child)執行的各個指標的值。通過上圖的時間面板可以直觀發現,整個trace時間段main線程做的事情特別多,其他的做的相對較少。當我們選擇上面的一個線程後可以發現下面的性能面板很復雜,其實這才是TraceView的核心圖表,它主要展示了線程中各個方法的調用信息(CPU使用時間、調用次數等),這些信息就是我們分析UI性能卡頓的核心關注點,所以我們先看幾個重要的屬性說明,如下:
有了對上面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個人覺得不錯的分析文章(引用自網絡,鏈接屬於原作者功勞)),如下圖:
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系統框架的原理才可以將該工具使用的得心應手。
ANR(Application Not Responding)是Android中AMS與WMS監測應用響應超時的表現;之所以把臭名昭著的ANR單獨作為UI性能卡頓的分析來說明是因為ANR是直接卡死UI不動且必須要解掉的Bug,我們必須盡量在開發時避免他的出現,當然了,萬一出現了那就用下面介紹的方法來分析吧。
我們應用開發中常見的ANR主要有如下幾類:
當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分析定位就可以解決了。
可以看見,關於Android UI卡頓的性能分析還是有很多工具的,上面只是介紹了應用開發中我們經常使用的一些而已,還有一些其他的,譬如Oprofile等工具不怎麼常用,這裡就不再詳細介紹。
通過上面UI性能的原理、原因、工具分析總結可以發現,我們在開發應用時一定要時刻重視性能問題,如若真的沒留意出現了性能問題,不妨使用上面的一些案例方式進行分析。但是那終歸是補救措施,在我們知道上面UI卡頓原理之後我們應該盡量從項目代碼架構搭建及編寫時就避免一些UI性能問題,具體項目中常見的注意事項如下:
當然了,上面只是列出了我們項目中常見的一些UI性能注意事項而已,相信還有很多其他的情況這裡沒有說到,歡迎補充。還有一點就是我們上面所謂的UI性能優化分析總結等都是建議性的,因為性能這個問題是一個涉及面很廣很泛的問題,有些優化不是必需的,有些優化是必需的,有些優化掉以後又是得不償失的,所以我們一般著手解決那些必須的就可以了。
說完了應用開發中的UI性能問題後我們就該來關注應用開發中的另一個重要、嚴重、非常重要的性能問題了,那就是內存性能優化分析。Android其實就是嵌入式設備,嵌入式設備核心關注點之一就是內存資源;有人說現在的設備都在堆硬件配置(譬如國產某米的某兔跑分手機、盒子等),所以內存不會再像以前那麼緊張了,其實這句話聽著沒錯,但為啥再牛逼配置的Android設備上有些應用還是越用系統越卡呢?這裡面的原因有很多,不過相信有了這一章下面的內容分析,作為一個移動開發者的你就有能力打理好自己應用的那一畝三分地內存了,能做到這樣就足以了。關於Android內存優化,這裡有一篇Google的官方指導文檔,但是本文為自己項目摸索,會有很多不一樣的地方。
系統級內存管理:
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打印出回收類型,常見的如下:
通過上面這幾點的分析可以發現,應用的內存管理其實就是一個蘿卜一個坑,坑都一般大,你在開發應用時要保證的是內存使用同一時刻不能超過坑的大小,否則就裝不下了。
有了關於Android的一些內存認識,接著我們來看看關於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就無法得到垃圾回收,也就造成了內存洩露。
內存洩露可以引發很多的問題,常見的內存洩露導致問題如下:
造成內存洩露洩露的最核心原理就是一個對象持有了超過自己生命周期以外的對象強引用導致該對象無法被正常垃圾回收;可以發現,應用內存洩露是個相當棘手重要的問題,我們必須重視。
知道了內存洩露的概念之後肯定就是想辦法來確認自己的項目是否存在內存洩露了,那該如何察覺自己項目是否存在內存洩露呢?如下提供了幾種常用的方式:
AS的Memory窗口如下,詳細的說明這裡就不解釋了,很簡單很直觀(使用頻率高):
DDMS-Heap內存監測工具窗口如下,詳細的說明這裡就不解釋了,很簡單(使用頻率不高):
dumpsys meminfo命令如下(使用頻率非常高,非常高效,我的最愛之一,平時一般關注幾個重要的Object個數即可判斷一般的洩露;當然了,adb shell dumpsys meminfo不跟參數直接展示系統所有內存狀態):
leakcanary神器使用這裡先不說,下文會專題介紹,你會震撼的一B。有了這些工具的定位我們就能很方便的察覺我們App的內存洩露問題,察覺到以後該怎麼定位分析呢,繼續往下看。
leakcanary是一個開源項目,一個內存洩露自動檢測工具,是著名的GitHub開源組織Square貢獻的,它的主要優勢就在於自動化過早的發覺內存洩露、配置簡單、抓取貼心,缺點在於還存在一些bug,不過正常使用百分之九十情況是OK的,其核心原理與MAT工具類似。
關於leakcanary工具的配置使用方式這裡不再詳細介紹,因為真的很簡單,詳情點我參考官方教程學習使用即可。
PS:之前在優化性能時發現我們有一個應用有兩個界面退出後Activity沒有被回收(dumpsys meminfo發現一直在加),所以就懷疑可能存在內存洩露。但是問題來了,這兩個Activity的邏輯十分復雜,代碼也不是我寫的,相關聯的代碼量也十分龐大,更加郁悶的是很難判斷是哪個版本修改導致的,這時候只知道有洩露,卻無法定位具體原因,使用MAT分析解決掉了一個可疑洩露後發現洩露又變成了概率性的。可以發現,對於這種概率性的洩露用MAT去主動抓取肯定是很耗時耗力的,所以決定直接引入leakcanary神器來檢測項目,後來很快就徹底解決了項目中所有必現的、偶現的內存洩露。
總之一點,工具再強大也只是幫我們定位可能的洩露點,而最核心的GC ROOT洩露信息推導出洩露問題及如何解決還是需要你把住代碼邏輯及洩露核心概念去推理解決。
Eclipse Memory Analysis Tools(點我下載)是一個專門分析Java堆數據內存引用的工具,我們可以使用它方便的定位內存洩露原因,核心任務就是找到GC ROOT位置即可,哎呀,關於這個工具的使用我是真的不想說了,自己搜索吧,實在簡單、傳統的不行了。
PS:這是開發中使用頻率非常高的一個工具之一,麻煩務必掌握其核心使用技巧,雖然Android Studio已經實現了部分功能,但是真的很難用,遇到問題目前還是使用Eclipse Memory Analysis Tools吧。
原諒我該小節的放蕩不羁!!!!(其實我是困了,嗚嗚!)
有了上面的原理及案例處理其實還不夠,因為上面這些處理辦法是補救的措施,我們正確的做法應該是在開發過程中就養成良好的習慣和敏銳的嗅覺才對,所以下面給出一些應用開發中常見的規避內存洩露建議:
關於規避內存洩露上面我只是列出了我在項目中經常遇見的一些情況而已,肯定不全面,歡迎拍磚!當然了,只有我們做到好的規避加上強有力的判斷嗅覺洩露才能讓我們的應用駕馭好自己的一畝三分地。
上面談論了Android應用開發的內存洩露,下面談談內存溢出(OOM);其實可以認為內存溢出與內存洩露是交集關系,具體如下圖:
下面我們就來看看內存溢出(OOM)相關的東東吧。
上面我們探討了Android內存管理和應用開發中的內存洩露問題,可以知道內存洩露一般影響就是導致應用卡頓,但是極端的影響是使應用掛掉。前面也提到過應用的內存分配是有一個阈值的,超過阈值就會出問題,這裡我們就來看看這個問題—–內存溢出(OOM–OutOfMemoryError)。
內存溢出的主要導致原因有如下幾類:
可以發現,無論哪種類型,導致內存溢出(OutOfMemoryError)的核心原因就是應用的內存超過阈值了。
通過上面的OOM概念和那幅交集圖可以發現,要想分析OOM原因和避免OOM需要分兩種情況考慮,洩露導致的OOM,申請過大導致的OOM。
內存洩露導致的OOM分析:
這種OOM一旦發生後會在logcat中打印相關OutOfMemoryError的異常棧信息,不過你別高興太早,這種情況下導致的OOM打印異常信息是沒有太大作用,因為這種OOM的導致一般都如下圖情況(圖示為了說明問題數據和場景有誇張,請忽略):
從圖片可以看見,這種OOM我們有時也遇到,第一反應是去分析OOM異常打印棧,可是後來發現打印棧打印的地方沒有啥問題,沒有可優化的余地了,於是就郁悶了。其實這時候你留心觀察幾個現象即可,如下:
確認了以上這些現象你基本可以斷定該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 <unknown> 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/AndroidRuntime: 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 <unknown> 10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: at android.view.LayoutInflater.createView(LayoutInflater.java:633)
通過上面的log可以很方便的看出來問題原因所在地,那接下來的做法就是優化呗,降低圖片的相關規格即可(譬如使用BitmapFactory的Option類操作等)。
PS:提醒一句的是記得應用所屬的內存是區分Java堆和native堆的!
還是那句話,等待OOM發生是為時已晚的事,我們應該將其扼殺於萌芽之中,至於如何在開發中規避OOM,如下給出一些我們應用開發中的常用的策略建議:
可以發現,上面只是列出了我們開發中常見的導致OOM異常的一些規避原則,還有很多相信還沒有列出來,大家可以自行追加參考即可。
無論是什麼電子設備的開發,內存問題永遠都是一個很深奧、無底洞的話題,上面的這些內存分析建議也單單只是Android應用開發中一些常見的場景而已,真正的達到合理的優化還是需要很多知識和功底的。
合理的應用架構設計、設計風格選擇、開源Lib選擇、代碼邏輯規范等都會決定到應用的內存性能,我們必須時刻頭腦清醒的意識到這些問題潛在的風險與優劣,因為內存優化必須要有一個度,不能一味的優化,亦不能置之不理。
在我們開發中除過常規的那些經典UI、內存性能問題外其實還存在很多潛在的性能優化、這種優化不是十分明顯,但是在某些場景下卻是非常有必要的,所以我們簡單列舉一些常見的其他潛在性能優化技巧,具體如下探討。
字符串操作在Android應用開發中是十分常見的操作,也就是這個最簡單的字符串操作卻也暗藏很多潛在的性能問題,下面我們實例來說說。
先看下面這個關於String和StringBuffer的對比例子:
//性能差的實現 String str1 = "Name:"; String str2 = "GJRS"; String Str = str1 + str2; //性能好的實現 String str1 = "Name:"; String str2 = "GJRS"; StringBuffer str = new StringBuilder().append(str1).append(str2);
通過這個例子可以看出來,String對象(記得是對象,不是常量)和StringBuffer對象的主要性能區別在於String對象是不可變的,所以每次對String對象做改變操作(譬如“+”操作)時其實都生成了新的String對象實例,所以會導致內存消耗性能問題;而StringBuffer對象做改變操作每次都會對自己進行操作,所以不需要消耗額外的內存空間。
我們再看一個關於String和StringBuffer的對比例子:
//性能差的實現 StringBuffer str = new StringBuilder().append("Name:").append("GJRS"); //性能好的實現 String Str = "Name:" + "GJRS";
在這種情況下你會發現StringBuffer的性能反而沒有String的好,原因是在JVM解釋時認為
String Str = "Name:" + "GJRS";
就是String Str = "Name:GJRS";
,所以自然比StringBuffer快了。
可以發現,如果我們拼接的是字符串常量則String效率比StringBuffer高,如果拼接的是字符串對象,則StringBuffer比String效率高,我們在開發中要酌情選擇。當然,除過注意StringBuffer和String的效率問題,我們還應該注意另一個問題,那就是StringBuffer和StringBuilder的區別,其實StringBuffer和StringBuilder都繼承自同一個父類,只是StringBuffer是線程安全的,也就是說在不考慮多線程情況下StringBuilder的性能又比StringBuffer高。
PS:如果想追究清楚他們之間具體細節差異,麻煩自己查看實現源碼即可。
OnTrimMemory是Android 4.0之後加入的一個回調方法,作用是通知應用在不同的情況下進行自身的內存釋放,以避免被系統直接殺掉,提高應用程序的用戶體驗(冷啟動速度是熱啟動的2~3倍)。系統會根據當前不同等級的內存使用情況調用這個方法,並且傳入當前內存等級,這個等級有很多種,我們可以依據情況實現不同的等級,這裡不詳細介紹,但是要說的是我們應用應該至少實現如下等級:
可以實現OnTrimMemory方法的系統組件有Application、Activity、Fragement、
Service、ContentProvider;關於OnTrimMemory釋放哪些內存其實在架構階段就要考慮清楚哪些對象是要常駐內存的,哪些是伴隨組件周期存在的,一般需要釋放的都是緩存。
如下給出一個我們項目中常用的例子:
@Override public void onTrimMemory(int level) { if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) { clearCache(); } }
通常在我們代碼實現了onTrimMemory後很難復顯這種內存消耗場景,但是你又怕引入新Bug,想想辦法測試。好在我們有一個快捷的方式來模擬觸發該水平內存釋放,如下命令:
adb shell dumpsys gfxinfo packagename -cmd trim value
packagename為包名或者進程id,value為ComponentCallbacks2.java裡面定義的值,可以為80、60、40、20、5等,我們模擬觸發其中的等級即可。
在Android開發中涉及到數據邏輯部分大部分用的都是Java的API(譬如HashMap),但是對於Android設備來說有些Java的API並不適合,可能會導致系統性能下降,好在Google團隊已經意識到這些問題,所以他們針對Android設備對Java的一些API進行了優化,優化最多就是使用了ArrayMap及SparseArray替代HashMap來獲得性能提升。
HashMap:
HashMap內部使用一個默認容量為16的數組來存儲數據,數組中每一個元素存放一個鏈表的頭結點,其實整個HashMap內部結構就是一個哈希表的拉鏈結構。HashMap默認實現的擴容是以2倍增加,且獲取一個節點采用了遍歷法,所以相對來說無論從內存消耗還是節點查找上都是十分昂貴的。
SparseArray:
SparseArray比HashMap省內存是因為它避免了對Key進行自動裝箱(int轉Integer),它內部是用兩個數組來進行數據存儲的(一個存Key,一個存Value),它內部對數據采用了壓縮方式來表示稀疏數組數據,從而節約內存空間,而且其查找節點的實現采用了二分法,很明顯可以看見性能的提升。
ArrayMap:
ArrayMap內部使用兩個數組進行數據存儲,一個記錄Key的Hash值,一個記錄Value值,它和SparseArray類似,也會在查找時對Key采用二分法。
有了上面的基本了解我們可以得出結論供開發時參考,當數據量不大(千位級內)且Key為int類型時使用SparseArray替換HashMap效率高;當數據量不大(千位級內)且數據類型為Map類型時使用ArrayMap替換HashMap效率高;其他情況下HashMap效率相對高於二者。
ContentProvider是Android應用開發的核心組件之一,有時候在開發中需要使用ContentProvider對多行數據進行操作,我們的做法一般是多次調運相關操作方法,殊不知這種實現方式是非常低性能的,取而代之的做法應該是使用批量操作,具體為了使批量更新、插入、刪除數據操作更加方便官方提供了ContentProviderOperation工具類。所以在我們開發中遇到類似情景時請務必使用批量操作,具體的優勢如下:
可以看見,這對於數據庫操作來說是一個非常有用的優化措施,煩請務必重視(我們項目優化過,的確有很大提升)。
關於API及邏輯性能優化其實有多知識點的,這裡無法一一列出,只能給出一些重要的知識點,下面再給出一些常見的優化建議:
哎呀,類似的小優化技巧有很多,這裡不一一列舉了,自行發揮留意即可。
有了UI性能優化、內存性能優化、代碼編寫優化之後我們在來說說應用開發中很重要的一個優化模塊—–電量優化。
在盒子等開發時可能電量優化不是特別重視(視盒子待機真假待機模式而定),但是在移動設備開發中耗電量是一個非常重要的指標,如果用戶一旦發現我們的應用非常耗電,不好意思,他們大多會選擇卸載來解決此類問題,所以耗電量是一個十分重要的問題。
關於我們應用的耗電量情況我們可以進行定長時間測試,至於具體的耗電量統計等請參考此文,同時我們還可以直接通過Battery Historian Tool來查看詳細的應用電量消耗情況。最簡單常用辦法是通過命令直接查看,如下:
adb shell dumpsys batterystats
其實我們一款應用耗電量最大的部分不是UI繪制顯示等,常見耗電量最大原因基本都是因為網絡數據交互、GPS定位、大量內存性能問題、冗余的後台線程和Service等造成。
優化電量使用情況我們不僅可以使用系統提供的一些API去處理,還可以在平時編寫代碼時就養成好的習慣。具體的一些建議如下:
可以看見,上面只是一些常見的電量消耗優化建議。總之,作為應用開發者的我們要意識到電量損耗對於用戶來說是非常敏感的,只有我們做到合理的電量優化才能贏得用戶的芳心。
性能優化是一個很大的話題,上面我們談到的只是應用開發中常見的性能問題,也是應用開發中性能問題的冰山一角,更多的性能優化技巧和能力不是靠看出來,而是靠經驗和實戰結果總結出來的,所以說性能優化是一個涉及面非常廣的話題,如果你想對你的應用進行性能你必須對你應用的整個框架有一個非常清晰的認識。
當然了,如果在我們開發中只是一味的追求各種極致的優化也是不對的。因為優化本來就是存在風險的,甚至有些過度的優化會直接導致項目的臃腫,所以不要因為極致的性能優化而破壞掉了你項目的合理架構。
總之一句話,性能優化適可而止,請酌情優化。
PS:附上Google關於Android開發的一些專題建議視頻鏈接,不過在天朝需要自備梯子哦。
DBFlow,綜合了 ActiveAndroid, Schematic, Ollie,Sprinkles 等庫的優點。同時不是基於反射,所以性能也是非常高,效率緊
本篇博客要給大家分享的一個關於Android應用換膚的Demo,大家可以到我的github去下載demo,以後博文涉及到的代碼均會上傳到github中統一管理。
本文由碼農網 – 豆照建原創翻譯,轉載請看清文末的轉載要求,歡迎參與我們的付費投稿計劃! 最近 Google 已經發布 Android 新版本 7.0 N
寫在前面 最近一直在做畢設項目的准備工作,考慮到可能要用到一個模糊的效果,所以就學習了一些高斯模糊效果的實現。比較有名的就是 FastBlur 以及它衍生的一些優