前言
這篇文章的內容是我回顧和再學習 Android 內存優(yōu)化的過程中整理出來的,整理的目的是讓我自己對 Android 內存優(yōu)化相關知識的認識更全面一些,分享的目的是希望大家也能從這些知識中得到一些啟發(fā)。
Android 應用運行在 Dalvik 虛擬機上,而 Dalvik 虛擬機是基于 JVM 優(yōu)化而來的,因此只有了解了 JAVA 的內存管理機制,才能更好地理解 Android 的內存管理機制,如果你對這一塊還不熟悉的話,可以看我的上一篇文章 探索 Java 內存管理機制。
本文的內容可分為兩部分,第一部分講的是 Android 內存管理機制相關的一些知識,第二部分講的是內存問題的解決與內存優(yōu)化方法,大家可以根據自己的需要選擇性地閱讀。
1. 為什么要做內存優(yōu)化?
內存優(yōu)化就是對內存問題的一個預防和解決,做內存優(yōu)化能讓應用掛得少、活得好和活得久。
- 掛得少 “掛”指的是 Crash,假如一個滿分的應用是 100 分,那么一個會 Crash 的應用在用戶心里會扣掉 90 分。 就像是我們在一家店吃到了一盤很難吃的小龍蝦,哪怕別人說這家店再好吃,我們以后都不想吃這家店了。 導致 Android 應用 Crash 的原因有很多種,而做內存優(yōu)化就能讓我們的應用避免由內存問題引起的 Crash。 內存問題導致 Crash 的具體表現就是內存溢出異常 OOM,引起 OOM 的原因有多種,在后面我會對它們做一個更詳細的介紹。
- 活得好 活得好指的是使用流暢,Android 中造成界面卡頓的原因有很多種,其中一種就是由內存問題引起的。 內存問題之所以會影響到界面流暢度,是因為垃圾回收(GC,Garbage Collection),在 GC 時,所有線程都要停止,包括主線程,當 GC 和繪制界面的操作同時觸發(fā)時,繪制的執(zhí)行就會被擱置,導致掉幀,也就是界面卡頓。 關于 GC 的更多介紹,可以看我的上一篇文章。
- 活得久 活得久指的是我們的應用在后臺運行時不會被干掉。 Android 會按照特定的機制清理進程,清理進程時優(yōu)先會考慮清理后臺進程。 清理進程的機制就是低殺,關于低殺在后面會有更詳細的介紹。 假如現在有個用戶小張想在我們的電商應用買一個商品,千辛萬苦挑到了一個自己喜歡的商品后,當他準備購買時,小張的老婆叫他去給孩子換尿布,等小張再打開應用時,發(fā)現商品頁已經被關閉了,也就是應用被干掉了,這時小張又想起了孩子的奶粉錢,可能就放棄這次購買了。 用戶在移動設備上使用應用的過程中被打斷是很常見的,如果我們的應用不能活到用戶回來的時候,要用戶再次進行操作的體驗就會很差。
2. 什么是 Dalvik?
要了解 Android 應用的內存管理機制,就要了解承載著 Android 應用的虛擬機 Dalvik,雖然 Android 現在是使用的 ART 來承載應用的執(zhí)行,但是 ART 也是基于 Dalvik 優(yōu)化而來的。
Dalvik 是 Dalvik Virtual machine(Dalvik 虛擬機)的簡稱,是 Android 平臺的核心組成部分之一,Dalvik 與 JVM 的區(qū)別有如下幾個。
2.1 Dalvik 與 JVM 的區(qū)別
- 架構 JVM 是基于棧的,也就是需要在棧中讀取數據,所需的指令會更多,這樣會導致速度慢,不適合性能優(yōu)先的移動設備。 Dalvik 是基于寄存器的,指令更緊湊和簡潔。 由于顯式指定了操作數,所以基于寄存器的指令會比基于棧的指令要大,但是由于指令數的減少,總的代碼數不會增加多少。
- 執(zhí)行代碼不同 在 Java SE 程序中,Java 類會被編譯成一個或多個 .class 文件,然后打包成 jar 文件,JVM 會通過對應的 .class 文件和 jar 文件獲取對應的字節(jié)碼。 而 Dalvik 會用 dx 工具將所有的 .class 文件轉換為一個 .dex 文件,然后會從該 .dex 文件讀取指令和數據。
- Zygote Dalvik 由 Zygote 孵化器創(chuàng)建,Zygote 本身也是一個 Dalvik VM 進程,當系統(tǒng)需要創(chuàng)建一個進程時,Zygote 就會進行 fork,快速創(chuàng)建和初始化一個 DVM 實例。 對于一些只讀的系統(tǒng)庫,所有的 Dalvik 實例都能和 Zygote 共享一塊內存區(qū)域,這樣能節(jié)省內存開銷。
- 有限內存運行多進程 在 Androd 中,每一個應用都運行在一個 Dalvik VM 實例中,每一個 Dalvik VM 都運行在一個獨立的進程空間,這種機制使得 Dalvik 運行在有限的內存中同時運行多個進程。
- 共享機制 Dalvik 擁有預加載—共享機制,不同應用之間在運行時可以共享相同的類,擁有更高的效率。 而 JVM 不存在這種共享機制,不同的程序,打包后的程序都是彼此獨立的,即使包中使用了同樣的類,運行時也是單獨加載和運行的,無法進行共享。
- 不是 JVM Dalvik 不是 Java 虛擬機,它并不是按照 Java 虛擬機規(guī)范實現的,兩者之間并不兼容。
2.2 Dalvik 堆大小
每一個手機廠商都可以設定設備中每一個進程能夠使用的堆大小,設置進程堆大小的值有下面三個。
- dalvik.vm.heapstartsize 堆分配的初始值大小,這個值越小,系統(tǒng)內存消耗越慢,但是當應用擴展這個堆,導致 GC 和堆調整時,應用會變慢。 這個值越大,應用越流暢,但是可運行的應用也會相對減少。
- dalvik.vm.heapgrowthlimit 如果在清單文件中聲明 largeHeap 為 true,則 App 使用的內存到 heapsize 才會 OOM,否則達到 heapgrowthlimit 就會 OOM。
- dalvik.vm.heapsize 進程可用的堆內存最大值,一旦應用申請的內存超過這個值,就會 OOM。
假如我們想看其中的一個值,我們可以通過命令查看,比如下面這條命令。
adb shell getprop dalvik.vm.heapsize
3. 什么是 ART?
ART 的全稱是 Android Runtime,是從 Android 4.4 開始新增的應用運行時環(huán)境,用于替代 Dalvik 虛擬機。
Dalvik VM 和 ART 都可以支持已轉換為 .dex(Dalvik Executable)格式的 Java 應用程序的運行。
ART 與 Dalvik 的區(qū)別有下面幾個。
- 預編譯 Dalvik 中的應用每次運行時,字節(jié)碼都需要通過即時編譯器 JIT 轉換為機器碼,這會使得應用的運行效率降低。 在 ART 中,系統(tǒng)在安裝應用時會進行一次預編譯,將字節(jié)碼預先編譯成機器碼并存儲在本地,這樣應用就不用在每次運行時執(zhí)行編譯了,運行效率也大大提高。
- GC 在 Dalvik 采用的垃圾回收算法是標記-清除算法,啟動垃圾回收機制會造成兩次暫停(一次在遍歷階段,另一次在標記階段)。 而在 ART 下,GC 速度比 Dalvik 要快,這是因為應用本身做了垃圾回收的一些工作,啟動 GC 后,不再是兩次暫停,而是一次暫停。 而且 ART 使用了一種新技術(packard pre-cleaning),在暫停前做了許多事情,減輕了暫停時的工作量。
- 64 位 Dalvik 是為 32 位 CPU 設計的,而 ART 支持 64 位并兼容 32 位 CPU,這也是 Dalvik 被淘汰的主要原因。
4. 什么是低殺?4.1 低殺簡介
在 Android 中有一個心狠手辣的殺手,要想讓我們的應用活下來,就要在開發(fā)應用時格外小心。
不過我們也不用太擔心,因為它只殺“壞蛋”,只要我們不使壞,那它就不會對我們下手。
這個殺手叫低殺,它的全名是 Low Memory Killer。
低殺跟垃圾回收器 GC 很像,GC 的作用是保證應用有足夠的內存可以使用,而低殺的作用是保證系統(tǒng)有足夠的內存可以使用。
GC 會按照引用的強度來回收對象,而低殺會按照進程的優(yōu)先級來回收資源,下面我們就來看看 Android 中的幾種進程優(yōu)先級。
4.2 進程優(yōu)先級
在 Android 中不同的進程有著不同的優(yōu)先級,當兩個進程的優(yōu)先級相同時,低殺會優(yōu)先考慮干掉消耗內存更多的進程。
也就是如果我們應用占用的內存比其他應用少,并且處于后臺時,我們的應用能在后臺活下來,這也是內存優(yōu)化為我們應用帶來競爭力的一個直接體現。
當用戶通過多次點擊達到一個頁面,然后又打開了其他應用時,這時我們的應用處于后臺,如果我們的應用在后臺能活下來,意味著當用戶再次啟動我們的應用時,不需要再次進行這個繁瑣的操作。
4.2.1 前臺進程
前臺進程(Foreground Process)是優(yōu)先級最高的進程,是正在于用戶交互的進程,如果滿足下面一種情況,則一個進程被認為是前臺進程。
- Activity 進程持有一個與用戶交互的 Activity(該 Activity 的 onResume 方法被調用)
- 進程持有一個 Service,并且這個 Service 處于下面幾種狀態(tài)之一
- Service 與用戶正在交互的 Activity 綁定
- Service 調用了 startForeground 方法
- Service 正在執(zhí)行以下生命周期函數(onCreate、onStart、onDestroy )
- BroadcastReceiver 進程持有一個 BroadcastReceiver,這個 BroadcastReceiver 正在執(zhí)行它的 onReceive 方法
4.2.2 可見進程
可見進程(Visible Process)不含有任何前臺組件,但用戶還能再屏幕上看見它,當滿足一下任一條件時,進程被認定是可見進程。
- Activity 進程持有一個 Activity,這個 Activity 處于 pause 狀態(tài),比如前臺 Activity 打開了一個對話框,這樣后面的 Activity 就處于 pause 狀態(tài)
- Service 進程持有一個 Service 這個 Service 和一個可見的 Activity 綁定。
可見進程是非常重要的進程,除非前臺進程已經把系統(tǒng)的可用內存耗光,否則系統(tǒng)不會終止可見進程。
4.2.3 服務進程
服務進程(Service Process)可能在播放音樂或在后臺下載文件,除非系統(tǒng)內存不足,否則系統(tǒng)會盡量維持服務進程的運行。
當一個進程滿足下面一個條件時,系統(tǒng)會認定它為服務進程。
- Service 如果一個進程中運行著一個 Service,并且這個 service 是通過 startService 開啟的,那這個進程就是一個服務進程。
4.2.4 后臺進程
當一個進程滿足下面條件時,系統(tǒng)會認定它為后臺進程。
- Activity 當進程持有一個用戶不可見的 Activity(Activity 的 onStop 方法被調用),但是 onDestroy 方法沒有被調用,這個進程就會被系統(tǒng)認定為后臺進程。
系統(tǒng)會把后臺進程保存在一個 LruCache 列表中,因為終止后臺進程對用戶體驗影響不大,所以系統(tǒng)會酌情清理部分后臺進程。
你可以在 Activity 的 onSaveInstanceState 方法中保存一些數據,以免在應用被系統(tǒng)清理掉后,用戶已輸入的信息被清空,導致要重新輸入。
4.2.5 空進程
當一個進程不包含任何活躍的應用組件,則被系統(tǒng)認定為是空進程。
系統(tǒng)保留空進程的目的是為了加快下次啟動進程的速度。
5. 圖片對內存有什么影響?
大部分 App 都免不了使用大量的圖片,比如電商應用和外賣應用等。
圖片在 Android 中對應的是 Bitmap 和 Drawable 類,我們從網絡上加載下來的圖片最終會轉化為 Bitmap。
圖片會消耗大量內存,如果使用圖片不當,很容易就會造成 OOM。
下面我們來看下 Bitmap 與內存有關的一些內容。
5.1 獲取 Bitmap 占用的內存大小
- Bitmap.getByteCount Bitmap 提供了一個 getByteCount 方法獲取圖片占用的內存大小,但是這個方法只能在程序運行時動態(tài)計算。
- 圖片內存公式 圖片占用內存公式:寬 * 高 * 一個像素占用的內存。 假如我們現在有一張 2048 * 2048 的圖片,并且編碼格式為 ARGB_8888,那么這個圖片的大小為 2048 * 2048 * 4 = 16, 777, 216 個字節(jié),也就是 16M。 如果廠商給虛擬機設置的堆大小是 256M,那么像這樣的圖片,應用最極限的情況只能使用 16 張。 我們的應用在運行時,不僅僅是我們自己寫的代碼需要消耗內存,還有庫中創(chuàng)建的對象同樣需要占用堆內存,也就是別說 16 張,多來幾張應用就掛了。
5.2 Bitmap 像素大小
一張圖片中每一個像素的大小取決于它的解碼選項,而 Android 中能夠選擇的 Bitmap 解碼選項有四種。
下面四種解碼選項中的的 ARGB 分別代表透明度和三原色 Alpha、Red、Green、Blue。
- ARGB_8888 ARGB 四個通道的值都是 8 位,加起來 32 位,也就是每個像素占 4 個字節(jié)
- ARGB_4444 ARGB 四個通道的值都是 4 位,加起來 16 位,也就是每個像素占 2 個字節(jié)
- RGB_565 RGB 三個通道分別是 5 位、6 位、5 位,加起來 16 位,也就是每個像素占 2 個字節(jié)
- ALPHA_8 只有 A 通道,占 8 位,也就是每個像素占 1 個字節(jié)
5.3 Glide
如果服務器返回給我們的圖片是 200 * 200,但是我們的 ImageView 大小是 100 * 100,如果直接把圖片加載到 ImageView 中,那就是一種內存浪費。
但是使用的 Glide 的話,那這個問題就不用擔心了,因為 Glide 會根據 ImageView 的大小把圖片大小調整成 ImageView 的大小加載圖片,并且 Glide 有三級緩存,在內存緩存中,Glide 會根據屏幕大小選擇合適的大小作為圖片內存緩存區(qū)的大小。
6. 什么是內存泄漏?6.1 內存泄漏簡介
內存泄漏指的是,當一塊內存沒有被使用,但無法被 GC 時的情況。
堆中一塊泄漏的內存就像是地上一塊掃不掉的口香糖,都很讓人討厭。
一個典型的例子就是匿名內部類持有外部類的引用,外部類應該被銷毀時,GC 卻無法回收它,比如在 Activity 中創(chuàng)建 Handler 就有可能出現這種情況。
內存泄漏的表現就是可用內存逐漸減少,比如下圖中是一種比較嚴重的內存泄漏現象,無法被回收的內存逐漸累積,直到無更多可用內存可申請時,就會導致 OOM。
6.2 常見的內存泄漏原因
常見的造成內存泄漏的原因有如下幾個。
6.2.1 非靜態(tài)內部類
- 原因 非靜態(tài)內部類會持有外部類的實例,比如匿名內部類。 匿名內部類指的是一個沒有人類可識別名稱的類,但是在字節(jié)碼中,它還是會有構造函數的,而它的構造函數中會包含外部類的實例。 比如在 Activity 中以匿名內部類的方式聲明 Handler 或 AsyncTask,當 Activity 關閉時,由于 Handler 持有 Activity 的強引用,導致 GC 無法對 Activity 進行回收。 當我們通過 Handler 發(fā)送消息時,消息會加入到 MessageQueue 隊列中交給 Looper 處理,當有消息還沒發(fā)送完畢時,Looper 會一直運行,在這個過程中會一直持有 Handler,而 Handler 又持有外部類 Activity 的實例,這就導致了 Activity 無法被釋放。
- 解決 我們可以把 Handler 或 AsyncTask 聲明為靜態(tài)內部類,并且使用 WeakReference 包住 Activity,這樣 Handler 拿到的就是一個 Activity 的弱引用,GC 就可以回收 Activity。 這種方式適用于所有匿名內部類導致的內存泄漏問題。
Activity activity;
publicMyHandler(Activity activity){
activity = newWeakReference<>(activity).get;
}
@Override
publicvoidhandleMessage(Message message){
// ...
}
}
6.2.2 靜態(tài)變量
- 原因 靜態(tài)變量導致內存泄漏的原因是因為長生命周期對象持有了短生命周期對象的引用,導致短生命周期對象無法被釋放。 比如一個單例持有了 Activity 的引用,而 Activity 的生命周期可能很短,用戶一打開就關閉了,但是單例的生命周期往往是與應用的生命周期相同的。
- 解決 如果單例需要 Context, 可以考慮使用 ApplicationContext,這樣單例持有的 Context 引用就是與應用的生命周期相同的了。
- 忘了注銷 BroadcastReceiver
- 打開了數據庫游標(Cursor)忘了關閉
- 打開流忘了關閉
- 創(chuàng)建了 Bitmap 但是調用 recycle 方法回收 Bitmap 使用的內存
- 使用 RxJava 忘了在 Activity 退出時取消任務
- 使用協程忘了在 Activity 退出時取消任務
- 原因 不同的 Android 版本的 Webview 會有差異,加上不同廠商定制 ROM 的 Webview 的差異,導致 Webview 存在很大的兼容問題。 一般情況下,在應用中只要使用一次 Webview,它占用的內存就不會被釋放。
- 解決 WebView內存泄漏--解決方法小結
7. 什么是內存抖動?7.1 內存抖動簡介
當我們在短時間內頻繁創(chuàng)建大量臨時對象時,就會引起內存抖動,比如在一個 for 循環(huán)中創(chuàng)建臨時對象實例。
下面這張圖就是內存抖動時的一個內存圖表現,它的形狀是鋸齒形的,而中間的垃圾桶代表著一次 GC。
這個是 Memory Profiler 提供的內存實時圖,后面會對 Memory Profiler 進行一個更詳細的介紹。
7.2 預防內存抖動的方法
- 盡量避免在循環(huán)體中創(chuàng)建對象
- 盡量不要在自定義 View 的 onDraw 方法中創(chuàng)建對象,因為這個方法會被頻繁調用
- 對于能夠復用的對象,可以考慮使用對象池把它們緩存起來
8. 什么是 Memory Profiler?8.1 Profiler8.1.1 Profiler 簡介
Profiler 是 Android Studio 為我們提供的性能分析工具,它包含了 CPU、內存、網絡以及電量的分析信息,而 Memory Profiler 則是 Profiler 中的其中一個版塊。
打開 Profiler 有下面三種方式。
- View > Tool windows > Android Profiler
- 下方的 Profiler 標簽
- 雙擊 shift 搜索 profiler
打開 Profiler 后,可以看到下面這樣的面板,而在左邊的 SESSIONS 面板的右上角,有一個加號,在這里可以選擇我們想要進行分析的應用。
8.1.2 Profiler 高級選項
打開了高級選項后,我們在 Memory Profiler 中就能看到用一個白色垃圾桶表示的 GC 動作。
打開 Profiler 的方式:Run > Edit Configucation > Profiling > Enable advanced profiling
8.2 Memory Profiler 簡介
Memory Profiler 是 Profiler 的其中一個功能,點擊 Profiler 中藍色的 Memory 面板,我們就進入了 Memory Profiler 界面。
8.3 堆轉儲
在堆轉儲(Dump Java Heap)面板中有 Instance View(實例視圖)面板,Instance View 面板的下方有 References 和 Bitmap Preview 兩個面板,通過 Bitmap Preview,我們能查看該 Bitmap 對應的圖片是哪一張,通過這種方式,很容易就能找到圖片導致的內存問題。
要注意的是,Bitmap Preview 功能只有在 7.1 及以下版本的設備中才能使用。
8.4 查看內存分配詳情
在 7.1 及以下版本的設備中,可以通過 Record 按鈕記錄一段時間內的內存分配情況。
而在 8.0 及以上版本的設別中,可以通過拖動時間線來查看一段時間內的內存分配情況。
點擊 Record 按鈕后,Profiler 會為我們記錄一段時間內的內存分配情況。在內存分配面板中,我們可以查看對象的分配的位置,比如下面的 Bitmap 就是在 onCreate 方法的 22 行創(chuàng)建的。
9. 什么是 MAT?9.1 MAT 介紹
對于內存泄漏問題,Memory Profiler 只能給我們提供一個簡單的分析,不能夠幫我們確認具體發(fā)生問題的地方。
而 MAT 就可以幫我們做到這一點,MAT 的全稱是 Memory Analyzer Tool,它是一款功能強大的 Java 堆內存分析工具,可以用于查找內存泄漏以及查看內存消耗情況。
9.2 MAT 使用步驟
要想通過 MAT 分析內存泄漏,我們做下面幾件事情。
- 到 MAT 的官網下載 MAT。
- 使用 Memory Profiler 的堆轉儲功能,導出 hprof(Heap Profile)文件。
- 配置 platform-tools 環(huán)境變量
- 使用命令將 Memory Profiler 中導出來的 hprof 文件轉換為 MAT 可以解析的 hprof 文件,命令如下 platform-tools hprof-conv ../原始文件.hprof ../輸出文件.hprof
- 打開 MAT
- File > open Heap dump ,選擇我們轉換后的文件
9.3 注意事項
- 如果在 mac 上打不開 MAT,可以參考Eclipse Memory Analyzer在Mac啟動報錯
- 如果在 mac 上配置 platform-tools 不成功的話,可以直接定位到 Android SDK 下的 platform-tools 目錄,直接使用 hprof-conv 工具,命令如下 hprof-conv -z ../原始文件.hprof ../輸出文件.hprof
10. 怎么用 MAT 分析內存泄漏?
我在項目中定義了一個靜態(tài)的回調列表 sCallbacks,并且把 MemoryLeakActivity 添加到了這個列表中,然后反復進出這個 Activity,我們可以看到這個 Activity 的實例有 8 個,這就屬于內存泄漏現象,下面我們來看下怎么找出這個內存泄漏。
首先,按 8.3 小節(jié)的步驟打開我們的堆轉儲文件,打開后,我們可以看到 MAT 為我們分析的一個預覽頁。
打開左上角的直方圖,我們可以看到一個類列表,輸入我們想搜索的類,就可以看到它的實例數。
我們右鍵 MemoryLeakActivity 類,選擇 List Objects > with incoming references 查看這個 Activity 的實例。
點擊后,我們能看到一個實例列表,再右鍵其中一個實例,選擇 Path to GC Roots > with all references 查看該實例被誰引用了,導致無法回收。
選擇 with all references 后,我們可以看到該實例被靜態(tài)對象 sCallbacks 持有,導致無法被釋放。
這樣就完成了一次內存泄漏的分析。
11. 什么是 LeakCanary?11.1 LeakCanary 簡介
如果使用 MAT 來分析內存問題,會有一些難度,而且效率也不是很高。
為了能迅速發(fā)現內存泄漏,Square 公司基于 MAT 開源了 LeakCanary。
LeakCanary 是一個內存泄漏檢測框架。
11.2 LeakCanary 原理
- 檢測保留的實例 LeakCanary 是基于 LeakSentry 開發(fā)的,LeakSentry 會 hook Android 聲明周期,并且會自動檢測當 Activity 或 Fragment 被銷毀時,它們的實例是否被回收了。 銷毀的實例會傳給 RefWatcher,RefWatcher 會持有它們的弱引用。 你也可以觀察所有不再需要的實例,比如一個不再使用的 View,不再使用的 Presenter 等。 如果等待了 5 秒,并且 GC 觸發(fā)了之后,弱引用還沒有被清理,那么 RefWatcher 觀察的實例就可能處于內存泄漏狀態(tài)了。
- 堆轉儲 當保留實例(Retained Instance)的數量達到了一個閾值,LeakCanary 會進行堆轉儲,并把數據放進 hprof 文件中。 當 App 可見時,這個閾值是 5 個保留實例,當 App 不可見時,這個閾值是 1 個保留實例。
- 泄漏蹤跡 LeakCanary 會解析 hprof 文件,并且找出導致 GC 無法回收實例的引用鏈,這也就是泄漏蹤跡(Leak Trace)。 泄漏蹤跡也叫最短強引用路徑,這個路徑是 GC Roots 到實例的路徑。
- 泄漏分組 當有兩個泄漏分析結果相同時,LeakCanary 會根據子引用鏈來判斷它們是否是同一個原因導致的,如果是的話,LeakCanary 會把它們歸為同一組,以免重復顯示同樣的泄漏信息。
11.2 安裝 LeakCanary11.2.1 AnroidX 項目
- 添加依賴 dependencies { // 使用 debugImplementation 是因為 LeakCanary 一般不用于發(fā)布版本 debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0-alpha-3' }
- 監(jiān)控特定對象 // 1. 在 Application 中定義一個 RefWatcher 的靜態(tài)變量 companion object { val refWatcher = LeakSentry.refWatcher } // 2. 使用 RefWatcher 監(jiān)控該對象 MyApplication.refWatcher.watch(object);
- 配置監(jiān)控選項private fun initLeakCanary { LeakSentry.config = LeakSentry.config.copy(watchActivities = false) }
11.2.1 非 AndroidX 項目
- 添加依賴dependencies { debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.3' releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.3' // 只有在你使用了 support library fragments 的時候才需要下面這一項 debugImplementation 'com.squareup.leakcanary:leakcanary-support-fragment:1.6.3' }
- 初始化 LeakCanarypublic class MyApplication extends Application { @Override public void onCreate { super.onCreate; // 不需要再 LeakCanary 用來做堆分析的進程中初始化 LeakCanary if (!LeakCanary.isInAnalyzerProcess(this)) { LeakCanary.install(this); return; } } }
- 監(jiān)控特定對象// 1. 在 Application 中定義一個獲取 RefWatcher 的靜態(tài)方法 public static RefWatcher getRefWatcher() { return LeakCanary.installedRefWatcher; } // 2. 使用 RefWatcher 監(jiān)控該對象 MyApplication.getRefWatcher.watch(object);
- 配置監(jiān)控選項public class MyApplication extends Application { private void installLeakCanary { RefWatcher refWatcher = LeakCanary.refWatcher(this) .watchActivities(false) .buildAndInstall; } }
當安裝完成,并且重新安裝了應用后,我們可以在桌面看到 LeakCanary 用于分析內存泄漏的應用。
下面這兩張圖中,第一個是 LeakCanary 為非 AndroidX 項目安裝的應用,第二個是 LeakCanary 為 AndroidX 項目安裝的應用。
11.4 使用 LeakCanary 分析內存泄漏
下面是一個靜態(tài)變量持有 Activity 導致 Activity 無法被釋放的一個例子。
publicclassMemoryLeakActivityextendsAppCompatActivity{
publicstaticList<Activity> activities = newArrayList<>;
@Override
protectedvoidonCreate(@Nullable Bundle savedInstanceState){
super.onCreate(savedInstanceState);
activities.add( this);
}
}
我們可以在 Logcat 中看到泄漏實例的引用鏈。
除了 Logcat,我們還可以在 Leaks App 中看到引用鏈。
點擊桌面上 LeakCanary 為我們安裝的 Leaks 應用后,可以看到 activities 變量,之所以在這里會顯示這個變量,是因為 LeakCanary 分析的結果是這個變量持有了某個實例,導致該實例無法被回收。
點擊這一項泄漏信息,我們可以看到一個泄漏信息概覽頁。
我們點擊第一項 MemoryActivity Leaked,可以看到泄漏引用鏈的詳情。
通過上面這些步驟,很簡單地就能找到 LeakCanary 為我們分析的導致內存泄漏的地方。
12. 怎么獲取和監(jiān)聽系統(tǒng)內存狀態(tài)?
Android 提供了兩種方式讓我們可以監(jiān)聽系統(tǒng)內存狀態(tài),下面我們就來看看這兩種方式的用法。
12.1 ComponentCallback2
在 Android 4.0 后,Android 應用可以通過在 Activity 中實現 ComponentCallback2 接口獲取系統(tǒng)內存的相關事件,這樣就能在系統(tǒng)內存不足時提前知道這件事,提前做出釋放內存的操作,避免我們自己的應用被系統(tǒng)干掉。
ComponentCallnback2 提供了 onTrimMemory(level) 回調方法,在這個方法里我們可以針對不同的事件做出不同的釋放內存操作。
importandroid.content.ComponentCallbacks2
classMainActivity: AppCompatActivity, ComponentCallbacks2 {
/**
* 當應用處于后臺或系統(tǒng)資源緊張時,我們可以在這里方法中釋放資源,
* 避免被系統(tǒng)將我們的應用進行回收
* @paramlevel 內存相關事件
*/
overridefunonTrimMemory(level: Int){
// 根據不同的應用生命周期和系統(tǒng)事件進行不同的操作
when(level) {
// 應用界面處于后臺
ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> {
// 可以在這里釋放 UI 對象
}
// 應用正常運行中,不會被殺掉,但是系統(tǒng)內存已經有點低了
ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE,
// 應用正常運行中,不會被殺掉,但是系統(tǒng)內存已經非常低了,
// 這時候應該釋放一些不必要的資源以提升系統(tǒng)性能
ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
// 應用正常運行,但是系統(tǒng)內存非常緊張,
// 系統(tǒng)已經開始根據 LRU 緩存殺掉了大部分緩存的進程
// 這時候我們要釋放所有不必要的資源,不然系統(tǒng)可能會繼續(xù)殺掉所有緩存中的進程
ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> {
// 釋放資源
}
// 系統(tǒng)內存很低,系統(tǒng)準備開始根據 LRU 緩存清理進程,
// 這時我們的程序在 LRU 緩存列表的最近位置,不太可能被清理掉,
// 但是也要去釋放一些比較容易恢復的資源,讓系統(tǒng)內存變得充足
ComponentCallbacks2.TRIM_MEMORY_BACKGROUND,
// 系統(tǒng)內存很低,并且我們的應用處于 LRU 列表的中間位置,
// 這時候如果還不釋放一些不必要資源,那么我們的應用可能會被系統(tǒng)干掉
ComponentCallbacks2.TRIM_MEMORY_MODERATE,
// 系統(tǒng)內存非常低,并且我們的應用處于 LRU 列表的最邊緣位置,
// 系統(tǒng)會有限考慮干掉我們的應用,如果想活下來,就要把所有能釋放的資源都釋放了
ComponentCallbacks2.TRIM_MEMORY_COMPLETE -> {
/*
* 把所有能釋放的資源都釋放了
*/
}
// 應用從系統(tǒng)接收到一個無法識別的內存等級值,
// 跟一般的低內存消息提醒一樣對待這個事件
else-> {
// 釋放所有不重要的數據結構。
}
}
}
}
12.2 ActivityManager.getMemoryInfo
Android 提供了一個 ActivityManager.getMemoryInfo 方法給我們查詢內存信息,這個方法會返回一個 ActivityManager.MemoryInfo 對象,這個對象包含了系統(tǒng)當前內存狀態(tài),這些狀態(tài)信息包括可用內存、總內存以及低殺內存閾值。
MemoryInfo 中包含了一個 lowMemory 布爾值,這個布爾值用于表明系統(tǒng)是否處于低內存狀態(tài)。
fundoSomethingMemoryIntensive{
// 在做一些需要很多內存的任務前,
// 檢查設備是否處于低內存狀態(tài)、
if(!getAvailableMemory.lowMemory) {
// 做需要很多內存的任務
}
}
// 獲取 MemoryInfo 對象
privatefungetAvailableMemory: ActivityManager.MemoryInfo {
valactivityManager = getSystemService(Context.ACTIVITY_SERVICE) asActivityManager
returnActivityManager.MemoryInfo.also { memoryInfo ->
activityManager.getMemoryInfo(memoryInfo)
}
}
13. 還有哪些內存優(yōu)化技巧?13.1 使用更高效的代碼結構13.1.1 謹慎使用 Service
(下面這些內容是我在 Andorid 官網上翻譯的,從我們的應用角度來說,當然希望是應用一直運行,這樣用戶每次打開都不用重新走各種初始化流程,但是對于系統(tǒng)來說,我們的這種行為傷害挺大的。)
讓一個沒用的 Service 在后臺運行對于一個應用的內存管理來說是一件最糟糕的事情。
要在 Service 的任務完成后停止它,不然 Service 占用的這塊內存會泄漏。
當你的應用中運行著一個 Service,除非系統(tǒng)內存不足,否則它不會被干掉。
這就導致對于系統(tǒng)來說 Service 的運行成本很高,因為 Service 占用的內存其他的進程是不能使用的。
Android 有一個緩存進程列表,當可用內存減少時,這個列表也會隨之縮小,這就會導致應用間的切換變得很慢。
如果我們是用 Service 監(jiān)聽一些系統(tǒng)廣播,可以考慮使用 JobScheduler。
如果你真的要用 Service,可以考慮使用 IntentService,IntentService 是 Service 的一個子類,在它的內部有一個工作線程來處理耗時任務,當任務執(zhí)行完后,IntentService 就會自動停止。
13.1.2 選擇優(yōu)化后的數據容器
Java 提供的部分數據容器并不適合 Android,比如 HashMap,HashMap 需要中存儲每一個鍵值對都需要一個額外的 Entry 對象。
Android 提供了幾個優(yōu)化后的數據容器,包括 SparseArray、SparseBooleanArray 以及 LongSparseArray。
SparseArray 之所以更高效,是因為它的設計是只能使用整型作為 key,這樣就避免了自動裝箱的開銷。
13.1.3 小心代碼抽象
抽象可以優(yōu)化代碼的靈活性和可維護性,但是抽象也會帶來其他成本。
抽象會導致更多的代碼需要被執(zhí)行,也就是需要更多的時間和把更多的代碼映射到內存中。
如果某段抽象代碼帶來的好處不大,比如一個地方可以直接實現而不需要用到接口的,那就不用接口。
13.1.4 使用 protobuf 作為序列化數據
Protocol buffers 是 google 設計的,它可以對結構化的數據序列化,與 XML 類似,不過比 XML 更小,更快,而且更簡單。
如果你決定使用 protobuf 作為序列化數據格式,那在客戶端代碼中應該使用輕量級的 protobuf。
因為一般的 protobuf 會生成冗長的代碼,這樣會導致內存增加、APK 大小增加,執(zhí)行速度變慢等問題。
更多關于 protobuf 的信息可以查看 protobuf readme 中的 “輕量級版本” 。
13.2 刪除內存消耗大的資源和第三方庫
有些資源和第三方庫會在我們不知情的情況下大量消耗內存。
APK 大小,第三方庫和嵌入式資源,會影響我們應用的內存消耗,我們可以通過刪除冗余和不必要的資源和第三方庫來減少應用的內存消耗。
13.2.1 Apk 瘦身
Bitmap 大小、資源、動畫以及第三方庫會影響到 APK 的大小,Android Studio 提供了 R8 和 ProGuard 幫助我們縮小 Apk,去掉不必要的資源。
如果你使用的 Android Studio 版本是 3.3 以下的,可以使用 ProGuard,3.3 及以上版本的可以使用 R8。
13.2.2 使用 Dagger2 進行依賴注入
依賴注入框架不僅可以簡化我們的代碼,而且能讓我們在測試代碼的時候更方便。
如果我們想在應用中使用依賴注入,可以考慮使用 Dagger2。
Dagger2 是在編譯期生成代碼,而不是用反射實現的,這樣就避免了反射帶來的內存開銷,而是在編譯期生成代碼,
13.2.3 謹慎使用第三方庫
當你決定使用一個不是為移動平臺設計的第三方庫時,你需要對它進行優(yōu)化,讓它能更好地在移動設備上運行。
這些第三方庫包括日志、分析、圖片加載、緩存以及其他框架,都有可能帶來性能問題。