一、內(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)存泄漏