文章目錄
- Android 性能優化之內存優化
- 內存問題
- 內存抖動
- 內存泄露
- 內存溢出
- 檢測工具
- Memory Profiler
- Memory Analyzer
- LeakCanary
- 內存管理機制
- Java
- Android
- 解決
- 內存抖動問題
- 模擬問題代碼
- 使用Memory Profiler工具檢測
- 優化技巧
- 內存泄露問題
- 模擬問題代碼
- 使用LeakCanary工具檢測
- 優化技巧
- Bitmap優化
- Bitmap內存模型
- 資源文件目錄
- 優化技巧
- 源碼下載
Android 性能優化之內存優化
內存問題
- 內存抖動
- 內存泄露
- 內存溢出
內存抖動
內存抖動指的是在短時間內大量對象被創建和銷毀,導致頻繁的垃圾回收(Garbage Collection, GC)活動。這種頻繁的GC活動會占用大量的CPU資源,可能導致應用程序的卡頓或性能下降。
表現:內存曲線呈鋸齒狀。
內存泄露
內存泄露是指應用程序持有了不再需要的對象的引用,導致這些對象無法被垃圾回收器回收,從而占用了本可以被釋放的內存空間。隨著時間的推移,內存泄露會導致可用內存越來越少,最終可能導致應用程序崩潰或性能下降。
內存溢出
內存溢出是指應用程序嘗試分配更多的內存空間,但系統無法滿足這個請求,因為已經沒有足夠的內存空間可供分配。這通常會導致應用程序拋出OutOfMemoryError異常。
檢測工具
- Memory Profiler
- Memory Analyzer
- LeakCanary
Memory Profiler
- Memory Profiler 是 Android Studio自帶的內存分析工具。
- 實時圖表展示程序內存使用情況。
- 識別內存泄露、抖動等。
- 提供捕獲堆轉儲、強制GC以及跟蹤內存分配的能力。
Memory Analyzer
- 強大的 Java Heap 分析工具,查找內存泄露已經內存占用。
- 生成整體報告、分析問題等。
LeakCanary
- 自動內存泄露檢測。
- LeakCanary自動檢測這些對象的泄露問題:Activity、Fragment、View、ViewModel、Service。
- 官網:https://github.com/square/leakcanary
內存管理機制
Java
Java 內存結構:堆、虛擬機棧、方法區、程序計數器、本地方法棧。
Java 內存回收算法:
- 標記-清除算法:
- 標記出所需要回收的對象
- 統一回收所有標記的對象。
- 復制算法:
- 將內存劃分為大小相等的兩塊。
- 一款內存用完后復制存活對象到另一塊中。
- 標記-整理算法:
- 標記過程與“標記-清除“算法一樣。
- 存活對象往一端進行移動。
- 清除其余內存。
- 分代收集算法:
- 結合多種收集算法優勢。
- 新生代對象存活率低,使用復制算法。
- 老年代對象存活率高,使用標記-整理算法。
標記-清除算法缺點:標記和清除效率不高,會產生大量不連續的內存碎片。
復制算法:實現簡單,運行高效。缺點:浪費一半空間。
標記-整理算法:避免標記-清理導致的內存碎片,避免復制算法的空間浪費。
Android
Android內存彈性分配,分配值與最大值受具體設備影響。
Dalvik 回收算法和 ART 回收算法都是 Android 操作系統中用于內存管理的垃圾回收機制
Dalvik 回收算法:
- 標記-清除算法。
- 優點是實現簡單。缺點是在標記和清除階段都會暫停應用程序的執行,這會導致應用程序出現短暫的卡頓,影響用戶體驗。
Art 回收算法:
- 壓縮式垃圾回收(Compacting Garbage Collection)的算法。在標記-清除算法的基礎上進行了改進,以減少垃圾回收過程中的暫停時間。
- 并發標記:ART 引入了并發標記階段,這意味著它可以與應用程序的執行同時進行。這減少了由于垃圾回收導致的暫停時間。
- 清除和壓縮:在清除階段,ART 不僅清除未被標記的對象,還會壓縮內存,這意味著它會將存活的對象移動到一起,減少內存碎片。這使得內存管理更高效,并減少了內存分配失敗的可能性。
- 自適應回收:ART 還引入了自適應回收的概念,這意味著它會根據應用程序的行為和內存使用模式自動調整垃圾回收的頻率和方式。這使得 ART 可以更好地適應不同的應用程序需求。
LMK機制:
- Low Memory Killer 機制。
- 主要作用是在系統內存不足時,根據一定的優先級策略,結束一些后臺進程,以釋放內存,保證系統的穩定性和響應性。
解決
內存抖動問題
模擬問題代碼
public class ShakeActivity extends AppCompatActivity {private static Handler mHandler = new Handler() {@Overridepublic void handleMessage(@NonNull Message msg) {super.handleMessage(msg);String str = "";for (int i = 0; i < 10000000; i++) {str += i;}mHandler.sendEmptyMessageDelayed(1, 30);}};@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_shake);}public void startClick(View view) {mHandler.sendEmptyMessage(1);}@Overrideprotected void onDestroy() {super.onDestroy();mHandler.removeCallbacksAndMessages(null);}
}
使用Memory Profiler工具檢測
Memory Profiler 可以查看內存分配情況,點擊“Record Java/Kotlin allocations"。
上面的含義:
- Java:Java 或 Kotlin 代碼分配的內存。
- Native:C 或 C++ 代碼分配的內存。
- Graphics:圖形緩沖區隊列為向屏幕顯示像素(包括 GL 表面、GL 紋理等等)所使用的內存。CPU共享的內存。
- Stack:應用中的原生堆棧和 Java 堆棧使用的內存。這通常與您的應用運行多少線程有關。
- 應用用于處理代碼和資源(如 dex 字節碼、經過優化或編譯的 dex 代碼、.so 庫和字體)的內存。
- 應用使用的系統不確定如何分類的內存。
- 應用分配的 Java/Kotlin 對象數。此數字沒有計入 C 或 C++ 中分配的對象。
下面的含義:
- Allocations:通過
malloc()
或new
運算符分配的對象數量。 - Deallocations:通過
free()
或delete
運算符解除分配的對象數量。 - Allocations Size:選定時間段內所有分配的總大小,單位是字節。
- Deallocations Size:選定時間段內釋放內存的總大小,單位是字節。
- Total Count:Allocations 減去 Deallocations 的結果。
- Remaining Size:Allocations Size 減去 Deallocations Size 的結果。
- Shallow Size:堆中所有實例的總大小,單位是字節。
上圖分析:
這塊地方 Allocations 和 Deallocations 的數值比較相近,同時 Shallow Size 比較大,說明可能頻繁的創建和銷毀對象。
點擊后,可以查看調用棧信息,結合代碼可以推測出 Handler 地方存在內存抖動問題。
優化技巧
- 避免大量頻繁創建和銷毀對象。
內存泄露問題
模擬問題代碼
public class CallbackManager {public static ArrayList<Callback> sCallbacks = new ArrayList<>();public static void addCallback(Callback callback) {sCallbacks.add(callback);}public static void removeCallback(Callback callback) {sCallbacks.remove(callback);}
}
public class LeakActivity extends AppCompatActivity implements Callback {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_leak);ImageView imageView = findViewById(R.id.imageView);Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.splash);imageView.setImageBitmap(bitmap);CallbackManager.addCallback(this);}@Overridepublic void onOperate() {}
}
使用LeakCanary工具檢測
添加依賴庫:
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.14'
發生內存泄露后,LeakCanary會生成相關信息,并自動轉儲:
上圖可知:是 LeakActivity 發生內存泄露了,并顯示出引用鏈關系。
當然也可以生成 hprof 文件,通過 Profiler 工具查看具體信息:
上圖可知:發生了10個泄露點,其中就有 LeakActivity,點擊 LeakActivity 可以查看這內存泄露的對象,并查看引用鏈,可知是被ArrayList持有了。
優化技巧
- 及時回收集合元素。
- 避免static引用過多實例。
- 使用靜態內部類。
- 及時關閉資源對象。
Bitmap優化
使用完Bitmap后若不釋放圖片資源,容易造成內存泄露,從而導致內存溢出。
Bitmap內存模型
- api10之前(Android2.3.3):Bitmap對象放在堆內存,像素數據放在本地內存。
- api10之后:均在堆內存。
- api26之后(Android8.0):像素數據放在本地內存。使得native層的Bitmap像素數據可以和Java層的對象一起快速釋放。
內存回收:
- 在Android 3.0之前,需要手動調用
Bitmap.recycle()
進行Bitmap的回收。 - 從Android 3.0開始,系統提供了更智能的內存管理,大多數情況下不需要手動回收Bitmap。
Bitmap的像素配置:
Config | 占用字節大小(byte) | 說明 |
---|---|---|
ALPHA_8 | 1 | 單透明通道 |
RGB_565 | 2 | 簡易RGB色調 |
ARGB_8888 | 4 | 24位真彩色 |
RGBA_F16 | 8 | Android 8.0 新增(HDR) |
計算Btimap占用內存:
- Bitmap#getByteCount()
- getWidth() * getHeight() * 1像素占用內存
資源文件目錄
資源文件問題:
- mdpi (中等密度):大約160dpi,1x資源。
- hdpi (高密度):大約240dpi,1.5x資源。
- xhdpi (超高密度):大約320dpi,2x資源。
- xxhdpi (超超高密度):大約480dpi,3x資源。
- xxxhdpi (超超超高密度):大約640dpi,4x資源。
測試代碼:
private void printBitmap(Bitmap bitmap, String drawable) {String builder = drawable +" Bitmap占用內存:" +bitmap.getByteCount() +" width:" +bitmap.getWidth() +" height:" +bitmap.getHeight() +" 1像素占用大小:" +getByteBy1px(bitmap.getConfig());Log.e("TAG", builder);
}private int getByteBy1px(Bitmap.Config config) {if (Bitmap.Config.ALPHA_8.equals(config)) {return 1;} else if (Bitmap.Config.RGB_565.equals(config)) {return 2;} else if (Bitmap.Config.ARGB_8888.equals(config)) {return 4;}return 1;
}
// 邏輯密度
float density = metrics.density;
// 物理密度
int densityDpi = metrics.densityDpi;
Log.e("TAG", density + "-" + densityDpi);// 1倍圖
Bitmap bitmap1 = BitmapFactory.decodeResource(getResources(), R.drawable.splash1);
printBitmap(bitmap1, "drawable-mdpi");// 2倍圖
Bitmap bitmap2 = BitmapFactory.decodeResource(getResources(), R.drawable.splash2);
printBitmap(bitmap2, "drawable-xhdpi");// 3倍圖
Bitmap bitmap3 = BitmapFactory.decodeResource(getResources(), R.drawable.splash3);
printBitmap(bitmap3, "drawable-xxhdpi");// 4倍圖
Bitmap bitmap4 = BitmapFactory.decodeResource(getResources(), R.drawable.splash4);
printBitmap(bitmap4, "drawable-xxxhdpi");// drawable
Bitmap bitmap5 = BitmapFactory.decodeResource(getResources(), R.drawable.splash);
printBitmap(bitmap5, "drawable");
/*3.0-480drawable-mdpi Bitmap占用內存:37127376 width:2574 height:3606 1像素占用大小:4drawable-xhdpi Bitmap占用內存:9281844 width:1287 height:1803 1像素占用大小:4drawable-xxhdpi Bitmap占用內存:4125264 width:858 height:1202 1像素占用大小:4drawable-xxxhdpi Bitmap占用內存:2323552 width:644 height:902 1像素占用大小:4drawable Bitmap占用內存:37127376 width:2574 height:3606 1像素占用大小:4*/
說明:
在 mdpi 的設備上 1dp1px,在 xhdpi 的設備上 1dp2px,在 xxhdpi 的設備上 1dp==3px。
因此當前設備是 xxhdpi,因此同一張圖片在 xxhdpi 資源下寬是858,在 mdpi 資源下會放大3倍寬是2574,在 xhdpi 資源下會放大1.5倍寬是1287。
優化技巧
- 設置多套圖片資源。
- 選擇合適的解碼方式。
- 設置圖片緩存。