標籤:
在Android開發中,我們經常會使用到static來修飾我們的成員變數,其本意是為了讓多個對象共用一份空間,節省記憶體,或者是使用單例模式,讓該類只生產一個執行個體而在整個app中使用。然而在某些時候不恰當的使用或者是編程的不規範卻會造成了記憶體泄露現象(java上的記憶體流失指記憶體得不到gc的及時回收,從而造成記憶體佔用過多的現象)
本文中我們主要分析的是static變數對activtiy的不恰當引用而造成的記憶體流失,因為對於同一個Activity頁面一般每次開啟時系統都會重建一個該activity的對象(standard模式下),而每個activity對象一般都含有大量的視圖對象和bitmap對象,如果之前的activity對象不能得到及時的回收,從而就造成了記憶體的泄漏現象。
下面一邊看代碼一邊講解。
單例模式不正確的擷取context
public class LoginManager { private Context context; private static LoginManager manager; public static LoginManager getInstance(Context context) { if (manager == null) manager = new LoginManager(context); return manager; } private LoginManager(Context context) { this.context = context; }
在LoginActivity中
public class LoginActivity extends Activity { private LoginManager loginManager; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); loginManager = LoginManager.getInstance(this); }
這種方式大家應該一看就明白問題在哪裡了,在LoginManager的單例中context持有了LoginActivity的this對象,即使登入成功後我們跳轉到了其他Activity頁面,LoginActivity的對象仍然得不到回收因為他被單例所持有,而單例的生命週期是同Application保持一致的。
正確的擷取context的方式
public class LoginManager { private Context context; private static LoginManager manager; public static LoginManager getInstance(Context context) { if (manager == null) manager = new LoginManager(context); return manager; } private LoginManager(Context context) { this.context = context.getApplicationContext(); }
修改方式也非常簡單我們單例中context不再持有Activity的context而是持有Application的context即可,因為Application本來就是單例,所以這樣就不會存在記憶體流失的的現象了。
單例模式中通過內部類持有activity對象
第一種方式記憶體流失太過與明顯,相信大家都不會犯這種錯誤,接下來要介紹的這種泄漏方式會比較不那麼容易發現,內部類的使用造成activity對象被單例持有。
還是看代碼再分析,下面是一個單例的類:
public class TestManager { public static final TestManager INSTANCE = new TestManager(); private List<MyListener> mListenerList; private TestManager() { mListenerList = new ArrayList<MyListener>(); } public static TestManager getInstance() { return INSTANCE; } public void registerListener(MyListener listener) { if (!mListenerList.contains(listener)) { mListenerList.add(listener); } } public void unregisterListener(MyListener listener) { mListenerList.remove(listener); }}interface MyListener { public void onSomeThingHappen();}
然後是activity:
public class TestActivity extends AppCompatActivity { private MyListener mMyListener=new MyListener() { @Override public void onSomeThingHappen() { } }; private TestManager testManager=TestManager.getInstance(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_test); testManager.registerListener(mMyListener); }}
我們知道在java中,非靜態內部類的對象都是會持有指向外部類對象的引用的,因此我們將內部類對象mMyListener讓單例所持有時,由於mMyListener引用了我們的activity對象,因此造成activity對象也不能被回收了,從而出現記憶體流失現象。
修改以上代碼,避免記憶體流失,在activity中添加以下代碼:`
@Overrideprotected void onDestroy() { testManager.unregisterListener(mMyListener); super.onDestroy();}AsyncTask不正確使用造成的記憶體流失
介紹完以上兩種情況的記憶體流失後,我們在來看一種更加容易被忽略的記憶體流失現象,對於AsyncTask不正確使用造成記憶體流失的問題。
mTask=new AsyncTask<String,Void,Void>() {@Overrideprotected Void doInBackground(String... params) { //doSamething.. return null; } }.execute("a task");
一般我們在主線程中開啟一個非同步任務都是通過實現一個內部類其繼承自AsyncTask類然後實現其相應的方法來完成的,那麼自然的mTask就會持有對activity執行個體對象的引用了。查看AsyncTask的實現,我們會通過一個SerialExecutor串列線程池來對我們的任務進行排隊,而這個SerialExecutor對象就是一個static final的常量。
具體的參考關聯性是:
1.我們的任務被封裝在一個FutureTask的對象中(它充當一個runable的作用),FutureTask的實現也是通過內部類來實現的,因此它也為持有AsyncTask對象,而AsyncTask對象引用了activity對象,因此activity對象間接的被FutureTask對象給引用了。
2.futuretask對象會被添加到一個ArrayDeque類型的任務隊列的mTasks執行個體中
3.mTasks任務隊列又被SerialExecutor對象所持有,剛也說了這個SerialExecutor對象是一個static final的常量。
具體AsyncTask的實現大家可以去參照下其原始碼,我這裡就通過文字描述一下其新增工作的實現過程就可以了,總之分析了這麼多通過層層引用後我們的activity會被一個static變數所引用到。所以我們在使用AsyncTask的時候不宜在其中執行太耗時的操作,假設activity已經退出了,然而AsyncTask裡任務還沒有執行完成或者是還在排隊等待執行,就會造成我們的activity對象被回收的時間延後,一段時間內記憶體佔有率變大。
解決方案在activity退出的時候應該調用cancel()函數
@Overrideprotected void onDestroy() { //mTask.cancel(false); mTask.cancel(true); super.onDestroy();}
具體cancel()裡傳遞true or false依實際情況而定:
1.當我們的任務還在排隊沒有被執行,調用cancel()無論true or false,任務會從排隊隊列中移除,即任務都不會被執行到了。
2.當我們的任務已經開始執行了(doInBackground被調用),傳入參數為false時並不會打斷doInBackground的執行,傳入參數為true時,如果我們的線程處於休眠或阻塞(如:sleep,wait)狀況是會打斷其執行。
這裡具體解釋下cancle(true)的意義:
mTask=new AsyncTask<String,Void,Void>() {@Overrideprotected Void doInBackground(String... params) { try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } Log.d("test", "task is running"); return null; } }.execute("a task"); try { //保證task得以執行 Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } mTask.cancel(true);
在這樣的情況下我們的線程處於休眠狀態調用cancel(true)方法會打斷doInBackground的執行——即不會看到log語句的輸出。
但在下面的這種情況的時候卻打斷不了
mTask=new AsyncTask<String,Void,Void>() {@Overrideprotected Void doInBackground(String... params) { Boolean loop=true; while (loop) { Log.d("test", "task is running"); } return null; } }.execute("a task"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } mTask.cancel(true);
由於我們的線程不處於等待或休眠的狀況及時調用cancel(true)也不能打斷doInBackground的執行——現象:log函數一直在列印輸出。
解決方案:
mTask=new AsyncTask<String,Void,Void>() {@Overrideprotected Void doInBackground(String... params) { //doSomething.. Boolean loop=true; while (loop) { if(isCancelled()) return null; Log.d("test", "task is running"); } return null; } }.execute("a task"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } mTask.cancel(true);
這裡我們通過在每次迴圈是檢查任務是否已經被cancle掉,如果是則退出。因此對於AsyncTask我們也得注意按照正確的方式進行使用,不然也會造成程式記憶體流失的現象。
以上內容就是在使用static時,我們需要怎麼做才能最佳化記憶體的使用,當然對於以上3種情況是我們編程中使用static經常遇到的記憶體流失的情況,但仍然還有很多情況我們不易察覺到。比如:如果不做介紹,上面的第三種情況就很難察覺到,這時我們最終的記憶體流失最佳化方法就是:使用記憶體流失分析工具,在下一篇文章裡我會參照第三種情況(AsyncTask)造成的記憶體流失,通過使用MAT工具進行分析,講解MAT排除記憶體流失的使用方法。
Android記憶體最佳化之——static使用篇