前言

面試中最常問的就是:「你了解Android內存泄漏和Android內存溢出的原因嗎,請簡述一下」 ,然後大多數的人都能說出原因及其例子和解決辦法,但是實際項目中稍微不注意還是會導致內存泄漏,今天就來梳理一下那些是常見的內存泄漏寫法和解決方法。

原因

內存泄漏的原理很多人都明白,但是為了加強大家的防止內存泄漏的意識,我再來說一遍。說到內存泄漏的原理就必須要講一下Java的GC的。Java之所以這麼流行不僅僅是他面向對象編程的方式,還有一個重要的原因是因為,它能幫程序員免去釋放內存的工作,但Java並沒有我們想像的那麼智能,它進行內存清理還得依靠固定的判斷邏輯。

Java的GC可分為

引用計數演算法

給對象添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;在任何時刻計數器的值為0的對象就是不可能再被使用的,也就是可被回收的對象。這個原理容易理解並且效率很高,但是有一個致命的缺陷就是無法解決對象之間互相循環引用的問題。如下圖所示

可達性分析演算法

針對引用計數演算法的致命問題,可達性分析演算法能夠輕鬆的解決這個問題。可達性演算法是通過從GC root往外遍歷,如果從root節點無法遍歷該節點表明該節點對應的對象處於可回收狀態,如下圖中obj1、obj2、obj3、obj5都是可以從root節點出發所能到達的節點。反觀obj4、obj6、obj7卻無法從root到達,即使obj6、obj7互相循環引用但是還是屬於可回收的對象最後被jvm清理。

看了這些知識點,我們再來尋找內存泄漏的原因,Android是基於Java的一門語言,其垃圾回收機制也是基於Jvm建立的,所以說Android的GC也是通過可達性分析演算法來判定的。但是如果一個存活時間長的對象持有另一個存活時間短的對象就會導致存活時間短的對象在GC時被認定可達而不能被及時回收也就是我們常說的內存泄漏。Android對每個App內存的使用有著嚴格的限制,大量的內存泄漏就可能導致OOM,也就是在new對象請求空間時,堆中沒有剩餘的內存分配所導致的。

既然知道了原理那麼平時什麼會出現這種問題和怎麼合理的解決這種問題呢。下面來按實例說話。

內存泄漏的例子

Handler

說到Handler這個東西,大家平時肯定沒少用這玩意,但是要是用的不好就非常容易出現問題。舉個例子

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

老實說寫過代碼的人肯定很多。其中不乏了解內存泄漏原理的人。但是平時需要多的時候一不小心就可能寫下這氣人的代碼。

了解Handler機制的人都明白,但message被Handler send出去的時候,會被加入的MessageQueue中,Looper會不停的從MessageQueue中取出Message並分發執行。但是如果Activity 銷毀了,Handler發送的message沒有執行完畢。那麼Handler就不會被回收,但是由於非靜態內部類默認持有外部類的引用。Handler可達,並持有Activity實例那麼自然jvm就會錯誤的認為Activity可達不就行GC。這時我們的Activity就泄漏,Activity作為App的一個活動頁面其所佔有的內存是不容小視的。那麼怎麼才能合理的解決這個問題呢

1、使用弱引用

Java裡面的引用分為四種類型強引用、軟引用、弱引用、虛引用。如果有不明白的可以先去了解一下4種引用的區別。

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");
}
}
}

引用了弱引用就不會打擾到Activity的正常回收。但是在使用之前一定要記得判斷弱引用中包含對象是否為空,如果為空則表明表明Activity被回收不再繼續防止空指針異常

2、使用Handler.removeMessages();

知道原因就很好解決問題,Handler所導致的Activity內存泄漏正是因為Handler發送的Message任務沒有完成,所以在onDestory中可以將handler中的message都移除掉,沒有延時任務要處理,activity的生命周期就不會被延長,則可以正常銷毀。

單例所導致的內存泄漏

在Android中單例模式中經常會需要Context對象進行初始化,如下簡單的一段單例代碼示例

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(){

}

}

這樣的寫法看起來好像沒啥問題,但是一旦如下調用就會產生內存溢出

public void singleInstanceLeakcanary(){
MyHelper.getInstance(this).doSomeThing();
}

首先單例中有一個static實例,實例持有Activity,但是static變數的生命周期是整個應用的生命周期,肯定是會比單個Activity的生命周期長的,所以,當Activity finish時,activity實例被static變數持有不能釋放內存,導致內存泄漏。

解決辦法:1.使用getApplicationContext()

private void singleInstanceResolve() {
MyHelper.getInstance(getApplicationContext()).doSomeThing();
}

2.改寫單例寫法,在Application裡面進行初始化。

匿名內部類導致的異常

/**
* 匿名內部類泄漏包括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();
}

這個和Handler內部類導致的異常原理一樣就不多說了。改為靜態內部類+弱引用方式調用就行了。

靜態變數引用內部類

private static Object inner;
public void innearClassLeakcanary(){

class InnearClass{

}
inner = new InnearClass();
}

因為靜態對象引用了方法內部類,方法內部類也是持有Activity實例的,會導致Activity泄漏

解決方法就是通過在onDestory方法中置空static變數

網路請求回調介面

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) {

}
});

這是一段很普通的請求代碼,一般情況下Wifi請求很快就回調回來了,並不會導致什麼問題,但是如果是在弱網情況下就會導致介面回來緩慢,這時用戶很可能就會退出Activity不在等待,但是這時網路請求還未結束,回調介面為內部類依然會持有Activity的對象,這時Activity就內存泄漏的,並且如果是在Fragment中這樣使用不僅會內存泄漏還可能會導致奔潰,之前在公司的時候就是寫了一個Fragment,裡面包含了四個網路請求,由於平時操作的時候在Wi-Fi情況下測試很難發現在這個問題,後麵灰度的時候出現Crash,一查才之後當所附屬的Activity已經finish了,但是網路請求未完成,首先是Fragment內存泄漏,然後調用getResource的時候返回為null導致異常。這類異常的原理和非靜態內部類相同,所以可以通過static內部類+弱引用進行處理。由於本例是通過Retrofit進行,還可以在onDestory進行call.cancel進行取消任務,也可以避免內存泄漏。

RxJava非同步任務

RxJava最近很火,用的人也多,經常拿來做網路請求和一些非同步任務,但是由於RxJava的consumer或者是Observer是作為一個內部類來請求的時候,內存泄漏問題可能又隨之而來

@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");
}
});
}

這個代碼很常見,但是consumer這個為內部類,如果非同步任務沒有完成Activity依然是存在泄漏的風險的。好在RxJava有取消訂閱的方法可通過如下方法解決

@Override
protected void onDestroy() {
super.onDestroy();
if (disposable!=null && !disposable.isDisposed()){
disposable.dispose();
}
}

Toast顯示

看到這個可能有些人會驚訝,為啥Toast會導致內存泄漏,首先看一下

Toast.makeText(this,"toast",Toast.LENGTH_SHORT);

這個代碼大家都很熟悉吧,但是如果直接這麼做就可能會導致內存泄漏

,這裡傳進去了一個Context,而Toast其實是在界面上加了一個布局,Toast裡面有一個LinearLayout,這個Context就是作為LinearLayout初始化的參數,它會一直持有Activity,大家都知道Toast顯示是有時間限制的,其實也就是一個非同步的任務,最後讓其消失,但是如果在Toast還在顯示Activity就銷毀了,由於Toast顯示沒有結束不會結束生命周期,這個時候Activity就內存泄漏了。解決方法就是不要直接使用那個代碼,自己封裝一個ToastUtil,使用ApplicationContext來調用。或者通過getApplicationContext來調用,還有一種通過toast變數的cancel來取消這個顯示

private void toast(String msg){
Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show();
}

總結

看了那麼多是不是感覺其實內存泄漏的原理很簡單,變來變去其實只是形式變了,換湯不換藥。但是在編碼中不注意還是可能會出現這些問題。了解原理之後就去寫代碼吧 ??

我花了一年時間整理出一份騰訊T4級別的Android架構師全套學習資料,特別適合有3-5年以上經驗的小夥伴深入學習提升。

主要包括騰訊,以及位元組跳動,華為,小米,等一線互聯網公司主流架構技術。如果你有需要,儘管拿走好了。至於能學會多少,真的只能看你自己

全套體系化高級架構視頻;七大主流技術模塊

部分展示;java內核視頻+源碼+筆記

免費分享

點擊獲取資料文檔;

《騰訊T4級別Android架構師技術腦圖+全套視頻》

為什麼免費分享?

我不想有很多開發者朋友因為門檻而錯過這套高級架構資料,錯過提升成為架構師的可能。國內程序員千千萬,大多數是溫水煮青蛙的現狀,靠著天天加班,拿著外人以為還不錯的薪資待遇。

請記住自身技術水平才是我們的核心競爭力,千萬別把年輕和能加班當做本錢。

推薦閱讀:

相关文章