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