Android中常見(jiàn)的內(nèi)存泄漏匯總

###集合類泄漏

集合類如果僅僅有添加元素的方法,而沒(méi)有相應(yīng)的刪除機(jī)制,導(dǎo)致內(nèi)存被占用。如果這個(gè)集合類是全局性的變量 (比如類中的靜態(tài)屬性,全局性的 map 等即有靜態(tài)引用或 final 一直指向它),那么沒(méi)有相應(yīng)的刪除機(jī)制,很可能導(dǎo)致集合所占用的內(nèi)存只增不減。比如上面的典型例子就是其中一種情況,當(dāng)然實(shí)際上我們?cè)陧?xiàng)目中肯定不會(huì)寫這么 2B 的代碼,但稍不注意還是很容易出現(xiàn)這種情況,比如我們都喜歡通過(guò) HashMap 做一些緩存之類的事,這種情況就要多留一些心眼。

###單例造成的內(nèi)存泄漏

由于單例的靜態(tài)特性使得其生命周期跟應(yīng)用的生命周期一樣長(zhǎng),所以如果使用不恰當(dāng)?shù)脑?,很容易造成?nèi)存泄漏。比如下面一個(gè)典型的例子,

public class AppManager {

private static AppManager instance;

private Context context;

private AppManager(Context context) {

this.context = context;

}

public static AppManager getInstance(Context context) {

if (instance == null) {

instance = new AppManager(context);

}

return instance;

}

}

這是一個(gè)普通的單例模式,當(dāng)創(chuàng)建這個(gè)單例的時(shí)候,由于需要傳入一個(gè)Context,所以這個(gè)Context的生命周期的長(zhǎng)短至關(guān)重要:

1、如果此時(shí)傳入的是 Application 的 Context,因?yàn)?Application 的生命周期就是整個(gè)應(yīng)用的生命周期,所以這將沒(méi)有任何問(wèn)題。

2、如果此時(shí)傳入的是 Activity 的 Context,當(dāng)這個(gè) Context 所對(duì)應(yīng)的 Activity 退出時(shí),由于該 Context 的引用被單例對(duì)象所持有,其生命周期等于整個(gè)應(yīng)用程序的生命周期,所以當(dāng)前 Activity 退出時(shí)它的內(nèi)存并不會(huì)被回收,這就造成泄漏了。

正確的方式應(yīng)該改為下面這種方式:

public class AppManager {

private static AppManager instance;

private Context context;

private AppManager(Context context) {

this.context = context.getApplicationContext();// 使用Application 的context

}

public static AppManager getInstance(Context context) {

if (instance == null) {

instance = new AppManager(context);

}

return instance;

}

}

或者這樣寫,連 Context 都不用傳進(jìn)來(lái)了:

在你的 Application 中添加一個(gè)靜態(tài)方法,getContext() 返回 Application 的 context,

...

context = getApplicationContext();

...

/**

* 獲取全局的context

* @return 返回全局context對(duì)象

*/

public static Context getContext(){

return context;

}

public class AppManager {

private static AppManager instance;

private Context context;

private AppManager() {

this.context = MyApplication.getContext();// 使用Application 的context

}

public static AppManager getInstance() {

if (instance == null) {

instance = new AppManager();

}

return instance;

}

}

###匿名內(nèi)部類/非靜態(tài)內(nèi)部類和異步線程

非靜態(tài)內(nèi)部類創(chuàng)建靜態(tài)實(shí)例造成的內(nèi)存泄漏

有的時(shí)候我們可能會(huì)在啟動(dòng)頻繁的Activity中,為了避免重復(fù)創(chuàng)建相同的數(shù)據(jù)資源,可能會(huì)出現(xiàn)這種寫法:

public class MainActivity extends AppCompatActivity {

private static TestResource mResource = null;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

if(mManager == null){

mManager = new TestResource();

}

//...

}

class TestResource {

//...

}

}

這樣就在Activity內(nèi)部創(chuàng)建了一個(gè)非靜態(tài)內(nèi)部類的單例,每次啟動(dòng)Activity時(shí)都會(huì)使用該單例的數(shù)據(jù),這樣雖然避免了資源的重復(fù)創(chuàng)建,不過(guò)這種寫法卻會(huì)造成內(nèi)存泄漏,因?yàn)榉庆o態(tài)內(nèi)部類默認(rèn)會(huì)持有外部類的引用,而該非靜態(tài)內(nèi)部類又創(chuàng)建了一個(gè)靜態(tài)的實(shí)例,該實(shí)例的生命周期和應(yīng)用的一樣長(zhǎng),這就導(dǎo)致了該靜態(tài)實(shí)例一直會(huì)持有該Activity的引用,導(dǎo)致Activity的內(nèi)存資源不能正?;厥铡U_的做法為:

將該內(nèi)部類設(shè)為靜態(tài)內(nèi)部類或?qū)⒃搩?nèi)部類抽取出來(lái)封裝成一個(gè)單例,如果需要使用Context,請(qǐng)按照上面推薦的使用Application 的 Context。當(dāng)然,Application 的 context 不是萬(wàn)能的,所以也不能隨便亂用,對(duì)于有些地方則必須使用 Activity 的 Context,對(duì)于Application,Service,Activity三者的Context的應(yīng)用場(chǎng)景如下:

其中: NO1表示 Application 和 Service 可以啟動(dòng)一個(gè) Activity,不過(guò)需要?jiǎng)?chuàng)建一個(gè)新的 task 任務(wù)隊(duì)列。而對(duì)于 Dialog 而言,只有在 Activity 中才能創(chuàng)建

###匿名內(nèi)部類

android開(kāi)發(fā)經(jīng)常會(huì)繼承實(shí)現(xiàn)Activity/Fragment/View,此時(shí)如果你使用了匿名類,并被異步線程持有了,那要小心了,如果沒(méi)有任何措施這樣一定會(huì)導(dǎo)致泄露

public class MainActivity extends Activity {

...

Runnable ref1 = new MyRunable();

Runnable ref2 = new Runnable() {

@Override

public void run() {

}

};

...

}

ref1和ref2的區(qū)別是,ref2使用了匿名內(nèi)部類。我們來(lái)看看運(yùn)行時(shí)這兩個(gè)引用的內(nèi)存:

可以看到,ref1沒(méi)什么特別的。

但ref2這個(gè)匿名類的實(shí)現(xiàn)對(duì)象里面多了一個(gè)引用:

this$0這個(gè)引用指向MainActivity.this,也就是說(shuō)當(dāng)前的MainActivity實(shí)例會(huì)被ref2持有,如果將這個(gè)引用再傳入一個(gè)異步線程,此線程和此Acitivity生命周期不一致的時(shí)候,就造成了Activity的泄露。

###Handler 造成的內(nèi)存泄漏

Handler 的使用造成的內(nèi)存泄漏問(wèn)題應(yīng)該說(shuō)是最為常見(jiàn)了,很多時(shí)候我們?yōu)榱吮苊?ANR 而不在主線程進(jìn)行耗時(shí)操作,在處理網(wǎng)絡(luò)任務(wù)或者封裝一些請(qǐng)求回調(diào)等api都借助Handler來(lái)處理,但 Handler 不是萬(wàn)能的,對(duì)于 Handler 的使用代碼編寫一不規(guī)范即有可能造成內(nèi)存泄漏。另外,我們知道 Handler、Message 和 MessageQueue 都是相互關(guān)聯(lián)在一起的,萬(wàn)一 Handler 發(fā)送的 Message 尚未被處理,則該 Message 及發(fā)送它的 Handler 對(duì)象將被線程 MessageQueue 一直持有。

由于 Handler 屬于 TLS(Thread Local Storage) 變量, 生命周期和 Activity 是不一致的。因此這種實(shí)現(xiàn)方式一般很難保證跟 View 或者 Activity 的生命周期保持一致,故很容易導(dǎo)致無(wú)法正確釋放。

舉個(gè)例子:

public class SampleActivity extends Activity {

private final Handler mLeakyHandler = new Handler() {

@Override

public void handleMessage(Message msg) {

// ...

}

}

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

// Post a message and delay its execution for 10 minutes.

mLeakyHandler.postDelayed(new Runnable() {

@Override

public void run() { /* ... */ }

}, 1000 * 60 * 10);

// Go back to the previous Activity.

finish();

}

}

在該 SampleActivity 中聲明了一個(gè)延遲10分鐘執(zhí)行的消息 Message,mLeakyHandler 將其 push 進(jìn)了消息隊(duì)列 MessageQueue 里。當(dāng)該 Activity 被 finish() 掉時(shí),延遲執(zhí)行任務(wù)的 Message 還會(huì)繼續(xù)存在于主線程中,它持有該 Activity 的 Handler 引用,所以此時(shí) finish() 掉的 Activity 就不會(huì)被回收了從而造成內(nèi)存泄漏(因 Handler 為非靜態(tài)內(nèi)部類,它會(huì)持有外部類的引用,在這里就是指 SampleActivity)。

修復(fù)方法:在 Activity 中避免使用非靜態(tài)內(nèi)部類,比如上面我們將 Handler 聲明為靜態(tài)的,則其存活期跟 Activity 的生命周期就無(wú)關(guān)了。同時(shí)通過(guò)弱引用的方式引入 Activity,避免直接將 Activity 作為 context 傳進(jìn)去,見(jiàn)下面代碼:

public class SampleActivity extends Activity {

/**

* Instances of static inner classes do not hold an implicit

* reference to their outer class.

*/

private static class MyHandler extends Handler {

private final WeakReference mActivity;

public MyHandler(SampleActivity activity) {

mActivity = new WeakReference(activity);

}

@Override

public void handleMessage(Message msg) {

SampleActivity activity = mActivity.get();

if (activity != null) {

// ...

}

}

}

private final MyHandler mHandler = new MyHandler(this);

/**

* Instances of anonymous classes do not hold an implicit

* reference to their outer class when they are "static".

*/

private static final Runnable sRunnable = new Runnable() {

@Override

public void run() { /* ... */ }

};

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

// Post a message and delay its execution for 10 minutes.

mHandler.postDelayed(sRunnable, 1000 * 60 * 10);

// Go back to the previous Activity.

finish();

}

}

綜述,即推薦使用靜態(tài)內(nèi)部類 + WeakReference 這種方式。每次使用前注意判空。

前面提到了 WeakReference,所以這里就簡(jiǎn)單的說(shuō)一下 Java 對(duì)象的幾種引用類型。

Java對(duì)引用的分類有 Strong reference, SoftReference, WeakReference, PhatomReference 四種。

在Android應(yīng)用的開(kāi)發(fā)中,為了防止內(nèi)存溢出,在處理一些占用內(nèi)存大而且聲明周期較長(zhǎng)的對(duì)象時(shí)候,可以盡量應(yīng)用軟引用和弱引用技術(shù)。

軟/弱引用可以和一個(gè)引用隊(duì)列(ReferenceQueue)聯(lián)合使用,如果軟引用所引用的對(duì)象被垃圾回收器回收,Java虛擬機(jī)就會(huì)把這個(gè)軟引用加入到與之關(guān)聯(lián)的引用隊(duì)列中。利用這個(gè)隊(duì)列可以得知被回收的軟/弱引用的對(duì)象列表,從而為緩沖器清除已失效的軟/弱引用。

假設(shè)我們的應(yīng)用會(huì)用到大量的默認(rèn)圖片,比如應(yīng)用中有默認(rèn)的頭像,默認(rèn)游戲圖標(biāo)等等,這些圖片很多地方會(huì)用到。如果每次都去讀取圖片,由于讀取文件需要硬件操作,速度較慢,會(huì)導(dǎo)致性能較低。所以我們考慮將圖片緩存起來(lái),需要的時(shí)候直接從內(nèi)存中讀取。但是,由于圖片占用內(nèi)存空間比較大,緩存很多圖片需要很多的內(nèi)存,就可能比較容易發(fā)生OutOfMemory異常。這時(shí),我們可以考慮使用軟/弱引用技術(shù)來(lái)避免這個(gè)問(wèn)題發(fā)生。以下就是高速緩沖器的雛形:

首先定義一個(gè)HashMap,保存軟引用對(duì)象。

private Map > imageCache = new HashMap > ();

再來(lái)定義一個(gè)方法,保存Bitmap的軟引用到HashMap。

使用軟引用以后,在OutOfMemory異常發(fā)生之前,這些緩存的圖片資源的內(nèi)存空間可以被釋放掉的,從而避免內(nèi)存達(dá)到上限,避免Crash發(fā)生。

如果只是想避免OutOfMemory異常的發(fā)生,則可以使用軟引用。如果對(duì)于應(yīng)用的性能更在意,想盡快回收一些占用內(nèi)存比較大的對(duì)象,則可以使用弱引用。

另外可以根據(jù)對(duì)象是否經(jīng)常使用來(lái)判斷選擇軟引用還是弱引用。如果該對(duì)象可能會(huì)經(jīng)常使用的,就盡量用軟引用。如果該對(duì)象不被使用的可能性更大些,就可以用弱引用。

ok,繼續(xù)回到主題。前面所說(shuō)的,創(chuàng)建一個(gè)靜態(tài)Handler內(nèi)部類,然后對(duì) Handler 持有的對(duì)象使用弱引用,這樣在回收時(shí)也可以回收 Handler 持有的對(duì)象,但是這樣做雖然避免了 Activity 泄漏,不過(guò) Looper 線程的消息隊(duì)列中還是可能會(huì)有待處理的消息,所以我們?cè)?Activity 的 Destroy 時(shí)或者 Stop 時(shí)應(yīng)該移除消息隊(duì)列 MessageQueue 中的消息。

下面幾個(gè)方法都可以移除 Message:

public final void removeCallbacks(Runnable r);

public final void removeCallbacks(Runnable r, Object token);

public final void removeCallbacksAndMessages(Object token);

public final void removeMessages(int what);

public final void removeMessages(int what, Object object);

###盡量避免使用 static 成員變量

如果成員變量被聲明為 static,那我們都知道其生命周期將與整個(gè)app進(jìn)程生命周期一樣。

這會(huì)導(dǎo)致一系列問(wèn)題,如果你的app進(jìn)程設(shè)計(jì)上是長(zhǎng)駐內(nèi)存的,那即使app切到后臺(tái),這部分內(nèi)存也不會(huì)被釋放。按照現(xiàn)在手機(jī)app內(nèi)存管理機(jī)制,占內(nèi)存較大的后臺(tái)進(jìn)程將優(yōu)先回收,yi'wei如果此app做過(guò)進(jìn)程互保?;?,那會(huì)造成app在后臺(tái)頻繁重啟。當(dāng)手機(jī)安裝了你參與開(kāi)發(fā)的app以后一夜時(shí)間手機(jī)被消耗空了電量、流量,你的app不得不被用戶卸載或者靜默。

這里修復(fù)的方法是:

不要在類初始時(shí)初始化靜態(tài)成員。可以考慮lazy初始化。 架構(gòu)設(shè)計(jì)上要思考是否真的有必要這樣做,盡量避免。如果架構(gòu)需要這么設(shè)計(jì),那么此對(duì)象的生命周期你有責(zé)任管理起來(lái)。

###避免 override finalize()

1、finalize 方法被執(zhí)行的時(shí)間不確定,不能依賴與它來(lái)釋放緊缺的資源。時(shí)間不確定的原因是: 虛擬機(jī)調(diào)用GC的時(shí)間不確定 Finalize daemon線程被調(diào)度到的時(shí)間不確定

2、finalize 方法只會(huì)被執(zhí)行一次,即使對(duì)象被復(fù)活,如果已經(jīng)執(zhí)行過(guò)了 finalize 方法,再次被 GC 時(shí)也不會(huì)再執(zhí)行了,原因是:

含有 finalize 方法的 object 是在 new 的時(shí)候由虛擬機(jī)生成了一個(gè) finalize reference 在來(lái)引用到該Object的,而在 finalize 方法執(zhí)行的時(shí)候,該 object 所對(duì)應(yīng)的 finalize Reference 會(huì)被釋放掉,即使在這個(gè)時(shí)候把該 object 復(fù)活(即用強(qiáng)引用引用住該 object ),再第二次被 GC 的時(shí)候由于沒(méi)有了 finalize reference 與之對(duì)應(yīng),所以 finalize 方法不會(huì)再執(zhí)行。

3、含有Finalize方法的object需要至少經(jīng)過(guò)兩輪GC才有可能被釋放。

###資源未關(guān)閉造成的內(nèi)存泄漏

對(duì)于使用了BraodcastReceiver,ContentObserver,F(xiàn)ile,游標(biāo) Cursor,Stream,Bitmap等資源的使用,應(yīng)該在Activity銷毀時(shí)及時(shí)關(guān)閉或者注銷,否則這些資源將不會(huì)被回收,造成內(nèi)存泄漏。

###一些不良代碼造成的內(nèi)存壓力

有些代碼并不造成內(nèi)存泄露,但是它們,或是對(duì)沒(méi)使用的內(nèi)存沒(méi)進(jìn)行有效及時(shí)的釋放,或是沒(méi)有有效的利用已有的對(duì)象而是頻繁的申請(qǐng)新內(nèi)存。

比如: Bitmap 沒(méi)調(diào)用 recycle()方法,對(duì)于 Bitmap 對(duì)象在不使用時(shí),我們應(yīng)該先調(diào)用 recycle() 釋放內(nèi)存,然后才它設(shè)置為 null. 因?yàn)榧虞d Bitmap 對(duì)象的內(nèi)存空間,一部分是 java 的,一部分 C 的(因?yàn)?Bitmap 分配的底層是通過(guò) JNI 調(diào)用的 )。 而這個(gè) recyle() 就是針對(duì) C 部分的內(nèi)存釋放。 構(gòu)造 Adapter 時(shí),沒(méi)有使用緩存的 convertView ,每次都在創(chuàng)建新的 converView。這里推薦使用 ViewHolder。

##總結(jié)

對(duì) Activity 等組件的引用應(yīng)該控制在 Activity 的生命周期之內(nèi); 如果不能就考慮使用 getApplicationContext 或者 getApplication,以避免 Activity 被外部長(zhǎng)生命周期的對(duì)象引用而泄露。

盡量不要在靜態(tài)變量或者靜態(tài)內(nèi)部類中使用非靜態(tài)外部成員變量(包括context ),即使要使用,也要考慮適時(shí)把外部成員變量置空;也可以在內(nèi)部類中使用弱引用來(lái)引用外部類的變量。

對(duì)于生命周期比Activity長(zhǎng)的內(nèi)部類對(duì)象,并且內(nèi)部類中使用了外部類的成員變量,可以這樣做避免內(nèi)存泄漏:

將內(nèi)部類改為靜態(tài)內(nèi)部類

靜態(tài)內(nèi)部類中使用弱引用來(lái)引用外部類的成員變量

Handler 的持有的引用對(duì)象最好使用弱引用,資源釋放時(shí)也可以清空 Handler 里面的消息。比如在 Activity onStop 或者 onDestroy 的時(shí)候,取消掉該 Handler 對(duì)象的 Message和 Runnable.

在 Java 的實(shí)現(xiàn)過(guò)程中,也要考慮其對(duì)象釋放,最好的方法是在不使用某對(duì)象時(shí),顯式地將此對(duì)象賦值為 null,比如使用完Bitmap 后先調(diào)用 recycle(),再賦為null,清空對(duì)圖片等資源有直接引用或者間接引用的數(shù)組(使用 array.clear() ; array = null)等,最好遵循誰(shuí)創(chuàng)建誰(shuí)釋放的原則。

正確關(guān)閉資源,對(duì)于使用了BraodcastReceiver,ContentObserver,F(xiàn)ile,游標(biāo) Cursor,Stream,Bitmap等資源的使用,應(yīng)該在Activity銷毀時(shí)及時(shí)關(guān)閉或者注銷。

保持對(duì)對(duì)象生命周期的敏感,特別注意單例、靜態(tài)對(duì)象、全局性集合等的生命周期。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • Android 內(nèi)存泄漏總結(jié) 內(nèi)存管理的目的就是讓我們?cè)陂_(kāi)發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問(wèn)題。內(nèi)存泄漏...
    _痞子閱讀 1,654評(píng)論 0 8
  • 內(nèi)存管理的目的就是讓我們?cè)陂_(kāi)發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問(wèn)題。內(nèi)存泄漏大家都不陌生了,簡(jiǎn)單粗俗的講,...
    宇宙只有巴掌大閱讀 2,415評(píng)論 0 12
  • Android 內(nèi)存泄漏總結(jié) 內(nèi)存管理的目的就是讓我們?cè)陂_(kāi)發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問(wèn)題。內(nèi)存泄漏...
    apkcore閱讀 1,237評(píng)論 2 7
  • 內(nèi)存管理的目的就是讓我們?cè)陂_(kāi)發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問(wèn)題。內(nèi)存泄漏大家都不陌生了,簡(jiǎn)單粗俗的講,...
    DreamFish閱讀 802評(píng)論 0 5
  • 轉(zhuǎn)載來(lái)之http://blog.nimbledroid.com/2016/05/23/memory-leaks-z...
    堅(jiān)持編程_lyz閱讀 1,072評(píng)論 0 0