在Android開發(fā)中,我們經(jīng)常會使用到static來修飾我們的成員變量桩撮,其本意是為了讓多個對象共用一份空間介劫,節(jié)省內存,或者是使用單例模式,讓該類只生產(chǎn)一個實例而在整個app中使用锨推。然而在某些時候不恰當?shù)氖褂没蛘呤蔷幊痰牟灰?guī)范卻會造成了內存泄露現(xiàn)象(java上的內存泄漏指內存得不到gc的及時回收,從而造成內存占用過多的現(xiàn)象)
本文中我們主要分析的是static變量對activtiy的不恰當引用而造成的內存泄漏公壤,因為對于同一個Activity頁面一般每次打開時系統(tǒng)都會重新生成一個該activity的對象(standard模式下)换可,而每個activity對象一般都含有大量的視圖對象和bitmap對象,如果之前的activity對象不能得到及時的回收,從而就造成了內存的泄漏現(xiàn)象厦幅。
下面一邊看代碼一邊講解沾鳄。
單例模式不正確的獲取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本來就是單例,所以這樣就不會存在內存泄漏的的現(xiàn)象了玫芦。
單例模式中通過內部類持有activity對象
第一種方式內存泄漏太過與明顯浆熔,相信大家都不會犯這種錯誤,接下來要介紹的這種泄漏方式會比較不那么容易發(fā)現(xiàn)桥帆,內部類的使用造成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中老虫,非靜態(tài)的內部類的對象都是會持有指向外部類對象的引用的叶骨,因此我們將內部類對象mMyListener讓單例所持有時,由于mMyListener引用了我們的activity對象祈匙,因此造成activity對象也不能被回收了忽刽,從而出現(xiàn)內存泄漏現(xiàn)象天揖。
修改以上代碼,避免內存泄漏跪帝,在activity中添加以下代碼:`@Override
protected?void?onDestroy()?{
testManager.unregisterListener(mMyListener);
super.onDestroy();
}
AsyncTask不正確使用造成的內存泄漏
介紹完以上兩種情況的內存泄漏后今膊,我們在來看一種更加容易被忽略的內存泄漏現(xiàn)象,對于AsyncTask不正確使用造成內存泄漏的問題伞剑。mTask=newAsyncTask()
{
@Override
protected?Void?doInBackground(String...?params)?{
//doSamething..
returnnull;
}
}.execute("a?task");
一般我們在主線程中開啟一個異步任務都是通過實現(xiàn)一個內部類其繼承自AsyncTask類然后實現(xiàn)其相應的方法來完成的斑唬,那么自然的mTask就會持有對activity實例對象的引用了。查看AsyncTask的實現(xiàn)黎泣,我們會通過一個SerialExecutor串行線程池來對我們的任務進行排隊恕刘,而這個SerialExecutor對象就是一個static final的常量。
具體的引用關系是:
1.我們的任務被封裝在一個FutureTask的對象中(它充當一個runable的作用)抒倚,F(xiàn)utureTask的實現(xiàn)也是通過內部類來實現(xiàn)的褐着,因此它也為持有AsyncTask對象,而AsyncTask對象引用了activity對象托呕,因此activity對象間接的被FutureTask對象給引用了含蓉。
2.futuretask對象會被添加到一個ArrayDeque類型的任務隊列的mTasks實例中
3.mTasks任務隊列又被SerialExecutor對象所持有,剛也說了這個SerialExecutor對象是一個static final的常量镣陕。
具體AsyncTask的實現(xiàn)大家可以去參照下其源代碼谴餐,我這里就通過文字描述一下其添加任務的實現(xiàn)過程就可以了姻政,總之分析了這么多通過層層引用后我們的activity會被一個static變量所引用到呆抑。所以我們在使用AsyncTask的時候不宜在其中執(zhí)行太耗時的操作,假設activity已經(jīng)退出了汁展,然而AsyncTask里任務還沒有執(zhí)行完成或者是還在排隊等待執(zhí)行鹊碍,就會造成我們的activity對象被回收的時間延后,一段時間內內存占有率變大食绿。
解決方法在activity退出的時候應該調用cancel()函數(shù)@Override
protected?void?onDestroy()?{
//mTask.cancel(false);
mTask.cancel(true);
super.onDestroy();
}
具體cancel()里傳遞true or false依實際情況而定:
1.當我們的任務還在排隊沒有被執(zhí)行侈咕,調用cancel()無論true or false,任務會從排隊隊列中移除,即任務都不會被執(zhí)行到了器紧。
2.當我們的任務已經(jīng)開始執(zhí)行了(doInBackground被調用)耀销,傳入?yún)?shù)為false時并不會打斷doInBackground的執(zhí)行,傳入?yún)?shù)為true時铲汪,如果我們的線程處于休眠或阻塞(如:sleep,wait)狀況是會打斷其執(zhí)行熊尉。
這里具體解釋下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得以執(zhí)行
Thread.sleep(2000);
}catch(InterruptedException?e)?{
e.printStackTrace();
}
mTask.cancel(true);
在這樣的情況下我們的線程處于休眠狀態(tài)調用cancel(true)方法會打斷doInBackground的執(zhí)行——即不會看到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的執(zhí)行——現(xiàn)象:log函數(shù)一直在打印輸出掌腰。
解決方法:
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);
這里我們通過在每次循環(huán)是檢查任務是否已經(jīng)被cancle掉狰住,如果是則退出。因此對于AsyncTask我們也得注意按照正確的方式進行使用齿梁,不然也會造成程序內存泄漏的現(xiàn)象催植。
以上內容就是在使用static時肮蛹,我們需要怎么做才能優(yōu)化內存的使用,當然對于以上3種情況是我們編程中使用static經(jīng)常遇到的內存泄漏的情況创南,但仍然還有很多情況我們不易察覺到伦忠。比如:如果不做介紹,上面的第三種情況就很難察覺到稿辙,這時我們最終的內存泄漏優(yōu)化方法就是:使用內存泄漏分析工具缓苛,在下一篇文章里我會參照第三種情況(AsyncTask)造成的內存泄漏,通過使用MAT工具進行分析邓深,講解MAT排除內存泄漏的使用方法未桥。
http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/1225/3800.html