android內(nèi)存泄露

參考
內(nèi)存泄露從入門到精通三部曲之基礎(chǔ)知識(shí)篇
Android 內(nèi)存泄漏總結(jié)
Android內(nèi)存泄漏研究
Android內(nèi)存優(yōu)化之——static使用篇
避免Android中Context引起的內(nèi)存泄露
Android 內(nèi)存泄漏案例和解析 附RXJAVA內(nèi)存泄露

一、方法區(qū) 棧區(qū) 堆區(qū)

參考
為什么要有堆區(qū)和棧區(qū)呢
Java里的堆(heap)棧(stack)和方法區(qū)(method)
結(jié)構(gòu)化語言里函數(shù)(子程序)調(diào)用最方便的實(shí)現(xiàn)方式就是用棧,以至于現(xiàn)在絕大部分芯片都對(duì)棧提供芯片級(jí)的硬件支持,一條指令即可搞定棧的pop操作。棧的好處是:方便、快、有效避免內(nèi)存碎片化。棧的問題是:不利于管理大內(nèi)存(尤其在16位和32位時(shí)代)、數(shù)據(jù)的生命周期難于控制(棧內(nèi)的有效數(shù)據(jù)通常是連續(xù)存儲(chǔ)的,所以pop時(shí)后申請(qǐng)的內(nèi)存必須早于先申請(qǐng)的內(nèi)存失效),所以棧不利于動(dòng)態(tài)地管理并且有效地利用寶貴的內(nèi)存資源。于是我們有了堆。。。
按照編譯原理的觀點(diǎn),程序運(yùn)行時(shí)的內(nèi)存分配有三種策略,分別是靜態(tài)的,棧式的,和堆式的,對(duì)應(yīng)的,三種存儲(chǔ)策略使用的內(nèi)存空間主要分別是靜態(tài)存儲(chǔ)區(qū)(也稱方法區(qū))、堆區(qū)和棧區(qū)。他們的功能不同,對(duì)他們使用方式也就不同。

靜態(tài)存儲(chǔ)區(qū)(方法區(qū)):
內(nèi)存在程序編譯的時(shí)候就已經(jīng)分配好,這塊內(nèi)存在程序整個(gè)運(yùn)行期間都存在。它主要存放靜態(tài)數(shù)據(jù)、全局static數(shù)據(jù)和常量。

棧區(qū):
棧內(nèi)存分配運(yùn)算內(nèi)置于處理器的指令集中,效率很高,但是分配的內(nèi)存容量有限。在函數(shù)中(說明是局部變量)定義的一些基本類型的變量和對(duì)象的引用變量都是在函數(shù)的棧內(nèi)存中分配。當(dāng)在一段代碼塊中定義一個(gè)變量時(shí),java就在棧中為這個(gè)變量分配內(nèi)存空間,當(dāng)超過變量的作用域后,java會(huì)自動(dòng)釋放掉為該變量分配的內(nèi)存空間,該內(nèi)存空間可以立刻被另作他用。

堆區(qū):
亦稱動(dòng)態(tài)內(nèi)存分配。程序在運(yùn)行的時(shí)候用malloc或new申請(qǐng)任意大小的內(nèi)存,程序員自己負(fù)責(zé)在適當(dāng)?shù)臅r(shí)候用free或delete釋放內(nèi)存(Java則依賴?yán)厥掌鳎6褍?nèi)存用于存放所有由new創(chuàng)建的對(duì)象(內(nèi)容包括該對(duì)象其中的所有成員變量)和數(shù)組。動(dòng)態(tài)內(nèi)存的生存期可以由我們決定,如果我們不釋放內(nèi)存,程序?qū)⒃谧詈蟛裴尫诺魟?dòng)態(tài)內(nèi)存。 但是,良好的編程習(xí)慣是:如果某動(dòng)態(tài)內(nèi)存不再使用,需要將其釋放掉。

堆是不連續(xù)的內(nèi)存區(qū)域(因?yàn)橄到y(tǒng)是用鏈表來存儲(chǔ)空閑內(nèi)存地址,自然不是連續(xù)的),堆大小受限于計(jì)算機(jī)系統(tǒng)中有效的虛擬內(nèi)存(32bit系統(tǒng)理論上是4G),所以堆的空間比較靈活,比較大。棧是一塊連續(xù)的內(nèi)存區(qū)域,大小是操作系統(tǒng)預(yù)定好的,windows下棧大小是2M(也有是1M,在編譯時(shí)確定,VC中可設(shè)置)。

對(duì)于堆,頻繁的new/delete會(huì)造成大量內(nèi)存碎片,使程序效率降低。對(duì)于棧,它是先進(jìn)后出的隊(duì)列,進(jìn)出一一對(duì)應(yīng),不產(chǎn)生碎片,運(yùn)行效率穩(wěn)定高。

至此,我們來看看Java中需要被回收的垃圾:

{
   Person p1 = new Person();
   ……
}

引用句柄p1的作用域是從定義到“}”處,執(zhí)行完這對(duì)大括號(hào)中的所有代碼后,產(chǎn)生的Person對(duì)象就會(huì)變成垃圾,因?yàn)橐眠@個(gè)對(duì)象的句柄p1已超過其作用域,p1失效,在棧中被銷毀,因此堆上的Person對(duì)象不再被任何句柄引用了。 因此person變?yōu)槔瑫?huì)被回收。

二、常見例子

1.以下Activity無法正常銷毀,因?yàn)殪o態(tài)變量sContext引用了它。靜態(tài)變量的生命周期,起始于類的加載,終止于類的釋放。

public class MainActivity extends Activity{
   private static final String TAG = "MainActivity";
   
   private static Context sContext;
   
   protected void onCreate(Bundle savedInstanceState){
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);
      sContext = this;
   }
}
public class MainActivity extends Activity{
   private static final String TAG = "MainActivity";
   
   private static View sView;
   
   protected void onCreate(Bundle savedInstanceState){
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);
      sContext = new View(this);
   }
}

解決辦法就是在這個(gè) Activity 的 onDestroy 時(shí)將 sContext 的值置空,或者避免使用靜態(tài)變量這樣的寫法。
2.非靜態(tài)內(nèi)部類

public class MainActivity extends AppCompatActivity {
   private static TestResource mResource = null;
   
   @Override
   protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);
      if(mManager == null){
         mManager = new TestResource();
      }
   //...
   }
   
   class TestResource {
      //...
   }
}

在Activity內(nèi)部創(chuàng)建了一個(gè)非靜態(tài)內(nèi)部類的單例,每次啟動(dòng)Activity時(shí)都會(huì)使用該單例的數(shù)據(jù),這樣雖然避免了資源的重復(fù)創(chuàng)建,不過這種寫法卻會(huì)造成內(nèi)存泄漏,因?yàn)榉庆o態(tài)內(nèi)部類默認(rèn)會(huì)持有外部類的引用,而該非靜態(tài)內(nèi)部類又創(chuàng)建了一個(gè)靜態(tài)的實(shí)例,該實(shí)例的生命周期和應(yīng)用的一樣長,這就導(dǎo)致了該靜態(tài)實(shí)例一直會(huì)持有該Activity的引用,導(dǎo)致Activity的內(nèi)存資源不能正常回收。正確的做法為:將該內(nèi)部類設(shè)為靜態(tài)內(nèi)部類或?qū)⒃搩?nèi)部類抽取出來封裝成一個(gè)單例。

3.匿名內(nèi)部類
android開發(fā)經(jīng)常會(huì)繼承實(shí)現(xiàn)Activity/Fragment/View,此時(shí)如果你使用了匿名類,并被異步線程持有了,那要小心了,如果沒有任何措施這樣一定會(huì)導(dǎo)致泄露。

public class MainActivity extends Activity {
...
Runnable ref1 = new MyRunable();
Runnable ref2 = new Runnable() {
    @Override
    public void run() {

    }
};
   ...
}

ref1和ref2的區(qū)別是,ref2使用了匿名內(nèi)部類。我們來看看運(yùn)行時(shí)這兩個(gè)引用的內(nèi)存:

Paste_Image.png

可以看到,ref1沒什么特別的。但ref2這個(gè)匿名類的實(shí)現(xiàn)對(duì)象里面多了一個(gè)引用:this$0這個(gè)引用指向MainActivity.this,也就是說當(dāng)前的MainActivity實(shí)例會(huì)被ref2持有,如果將這個(gè)引用再傳入一個(gè)異步線程,此線程和此Acitivity生命周期不一致的時(shí)候,就造成了Activity的泄露。

4.Handler
Handler 的使用造成的內(nèi)存泄漏問題應(yīng)該說是最為常見了,很多時(shí)候我們?yōu)榱吮苊?ANR 而不在主線程進(jìn)行耗時(shí)操作,在處理網(wǎng)絡(luò)任務(wù)或者封裝一些請(qǐng)求回調(diào)等api都借助Handler來處理,但 Handler 不是萬能的,對(duì)于 Handler 的使用代碼編寫一不規(guī)范即有可能造成內(nèi)存泄漏。另外,我們知道 Handler、Message 和 MessageQueue 都是相互關(guān)聯(lián)在一起的,萬一 Handler 發(fā)送的 Message 尚未被處理,則該 Message 及發(fā)送它的 Handler 對(duì)象將被線程 MessageQueue 一直持有。
由于 Handler 屬于 TLS(Thread Local Storage) 變量, 生命周期和 Activity 是不一致的。因此這種實(shí)現(xiàn)方式一般很難保證跟 View 或者 Activity 的生命周期保持一致,故很容易導(dǎo)致無法正確釋放。

舉個(gè)例子:

public class SampleActivity extends Activity {

   private final Handler mLeakyHandler = new Handler() {
      @Override
      public void handleMessage(Message msg) {
        // ...
      }
   }

   @Override
   protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);

      // Post a message and delay its execution for 10 minutes.
      mLeakyHandler.postDelayed(new Runnable() {
        @Override
        public void run() { /* ... */ }
      }, 1000 * 60 * 10);

      // Go back to the previous Activity.
      finish();
   }
}

在該 SampleActivity 中聲明了一個(gè)延遲10分鐘執(zhí)行的消息 Message,mLeakyHandler 將其 push 進(jìn)了消息隊(duì)列 MessageQueue 里。當(dāng)該 Activity 被 finish() 掉時(shí),延遲執(zhí)行任務(wù)的 Message 還會(huì)繼續(xù)存在于主線程中,它持有該 Activity 的 Handler 引用,所以此時(shí) finish() 掉的 Activity 就不會(huì)被回收了從而造成內(nèi)存泄漏(因 Handler 為非靜態(tài)內(nèi)部類,它會(huì)持有外部類的引用,在這里就是指 SampleActivity)。

修復(fù)方法:在 Activity 中避免使用非靜態(tài)內(nèi)部類,比如上面我們將 Handler 聲明為靜態(tài)的,則其存活期跟 Activity 的生命周期就無關(guān)了。同時(shí)通過弱引用的方式引入 Activity,避免直接將 Activity 作為 context 傳進(jìn)去,見下面代碼:

public class SampleActivity extends Activity {

  /**
   * Instances of static inner classes do not hold an implicit
   * reference to their outer class.
   */
  private static class MyHandler extends Handler {
    private final WeakReference<SampleActivity> mActivity;

    public MyHandler(SampleActivity activity) {
      mActivity = new WeakReference<SampleActivity>(activity);
    }

    @Override
    public void handleMessage(Message msg) {
      SampleActivity activity = mActivity.get();
      if (activity != null) {
        // ...
      }
    }
  }

  private final MyHandler mHandler = new MyHandler(this);

  /**
   * Instances of anonymous classes do not hold an implicit
   * reference to their outer class when they are "static".
   */
  private static final Runnable sRunnable = new Runnable() {
      @Override
      public void run() { /* ... */ }
  };

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Post a message and delay its execution for 10 minutes.
    mHandler.postDelayed(sRunnable, 1000 * 60 * 10);

    // Go back to the previous Activity.
    finish();
  }
}

綜述,即推薦使用靜態(tài)內(nèi)部類 + WeakReference 這種方式。每次使用前注意判空。

5.單例模式
Activity對(duì)象被單例模式的TestManager所持有,而單例模式特點(diǎn)是其生命周期與Application保持一致。
例1

package com.ryg.chapter_15.manager;

import java.util.ArrayList;
import java.util.List;

public class TestManager {

    private List<OnDataArrivedListener> mOnDataArrivedListeners = new ArrayList<OnDataArrivedListener>();

    private static class SingletonHolder {
        public static final TestManager INSTANCE = new TestManager();
    }

    private TestManager() {
    }

    public static TestManager getInstance() {
        return SingletonHolder.INSTANCE;
    }

    public synchronized void registerListener(OnDataArrivedListener listener) {
        if (!mOnDataArrivedListeners.contains(listener)) {
            mOnDataArrivedListeners.add(listener);
        }
    }

    public synchronized void unregisterListener(OnDataArrivedListener listener) {
        mOnDataArrivedListeners.remove(listener);
    }

    public interface OnDataArrivedListener {
        public void onDataArrived(Object data);
    }
}
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TestManager.getInstance().registerListener(this);
   }

在銷毀前記得注銷掉即可

    @Override
    protected void onDestroy() {
        testManager.unregisterListener(mMyListener);
        super.onDestroy();
    }

例2

public class AppManager {
   private static AppManager instance;
   private Context context;
   private AppManager(Context context) {
      this.context = context;
   }
   
   public static AppManager getInstance(Context context) {
      if (instance == null) {
         instance = new AppManager(context);
      }
      return instance;
   }
}

如果此時(shí)傳入的是 Activity 的 Context,當(dāng)這個(gè) Context 所對(duì)應(yīng)的 Activity 退出時(shí),由于該 Context 的引用被單例對(duì)象所持有,其生命周期等于整個(gè)應(yīng)用程序的生命周期,所以當(dāng)前 Activity 退出時(shí)它的內(nèi)存并不會(huì)被回收,這就造成泄漏了。

正確的方式應(yīng)該改為下面這種方式:
this.context = context.getApplicationContext();// 使用Application 的context

6.集合類泄漏
集合類如果僅僅有添加元素的方法,而沒有相應(yīng)的刪除機(jī)制,導(dǎo)致內(nèi)存被占用。如果這個(gè)集合類是全局性的變量 (比如類中的靜態(tài)屬性,全局性的 map 等即有靜態(tài)引用或 final 一直指向它),那么沒有相應(yīng)的刪除機(jī)制,很可能導(dǎo)致集合所占用的內(nèi)存只增不減。比如上面的典型例子就是其中一種情況,當(dāng)然實(shí)際上我們?cè)陧?xiàng)目中肯定不會(huì)寫這么 2B 的代碼,但稍不注意還是很容易出現(xiàn)這種情況,比如我們都喜歡通過 HashMap 做一些緩存之類的事,這種情況就要多留一些心眼。

7.屬性動(dòng)畫導(dǎo)致內(nèi)存泄露
屬性動(dòng)畫如果無限循環(huán),需要在destory中將其停止。

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        sContext = this;
        ObjectAnimator animator = ObjectAnimator.ofFloat(mButton, "rotation",0, 360).setDuration(2000);
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.start();
        //animator.cancel();在destory中調(diào)用cancel即可

8.RxJava 使用不當(dāng)造成內(nèi)存泄漏
RxJava 是一個(gè)非常易用且優(yōu)雅的異步操作庫。對(duì)于異步的操作,如果沒有及時(shí)取消訂閱,就會(huì)造成內(nèi)存泄漏:

Observable.interval(1, TimeUnit.SECONDS)
          .subscribe(new Action1<Long>() {
              @Override public void call(Long aLong) {
                  // pass
              }
          });

同樣是匿名內(nèi)部類造成的引用沒法被釋放,使得如果在 Activity 中使用就會(huì)導(dǎo)致它無法被回收,即使我們的 Action1 看起來什么也沒有做。解決辦法就是接收 subscribe 返回的 Subscription 對(duì)象,在 Activity onDestroy 的時(shí)候?qū)⑵淙∠嗛喖纯桑?/p>

public class LeakActivity extends AppCompatActivity {
 
    private Subscription mSubscription;
 
 
    @Override protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_leak);
 
        mSubscription = Observable.interval(1, TimeUnit.SECONDS)
            .subscribe(new Action1<Long>() {
              @Override public void call(Long aLong) {
                  // pass
              }
            });
    }
 
 
    @Override protected void onDestroy() {
        super.onDestroy();
        mSubscription.unsubscribe();
    }
}
三、內(nèi)存泄漏的檢測(cè)

觀察 Memory Monitor 內(nèi)存走勢(shì)圖,可以或多或少知道內(nèi)存情況,但如果要精確地追蹤到內(nèi)存泄漏點(diǎn),這里特別推薦偉大的 Square 公司開源的 LeakCanary 方案,LeakCanary 可以做到非常簡單方便、低侵入性地捕獲內(nèi)存泄漏代碼,甚至很多時(shí)候你可以捕捉到 Android 官方組件的內(nèi)存泄漏代碼。
參考利用 LeakCanary 來檢查 Android 內(nèi)存泄漏

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,983評(píng)論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,772評(píng)論 3 422
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,947評(píng)論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,201評(píng)論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,960評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,350評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,406評(píng)論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,549評(píng)論 0 289
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,104評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,914評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,089評(píng)論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,647評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,340評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,753評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,007評(píng)論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,834評(píng)論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,106評(píng)論 2 375

推薦閱讀更多精彩內(nèi)容

  • 內(nèi)存泄露指的是該釋放的對(duì)象沒有釋放,一直被某個(gè)或某些實(shí)例特持有卻不再被使用導(dǎo)致GC不能回收。首先,我們先看看Jav...
    PeOS閱讀 690評(píng)論 0 2
  • 前言 對(duì)于內(nèi)存泄漏,我想大家在開發(fā)中肯定都遇到過,只不過內(nèi)存泄漏對(duì)我們來說并不是可見的,因?yàn)樗窃诙阎谢顒?dòng),而要想...
    EsonJack閱讀 928評(píng)論 1 3
  • 寫在前面 前段時(shí)間寫了一篇MVP初嘗試,由于當(dāng)時(shí)只是剛接觸,只是簡單的實(shí)現(xiàn),還有很多問題沒想明白。關(guān)于內(nèi)存泄露這事...
    xiasuhuei321閱讀 3,528評(píng)論 9 38
  • 我的博客:http://xuyushi.github.io原文地址 [TOC] 內(nèi)存泄露 內(nèi)存泄露的定義:當(dāng)某些對(duì)...
    接地氣的二呆閱讀 1,296評(píng)論 2 23
  • 深入內(nèi)存泄露 android應(yīng)用層的內(nèi)存泄露,其實(shí)就是java虛擬機(jī)的內(nèi)存泄漏.(這里,暫不討論C/C++本地內(nèi)存...
    Chauncey_Chen閱讀 1,302評(píng)論 3 36