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

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

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

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

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

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

內(nèi)存泄漏是造成應用程序OOM的主要原因之一,由于Android系統(tǒng)為每個應用程序分配的內(nèi)存有限纪蜒,當一個應用中產(chǎn)生的內(nèi)存泄漏比較多時衷恭,就會導致應用所需要的內(nèi)存超系統(tǒng)為其分配的內(nèi)存限額,這就造成了內(nèi)存溢出纯续,從而導致應用Crash随珠。

特殊現(xiàn)象:有時App發(fā)生Crash退出后又會重新啟動的現(xiàn)象。原因是App雖然退出猬错,但某個位置可能還持有歡迎頁(啟動頁)SplashActivity的引用窗看。

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

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

靜態(tài)存儲區(qū):又稱方法區(qū)显沈,主要存儲全局變量和靜態(tài)變量,在整個程序運行期間都存在逢唤;

堆區(qū):保存動態(tài)產(chǎn)生的數(shù)據(jù)拉讯,如:new出來的對象和數(shù)組,在不使用的時候由JVM GC回收鳖藕;

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

2著恩、本質(zhì)原因

本該被回收的對象因為某些原因而不能被回收院尔,從而繼續(xù)停留在堆內(nèi)存中。當一個對象本該被GC回收時喉誊,但有另一個正在使用的對象持有它的引用邀摆,從而導致它不能被GC回收而停留在堆內(nèi)存中。即長生命周期的對象持有短生命周期對象的引用就很可能發(fā)生內(nèi)存泄漏.

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

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

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

問題描述:static是Java中的一個關(guān)鍵字,當用它來修飾成員變量時敷矫,那么該變量就屬于該類例获,而不是該類的實例音念。用static這個關(guān)鍵字修飾變量,使得變量的生命周期大大延長躏敢,并且訪問的時候闷愤,也極其的方便,用類名就能直接訪問件余,各個資源間傳值也極其的方便讥脐,所以它經(jīng)常被使用。但如果用它來引用一些資源耗費過的實例(Context的情況最多)啼器,這時就要謹慎對待了旬渠。

public class ClassName {

private static Context mContext;

}

以上代碼很容易出現(xiàn)內(nèi)存泄露,因為如果mContext的賦值是Activity的端壳,那么即使Activity已經(jīng)finish 后告丢,執(zhí)行onDestory函數(shù),但是由于它的對象還是被其它的類引用损谦,導致Activity依然不會被釋放岖免。如果該Activity里面再持有一些資源,同樣那些資源也沒有被釋放照捡,這個時候就會導致內(nèi)存泄露颅湘;

解決辦法:

?(1).盡量避免 Static 成員變量引用資源耗費過多的實例(如Context)。

(2).使用弱引用WeakReference代替強引用持有實例栗精。比如可以使用WeakReference mContextRef闯参。


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

問題描述:Android的單例模式使用的不恰當會造成內(nèi)存泄漏悲立。因為單例的靜態(tài)特性使得單例的生命周期和應用的生命周期一樣長鹿寨,如果一個對象已經(jīng)不需要使用了,而單例對象還持有該對象的引用薪夕,那么這個對象將不能被正辰挪荩回收,這就導致了內(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:單例的生命周期和Application的一樣長玩讳,這將沒有任何問題涩蜘;

傳入的是Activity的Context:由于該Context和Activity的生命周期一樣長(Activity間接繼承于Context)嚼贡,單例的生命周期可能大于Activity的生命周期。當這個Context所對應的Activity退出時它的內(nèi)存并不會被回收同诫,因為單例對象持有該Activity的引用粤策。

解決辦法:

Context 盡量使用Application Context,因為Application的Context的生命周期比較長误窖,引用它不會出現(xiàn)內(nèi)存泄露的問題叮盘。正確的單例應該為下面的方式:

this.context =?context.getApplicationContext();

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

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

問題描述:在啟動頻繁的Activity中,為了避免重復創(chuàng)建相同的數(shù)據(jù)資源柔吼,會在Activity內(nèi)部創(chuàng)建一個非靜態(tài)內(nèi)部類的單例毒费,每次啟動Activity時都會使用該單例的數(shù)據(jù)。若非靜態(tài)內(nèi)部類所創(chuàng)建的實例的生命周期等于應用的生命周期愈魏,會因非靜態(tài)內(nèi)部類持有外部類的引用觅玻,而導致外部類無法釋放,最終造成內(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)建了一個非靜態(tài)內(nèi)部類的單例溪厘,每次啟動Activity時都會使用該單例的數(shù)據(jù),這樣雖然避免了資源的重復創(chuàng)建牌柄,卻會造成內(nèi)存泄漏畸悬。非靜態(tài)內(nèi)部類會持有外部類的引用,而又使用非靜態(tài)內(nèi)部類創(chuàng)建靜態(tài)實例珊佣,該實例和應用的生命周期一樣長蹋宦,這就導致該靜態(tài)實例一直會持有Activity的引用,導致Activity的內(nèi)存資源不能回收咒锻。

解決辦法:

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

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

問題描述:Handler的使用造成的內(nèi)存泄漏問題應該最為常見贾惦,在處理網(wǎng)絡(luò)任務(wù)或者封裝一些請求回調(diào)等api都應該會借助Handler來處理,對于Handler的使用代碼編寫一不規(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的方式會造成內(nèi)存泄漏须板,由于mHandler是Handler的非靜態(tài)匿名內(nèi)部類的實例,所以它持有外部類Activity的引用兢卵,消息隊列MessageQueue在一個Looper線程中不斷輪詢處理消息习瑰,那么當這個Activity退出時,消息隊列中還有未處理的消息Message或者正在處理消息秽荤,而消息隊列中的Message持有mHandler實例的引用甜奄,mHandler又持有Activity的引用,所以導致該Activity的內(nèi)存資源無法及時回收窃款,引發(fā)內(nèi)存泄漏课兄。

解決辦法:

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

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("請求成功");

}

}

}

@Override

protected void onDestroy() {

super.onDestroy();

mHandler.removeCallbacksAndMessages(null);

}

}

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

問題描述:工作線程Thread類屬于非靜態(tài)內(nèi)部類/匿名內(nèi)部類玩祟,運行時默認持有外部類的引用。當工作線程運行時屿聋,若外部類MainActivity需銷毀卵凑,由于此時工作線程類實例持有外部類的引用,將使得外部類無法被垃圾回收器(GC)回收胜臊,從而造成內(nèi)存泄露勺卢。

對于線程造成的內(nèi)存泄漏,也是平時比較常見的象对,如下這兩個示例:

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)部類不持有外部類的引用黑忱,從而使得工作線程實例不會持有外部類引用。

當外部類結(jié)束生命周期時勒魔,強制結(jié)束線程--使得工作線程實例的生命周期與外部類的生命周期同步甫煞。使用靜態(tài)內(nèi)部類 & 強制結(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é)束時冠绢,強制結(jié)束線程

}

}

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

問題描述:對于資源的使用(如 廣播BraodcastReceiver抚吠、文件流File、數(shù)據(jù)庫游標Cursor弟胀、圖片資源Bitmap等)楷力,若在Activity銷毀時無及時關(guān)閉或者注銷這些資源,則這些資源將不會被回收孵户,從而造成內(nèi)存泄漏萧朝。

解決辦法:

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

//廣播BraodcastReceiver:注銷注冊

unregisterReceiver()

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

InputStream/OutputStream.close()

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

cursor.close()

//圖片資源Bitmap:當它不再被使用時,應調(diào)用recycle()回收此對象的像素所占用的內(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)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市进胯,隨后出現(xiàn)的幾起案子用爪,更是在濱河造成了極大的恐慌,老刑警劉巖龄减,帶你破解...
    沈念sama閱讀 222,590評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件项钮,死亡現(xiàn)場離奇詭異班眯,居然都是意外死亡希停,警方通過查閱死者的電腦和手機烁巫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,157評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來宠能,“玉大人亚隙,你說我怎么就攤上這事∥コ纾” “怎么了阿弃?”我有些...
    開封第一講書人閱讀 169,301評論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長羞延。 經(jīng)常有香客問我渣淳,道長,這世上最難降的妖魔是什么伴箩? 我笑而不...
    開封第一講書人閱讀 60,078評論 1 300
  • 正文 為了忘掉前任入愧,我火速辦了婚禮,結(jié)果婚禮上嗤谚,老公的妹妹穿的比我還像新娘棺蛛。我一直安慰自己,他們只是感情好巩步,可當我...
    茶點故事閱讀 69,082評論 6 398
  • 文/花漫 我一把揭開白布旁赊。 她就那樣靜靜地躺著,像睡著了一般椅野。 火紅的嫁衣襯著肌膚如雪终畅。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,682評論 1 312
  • 那天竟闪,我揣著相機與錄音声离,去河邊找鬼。 笑死瘫怜,一個胖子當著我的面吹牛术徊,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播鲸湃,決...
    沈念sama閱讀 41,155評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼赠涮,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了暗挑?” 一聲冷哼從身側(cè)響起笋除,我...
    開封第一講書人閱讀 40,098評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎炸裆,沒想到半個月后垃它,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,638評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,701評論 3 342
  • 正文 我和宋清朗相戀三年国拇,在試婚紗的時候發(fā)現(xiàn)自己被綠了洛史。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,852評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡酱吝,死狀恐怖也殖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情务热,我是刑警寧澤忆嗜,帶...
    沈念sama閱讀 36,520評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站崎岂,受9級特大地震影響捆毫,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜冲甘,卻給世界環(huán)境...
    茶點故事閱讀 42,181評論 3 335
  • 文/蒙蒙 一冻璃、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧损合,春花似錦省艳、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,674評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至律适,卻和暖如春辐烂,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背捂贿。 一陣腳步聲響...
    開封第一講書人閱讀 33,788評論 1 274
  • 我被黑心中介騙來泰國打工纠修, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人厂僧。 一個月前我還...
    沈念sama閱讀 49,279評論 3 379
  • 正文 我出身青樓扣草,卻偏偏與公主長得像,于是被迫代替她去往敵國和親颜屠。 傳聞我的和親對象是個殘疾皇子辰妙,可洞房花燭夜當晚...
    茶點故事閱讀 45,851評論 2 361