單例導(dǎo)致內(nèi)存泄漏
首先來看一下一種單例的寫法:多種單例寫法參考
public class Utils{
private static Utils mInstance;
private Context mContext;
private Utils(Context context){
this.mContext = context;
}
public static Utils getInstance(Context context){
if(mInstance == null){
Util = new Utils(context);
}
return mInstance;
}
}
以Activity為例,當(dāng)我們啟動一個Activity,并調(diào)用getInstance(Context context)Utils绣硝,傳入Activity.this作為context躺同,這樣Utils類的單例mInstance就持有了Activity的引用老赤,當(dāng)我們退出Activity時臼膏,該Activity就沒有用了浦旱,但是因為mIntance作為靜態(tài)單例(在應(yīng)用程序的整個生命周期中存在)會繼續(xù)持有這個Activity的引用直奋,導(dǎo)致這個Activity對象無法被回收釋放能庆,這就造成了內(nèi)存泄露。
為了避免這樣單例導(dǎo)致內(nèi)存泄露脚线,我們可以將context參數(shù)改為全局的上下文:
java private Utils(Context context){ this.mContext=context.getApplicationContext(); }
全局的上下文Application Context就是應(yīng)用程序的上下文搁胆,和單例的生命周期一樣長,這樣就避免了內(nèi)存泄漏邮绿。單例模式對應(yīng)應(yīng)用程序的生命周期渠旁,所以我們在構(gòu)造單例的時候盡量避免使用Activity的上下文,而是使用Application的上下文船逮。
非靜態(tài)內(nèi)部類導(dǎo)致內(nèi)存泄漏
我們都知道內(nèi)部類會持有外部類引用顾腊。 原因通過編譯查看.class文件可知,編譯器會為內(nèi)部類構(gòu)造傳入外部類實例.所以非靜態(tài)內(nèi)部類會持有外部類引用挖胃。
這樣在特定情況下就會產(chǎn)生一個問題杂靶。就是如果內(nèi)部類的生命周期比外部類的生命周期長,那么在外部類無用時酱鸭,內(nèi)部類依然持有外部類引用吗垮,導(dǎo)致外部類無法釋放,從而導(dǎo)致內(nèi)存泄漏凹髓。
Android 中典型的使用場景就是Handler.通常我們的寫法是這樣:
public class MainActivity extends AppCompatActivity{
@Override
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
start();
}
private void start(){
Messagemsg=Message.obtain();
msg.what=1;
mHandler.sendMessage(msg);
}
private Handler mHandler=new Handler(){
@Override
public void handleMessage(Message msg){
if(msg.what==1){
//做相應(yīng)邏輯
}
}
};
}
熟悉Handler消息機(jī)制的都知道烁登,mHandler會作為成員變量保存在發(fā)送的消息msg中,即msg持有mHandler的引用蔚舀,而mHandler是Activity的非靜態(tài)內(nèi)部類實例饵沧,即mHandler持有Activity的引用锨络,那么我們就可以理解為msg間接持有Activity的引用。msg被發(fā)送后先放到消息隊列MessageQueue中狼牺,然后等待Looper的輪詢處理: MessageQueue和Looper都是與線程相關(guān)聯(lián)的羡儿,MessageQueue是Looper引用的成員變量,而Looper是保存在ThreadLocal中的锁右。那么當(dāng)Activity退出后失受,msg可能仍然存在于消息對列MessageQueue中未處理或者正在處理,那么這樣就會導(dǎo)致Activity無法被回收咏瑟,以致發(fā)生Activity的內(nèi)存泄露拂到。
通常在Android開發(fā)中如果要使用內(nèi)部類,但又要規(guī)避內(nèi)存泄露码泞,一般都會采用靜態(tài)內(nèi)部類+弱引用的方式兄旬。
在Activity中創(chuàng)建靜態(tài)內(nèi)部類:
public class MainActivity extends AppcompatActivity{
private static class MyHandler extends Handler{
? ? ? ? private WeakReference<MainActivity> activityWeakReference;
? ? ? ? public MyHandler(MainActivity activity){
? ? ? ? ? ? activityWeakReference = new WeakReference<>(activity);
? ? ? ? }
? ? ? ? @Override
? ? ? ? public void handleMessage(@NonNull Message msg) {
? ? ? ? ? ? if(activityWeakReference.get() != null){
? ? ? ? ? ? ? ? if(msg.what == 1 ){
? ? ? ? ? ? ? ? ? ? //do something
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? }
? ? }
上面的做法確實避免了Activity導(dǎo)致的內(nèi)存泄露,發(fā)送的msg不再已經(jīng)沒有持有Activity的引用了余寥,但是msg還是有可能存在消息隊列MessageQueue中领铐,所以更好的是在Activity銷毀時就將mHandler的回調(diào)和發(fā)送的消息給移除掉。
@Override
protected void onDestroy(){
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}
非靜態(tài)內(nèi)部類造成內(nèi)存泄露還有一種情況就是使用Thread或者AsyncTask宋舷。 同樣類似于handler,生命周期的不一致導(dǎo)致內(nèi)存泄漏绪撵。
總之,在使用內(nèi)部類時祝蝠,要時刻注意生命周期是否一致音诈,如果不一致就要考慮是否會發(fā)生內(nèi)存泄漏的問題。
未取消注冊或回調(diào)導(dǎo)致內(nèi)存泄露
在注冊觀察則模式的時候绎狭,如果不及時取消也會造成內(nèi)存泄露细溅。比如使用Retrofit+RxJava注冊網(wǎng)絡(luò)請求的觀察者回調(diào),同樣作為匿名內(nèi)部類持有外部引用儡嘶,所以需要記得在不用或者銷毀的時候取消注冊`
靜態(tài)變量導(dǎo)致內(nèi)存泄露
靜態(tài)變量存儲在方法區(qū)喇聊,它的生命周期從類加載開始,到整個進(jìn)程結(jié)束蹦狂。一旦靜態(tài)變量初始化后誓篱,它所持有的引用只有等到進(jìn)程結(jié)束才會釋放。
盡量少地使用靜態(tài)持有的變量凯楔,以避免發(fā)生內(nèi)存泄露窜骄。當(dāng)然,我們也可以在適當(dāng)?shù)臅r候講靜態(tài)量重置為null啼辣,使其不再持有引用,這樣也可以避免內(nèi)存泄露御滩。
不要在類初始時初始化靜態(tài)成員鸥拧〉吃叮可以考慮lazy初始化。
架構(gòu)設(shè)計上要思考是否真的有必要這樣做富弦,盡量避免沟娱。如果架構(gòu)需要這么設(shè)計,那么此對象的生命周期你有責(zé)任管理起來腕柜。
集合中的對象未清理造成內(nèi)存泄露
這個比較好理解济似,如果一個對象放入到ArrayList、HashMap等集合中盏缤,這個集合就會持有該對象的引用砰蠢。當(dāng)我們不再需要這個對象時,也并沒有將它從集合中移除唉铜,這樣只要集合還在使用(而此對象已經(jīng)無用了)台舱,這個對象就造成了內(nèi)存泄露。并且如果集合被靜態(tài)引用的話潭流,集合里面那些沒有用的對象更會造成內(nèi)存泄露了竞惋。所以在使用集合時要及時將不用的對象從集合remove,或者clear集合灰嫉,以避免內(nèi)存泄漏
屬性動畫造成內(nèi)存泄露
動畫同樣是一個耗時任務(wù)拆宛,比如在Activity中啟動了屬性動畫(ObjectAnimator),但是在銷毀的時候讼撒,沒有調(diào)用cancle方法浑厚,雖然我們看不到動畫了,但是這個動畫依然會不斷地播放下去椿肩,動畫引用所在的控件瞻颂,所在的控件引用Activity,這就造成Activity無法正常釋放郑象。因此同樣要在Activity銷毀的時候cancel掉屬性動畫贡这,避免發(fā)生內(nèi)存泄漏。
資源未關(guān)閉或釋放導(dǎo)致內(nèi)存泄露
在使用IO厂榛、File流或者Sqlite盖矫、Cursor等資源時要及時關(guān)閉。這些資源在進(jìn)行讀寫操作時通常都使用了緩沖击奶,如果及時不關(guān)閉辈双,這些緩沖對象就會一直被占用而得不到釋放,以致發(fā)生內(nèi)存泄露柜砾。因此我們在不需要使用它們的時候就及時關(guān)閉湃望,以便緩沖能及時得到釋放,從而避免內(nèi)存泄露
Bitmap 沒調(diào)用 recycle()方法,對于 Bitmap 對象在不使用時,我們應(yīng)該先調(diào)用 recycle() 釋放內(nèi)存证芭,然后才它設(shè)置為 null. 因為加載 Bitmap 對象的內(nèi)存空間瞳浦,一部分是 java 的,一部分 C的(因為 Bitmap 分配的底層是通過 JNI 調(diào)用的 )废士。 而這個 recyle() 就是針對 C 部分的內(nèi)存釋放叫潦。
總結(jié)
內(nèi)存泄露在Android內(nèi)存優(yōu)化是一個比較重要的一個方面,很多時候程序中發(fā)生了內(nèi)存泄露我們不一定就能注意到官硝,所有在編碼的過程要養(yǎng)成良好的習(xí)慣矗蕊。
總結(jié)下來只要做到以下這幾點(diǎn)就能避免大多數(shù)情況的內(nèi)存泄漏:
構(gòu)造單例的時候盡量別用Activity的引用;
靜態(tài)引用時注意應(yīng)用對象的置空或者少用靜態(tài)引用氢架;
使用靜態(tài)內(nèi)部類+軟引用代替非靜態(tài)內(nèi)部類傻咖;
及時取消廣播或者觀察者注冊;
耗時任務(wù)达箍、屬性動畫在Activity銷毀時記得cancel没龙;
文件流、Cursor等資源及時關(guān)閉缎玫;
Activity銷毀時WebView的移除和銷毀(android5.1上的bug,當(dāng)Activity銷毀時硬纤,webView并沒有被真正移除掉,還持有Activity的引用)赃磨。