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