前言
面試中最常問(wèn)的就是:“你了解Android內(nèi)存泄漏和Android內(nèi)存溢出的原因嗎,請(qǐng)簡(jiǎn)述一下” ,然后大多數(shù)的人都能說(shuō)出原因及其例子和解決辦法,但是實(shí)際項(xiàng)目中稍微不注意還是會(huì)導(dǎo)致內(nèi)存泄漏,今天就來(lái)梳理一下那些是常見(jiàn)的內(nèi)存泄漏寫(xiě)法和解決方法。
原因
內(nèi)存泄漏的原理很多人都明白,但是為了加強(qiáng)大家的防止內(nèi)存泄漏的意識(shí),我再來(lái)說(shuō)一遍。說(shuō)到內(nèi)存泄漏的原理就必須要講一下Java的GC的。Java之所以這么流行不僅僅是他面向?qū)ο缶幊痰姆绞?,還有一個(gè)重要的原因是因?yàn)?,它能幫程序員免去釋放內(nèi)存的工作,但Java并沒(méi)有我們想象的那么智能,它進(jìn)行內(nèi)存清理還得依靠固定的判斷邏輯。
Java的GC可分為
引用計(jì)數(shù)算法
給對(duì)象添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用它時(shí),計(jì)數(shù)器值就加1;當(dāng)引用失效時(shí),計(jì)數(shù)器值就減1;在任何時(shí)刻計(jì)數(shù)器的值為0的對(duì)象就是不可能再被使用的,也就是可被回收的對(duì)象。這個(gè)原理容易理解并且效率很高,但是有一個(gè)致命的缺陷就是無(wú)法解決對(duì)象之間互相循環(huán)引用的問(wèn)題。如下圖所示

可達(dá)性分析算法
針對(duì)引用計(jì)數(shù)算法的致命問(wèn)題,可達(dá)性分析算法能夠輕松的解決這個(gè)問(wèn)題。可達(dá)性算法是通過(guò)從GC root往外遍歷,如果從root節(jié)點(diǎn)無(wú)法遍歷該節(jié)點(diǎn)表明該節(jié)點(diǎn)對(duì)應(yīng)的對(duì)象處于可回收狀態(tài),如下圖中obj1、obj2、obj3、obj5都是可以從root節(jié)點(diǎn)出發(fā)所能到達(dá)的節(jié)點(diǎn)。反觀obj4、obj6、obj7卻無(wú)法從root到達(dá),即使obj6、obj7互相循環(huán)引用但是還是屬于可回收的對(duì)象最后被jvm清理。

看了這些知識(shí)點(diǎn),我們?cè)賮?lái)尋找內(nèi)存泄漏的原因,Android是基于Java的一門(mén)語(yǔ)言,其垃圾回收機(jī)制也是基于Jvm建立的,所以說(shuō)Android的GC也是通過(guò)可達(dá)性分析算法來(lái)判定的。但是如果一個(gè)存活時(shí)間長(zhǎng)的對(duì)象持有另一個(gè)存活時(shí)間短的對(duì)象就會(huì)導(dǎo)致存活時(shí)間短的對(duì)象在GC時(shí)被認(rèn)定可達(dá)而不能被及時(shí)回收也就是我們常說(shuō)的內(nèi)存泄漏。Android對(duì)每個(gè)App內(nèi)存的使用有著嚴(yán)格的限制,大量的內(nèi)存泄漏就可能導(dǎo)致OOM,也就是在new對(duì)象請(qǐng)求空間時(shí),堆中沒(méi)有剩余的內(nèi)存分配所導(dǎo)致的。
既然知道了原理那么平時(shí)什么會(huì)出現(xiàn)這種問(wèn)題和怎么合理的解決這種問(wèn)題呢。下面來(lái)按實(shí)例說(shuō)話(huà)。
內(nèi)存泄漏的例子
Handler
說(shuō)到Handler這個(gè)東西,大家平時(shí)肯定沒(méi)少用這玩意,但是要是用的不好就非常容易出現(xiàn)問(wèn)題。舉個(gè)例子
public Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
toast("handlerLeakcanary");
}
};
private void handlerLeakcanary(){
Message message = new Message();
handler.sendMessageDelayed(message,TIME);
}
老實(shí)說(shuō)寫(xiě)過(guò)代碼的人肯定很多。其中不乏了解內(nèi)存泄漏原理的人。但是平時(shí)需要多的時(shí)候一不小心就可能寫(xiě)下這氣人的代碼。
了解Handler機(jī)制的人都明白,但message被Handler send出去的時(shí)候,會(huì)被加入的MessageQueue中,Looper會(huì)不停的從MessageQueue中取出Message并分發(fā)執(zhí)行。但是如果Activity 銷(xiāo)毀了,Handler發(fā)送的message沒(méi)有執(zhí)行完畢。那么Handler就不會(huì)被回收,但是由于非靜態(tài)內(nèi)部類(lèi)默認(rèn)持有外部類(lèi)的引用。Handler可達(dá),并持有Activity實(shí)例那么自然jvm就會(huì)錯(cuò)誤的認(rèn)為Activity可達(dá)不就行GC。這時(shí)我們的Activity就泄漏,Activity作為App的一個(gè)活動(dòng)頁(yè)面其所占有的內(nèi)存是不容小視的。那么怎么才能合理的解決這個(gè)問(wèn)題呢
1、使用弱引用
Java里面的引用分為四種類(lèi)型強(qiáng)引用、軟引用、弱引用、虛引用。如果有不明白的可以先去了解一下4種引用的區(qū)別。
public static class MyHandler extends Handler{
WeakReference<ResolveLeakcanaryActivity> reference;
public MyHandler(WeakReference<ResolveLeakcanaryActivity> activity){
reference = activity;
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (reference.get()!=null){
reference.get().toast("handleMessage");
}
}
}
引用了弱引用就不會(huì)打擾到Activity的正?;厥?。但是在使用之前一定要記得判斷弱引用中包含對(duì)象是否為空,如果為空則表明表明Activity被回收不再繼續(xù)防止空指針異常
2、使用Handler.removeMessages();
知道原因就很好解決問(wèn)題,Handler所導(dǎo)致的Activity內(nèi)存泄漏正是因?yàn)镠andler發(fā)送的Message任務(wù)沒(méi)有完成,所以在onDestory中可以將handler中的message都移除掉,沒(méi)有延時(shí)任務(wù)要處理,activity的生命周期就不會(huì)被延長(zhǎng),則可以正常銷(xiāo)毀。
單例所導(dǎo)致的內(nèi)存泄漏
在Android中單例模式中經(jīng)常會(huì)需要Context對(duì)象進(jìn)行初始化,如下簡(jiǎn)單的一段單例代碼示例
public class MyHelper {
private static MyHelper myHelper;
private Context context;
private MyHelper(Context context){
this.context = context;
}
public static synchronized MyHelper getInstance(Context context){
if (myHelper == null){
myHelper = new MyHelper(context);
}
return myHelper;
}
public void doSomeThing(){
}
}
這樣的寫(xiě)法看起來(lái)好像沒(méi)啥問(wèn)題,但是一旦如下調(diào)用就會(huì)產(chǎn)生內(nèi)存溢出
public void singleInstanceLeakcanary(){
MyHelper.getInstance(this).doSomeThing();
}
首先單例中有一個(gè)static實(shí)例,實(shí)例持有Activity,但是static變量的生命周期是整個(gè)應(yīng)用的生命周期,肯定是會(huì)比單個(gè)Activity的生命周期長(zhǎng)的,所以,當(dāng)Activity finish時(shí),activity實(shí)例被static變量持有不能釋放內(nèi)存,導(dǎo)致內(nèi)存泄漏。
解決辦法:
1.使用getApplicationContext()
private void singleInstanceResolve() {
MyHelper.getInstance(getApplicationContext()).doSomeThing();
}
2.改寫(xiě)單例寫(xiě)法,在Application里面進(jìn)行初始化。
匿名內(nèi)部類(lèi)導(dǎo)致的異常
/**
* 匿名內(nèi)部類(lèi)泄漏包括Handler、Runnable、TimerTask、AsyncTask等
*/
public void anonymousClassInstanceLeakcanary(){
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(TIME);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
這個(gè)和Handler內(nèi)部類(lèi)導(dǎo)致的異常原理一樣就不多說(shuō)了。改為靜態(tài)內(nèi)部類(lèi)+弱引用方式調(diào)用就行了。
靜態(tài)變量引用內(nèi)部類(lèi)
private static Object inner;
public void innearClassLeakcanary(){
class InnearClass{
}
inner = new InnearClass();
}
因?yàn)殪o態(tài)對(duì)象引用了方法內(nèi)部類(lèi),方法內(nèi)部類(lèi)也是持有Activity實(shí)例的,會(huì)導(dǎo)致Activity泄漏
解決方法就是通過(guò)在onDestory方法中置空static變量
網(wǎng)絡(luò)請(qǐng)求回調(diào)接口
Retrofit retrofit = new Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl("http://gank.io/api/data/")
.build();
Api mApi = retrofit.create(Api.class);
Call<AndroidBean> androidBeanCall = mApi.getData(20,1);
androidBeanCall.enqueue(new Callback<AndroidBean>() {
@Override
public void onResponse(Call<AndroidBean> call, Response<AndroidBean> response) {
toast("requestLeakcanary");
}
@Override
public void onFailure(Call<AndroidBean> call, Throwable t) {
}
});
這是一段很普通的請(qǐng)求代碼,一般情況下Wifi請(qǐng)求很快就回調(diào)回來(lái)了,并不會(huì)導(dǎo)致什么問(wèn)題,但是如果是在弱網(wǎng)情況下就會(huì)導(dǎo)致接口回來(lái)緩慢,這時(shí)用戶(hù)很可能就會(huì)退出Activity不在等待,但是這時(shí)網(wǎng)絡(luò)請(qǐng)求還未結(jié)束,回調(diào)接口為內(nèi)部類(lèi)依然會(huì)持有Activity的對(duì)象,這時(shí)Activity就內(nèi)存泄漏的,并且如果是在Fragment中這樣使用不僅會(huì)內(nèi)存泄漏還可能會(huì)導(dǎo)致奔潰,之前在公司的時(shí)候就是寫(xiě)了一個(gè)Fragment,里面包含了四個(gè)網(wǎng)絡(luò)請(qǐng)求,由于平時(shí)操作的時(shí)候在Wi-Fi情況下測(cè)試很難發(fā)現(xiàn)在這個(gè)問(wèn)題,后面灰度的時(shí)候出現(xiàn)Crash,一查才之后當(dāng)所附屬的Activity已經(jīng)finish了,但是網(wǎng)絡(luò)請(qǐng)求未完成,首先是Fragment內(nèi)存泄漏,然后調(diào)用getResource的時(shí)候返回為null導(dǎo)致異常。這類(lèi)異常的原理和非靜態(tài)內(nèi)部類(lèi)相同,所以可以通過(guò)static內(nèi)部類(lèi)+弱引用進(jìn)行處理。由于本例是通過(guò)Retrofit進(jìn)行,還可以在onDestory進(jìn)行call.cancel進(jìn)行取消任務(wù),也可以避免內(nèi)存泄漏。
RxJava異步任務(wù)
RxJava最近很火,用的人也多,經(jīng)常拿來(lái)做網(wǎng)絡(luò)請(qǐng)求和一些異步任務(wù),但是由于RxJava的consumer或者是Observer是作為一個(gè)內(nèi)部類(lèi)來(lái)請(qǐng)求的時(shí)候,內(nèi)存泄漏問(wèn)題可能又隨之而來(lái)
@SuppressLint("CheckResult")
public void rxJavaLeakcanary(){
AppModel.getData()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
new Consumer<Object>() {
@Override
public void accept(Object o) throws Exception {
toast("rxJavaLeakcanary");
}
});
}
這個(gè)代碼很常見(jiàn),但是consumer這個(gè)為內(nèi)部類(lèi),如果異步任務(wù)沒(méi)有完成Activity依然是存在泄漏的風(fēng)險(xiǎn)的。好在RxJava有取消訂閱的方法可通過(guò)如下方法解決
@Override
protected void onDestroy() {
super.onDestroy();
if (disposable!=null && !disposable.isDisposed()){
disposable.dispose();
}
}
Toast顯示
看到這個(gè)可能有些人會(huì)驚訝,為啥Toast會(huì)導(dǎo)致內(nèi)存泄漏,首先看一下
Toast.makeText(this,"toast",Toast.LENGTH_SHORT);
這個(gè)代碼大家都很熟悉吧,但是如果直接這么做就可能會(huì)導(dǎo)致內(nèi)存泄漏,這里傳進(jìn)去了一個(gè)Context,而Toast其實(shí)是在界面上加了一個(gè)布局,Toast里面有一個(gè)LinearLayout,這個(gè)Context就是作為L(zhǎng)inearLayout初始化的參數(shù),它會(huì)一直持有Activity,大家都知道Toast顯示是有時(shí)間限制的,其實(shí)也就是一個(gè)異步的任務(wù),最后讓其消失,但是如果在Toast還在顯示Activity就銷(xiāo)毀了,由于Toast顯示沒(méi)有結(jié)束不會(huì)結(jié)束生命周期,這個(gè)時(shí)候Activity就內(nèi)存泄漏了。
解決方法就是不要直接使用那個(gè)代碼,自己封裝一個(gè)ToastUtil,使用ApplicationContext來(lái)調(diào)用。或者通過(guò)getApplicationContext來(lái)調(diào)用,還有一種通過(guò)toast變量的cancel來(lái)取消這個(gè)顯示
private void toast(String msg){
Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show();
}
總結(jié)
看了那么多是不是感覺(jué)其實(shí)內(nèi)存泄漏的原理很簡(jiǎn)單,變來(lái)變?nèi)テ鋵?shí)只是形式變了,換湯不換藥。但是在編碼中不注意還是可能會(huì)出現(xiàn)這些問(wèn)題。還有很多示例我沒(méi)有舉出來(lái),但是不表示就沒(méi)有其他情況了,這需要大家編程的時(shí)候自己耐心去發(fā)覺(jué),有一些View或者其他對(duì)象默認(rèn)持有Activity對(duì)象的時(shí)候,如果不小心被其他生命周期更長(zhǎng)的對(duì)象引用了,依然會(huì)導(dǎo)致內(nèi)存泄漏。并且只要有一個(gè)強(qiáng)引用應(yīng)用了Activity即使你對(duì)其他很多地方做了防泄漏處理結(jié)果還是一樣的---Activity泄漏(這就很扎心了),所以這就是一個(gè)細(xì)節(jié)與編程規(guī)范的問(wèn)題,但是可有利用一些工具來(lái)找出我們粗心寫(xiě)下的代碼,比如leakcanary內(nèi)存泄漏分析工具。找出來(lái)利用自己對(duì)原理的理解解決這些問(wèn)題是很簡(jiǎn)單的。
了解原理之后就去寫(xiě)代碼吧~

