開篇廢話
通過我之前的兩篇文章
Android性能調(diào)優(yōu)篇之探索JVM內(nèi)存分配
Android性能調(diào)優(yōu)篇之探索垃圾回收機制
我們大概了解了Java內(nèi)存的一些基本知識,這個對于本篇文章的要講的內(nèi)存泄露掉冶,還是挺有幫助的。
本來最開始就想寫關(guān)于內(nèi)存泄露的文章的荐开,由于它涉及了一些Java內(nèi)存的基本知識,所以為了鋪墊简肴,寫下了內(nèi)存分配機制以及垃圾回收的兩篇文章誓焦。
關(guān)于內(nèi)存泄露,Memory Leak,我想基本上所有開發(fā)人員都多多少少接觸過這個概念着帽,因為它確實與我們的實際開發(fā)脫不了干系杂伟。這次我講述內(nèi)存泄露的角度主要是從Android實際開發(fā)的角度。
技術(shù)詳情
1.什么是內(nèi)存泄露
所謂內(nèi)存泄露仍翰,就是指我們不再使用的對象持續(xù)占有內(nèi)存赫粥,或者這些不再使用的對象沒有辦法得到及時釋放(GC Roots依然可達),而導(dǎo)致內(nèi)存空間的浪費予借。值得注意的是越平,我們App的內(nèi)存泄露的不斷積累,最終會導(dǎo)致OOM(Out Of Memory)灵迫,更嚴(yán)重的導(dǎo)致程序崩潰秦叛,所以我們平時一定要處理內(nèi)存泄露。
2.Android中的內(nèi)存泄露
2.1 單例
我們通過代碼瀑粥,來看一下單例模式產(chǎn)生的內(nèi)存泄露挣跋。
首先是單例類OyTestManager.java(這里就不寫關(guān)于實際業(yè)務(wù)的代碼了):
import android.content.Context;
/**
* *****************************************************************
* * 文件作者:ouyangshengduo
* * 創(chuàng)建時間:2017/8/14
* * 文件描述:單例模式演示內(nèi)存泄露
* * 修改歷史:2017/8/14 21:41*************************************
**/
public class OyTestManager {
private static OyTestManager mInstance;
private Context mContext;
private OyTestManager(Context mContext){
this.mContext = mContext;
}
public static OyTestManager getmInstance(Context mContext){
if(null == mInstance){
synchronized (OyTestManager.class){
if(null == mInstance){
mInstance = new OyTestManager(mContext);
}
}
}
return mInstance;
}
}
然后再MainActivity.java里面使用這個單例:
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
private OyTestManager oyTestManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initData();
}
/**
* 數(shù)據(jù)初始化
*/
private void initData(){
oyTestManager = OyTestManager.getmInstance(this);
}
}
代碼非常簡單,這里只是為了講解我們實際開發(fā)中使用單例造成的內(nèi)存泄露狞换,通過上面的代碼避咆,我們使用Android Studio里面的Android Monitor進行內(nèi)存泄露的分析(也可以使用其他的工具,如MAT)修噪,分析步驟為:
1 將以上代碼在Android設(shè)備上跑起來查库,然后點擊返回退出軟件
2.點擊Android Studio的Android Monitor中的Initiate GC,觸發(fā)系統(tǒng)的一次GC,具體操作截圖如下:
3.然后點擊Initiate GC旁邊的Dump Java Heap,將此時的系統(tǒng)的Java堆的情況導(dǎo)出來黄琼,稍等一會樊销,會生成一個.hprof的文件
具體操作截圖如下:
4.點擊任務(wù)分析按鈕,開始分析脏款,分析結(jié)束會有一個分析結(jié)果围苫,具體查看分析結(jié)果中的內(nèi)容,看我們的軟件退出之后弛矛,是否還有資源沒有得到釋放
從以上操作中够吩,我們能夠看出,我們的MainActivity已經(jīng)退出了丈氓,但系統(tǒng)并沒有回收掉這個MainActivity,因為在MainActivity中使用了單例模式周循,mInstance這個靜態(tài)對象與MainAcitivty依然存在引用關(guān)系,從之前的內(nèi)存相關(guān)的知識可以知道万俗,mInstance在這里就可以作為一個GC Root湾笛,因為從GC Root開始進行搜索,對于MainActivity這個對象是可達的闰歪,所以嚎研,系統(tǒng)沒有回收掉這個對象。
知道了原因,我們就可以對其進行優(yōu)化了临扮。
我們知道單例的靜態(tài)特性與我們的App的生命周期是一樣長的论矾,所以,我們只需要把MainActivity的引用替換成我們的ApplictionContext,這樣杆勇,系統(tǒng)就能回收掉MainActivity對象了贪壳,以下是單例模式進行優(yōu)化后的寫法:
import android.content.Context;
/**
* *****************************************************************
* * 文件作者:ouyangshengduo
* * 創(chuàng)建時間:2017/8/14
* * 文件描述:單例模式演示內(nèi)存泄露
* * 修改歷史:2017/8/14 21:41*************************************
**/
public class OyTestManager {
private static OyTestManager mInstance;
private Context mContext;
private OyTestManager(Context mContext){
this.mContext = mContext.getApplicationContext();
}
public static OyTestManager getmInstance(Context mContext){
if(null == mInstance){
synchronized (OyTestManager.class){
if(null == mInstance){
mInstance = new OyTestManager(mContext);
}
}
}
return mInstance;
}
}
在構(gòu)造方法中this.mContext = mContext 改成了:this.mContext = mContext.getApplicationContext();
通過以上的寫法,我們再進行上面那種分析方法蚜退,就不會出現(xiàn)Leaked Activity這一項了闰靴,也就意味著,我們已經(jīng)對這個單例優(yōu)化成功了钻注。
2.2 匿名內(nèi)被類
匿名內(nèi)部類蚂且,在Java當(dāng)中,非靜態(tài)內(nèi)部類默認將會有持有外部類的引用幅恋,當(dāng)在內(nèi)部類實例化一個靜態(tài)的對象杏死,那么,這個對象將會與App的生命周期一樣長佳遣,又因為非靜態(tài)內(nèi)部類一直持有外部的MainActivity的引用识埋,導(dǎo)致MainActivity無法被回收,內(nèi)存泄露的代碼如下:
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
private OyTestManager oyTestManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initData();
}
/**
* 數(shù)據(jù)初始化
*/
private void initData(){
oyTestManager = OyTestManager.getmInstance(this);
}
//定義一個內(nèi)部類
class LeakTest{
private static final String TAG = "Just a test";
}
}
用上面的分析方法區(qū)分析零渐,同樣會出現(xiàn)Leaked Activities,也就是存在內(nèi)存泄露
這種情況窒舟,我們需要把匿名內(nèi)部類修改為靜態(tài)內(nèi)部類,靜態(tài)內(nèi)部類诵盼,這樣靜態(tài)內(nèi)部類就不會持有外部MainActivity的引用惠豺,從而不會有內(nèi)存泄露的問題,優(yōu)化后的代碼如下:
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
private OyTestManager oyTestManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initData();
}
/**
* 數(shù)據(jù)初始化
*/
private void initData(){
oyTestManager = OyTestManager.getmInstance(this);
}
//定義一個內(nèi)部類
static class LeakTest{
private static final String TAG = "Just a test";
}
}
2.3 Handler
Handler 我們應(yīng)該比較熟悉风宁,我們通常使用它來進行子線程到主線程的UI更新洁墙。不過,我們實際開發(fā)中戒财,因為Handler而造成的內(nèi)存泄漏是最常見的热监,比如說,我們平時處理一些網(wǎng)絡(luò)數(shù)據(jù)獲取的時候饮寞,會請求一個回調(diào)孝扛,然后我們會使用Handler進行處理。這個時候幽崩,如果我們沒有考慮到內(nèi)存泄露苦始,就會造成比較嚴(yán)重的問題。
首先慌申,我們平時使用Handler 都是這樣的:
private Handler mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
//業(yè)務(wù)邏輯代碼
}
};
我們來分析一下這種寫法:
1.mHandler在這里是Handler的非靜態(tài)內(nèi)部類的一個實例陌选,會持有外部類MainActiivty的引用
2.Handler的消息隊列是在Looper線程中不斷輪詢處理消息,當(dāng)我們的MainActivity退出
的時候,消息隊列中還有未處理的消息或者正在處理消息咨油,而消息隊列中的Message又持
有mHandler的實例引用您炉,而且,mHandler也持有外部類MainActivity的引用臼勉,導(dǎo)致系統(tǒng)
無法對MainActivity進行回收邻吭,而造成內(nèi)存泄露
所以,這種寫法無法保證mHandler的生命周期與MainActivity一樣宴霸,很經(jīng)常造成內(nèi)存泄露。
而正確的寫法是把Handler改成MainActivity的一個靜態(tài)內(nèi)部類膏蚓,同時在其內(nèi)部持有外部類的弱引用瓢谢,這樣就能解決好這個內(nèi)存泄露問題:
private MyHandler mHandler = new MyHandler(this);
private static class MyHandler extends Handler{
private WeakReference<Context> reference;
public MyHandler(Context context){
reference = new WeakReference<Context>(context);
}
@Override
public void handleMessage(Message msg) {
MainActivity mainActivity = (MainActivity) reference.get();
if(mainActivity != null){
//業(yè)務(wù)處理邏輯
}
}
}
2.4 盡量避免使用static變量
我們平時實際開發(fā)中,經(jīng)常會使用static的變量驮瞧,能夠在不同的類和包中使用氓扛,但我們需要知道,static變量的還是有一些坑的:
1.占用內(nèi)存论笔,系統(tǒng)一般不會進行釋放
2.當(dāng)系統(tǒng)內(nèi)存不夠用的時候采郎,會自動回收靜態(tài)內(nèi)存,這樣就有可能導(dǎo)致我們的程序訪問
某一個靜態(tài)對象的時候發(fā)生不可預(yù)測的錯誤狂魔。
3.當(dāng)Android App退出的時候蒜埋,進程并沒有馬上退出,app的一些靜態(tài)變量還存在內(nèi)存中最楷,這是不安全的整份。
因此,我們使用static變量的時候的籽孙,必須考慮好真的有沒有必要使用static模型烈评,一旦static使用的不合理,會造成大量的內(nèi)存浪費犯建。很多時候讲冠,我們可以在Application
里聲明定義全局變量,或者使用持久化數(shù)據(jù)存儲來保存全局變量适瓦。
2.5 資源未關(guān)閉造成的內(nèi)存泄漏
資源未關(guān)閉的情況竿开,這個我們平時開發(fā)中,應(yīng)該會比較重視犹菇,因為特別容易出現(xiàn)內(nèi)存泄露德迹,最終導(dǎo)致程序內(nèi)存溢出而崩潰,因為容易呈現(xiàn)揭芍,所以我們知道其必要性胳搞。
當(dāng)我們使用了BroadcastReceiver,ContentObserverr,File,Cursor,Stream,Bitmap等資源的時候,使用完一定要記得及時關(guān)閉或者銷毀。
例如肌毅,我們一般在某個Activity中register了某一個廣播BroadcastReceiver,在Activity結(jié)束的時候沒有調(diào)用unregister,這明顯就會造成內(nèi)存泄露筷转。
還有的時候使用查詢數(shù)據(jù)庫,讀取文件等一些資源型對象的時候悬而,一定要記得調(diào)用關(guān)閉的方法呜舒。
還就是Bitmap的調(diào)用了,這個東西特別占用內(nèi)存笨奠,使用完可以調(diào)用Bitmap.recycle()方法回收此對象的像素所占用的內(nèi)存袭蝗。
2.6 AsnycTask造成的內(nèi)存泄露
其實AsnycTask造成的內(nèi)存泄露的原理與Handler是一樣的,主要還是因為非靜態(tài)匿名內(nèi)部類持有外部類的引用般婆,在AsnycTask的doInBackground的方法中到腥,可能還有任務(wù)正在處理,從而導(dǎo)致外部類Activity不能被釋放蔚袍。
解決方案也可以使用靜態(tài)內(nèi)部類乡范,也可以在Activity的onDestory方法中調(diào)用cancle方法,我這里就不貼代碼了啤咽。
干貨總結(jié)
以上介紹了我們Android開發(fā)中晋辆,經(jīng)常遇到的六種內(nèi)存泄露的情況,實際上內(nèi)存泄露遠不及這六種宇整,牽扯到方方面面瓶佳,很多時候,有些內(nèi)存泄露并不能百分百的去解決没陡,需考慮一些系統(tǒng)的權(quán)衡來制定方案涩哟。關(guān)于內(nèi)存泄露,有些點在我們編碼過程中還是需要再次重申一下:
1.恰當(dāng)使用單例模式盼玄,Handler機制
2.資源對象使用完了贴彼,一定要記得關(guān)閉或者釋放掉
3.老生常談的ListView,一定要記得使用緩存convertView
4.Bitmap對象使用完了要記得調(diào)用recycle()方法來釋放底層C那一塊的內(nèi)存
5.可能的話,將Activity的相關(guān)的context,用Application的context來替代
6.集合當(dāng)中存放的對象引用埃儿,某個對象使用完了器仗,記得從這個集合當(dāng)中清理掉該引用
7.匿名內(nèi)部類也要慎用,可能的話童番,用靜態(tài)內(nèi)部類替代精钮,避免持有外部類引用而導(dǎo)致外部類無法被回收
8.熟悉內(nèi)存泄露檢查和分析的工具,如MAT,LeakCanary,Android Monitor等工具