在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?=newLoginManager(context);
returnmanager;
}
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?=newLoginManager(context);
returnmanager;
}
private?LoginManager(Context?context)?{
this.context?=?context.getApplicationContext();
}
修改方式也非常簡單我們單例中context不再持有Activity的context而是持有Application的context即可,因為Application本來就是單例,所以這樣就不會存在內存泄漏的的現象了。
單例模式中通過內部類持有activity對象
第一種方式內存泄漏太過與明顯,相信大家都不會犯這種錯誤,接下來要介紹的這種泄漏方式會比較不那么容易發現,內部類的使用造成activity對象被單例持有。
還是看代碼再分析,下面是一個單例的類:
25public?class?TestManager?{
public?static?final?TestManager?INSTANCE?=newTestManager();
private?List?mListenerList;
private?TestManager()?{
mListenerList?=newArrayList();
}
public?static?TestManager?getInstance()?{
returnINSTANCE;
}
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=newMyListener()?{
@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中添加以下代碼:`@Override
protected?void?onDestroy()?{
testManager.unregisterListener(mMyListener);
super.onDestroy();
}
AsyncTask不正確使用造成的內存泄漏
介紹完以上兩種情況的內存泄漏后,我們在來看一種更加容易被忽略的內存泄漏現象,對于AsyncTask不正確使用造成內存泄漏的問題。mTask=newAsyncTask()
{
@Override
protected?Void?doInBackground(String...?params)?{
//doSamething..
returnnull;
}
}.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()函數@Override
protected?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=newAsyncTask()
{
@Override
protected?Void?doInBackground(String...?params)?{
try{
Thread.sleep(10000);
}catch(InterruptedException?e)?{
e.printStackTrace();
}
Log.d("test","task?is?running");
returnnull;
}
}.execute("a?task");
try{
//保證task得以執行
Thread.sleep(2000);
}catch(InterruptedException?e)?{
e.printStackTrace();
}
mTask.cancel(true);
在這樣的情況下我們的線程處于休眠狀態調用cancel(true)方法會打斷doInBackground的執行——即不會看到log語句的輸出。
但在下面的這種情況的時候卻打斷不了
18mTask=newAsyncTask()
{
@Override
protected?Void?doInBackground(String...?params)?{
Boolean?loop=true;
while(loop)?{
Log.d("test","task?is?running");
}
returnnull;
}
}.execute("a?task");
try{
Thread.sleep(2000);
}catch(InterruptedException?e)?{
e.printStackTrace();
}
mTask.cancel(true);
由于我們的線程不處于等待或休眠的狀況及時調用cancel(true)也不能打斷doInBackground的執行——現象:log函數一直在打印輸出。
解決方法:
20mTask=newAsyncTask()
{
@Override
protected?Void?doInBackground(String...?params)?{
//doSomething..
Boolean?loop=true;
while(loop)?{
if(isCancelled())
returnnull;
Log.d("test","task?is?running");
}
returnnull;
}
}.execute("a?task");
try{
Thread.sleep(2000);
}catch(InterruptedException?e)?{
e.printStackTrace();
}
mTask.cancel(true);
這里我們通過在每次循環是檢查任務是否已經被cancle掉,如果是則退出。因此對于AsyncTask我們也得注意按照正確的方式進行使用,不然也會造成程序內存泄漏的現象。
以上內容就是在使用static時,我們需要怎么做才能優化內存的使用,當然對于以上3種情況是我們編程中使用static經常遇到的內存泄漏的情況,但仍然還有很多情況我們不易察覺到。比如:如果不做介紹,上面的第三種情況就很難察覺到,這時我們最終的內存泄漏優化方法就是:使用內存泄漏分析工具,在下一篇文章里我會參照第三種情況(AsyncTask)造成的內存泄漏,通過使用MAT工具進行分析,講解MAT排除內存泄漏的使用方法。
http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/1225/3800.html