在上一篇文章Android內存優化(一):Java內存區域中已經大體上介紹了Java中的內存分布情況,這一篇主要講一下內存泄漏的產生原因、內存泄漏的危害、內存泄漏一鍵分析與定位、以及代碼中常見的內存泄漏。
1內存泄漏的產生原因
前方高能,18歲以下請避讓!!!
驚天大咪咪:內存泄漏產生的原因是對象占著茅坑不拉屎!!!
有必要講一下Android中的垃圾收集是怎么進行的,Android中使用標記-清除(Mark-Sweep)算法進行垃圾回收(garbage collection,簡稱GC),就是按照正常套路來說,在坑位(內存)不夠的情況下,垃圾收集器會遍歷全部對象,看哪些對象是可以被回收掉騰出內存的,這個過程稱為Mark(標記),Mark的時候要求除了垃圾收集線程之外,其它的線程都停止,這種吊炸天的現象在垃圾收集算法中稱為Stop The World,世界圍著他轉,這就造成了我們的程序會卡頓,但是一般情況下這個時間就幾十毫秒,我根本就感受不到好嗎。Mark完之后,就是釋放內存空間啦,這個過程稱為Sweep(清除)。
這一切看起來很美好,但是就是有內存泄漏發生,所以得提一下,不是所有的對象都是特侖蘇,阿呸,不是所有的對象都能被回收的,比如下面的傲嬌賤貨。
- 垃圾回收的原則:被全局變量(static)、棧變量和寄存器等直接引用和間接引用的對象不能被回收。
所以說,對象即使已經使用完,但卻一直被其它對象引用,就會導致這個對象無法被回收,造成內存的浪費,讓別的對象無屎可拉。對象無法被GC回收就是造成內存泄露的原因!
2內存泄漏的可能會造成的創傷
如果不是利用工具去找的話,一般情況下內存泄漏是比較難發現的,因為Java中不會報內存泄漏這種異常,所以在輕微的內存泄漏表面上看是跟正常情況下沒有區別的。
- 2.1 內存泄漏跟內存溢出(OOM)的區別就是:量變和質變。一個兩個內存泄漏表面看起來沒毛病,但是量變可以導致質變,內存泄漏多了會炸的,就是報OOM異常,應用直接崩潰,連解釋的機會都沒有。
- 2.2 堆得內存大小是確定的,出現內存泄漏后可用的內存會減小,這又會造成垃圾回收的頻率加劇,上面提到過,垃圾回收的Mark階段會有一種吊炸天的現象,就是Stop The World,除了垃圾回收線程之外的線程會停止,頻繁的垃圾回收卡頓明顯的感受到。
- 2.3 應用后臺運行的時候,內存占用大,進程被系統殺死的概率就會大咯。
3內存泄漏的發現
內存泄漏的分析的話,必須使用工具才行,慶幸的是,各路大神已經給我們提供了很多強大的內存分析工具,我這里只會講最方便的。這里提供幾個套餐供選擇
3.1 套餐一:Studio自帶Heap Viewer
想不想知道你的應用到底有沒有內存泄漏呢?說真,就一分鐘的事。
3.1.1打開Studio,連上你的應用,然后Android Monitor (1)->Monitors(2)->Memory,上面有四個圖標,暫停圖標是開啟內存使用狀態追蹤的開關,默認是開啟的,小車圖標就是手動GC(3),向下箭頭圖標(4)是查看堆的分配情況,最后的圖標allocation tracker用來跟蹤內存分配情況。
3.1.2我講一下我的使用方式,在應用中操作,從activity1跳轉到activity2,然后跳回到activity1界面,這樣是為了分析activity2是否會產生內存泄漏。接下來就是真刀真槍的干了。
-
3.1.3點擊小車圖標(3),手動GC進行垃圾回收,這樣才能更準確的判斷activity2是否有內存泄漏發生,最后點擊向下箭頭圖標(4),Studio會自動生成hprof文件并自動展示在Studio界面中。
-
3.1.4這個就是內存的分析文件了,點擊Analyzer Tasks(5),這是讓Studio幫我們自動分析是否出現內存泄漏。
-
3.1.5勾上Detect Leaked Activities(6),最后運行(7)就出現分析結果了
-
3.1.6看到沒,activity2出現內存泄漏了(8),左下角是引用樹(9),通過引用樹就可以定位到內存泄漏的具體信息了。
3.2套餐二:Heap Viewer + MAT
是啊,發現有內存泄漏了,然而還有其它的選擇,這里就必須使用到其它的工具進行輔助了。
MAT(Memory Analyzer)內存分析工具,這個工具的使用我只簡單講一下,因為我一般不用,不要問為什么,因為用起來比較麻煩一些。
- 3.2.1MAT下載,進入下載的官網,我電腦是64位的,所以選擇Windows(x86_64),整個下載安裝流程跟一般軟件沒啥區別,進入新頁面然后點擊DOWNLOAD
點擊click here就可以下載使用了
-
3.2.2 hprof文件導入,這個文件的獲取流程跟內存泄漏的發現流程基本一樣,按上面說的通過Studio的Heap工具獲取的,但是文件導入前需要進行一下轉換,因為MAT工具不能直接使用,轉換也
不麻煩,Studio已經幫你簡化這個過程,一鍵導出轉換文件,請看過來
-
3.2.3 用MAT打開hprof的轉換文件,其中Histogram和Dominator Tree比較常用,分析內存泄漏特別需要用到Histogram的兩份文件對比分析,就是獲取兩份內存泄漏前后的hprof轉換文件
-
3.2.3 標題欄Window->Navigator History,打開 Navigator History面板,然后點擊打開Histogram
-
3.2.4 右鍵histogram,將兩份分析文件的
Histogram結果都添加到 Compare Basket中,點擊右上角的!圖標就會生成對比文件
-
3.2.5 這就是最后生成的對比文件,你還可以自己選擇對比的方式,紅圈里面提供不同的對比方式,這樣就可以很直觀的看出差異,因為我對比的是同一份文件,所以對象間木有差異。
3.3套餐三:Leakcanary
square的開源內存泄漏分析框架,好用得不得了,配置很簡單
- 3.3.1建議在app的
build.gradle
文件下添加下面的依賴
dependencies {
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
}
- 3.3.2在你的
Application
中的onCreate()
方法中進行初始化
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...
}
}
-
3.3.3然后,就沒有然后了,編譯完后運行你的項目,會在項目安裝成功后出現附加的組件,里面會展示具體的內存泄漏路徑。
-
3.3.4通過這個泄漏路徑,就對應進行內存泄漏的原因進行分析了,你也可以通過輸出的日志進行內存泄漏的定位。
注:到這里3個套餐已經講完了,關于MAT這個套餐我只是講一下基本的使用,其實已經夠用了,怎么說呢,用起來比較麻煩,所以我自己本身也很少用,我就按自己的使用對比一下三者。
套餐三>套餐一>套餐二
1.套餐三使用最方便,一勞永逸,解析hprof的速度有點慢,但是因為后臺自動解析,所以基本上沒多大關系;
2.套餐一使用最快,切換一下頁面分分鐘就知道有沒有內存泄漏,但是需要你每一次都要手動操作;
3.套餐三最麻煩,耗時耗力,但是自動分析工具并不能保證找出所有的內存泄漏,這個時候就需要通過MAT輔助分析了。
4代碼里頭內存泄漏的常見原因
代碼中內存泄漏大多數產生的原因是不遵循activity的生命周期。
- 4.1單例模式(靜態activity):在你的Activity中定義了一個 static 變量引用了activity,因為static變量的生命周期和app一樣長,就算activity被銷毀,activity對象還是會被static變量持有,一直到app被銷毀,這也是單例模式最容易造成泄漏的原因,如果靜態的單例對象持有activity對象的引用,就會使得該對象不能被正常回收,從而導致了內存泄漏。解決辦法是使用Application的Context代替activity的context;
/**
* 單例模式
*/
public class SingletonClass{
private static SingletonClass instance;
private Context context;
public static SingletonClass getInstance(Context context){
synchronized(SingletonClass.class){
if(instance==null){
instance=new SingletonClass(Context context);
}
}
return instance;
}
private SingletonClass(Context context){
this.context = context; //傳入activity的context就會造成內存泄露咯
}
}
- 4.2靜態View:當一個view 被加入到界面中時,它就會持有 context 的強引用,也就是我們的 activity。如果我們通過一個static成員變量引用了這個 view,相當于直接引用了 activity,然后就泄漏了;
private static View view;
view = findViewById(R.id.sv_button);
- 4.3非靜態內部類:我們都知道,內部類能夠引用外部類的成員,這正是內部類的好處所在,但是恰恰是這個優勢會導致activity內存泄漏,因為非靜態內部類默認持有外部類的引用。如果我們創建了一個內部類的對象,并且通過靜態變量持有這個對象,就會導致內存泄漏;
private static InnerClass inner = new InnerClass();
class InnerClass {
}
- 4.4匿名內部類:匿名類同樣會持有定義它們的對象的引用,如果在 activity 內定義了一個匿名的 AsyncTask 對象,就有可能發生內存泄漏了。因為在activity被銷毀之后AsyncTask可能仍然在運行,這樣只能等到AsyncTask執行結束才能回收activity;
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
while(true);
}
}.execute();
- 4.5Handler+Runnable:定義一個匿名的 Runnable 對象并將其提交到 Handler 上也可能導致 activity 泄漏。Runnable對象引用了定義它的 activity 對象,而它會被提交到 Handler 的 MessageQueue 中,如果它在 activity 銷毀時還沒有被處理,那就會導致內存泄漏了。
new Handler() {
@Override
public void handleMessage(Message message) {
super.handleMessage(message);
}
}.postDelayed(new Runnable() {
@Override public void run() {
while(true);
}
}, 1000);
- 4.6Thread:原因類似4.5,盡管是在單獨的線程執行任務,但是線程還是會默認持有外部對象,任務沒有執行完成就不會釋放持有的引用;
new Thread() {
@Override public void run() {
while(true);
}
}.start();
- 4.7資源未關閉:如果使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等資源,應該在Activity銷毀時及時關閉或者注銷,否則這些資源將不會被回收,從而造成內存泄漏。
- 4.8集合容器:在我們做緩存的時候會用一些數據結構來存儲一些數據,當我們不需要它時要及時清理,不然就會像滾雪球一樣會越來越大,想不泄露都難。
可以了,造成內存泄露還有很多原因,這就靠慢慢跳坑了,生活太艱難。再話癆一下,“千丈之堤,以螻蟻之穴潰;百尺之室,以突隙之煙焚。”,所以我推薦套餐三Leakcanary,讓你的整個開發過程伴隨著內存泄露的監控。