Android內(nèi)存泄漏原因及解決辦法

前言

面試中最常問(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 &amp;&amp; !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ě)代碼吧~

image
image

+qq群: 457848807。獲取以上高清技術(shù)思維圖,以及相關(guān)技術(shù)的免費(fèi)視頻學(xué)習(xí)資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容