探索App性能優(yōu)化之Android內(nèi)存泄漏

一、內(nèi)存泄露和內(nèi)存溢出

內(nèi)存泄露(Memory Leak):是指程序在申請(qǐng)內(nèi)存后,無(wú)法釋放已申請(qǐng)的內(nèi)存空間;

內(nèi)存溢出(Out Of Memory):指程序在申請(qǐng)內(nèi)存時(shí),沒(méi)有足夠的內(nèi)存空間供其使用;即應(yīng)用程序所需內(nèi)存 超出 系統(tǒng)為其分配的內(nèi)存限額。

二者關(guān)系:內(nèi)存溢出的根本原因就是內(nèi)存泄漏,ML會(huì)導(dǎo)致OOM。

二、內(nèi)存泄露的影響

內(nèi)存泄漏是造成應(yīng)用程序OOM的主要原因之一,由于Android系統(tǒng)為每個(gè)應(yīng)用程序分配的內(nèi)存有限,當(dāng)一個(gè)應(yīng)用中產(chǎn)生的內(nèi)存泄漏比較多時(shí),就會(huì)導(dǎo)致應(yīng)用所需要的內(nèi)存超系統(tǒng)為其分配的內(nèi)存限額,這就造成了內(nèi)存溢出,從而導(dǎo)致應(yīng)用Crash。

特殊現(xiàn)象:有時(shí)App發(fā)生Crash退出后又會(huì)重新啟動(dòng)的現(xiàn)象。原因是App雖然退出,但某個(gè)位置可能還持有歡迎頁(yè)(啟動(dòng)頁(yè))SplashActivity的引用。

三、內(nèi)存泄露的原因

1、Java內(nèi)存分配策略

靜態(tài)存儲(chǔ)區(qū):又稱方法區(qū),主要存儲(chǔ)全局變量和靜態(tài)變量,在整個(gè)程序運(yùn)行期間都存在;

堆區(qū):保存動(dòng)態(tài)產(chǎn)生的數(shù)據(jù),如:new出來(lái)的對(duì)象和數(shù)組,在不使用的時(shí)候由JVM GC回收;

棧區(qū):方法體的局部變量會(huì)在棧區(qū)創(chuàng)建空間,并在方法執(zhí)行結(jié)束后會(huì)被JVM自動(dòng)釋放變量的空間和內(nèi)存;

2、本質(zhì)原因

本該被回收的對(duì)象因?yàn)槟承┰蚨荒鼙换厥眨瑥亩^續(xù)停留在堆內(nèi)存中。當(dāng)一個(gè)對(duì)象本該被GC回收時(shí),但有另一個(gè)正在使用的對(duì)象持有它的引用,從而導(dǎo)致它不能被GC回收而停留在堆內(nèi)存中。即長(zhǎng)生命周期的對(duì)象持有短生命周期對(duì)象的引用就很可能發(fā)生內(nèi)存泄漏.

四、Android內(nèi)存泄露主要原因

(一)static關(guān)鍵字修飾的成員變量

1、static修飾context造成的內(nèi)存泄漏

問(wèn)題描述:static是Java中的一個(gè)關(guān)鍵字,當(dāng)用它來(lái)修飾成員變量時(shí),那么該變量就屬于該類,而不是該類的實(shí)例。用static這個(gè)關(guān)鍵字修飾變量,使得變量的生命周期大大延長(zhǎng),并且訪問(wèn)的時(shí)候,也極其的方便,用類名就能直接訪問(wèn),各個(gè)資源間傳值也極其的方便,所以它經(jīng)常被使用。但如果用它來(lái)引用一些資源耗費(fèi)過(guò)的實(shí)例(Context的情況最多),這時(shí)就要謹(jǐn)慎對(duì)待了。

public class ClassName {

private static Context mContext;

}

以上代碼很容易出現(xiàn)內(nèi)存泄露,因?yàn)槿绻鹠Context的賦值是Activity的,那么即使Activity已經(jīng)finish 后,執(zhí)行onDestory函數(shù),但是由于它的對(duì)象還是被其它的類引用,導(dǎo)致Activity依然不會(huì)被釋放。如果該Activity里面再持有一些資源,同樣那些資源也沒(méi)有被釋放,這個(gè)時(shí)候就會(huì)導(dǎo)致內(nèi)存泄露;

解決辦法:

?(1).盡量避免 Static 成員變量引用資源耗費(fèi)過(guò)多的實(shí)例(如Context)。

(2).使用弱引用WeakReference代替強(qiáng)引用持有實(shí)例。比如可以使用WeakReference mContextRef。


2、單例造成的內(nèi)存泄露

問(wèn)題描述:Android的單例模式使用的不恰當(dāng)會(huì)造成內(nèi)存泄漏。因?yàn)閱卫撵o態(tài)特性使得單例的生命周期和應(yīng)用的生命周期一樣長(zhǎng),如果一個(gè)對(duì)象已經(jīng)不需要使用了,而單例對(duì)象還持有該對(duì)象的引用,那么這個(gè)對(duì)象將不能被正?;厥?,這就導(dǎo)致了內(nèi)存泄漏。

public class ClassB {

private static ClassB instance;

private Context context;

private ClassB(Context context) {this.context = context;}

public static ClassB getInstance(Context context) {

if (instance == null) ?{ ??instance = new ClassB(context); ? ?} ? ??return instance; ?}

}

傳入的是Application的Context:?jiǎn)卫纳芷诤虯pplication的一樣長(zhǎng),這將沒(méi)有任何問(wèn)題;

傳入的是Activity的Context:由于該Context和Activity的生命周期一樣長(zhǎng)(Activity間接繼承于Context),單例的生命周期可能大于Activity的生命周期。當(dāng)這個(gè)Context所對(duì)應(yīng)的Activity退出時(shí)它的內(nèi)存并不會(huì)被回收,因?yàn)閱卫龑?duì)象持有該Activity的引用。

解決辦法:

Context 盡量使用Application Context,因?yàn)锳pplication的Context的生命周期比較長(zhǎng),引用它不會(huì)出現(xiàn)內(nèi)存泄露的問(wèn)題。正確的單例應(yīng)該為下面的方式:

this.context =?context.getApplicationContext();

(二)非靜態(tài)內(nèi)部類/匿名類

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

問(wèn)題描述:在啟動(dòng)頻繁的Activity中,為了避免重復(fù)創(chuàng)建相同的數(shù)據(jù)資源,會(huì)在Activity內(nèi)部創(chuàng)建一個(gè)非靜態(tài)內(nèi)部類的單例,每次啟動(dòng)Activity時(shí)都會(huì)使用該單例的數(shù)據(jù)。若非靜態(tài)內(nèi)部類所創(chuàng)建的實(shí)例的生命周期等于應(yīng)用的生命周期,會(huì)因非靜態(tài)內(nèi)部類持有外部類的引用,而導(dǎo)致外部類無(wú)法釋放,最終造成內(nèi)存泄露。

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();

}

}

private class TestResource {

????//...

}

}

以上代碼就在Activity內(nèi)部創(chuàng)建了一個(gè)非靜態(tài)內(nèi)部類的單例,每次啟動(dòng)Activity時(shí)都會(huì)使用該單例的數(shù)據(jù),這樣雖然避免了資源的重復(fù)創(chuàng)建,卻會(huì)造成內(nèi)存泄漏。非靜態(tài)內(nèi)部類會(huì)持有外部類的引用,而又使用非靜態(tài)內(nèi)部類創(chuàng)建靜態(tài)實(shí)例,該實(shí)例和應(yīng)用的生命周期一樣長(zhǎng),這就導(dǎo)致該靜態(tài)實(shí)例一直會(huì)持有Activity的引用,導(dǎo)致Activity的內(nèi)存資源不能回收。

解決辦法:

1.將非靜態(tài)內(nèi)部類改為靜態(tài)內(nèi)部類(靜態(tài)內(nèi)部類默認(rèn)不持有外部類的引用)2.該內(nèi)部類抽取出來(lái)封裝成一個(gè)單例。若需使用Context,建議使用 Application 的 Context

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

問(wèn)題描述:Handler的使用造成的內(nèi)存泄漏問(wèn)題應(yīng)該最為常見(jiàn),在處理網(wǎng)絡(luò)任務(wù)或者封裝一些請(qǐng)求回調(diào)等api都應(yīng)該會(huì)借助Handler來(lái)處理,對(duì)于Handler的使用代碼編寫(xiě)一不規(guī)范即有可能造成內(nèi)存泄漏,如下示例:

public class MainActivity extends AppCompatActivity {

private Handler mHandler = new Handler() {

@Override

public void handleMessage(Message msg) {

//...doSomething

}

};

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

loadData();

}

private void loadData(){

//...request http

Message message = Message.obtain();

mHandler.sendMessage(message);

}

}

這種創(chuàng)建Handler的方式會(huì)造成內(nèi)存泄漏,由于mHandler是Handler的非靜態(tài)匿名內(nèi)部類的實(shí)例,所以它持有外部類Activity的引用,消息隊(duì)列MessageQueue在一個(gè)Looper線程中不斷輪詢處理消息,那么當(dāng)這個(gè)Activity退出時(shí),消息隊(duì)列中還有未處理的消息Message或者正在處理消息,而消息隊(duì)列中的Message持有mHandler實(shí)例的引用,mHandler又持有Activity的引用,所以導(dǎo)致該Activity的內(nèi)存資源無(wú)法及時(shí)回收,引發(fā)內(nèi)存泄漏。

解決辦法:

1.創(chuàng)建一個(gè)靜態(tài)Handler內(nèi)部類,然后對(duì)Handler持有的對(duì)象使用弱引用,這樣在回收時(shí)也可以回收Handler持有的對(duì)象,這樣雖然避免了Activity泄漏。2.Looper線程的消息隊(duì)列中還是可能會(huì)有待處理的消息,所以在Activity的Destroy時(shí)或者Stop時(shí)應(yīng)該移除消息隊(duì)列中的消息。

public class MainActivity extends AppCompatActivity {

private MyHandler mHandler;

private TextView mTextView

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

mTextView = (TextView)findViewById(R.id.textview);

mHandler = new MyHandler(this);

loadData();

}

private void loadData() {

//...request http

Message message = Message.obtain();

mHandler.sendMessage(message);

}

private static class MyHandler extends Handler {

private WeakReference reference;

public MyHandler(Context context) {

reference = new WeakReference<>(context);

}

@Override

public void handleMessage(Message msg) {

MainActivity activity = (MainActivity) reference.get();

if(activity != null){

activity.mTextView.setText("請(qǐng)求成功");

}

}

}

@Override

protected void onDestroy() {

super.onDestroy();

mHandler.removeCallbacksAndMessages(null);

}

}

3、多線程造成的內(nèi)存泄漏(AsyncTask、實(shí)現(xiàn)Runnable接口、繼承Thread類)

問(wèn)題描述:工作線程Thread類屬于非靜態(tài)內(nèi)部類/匿名內(nèi)部類,運(yùn)行時(shí)默認(rèn)持有外部類的引用。當(dāng)工作線程運(yùn)行時(shí),若外部類MainActivity需銷毀,由于此時(shí)工作線程類實(shí)例持有外部類的引用,將使得外部類無(wú)法被垃圾回收器(GC)回收,從而造成內(nèi)存泄露。

對(duì)于線程造成的內(nèi)存泄漏,也是平時(shí)比較常見(jiàn)的,如下這兩個(gè)示例:

new AsyncTask() {

@Override

protected Void doInBackground(Void... params) {

SystemClock.sleep(10000);

return null;

}

}.execute();

new Thread(new Runnable() {

@Override

public void run() {

SystemClock.sleep(10000);

}

}).start();

解決辦法:

靜態(tài)內(nèi)部類--靜態(tài)內(nèi)部類不持有外部類的引用,從而使得工作線程實(shí)例不會(huì)持有外部類引用。

當(dāng)外部類結(jié)束生命周期時(shí),強(qiáng)制結(jié)束線程--使得工作線程實(shí)例的生命周期與外部類的生命周期同步。使用靜態(tài)內(nèi)部類 & 強(qiáng)制結(jié)束線程 的方式,如下:

public class MainActivity extends AppCompatActivity {

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

new Thread(new MyRunnable()).start();

}

static class MyRunnable implements Runnable{

@Override

public void run() {

SystemClock.sleep(10000);

}

}

@Override

protected void onDestroy() {

super.onDestroy();

Thread.stop();// 外部類Activity生命周期結(jié)束時(shí),強(qiáng)制結(jié)束線程

}

}

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

問(wèn)題描述:對(duì)于資源的使用(如 廣播BraodcastReceiver、文件流File、數(shù)據(jù)庫(kù)游標(biāo)Cursor、圖片資源Bitmap等),若在Activity銷毀時(shí)無(wú)及時(shí)關(guān)閉或者注銷這些資源,則這些資源將不會(huì)被回收,從而造成內(nèi)存泄漏。

解決辦法:

在Activity銷毀時(shí)及時(shí)關(guān)閉或者注銷資源

//廣播BraodcastReceiver:注銷注冊(cè)

unregisterReceiver()

//文件流File:關(guān)閉流

InputStream/OutputStream.close()

//數(shù)據(jù)庫(kù)游標(biāo)cursor:使用后關(guān)閉游標(biāo)

cursor.close()

//圖片資源Bitmap:當(dāng)它不再被使用時(shí),應(yīng)調(diào)用recycle()回收此對(duì)象的像素所占用的內(nèi)存;最后再賦為null

Bitmap.recycle()

Bitmap = null

五、總結(jié)

以上是Android內(nèi)存泄漏的三大主要原因:

1、static關(guān)鍵字引起的內(nèi)存泄漏

2、非靜態(tài)內(nèi)部類/匿名類引起的內(nèi)存泄漏

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容