Android內存泄漏分析實例

內存泄漏簡介

    Java可以保證當沒有引用指向對象的時候,對象會被垃圾回收器回收,與c語言自己申請的內存自己釋放相比,java程序員輕鬆了很多,但是並不代表java程序員不用擔心內存泄漏。當java程序發生內存泄漏的時候往往具有隱蔽性。


定義

內存泄漏用動態存儲分配函數動態開闢的空間,在使用完畢後未釋放,結果導致一直佔據該內存單元。直到程序結束。從程序員的角度來看“內存泄漏”,其實就是一個對象的生命週期超出了程序員所預期的長度,那麼這個對象就泄漏了。


Android開發中的內存泄漏

Android應用程序本身系統分配的內存很少,一旦發生泄漏,程序很快就會變得非常卡頓,直至OOM崩潰本文過兩個介紹內存泄漏分析工具MAT,以及內存分析的技巧。


需要工具

備內存泄漏的分析工具,可以安裝eclipse插件mat。如使AndroidStudio開發,可以將hprof文件導出再另外使用mat進行分析,不過會缺少某些功能,但是也夠用了,本文適應的場景是AndroidStudio+MAT


Android開發中常見的內存泄漏

•對象沒有反註冊
•數據庫cursor沒有關閉
•Bitmap沒有回收
•ListViewitem沒有複用
•Handler在Activity中定義爲非static的匿名內部類

人爲製造一個內存泄漏

自定義一個ActivityManager,提供兩個方法,分別用來註冊與反註冊Activity

package com.example.qinghua_liu.myapplication;

importandroid.app.Activity;
importandroid.util.Log;
importjava.util.ArrayList;
importjava.util.List;


publicclass ActivityManager{
   
private List<Activity>mActivities=newArrayList<>();
   
private static ActivityManagersInstance;

   
private ActivityManager(){
    };

   
public static ActivityManagerinstance() {
       
if (sInstance==null){
           
sInstance=newActivityManager();
       
}
       
return sInstance;
    }

   
public void registActivity(Activityactivity) {
       
mActivities.add(activity);
       
Log.d("TAG","mActivities.size()="+mActivities.size());
    }

   
public void unRigistActivity(Activityactivity) {
       
mActivities.remove(activity);
    }

}


MainActivityonCreateonDestroy中分別調用registActivityregistActivity方法進行註冊與反註冊。但Main2Activity記了反註冊

public classMainActivityextendsAppCompatActivity{

   Button mBtn;

@Override
protectedvoid onCreate(BundlesavedInstanceState){
   
super.onCreate(savedInstanceState);
   
setContentView(R.layout.activity_main);

mBtn = (Button)findViewById(R.id.mybutton);

mBtn("newActivity");
mBtnView.OnClickListener(){
    @Override
   
public void onClick(Viewv) {
        Intent it =
newIntent(MainActivity.this,
                Main2Activity.
class);
       
try {
           
startActivity(it);
        }
catch (Exceptione) {
           
Log.e(TAG,e.toString());
       
}
  
}
});
   
ActivityManager.instance().registActivity(this);

}

@Override
protectedvoid onDestroy(){
   
super.onDestroy();
   
ActivityManager.instance().unRigistActivity(this);
}

}


public classMain2ActivityextendsActivity{

private Object[] mObjs= new Object[10000];

@Override
protectedvoid onCreate(BundlesavedInstanceState){
   
super.onCreate(savedInstanceState);
   
setContentView(R.layout.activity_main);

//模擬快速消耗大量內存,使效果明

for (inti=0;i<mObjs.length;i++){
   
mObjs[i] =new Object();
}

ActivityManager.instance().registActivity(this);

}

@Override
protectedvoid onDestroy(){
   
super.onDestroy();
   
//ActivityManager.instance().unRigistActivity(this);// if forget unRigist
}

}


AndroidStudio Monitor Debug



Monitor界面可以大概現內存,CPUGPUNetWork的佔用消耗情況。

    詳細分析還要依靠MatAPP操作多次叫起和銷燬MainActivity以及Main2Activity

      導出Memory hprof 文件:

      AS Monitor Debug界面按DumpJava Heap,工具會自動生成一份hprof後綴文件。


hprof文件分析

Android Studio 生成的hprof文件 MAT還不能直接使用,需用轉換成標準的hprof件。

轉換可藉助AndroidSdkPlatform toolhprof具體命令下圖:




MAT分析工具

經轉換得到的hprof文件可以由MAT直接打開了。


MAT分析

Matdump一個內存快照出來,然後從分析報告中點擊“Leaksuspects”這裏會列出可能泄漏的對象,其中你會發現com.example.qinghua_liu.myapplication.Main2Activity的身影Main2Activity個類49實例CodeAuthor該一下子就會發現,原來Main2Activity漏了。發現它泄漏之後,如何找出是哪一個對象持有Main2Activity象的引用呢?


找出引用鏈

使OQL對象查詢語言查詢出泄漏的對象,和SQL非常相似,語法簡單易懂,卻非常強大。select* from com.example.qinghua_liu.myapplication.Main2Activity篩選出Main2Activity一類對象。


 後選擇“excludeweak/soft references”篩選出除了軟引用和弱引用之外(即強引用)的對象。


後找出的GCRoot,從下圖可以看出,原來Activity對象被ActivityManager裏面的ArrayListhold住了,所以接下來的工作就是Main2Activity檢查反註冊,內存泄漏的原因很快就可以清楚了。


鏈接

Eclipse Mat 工具下載地址

http://www.eclipse.org/mat/


Handler使用過程中可能引發的內存泄

我們UI編程時一般會使Handler進行UI更新,環境會有下面一段溫馨的提示ThisHandler class should be static or leaksmightoccur

    另外還有段詳細英文描述,大概意思就是:

   一Handler被聲明爲內部類,那麼可能導致它的外部類不能夠被垃圾回收。如果Handler是在其他線程(通常是workerthread)使用LooperMessageQueue(消息隊列),而不是main線程(UI線程),那麼就沒有這個問題。如果Handler使用LooperMessageQueue在主線程(mainthread),你需要對Handler的聲明做如下修改:
    聲Handlerstatic類;在外部類中實例化一個外部類的WeakReference(弱引用)並且在Handler初始化時傳入這個對象給你的Handler;將所有引用的外部類成員使用WeakReference對象。(與Bitmap工作線程弱引用相似?)


解決方案

private static class CopyFileHandlerextendsHandler{
   
WeakReference<MainActivity>mActivity;
   
public CopyFileHandler(MainActivityactivity) {
       
mActivity=newWeakReference<>(activity);
    }

   
public void handleMessage(Messagemsg){
       
final MainActivityactivity =mActivity.get();
       
if(null!=activity&&null!=activity.btn){
       
//handle you message here!
           
activity.btn.setText("clickme");
           
activity.btn.setClickable(true);
       
}
   
}
}

private CopyFileHandlermHandler;

@Override

protected void onCreate(BundlesavedInstanceState){

mHandler=newCopyFileHandler(this);

}

private void startCopyFileThread(){
   
Log.d(TAG,"startCopyDBThread");
   
new Thread(newRunnable() {
        @Override
       
public void run(){
           
//DOSOMETHING LIKE: copyDBFile();
           
Messagemsg=mHandler.obtainMessage();
           
mHandler.sendMessageDelayed(msg,1000);
       
}
    }).start();
}


爲什麼會內存泄漏

這與幾個關鍵詞有關:內部類、Handler的消息循環(Looper)、Java垃圾回收機制。需要強調一下,並不是每次使用Handler都會引發內存泄漏,這裏面有一定的機率,需要滿足特定條件纔會引起泄漏。
  內部類會有一個指向外部類的(硬)引用。垃圾回收機制中約定,當內存中的一個對象的引用計數爲0時,將會被回收。
  Handler作爲Android上的異步消息處理機制,它的工作是需要Looper和MessageQueue配合的。簡單的說,要維護一個循環體(Looper)處理消息隊列(MessageQueue)。每循環一次就從MessageQueue中取出一個Message,然後回調相應的消息處理函數。

  如果循環體中有消息未處理(Message排隊中),那麼Handler會一直存在,那麼Handler的外部類(通常是Activity)的引用計數一直不會是0,所以那個外部類就不能被垃圾回收。很多人會遇到activity的onDestroy方法一直不執行就是這個原因。


另一個解決方案的嘗試

提示描述中提到了也可以讓Handlerworkerthread中使用LooperMessageQueue


private class WorkThreadextendsThread{
        @Override
       
public void run(){
           
super.run();
           
Looper.prepare();
           
testHandler=newHandler(){
               
publicvoid handleMessage(Messagemsg){
                   
mybutton1.post(newRunnable(){
                       @Override
                       
publicvoid run(){
                           
mybutton1.setText("clickme");
                           
mybutton1.setClickable(true);
                        }
                    });
                }
            };
           
Looper.loop();
        }
    }


private WorkThreadmThread;

@Override
protectedvoid onCreate(BundlesavedInstanceState){

   mThread=newWorkThread();
    if(Thread.State.NEW==mThread.getState()){
       
mThread.start();
    }

}

@OnClick({R.id.hello,R.id.mybutton1,R.id.radioButton})
publicvoid onClick(Viewview) {
   
switch (view.getId()){
       
case R.id.mybutton1:
           
mybutton1.setText("clicked");
           
mybutton1.setClickable(false);
           
testHandler.sendEmptyMessageDelayed(1,3000);
           
break;
       
case R.id.radioButton:
           
break;
    }
}






發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章