優(yōu)化的意義
- 減少 OOM,提高應(yīng)用穩(wěn)定性扯夭。
- 減少卡頓鳍贾,提高應(yīng)用流暢度。
- 減少內(nèi)存占用交洗,提高應(yīng)用后臺運行時的存活率骑科。
- 減少異常發(fā)生,減少代碼邏輯隱患构拳。
垃圾回收
在 GC 的過程中咆爽,其它在工作的線程會暫停,包括負責(zé)繪制的 UI 線程置森,并且在不同區(qū)域的內(nèi)存釋放速度也有一定的差異斗埂,但不管在哪個區(qū)域,都要到這次 GC 內(nèi)存回收完成后凫海,才會繼續(xù)執(zhí)行原來的線程呛凶。
雖然一次消耗性能不大,但如果大量這樣的重復(fù)行贪,就會影響到應(yīng)用的渲染 工作漾稀,造成垃圾回收動作太頻繁。這種情況很容易發(fā)生在短時間內(nèi)申請大量 的對象時建瘫,并且它們在極少的情況下能得到有效的釋放崭捍,這樣會出現(xiàn)內(nèi)存泄漏的情況。
一旦達到了剩余內(nèi)存的閾值啰脚,垃圾回收活動就會啟動殷蛇。即使有時內(nèi)存申請 很小,它們?nèi)匀粫o應(yīng)用程序的堆內(nèi)存造成壓力橄浓,還是會啟動垃圾回收晾咪,在 GC 頻繁的工作過程中消耗了非常多的時間,并且可能導(dǎo)致卡頓贮配。為了避免這樣的情況,設(shè)置一個 16ms 界線塞赂,只要 GC 消耗的時間超過了 16ms 的閾值泪勒,就會有丟幀的情況出現(xiàn)。
分析工具
使用 Memory Profiler 查看 Java 堆和內(nèi)存分配可分析內(nèi)存情況和內(nèi)存泄露。
內(nèi)存泄露
內(nèi)存泄漏就是存在一些被分配的對象圆存,可達但不可用叼旋,用不著了但還有鏈接引用著,導(dǎo)致 GC 無法回收沦辙。會導(dǎo)致內(nèi)存空間不斷減少夫植,最終內(nèi)存耗盡引起 OOM 問題。
分類
-
資源對象未關(guān)閉
資源性對象比如 BraodcastReceiver油讯、Cursor详民、File 等、往往都用了一些緩沖陌兑,在不使用的時候沈跨,應(yīng)該及時關(guān)閉它們,以便它們的緩沖及時回收內(nèi)存兔综。
它們的緩沖不僅存在于 Java 虛擬機內(nèi)饿凛,還存在于 Java 虛擬機外。如果我們僅僅是把它的引用設(shè)置為 null软驰,而不關(guān)閉它們涧窒,往往會造成內(nèi)存泄露。因為有些資源性對象锭亏,比如 SQLiteCursor(在析構(gòu)函數(shù)finalize()纠吴,如果沒有關(guān)閉它,它自己會調(diào) close() 關(guān)閉)贰镣,但是這樣的效率太低呜象。
對于資源性對象不使用的時候,應(yīng)該立即調(diào)用它的 close() 函數(shù)碑隆,將其關(guān)閉掉恭陡,然后再置為 null。
-
注冊對象未注銷
比如廣播上煤、觀察者監(jiān)聽未解除注冊休玩,會導(dǎo)致所在的 Activity 退出后無法釋放,不斷重新進入劫狠,可能造成多個對象一直釋放不掉拴疤。
-
類的靜態(tài)變量持有大數(shù)據(jù)對象
靜態(tài)變量長期維持對象的引用,阻止垃圾回收独泞,如果靜態(tài)變量持有大的 數(shù)據(jù)對象呐矾,如 Bitmap 等,就很容易引起內(nèi)存不足等問題懦砂。
比如 Activity 里創(chuàng)建靜態(tài)的 View蜒犯,而 View 又持有 Activity 對象组橄,導(dǎo)致資源無法釋放。
-
非靜態(tài)內(nèi)部類的靜態(tài)實例
非靜態(tài)內(nèi)部類會維持一個到外部類實例的引用罚随,如果非靜態(tài)內(nèi)部類的實例是靜態(tài)的玉工,就會間接長期維持著外部類的引用,阻止被系統(tǒng)回收淘菩。
比如 AsyncTask 或線程 new Runnable 都會有一個匿名內(nèi)部類遵班,因此它們對當(dāng)前 Activity 都有一個隱式引用,如果 Activity 在銷毀之前任務(wù)還未完成潮改,那么將導(dǎo)致 Activity 的內(nèi)存資源無法回收狭郑,造成內(nèi)存泄漏。
-
非靜態(tài) Handler
Handler 通過發(fā)送 Message 與主線程交互进陡,Message 發(fā)出之后存儲在 MessageQueue 中愿阐,有些 Message 不能馬上被處理。
在 Message 中存在一個 target趾疚,是 Handler 的一個引用缨历,如果 Message 在 Queue 中存在的時間過長,就會導(dǎo)致 Handler 無法被回收糙麦。
-
如果 Handler 是非靜態(tài)的辛孵,則會導(dǎo)致 Activity 或者 Service 不會被回收。所以 Handler 應(yīng)該定義為靜態(tài)內(nèi)部類赡磅,通過弱引用持有 Activity魄缚。
```java static class MyHandler extends Handler { WeakReference<Activity> mActivityReference; MyHandler(Activity activity) { mActivityReference = new WeakReference<Activity>(activity); } @Override public void handleMessage(Message msg) { final Activity activity = mActivityReference.get(); if (activity != null) { activity.mImageView.setImageBitmap(mBitmap); } } } ```
退出時
mHandler.removeCallbacksAndMessages(null)
,移除消息隊列中所有消息和所有的 Runnable焚廊。
-
-
集合中對象沒清理
把一些對象的引用加入到了集合中冶匹,當(dāng)不需要該對象時,如果沒有把它的引用從集合中清理掉咆瘟,這樣這個集合就會越來越大嚼隘。如果這個集合是 static 的話,情況就更嚴重袒餐。
-
WebView 泄露
為 WebView 開啟獨立的一個進程飞蛹,使用 AIDL 與應(yīng)用的主進程通信,WebView 所在的進程可以根據(jù)業(yè)務(wù)的需要選擇合適的時機進行銷毀灸眼,達到正常釋放內(nèi)存的目的卧檐。
-
HandlerThread 沒有主動調(diào)用 quit
HandlerThread 的 run 方法是一個死循環(huán),它不會自己結(jié)束焰宣。線程的生命周期超過了 Activity 生命周期霉囚,當(dāng)橫豎屏切換,HandlerThread 線程的數(shù)量會隨著 Activity 重建次數(shù)的增加而增加匕积。
應(yīng)該在 onDestroy 時將線程停止掉:mThread.getLooper().quit()佛嬉,比如 IntentService 里做完任務(wù)自動調(diào)用了 stopSelf逻澳,進而調(diào)用 quit。
-
Bitmap 使用不當(dāng)
用完 Bitmap 時暖呕,要及時的 recycle 掉。recycle 并不能確定立即就會將 Bitmap 釋放掉苞氮,但是會給虛擬機一個暗示:“該圖片可以釋放了”湾揽。
-
獲取系統(tǒng)服務(wù)
用 ApplicationContext 代替 Activity。
檢測函數(shù)庫 LeakCanary
LeakCanary 是 Square 公司的檢測內(nèi)存泄漏的函數(shù)庫笼吟,在 Debug 版本中監(jiān)控 Activity库物、Fragment 等的內(nèi)存泄漏。檢測到內(nèi)存泄漏時會將消息發(fā)到系統(tǒng)通知欄贷帮,點擊后打開 DisplayLeakActivity 的頁面戚揭,顯示泄漏的跟蹤消息,還默認保存了最近的 7 個 dump 文件到 APP 的目錄中撵枢,可以用 MAT 等工具進一步分析民晒。
使用
配置 gradle 文件:
dependencies {
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
}
只有 Debug 版本使用,Release 和 Test 版本用 no-op 版本锄禽,沒有實際代碼和操作潜必,不會對 APP 體積和性能產(chǎn)生影響。
在 Application 中初始化:
public class ExampleApplication extends Application {
@Override public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
LeakCanary.install(this);
// Normal app init code...
}
}
其中沃但,LeakCanary.install 方法會自動啟動一個 ActivityRefWatcher磁滚,自動監(jiān)控應(yīng)用中調(diào)用 Activity.onDestroy 之后發(fā)生泄漏的 Activity。
如果想監(jiān)控其它的對象宵晚,比如 Fragment垂攘,可以通過 install 方法返回的 RefWatcher 去監(jiān)控。
public class ExampleApplication extends Application {
@Override public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
refWatcher = LeakCanary.install(this);
// Normal app init code...
}
private RefWatcher refWatcher;
// get 方法返回 RefWatcher 對象
public static RefWatcher getRefWatcher(Context context) {
ExampleApplication application = (ExampleApplication) context.getApplicationContext();
return application.refWatcher;
}
}
然后在 Fragment 的 onDestroy 方法中調(diào)用 refWatcher 監(jiān)控
@Override
public void onDestroy() {
super.onDestroy();
RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity());
refWatcher.watch(this);
}
可以使用 watch 來監(jiān)控任何你認為已經(jīng)銷毀的對象淤刃。
原理
- RefWatcher.watch() 為被監(jiān)控對象創(chuàng)建一個 KeyedWeakReference 弱引用對象晒他,它是 WeakReference 的子類,添加鍵值對钝凶,后面會根據(jù)指定 Key 找到弱引用對象仪芒。
- 在后臺線程 AndroidWatchExecutor 中,檢查 KeyedWeakReference 弱引用是否被清除耕陷,如果存在則觸發(fā)一次垃圾回收掂名。垃圾回收后,如果弱引用對象依然存在哟沫,說明已經(jīng)內(nèi)存泄漏饺蔑,會將 Heap 內(nèi)存導(dǎo)出到
.hprof
文件中,并將文件放在 APP 的文件目錄中嗜诀。 - 在一個獨立的進程中啟動 HeapAnalyzerService 服務(wù)猾警,解析 heap dump 信息孔祸。基于唯一的 reference key发皿,在 heap dump 中找到對應(yīng)的 KeyedWeakReference崔慧,并定位發(fā)生內(nèi)存泄漏的對象引用。HeapAnalyzer 會計算 GC Roots 的最短強引用路徑穴墅,并判斷是否存在泄漏惶室,并構(gòu)建出導(dǎo)致泄漏的對象引用鏈。
定制
RefWatcher 的自定義
由于 Release 版本使用的 leakcanary-android-no-op 庫玄货,若自定義 LeakCanary皇钞,需確保只影響 Debug 版本,因為可能引用到 leakcanary-android-no-op 中沒有的 API松捉。因此需要將 Release 和 Debug 部分的代碼分離夹界。例如定義 ExampleApplication 用于 Release 版本,DebugExampleApplication 用于 Debug 版本隘世,繼承 ExampleApplication可柿。
public class ExampleApplication extends Application {
public static RefWatcher getRefWatcher(Context context) {
ExampleRefWatcher application = (ExampleRefWatcher) context.getApplicationContext();
return application.refWatcher();
}
private RefWatcher refWatcher;
@Override
public void onCreate() {
super.onCreate();
...
// 不再是調(diào)用 install 方法
refWatcher = installLeakCanary();
...
}
protected RefWatcher installLeakCanary() {
return RefWatcher.DISABLED;
}
}
新建 src/debug/java 文件夾,在其中創(chuàng)建 DebugExampleApplication:
// Debug 版本的 Application 類
public class DebugExampleApplication extends ExampleApplication {
protected RefWatcher installLeakCanary() {
RefWatcher refWatcher = LeakCanary.install(this);
return refWatcher;
}
}
在 src/debug 中新建 AndroidManifest.xml 文件:
<?xml version="1.0 encoding="utf-8" ?>
<manifest ...>
<application
tools:replace="android:name"
android:name=".DebugExampleApplication" />
</manifest>
Gradle 構(gòu)建時以舒,如果是 debug 版本趾痘,會將 src/debug/AndroidManifest.xml
的內(nèi)容合并入 src/main/AndroidManifest.xml
文件中。同時由于使用了 tools:replace
屬性蔓钟,所以 android:name
的值 DebugExampleApplication 會替換 ExampleApplication永票。
通知頁面樣式的自定義
內(nèi)存泄漏通知頁面 DisplayLeakActivity 默認的圖標和標簽兩個值,可以進行覆蓋滥沫。
圖標定義在 res 下的 drawable-hdpi/drawable-mdpi/drawable-xhdpi/drawable-xxhdpi/drawable-xxxhdpi
里侣集,名為 __leak_canary_icon.png
。
標簽定義在:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="__leak_canary_display_activity_label">MyLeaks</string>
</resources>
內(nèi)存泄漏堆棧信息保存?zhèn)€數(shù)的自定義
默認情況下兰绣,DisplayLeakActivity 在 APP 目錄中最多保存 7 個 HeapDump 文件和泄漏堆棧信息世分,可以在 APP 中定義 R.integer.__leak_canary_max_stored_leaks
來修改。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="__leak_canary_max_stored_leaks">20</string>
</resources>
Watcher 的延時
通過定義 R.integer.leak_canary_watch_delay_millis
來修改弱引用對象被認為出現(xiàn)內(nèi)存泄漏的延時時間缀辩,默認 5 秒臭埋,下面修改為 1.5 秒:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="leak_canary_watch_delay_millis">1500</string>
自定義堆棧信息和 heap dump 的處理方式
可以通過繼承 DisplayLeakService 并重寫其中的 afterDefaultHandling 函數(shù)來實現(xiàn)定制化操作,例如將 heap dump 文件發(fā)送到服務(wù)端:
public class LeakUploadService extends DisplayLeakService {
@Override
protected void afterDefaultHandling(HeapDump headDump, AnalysisResult result, String leakInfo) {
if (!result.leakFound || result.excludedLeak) {
return;
}
myServer.uploadLeakBlocking(heapDump.headDumpFile, leakInfo);
}
}
public class DebugExampleApplication extends ExampleApplication {
protected RefWatcher installLeakCanary() {
return LeakCanary.install(app, LeakUploadService.class, AndroidExcludedRefs.createAppDefaults().build());
}
}
為了使 LeakUploadService 生效臀玄,需要在 AndroidManifest.xml 中注冊瓢阴。
忽略特定的弱引用
實現(xiàn)自己的 ExcludedRefs 忽略某些特定的弱引用對象,不對其進行內(nèi)存泄漏的監(jiān)視健无。
public class DebugExampleApplication extends ExampleApplication {
protected RefWatcher installLeakCanary() {
ExcludedRefs excludedRefs = AndroidExcludedRefs.createAppDefaults()
.instanceField("com.example.Example.class", "exampleField")
.build();
return LeakCanary.install(this, DisplayLeakService.class, excludedRefs);
}
}
不監(jiān)視特定 Activity
默認會監(jiān)視所有 Activity 的內(nèi)存泄漏荣恐,默認只支持 Android 4.0 以上的系統(tǒng),如果 4.0 以下需要在 onDestroy 中主動 watch。
public class DebugExampleApplication extends ExampleApplication {
@Override
protected RefWatcher installLeakCanary() {
if (LeakCanary.isInAnalyzerProcess(this)) {
return RefWatcher.DISABLED;
} else {
ExcludedRefs excludedRefs = AndroidExcludedRefs.createAppDefaults().build();
LeakCanary.enableDisplayLeakActivity(this);
ServiceHeapDumpListener heapDumpListener = new ServiceHeapDumpListener(this, DisplayLeakService.class);
final RefWatcher refWatcher = LeakCanary.androidWathcer(this, heapDumpListener, exlcudedRefs);
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
public void onActivityDestroyed(Activity activity) {
if (activity instanceof MainActivyt) { // 排除某些 Activity
return;
}
refWatcher.watch(activity);
}
});
return refWatcher;
}
}
}
內(nèi)存優(yōu)化
使用軟/弱/虛引用
使用 ArrayMap 代替 HashMap
使用 SparseArray叠穆,SparseBooleanArray少漆,SparseLongArray 和 SparseIntArray 替換 HashMap,以減少裝箱帶來的內(nèi)存占用硼被,也避免了拆箱示损。
@IntDef,@StringDef 代替枚舉
zipalign 優(yōu)化 apk
-
節(jié)制使用 Service
如果需要使用 Service 來執(zhí)行后臺任務(wù)嚷硫,一定要任務(wù)正在執(zhí)行的時候才啟動 Service屎媳。另外,當(dāng)任務(wù)執(zhí)行完之后去停止 Service 的時候论巍,要小心停止失敗導(dǎo)致內(nèi)存泄漏的情況。
可以使用 IntentService风响,后臺任務(wù)結(jié)束后會自動停止嘉汰,從而極大程度上避免了 Service 內(nèi)存泄漏的可能性。
-
當(dāng)界面不可見時釋放內(nèi)存
Activity 中重寫 onTrimMemory()状勤,當(dāng)處于
TRIM_MEMORY_UI_HIDDEN
這個級別時鞋怀,表明用戶已經(jīng)離開了程序,所有界面都不可見持搜,此時可以進行一些資源釋放操作密似。@Override public void onTrimMemory(int level) { super.onTrimMemory(level); switch (level) { case TRIM_MEMORY_UI_HIDDEN: // 釋放資源 break; } }
圖片優(yōu)化
-
設(shè)置位圖規(guī)格
ARGB_8888 占用內(nèi)存最高,是系統(tǒng)默認葫盼。
RGB_565 會損失較多的圖片數(shù)據(jù)残腌,但除了大圖,一般看不出什么區(qū)別贫导。但它不支持 PNG 圖片的透明通道抛猫。
ARGB_4444 減少一半的數(shù)據(jù),但保留了透明通道孩灯,視覺差異變化較大闺金,一般用于用戶頭像,特別是圓角頭像峰档。
Aplha_8 主要用于 Alpha 通道模板败匹,相當(dāng)于做一個染色。圖像要渲染兩次讥巡,雖然減少內(nèi)存掀亩,但增加了 繪制的開銷。
在 Android 的基本文件結(jié)構(gòu)中不支持 PNG尚卫、JPEG 和 WEBP 格式归榕,因此需要通過 inPreferredConfig 參數(shù)來實現(xiàn)不同的位圖規(guī)格
BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = Bitmap.Config.RGB_565; BitmapFactory.decodeStream(is, null, options);
-
設(shè)置采樣率
BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(getResource(), R.drawable.ic, options); int height = options.outHeight; int width = options.outWidth; String imageType = options.outMimeType; options.inSampleSize = 2; options.inJustDecodeBounds = false; BitmapFactory.decodeResource(getResource(), R.drawable.ic, options)
-
inScaled,inDensity 和 inTargetDensity
BitmapFactory.Options options = new BitmapFactory.Options(); options.inScaled = true; options.inDensity = srcWidth; options.inTargetDensity = dstWidth; BitmapFactory.decodeStream(is, null, options);
當(dāng) inScaled 設(shè)為 true 時吱涉,系統(tǒng)會按照現(xiàn)有的密度來劃分目標密度刹泄,通過 派生綻放數(shù)來應(yīng)用到位圖上外里,使用這個方法會重設(shè)圖片大小,并對它應(yīng)用一個新的過濾特石。
雖然這些方法都非常好用盅蝗,并且減少圖片顯示需要的內(nèi)存,但因為過多的算法姆蘸,導(dǎo)致圖片顯示的過程需要更多的時間開銷墩莫,如果圖片很多的話,就影響到圖片的顯示效果逞敷。
最好的方案是結(jié)合這兩個方法狂秦,首先使用 inSampleSize 處理圖片,轉(zhuǎn)換為接近目標的 2 次冪推捐,然后用 inDensity 和 inTargetdensy 生成最終想要的準確大小裂问,因為 inSamplesize 會減少像素的數(shù)量,而 基于輸出密度的需要對像素重新過濾牛柒。
BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeStream(is, null, options); options.inScaled = true; options.inDensity = options.outWidth; options.inSampleSize = 4; options.inTargetDensity = dstWith * options.inSampleSize; options.inJustDecodeBounds = false; BitmapFactory.decodeStream(is, null, options);
-
inBitmap
Android 3.0(API 11)引進了 BitmapFactory.Options.inBitmap 字段堪簿,設(shè)置該屬性后,當(dāng)使用 了帶有該 Options 參數(shù)的 decode 方法加載內(nèi)容時皮壁,decode 方法會嘗試重用一個已經(jīng)存在的位圖椭更。這意味著位圖內(nèi)存被重用,從而改善性能蛾魄,并且沒有內(nèi)存的分配和釋放過程虑瀑。
常見的使用方案可以結(jié)合 LruCache 來實現(xiàn),在 LruCache 移除超出 cache size 的圖片時畏腕,暫時緩存 Bitmap 到一個軟引用集合缴川,需要創(chuàng)建新的 Bitmap 時,可以從這個軟引用集合中找到最適合重用的 Bitmap 來重用它的內(nèi)存區(qū)域描馅。
新申請 Bitmap 與舊的 Bitmap 必須有相同的解碼格式把夸,并且在 Android 4.4 之前,只能重用相同大小的 Bitmap 的內(nèi)存區(qū)域铭污,Android 4.4 后可以重用任何 bitmap 的內(nèi)存區(qū)域恋日。
-
drawable 目錄
不同的目錄對應(yīng)不同的顯示密度
目錄名稱 Density res/drawable 0 res/drawable-hdpi 240 res/drawable-ldpi 120 res/drawable-mdpi 160 res/drawable-xhdpi 320 res/drawable-xxhdpi 480 加載資源圖片時,會先算出屏幕密度嘹狞,然后再到對應(yīng)的資源目錄下尋找圖片岂膳,如果沒有,則到最近的目錄中尋找磅网。
比如一張圖片只放在了 res/drawable-mdpi谈截,但當(dāng)前設(shè)備密度是 480,那么系統(tǒng)會將這張圖片放大 3 倍加載到內(nèi)存。
res/drawable 在不同的設(shè)備下會被替換成不同的密度簸喂,即系統(tǒng)本身的默認密度毙死。
所以抓不準該放到哪個目錄的圖片,就盡量問設(shè)計人員要高品質(zhì)圖片然后往高密度目錄下放喻鳄,這樣在低密屏上“放大倍數(shù)”是小于 1 的扼倘,在保證畫質(zhì)的前提下,內(nèi)存也是可控的除呵。
拿不準的圖片再菊,使用
Drawable.createFromStream
替換getResources().getDrawable
來加載,這樣就可以繞過 Android 的這套默認適配法則颜曾。