Android常見面試題——內存泄漏原因及解決辦法

前言

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

原因

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

Java的GC可分爲

引用計數算法

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

webp

可達性分析算法

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

webp

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

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

webp

內存泄漏的例子

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

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

webp

瞭解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();
  }

總結

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章