天天看點

Android記憶體洩漏和記憶體溢出(oom)的差別及解決方案

本文主要是整理!

記憶體洩漏(memory leak)

什麼是記憶體洩漏

記憶體洩漏是指你申請了一塊記憶體,但沒有及時釋放,而這塊記憶體會一直占用無法在進行配置設定,
這樣就會出現記憶體洩漏。
           

java的GC記憶體回收機制

某對象不再有任何的引用的時候才會進行回收。
           

記憶體配置設定的政策

  • 靜态的
靜态的存儲區:記憶體在程式編譯的時候就已經配置設定好,這塊的記憶體在程式整個運作期間都一直存在。
它主要存放靜态資料、全局的static資料和一些常量。
           
  • 棧式的
在執行函數(方法)時,函數一些内部變量的存儲都可以放在棧上面建立,
函數執行結束的時候這些存儲單元就會自動被釋放掉。
注:
    棧記憶體包括配置設定的運算速度很快,因為内置在處理器的裡面的。當然容量有限。
           
  • 堆式的
也可以成為動态記憶體配置設定。常用的方式就是new來申請一塊記憶體。
java裡面是直接依賴的java的gc回收機制。
           
  • 棧式的 和 堆式的的差別

1.堆是不連續的記憶體區域,堆空間比較靈活也特别大。

2.棧是一塊連續的記憶體區域,大小是有作業系統覺決定的。

3.堆管理很麻煩,頻繁地new/remove會造成大量的記憶體碎片,這樣就會慢慢導緻效率低下。

4.對于棧的話,它先進後出,進出完全不會産生碎片,運作效率高且穩定。

看下面代碼示例:

public class OOMMain {
    // 堆
    int a=0;
    Book book1=new Book();

    private void main(){
    //棧
        int b=9;
        Book2 book2=new Book2();
    }
}

           

A.成員變量全部存儲在堆中(包括基本資料類型,引用及引用的對象實體)—因為他們屬于類,類對象,最終還是要被new出來的。

B.局部變量的基本資料類型和引用存儲于棧當中,引用的對象實體存儲在堆中。-----因為他們屬于方法當中的變量,生命周期會随着方法一起結束。

從以上内容我們可以知道:記憶體洩露,主要讨論堆記憶體,它存放的就是引用指向的對象實體。

記憶體洩漏4種狀态

  • 常發性記憶體洩漏。

    發生記憶體洩漏的代碼會被多次執行到,每次被執行的時候都會導緻一塊記憶體洩漏。

  • 偶發性記憶體洩漏。

    發生記憶體洩漏的代碼隻有在某些特定環境或操作過程下才會發生。常發性和偶發性是相對的。對于特定的環境,偶發性的也許就變成了常發性的。是以測試環境和測試方法對檢測記憶體洩漏至關重要。

  • 一次性記憶體洩漏。

    發生記憶體洩漏的代碼隻會被執行一次,或者由于算法上的缺陷,導緻總會有一塊僅且一塊記憶體發生洩漏。比如,在類的構造函數中配置設定記憶體,在析構函數中卻沒有釋放該記憶體,是以記憶體洩漏隻會發生一次。

  • 隐式記憶體洩漏。

    程式在運作過程中不停的配置設定記憶體,但是直到結束的時候才釋放記憶體。嚴格的說這裡并沒有發生記憶體洩漏,因為最終程式釋放了所有申請的記憶體。但是對于一個伺服器程式,需要運作幾天,幾周甚至幾個月,不及時釋放記憶體也可能導緻最終耗盡系統的所有記憶體。是以,我們稱這類記憶體洩漏為隐式記憶體洩漏。

危害

  • 過多的記憶體洩露最終會導緻記憶體溢出(OOM)
  • 記憶體洩露導緻可用記憶體不足,會觸發頻繁GC,不管是Android2.2以前的單線程GC還是現在的CMS和G1,都有一部分的操作會導緻使用者線程停止(就是所謂的Stop the world),進而導緻UI卡頓。

産生的常見原因

  • 資源未關閉造成的記憶體洩漏

BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等資源的使用,應該在Activity銷毀時及時關閉或者登出,否則這些資源将不會被回收,造成記憶體洩漏。

  • 記得登出監聽器

注冊監聽器的時候會add Listener,不要忘記在不需要的時候remove掉Listener

  • 單例造成的記憶體洩漏

單例的靜态特性使得單例的生命周期和應用的生命周期一樣長,這就說明了如果一個對象已經不需要使用了,而單例對象還持有該對象的引用,那麼這個對象将不能被正常回收,這就導緻了記憶體洩漏。

看一個經典案例:

//單例需要傳入一個Context,是以這個Context的生命周期的長短至關重要:
public class AppManager {
    private static AppManager instance;
    private Context context;
     private AppManager(Context context) {
     
        this.context = context.getApplicationContext();
//1.這裡傳入一個Application的Context:這将沒有任何問題,因為單例的生命周期和Application的一樣長

	//this.context = context;
//2、傳入的是Activity的Context:當這個Context所對應的Activity退出時,由于該Context和Activity的生命周期一樣長(Activity間接繼承于Context),是以目前Activity退出時它的記憶體并不會被回收,因為單例對象持有該Activity的引用。
        
    }

    public static AppManager getInstance(Context context) {
        if (instance != null) {
            instance = new AppManager(context);
        }
        return instance;
    }
}
           
  • 線程造成的記憶體洩漏
//-------------------第一種
        new AsyncTask() {
            @Override
            protected Void doInBackground(Void... params) {
                SystemClock.sleep(100000);
                return null;
            }
        }.execute();
//-------------------第二種
        new Thread(new Runnable() {
            @Override
            public void run() {
                SystemClock.sleep(100000);
            }
        }).start();
           

這裡的異步任務和Runnable都是一個匿名内部類,是以它們對目前Activity都有一個隐式引用。如果Activity在銷毀之前,任務還未完成,那麼将導緻Activity的記憶體資源無法回收,造成記憶體洩漏 。

正确的寫法:使用靜态内部類的寫法

static class MyAsyncTask extends AsyncTask {
        private WeakReference weakReference;

        public MyAsyncTask(Context context) {
            weakReference = new WeakReference<>(context);
        }

        @Override
        protected Void doInBackground(Void... params) {
            SystemClock.sleep(10000);
            return null;
        }

        @Override
        protected void onPostExecute(Void aVoid) {
            super.onPostExecute(aVoid);
            MainActivity activity = (MainActivity) weakReference.get();
            if (activity != null) {
                //...
            }
        }
    }
    static class MyRunnable implements Runnable{
        @Override
        public void run() {
            SystemClock.sleep(10000);
        }
    }
//——————
    new Thread(new MyRunnable()).start();
    new MyAsyncTask(this).execute();
           

這樣就避免了Activity的記憶體資源洩漏,當然在Activity銷毀時候也應該取消相應的任務AsyncTask::cancel(),避免任務在背景執行浪費資源。

  • Handler造成的記憶體洩漏

    Handler的使用代碼編寫不規範即有可能造成記憶體洩漏,如下示例:

public class MainActivity extends AppCompatActivity {
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            //...
        }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        loadData();
    }
    private void loadData(){
        //...request
        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }
}
           

由于mHandler是Handler的非靜态匿名内部類的執行個體,是以它持有外部類Activity的引用,我們知道消息隊列是在一個Looper線程中不斷輪詢處理消息,那麼當這個Activity退出時消息隊列中還有未處理的消息或者正在處理消息,而消息隊列中的Message持有mHandler執行個體的引用,mHandler又持有Activity的引用,是以導緻該Activity的記憶體資源無法及時回收,引發記憶體洩漏

正确寫法:

public class MainActivity extends AppCompatActivity {
    private MyHandler mHandler = new MyHandler(this);
    private TextView mTextView ;
    private static class MyHandler extends Handler {
        private WeakReference reference;
        public MyHandler(Context context) {
            reference = new WeakReference<>(context);
        }
        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = (MainActivity) reference.get();
            if(activity != null){
                activity.mTextView.setText("");
            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = (TextView)findViewById(R.id.textview);
        loadData();
    }

    private void loadData() {
        //...request
        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
    }
}

           

使用mHandler.removeCallbacksAndMessages(null);是移除消息隊列中所有消息和所有的Runnable。當然也可以使用mHandler.removeCallbacks();或mHandler.removeMessages();來移除指定的Runnable和Message。

如何避免記憶體洩漏

  • 使用輕量的資料結構

    使用ArrayMap/SparseArray來代替HashMap,ArrayMap/SparseArray是專門為移動裝置設計的高效的資料結構

1.SparseArray

a.支援int類型,避免自動裝箱,但是也隻支援int類型的key
   
   b.内部通過兩個數組來進行資料存儲的,一個存儲key,另外一個存儲value
   
   c.因為key是int,在查找時,采用二分查找,效率高,SparseArray存儲的元素都是按元素的key值從小到大排列好的。 (Hashmap通過周遊Entry數組來擷取對象) 
   
   d.預設初始size為0,每次增加元素,size++
           
  • ArrayMap
    a.跟SparseArray一樣,内部兩個數組,但是第一個存key的hash值,一個存value,對象按照key的hash值排序,二分查找也是按照hash
     
     b.查找index時,傳入key,計算出hash,通過二分查找hash數組,确定index
               
  • 不要使用Enum
  • Bitmap的處理
    1.Bitmap壓縮(品質壓縮/尺寸壓縮)
      2. Lru機制處理Bitmap,也可以使用那些有名的圖檔緩存架構。
               

關于這個Lru大緻提一下:Picasso和glide(大部分都是基于LruCache的)。

LruCache近期最少使用算法的緩存機制,LruCache内部擁有一個LinkedHashMap,其key 為索引,value為Bitmap。LinkedHashMap的内部實作儲存了添加的順序,是以,當LruCache裡面的緩存比設定的 最大值大時,就會把最先添加的那些緩存清除掉。

linkedHashMap:

Android記憶體洩漏和記憶體溢出(oom)的差別及解決方案

記憶體溢出(oom)

定義

我們需要一定記憶體的大小,但是系統無法配置設定給我們,滿足不了我們的需求,是以導緻oom

産生原因及如何避免

  • 圖檔過大導緻 OOM

可以對圖檔進行:品質壓縮或尺寸壓縮

  • Bitmap 對象不再使用時調用 recycle()釋放記憶體
  • 查詢資料庫沒有關閉遊标
  • 界面切換導緻 OOM

    有時候我們會發現這樣的問題,橫豎屏切換 N 次後 OOM 了。

    這種問題沒有固定的解決方法,但是我們可以從以下幾個方面下手分析。

    1.看看頁面布局當中有沒有大的圖檔,比如背景圖之類的。

/**去除 xml 中相關設定,改在程式中設定背景圖(放在 onCreate()方法中):
*/
Drawable drawable = getResources().getDrawable(R.drawable.id);
ImageView imageView = new ImageView(this);
imageView.setBackgroundDrawable(drawable);
           

在 Activity destory 時注意,drawable.setCallback(null);防止 Activity 得不到及時的釋放。

  • 記憶體洩漏造成的記憶體溢出
  • 其他

Android 應用程式中最典型的需要注意釋放資源的情況是在 Activity 的生命周期中,在 onPause()、onStop()、onDestroy()方法中需要适當的釋放資源的情況。