Android 內(nèi)存泄露

我的博客:http://xuyushi.github.io
原文地址

[TOC]

內(nèi)存泄露

內(nèi)存泄露的定義:
當(dāng)某些對(duì)象不再被應(yīng)用程序所使用,但是由于仍然被引用而導(dǎo)致垃圾收集器不能釋放(Remove,移除)他們.

產(chǎn)生的原因:

內(nèi)存對(duì)象明明已經(jīng)不需要的時(shí)候,還仍然保留著這塊內(nèi)存和它的訪問(wèn)方式(引用)

長(zhǎng)生命周期的對(duì)象持有短生命周期對(duì)象的引用就很可能發(fā)生內(nèi)存泄露,盡管短生命周期對(duì)象已經(jīng)不再需要,但是因?yàn)殚L(zhǎng)生命周期對(duì)象持有它的引用而導(dǎo)致不能被回收,這就是java中內(nèi)存泄露的發(fā)生場(chǎng)景。

常見(jiàn)的內(nèi)存泄漏

非靜態(tài)內(nèi)部類(lèi)的靜態(tài)實(shí)例容易造成內(nèi)存泄漏

public class MainActivityextends Activity {  
         static Demo sInstance = null;  
          
    @Override  
    public void onCreate(BundlesavedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  
        if (sInstance == null) {  
           sInstance= new Demo();  
        }  
    }  
    class Demo{  
    voiddoSomething()  {  
               System.out.print("dosth.");  
    }  
    }  
} 

上面的代碼中的sInstance實(shí)例類(lèi)型為靜態(tài)實(shí)例,在第一個(gè)MainActivity act1實(shí)例創(chuàng)建時(shí),sInstance會(huì)獲得并一直持有act1的引用。當(dāng)MainAcitivity銷(xiāo)毀后重建,因?yàn)閟Instance持有act1的引用,所以act1是無(wú)法被GC回收的,進(jìn)程中會(huì)存在2個(gè)MainActivity實(shí)例(act1和重建后的MainActivity實(shí)例),這個(gè)act1對(duì)象就是一個(gè)無(wú)用的但一直占用內(nèi)存的對(duì)象,即無(wú)法回收的垃圾對(duì)象。所以,對(duì)于lauchMode不是singleInstance的Activity, 應(yīng)該避免在activity里面實(shí)例化其非靜態(tài)內(nèi)部類(lèi)的靜態(tài)實(shí)例。

activity使用靜態(tài)成員

private static Drawable sBackground;    
@Override    
protected void onCreate(Bundle state) {    
    super.onCreate(state);    
    
    TextView label = new TextView(this);    
    label.setText("Leaks are bad");    
    
    if (sBackground == null) {    
        sBackground = getDrawable(R.drawable.large_bitmap);    
    }    
    label.setBackgroundDrawable(sBackground);    
    
    setContentView(label);    
}  

由于用靜態(tài)成員sBackground 緩存了drawable對(duì)象,所以activity加載速度會(huì)加快,但是這樣做是錯(cuò)誤的。因?yàn)樵赼ndroid 2.3系統(tǒng)上,它會(huì)導(dǎo)致activity銷(xiāo)毀后無(wú)法被系統(tǒng)回收。

label .setBackgroundDrawable函數(shù)調(diào)用會(huì)將label賦值給sBackground的成員變量mCallback。

上面代碼意味著:sBackground(GC Root)會(huì)持有TextView對(duì)象,而TextView持有Activity對(duì)象。所以導(dǎo)致Activity對(duì)象無(wú)法被系統(tǒng)回收。

下面看看android4.0為了避免上述問(wèn)題所做的改進(jìn)。

先看看android 2.3的Drawable.Java對(duì)setCallback的實(shí)現(xiàn):

public final void setCallback(Callback cb){

    mCallback = cb;

}

再看看android 4.0的Drawable.Java對(duì)setCallback的實(shí)現(xiàn):

public final void setCallback(Callback cb){

    mCallback = newWeakReference<Callback> (cb);

}

在android 2.3中要避免內(nèi)存泄漏也是可以做到的, 在activity的onDestroy時(shí)調(diào)用

sBackgroundDrawable.setCallback(null)。

以上2個(gè)例子的內(nèi)存泄漏都是因?yàn)锳ctivity的引用的生命周期超越了activity對(duì)象的生命周期。也就是常說(shuō)的Context泄漏,因?yàn)閍ctivity就是context。

想要避免context相關(guān)的內(nèi)存泄漏,需要注意以下幾點(diǎn):

  • 不要對(duì)activity的context長(zhǎng)期引用(一個(gè)activity的引用的生存周期應(yīng)該和activity的生命周期相同)
  • 如果可以的話,盡量使用關(guān)于application的context來(lái)替代和activity相關(guān)的context
  • 如果一個(gè)acitivity的非靜態(tài)內(nèi)部類(lèi)的生命周期不受控制,那么避免使用它;正確的方法是使用一個(gè)靜態(tài)的內(nèi)部類(lèi),并且對(duì)它的外部類(lèi)有一WeakReference,就像在ViewRootImpl中內(nèi)部類(lèi)W所做的那樣。

使用handler時(shí)的內(nèi)存問(wèn)題

我們知道,Handler通過(guò)發(fā)送Message與其他線程交互,Message發(fā)出之后是存儲(chǔ)在目標(biāo)線程的MessageQueue中的,而有時(shí)候Message也不是馬上就被處理的,可能會(huì)駐留比較久的時(shí)間。在Message類(lèi)中存在一個(gè)成員變量 target,它強(qiáng)引用了handler實(shí)例,如果Message在Queue中一直存在,就會(huì)導(dǎo)致handler實(shí)例無(wú)法被回收,如果handler對(duì)應(yīng)的類(lèi)是非靜態(tài)內(nèi)部類(lèi) ,則會(huì)導(dǎo)致外部類(lèi)實(shí)例(Activity或者Service)不會(huì)被回收,這就造成了外部類(lèi)實(shí)例的泄露。 所以正確處理Handler等之類(lèi)的內(nèi)部類(lèi),應(yīng)該將自己的Handler定義為靜態(tài)內(nèi)部類(lèi),并且在類(lèi)中增加一個(gè)成員變量,用來(lái)弱引用外部類(lèi)實(shí)例,如下:

public class OutterClass  
{  
        ......  
        ......  
        static class InnerClass  
        {  
            private final WeakReference<OutterClass> mOutterClassInstance;  
            ......  
            ......  
        }  
}  

Android lint 會(huì)產(chǎn)生一個(gè)警告

This Handler class should be static or leaks might occur (com.example.ta.MainActivity.1)
Issue: Ensures that Handler classes do not hold on to a reference to an outer class
Id: HandlerLeak
In Android, Handler classes should be static or leaks might occur. Messages enqueued on the application thread’s MessageQueue also retain their target Handler. If the Handler is an inner class, its outer class will be retained as well. To avoid leaking the outer class, declare the Handler as a static nested class with a WeakReference to its outer class.

原因是:
當(dāng)Android應(yīng)用啟動(dòng)的時(shí)候,會(huì)先創(chuàng)建一個(gè)應(yīng)用主線程的Looper對(duì)象,Looper實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的消息隊(duì)列,一個(gè)一個(gè)的處理里面的Message對(duì)象。主線程Looper對(duì)象在整個(gè)應(yīng)用生命周期中存在。
當(dāng)在主線程中初始化Handler時(shí),該Handler和Looper的消息隊(duì)列關(guān)聯(lián)。發(fā)送到消息隊(duì)列的Message會(huì)引用發(fā)送該消息的Handler對(duì)象,這樣系統(tǒng)可以調(diào)用 Handler#handleMessage(Message) 來(lái)分發(fā)處理該消息。
在Java中,非靜態(tài)(匿名)內(nèi)部類(lèi)會(huì)引用外部類(lèi)對(duì)象。而靜態(tài)內(nèi)部類(lèi)不會(huì)引用外部類(lèi)對(duì)象。
如果外部類(lèi)是Activity,則會(huì)引起Activity泄露 。
當(dāng)Activity finish后,延時(shí)消息會(huì)繼續(xù)存在主線程消息隊(duì)列中1分鐘,然后處理消息。而該消息引用了Activity的Handler對(duì)象,然后這個(gè)Handler又引用了這個(gè)Activity。這些引用對(duì)象會(huì)保持到該消息被處理完,這樣就導(dǎo)致該Activity對(duì)象無(wú)法被回收,從而導(dǎo)致了上面說(shuō)的 Activity泄露。
要修改該問(wèn)題,只需要按照Lint提示的那樣,把Handler類(lèi)定義為靜態(tài)即可,然后通過(guò)WeakReference 來(lái)保持外部的Activity對(duì)象。

注冊(cè)某個(gè)對(duì)象后未反注冊(cè)

集合中對(duì)象沒(méi)清理造成的內(nèi)存泄露

資源對(duì)象沒(méi)關(guān)閉造成的內(nèi)存泄露

比如 cursor 、file

GC 機(jī)制

如上圖所示,GC會(huì)選擇一些它了解還存活的對(duì)象作為內(nèi)存遍歷的根節(jié)點(diǎn)(GC Roots),比方說(shuō)thread stack中的變量,JNI中的全局變量,zygote中的對(duì)象(class loader加載)等,然后開(kāi)始對(duì)heap進(jìn)行遍歷。到最后,部分沒(méi)有直接或者間接引用到GC Roots的就是需要回收的垃圾,會(huì)被GC回收掉。如下圖藍(lán)色部分

GC
GC
  • Shallow heap表示對(duì)象本身所占內(nèi)存大小,一個(gè)內(nèi)存大小100bytes的對(duì)象Shallow heap就是100bytes。
  • Retained heap表示通過(guò)回收這一個(gè)對(duì)象總共能回收的內(nèi)存,比方說(shuō)一個(gè)100bytes的對(duì)象還直接或者間接地持有了另外3個(gè)100bytes的對(duì)象引用,回收這個(gè)對(duì)象的時(shí)候如果另外3個(gè)對(duì)象沒(méi)有其他引用也能被回收掉的時(shí)候,Retained heap就是400bytes。

內(nèi)存管理分析

  1. 打開(kāi) DDMS ,選中需要分析的進(jìn)程,然后點(diǎn)擊update heap
  2. 在app 中操作后,點(diǎn)擊 Cause GC,觀察heap 的使用量


  3. 點(diǎn)擊

    導(dǎo)出 HPROF 文件

  4. 轉(zhuǎn)換 hprof 的格式,從而支持使用 MAT 工具打開(kāi)分析
  5. hprof-conv <source_file> <dest_file>
  6. 使用 MAT 打開(kāi)生成文件
  7. 點(diǎn)擊

    查看詳情

MAT 使用

  • 可以在上面過(guò)濾自己需要的類(lèi)名,支持正則表達(dá)式
  • 在某一項(xiàng)上右鍵打開(kāi)菜單選擇 list objects ->with incoming refs 將列出該類(lèi)的實(shí)例:
  • 快速找出某個(gè)實(shí)例沒(méi)被釋放的原因,可以右健 Path to GC Roots-->exclue all phantom/weak/soft etc
  • Shallow heap表示對(duì)象本身所占內(nèi)存大小,一個(gè)內(nèi)存大小100bytes的對(duì)象Shallow heap就是100bytes。
  • Retained heap表示通過(guò)回收這一個(gè)對(duì)象總共能回收的內(nèi)存,比方說(shuō)一個(gè)100bytes的對(duì)象還直接或者間接地持有了另外3個(gè)100bytes的對(duì)象引用,回收這個(gè)對(duì)象的時(shí)候如果另外3個(gè)對(duì)象沒(méi)有其他引用也能被回收掉的時(shí)候,Retained heap就是400bytes。

分析實(shí)例

實(shí)例1

Mat中導(dǎo)入數(shù)據(jù)后,過(guò)濾包名,按照 obj 數(shù)量排序



發(fā)現(xiàn) AsyncTaskService 有9個(gè)實(shí)例,不正常,右鍵 Path to GC Roots 查看



發(fā)現(xiàn)是 volley 中的 NetworkDispatcher 持有了 context 導(dǎo)致釋放不了

NetworkDispatcher 持有(用到了) UserStatsPostRequest 持有 mLIstener、mErrorListener 持有 context

AsyncTaskServicePoiInfoApi.refreshPoiInfo(AsyncTaskService.this, null);
傳入的 contextAsyncTaskService,導(dǎo)致 PoiInfoAsyncTaskService的生命周期相關(guān)聯(lián)
當(dāng) service生命周期結(jié)束時(shí),mLIstener 仍然持有 Service ,導(dǎo)致 GC無(wú)法回收,當(dāng)下次再次請(qǐng)求 Poiapi時(shí),會(huì)再次用到一個(gè)新的AsyncTaskService

解決方法

傳入的 context 不使用 activityService這樣和 Android 生命周期相關(guān)的,使用 appcation 中的 sContext

實(shí)例2

  1. 點(diǎn)擊

    ,按照堆排序


此時(shí)是按照占用的大小排序的,首先Retained Heap表示這個(gè)對(duì)象以及它所持有的其它引用(包括直接和間接)所占的總內(nèi)存,因此從上圖中看,前兩行的Retained Heap是最大的,我們分析內(nèi)存泄漏時(shí),內(nèi)存最大的對(duì)象也是最應(yīng)該去懷疑的。

在每一行的最左邊都有一個(gè)文件型的圖標(biāo),這些圖標(biāo)有的左下角帶有一個(gè)紅色的點(diǎn),有的則沒(méi)有。帶有紅點(diǎn)的對(duì)象就表示是可以被GC Roots訪問(wèn)到的,根據(jù)上面的講解,可以被GC Root訪問(wèn)到的對(duì)象都是無(wú)法被回收的

第一行為資源文件,比較大很正常,第二行為 bitmap,點(diǎn)擊 -> Path to GC Roots -> exclude weak references,為什么選擇exclude weak references呢?因?yàn)槿跻檬遣粫?huì)阻止對(duì)象被垃圾回收器回收的,所以我們這里直接把它排除掉


黑體表示變量名,右側(cè)正常體表示類(lèi)名, 由上到下表示含有關(guān)系

可以看到 bitmap 之所以釋放不掉是因?yàn)?linearLayout 無(wú)法釋放,而linearLayout之所以釋放不掉是因?yàn)?linearLayout 無(wú)法釋放,是因?yàn)?LoginActivity 無(wú)法釋放,....最終發(fā)現(xiàn)是 SPDataManager中 的 sContext 持有了 Activity,導(dǎo)致 Activity 無(wú)法被 GC回收

解決方法

在構(gòu)造 SPDataManger 時(shí),不使用 Activity 的 context ,而使用 getapplicationContext,避免與 Actvity 的生命周期關(guān)聯(lián)

參考

http://blog.csdn.net/gemmem/article/details/13017999
http://blog.csdn.net/guolin_blog/article/details/4223863

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容