記憶體效能分析及優化的意義
Overview of memory management
記憶體管理介紹
OOM
系統分配給app的堆記憶體是有上限的,不是系統空閒多少記憶體app就可以用多少,getMemoryClass()可以獲取到這個值
可以在manifest檔案中設定largeHeap為true,這樣會增大堆記憶體上限,getLargeMemoryClass()可以獲取到這個值
超出虛擬機器堆記憶體上限會造成OOM
Low Memory Killer
android記憶體管理使用了分頁(paging)和記憶體對映(memory-mapping)技術,但是沒有使用swap,而是使用Low Memory Killer策略來提升實體記憶體的利用率 ,導致除了gc和殺死程式回收實體記憶體之外沒有其他方式來利用已經被佔用的記憶體
當前臺應用切換到後臺後,系統並不結束它的程式,而是把它快取起來,供下次啟動。當系統記憶體不足時,按最近最少使用+優先釋放記憶體使用密集的策略釋放快取程式。
GC
記憶體使用的多也會造成GC速度變慢,造成卡頓
記憶體佔用過高,在建立物件時記憶體不足,很容易造成觸發GC影響APP效能
綜上
關注並減少應用的記憶體消耗可以減少oom的概率,在記憶體緊張的場景下獲得更好的使用者體驗,也可以增加應用的後臺存活時間
工具介紹
GC-LOG
dumpsys meminfo
Profiler
jhat
dumpsys procstats
用來衡量一段時間內應用消耗記憶體的情況
PSS:Proportional Set Size按比例分配共享記憶體的實際記憶體
USS:Unique Set Size程式私有記憶體
PSS=USS+共享記憶體/共享記憶體的程式數
(最小PSS-平均PSS-最大PSS/最小USS-平均USS-最大USS)
LeakCanary
檢測記憶體洩漏的工具
MAT
比較常用的記憶體dump檔案分析工具
使用方法
使用Memory Profiler Dump記憶體資料
匯出的hprof檔案不是MAT的標準檔案,需要使用sdk帶的hprof-conv轉換
hprof-conv -z src dst //-z可以排除android框架建立的物件
複製程式碼
使用場景
總體性找出記憶體優化的瓶頸
只有dump檔案的現實場景,或者無法定位具體問題等只有現場而沒有線索的情況下庖丁解牛的工具
對於專項功能的記憶體優化感覺不如程式碼除錯+profiler
分析場景構建
效能測試的一些注意點
需要考慮儘量真實的場景
需要關閉log等除錯元件避免干擾
常見的效能測試方式
切換到後臺
反覆執行功能
長時間執行功能
多個場景來回切換
容易出現記憶體問題的場景
包含了圖片顯示的介面
網路傳輸大量資料的場景
需要快取資料的場景
常見的記憶體問題
記憶體洩漏
記憶體洩漏產生的原因
一個物件的生命週期已經結束了,但是有其他物件持有了它的例項導致無法在GC時被回收,在Android中通常是Activity在finish之後依然有物件引用它導致記憶體洩漏
記憶體洩漏的常見場景
非同步操作中非同步邏輯未結束,而Activity結束或者重建了
Thread/Handler/AsyncTask/Rxjava/Timer等
使用靜態變數或者單例直接或者間接的儲存Activty例項但是未及時釋放
註冊廣播未登出
ObjectAnimator未呼叫cancel
I/O操作等完成後未及時關閉或者釋放
WebView造成的記憶體洩漏 Android 5.1 WebView記憶體洩漏分析
記憶體洩漏在分析工具上的表現
每次activity的重建都會造成記憶體上升且gc不會使記憶體使用降低
記憶體洩漏的避免
LeakCanary
StrictMode
沒有必要使用Activity作為Context的地方全部使用ApplicationContext
使用WeakReference
使用ViewModel+LiveData/RxJava+Rxlifecycle等工具實現非同步邏輯避免記憶體洩漏
對需要銷燬時進行處理的操作進行檢查,如xxx.cancel()/xxx.close()/xxx.unregister()/xxx.remove()等操作
記憶體抖動
記憶體抖動的原因
記憶體抖動一般是瞬間建立了大量物件,會在短時間內觸發多次GC,產生卡頓
記憶體抖動的場景
- IM通知需要轉發到所有WebView介面,當剛開啟APP時多個通知同時到達,或者在群聊中訊息很多的場景下,會造成短時間內頻繁GC,同時伴隨介面卡頓
記憶體抖動的在分析工具上的表現
製造了一個記憶體抖動的場景
public void testThrashing(boolean needLog) {
int dimension = 300;
int[][] lotsOfInts = new int[dimension][dimension];
Random randomGenerator = new Random();
for (int i = 0; i < lotsOfInts.length; i++) {
for (int j = 0; j < lotsOfInts[i].length; j++) {
lotsOfInts[i][j] = randomGenerator.nextInt();
}
}
//優化以前
for (int i = 0; i < lotsOfInts.length; i++) {
String rowAsStr = "";
int[] sorted = getSorted(lotsOfInts[i]);
for (int j = 0; j < lotsOfInts[i].length; j++) {
rowAsStr += sorted[j];
if (j < (lotsOfInts[i].length - 1)) {
rowAsStr += ", ";
}
}
if (needLog) {
Log.i(TAG, "Row " + i + ": " + rowAsStr);
}
}
}
public void optimizeThrashing() {
int dimension = 300;
int[][] lotsOfInts = new int[dimension][dimension];
Random randomGenerator = new Random();
for (int i = 0; i < lotsOfInts.length; i++) {
for (int j = 0; j < lotsOfInts[i].length; j++) {
lotsOfInts[i][j] = randomGenerator.nextInt();
}
}
//優化以後
StringBuilder sb = new StringBuilder();
for (int i = 0; i < lotsOfInts.length; i++) {
sb.delete(0, sb.length());
int[] sorted = getSorted(lotsOfInts[i]);
for (int j = 0; j < lotsOfInts[i].length; j++) {
sb.append(sorted[j]);
if (j < (lotsOfInts[i].length - 1)) {
sb.append(", ");
}
}
Log.e(TAG, "Row " + i + ": " + sb);
}
}
複製程式碼
自己試驗的感受,gc帶來的卡頓其實並不明顯(也可能是demo不太複雜,GC耗時不長)
個人感覺卡頓主要是因為記憶體抖動大多出現在一些複雜場景,通常伴隨著主執行緒的大量操作已經出現了卡頓,而記憶體抖動引起的頻繁GC會加劇卡頓的程度
解決方案
最簡單的做法就是把之前的主執行緒操作放到子執行緒去,雖然記憶體抖動依然存在,但是卡頓問題可以大大緩解
對於記憶體抖動本身
儘量避免在迴圈體內建立物件,應該把物件建立移到迴圈體外
需要大量使用Bitmap和其他大型物件時,儘量嘗試複用之前建立的物件
對於黑盒子(例如之前例子中im通知造成的webview的記憶體抖動和主執行緒耗時操作)
控制觸發頻率,減輕卡頓程度
新增序號產生器制,需要接收通知的頁面才傳送通知
圖片載入的記憶體佔用
不同dpi資料夾對圖片記憶體的影響
- 不同dpi限定符對應的dpi
xxxhdpi-640
xxhdpi-480
xhdpi-320
mdpi-160
通過resId載入的Bitmap的寬高計算
bitmap寬高=圖片實際寬高*螢幕dpi/資料夾對應的dpinodpi
從這個資料夾中載入的圖片資源生成的Bitmap會保持圖片本身的尺寸1920*1080圖片資源放在不同的資料夾下載入的Bitmap大小計算
使用裝置小米note,裝置dpi為440
資料夾 | 對應dpi | bitmap width | height | size | 倍數 |
---|---|---|---|---|---|
nodpi | 1920 | 1080 | 8294400 | 1 | |
xxxhdpi | 640 | 1320 | 743 | 3923040 | 0.47 |
xxhdpi | 480 | 1760 | 990 | 6969600 | 0.84 |
xhdpi | 320 | 2640 | 1485 | 15681600 | 1.89 |
mdpi | 160 | 5280 | 2970 | 62726400 | 7.56 |
使用圖片的建議
儘量使用1080p的尺寸下的切圖
圖片儘量放xxhdpi以上的資料夾下
大圖如Splash頁和引導頁的圖片放在nodpi資料夾下,通過控制ImageView大小來限制圖片大小
按照上面操作會導致apk大小增加,可以將圖片轉成webp並進行壓縮
RGB565
除了圖片資源的資料夾,載入圖片時使用的色彩模式也影響了Bitmap大小。ARGB8888使用了32bit,所以一個畫素需要4byte;RGB565使用了16bit,一個畫素只需要2byte
但是因為RGB565少了alpha通道,對有透明度的圖片顯示有問題,而且顯示效果上還是有些區別,所以並不建議修改這個屬性,只是在對記憶體有嚴格要求的場景下可以作為特殊手段進行優化
ProGuard對記憶體的影響
ProGuard可以對類、方法和變數重新命名,剔除無用程式碼和資源,減小dex大小,除了減小了apk的大小,同時也減小了載入dex所需的記憶體
因為虛擬機器載入dex檔案是按需載入的,而記憶體分配的最小單位是頁,所以載入一個功能的程式碼時同一個記憶體頁中也會載入dex檔案中該功能前後不相關的程式碼,ProGuard可以重新排序類的位元組碼在dex檔案中位置,使得有相互呼叫關係的類在dex中更加緊湊,載入相同功能所需的記憶體更小
記憶體碎片
Davik的記憶體回收演算法不能移動物件,所以會造成一個小物件佔據整個記憶體頁,產生記憶體碎片
而ART虛擬機器的可以在GC時對記憶體空間進行整理,隨著5.0以上系統的佔有率逐漸提升,記憶體碎片造成的記憶體消耗可以不必過於關心
其他記憶體問題
頁面不可見收到onTrimMemory(TRIM_MEMORY_UI_HIDDEN)時釋放UI資源
通過getMemoryInfo()獲取記憶體資訊,保證自己不開闢大記憶體導致oom
謹慎的使用Service
使用IntentService
使用JobScheduler進行後臺排程
使用優化的容器如SparseArray等
程式碼抽象會帶來額外的記憶體消耗
使用@IntDef、@StringDef代替列舉
...