Android 內(nèi)存泄露

個(gè)人主頁:http://shiyiliang.cn

什么是內(nèi)存泄露

通俗的講:不在使用的對(duì)象第美,其內(nèi)存不能回收,導(dǎo)致能使用的內(nèi)存越來越少,這就是內(nèi)存泄露

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

在Android開發(fā)中院刁,最主要的原因就是生命周期長的對(duì)象,持有生命周期短對(duì)象的強(qiáng)引用

內(nèi)存泄露的例子

1. Handler內(nèi)存泄漏

Handler 的使用造成的內(nèi)存泄漏問題應(yīng)該說是最為常見了碟婆,很多時(shí)候我們?yōu)榱吮苊?ANR 而不在主線程進(jìn)行耗時(shí)操作,在處理網(wǎng)絡(luò)任務(wù)或者封裝一些請(qǐng)求回調(diào)等api都借助Handler來處理惕稻,但 Handler 不是萬能的竖共,對(duì)于 Handler 的使用代碼編寫一不規(guī)范即有可能造成內(nèi)存泄漏。另外俺祠,我們知道 Handler公给、Message 和 MessageQueue 都是相互關(guān)聯(lián)在一起的借帘,萬一 Handler 發(fā)送的 Message 尚未被處理,則該 Message 及發(fā)送它的 Handler 對(duì)象將被線程 MessageQueue 一直持有淌铐。
由于 Handler 屬于 TLS(Thread Local Storage) 變量, 生命周期和 Activity 是不一致的肺然。因此這種實(shí)現(xiàn)方式一般很難保證跟 View 或者 Activity 的生命周期保持一致,故很容易導(dǎo)致無法正確釋放腿准。

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);
        mLeakyHandler.postDelayed(new Runnable() {
            @Override
            public void run() { 
                        }
        }, 1000 * 60 * 10);
        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)

正確的寫法是:

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<SampleActivity> mActivity;

        public MyHandler(SampleActivity activity) {
            mActivity = new WeakReference<SampleActivity>(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();
    }
}

2. 單例模式導(dǎo)致的泄漏

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的生命周期的長短至關(guān)重要:

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

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ì)被回收伯诬,這就造成泄漏了晚唇。

正確的寫法是:


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 = 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;
}
}

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

非靜態(tài)內(nèi)部類創(chuàng)建靜態(tài)實(shí)例造成的內(nèi)存泄漏
public class MainActivity extends AppCompatActivity {
    private static Test mResource = null;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if(mManager == null){
            mManager = new Test();
        }
    }
    class Test {
        //// TODO: 2017-03-22
    }
}

這樣就在Activity內(nèi)部創(chuàng)建了一個(gè)非靜態(tài)內(nèi)部類的單例,每次啟動(dòng)Activity時(shí)都會(huì)使用該單例的數(shù)據(jù)盗似,這樣雖然避免了資源的重復(fù)創(chuàng)建哩陕,不過這種寫法卻會(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)用的一樣長,這就導(dǎo)致了該靜態(tài)實(shí)例一直會(huì)持有該Activity的引用接癌,導(dǎo)致Activity的內(nèi)存資源不能正承母希回收。正確的做法為:

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

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

匿名內(nèi)部類
public class MainActivity extends Activity {
    Runnable ref1 = new MyRunable();
    Runnable ref2 = new Runnable() {
        @Override
        public void run() {

        }
    };
}

ref1沒什么特別的。
但ref2這個(gè)匿名類的實(shí)現(xiàn)對(duì)象里面多了一個(gè)引用:
this$0這個(gè)引用指向MainActivity.this据某,也就是說當(dāng)前的MainActivity實(shí)例會(huì)被ref2持有橡娄,如果將這個(gè)引用再傳入一個(gè)異步線程,此線程和此Acitivity生命周期不一致的時(shí)候癣籽,就造成了Activity的泄露挽唉。

4.盡量避免使用 static 成員變量

如果成員變量被聲明為 static,那我們都知道其生命周期將與整個(gè)app進(jìn)程生命周期一樣筷狼。這會(huì)導(dǎo)致一系列問題瓶籽,如果你的app進(jìn)程設(shè)計(jì)上是長駐內(nèi)存的,那即使app切到后臺(tái)埂材,這部分內(nèi)存也不會(huì)被釋放塑顺。按照現(xiàn)在手機(jī)app內(nèi)存管理機(jī)制,占內(nèi)存較大的后臺(tái)進(jìn)程將優(yōu)先回收俏险,如果此app做過進(jìn)程互保毖暇埽活,那會(huì)造成app在后臺(tái)頻繁重啟竖独。當(dāng)手機(jī)安裝了你參與開發(fā)的app以后一夜時(shí)間手機(jī)被消耗空了電量裤唠、流量,你的app不得不被用戶卸載或者靜默莹痢。這里修復(fù)的方法是:不要在類初始時(shí)初始化靜態(tài)成員种蘸。可以考慮lazy初始化格二。架構(gòu)設(shè)計(jì)上要思考是否真的有必要這樣做劈彪,盡量避免。如果架構(gòu)需要這么設(shè)計(jì)顶猜,那么此對(duì)象的生命周期你有責(zé)任管理起來沧奴。

5.避免 override finalize()

1、finalize 方法被執(zhí)行的時(shí)間不確定长窄,不能依賴與它來釋放緊缺的資源滔吠。時(shí)間不確定的原因是:

  • 虛擬機(jī)調(diào)用GC的時(shí)間不確定
  • Finalize daemon線程被調(diào)度到的時(shí)間不確定

2纲菌、finalize 方法只會(huì)被執(zhí)行一次,即使對(duì)象被復(fù)活疮绷,如果已經(jīng)執(zhí)行過了 finalize 方法翰舌,
再次被 GC 時(shí)也不會(huì)再執(zhí)行了,原因是:含有 finalize 方法的 object 是在 new 的時(shí)候由虛擬機(jī)生成了一個(gè) finalize reference 來引用到該Object的冬骚,而在 finalize 方法執(zhí)行的時(shí)候椅贱,該 object 所對(duì)應(yīng)的 finalize Reference 會(huì)被釋放掉,即使在這個(gè)時(shí)候把該 object 復(fù)活(即用強(qiáng)引用引用住該 object ,再第二次被 GC 的時(shí)候由于沒有了 finalize reference 與之對(duì)應(yīng)只冻,所以 finalize 方法
不會(huì)再執(zhí)行庇麦。

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

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

對(duì)于使用了BraodcastReceiver山橄,ContentObserverFile舍悯,游標(biāo) Cursor航棱,StreamBitmap等資源的使用萌衬,應(yīng)該在Activity銷毀時(shí)及時(shí)關(guān)閉或者注銷饮醇,否則這些資源將不會(huì)被回收,造成內(nèi)存泄漏奄薇。

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

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

  • Bitmap沒調(diào)用recycle()方法蜘腌,對(duì)于 Bitmap 對(duì)象在不使用時(shí),我們應(yīng)該先調(diào)用 recycle() 釋放內(nèi)存沫屡,然后才它設(shè)置為 null.為加載 Bitmap 對(duì)象的內(nèi)存空間,一部分是 java 的撮珠,一部分 C 的(因?yàn)?Bitmap 分配的底層是通過 JNI 調(diào)用的 )沮脖。 而這個(gè) recyle() 就是針對(duì) C 部分的內(nèi)存釋放。
  • 構(gòu)造 Adapter 時(shí)芯急,沒有使用緩存的 convertView ,每次都在創(chuàng)建新的 converView勺届。這里推薦使用 ViewHolder。

參考:
Android 內(nèi)存泄漏總結(jié)
Android內(nèi)存泄漏的八種可能(上)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末娶耍,一起剝皮案震驚了整個(gè)濱河市免姿,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌榕酒,老刑警劉巖胚膊,帶你破解...
    沈念sama閱讀 222,183評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件故俐,死亡現(xiàn)場離奇詭異,居然都是意外死亡紊婉,警方通過查閱死者的電腦和手機(jī)药版,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來喻犁,“玉大人槽片,你說我怎么就攤上這事≈。” “怎么了筐乳?”我有些...
    開封第一講書人閱讀 168,766評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長乔妈。 經(jīng)常有香客問我蝙云,道長,這世上最難降的妖魔是什么路召? 我笑而不...
    開封第一講書人閱讀 59,854評(píng)論 1 299
  • 正文 為了忘掉前任勃刨,我火速辦了婚禮,結(jié)果婚禮上股淡,老公的妹妹穿的比我還像新娘身隐。我一直安慰自己,他們只是感情好唯灵,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評(píng)論 6 398
  • 文/花漫 我一把揭開白布贾铝。 她就那樣靜靜地躺著,像睡著了一般埠帕。 火紅的嫁衣襯著肌膚如雪垢揩。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,457評(píng)論 1 311
  • 那天敛瓷,我揣著相機(jī)與錄音叁巨,去河邊找鬼。 笑死呐籽,一個(gè)胖子當(dāng)著我的面吹牛锋勺,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播狡蝶,決...
    沈念sama閱讀 40,999評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼庶橱,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了贪惹?” 一聲冷哼從身側(cè)響起苏章,我...
    開封第一講書人閱讀 39,914評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎馍乙,沒想到半個(gè)月后布近,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體垫释,經(jīng)...
    沈念sama閱讀 46,465評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評(píng)論 3 342
  • 正文 我和宋清朗相戀三年撑瞧,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了棵譬。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,675評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡预伺,死狀恐怖订咸,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情酬诀,我是刑警寧澤脏嚷,帶...
    沈念sama閱讀 36,354評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站瞒御,受9級(jí)特大地震影響父叙,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜肴裙,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評(píng)論 3 335
  • 文/蒙蒙 一趾唱、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蜻懦,春花似錦甜癞、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至征炼,卻和暖如春析既,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背柒室。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評(píng)論 1 274
  • 我被黑心中介騙來泰國打工渡贾, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人雄右。 一個(gè)月前我還...
    沈念sama閱讀 49,091評(píng)論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像纺讲,于是被迫代替她去往敵國和親擂仍。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評(píng)論 2 360

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

  • 內(nèi)存泄露指的是該釋放的對(duì)象沒有釋放熬甚,一直被某個(gè)或某些實(shí)例特持有卻不再被使用導(dǎo)致GC不能回收逢渔。首先,我們先看看Jav...
    PeOS閱讀 681評(píng)論 0 2
  • 前言 對(duì)于內(nèi)存泄漏乡括,我想大家在開發(fā)中肯定都遇到過肃廓,只不過內(nèi)存泄漏對(duì)我們來說并不是可見的智厌,因?yàn)樗窃诙阎谢顒?dòng),而要想...
    EsonJack閱讀 897評(píng)論 1 3
  • 參考內(nèi)存泄露從入門到精通三部曲之基礎(chǔ)知識(shí)篇Android 內(nèi)存泄漏總結(jié)Android內(nèi)存泄漏研究Android內(nèi)存...
    合肥黑閱讀 441評(píng)論 0 3
  • 我的博客:http://xuyushi.github.io原文地址 [TOC] 內(nèi)存泄露 內(nèi)存泄露的定義:當(dāng)某些對(duì)...
    接地氣的二呆閱讀 1,296評(píng)論 2 23
  • 1. 概述 Java內(nèi)存泄漏指的是進(jìn)程中某些對(duì)象(垃圾對(duì)象)已經(jīng)沒有使用價(jià)值了盲赊,但是它們卻可以直接或間接地引用到g...
    Jinwong閱讀 3,138評(píng)論 0 6