android內(nèi)存優(yōu)化之Leakcanary淺談

1.Java內(nèi)存概要

在java內(nèi)存模型中,一般分為5個部分,棧(stack),堆(heap),方法區(qū)(method),本地方法區(qū)(native method),程序計(jì)數(shù)器。其實(shí)我們比較熟悉是棧,堆和方法區(qū)。

主要存儲基本數(shù)據(jù)類型和引用類型

主要存儲對象類型,一個虛擬機(jī)只有一個,所有線程共享,由虛擬機(jī)GC管理

方法區(qū)

又稱靜態(tài)區(qū),主要存儲class對象和靜態(tài)變量,一個虛擬機(jī)只有一個,所有線程是共享

2.內(nèi)存泄漏的原因與后果

對象有自己的生命周期,大部分情況都是比較短暫的,當(dāng)對象的生命周期結(jié)束后,由于一些特殊原因?qū)е略搶ο鬅o法被虛擬機(jī)回收,這就是內(nèi)存泄漏。當(dāng)內(nèi)存泄漏到了一定數(shù)值導(dǎo)致應(yīng)用占用的內(nèi)存達(dá)到閥值,這時虛擬機(jī)就會報出OutOfMenoryError的錯誤,從而程序崩潰。在android平臺中,由于移動端的特點(diǎn),這個問題將會更加明顯。

3.android常見的內(nèi)存泄漏情景

單例模式的內(nèi)存泄漏

單例對象的生命周期一般是從第一次創(chuàng)建使用到應(yīng)用結(jié)束,雖然不能完全和應(yīng)用的生命周期保持一致,但是也是和應(yīng)用同步消亡的。而在android中使用單例中,往往會把上下文對象(Context)傳給單例對象,一般會把Activity對象傳進(jìn)去,這時就產(chǎn)生了矛盾,兩者的生命周期是不一致的,這種不一致就會導(dǎo)致Activity對象onDestroy以后不能被虛擬機(jī)回收。處理的方法簡單,只需要調(diào)用Context的getApplicationContext,把對象轉(zhuǎn)稱application的上下文對象,這樣兩者消亡的生命周期就一致了,就不會內(nèi)存泄漏了。(注:這里不用考慮單例的寫法,不管是餓漢,懶漢,以及靜態(tài)單例或者枚舉寫法都是一樣)
修改前代碼:

public class Singleton {
    private static Singleton instacne = null;
    private Context mContext;

    private Singleton(){}

    public static Singleton getInstacne(Context context){
        if (instacne == null)
            instacne = new Singleton();
        instacne.mContext = context;
        return instacne;
    }
}

修改后代碼:

public class Singleton {
    private static Singleton instacne = null;
    private Context mContext;

    private Singleton(){}

    public static Singleton getInstacne(Context context){
        if (instacne == null)
            instacne = new Singleton();
        instacne.mContext = context.getApplicationContext();
        return instacne;
    }
}

非靜態(tài)內(nèi)部類創(chuàng)建靜態(tài)變量

非靜態(tài)內(nèi)部類會隱式持有外部類的實(shí)例,如果用非靜態(tài)內(nèi)部類創(chuàng)建靜態(tài)變量就會導(dǎo)致外部類的實(shí)例不能被及時回收。打個比方,如果這個外部類是Activity,非靜態(tài)內(nèi)部類是Adapter,在Activity中有個Adapter類型的靜態(tài)變量mAdapter,Adapter隱式持有Activity實(shí)例,mAdapter由于是靜態(tài)的,將和應(yīng)用消失的生命周期一致,當(dāng)Actviity銷毀后所占有的內(nèi)存就不會被虛擬機(jī)回收造成類內(nèi)存泄漏。修改的方式也比較簡單:1.把內(nèi)部類修飾成靜態(tài)的,2.或者把Adapter抽出來寫。
修改前

public class MainActivity extends Activity {
    private static MyAdapter mAdapter;
......
}
class MyAdapter extends BaseAdapter{
 ......
}

修改后

public class MainActivity extends Activity {
    private static MyAdapter mAdapter;
......
}
static class MyAdapter extends BaseAdapter{
 ......
}

使用Handler的內(nèi)存泄漏

handler的內(nèi)存泄漏場景本質(zhì)上和第二種是一樣的,所以原因也是大同小異的,先看一段代碼

public class MainActivity extends Activity {
    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };
}

這段代碼是不是非常熟悉,很多時候我們代碼就是這么寫的,一般情況下,這段代碼也是不會產(chǎn)生內(nèi)存泄漏的,但是出現(xiàn)下面的場景時就會內(nèi)存泄漏。

public class MainActivity extends Activity {
    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };
  
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Message msg = Message.obtain();
                mHandler.sendMessageDelayed(msg,1000*60*5);
                finish();
            }
        });
    }
}

問題代碼的邏輯是點(diǎn)擊按鈕通過handler發(fā)送一條需要延遲5分鐘的message到消息隊(duì)列,然后退出當(dāng)前activity。那為什么這樣就會引起內(nèi)存泄漏呢?由于mHandler是非靜態(tài)的變量,他持有了Activity的實(shí)例,在handler,messageQueue,Loop機(jī)制中,handler既是發(fā)送者又是消費(fèi)者,所以當(dāng)handler在某些業(yè)務(wù)場景下不能跟activity生命周期產(chǎn)生同步就會引起actviity的內(nèi)存泄漏。修改的方式跟第二種類型,要將mHandler修飾成static。

線程的內(nèi)存泄漏

這種場景和第2,3情況是類似的,就是線程持有當(dāng)前activity的實(shí)例,當(dāng)activity要銷毀時,線程還沒有完成任務(wù),這樣就導(dǎo)致了activity的內(nèi)存泄漏,下面我們用thread來舉個例子。

new Thread(){
    @Override
     public void run() {
        super.run();
        ......
     }
}.start();

這樣的寫法在我們?nèi)粘i_發(fā)中極為常見,為了方便快速直接在邏輯中起個線程區(qū)完成異步任務(wù),這樣的寫法會導(dǎo)致線程管理混亂,更會引起activity的內(nèi)存泄漏。當(dāng)線程中的任務(wù)耗時過長,已經(jīng)大大超出activity的生命周期范圍中效果就更加明顯。原因跟上兩種雷同,thread匿名內(nèi)部類持也有了activity的實(shí)例,本身的任務(wù)又不能及時完成,影響activity內(nèi)存回收。修改的方式原則就是將線程和當(dāng)前activity脫離干系,交與專門的線程管理類操作。

Webview的內(nèi)存泄漏

現(xiàn)在的app的開發(fā)多多少少會涉及混合開發(fā),當(dāng)webview加載的頁面過多過于復(fù)雜,或者web頁面中js代碼不太完善時,webview就會占有大量內(nèi)存,同時也不可避免會出現(xiàn)內(nèi)存泄漏的情況。解決的方案就是給webview一個單獨(dú)的進(jìn)程,當(dāng)webview所在的activity銷毀時,直接殺掉這個單獨(dú)開啟的進(jìn)程。

AndroidManifeast.xml
<activity
            android:name=".WebviewActivity"
            android:process=":web_h5"
            android:screenOrientation="portrait"
            android:configChanges="orientation|screenSize|keyboardHidden"
/>
Activity
 @Override
    protected void onDestroy() {
        super.onDestroy();
        android.os.Process.killProcess(android.os.Process.myPid());
    }

此方案的注意點(diǎn):
1.app內(nèi)進(jìn)程間通信需要通過廣播和aidl交互
2.數(shù)據(jù)持久化需要通過ContentProvider進(jìn)行處理

4.Java引用類型

在講Leakcanary之前,先復(fù)習(xí)下java中4中引用類型和引用隊(duì)列

強(qiáng)引用(StrongReference)

我們一般情況下使用的都是強(qiáng)引用,申請內(nèi)存后,如果不手動設(shè)置成Null,虛擬機(jī)是你會回收此內(nèi)存,就算內(nèi)存不足也不會回收,哪怕報出OOM

軟引用(SoftReference)

在虛擬機(jī)內(nèi)存足夠的情況下,掃描到軟引用是不會被GC回收的,但是內(nèi)存不足的情況下也會被GC回收

弱引用(WeakReference)

不管虛擬機(jī)內(nèi)存是否足夠,只要被GC掃描到,就會被回收

虛引用(PhantomReference)

形同虛設(shè)的引用,不會決定對象的生命周期,很少涉及

引用隊(duì)列(ReferenceQueue)

一般和軟引用弱引用配合使用

5.Leakcanary的介紹及使用

Leakcanary是square公司開源的一款內(nèi)存監(jiān)測框架,Github的地址:https://github.com/square/leakcanary
集成的方式

dependencies {
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.3'
  releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.3'
  // Optional, if you use support library fragments:
  debugImplementation 'com.squareup.leakcanary:leakcanary-support-fragment:1.6.3'
}

初始化的方式

public class ExampleApplication extends Application {

  @Override public void onCreate() {
    super.onCreate();
    if (LeakCanary.isInAnalyzerProcess(this)) {
      // This process is dedicated to LeakCanary for heap analysis.
      // You should not init your app in this process.
      return;
    }
    LeakCanary.install(this);
    // Normal app init code...
  }
}

leakcanary集成工作結(jié)束,然后我們寫一個內(nèi)存泄漏代碼也實(shí)驗(yàn)下。我們選擇上面介紹過的handler內(nèi)存泄漏的代碼去執(zhí)行。執(zhí)行完代碼邏輯,發(fā)現(xiàn)手機(jī)里面不僅有我們實(shí)驗(yàn)的app,還有一個名字叫做Leaks的圖標(biāo),進(jìn)入Leaks后發(fā)現(xiàn)真的發(fā)現(xiàn)檢測到了內(nèi)存泄漏的activity,引起的原因是MessageQueue,果然厲害。然后我們把handler的解決方法試下,運(yùn)行后發(fā)現(xiàn)Leaks里面空空如也,這說明我們修復(fù)了之前的內(nèi)存泄漏。


Screenshot_2019-01-13-21-33-08-600_Leaks.png

6.Leakcanary源碼及原理(Activity為例)

Leakcanary的原理就是監(jiān)控應(yīng)用中所有activity生命周期中的onDestroy方法,在onDestroy中把a(bǔ)ctivity轉(zhuǎn)成弱引用保存在引用隊(duì)列中,然后框架會不斷遍歷分析引用隊(duì)列,檢測到內(nèi)存地址是否可達(dá),并輸出內(nèi)存泄漏的最短路徑。
開始看源碼,我們從LeakCanary.install()入口。

/**
   * Creates a {@link RefWatcher} that works out of the box, and starts watching activity
   * references (on ICS+).
   */
  public static @NonNull RefWatcher install(@NonNull Application application) {
    return refWatcher(application).listenerServiceClass(DisplayLeakService.class)
        .excludedRefs(AndroidExcludedRefs.createAppDefaults().build())
        .buildAndInstall();
  }

此方法會創(chuàng)建并返回RefWatcher對象,這對象是Leakcanary的核心,buildAndInstall方法是該方法的核心,我們繼續(xù)跟進(jìn)去看

  /**
   * Creates a {@link RefWatcher} instance and makes it available through {@link
   * LeakCanary#installedRefWatcher()}.
   *
   * Also starts watching activity references if {@link #watchActivities(boolean)} was set to true.
   *
   * @throws UnsupportedOperationException if called more than once per Android process.
   */
  public @NonNull RefWatcher buildAndInstall() {
    if (LeakCanaryInternals.installedRefWatcher != null) {
      throw new UnsupportedOperationException("buildAndInstall() should only be called once.");
    }
    RefWatcher refWatcher = build();
    if (refWatcher != DISABLED) {
      if (enableDisplayLeakActivity) {
        LeakCanaryInternals.setEnabledAsync(context, DisplayLeakActivity.class, true);
      }
      if (watchActivities) {
        ActivityRefWatcher.install(context, refWatcher);
      }
      if (watchFragments) {
        FragmentRefWatcher.Helper.install(context, refWatcher);
      }
    }
    LeakCanaryInternals.installedRefWatcher = refWatcher;
    return refWatcher;
  }

真正創(chuàng)建RefWatcher對象就是這里的build方法,Leakcanary支持監(jiān)控Activity和Fragment的內(nèi)存泄漏情況,我們這里主要是研究Activity,所以這里接著跟ActivityRefWatcher.install方法

  public static void install(@NonNull Context context, @NonNull RefWatcher refWatcher) {
    Application application = (Application) context.getApplicationContext();
    ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);

    application.registerActivityLifecycleCallbacks(activityRefWatcher.lifecycleCallbacks);
  }

application.registerActivityLifecycleCallbacks此方法是系統(tǒng)方法,用來監(jiān)控app內(nèi)部所有Activity的生命周期。
那我們就來看看activityRefWatcher.lifecycleCallbacks的實(shí)現(xiàn)是什么?

private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
      new ActivityLifecycleCallbacksAdapter() {
        @Override public void onActivityDestroyed(Activity activity) {
          refWatcher.watch(activity);
        }
      };

只實(shí)現(xiàn)了onActivityDestroyed回調(diào),接著跟進(jìn)去refWatcher.watch方法

public void watch(Object watchedReference, String referenceName) {
    if (this == DISABLED) {
      return;
    }
    checkNotNull(watchedReference, "watchedReference");
    checkNotNull(referenceName, "referenceName");
    final long watchStartNanoTime = System.nanoTime();
    String key = UUID.randomUUID().toString();
    retainedKeys.add(key);
    final KeyedWeakReference reference =
        new KeyedWeakReference(watchedReference, key, referenceName, queue);

    ensureGoneAsync(watchStartNanoTime, reference);
  }

這個方法的核心是給每個觀察的引用產(chǎn)生一個隨機(jī)的key,保存在集合中,并把引用轉(zhuǎn)成弱引用保存在引用隊(duì)列中,ensureGoneAsync方法是核心的工作方法,作用是啟動工作線程異步的去分析內(nèi)存泄漏的情況。

  private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
    watchExecutor.execute(new Retryable() {
      @Override public Retryable.Result run() {
        return ensureGone(reference, watchStartNanoTime);
      }
    });
  }
Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
    long gcStartNanoTime = System.nanoTime();
    long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);

    removeWeaklyReachableReferences();

    if (debuggerControl.isDebuggerAttached()) {
      // The debugger can create false leaks.
      return RETRY;
    }
    if (gone(reference)) {
      return DONE;
    }
    gcTrigger.runGc();
    removeWeaklyReachableReferences();
    if (!gone(reference)) {
      long startDumpHeap = System.nanoTime();
      long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);

      File heapDumpFile = heapDumper.dumpHeap();
      if (heapDumpFile == RETRY_LATER) {
        // Could not dump the heap.
        return RETRY;
      }
      long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);

      HeapDump heapDump = heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key)
          .referenceName(reference.name)
          .watchDurationMs(watchDurationMs)
          .gcDurationMs(gcDurationMs)
          .heapDumpDurationMs(heapDumpDurationMs)
          .build();

      heapdumpListener.analyze(heapDump);
    }
    return DONE;
  }

ensureGone方法的作用就是確保能回收的引用從隊(duì)列中刪除,如果引用還在隊(duì)列中沒有被回收就會生成heap文件,然后分析這個heap文件。真正分析heap文件的工作在HeapAnalyzerService服務(wù)中。

  @Override protected void onHandleIntentInForeground(@Nullable Intent intent) {
    if (intent == null) {
      CanaryLog.d("HeapAnalyzerService received a null intent, ignoring.");
      return;
    }
    String listenerClassName = intent.getStringExtra(LISTENER_CLASS_EXTRA);
    HeapDump heapDump = (HeapDump) intent.getSerializableExtra(HEAPDUMP_EXTRA);

    HeapAnalyzer heapAnalyzer =
        new HeapAnalyzer(heapDump.excludedRefs, this, heapDump.reachabilityInspectorClasses);

    AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey,
        heapDump.computeRetainedHeapSize);
    AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);
  }

整個框架中最核心的方法出現(xiàn)了heapAnalyzer.checkForLeak,通過這個方法就能判斷是否內(nèi)存泄漏,并輸出結(jié)果。

  public @NonNull AnalysisResult checkForLeak(@NonNull File heapDumpFile,
      @NonNull String referenceKey,
      boolean computeRetainedSize) {
    long analysisStartNanoTime = System.nanoTime();

    if (!heapDumpFile.exists()) {
      Exception exception = new IllegalArgumentException("File does not exist: " + heapDumpFile);
      return failure(exception, since(analysisStartNanoTime));
    }

    try {
      listener.onProgressUpdate(READING_HEAP_DUMP_FILE);
      HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
      HprofParser parser = new HprofParser(buffer);
      listener.onProgressUpdate(PARSING_HEAP_DUMP);
      Snapshot snapshot = parser.parse();
      listener.onProgressUpdate(DEDUPLICATING_GC_ROOTS);
      deduplicateGcRoots(snapshot);
      listener.onProgressUpdate(FINDING_LEAKING_REF);
      Instance leakingRef = findLeakingReference(referenceKey, snapshot);

      // False alarm, weak reference was cleared in between key check and heap dump.
      if (leakingRef == null) {
        String className = leakingRef.getClassObj().getClassName();
        return noLeak(className, since(analysisStartNanoTime));
      }
      return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, computeRetainedSize);
    } catch (Throwable e) {
      return failure(e, since(analysisStartNanoTime));
    }
  }

先把heap文件讀到內(nèi)存緩存中HprofBuffer,然后通過HprofParser 進(jìn)行解析內(nèi)容,產(chǎn)生Snapshot快照,deduplicateGcRoots去除重復(fù)的GcRoot,findLeakingReference方法是通過之前產(chǎn)生的key,在快照中查找我們真正檢測的引用是否存在,如果不存在說明無內(nèi)存泄漏,如果存在,就找出內(nèi)存泄漏的路徑,輸出結(jié)果。

以上就是Leakcanary檢測內(nèi)存泄漏的完整流程。第一遍看框架源碼主要還是梳理執(zhí)行流程,抓核心,放細(xì)節(jié)。如果對Leakcanary這個框架設(shè)計(jì)感興趣不妨多看幾遍源碼。這邊就不做展開了。

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

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

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