Android內存泄漏的本質原因、解決辦法、操作實例


今年最后一個迭代終於結束了,把過程中碰到的不熟悉的東西拉出來學習總結一下
 
內存泄漏的本質是:【一個(巨大的)短生命周期對象的引用被一個長生命周期(異步生命周期)的對象持有】
 
這個東西分為兩個部分
  1. 獲得一個(巨大的)短生命周期的對象
    1. 這個【巨大的短生命周期的對象】在Android中最有可能的就是【Activity】了
    2. 最容易無意識獲得它的方式就是【非靜態內部類隱式自動持有外部類的強引用】
  2. 把這個對象賦值給了一個長生命周期的對象
    1. 這個有一些常見的套路
      1. 套路一:直接賦值給了一個類的靜態成員
        1. 這個靜態成員的生命周期和應用的一樣長,這就導致了該靜態實例一直會持有該Activity的引用,導致Activity的內存資源不能正常回收
      2. 套路二:一個匿名內部handler的形式持有acitivity,然后message又持有handler,最后長生命周期的Looper持有了這個message
        1. Handler屬於TLS(Thread Local Storage)變量,生命周期和Activity是不一致的
        2. 當Android應用啟動的時候,會先創建一個UI主線程的Looper對象,Looper實現了一個簡單的消息隊列,一個一個的處理里面的Message對象。主線程Looper對象在整個應用生命周期中存在。
        3. 當在主線程中初始化Handler時,該Handler和Looper的消息隊列關聯,同時發送到消息隊列的Message會引用發送該消息的Handler對象
        4. 只要Handler發送的Message尚未被處理,則該Message及發送它的Handler對象將被線程MessageQueue一直持有
        5. 當這個Activity退出時消息隊列中還有未處理的消息或者正在處理消息,而消息隊列中的Message持有mHandler實例的引用,mHandler又持有Activity的引用,所以導致該Activity的內存資源無法及時回收,引發內存泄漏
      3. 套路三:一個匿名內部Runnable持有acitivity,然后這個Runnable有一個耗時任務,這個耗時任務的生命周期比acitivity長
        1. 異步任務AsyncTask和Runnable都是一個匿名內部類,因此它們對當前Activity都有一個隱式引用
        2. 線程產生的內存泄漏主要原因在於線程生命周期的不可控。比如線程是Activity的內部類,則線程對象中保存了Activity的一個引用,當線程的run函數耗時較長沒有結束時,線程對象是不會被銷毀的,因此它引用的老的Activity也不會被銷毀,因此就出現了內存泄漏的問題
        3. Java中的Thread有一個特點就是她們都是直接被GC Root所引用,也就是說Dalvik虛擬機對所有被激活狀態的線程都是持有強引用,導致GC永遠都無法回收掉這些線程對象,除非線程被手動停止並置為null或者用戶直接kill進程操作。所以當使用線程時,一定要考慮在Activity退出時,及時將線程也停止並釋放掉
      4. 套路四:把這個對象傳入了一個異步線程
 
所以解決方法就是(以下只要阻斷一處就OK了)
  1. 不要獲得一個(巨大的)短生命周期的對象,假如不需要的時候
    1. 比如最容易無意識獲得Activity的方式就是【非靜態內部類隱式自動持有外部類的強引用】
      1. 為了不讓內部類自動持有外部類的強引用,把原來的【非靜態內部類或者是匿名內部類】重寫為【靜態內部類】就可以了,也就是獨立出來加一個static
      2. 或者是不要使用內部類,抽出來成為一個外部類
      3. (這樣做之后內部類里就無法直接使用外部環境了(調用外部類的變量和方法),如果要使用的話,就通過構造方法傳進來)
      4. (這樣就避免了內部類自動的無法控制的持有全部外部環境,只讓內部類使用指定的外部環境的資源)
  2. 需要獲得一個(巨大的)短生命周期的對象時,使用弱引用
    1. 如果一定要持有acitivty的引用,那就把這個引用改成弱引用
    2. 不過在【非靜態內部類或者是匿名內部類】的情況下,需要先重寫為【靜態內部類】;因為得先把自動持有acitivity這東西廢了,再通過構造方法把activity傳進來,才能把acitivity的引用改為弱引用
  3. 不要賦值給了一個長生命周期的對象,假如可以的話
    1. 所以就是不需要使用靜態變量的地方就不用
    2. 不過前面的hander和runnable就沒有辦法了,改不了
  4. 控制這個長生命周期的對象的生命周期,假如可以的話
    1. 比如靜態變量,就可以在該清空的時候清空
  5. 把【(巨大的)短生命周期的對象】換成【(巨大的)長生命周期的對象】
 
所以就能推出大部分場景了(從上面的套路直接可以得到幾種場景)
  1. 套路一:單例持有外部引用
    1. 發生場景
      1. 網絡訪問庫中VolleyCreator單例持有外部Context
      2. ToastUtil中Toast單例持有外部Context
      3. NewMsgReceiver單例中的成員變量observers存入了外部Activity引用
    2. 原因:
      1. 單例的靜態特性使得單例的生命周期和應用的生命周期一樣長
    3. 解決方法:
      1. 適合使用方法五
      2. 單例引用Context要注意Context的生命周期,一般的Context可以使用ApplicationContext,對於單例成員變量注意在onDestory移除引用,比如觀察者模式取消注冊。
      3. 或者直接在單例里取ApplicationContext就好了
  2. 套路一:activity的靜態變量持有自己的引用
    1. 這是之前在場景 “一個acitivity殺掉前面的另一個acitivity” 時我們經常使用的方法
    2. 解決辦法
      1. 可以使用方法四:在onDestroy的時候清空這個靜態變量
      2. 或者不使用這種方法來殺activity,使用eventbus
  3. 套路一:使用非靜態內部類創建靜態實例
    1. 很容易理解,你這個對象持有context,然后把這個對象賦值給靜態變量,那這個context就直接泄漏了
    2. 舉例:在啟動頻繁的Activity中,為了避免重復創建相同的數據資源,會出現:
      1. 在Activity內部創建了一個非靜態內部類的單例,每次啟動Activity時都會使用該單例的數據,這樣雖然避免了資源的重復創建,不過這種寫法卻會造成內存泄漏
        1. public class MainActivity extends AppCompatActivity {
        1.     private static TestResource mResource = null;
        1.     @Override
        1.     protected void onCreate(Bundle savedInstanceState) {
        1.         super.onCreate(savedInstanceState);
        1.         setContentView(R.layout.activity_main);
        1.         if(mResource == null){
        1.             mResource = new TestResource();
        1.         }
        1.         //...
        1.     }
        1.     class TestResource {
        1.         //...
        1.     }
        1. }
      2. 這種必須要使用靜態變量,同時不知道什么時候釋放,解決方法就是方法一,直接把非靜態內部類改成靜態就好了
  4. 套路二:非靜態Handler持有外部Context引用
    1. 這種情況由於handler需要持有activity,所以使用方法二
    2. 然后具體是:
      1. 創建一個靜態Handler內部類,然后對Handler持有的對象使用弱引用,這樣在回收時也可以回收Handler持有的對象
      2. 這樣雖然避免了Activity泄漏,不過Looper線程的消息隊列中還是可能會有待處理的消息,所以更好的做法是在Activity的Destroy時或者Stop時應該移除消息隊列中的消息
    3. 另外,還可以使用開源庫WeakHandler,一個防止內存泄漏的Handler,詳見banner源碼WeakHandler.java
      1. public class MainActivity extends AppCompatActivity {
      1.     private MyHandler mHandler = new MyHandler(this);
      1.     private TextView mTextView ;
      1.     private static class MyHandler extends Handler {
      1.         private WeakReference<Activity> reference;
      1.         public MyHandler(Activity activity) {
      1.             reference = new WeakReference<>(activity);
      1.         }
      1.         @Override
      1.         public void handleMessage(Message msg) {
      1.             if(reference != null && reference.get() != null ){
      1.                 Activity activity = reference.get();
      1.                 activity.mTextView.setText("");
      1.             }
      1.         }
      1.     }
      1.     @Override
      1.     protected void onCreate(Bundle savedInstanceState) {
      1.         super.onCreate(savedInstanceState);
      1.         setContentView(R.layout.activity_main);
      1.         mTextView = (TextView)findViewById(R.id.textview);
      1.         loadData();
      1.     }
      1.     private void loadData() {
      1.         //...request
      1.         Message message = Message.obtain();
      1.         mHandler.sendMessage(message);
      1.     }
      1.     @Override
      1.     protected void onDestroy() {
      1.         super.onDestroy();
      1.         //使用mHandler.removeCallbacksAndMessages(null);是為了移除消息隊列中所有消息和所有的Runnable
      1.         mHandler.removeCallbacksAndMessages(null);
      1.     }
      1. }
  5. 套路三:線程造成的內存泄漏
    1. 比較常見,這兩個示例可能每個人都寫過:
        1. new AsyncTask<Void, Void, Void>() {
        1.     @Override
        1.     protected Void doInBackground(Void... params) {
        1.         SystemClock.sleep(10000);
        1.         return null;
        1.     }
        1. }.execute();
        1. new Thread(new Runnable() {
        1.     @Override
        1.     public void run() {
        1.         SystemClock.sleep(10000);
        1.     }
        1. }).start();
    2. 如果任務可能執行很久,那就需要處理了,使用方法一或方法二就OK了
      1. //在Activity銷毀時候也應該取消相應的任務AsyncTask.cancel(),避免任務在后台執行浪費資源
      1. static class MyAsyncTask extends AsyncTask<Void, Void, Void> {
      1.     private WeakReference<Context> weakReference;
      1.     public MyAsyncTask(Context context) {
      1.         weakReference = new WeakReference<>(context);
      1.     }
      1.     @Override
      1.     protected Void doInBackground(Void... params) {
      1.         SystemClock.sleep(10000);
      1.         return null;
      1.     }
      1.     @Override
      1.     protected void onPostExecute(Void aVoid) {
      1.         super.onPostExecute(aVoid);
      1.         MainActivity activity = (MainActivity) weakReference.get();
      1.         if (activity != null) {
      1.             //...
      1.         }
      1.     }
      1. }
      1. static class MyRunnable implements Runnable{
      1.     @Override
      1.     public void run() {
      1.         SystemClock.sleep(10000);
      1.     }
      1. }
      1. //——————使用——————
      1. new Thread(new MyRunnable()).start();
      1. new MyAsyncTask(this).execute();
 
其他套路外會導致內存泄漏的場景(其實深究原因也是套路內的)
  1. MainActivity和HomeFragment EventBus沒有反注冊
    1. 解決方案: onDestory注意反注冊
  2. 資源未關閉造成的內存泄漏
    1. 對於使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等資源的使用,應該在Activity銷毀時及時關閉或者注銷,否則這些資源將不會被回收,造成內存泄漏
  3. 有些代碼並不造成內存泄露,但是它們,或是對沒使用的內存沒進行有效及時的釋放,或是沒有有效的利用已有的對象而是頻繁的申請新內存
    1. Bitmap沒調用recycle()
      1. Bitmap對象在不使用時,我們應該先調用recycle()釋放內存,然后將它設置為null。因為加載Bitmap對象的內存空間,一部分是java的,一部分C的(因為Bitmap分配的底層是通過JNI調用的)。而這個recycle()就是針對C部分的內存釋放
    2. ViewHolder
      1. 構造 Adapter 時,沒有使用緩存的 convertView ,每次都在創建新的 converView。這里推薦使用ViewHolder
  4. 集合類
    1. 集合類如果僅僅有添加元素的方法,而沒有相應的刪除機制,導致內存被占用。如果這個集合類是全局性的變量(比如類中的靜態屬性,全局性的map等即有靜態引用或final一直指向它),那么沒有相應的刪除機制,很可能導致集合所占用的內存只增不減
 
一些tips
  1. 在activity的onDestroy方法中手動吧所有需要置為Null的靜態變量置為null
  2. 不要在類初始時初始化靜態成員。可以考慮lazy初始化
  3. 盡量不要在靜態內部類中使用非靜態外部成員變量,使用的話也用弱引用
  4. 在 Java 的實現過程中,也要考慮其對象釋放,最好的方法是在不使用某對象時,顯式地將此對象賦值為 null,比如使用完Bitmap后先調用 recycle(),再賦為null,清空對圖片等資源有直接引用或者間接引用的數組(使用 array.clear() ;array = null)等,最好遵循誰創建誰釋放的原則
 
 

附錄:
非靜態內部類隱式自動持有外部類的強引用
  1. 這是個非常容易發生的情況,比如你隨便new一個匿名內部類的時候就會發生,比如new一個clicklistener,new一個Runnable,new一個Handler,但是這種情況本身沒有問題,問題是在搭配其他條件的時候發生的
  2. 這種情況說白了就是:非靜態內部類的對象會持有外部對象的強引用
  3. 為什么會這樣,很好理解: 因為內部類要能使用外部類的資源,就是通過這個引用實現的.
 
弱引用持有的寫法(以handler持有acitivty為例)
public static class MyHandler extends Handler
    //聲明一個弱引用對象
    WeakReference<MainActivity> mReference;
 
    MyHandler(MainActivity activity)
        //在構造器中傳入Activity,創建弱引用對象
        mReference = new WeakReference<MainActivity>(activity);
    }
 
    public void handleMessage(Message msg)
        //在使用activity之前先判空處理
        if (mReference != null && mReference.get() != null)
            mReference.get().text.setText(hello word);
        }
    }
}
 
向靜態方法中傳入短生命周期的變量(比如acitivity)不會導致內存泄漏
  1. 場景
    1. 比如靜態啟動acitivity方法中傳入上一個acitivity
    2. 把一些常用的或者公共方法放到一個工具類里,寫成靜態(static)的形式,如果這個方法需要傳遞一個參數(外部短生命周期對象的引用)的話
  2. 原因
    1. 要想造成內存泄漏,你的工具類對象本身要持有指向傳入對象的引用才行!但是當你的業務方法調用工具類的靜態方法時,會生產一個稱為方法棧幀的東西(每次方法調用,JVM都會生成一個方法棧幀),當方法調用結束返回的時候,當前方法棧幀就已經被彈出了並且被釋放掉了。 整個過程結束時,工具類對象本身並不會持有傳入對象的引用。 
    2. 把對象引用傳遞給靜態方法(不是靜態方法也是一樣的),在調用結束時,工具類對象本身並不會引用傳入的對象。所以就沒有問題。 
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM