記憶體優化(二)如何避免記憶體洩漏
阿新 • • 發佈:2019-01-03
文章目錄
一、不同生命週期導致的記憶體洩漏
前面有分析了記憶體洩漏的原因,本該被回收的物件被佔用,得不到回收便會記憶體洩漏。總歸到底的原因還是物件引用在類之間傳遞,它們的生命週期不同,導致回收時發生問題。
舉個簡單的例子:
當單列模式中傳入的Activity是,ToastRouter便持有了MainActivity的強引用,當MainActivity結束時,便得不到回收,這是記憶體洩漏發生了
public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ToastRouter.getInstance(this); } } ... public class ToastRouter { private static final ToastRouter ourInstance = new ToastRouter(); private Context mContext; public static ToastRouter getInstance(Context context) { this.mContext=context; return ourInstance; } private ToastRouter() { } }
解決辦法
- 儘量避免生命週期不對等的物件引用傳遞,如果避免不了,應用對等生命週期的物件替代。如上Activity傳入,可以用ApplicationContext傳遞代替,因為ApplicationContext的生命週期與APP對等。
- 與物件生命週期繫結釋放,如上例子,應該在MainActivity onDestary()方法中釋放傳入物件引用
二、非靜態內部類持有物件導致的記憶體洩漏
非靜態內部類持有物件導致的記憶體洩漏也很好理解,就是內部類持有了外部類的物件,導致的外部類回收失敗造成的記憶體洩漏
1. 非靜態內部類呼叫外部類的方法的
-
看一個簡單的JAVA程式碼案例
-
通過匿名內部類,內部類分別列印一個延時log日誌。
-
其中分別用的AsyncTask和Handler舉例
/** * 通過匿名內部類,列印一個1.5s延時log */ public void doAnonymousInnerClassTask() { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... voids) { //睡眠1.5s try { Thread.sleep(1500); } catch (InterruptedException e) { e.printStackTrace(); } logMsg(" AsyncTask 匿名內部類 doInBackground"); return null; } }.execute(); } /** * 通過內部類,列印一個1.5s延時log */ public void doNormalInnerTask() { TestAsyncTask testAsyncTask = new TestAsyncTask(); testAsyncTask.execute(); } private class TestAsyncTask extends AsyncTask<Void, Integer, Void> { @Override protected Void doInBackground(Void... voids) { try { Thread.sleep(1500); } catch (InterruptedException e) { e.printStackTrace(); } logMsg(" TestAsyncTask內部類 doInBackground"); return null; } } private void logMsg(String msg) { Log.d(TAG, msg); }
-
-
我們通過.class檔案,檢視它的物件引用發現,在匿名內部類和內部類中,呼叫MainActivity的logMsg方法時,已持有了MainActivity.this的引用,因此當非靜態內部類生命週期比MainActivity長時,即發生了記憶體洩漏。
public void doAnonymousInnerClassTask() { (new AsyncTask<Void, Void, Void>() { protected Void doInBackground(Void... voids) { try { Thread.sleep(1500L); } catch (InterruptedException var3) { var3.printStackTrace(); } MainActivity.this.logMsg(" AsyncTask 匿名內部類 doInBackground"); return null; } }).execute(new Void[0]); } private class TestAsyncTask extends AsyncTask<Void, Integer, Void> { private TestAsyncTask() { } protected Void doInBackground(Void... voids) { try { Thread.sleep(1500L); } catch (InterruptedException var3) { var3.printStackTrace(); } MainActivity.this.logMsg(" TestAsyncTask內部類 doInBackground"); return null; } }
2. 內部類是如何持有外部類物件?
上述通過.class檔案可以看到內部類在呼叫外部類的方法時,通過持有的外部類物件去呼叫。那麼外部類物件的引用是什麼時候傳入的呢。通過
參考文章:深入理解Java中為什麼內部類可以訪問外部類的成員得到結論:
- 編譯器自動為內部類新增一個成員變數, 這個成員變數的型別和外部類的型別相同, 這個成員變數就是指向外部類物件的引用;
- 編譯器自動為內部類的構造方法新增一個引數, 引數的型別是外部類的型別, 在構造方法內部使用這個引數為1中新增的成員變數賦值;
- 在呼叫內部類的建構函式初始化內部類物件時, 會預設傳入外部類的引用。
由此,即使內部類並沒有呼叫外部類的方法或者變數,也一樣會持有外部類的引用。
3. 如何處理非靜態內部類記憶體洩漏問題
-
可以使用,但是前提是確保非靜態內部類的生命週期超過外部類
-
使用靜態內部類,在靜態內部類需要持有外部類引用時,通過關聯外部類的弱引用去呼叫。
/** * 通過靜態內部類列印一個1.5s延時log */ public void doStaticInnerClassTask() { mHandler.sendEmptyMessageDelayed(0, 1500); } private final StaticHandler mHandler = new StaticHandler(this); private static class StaticHandler<T> extends Handler { protected WeakReference<T> ref; public StaticHandler(T cls) { ref = new WeakReference<T>(cls); } public T getRef() { return ref != null ? ref.get() : null; } @Override public void handleMessage(Message msg) { super.handleMessage(msg); MainActivity _Activity = (MainActivity) ref.get(); _Activity.logMsg("StaticHandler 靜態內部類log "); } }
-
在外部類生命週期結束前自主釋放外部類的引用