前言:
術(shù)語
開機內(nèi)存:手機連接Wifi熱點、插入注冊網(wǎng)絡(luò)的SIM卡,重啟后、靜置5分鐘,采集的進程內(nèi)存值;
常駐內(nèi)存:業(yè)務(wù)進程工作任務(wù)結(jié)束退至后臺、靜置5分鐘,采集的進程內(nèi)存值;
動態(tài)內(nèi)存:業(yè)務(wù)進程在后臺運行工作任務(wù)時,采集的進程內(nèi)存峰值;
場景內(nèi)存:用戶使用典型業(yè)務(wù)場景時,相關(guān)依賴服務(wù)進程在前臺、后臺運行的內(nèi)存總和;
駐留比率:用于標識進程在后臺的常駐概率,駐留比率=“B Serveices優(yōu)先級及以上采樣命中數(shù)” /“總樣本數(shù)”。
常用分析工具
Android Studio Profile
Android Studio 3.0 及更高版本中的 Android Profiler 取代了 Android Monitor 工具。Android Profiler 工具可提供實時數(shù)據(jù),幫助您了解應(yīng)用的 CPU、內(nèi)存、網(wǎng)絡(luò)和電池資源使用情況。該工具大家用得比較多,這里就不過多贅述。
傳送門:https://developer.android.google.cn/studio/profile/android-profiler
Memory Analyzer工具
MAT 是一個快速,功能豐富的 Java Heap 分析工具,通過分析 Java 進程的內(nèi)存快照 HPROF 分析,從眾多的對象中分析,快速計算出在內(nèi)存中對象占用的大小,查看哪些對象不能被垃圾收集器回收,并可以通過視圖直觀地查看可能造成這種結(jié)果的對象。
adb或Android studio Profile抓取的heap文件,需要使用(AndroidSdk\platform-tools\hprof-conv.exe)轉(zhuǎn)換之后才能打開
傳送門:https://www.eclipse.org/mat/
Perfetto
可以分析內(nèi)存產(chǎn)生過程的方法棧,區(qū)別于hprof文件,heap文件時某個時刻的內(nèi)存。而perfetto作用的是過程內(nèi)存。一般用于分析可以復(fù)現(xiàn)內(nèi)存問題的場景,這里不過多贅述。
傳送門:https://ui.perfetto.dev/#!/record?p=instructions
Jadx-gui
jadx是個人首選的反編譯利器,同時支持命令行和圖形界面,能以最簡便的方式完成apk的反編譯操作。
下載地址
常用adb命令
adb shell dumpsys meminfo <package>
查看進程內(nèi)存分布信息,如:adb shell dumpsys meminfo com.demo
注意該方式會產(chǎn)生一次gc
adb shell "dumpsys meminfo | grep pcakgename"
查看某個進程內(nèi)存總值,該方式不會觸發(fā)gc
adb shell showmap <pid>
adb pull proc/<pid>/smaps
code,system的內(nèi)存分布,可以用于分析代碼量內(nèi)存分布,包括系統(tǒng)的類
adb shell am dumpheap <pid>
dump javaHeap部分的對象實例,可以借助Android Profile,MAT打開hprof文件分析
Android Profile會顯示所有的對象實例,包括可以待gc回收的對象,而且方便清除知道對象的變量,方便定義是什么業(yè)務(wù)產(chǎn)生的
MAT只會顯示不會被gc回收的對象,可以查看GC鏈,但是可以對比兩個hprof文件差異性
adb shell am dumpheap -n <pid>
dump native的對象,然后根據(jù)編譯Rom的產(chǎn)物之一:帶有符號信息so文件(默認在$ANDROID_PRODUCT_OUT/symbols目錄下),如果沒有可以從root的手機里獲取(system/lib64,vendor/lib64),使用native_heapdump_viewer.py解析,然后前后對比,就可以知道是哪些方法導(dǎo)致的內(nèi)存增長。
Linux命令:python native_heapdump_viewer.py --html --symbols /symbols/ heap.txt > heap_info.tx
注意需要在Linux服務(wù)器上執(zhí)行,要不然無法全部解析所有的方法棧?;蛘呤褂胮ython工程解析也可以
傳送門:
native_heapdump_viewer.py
案例分析:
一、常駐內(nèi)存優(yōu)化
根據(jù)dumpsys meminfo查看內(nèi)存分布后,根據(jù)不同內(nèi)存分布,做相應(yīng)的優(yōu)化
如下為應(yīng)用優(yōu)化前的內(nèi)存分布:
?App Summary
? ? ? ? ? ? ? ? ? ? ? ?Pss(KB)? ? ? ? ? ? ? ? ? ? ? ? Rss(KB)
? ? ? ? ? ? ? ? ? ? ? ? ------? ? ? ? ? ? ? ? ? ? ? ? ?------
? ? ? ? ? ?Java Heap:? ? ?4488? ? ? ? ? ? ? ? ? ? ? ? ? 32216
? ? ? ? ?Native Heap:? ? ?7016? ? ? ? ? ? ? ? ? ? ? ? ? 12576
? ? ? ? ? ? ? ? Code:? ? 16596? ? ? ? ? ? ? ? ? ? ? ? ? 62912
? ? ? ? ? ? ? ?Stack:? ? ?2272? ? ? ? ? ? ? ? ? ? ? ? ? ?2288
? ? ? ? ? ? Graphics:? ? ? ? 0? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0
? ? ? ?Private Other:? ? ?3416
? ? ? ? ? ? ? System:? ? ?1964
? ? ? ? ? ? ?Unknown:? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 7880
? ? ? ? ? ?TOTAL PSS:? ? 35752? ? ? ? ? ? TOTAL RSS:? ?117872? ? ? TOTAL SWAP (KB):? ? ? ? 0
數(shù)據(jù)分析初步結(jié)論:發(fā)現(xiàn)code部分占大頭,native內(nèi)存也偏高
Java Heap:
通過adb shell am dumpheap或profile工具抓取javaHeap文件?重點關(guān)注以下幾個點(要很細心,一個個查看,不要錯過任何懷疑的可能性)
1、對象個數(shù)(Allocations值)高的對象
2、對象內(nèi)存占用值(Shallow Size),查看代碼確認該對象是否有必要常駐
3、相關(guān)聯(lián)的內(nèi)存值(Retainaed Size)
4、預(yù)期結(jié)果是只有單個實例的,是否出現(xiàn)了多個實例
5、常見的內(nèi)存大對象,比如:Thread,HandlerThread等
通過javaHeap文件可知,
1、其中一個內(nèi)存大塊為數(shù)據(jù)庫相關(guān),應(yīng)用由5個db數(shù)據(jù)庫,
只能做到延遲加載,使用到對應(yīng)的數(shù)據(jù)庫之后才進行加載,并且減少數(shù)據(jù)庫執(zhí)行語句的緩存數(shù),
SQLiteDatabase.setMaxSqlCacheSize()
另外,我也嘗試過,寫一個有效期的Map,長時間不使用主動關(guān)閉數(shù)據(jù)庫,并且釋放緩存。發(fā)現(xiàn)沒法完全釋放掉,會殘留一些用于同步的ThreadLocal對象,如果頻繁的創(chuàng)建,連接、關(guān)閉數(shù)據(jù)庫,就會累計很多無用的對象,導(dǎo)致內(nèi)存泄漏,所有我放棄了該方式
2、Thread、HandlerThread的優(yōu)化
重點:自定義線程,自定義線程池,一定要復(fù)寫自己的線程名,方便定位問題,要不然dump內(nèi)存時或者看日志時,都不知道該線程是由哪個業(yè)務(wù)創(chuàng)建的
線程的創(chuàng)建,也會帶來Stack內(nèi)存
修改措施:
1)、使用線程池,防止頻繁創(chuàng)建線程,產(chǎn)生臨時內(nèi)存,
2)、去掉沒有必要的HandlerThread,使用公共HandlerThread或者線程池替代
3)、未及時關(guān)閉的Closeable對象
可以配置
StrictMode.VmPolicy vmPolicy = new StrictMode.VmPolicy.Builder().detectActivityLeaks()
? ? ? ? ? ? .detectLeakedClosableObjects()
? ? ? ? ? ? .detectLeakedSqlLiteObjects()
? ? ? ? ? ? .penaltyLog()
? ? ? ? ? ? .build();
? ? ? ? StrictMode.setVmPolicy(vmPolicy);
如果沒有及時關(guān)閉,日志里會打印相關(guān)的堆棧打印
如:W/System: A resource failed to call close,
Native Heap:
方式一、通過perfetto,和Android Profile的抓取,查看執(zhí)行過程
主要如下幾個方面:
1)、數(shù)據(jù)操作相關(guān)的,而且還發(fā)現(xiàn)不僅打開數(shù)據(jù)時會產(chǎn)生內(nèi)存,執(zhí)行sqlite語句,也會產(chǎn)生內(nèi)存,目前還沒分析出原因,使用的是加密的數(shù)據(jù)庫,沒找到對應(yīng)的源碼,只能暫時放棄了,知道的小伙伴歡迎留言
2)、另外一塊就是網(wǎng)絡(luò)安全請求相關(guān),目前也沒想到好的優(yōu)化方法
方式二、因為找不到Rom編譯的產(chǎn)物,帶有符號信息so文件,所有放棄了查看當(dāng)前native堆棧對象
Code:
1、反編譯apk,去除不必要的SDK,不必要的代碼
1.1、應(yīng)用是由多個插件化apk的,
使用jadx工具反編譯,先分析baseApk發(fā)現(xiàn)有引用很多界面相關(guān)的代碼,
androidx.appcompat:appcompat
com.google.android.material:material
androidx.constraintlayout:constraintlayout
是后臺常駐系統(tǒng)應(yīng)用,不需要界面相關(guān)的內(nèi)容,并且通過搜索反編譯的代碼,無界面相關(guān)的應(yīng)用,可去除掉,并且引入
去掉之后,release APK由7726KB減少至3983KB
1.2、應(yīng)用插件apk去掉baseApk已經(jīng)引入的SDK
可以通過gradlew app:dependencies 命令可以查看SDK引用之間的依賴關(guān)系
引入某個SDK間接引入其他SDK的,可以通過在gradle文件里exclude去除如:
通過一通裁剪后,插件APK由7.7M降到3.7M
2、額外引入的SDK,深度裁剪
查看引用的SDK實現(xiàn)的功能,原生Java、Android接口是否有等效接口
案例:應(yīng)用的某個業(yè)務(wù)使用到j(luò)oda-time:joda-time SDK,用于實現(xiàn)某個時間戳是星期幾、一年中的第幾天等功能。該SDK的引用會帶來應(yīng)用進程1M左右的Code?內(nèi)存。
□修改方案:使用等效功能的java原生接口(java.util.Calendar)替代額外引用的SDK(org.joda.time.DateTime)的功能,并打包apk時不引用該SDK
裁剪后APK 大小由3.7M降至2.4M
替換的接口,建議大家最好寫個單元測試,測試一下替換前后返回的結(jié)果都是一致的
Graphics
該部分多數(shù)為view,圖片等原因?qū)е?,本?yīng)用這里不涉及,不過多贅述
二、場景內(nèi)存優(yōu)化
主要為執(zhí)行業(yè)務(wù)過程中,內(nèi)存是否有優(yōu)化的空間
主要從如下幾個方面分析
1、內(nèi)存碎片化
在執(zhí)行周期性任務(wù),或者頻繁執(zhí)行的任務(wù)時,避免創(chuàng)建新的實例對象
可能會導(dǎo)致產(chǎn)生很多臨時對象,只能等到下次gc觸發(fā)了才可以釋放,會出現(xiàn)內(nèi)存抖動的情況
在實現(xiàn)自定義view中,咱們都知道不能在onDraw()頻繁創(chuàng)建對象
案例:
主要通過dump java heap分析,對象GC引用鏈為0的(Depth為-),就代表該對象待GC回收
手機靜置5分鐘左右,出現(xiàn)了大量ThreadPool$ThreadTask,Runnable,ConnectedWiifBean等對象
通過業(yè)務(wù)代碼,可知,
1)、業(yè)務(wù)會周期性獲取wifi連接信息,并且做一些業(yè)務(wù)處理,周期性的任務(wù)調(diào)度會產(chǎn)生大量(ThreadTask,Runnable)
修復(fù)方案:參考android.os.Message的緩存策略,通過鏈表方式緩存Message,防止頻繁創(chuàng)建新的對象
2)、業(yè)務(wù)會保持一次隊列,只需要保存最近獲取到20條wifi連接信息,舊的移除掉
修復(fù)方案:復(fù)用被移除的對象,清除內(nèi)部變量值,并重新賦值
2、內(nèi)存泄露
估計大家都熟悉,這里就不過多贅述,
個人認為只要該內(nèi)存對象以后多不需要用到了,但是無法gc回收,都屬于內(nèi)存泄露的范疇
主要分析方法為通過MAT查看GC引用鏈來定位
一般開發(fā)過程需要注意,Listener之類的要成對出現(xiàn),結(jié)束后確保結(jié)束監(jiān)聽等
3、提前初始化
分離測試代碼,正式版本使用啞類來實現(xiàn)
惰性加載等方式延遲初始化,防止無效的內(nèi)存占用
如果使用kotlin就有現(xiàn)成的語法糖,by lazy,koin依賴注入框架等
三、日常開發(fā)建議
1、盡量縮小變量的應(yīng)用范圍,能使用局部變量,傳參方式實現(xiàn)的,就不使用全局變量,即可以優(yōu)化內(nèi)存開銷,也可以解決多線程帶來的錯誤數(shù)據(jù)
2、線程一定要自定義線程名,便于定位是哪個業(yè)務(wù)使用的線程
附錄:
1、如果有發(fā)現(xiàn)不妥的地方,歡迎來擾