內(nèi)存泄露分析與解決

Android應(yīng)用程序中,經(jīng)常會(huì)出現(xiàn)內(nèi)存泄露的問題,可能一兩次的啟動(dòng)沒有對(duì)應(yīng)用產(chǎn)生什么崩潰呀之類的問題,但是長(zhǎng)時(shí)間使用或者頻繁退出進(jìn)入使用,因?yàn)閮?nèi)存泄露,應(yīng)用超過了系統(tǒng)允許的內(nèi)存范圍就會(huì)出現(xiàn)ANR,卡頓,OOM等問題。有經(jīng)驗(yàn)和注重性能的程序員們都會(huì)在編程過程中,注意代碼的編寫,避免內(nèi)存泄漏。這邊文章也是我自己邊學(xué)習(xí)邊總結(jié)邊應(yīng)用??偨Y(jié)下來,給自己解疑,好記性不如爛筆頭,用了之后再總結(jié)一下,真的更有助于對(duì)知識(shí)的吸收,更希望能夠幫助跟我一樣還在學(xué)習(xí)路上的人解疑,有什么不對(duì)的地方,多多提出來哈!開篇啦!

為什么會(huì)有內(nèi)存泄露?

Android程序中,對(duì)象與對(duì)象之間存在引用的關(guān)系,當(dāng)一個(gè)對(duì)象因?yàn)楸黄渌麑?duì)象引用,在用完之后無法GC回收一直存在內(nèi)存中的時(shí)候就會(huì)出現(xiàn)內(nèi)存泄漏,因?yàn)閷?duì)象無法被回收,所以一直存在內(nèi)存里面,隨著使用時(shí)間的增長(zhǎng),同一個(gè)對(duì)象在內(nèi)存中存在很多對(duì)象,占用了應(yīng)用的內(nèi)存越來越大。

所以內(nèi)存泄露的原因就是本該回收的對(duì)象沒有被GC回收,一直存留在內(nèi)存中。
從這點(diǎn)我們也可以知道我們的要解決內(nèi)存泄露的方法就是讓應(yīng)該回收的對(duì)象可以正常的被回收。

在這里我們簡(jiǎn)單的提一下那些對(duì)象需要由GC來回收。方法里面的定義的局部變量存儲(chǔ)在棧中,變量使用完之后就會(huì)自動(dòng)釋放回收變量。而new出來的對(duì)象都存放在堆里面,有Java回收器回收。

對(duì)象引用

在一起學(xué)習(xí)什么時(shí)候?qū)ο笾g會(huì)存在引用關(guān)系之前,首先來將一下Android中四種引用的弱引用,強(qiáng)引用,軟引用,虛引用。

  • 弱引用
    關(guān)鍵詞WeakReference.定義。弱引用當(dāng)只有弱引用在引用時(shí),GC會(huì)回收該弱引用。

  • 強(qiáng)引用
    直接new的對(duì)象都是強(qiáng)引用,強(qiáng)引用,只有沒有對(duì)象在引用他的時(shí)候,才會(huì)釋放掉。

  • 軟引用
    關(guān)鍵詞SoftReference定義。軟引用的比較少,如果一個(gè)對(duì)象只具有軟引用,那么在內(nèi)存空間足夠時(shí),垃圾回收器就不會(huì)回收它;如果內(nèi)存空間不足了,就會(huì)回收這些對(duì)象的內(nèi)存。只要垃圾回收器沒有回收它,該對(duì)象就可以被使用。

  • 虛引用
    PhantomReference引用。

對(duì)象的引用這個(gè)概念之前也知道一些,但是經(jīng)過這次對(duì)內(nèi)存泄露的學(xué)習(xí)之后,對(duì)于自己以前不知道的引用的情況有了新的理解。下面我們來一起學(xué)習(xí)下引用吧。對(duì)了,這是基礎(chǔ)知識(shí)的講解。對(duì)于引用有深刻理解的同仁們可以直接跳下一節(jié)。
首先我們首先來看看最簡(jiǎn)單的引用的情況:

   假設(shè)有兩個(gè)類,類A和類B。
① class A {
     private B b;
     public A() {
         b = new B();
     }   
  }
  
  class B{
      
  }
  如上述代碼所示, 在A里面創(chuàng)建了一個(gè)B的實(shí)例,并且賦給了b,這樣A就引用b。因?yàn)閎是A的成員變量所以當(dāng)A釋放的時(shí)候,b也會(huì)被釋放。  
② 當(dāng)我們?cè)贏中分別定義一個(gè)內(nèi)部靜態(tài)類和一個(gè)內(nèi)部非靜態(tài)類,以及添加一個(gè)外部接口。RefenrenceListener如下:  
   interface RefenrenceListener {
      void onClick();
   }
   
   
   
   類A修改成這個(gè)樣子:
   
   class A{
   
       private C c ; 
       private D d;
       // 非靜態(tài)匿名內(nèi)部類會(huì)引用外部類
       class C {
           
       }   
         
    //靜態(tài)匿名內(nèi)部類不會(huì)引用外部類
       static class D{
           
       }
       public A(){
          c = new C();
          //拿有外部類的引用
          d = new D(); //不引用外部類
           setRefenrenceListener(new RefenrenceListenr(){
              void onClick(){
                  Log.i("doris", "onclick");
              }   );
           }  //匿名內(nèi)部類RefenrenceListener會(huì)有外部類的引用
       }
       
       
       /* 靜態(tài)的匿名內(nèi)部類成員變量不引用外部類 */
    private static RefenrenceListenr mRefenrenceListenr = new RefenrenceListenr() {
        @Override
        public void onclick() {
        
        }
    };
       
   }
   
   /***關(guān)于引用關(guān)系可以用個(gè)方法獲取到某個(gè)對(duì)象的引用情況   
   用法: getAllFieldName(c.getClass())***/
   private void getAllFieldName(Class<?> clazz) {
        System.out.println("className: ======> " + clazz.getSimpleName());
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            System.out.println(field.getName());
        }
    }


   
  

從上面總結(jié)可以得到:

  1. 靜態(tài)匿名內(nèi)部類成員變量不引用外部類。其他的非靜態(tài)匿名類成員變量,以及匿名內(nèi)部類局部變量都會(huì)引用外部類。
  2. 非靜態(tài)內(nèi)部類一定引用外部類。
  3. 靜態(tài)內(nèi)部類不引用外部類
  4. 方法內(nèi)的局部?jī)?nèi)部類和匿名內(nèi)部類都會(huì)引用外部類。
  5. 當(dāng)某個(gè)對(duì)象是某個(gè)類的成員變量的時(shí)候那么就一定引用了這個(gè)對(duì)象。

從上面我們知道了,對(duì)象之間引用關(guān)系的形成,但是并不是所有的應(yīng)用都會(huì)引起內(nèi)存泄露。主要看,引用對(duì)象與被引用對(duì)象那個(gè)生命周期比較長(zhǎng),如果引用對(duì)象生命周期長(zhǎng)于本來應(yīng)該釋放的對(duì)象的話,那就會(huì)引起內(nèi)存泄露。注意這句話,不管是那種引用其實(shí)都不是百分之百的會(huì)引起內(nèi)存泄露,只有當(dāng)引用對(duì)象生命周期夠長(zhǎng)的時(shí)候才會(huì)引起內(nèi)存泄露。 曾經(jīng)以為只要有非靜態(tài)內(nèi)部類就一定會(huì)引起內(nèi)存泄露、而且當(dāng)我知道回調(diào)也引用外部類的時(shí)候,我開始想那這內(nèi)存泄露是避免不了了,總不能不用回調(diào)吧。是不是很傻。還好學(xué)習(xí)更多,讓我知道了更多。所以記住哦?。。?!

常見內(nèi)存泄露的原因

  • 非靜態(tài)匿名內(nèi)部類和非靜態(tài)內(nèi)部類會(huì)引用外部類。
  • 回調(diào)有可能會(huì)引起內(nèi)存泄露,如果回調(diào)對(duì)象被靜態(tài)對(duì)象引用或者其他原因引用而無法釋放,就會(huì)導(dǎo)致內(nèi)存泄露。
  • Dialog有可能引發(fā)泄露
  • 非靜態(tài)Handler引用外部類引起內(nèi)存泄露
  • 線程,動(dòng)畫等無限循環(huán)執(zhí)行,引用了需要釋放的對(duì)象,也會(huì)引起內(nèi)存泄露
  • 靜態(tài)成員集合類和靜態(tài)View對(duì)象 以及靜態(tài)的非靜態(tài)成員變量
  • 單例類
  • 資源未關(guān)閉導(dǎo)致的泄露。BroadcastReceiver未解除注冊(cè)

當(dāng)然要記住 不是說有強(qiáng)引用就一定會(huì)引起內(nèi)存泄露,關(guān)鍵看可能泄露的對(duì)象和引用他的對(duì)象那個(gè)生命力更能頑強(qiáng)。所以我們要做的就是保證不該被強(qiáng)引用的不被強(qiáng)引用, 被強(qiáng)引用了也不會(huì)釋放。

經(jīng)典案例

下面我將舉例一些常見的內(nèi)存泄露的例子,并給出相應(yīng)的解決方法。

  • 最最經(jīng)典的Handler內(nèi)存泄露
    在程序中,我們通常會(huì)在一個(gè)類里面定義個(gè)內(nèi)部類Handler用來對(duì)消息的處理,從而達(dá)到異步處理。如果我們定義一個(gè)非靜態(tài)匿名內(nèi)部類的話,那么Handler就會(huì)持有外部類的引用,假設(shè)外部類是Activity。那么就會(huì)導(dǎo)致當(dāng)Activity退出之后,Handler如果還持有消息的話,就會(huì)無法釋放Activity。所以我們應(yīng)該定義一個(gè)靜態(tài)的內(nèi)部Handler類。但是我們要使用外部類的方法以及變量那該怎么辦呢,那也好辦這個(gè)時(shí)候就要用到弱引用了。弱引用就是當(dāng)對(duì)象只被弱引用引用的時(shí)候,會(huì)被GC回收。
    代碼如下:
    static  class MyHandler extends WeakHandler<MainActivity> {
        public MyHandler(MainActivity owner) {
            super(owner);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (getOwner() == null) {
                return;
            }
            if (msg.what == MSG_SHOW_REPLAYEPG_WINDOW) {
                if (getOwner().mInfoBar != null) {
                    getOwner().mInfoBar.dismisss();
                }
                if (getOwner().mLookBackView == null) {
                    getOwner().mLookBackView = new LookBackView(getOwner());
                    getOwner().mLookBackView.setOnScheduleListItemListener(getOwner().mOnListItemClickListener);
                }
                getOwner().mLookBackView.showLookBackWindow();
            }
        }
    }

其實(shí)追其根本,非靜態(tài)的Handler之所以會(huì)導(dǎo)致內(nèi)存泄漏就是因?yàn)椋庆o態(tài)的內(nèi)部類會(huì)引用外部類,當(dāng)外部類銷毀,而Handler仍然有消息要處理,未銷毀的時(shí)候,外部類就無法銷毀而引發(fā)了Activity的內(nèi)存泄漏。所以觸類旁通,所有的非靜態(tài)的內(nèi)部類如果引用了Activity,都有可能引起內(nèi)存泄漏,這個(gè)時(shí)候我們就應(yīng)該像Handler這樣處理,改成靜態(tài)類并且使用弱引用引用。如果可以當(dāng)然能不引用,外部類就不引用。

  • Thread
    在線程中可能發(fā)引發(fā)內(nèi)存泄漏的原因有,應(yīng)用退出之后,線程沒有停止,并且引用了資源,導(dǎo)致資源無法釋放,從而引起內(nèi)存泄漏,所以要注意線程的停止以及資源的釋放。 這里就不列舉代碼了。
  • 單例
    單例中比較容易引發(fā)內(nèi)存泄漏的情況是靜態(tài)變量引用了本需要被釋放掉的資源,比如Activity,導(dǎo)致Activity無法釋放。下面舉例說明:
public class ServerManager {

    public static ServerManager mServerManager;
    private Context mContext;
    private  ServerManager(Context context) {
        mContext = context;
    }

    public static ServerManager getInstance(Context context) {
        if (mServerManager == null) {
            mServerManager = new ServerManager(context);
        }
        return mServerManager;
    }

}

外部調(diào)用: 
public class MainActivity extends AppCompatActivity {

    private Button mBtnNotification;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ServerManager serverManager =   ServerManager.getInstance(this);
    }

}

上面的context引用有問題,問題在哪里呢,單例類ServerManager的實(shí)例是一個(gè)靜態(tài)變量,然后引用了context。context是一個(gè)指向MainActivity類的對(duì)象,那么

  • 回調(diào)被別的靜態(tài)對(duì)象引用。當(dāng)MainActivity銷毀的時(shí)候就出現(xiàn)問題了,由于mServerManager是一個(gè)靜態(tài)變量所以一直存在于內(nèi)存中,并持有mainactivity的引用,所以MainActvity無法釋放。這是一個(gè)典型的以及我們很容易出錯(cuò)的地方,但是也是很容易修改的地方,那就是將MainActivity中的this改成getApplicationContext(),從而ServerManager就不會(huì)影響Activity的釋放。
    這里也引申除了一個(gè)新的點(diǎn),那就是能用ApplicationContext的地方,千萬不要用Activity的context,這樣就可以避免不必要的內(nèi)存泄漏。

  • 靜態(tài)成員變量引用類引發(fā)的內(nèi)存泄漏
    靜態(tài)變量如果引用了資源,也會(huì)導(dǎo)致資源無法釋放。

總結(jié)

其實(shí)內(nèi)存泄漏的本質(zhì),就是要釋放的對(duì)象由于被GC-ROOt引用而導(dǎo)致無法釋放,這就有可能引起內(nèi)存泄漏,之所以說有可能還是因?yàn)椋惨匆尫诺膶?duì)象和引用他的對(duì)象那個(gè)對(duì)象生命長(zhǎng)。如果引用他的GC-root對(duì)象的生命長(zhǎng)于要釋放的對(duì)象,那么就會(huì)引發(fā)內(nèi)存泄漏。 而解決內(nèi)存泄漏的方法的本質(zhì)就是1.避免造成可能引發(fā)內(nèi)存泄漏的情況。2.當(dāng)不可避免的要引用的時(shí)候,記得在使用完之后及時(shí)的釋放資源,切換引用鏈中導(dǎo)致內(nèi)存泄漏的關(guān)鍵點(diǎn) 3.圖片資源等用完之后記得及時(shí)釋放。

這是我自己學(xué)習(xí)查閱了相關(guān)的內(nèi)存泄漏以及實(shí)踐之后的一些總結(jié),如果有哪里寫的不對(duì)的話,歡迎指出,以防他繼續(xù)遺丑萬年。哈哈。

最后貼出我覺得寫的很不錯(cuò)的文章,雖然人家已經(jīng)寫的很不錯(cuò)了,但是自己總結(jié)一番對(duì)自己吸收這個(gè)知識(shí)是很有幫助的。所以總結(jié)的博客 ,不只是為了給更多的人看,其實(shí)也是個(gè)人知識(shí)體系構(gòu)建的過程。你也可以一起來建立,貴在堅(jiān)持,簡(jiǎn)單的一句加油,共勉!

參考鏈接:
Android內(nèi)存泄露案例分析

徹底搞懂Java內(nèi)存泄露

AlertDialog引起內(nèi)存泄漏

一個(gè)內(nèi)存泄漏引發(fā)的血案

Android 內(nèi)存泄漏全解

?著作權(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)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • 內(nèi)存管理的目的就是讓我們?cè)陂_發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問題。內(nèi)存泄漏大家都不陌生了,簡(jiǎn)單粗俗的講,...
    宇宙只有巴掌大閱讀 2,483評(píng)論 0 12
  • Android 內(nèi)存泄漏總結(jié) 內(nèi)存管理的目的就是讓我們?cè)陂_發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問題。內(nèi)存泄漏...
    _痞子閱讀 1,695評(píng)論 0 8
  • Android 內(nèi)存泄漏總結(jié) 內(nèi)存管理的目的就是讓我們?cè)陂_發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問題。內(nèi)存泄漏...
    apkcore閱讀 1,303評(píng)論 2 7
  • 內(nèi)存管理的目的就是讓我們?cè)陂_發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問題。內(nèi)存泄漏大家都不陌生了,簡(jiǎn)單粗俗的講,...
    DreamFish閱讀 863評(píng)論 0 5
  • 懶人自有懶惰的道理, 合并雙手,頭皮癢 可以不抓, 任由紛亂稀疏的胡渣, 頭發(fā), 似乎絕望, 心像迷茫的荒場(chǎng), 草...
    卓涵閱讀 264評(píng)論 0 1

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