Android性能優(yōu)化(內(nèi)存泄露第一篇)

原文鏈接:https://blog.lujun.co/2015/12/22/Android%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96(%E5%86%85%E5%AD%98%E6%B3%84%E9%9C%B2%E7%AC%AC%E4%B8%80%E7%AF%87)/

首先我們關(guān)注一個(gè)內(nèi)存泄露的場(chǎng)景,相信大家都知道在Android中非靜態(tài)的內(nèi)部類或匿名內(nèi)部類都很有可能造成Context泄露。主要原因就是在某些情況下,Context的生命周期已經(jīng)走完,但是這些類的生命還未到盡頭,而他們又持有Context的引用,導(dǎo)致GC時(shí)無(wú)法回收該回收的內(nèi)存空間從而導(dǎo)致類存泄露。

上面這段話應(yīng)該不難理解,下面就用一些簡(jiǎn)單的例子說(shuō)明這個(gè)問(wèn)題。

一、普通內(nèi)部類或匿名類造成內(nèi)存泄露

public class SecondActivity extends Activity {

    private static final String TAG = "WeakReferenceTest";
    private ImageView ivTest;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.layout_2);

        ivTest = (ImageView) findViewById(R.id.image);
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test);
        ivTest.setImageBitmap(bitmap);

        // 匿名內(nèi)部類會(huì)持有外部類的引用
        final Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000 * 100);
                    Log.i(TAG, "This log is from SecondActivity!");
                }catch (InterruptedException e){

                }
            }
        });

        Button button = (Button) findViewById(R.id.btn_2);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                thread.start();
                finish();
            }
        });
    }
}

上面的代碼中,有一個(gè)匿名的Runnable類讓其所在線程sleep 100秒,在這個(gè)Activity中有一個(gè)ImageView并為其設(shè)置了一張圖片。我們連續(xù)的進(jìn)行打開(kāi)->關(guān)閉Activity這項(xiàng)操作,發(fā)現(xiàn)越到后面卡頓越嚴(yán)重。看下面兩張圖,這是某兩個(gè)時(shí)刻的內(nèi)存使用情況(一前一后):


first_time_capture.png

second_time_capture.png

可以發(fā)現(xiàn),在連續(xù)進(jìn)行上述同一操作的時(shí)候,程序內(nèi)存增大了很多!再看看Dalvikvm(4.4以上系統(tǒng)可能是ART)打印的日志:


dalvikvm_log_1.png

GC操作顯示當(dāng)前活動(dòng)對(duì)象占用的內(nèi)存越來(lái)越多,最后直至程序崩潰!這里可以肯定,我們上面寫(xiě)的代碼確實(shí)造成了內(nèi)存泄露。就是這個(gè)匿名內(nèi)部類,它持有外部Activity的引用,當(dāng)我們點(diǎn)擊Button開(kāi)啟了線程的同時(shí)結(jié)束了當(dāng)前Activvity,此時(shí)GC正要回收此Activity占用的內(nèi)存空間,發(fā)現(xiàn)還有對(duì)象持有它的引用所以無(wú)法進(jìn)行內(nèi)存回收;當(dāng)我們多次進(jìn)行打開(kāi)->關(guān)閉Activity操作的時(shí)候,就導(dǎo)致了內(nèi)存泄露,最后程序也崩了。

問(wèn)題來(lái)了,如何避免。其實(shí)這里相信大家都知道,將其聲明為靜態(tài)的就行,如下:

private static class MyRunnable implements Runnable{

    @Override
    public void run() {
        try {
            Thread.sleep(1000 * 100);
            Log.i(TAG, "This log is from SecondActivity!");
        }catch (InterruptedException e){

        }
    }
}

// 使用
final Thread thread = new Thread(new MyRunnable());

Button button = (Button) findViewById(R.id.btn_2);
button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        thread.start();
        finish();
    }
});

修改后Dalvikvm打印日志如下圖:


dalvikvm_log_2.png

程序的內(nèi)存不在一直飆升,而是穩(wěn)定在一個(gè)范圍內(nèi)。這里的主要原因就在于內(nèi)部類和靜態(tài)內(nèi)部類的區(qū)別:

  • 靜態(tài)內(nèi)部類不同于普通內(nèi)部類,它不會(huì)持有外部類的引用;而普通內(nèi)部類或匿名類則相反
  • 普通內(nèi)部類或匿名類因?yàn)槌钟型獠款惖囊茫钥梢栽L問(wèn)外部類的資源屬性成員變量等;靜態(tài)內(nèi)部類不行
  • 因?yàn)槠胀▋?nèi)部類或匿名類依賴外部類,所以必須先創(chuàng)建外部類,再創(chuàng)建普通內(nèi)部類或匿名類;而靜態(tài)內(nèi)部類隨時(shí)都可以在其他外部類中隨時(shí)創(chuàng)建

所以上面的代碼中,由于使用的是靜態(tài)內(nèi)部類,當(dāng)外部類Activity需要被GC回收內(nèi)存時(shí),Activity的引用數(shù)為0,所以能被正常回收。

二、Handler造成Context泄露

先看代碼:

public class SecondActivity extends Activity {

    private static final String TAG = "WeakReferenceTest";

    private ImageView ivTest;

    private Handler mHandler = new Handler(){

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            Log.i(TAG, msg.obj.toString());
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.layout_2);

        ivTest = (ImageView) findViewById(R.id.image);
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test);
        ivTest.setImageBitmap(bitmap);

        Message msg = mHandler.obtainMessage();
        msg.obj = "This is a message!";
        mHandler.sendMessageDelayed(msg, 1000 * 10);
        finish();
    }
}

當(dāng)我們寫(xiě)下這段代碼的時(shí)候,IDE會(huì)提示一個(gè)警告如下:

ide_error.png

提示Handler類應(yīng)該是靜態(tài)的,否則可能會(huì)發(fā)生泄露。

其實(shí)這里發(fā)生泄露和上面說(shuō)的普通/匿名內(nèi)部類是類似的。根據(jù)Android的消息機(jī)制,每個(gè)Message對(duì)象都保存著處理其Handler的引用,而在Activity中實(shí)例化一個(gè)非靜態(tài)的Handler類,此類又會(huì)持有Activity的引用;當(dāng)消息沒(méi)處理完或者需要延遲處理就結(jié)束了當(dāng)前Activity,此時(shí)Activity引用數(shù)不為0,就會(huì)造成Context泄露。問(wèn)題就是這樣,對(duì)策是不是也同樣出來(lái)了,將Handler類聲明為靜態(tài)內(nèi)部類,代碼如下:

static class MyHandler extends Handler{

    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
    }
}

警告確實(shí)沒(méi)有了,但是問(wèn)題又來(lái)了。一般情況下,我們使用Handler就是為了配合Thread進(jìn)行耗時(shí)操作然后更新UI,但是這里的Handler類是靜態(tài)內(nèi)部類,不能訪問(wèn)外部類的成員變量,怎么破!接下來(lái),就該WeakReference派上用場(chǎng)了!

Google對(duì)WeakReference介紹不多,下面是官方文檔中的介紹(以下”入隊(duì)”指將該引用加入引用隊(duì)列(Reference Queen)):

弱引用(WeakReference)是三種引用中間的一種。一旦GC判定一個(gè)對(duì)象時(shí)弱引用可到達(dá),會(huì)發(fā)生以下情況:

  • 有一組引用ref,這組引用包含以下元素:

指向該對(duì)象的所有弱引用
所有弱引用指向的軟引用/強(qiáng)引用可到達(dá)對(duì)象

  • 所有在這組ref中的引用會(huì)被自動(dòng)清除
  • 所以之前被ref引用的對(duì)象都可以被析構(gòu)(回收)
  • 在未來(lái)的某個(gè)時(shí)候,ref中所有的引用會(huì)根據(jù)自己的相應(yīng)的引用隊(duì)列(如果有)入隊(duì)
    弱引用在Map中很有用,如果一個(gè)弱引用沒(méi)有被外部任何地方引用,它就會(huì)自動(dòng)被移除。SoftReference和WeakReference的區(qū)別就在于對(duì)象被回收、引用入隊(duì)的時(shí)間點(diǎn)不同:
  • 如果一個(gè)對(duì)象是軟引用可到達(dá),那么這個(gè)對(duì)象會(huì)盡可能晚的被回收,這個(gè)引用同樣會(huì)盡可能晚的入隊(duì)。比如當(dāng)VM內(nèi)存不足時(shí)這種情形。
  • 如果一個(gè)對(duì)象被判定是弱引用可到達(dá),那么這個(gè)對(duì)象會(huì)盡快被回收,這個(gè)引用也會(huì)盡快入隊(duì)。
  • 弱引用不能阻擋GC對(duì)對(duì)象進(jìn)行回收,由GC決定引用的對(duì)象何時(shí)回收并且將對(duì)象從內(nèi)存移除
  • 使用get()方法獲取其引用的對(duì)象

介紹完了弱引用,看看我們修改后的代碼:

static class MyHandler extends Handler{

    private final WeakReference<Context> mWeakReference;

    public MyHandler(Context context){
        mWeakReference = new WeakReference<Context>(context);
    }

    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        Activity mActivity;
        if ((mActivity = (Activity)mWeakReference.get()) != null){
            // Activity operation
            // ...
        }
    }
}

這樣我們就可以在靜態(tài)內(nèi)部類中使用操作Activity。

除了弱引用(WeakReference)和上面稍微提到的軟引用(SoftReference),還有強(qiáng)引用(StrongReference)和虛引用 (PhantomReference)。

軟引用(SoftReference)

一旦GC判定一個(gè)對(duì)象時(shí)弱引用可到達(dá),會(huì)發(fā)生以下情況:

  • 有一組引用ref,這組引用包含以下元素:

指向該對(duì)象的所有弱引用
所有軟引用指向的強(qiáng)引用可到達(dá)的對(duì)象

  • 所有在這組ref中的引用會(huì)被自動(dòng)清除
  • 在同一時(shí)間或是未來(lái)的某一時(shí)間,ref中所有的引用會(huì)根據(jù)自己的相應(yīng)的引用隊(duì)列(如果有)入隊(duì)
  • 系統(tǒng)會(huì)延遲清除軟引用指向的對(duì)象,該軟引用也會(huì)延遲入隊(duì),但是再系統(tǒng)拋出OutOfMemoryError異常的時(shí)候所有的軟引用可到達(dá)的對(duì)象會(huì)被回收。當(dāng)系統(tǒng)需要回收內(nèi)存來(lái)滿足分配,軟引用可到達(dá)的對(duì)象會(huì)才會(huì)被回收,軟引用入隊(duì)。簡(jiǎn)單來(lái)說(shuō)就是軟引用阻止GC回收其指向的對(duì)象的能力相對(duì)弱引用強(qiáng)。

軟引用上面說(shuō)到了當(dāng)內(nèi)存不足時(shí)才會(huì)回收這些軟引用指向的對(duì)象,所以挺適合做緩存用。但是Google可不推薦這么做,因?yàn)楹芏嘣蛳拗屏怂`活的處理緩存相關(guān)的事情。所以關(guān)于SoftReference官方文檔提到這樣一句:Most applications should use an android.util.LruCache instead of soft references. LruCache has an effective eviction policy and lets the user tune how much memory is allotted. 所以要做緩存還是得用LruCache。

強(qiáng)引用(StrongReference)

我們使用的最多的就是強(qiáng)引用,比如一句簡(jiǎn)單的賦值代碼:

Button button = new Button(this); // 創(chuàng)建一個(gè)Button對(duì)象,并將這個(gè)對(duì)象的引用存到button中。
虛引用 (PhantomReference)

虛引用是幾類引用中最弱的一種,當(dāng)一個(gè)對(duì)象被判定是虛引用可到達(dá)時(shí),該引用就會(huì)被加入到引用隊(duì)列(也就是當(dāng)一個(gè)對(duì)象被回收之后),但是它的指向不會(huì)被清除。虛引用適合在一個(gè)對(duì)象回收前做一些清理操作,因?yàn)樗萬(wàn)inalize()方法更靈活。

關(guān)于Java中的弱引用,這篇文章(譯文)關(guān)于WeakReference寫(xiě)的很好,推薦。

參考
[Android最佳性能實(shí)踐][1]
[http://developer.android.com/reference][2]
[1]:http://blog.csdn.net/guolin_blog/article/details/42238633/
[2]:http://developer.android.com/reference

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評(píng)論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,687評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 178,640評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,957評(píng)論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,682評(píng)論 6 413
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 56,011評(píng)論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評(píng)論 3 449
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 43,183評(píng)論 0 290
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,714評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,435評(píng)論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,665評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評(píng)論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,838評(píng)論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 35,251評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 36,588評(píng)論 1 295
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,379評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,627評(píng)論 2 380

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