1. 程式人生 > >Android 常見記憶體洩漏之四大元凶

Android 常見記憶體洩漏之四大元凶

對於記憶體洩漏,我想大家應該都有碰到過,常見的表現就是異常程式退出。

到了程式強制關閉的時候,那已經到了一定的程度了。一般時候記憶體洩漏了我們是看不見的。因為它在堆中活動。

所以常常我們會通過一些工具來檢測。例如:LeakCanary、MAT等工具。

MAT是一款強大的記憶體分析工具,功能繁多而複雜,而LeakCanary則是由Square開源的一款輕量第三方記憶體洩漏檢測工具,當它檢測到程式中有記憶體洩漏的產生時,它將以最直觀的方式告訴我們該記憶體洩漏是由誰產生的和該記憶體洩漏導致誰洩漏了而不能回收,供我們複查。

下面是一個記憶體檢測是圖:


data boject變化的值,一般在1.5M到2.5M左右,過大肯定是不行的。另外就是我們可以在手機上點選各個位置看這個值的變化情況。如果我們點選了一個頁面這個值上去了,然後我們退出了這個介面的時候,這個值久久不能退下來,那麼初步判斷這個頁面可能造成了記憶體洩漏。


個人總結最常見的記憶體洩漏,可以說是幾大元凶:

資源沒有關閉造成的記憶體洩漏

常見的BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap 。其中Bitmap是我們最常用碰到的,也是很厲害的。在Activity銷燬時及時關閉或者登出,不然這些資源是不會被回收的,也就造成了記憶體的洩漏。

不妨可以試試看咯。用Mat檢測一下。你會發現記憶體上去了下不來。

Handler造成的記憶體洩漏

Handler在應用中的表現我就不說了。但是它為什麼會造成記憶體洩漏呢?沒錯,就是它,而且還很常見。

 public class MainActivity extends AppActivity {
		 
	        private Handler mHandler = new Handler() {
	            @Override
	            public void handleMessage(Message msg) {
	                //...do some thing
	            }
	        };

	        @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);
	        }
	    }

這種是我們常見的Handler建立方式,由於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 MyHandlerextendsHandler {
            private WeakReference<Context> 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);
        }
    }

建立一個靜態Handler內部類,然後對Handler持有的物件使用弱引用,這樣在回收時也可以回收Handler持有的物件。

同時我們在Activity的Destroy或Stop中移除訊息佇列中的訊息,mHandler.removeCallbacksAndMessages(null);是移除訊息佇列中所有訊息和所有的Runnable。

執行緒造成的記憶體洩漏

執行緒造成的記憶體洩漏和Handler的過程你可以看成是差不多的。它們都是在Activity關閉的時候可能還未處理完造成的。

同樣我們做法是這樣的

   static class MyAsyncTask extends AsyncTask<Void,Void,Void>{

        private WeakReference<Context>weakReference;

        public MyAsyncTask(Context context){

            weakReference=new WeakReference<>(context);

        }

        @Override
        protected Void doInBackground(Void...params){

            SystemClock.sleep(10000);

            return null;

        }

        @Override
        protected void onPostExecute(VoidaVoid){

            super.onPostExecute(aVoid);
            MainActivity activity=(MainActivity)weakReference.get();
            if(activity!=null){

            }
        }


    }
Activity銷燬時候也應該取消相應的任務AsyncTask::cancel(),如果我們沒有這樣在後臺繼續請求的需求,那麼就可以避免任務在後臺執行浪費資源。

Context造成的記憶體洩漏

這個說的大家可能不大相信了,上下文會造成記憶體洩漏嗎?

看你這個context傳到了什麼地方,如果你傳過去的地方在你關閉的時候都還一直佔用著,必然會造成對記憶體的佔用,造成記憶體的洩漏。下面簡單舉個例子,就拿單例來說,它也是會造成內訓洩漏的。

 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;
        }
    }
在我們建立的時候就傳入一個context進去,它的生命週期很重要,單例的生命週期和Application的一樣長。

傳入的是Activity的Context:當這個Context所對應的Activity退出時,由於該Context和Activity的生命週期一樣長(Activity間接繼承於Context),所以當前Activity退出時它的記憶體並不會被回收,因為單例物件持有該Activity的引用。

所以有了下面這種寫法:

 public class AppManager {
        private static AppManager instance;
        private Context context;

        private AppManager(Context context) {
            this.context = context.getApplicationContext();
        }

        public static AppManager getInstance(Context context) {
            if (instance != null) {
                instance = new AppManager(context);
            }
            return instance;
        }
    }

單例是生命週期應該和應用的生命週期是一樣的,所以傳入Application的context是可以防止記憶體洩漏的。

全域性靜態變數不會造成記憶體洩漏,但是會造成記憶體無法回收。給記憶體很大的壓力,儘量少用全域性靜態變數。

單例、靜態物件、全域性性的集合等,全域性靜態用起來很爽,但是在爽的時候請注意戴好保險!