Android內(nèi)存優(yōu)化:常見內(nèi)存泄露及優(yōu)化方案

如果一個(gè)無用對象(不需要再使用的對象)仍然被其他對象持有引用,造成該對象無法被系統(tǒng)回收咬像,以致該對象在堆中所占用的內(nèi)存單元無法被釋放而造成內(nèi)存空間浪費(fèi)算撮,這中情況就是內(nèi)存泄露。

在Android開發(fā)中县昂,一些不好的編程習(xí)慣會(huì)導(dǎo)致我們的開發(fā)的app存在內(nèi)存泄露的情況肮柜。下面介紹一些在Android開發(fā)中常見的內(nèi)存泄露場景及優(yōu)化方案。

單例導(dǎo)致內(nèi)存泄露

單例模式在Android開發(fā)中會(huì)經(jīng)常用到倒彰,但是如果使用不當(dāng)就會(huì)導(dǎo)致內(nèi)存泄露审洞。因?yàn)閱卫撵o態(tài)特性使得它的生命周期同應(yīng)用的生命周期一樣長,如果一個(gè)對象已經(jīng)沒有用處了待讳,但是單例還持有它的引用芒澜,那么在整個(gè)應(yīng)用程序的生命周期它都不能正常被回收,從而導(dǎo)致內(nèi)存泄露创淡。

public class AppSettings {

    private static AppSettings sInstance;
    private Context mContext;

    private AppSettings(Context context) {
        this.mContext = context;
    }

    public static AppSettings getInstance(Context context) {
        if (sInstance == null) {
            sInstance = new AppSettings(context);
        }
        return sInstance;
    }
}

像上面代碼中這樣的單例痴晦,如果我們在調(diào)用getInstance(Context context)方法的時(shí)候傳入的context參數(shù)是ActivityService等上下文琳彩,就會(huì)導(dǎo)致內(nèi)存泄露誊酌。

Activity為例,當(dāng)我們啟動(dòng)一個(gè)Activity露乏,并調(diào)用getInstance(Context context)方法去獲取AppSettings的單例碧浊,傳入Activity.this作為context,這樣AppSettings類的單例sInstance就持有了Activity的引用瘟仿,當(dāng)我們退出Activity時(shí)箱锐,該Activity就沒有用了,但是因?yàn)?code>sIntance作為靜態(tài)單例(在應(yīng)用程序的整個(gè)生命周期中存在)會(huì)繼續(xù)持有這個(gè)Activity的引用猾骡,導(dǎo)致這個(gè)Activity對象無法被回收釋放瑞躺,這就造成了內(nèi)存泄露敷搪。

為了避免這樣單例導(dǎo)致內(nèi)存泄露,我們可以將context參數(shù)改為全局的上下文:

private AppSettings(Context context) {
    this.mContext = context.getApplicationContext();
}

全局的上下文Application Context就是應(yīng)用程序的上下文幢哨,和單例的生命周期一樣長赡勘,這樣就避免了內(nèi)存泄漏。

單例模式對應(yīng)應(yīng)用程序的生命周期捞镰,所以我們在構(gòu)造單例的時(shí)候盡量避免使用Activity的上下文闸与,而是使用Application的上下文。

靜態(tài)變量導(dǎo)致內(nèi)存泄露

靜態(tài)變量存儲(chǔ)在方法區(qū)岸售,它的生命周期從類加載開始践樱,到整個(gè)進(jìn)程結(jié)束。一旦靜態(tài)變量初始化后凸丸,它所持有的引用只有等到進(jìn)程結(jié)束才會(huì)釋放拷邢。

比如下面這樣的情況,在Activity中為了避免重復(fù)的創(chuàng)建info屎慢,將sInfo作為靜態(tài)變量:

public class MainActivity extends AppCompatActivity {

    private static Info sInfo;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if (sInfo != null) {
            sInfo = new Info(this);
        }
    }
}

class Info {
    public Info(Activity activity) {
    }
}

Info作為Activity的靜態(tài)成員瞭稼,并且持有Activity的引用,但是sInfo作為靜態(tài)變量腻惠,生命周期肯定比Activity長环肘。所以當(dāng)Activity退出后,sInfo仍然引用了Activity集灌,Activity不能被回收悔雹,這就導(dǎo)致了內(nèi)存泄露。

在Android開發(fā)中欣喧,靜態(tài)持有很多時(shí)候都有可能因?yàn)槠涫褂玫纳芷诓灰恢露鴮?dǎo)致內(nèi)存泄露腌零,所以我們在新建靜態(tài)持有的變量的時(shí)候需要多考慮一下各個(gè)成員之間的引用關(guān)系,并且盡量少地使用靜態(tài)持有的變量续誉,以避免發(fā)生內(nèi)存泄露莱没。當(dāng)然,我們也可以在適當(dāng)?shù)臅r(shí)候講靜態(tài)量重置為null酷鸦,使其不再持有引用饰躲,這樣也可以避免內(nèi)存泄露。

非靜態(tài)內(nèi)部類導(dǎo)致內(nèi)存泄露

非靜態(tài)內(nèi)部類(包括匿名內(nèi)部類)默認(rèn)就會(huì)持有外部類的引用臼隔,當(dāng)非靜態(tài)內(nèi)部類對象的生命周期比外部類對象的生命周期長時(shí)嘹裂,就會(huì)導(dǎo)致內(nèi)存泄露。

非靜態(tài)內(nèi)部類導(dǎo)致的內(nèi)存泄露在Android開發(fā)中有一種典型的場景就是使用Handler摔握,很多開發(fā)者在使用Handler是這樣寫的:

public class MainActivity extends AppCompatActivity {

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

    private void start() {
        Message msg = Message.obtain();
        msg.what = 1;
        mHandler.sendMessage(msg);
    }

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            if (msg.what == 1) {
                // 做相應(yīng)邏輯
            }
        }
    };
}

也許有人會(huì)說寄狼,mHandler并未作為靜態(tài)變量持有Activity引用,生命周期可能不會(huì)比Activity長,應(yīng)該不一定會(huì)導(dǎo)致內(nèi)存泄露呢泊愧,顯然不是這樣的伊磺!

熟悉Handler消息機(jī)制的都知道,mHandler會(huì)作為成員變量保存在發(fā)送的消息msg中删咱,即msg持有mHandler的引用屑埋,而mHandlerActivity的非靜態(tài)內(nèi)部類實(shí)例,即mHandler持有Activity的引用痰滋,那么我們就可以理解為msg間接持有Activity的引用摘能。msg被發(fā)送后先放到消息隊(duì)列MessageQueue中,然后等待Looper的輪詢處理(MessageQueueLooper都是與線程相關(guān)聯(lián)的敲街,MessageQueueLooper引用的成員變量团搞,而Looper是保存在ThreadLocal中的)。那么當(dāng)Activity退出后多艇,msg可能仍然存在于消息對列MessageQueue中未處理或者正在處理逻恐,那么這樣就會(huì)導(dǎo)致Activity無法被回收,以致發(fā)生Activity的內(nèi)存泄露墩蔓。

通常在Android開發(fā)中如果要使用內(nèi)部類梢莽,但又要規(guī)避內(nèi)存泄露,一般都會(huì)采用靜態(tài)內(nèi)部類+弱引用的方式奸披。

public class MainActivity extends AppCompatActivity {

    private Handler mHandler;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mHandler = new MyHandler(this);
        start();
    }

    private void start() {
        Message msg = Message.obtain();
        msg.what = 1;
        mHandler.sendMessage(msg);
    }

    private static class MyHandler extends Handler {

        private WeakReference<MainActivity> activityWeakReference;

        public MyHandler(MainActivity activity) {
            activityWeakReference = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = activityWeakReference.get();
            if (activity != null) {
                if (msg.what == 1) {
                    // 做相應(yīng)邏輯
                }
            }
        }
    }
}

mHandler通過弱引用的方式持有Activity,當(dāng)GC執(zhí)行垃圾回收時(shí)涮雷,遇到Activity就會(huì)回收并釋放所占據(jù)的內(nèi)存單元阵面。這樣就不會(huì)發(fā)生內(nèi)存泄露了。

上面的做法確實(shí)避免了Activity導(dǎo)致的內(nèi)存泄露洪鸭,發(fā)送的msg不再已經(jīng)沒有持有Activity的引用了样刷,但是msg還是有可能存在消息隊(duì)列MessageQueue中,所以更好的是在Activity銷毀時(shí)就將mHandler的回調(diào)和發(fā)送的消息給移除掉览爵。

@Override
protected void onDestroy() {
    super.onDestroy();
    mHandler.removeCallbacksAndMessages(null);
}

非靜態(tài)內(nèi)部類造成內(nèi)存泄露還有一種情況就是使用Thread或者AsyncTask置鼻。

比如在Activity中直接new一個(gè)子線程Thread

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 模擬相應(yīng)耗時(shí)邏輯
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

或者直接新建AsyncTask異步任務(wù):

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... params) {
                // 模擬相應(yīng)耗時(shí)邏輯
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return null;
            }
        }.execute();
    }
}

很多初學(xué)者都會(huì)像上面這樣新建線程和異步任務(wù),殊不知這樣的寫法非常地不友好蜓竹,這種方式新建的子線程ThreadAsyncTask都是匿名內(nèi)部類對象箕母,默認(rèn)就隱式的持有外部Activity的引用,導(dǎo)致Activity內(nèi)存泄露俱济。要避免內(nèi)存泄露的話還是需要像上面Handler一樣使用靜態(tài)內(nèi)部類+弱應(yīng)用的方式(代碼就不列了嘶是,參考上面Hanlder的正確寫法)。

未取消注冊或回調(diào)導(dǎo)致內(nèi)存泄露

比如我們在Activity中注冊廣播蛛碌,如果在Activity銷毀后不取消注冊聂喇,那么這個(gè)剛播會(huì)一直存在系統(tǒng)中,同上面所說的非靜態(tài)內(nèi)部類一樣持有Activity引用蔚携,導(dǎo)致內(nèi)存泄露希太。因此注冊廣播后在Activity銷毀后一定要取消注冊克饶。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        this.registerReceiver(mReceiver, new IntentFilter());
    }

    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            // 接收到廣播需要做的邏輯
        }
    };

    @Override
    protected void onDestroy() {
        super.onDestroy();
        this.unregisterReceiver(mReceiver);
    }
}

在注冊觀察則模式的時(shí)候,如果不及時(shí)取消也會(huì)造成內(nèi)存泄露誊辉。比如使用Retrofit+RxJava注冊網(wǎng)絡(luò)請求的觀察者回調(diào)矾湃,同樣作為匿名內(nèi)部類持有外部引用,所以需要記得在不用或者銷毀的時(shí)候取消注冊芥映。

Timer和TimerTask導(dǎo)致內(nèi)存泄露

TimerTimerTask在Android中通常會(huì)被用來做一些計(jì)時(shí)或循環(huán)任務(wù)洲尊,比如實(shí)現(xiàn)無限輪播的ViewPager

public class MainActivity extends AppCompatActivity {

    private ViewPager mViewPager;
    private PagerAdapter mAdapter;
    private Timer mTimer;
    private TimerTask mTimerTask;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        init();
        mTimer.schedule(mTimerTask, 3000, 3000);
    }

    private void init() {
        mViewPager = (ViewPager) findViewById(R.id.view_pager);
        mAdapter = new ViewPagerAdapter();
        mViewPager.setAdapter(mAdapter);

        mTimer = new Timer();
        mTimerTask = new TimerTask() {
            @Override
            public void run() {
                MainActivity.this.runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        loopViewpager();
                    }
                });
            }
        };
    }

    private void loopViewpager() {
        if (mAdapter.getCount() > 0) {
            int curPos = mViewPager.getCurrentItem();
            curPos = (++curPos) % mAdapter.getCount();
            mViewPager.setCurrentItem(curPos);
        }
    }

    private void stopLoopViewPager() {
        if (mTimer != null) {
            mTimer.cancel();
            mTimer.purge();
            mTimer = null;
        }
        if (mTimerTask != null) {
            mTimerTask.cancel();
            mTimerTask = null;
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        stopLoopViewPager();
    }
}

當(dāng)我們Activity銷毀的時(shí),有可能Timer還在繼續(xù)等待執(zhí)行TimerTask奈偏,它持有Activity的引用不能被回收坞嘀,因此當(dāng)我們Activity銷毀的時(shí)候要立即cancelTimerTimerTask,以避免發(fā)生內(nèi)存泄漏惊来。

集合中的對象未清理造成內(nèi)存泄露

這個(gè)比較好理解丽涩,如果一個(gè)對象放入到ArrayListHashMap等集合中裁蚁,這個(gè)集合就會(huì)持有該對象的引用矢渊。當(dāng)我們不再需要這個(gè)對象時(shí),也并沒有將它從集合中移除枉证,這樣只要集合還在使用(而此對象已經(jīng)無用了)矮男,這個(gè)對象就造成了內(nèi)存泄露。并且如果集合被靜態(tài)引用的話室谚,集合里面那些沒有用的對象更會(huì)造成內(nèi)存泄露了毡鉴。所以在使用集合時(shí)要及時(shí)將不用的對象從集合remove,或者clear集合秒赤,以避免內(nèi)存泄漏猪瞬。

資源未關(guān)閉或釋放導(dǎo)致內(nèi)存泄露

在使用IOFile流或者Sqlite入篮、Cursor等資源時(shí)要及時(shí)關(guān)閉陈瘦。這些資源在進(jìn)行讀寫操作時(shí)通常都使用了緩沖,如果及時(shí)不關(guān)閉潮售,這些緩沖對象就會(huì)一直被占用而得不到釋放痊项,以致發(fā)生內(nèi)存泄露。因此我們在不需要使用它們的時(shí)候就及時(shí)關(guān)閉饲做,以便緩沖能及時(shí)得到釋放线婚,從而避免內(nèi)存泄露。

屬性動(dòng)畫造成內(nèi)存泄露

動(dòng)畫同樣是一個(gè)耗時(shí)任務(wù)盆均,比如在Activity中啟動(dòng)了屬性動(dòng)畫(ObjectAnimator)塞弊,但是在銷毀的時(shí)候,沒有調(diào)用cancle方法,雖然我們看不到動(dòng)畫了游沿,但是這個(gè)動(dòng)畫依然會(huì)不斷地播放下去饰抒,動(dòng)畫引用所在的控件,所在的控件引用Activity诀黍,這就造成Activity無法正常釋放袋坑。因此同樣要在Activity銷毀的時(shí)候cancel掉屬性動(dòng)畫,避免發(fā)生內(nèi)存泄漏眯勾。

@Override
protected void onDestroy() {
    super.onDestroy();
    mAnimator.cancel();
}

WebView造成內(nèi)存泄露

關(guān)于WebView的內(nèi)存泄露枣宫,因?yàn)閃ebView在加載網(wǎng)頁后會(huì)長期占用內(nèi)存而不能被釋放,因此我們在Activity銷毀后要調(diào)用它的destory()方法來銷毀它以釋放內(nèi)存吃环。

另外在查閱WebView內(nèi)存泄露相關(guān)資料時(shí)看到這種情況:

Webview下面的Callback持有Activity引用也颤,造成Webview內(nèi)存無法釋放,即使是調(diào)用了Webview.destory()等方法都無法解決問題(Android5.1之后)郁轻。

最終的解決方案是:在銷毀WebView之前需要先將WebView從父容器中移除翅娶,然后在銷毀WebView。詳細(xì)分析過程請參考這篇文章:WebView內(nèi)存泄漏解決方法好唯。

@Override
protected void onDestroy() {
    super.onDestroy();
    // 先從父控件中移除WebView
    mWebViewContainer.removeView(mWebView);
    mWebView.stopLoading();
    mWebView.getSettings().setJavaScriptEnabled(false);
    mWebView.clearHistory();
    mWebView.removeAllViews();
    mWebView.destroy();
}

總結(jié)

內(nèi)存泄露在Android內(nèi)存優(yōu)化是一個(gè)比較重要的一個(gè)方面竭沫,很多時(shí)候程序中發(fā)生了內(nèi)存泄露我們不一定就能注意到,所有在編碼的過程要養(yǎng)成良好的習(xí)慣骑篙。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蜕提,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子靶端,更是在濱河造成了極大的恐慌贯溅,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,729評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件躲查,死亡現(xiàn)場離奇詭異,居然都是意外死亡译柏,警方通過查閱死者的電腦和手機(jī)镣煮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,226評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鄙麦,“玉大人典唇,你說我怎么就攤上這事】韪” “怎么了介衔?”我有些...
    開封第一講書人閱讀 169,461評論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長骂因。 經(jīng)常有香客問我炎咖,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,135評論 1 300
  • 正文 為了忘掉前任乘盼,我火速辦了婚禮升熊,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘绸栅。我一直安慰自己级野,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,130評論 6 398
  • 文/花漫 我一把揭開白布粹胯。 她就那樣靜靜地躺著蓖柔,像睡著了一般。 火紅的嫁衣襯著肌膚如雪风纠。 梳的紋絲不亂的頭發(fā)上况鸣,一...
    開封第一講書人閱讀 52,736評論 1 312
  • 那天,我揣著相機(jī)與錄音议忽,去河邊找鬼懒闷。 笑死,一個(gè)胖子當(dāng)著我的面吹牛栈幸,可吹牛的內(nèi)容都是我干的愤估。 我是一名探鬼主播,決...
    沈念sama閱讀 41,179評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼速址,長吁一口氣:“原來是場噩夢啊……” “哼玩焰!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起芍锚,我...
    開封第一講書人閱讀 40,124評論 0 277
  • 序言:老撾萬榮一對情侶失蹤昔园,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后并炮,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體默刚,經(jīng)...
    沈念sama閱讀 46,657評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,723評論 3 342
  • 正文 我和宋清朗相戀三年逃魄,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了荤西。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,872評論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡伍俘,死狀恐怖邪锌,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情癌瘾,我是刑警寧澤觅丰,帶...
    沈念sama閱讀 36,533評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站妨退,受9級特大地震影響妇萄,放射性物質(zhì)發(fā)生泄漏蜕企。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,213評論 3 336
  • 文/蒙蒙 一嚣伐、第九天 我趴在偏房一處隱蔽的房頂上張望糖赔。 院中可真熱鬧,春花似錦轩端、人聲如沸放典。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,700評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽奋构。三九已至,卻和暖如春拱层,著一層夾襖步出監(jiān)牢的瞬間弥臼,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,819評論 1 274
  • 我被黑心中介騙來泰國打工根灯, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留径缅,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,304評論 3 379
  • 正文 我出身青樓烙肺,卻偏偏與公主長得像纳猪,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子桃笙,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,876評論 2 361

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