LeakCanary内存优化学习

内存检测工具学习

Posted by GL on September 13, 2018

Android常见的内存泄漏

单例造成的内存泄漏

单例错误写法

public class SingletonActivityContext {
    private static SingletonActivityContext instance;
	private Context context;
	
	private SingletonActivityContext(Context context) {
        this.context = context;    
    }

    public static SingletonActivityContext getInstance(Context context) {
        if(instance == null) {
            instance = new SingletonActivityContext(context);
        }
        return instance;
    }
}

使用内部静态类的单例会使这个类的生命周期与整个APP的生命周期一样长,如果使用不当,很容易造成内存泄漏。最简单的处理方法就是传入Application的context,就不会内存泄漏了。

非静态内部类创建静态实例造成的内存泄漏

public class StaticLeakActivity extends Activity {
    private static noneStaticClass mResource = null;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if(mResource == null) {
            mResource = new nonoStaticClass();
        }
    }

    private class noneStaticClass {
    }
}

在一个频繁启动的Activity中使用一个静态变量持有一个非静态内部类的好处是,可以减少创建非静态内部类,但造成的问题是产生了内存泄漏。因为非静态内部类持有Activity的引用,而mResource的生命周期与整个App的声明周期一样长,并且持有非静态内部类的引用,造成Activity不能被回收,产生内存泄漏。

正确的做法是将这个非静态内部类改为静态内部类。

handler造成内存泄漏

public class HandlerLeakActivity extends Activity {
    private final Handler mLeakyHandler = new Handler() {
        @Override
        public void handlerMessage(Message msg) {
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mLeakyHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
            }
        }, 1000 * 60 * 10); // 延迟10分钟发送消息
    }
}

HandlerLeakActivity中我们的handler延迟10分钟发送消息,在10分钟之前,这个消息会放到messageQuene当中。如果我们在10分钟之前调用了HandlerLeakActivityfinish()方法,message持有handler的引用,handler又持有HandlerLeakActivity的引用,那么HandlerLeakActivity就不会被回收,产生了内存泄漏。

正确做法:1)将handler声明为静态的;2)通过弱引用的方式持有handler。

线程所造成的内存泄漏

正确写法,将异步执行的类都写成static,防止内部类持有外部类的匿名引用,避免内存泄漏。

public class ThreadLeakActivity extends Activity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        testThreadLeak();
    }

    private void testThreadLeak() {
        (AsyncTask) (params) -> {
            SystemClock.sleep(10 * 1000);
            return null;
        }.execute();

        new Thread((Runnable) () -> {
            SystemClock.sleep(10 * 1000);
        }).start();
    }

    static class MyAsyncTask extends AsyncTask<Void, void, Void> {
        private WeakReference<Context> weakReference;
        public MyAsyncTask(Context context) {
            weakReference = new WeakReference;
        }

        @Override
        protected Void doInbackground(Void... params) {
            SystemClock.sleep(10 * 1000);
            return null;
        }

        @Override
        protected void onPostExecute(Void aVoid) {
            super.onPostExecute(aVoid);
            TheadLeakyActivity activity = (ThreadLeakyActivity) weakReference;
            if(activity != null) {
                //...
            }
        }
    }

    static class MyRunnable implements Runnable {
        @Override
        public void run() {
            SystemClock.sleep(10 * 1000);
        }
    }
}

WebView造成的内存泄漏

public class WebviewLeakActivity extends AppCompatActivity {
    private Webview mWebview;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mWebview = (WebView) findViewById(R.id.wv_show);
        mWebview.loadUrl("http://www.baidu.com");
    }

    @Override
    protected void onDestroy() {
        destroyWebView();
        android.os.Process.killProcess(android.os.Process.myPid);
        super.onDestroy();
    }

    private void destroyWebView() {
        if(mWebview != null) {
            mWebview.pauseTimers();
            mWebview.removeAllViews();
            mWebview.destroy();
        }
    }
}

WebView加载页面时会使用堆内存存储页面元素。正确的避免WebView的做法是,将WebView放到一个单独的进程中,在监测内存占用过大时调用killProcess()方法杀掉这个进程。

LeakCanary原理

  1. Activity Destroy之后将它放在一个WeakReference中。
  2. 将这个WeakReference关联到一个ReferenceQueue中。
  3. 查看ReferenceQueue是否存在Activity的引用。
  4. 如果Activity泄露了,Dump出heap信息,然后再去分析泄露路径。

四种引用回顾:

1)强引用(StrongReference):我们平常创建的对象引用,在内存不足时,jvm宁愿抛出oom,也不会轻易回收强引用。

2)软引用(SoftReference):当内存空间足够时,软引用和强引用是一样的,只有在内存空间不足时,会回收软引用。可以和引用队列关联起来。

3)弱引用(WeakReference):当gc扫描内存空间时,不会区分内存够不够,遇到弱引用直接回收。可以和引用队列关联起来。

4)虚引用:它不会决定一个对象的生命周期。换句话说就是虚引用持有一个对象时,gc可以在任何时候回收它。

ReferenceQueue

  • 软引用/弱引用
  • 对象被垃圾回收,Java虚拟机就会把这个引用加入到与之关联的引用队列中。

LeakCanary源码分析

按照文档说明,LeakCanary只需在Application的onCreate()方法中写一句LeakCanary.install(this);即可。那我们先看install()方法。

public final class LeakCanary {

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

这个install方法返回一个RefWatcher类,用来启动Activity的RefWatcher类的。在Activity调用完onCreate(savedInstanceState)方法之后,RefWatcher会探测Activity的内存泄漏。这个install方法最终会返回一个buildAndInstall()方法,我们进去看一下它的实现。

// AndroidRefWatcherBuilder.java
public RefWatcher buildAndInstall() {
    if (LeakCanaryInternals.installedRefWatcher != null) {
        throw new UnsupportedOperationException("buildAndInstall() should only be called once.");
    }
    RefWatcher refWatcher = build();
    if (refWatcher != DISABLED) {
        if (watchActivities) {
            ActivityRefWatcher.install(context, refWatcher);
        }
        if (watchFragments) {
            FragmentRefWatcher.Helper.install(context, refWatcher);
        }
    }
    LeakCanaryInternals.installedRefWatcher = refWatcher;
    return refWatcher;
}

这个方法首先创建了一个RefWatcher对象,然后通过判断这个refWatcher对象是否可用并且按需对Activity和Fragment进行监测。这个方法的返回值是一个refWatcher,用来检测内存泄漏的。

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

    application.registerActivityLifecycleCallbacks(activityRefWatcher.lifecycleCallbacks);
}

//FragmentRefWatcher.Helper.install(context, refWatcher);
public static void install(Context context, RefWatcher refWatcher) {
    List<FragmentRefWatcher> fragmentRefWatchers = new ArrayList<>();

    if (SDK_INT >= O) {
      fragmentRefWatchers.add(new AndroidOFragmentRefWatcher(refWatcher));
    }

    try {
        Class<?> fragmentRefWatcherClass = Class.forName(SUPPORT_FRAGMENT_REF_WATCHER_CLASS_NAME);
        Constructor<?> constructor =
            fragmentRefWatcherClass.getDeclaredConstructor(RefWatcher.class);
        FragmentRefWatcher supportFragmentRefWatcher =
            (FragmentRefWatcher) constructor.newInstance(refWatcher);
        fragmentRefWatchers.add(supportFragmentRefWatcher);
    } catch (Exception ignored) {
    }

    if (fragmentRefWatchers.size() == 0) {
        return;
    }

    Helper helper = new Helper(fragmentRefWatchers);

    Application application = (Application) context.getApplicationContext();
    application.registerActivityLifecycleCallbacks(helper.activityLifecycleCallbacks);
}

先说一下ActivityRefWatcher.install()逻辑,首先获取Application的context,然后new一个ActivityRefWatcher,最后将ActivityRefWatcher的生命周期回调接口注册到application的Activity生命周期回调接口列表中。ActivityRefWatcher的lifecycleCallbacks接口并不神秘,它只是一些列在Activity各个声明周期会执行的方法。

public interface ActivityLifecycleCallbacks {
    void onActivityCreated(Activity activity, Bundle savedInstanceState);
    void onActivityStarted(Activity activity);
    void onActivityResumed(Activity activity);
    void onActivityPaused(Activity activity);
    void onActivityStopped(Activity activity);
    void onActivitySaveInstanceState(Activity activity, Bundle outState);
    void onActivityDestroyed(Activity activity);
}

然后我们说一下FragmentRefWatcher.Helper.install()方法,首先在该方法中有一个列表存储了fragmentRefWatcher,当SDK_INT >= O(API level >= 26)时会单独加入一个AndroidOFragmentRefWatcher,然后利用反射创建了一个SupportFragmentRefWatcher对象并加入到fragmentRefWatcher列表中,如果上述过程发生错误程序直接返回,从而不对fragment的内存泄露进行监听,当FragmentRefWatcher成功加入列表后,最终将这个列表检测用到的生命周期回调接口注册到Application之中。

在创建ActivityRefWatcher时,有一个RefWatcher类非常重要,这个类实现了对Activity和Fragment生命周期内内存泄漏的监测,接下来我们着重看一下它的实现。

public final class RefWatcher {
    public static final RefWatcher DISABLED = (new RefWatcherBuilder()).build();
    private final WatchExecutor watchExecutor; // 用于内存泄漏检测的类
    private final DebuggerControl debuggerControl; // 查询是否正在进行调试中,如果在调试中就不会进行内存泄漏检测的判断
    private final GcTrigger gcTrigger; // 在内存泄漏之前会给泄漏变量一次机会去调用GcTrigger方法中的gc,判断是否会被gc,若不gc就会给用户显示出来
    private final HeapDumper heapDumper; // 内存泄漏的堆文件
    private final Listener heapdumpListener; // 用于分析产生heap文件的一些回调
    private final Builder heapDumpBuilder; 
    private final Set<String> retainedKeys; // 持有待检测和已经内存泄漏对象引用的key
    private final ReferenceQueue<Object> queue;// 引用队列,用来判断弱引用持有的对象是否已经执行垃圾回收
}

分析完RefWatcher的成员变量后,我们分析一下RefWatcher的成员方法watch。

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

核心语句是这句String key = UUID.randomUUID().toString();,用处是对我们的reference加一个唯一的key值,这个key值在后面会有用。然后将这个key添加到成员变量retainedKeys列表中。接着根据观测对象和它对应的key值new了一个KeyedWeakReference弱引用对象。最后开启一个异步线程去分析这个对象引用。

接下来我们看一下异步线程ensureGoneAsync()方法中做了什么操作。

private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
    this.watchExecutor.execute(new Retryable() {
        public Result run() {
            return RefWatcher.this.ensureGone(reference, watchStartNanoTime);
        }
    });
}

我们在到ensureGone()方法中去看一下。

Result ensureGone(KeyedWeakReference reference, long watchStartNanoTime) {
    long gcStartNanoTime = System.nanoTime();
    // 用于计算当我们调用watch方法到gc垃圾回收一共花费的时间
    long watchDurationMs = TimeUnit.NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
    // 清除已经到达引用队列的弱引用,将已经回收的key从我们的引用队列里移除,剩下的key就是未被回收的对象
    this.removeWeaklyReachableReferences(); 
    if(this.debuggerControl.isDebuggerAttached()) {
        return Result.RETRY; // 如果已经进入调试状态,则不会进行内存泄漏的分析
    } else if(this.gone(reference)) {
        return Result.DONE; // 如果所有引用都已经被回收了,则返回DONE,此时未发生内存泄漏
    } else {
        this.gcTrigger.runGc(); // 如果当前还有对象未被回收,则再次调用gc,来进行手动的垃圾回收
        this.removeWeaklyReachableReferences(); // 清除到达引用队列的弱引用,与上次手动gc相对应
        if(!this.gone(reference)) { // 此时若能进到if语句中,说明了真发生了内存泄漏
            long startDumpHeap = System.nanoTime();
            long gcDurationMs = TimeUnit.NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
            File heapDumpFile = this.heapDumper.dumpHeap(); // 将内存泄漏信息保存到文件中
            if(heapDumpFile == HeapDumper.RETRY_LATER) {
                return Result.RETRY;
            }

            long heapDumpDurationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
            HeapDump heapDump = this.heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key).referenceName(reference.name).watchDurationMs(watchDurationMs).gcDurationMs(gcDurationMs).heapDumpDurationMs(heapDumpDurationMs).build();
            this.heapdumpListener.analyze(heapDump); // 在这里会去分析内存泄漏的路径
        }

        return Result.DONE;
    }
}

这个函数从名字上看就是确保我们的Activity进入到GONE状态,换句话说就是Activity是否已经被回收了。我们继续看分析内存泄漏的方法analyze()方法。

// HeapDump.java
public interface Listener {
    HeapDump.Listener NONE = new HeapDump.Listener() {
        public void analyze(HeapDump heapDump) {
        }
    };

    void analyze(HeapDump var1);
}

可以看出analyze方法是HeapDump的Listener接口的一个方法,我们继续看它的实现。

// ServiceHeapDumpListener.java
@Override public void analyze(HeapDump heapDump) {
    checkNotNull(heapDump, "heapDump");
    HeapAnalyzerService.runAnalysis(context, heapDump, listenerServiceClass);
}

继续进入runAnalysis()方法。

// HeapAnalyzerService.java 继承与ForegroundService
public static void runAnalysis(Context context, HeapDump heapDump,
      Class<? extends AbstractAnalysisResultService> listenerServiceClass) {
    setEnabledBlocking(context, HeapAnalyzerService.class, true);
    setEnabledBlocking(context, listenerServiceClass, true);
    Intent intent = new Intent(context, HeapAnalyzerService.class);
    intent.putExtra(LISTENER_CLASS_EXTRA, listenerServiceClass.getName());
    intent.putExtra(HEAPDUMP_EXTRA, heapDump);
    ContextCompat.startForegroundService(context, intent);
}

既然是Service,那我们就看它的onHandleIntentInForeground方法。

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

首先会对传进来的intent进行判断,如果是null打印一条日志。如果intent不为null,那么我们就先获取intent传进来的两个参数监听类的名字LISTENER_CLASS_EXTRA和堆文件信息HEAPDUMP_EXTRA。接着就开始新建一个HeapAnalyzer来分析堆内存。然后HeapAnalyzer对象会调用checkForLeak()方法来检测是否内存泄漏的结果。最后将这个结果返回给我们的AbstractAnalysisResultService,用来进行显示。

不用多说,我们来看一下checkForLeak()方法。

// HeapAnalyzer.java
public AnalysisResult checkForLeak(File heapDumpFile, 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); // 根据hprof文件,新建一个HprofBuffer对象
        HprofParser parser = new HprofParser(buffer); // 新建一个HprofParser解析器
        listener.onProgressUpdate(PARSING_HEAP_DUMP);
        Snapshot snapshot = parser.parse(); // 将hprof文件解析成Snapshot内存快照对象
        listener.onProgressUpdate(DEDUPLICATING_GC_ROOTS);
        deduplicateGcRoots(snapshot); // 去重的GcRoots,对我们分析结果进行去重,把那些重复内存泄漏和它的GcRoots删除。
        listener.onProgressUpdate(FINDING_LEAKING_REF);
        Instance leakingRef = findLeakingReference(referenceKey, snapshot); // 根据referenceKey去查找内存快照中的泄露对象

        // False alarm, weak reference was cleared in between key check and heap dump.
        if (leakingRef == null) {
          return noLeak(since(analysisStartNanoTime)); // 没有发生泄漏
        }
        return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, computeRetainedSize); // 把整个泄露对象的路径找出来
    } catch (Throwable e) {
        return failure(e, since(analysisStartNanoTime));
    }
}

checkForLeak方法就是进一步分析内存,将我们前面创建好的hprof文件解析成内存快照Snapshot对象,就可以进行内存分析了。总而言之,checkForLeak方法做了三件事情:

  1. 把hprof文件转为Snapshot
  2. 优化GcRoots
  3. 找出泄露的对象/找出泄露对象的最短路径

接下来我们会分析如何找出泄漏对象和泄露对象的最短路径

我们先看findLeakingReference()方法,它可以找到泄露对象。

// HeapAnalyzer.java
private Instance findLeakingReference(String key, Snapshot snapshot) {
    ClassObj refClass = snapshot.findClass(KeyedWeakReference.class.getName()); // 由于我们给每个监测对象都创建了一个弱引用,这里我们可以通过弱引用来从snapshot中获取引用对象
    if (refClass == null) { // 如果引用对象为null,则报异常
        throw new IllegalStateException(
            "Could not find the " + KeyedWeakReference.class.getName() + " class in the heap dump.");
    }
    List<String> keysFound = new ArrayList<>();
    for (Instance instance : refClass.getInstancesList()) { // 通过不断遍历获取我们需要的引用对象的key值
        List<ClassInstance.FieldValue> values = classInstanceValues(instance);
        Object keyFieldValue = fieldValue(values, "key");
        if (keyFieldValue == null) {
            keysFound.add(null);
            continue;
        }
        String keyCandidate = asString(keyFieldValue);
        if (keyCandidate.equals(key)) { // 当key值相等时,说明找到了这个泄露变量的引用 
            return fieldValue(values, "referent");
        }
        keysFound.add(keyCandidate);
    }
    throw new IllegalStateException(
        "Could not find weak reference with key " + key + " in " + keysFound);
}

findLeakingReference()方法实际上做了三件事:

  1. 在snapshot快照中找到第一个弱引用
  2. 遍历这个对象的所有实例
  3. 如果key值和最开始定义封装的key值相同,那么返回这个泄漏对象

我们再看findLeakTrace()方法,它可以找到泄露对象的最短路径。

// HeapAnalyzer.java
private AnalysisResult findLeakTrace(long analysisStartNanoTime, Snapshot snapshot,
      Instance leakingRef, boolean computeRetainedSize) {

    listener.onProgressUpdate(FINDING_SHORTEST_PATH);
    ShortestPathFinder pathFinder = new ShortestPathFinder(excludedRefs);
    ShortestPathFinder.Result result = pathFinder.findPath(snapshot, leakingRef); // 这里找到了泄露对象的最短路径

    // False alarm, no strong reference path to GC Roots.
    if (result.leakingNode == null) {
        return noLeak(since(analysisStartNanoTime));
    }

    listener.onProgressUpdate(BUILDING_LEAK_TRACE);
    LeakTrace leakTrace = buildLeakTrace(result.leakingNode); // 展示在屏幕上的LeakTrace

    String className = leakingRef.getClassObj().getClassName();

    long retainedSize;
    if (computeRetainedSize) {

        listener.onProgressUpdate(COMPUTING_DOMINATORS);
        // Side effect: computes retained size.
        snapshot.computeDominators();

        Instance leakingInstance = result.leakingNode.instance;

        retainedSize = leakingInstance.getTotalRetainedSize(); // 用来计算泄露对象的占用内存大小

        // TODO: check O sources and see what happened to android.graphics.Bitmap.mBuffer
        if (SDK_INT <= N_MR1) {
            listener.onProgressUpdate(COMPUTING_BITMAP_SIZE);
            retainedSize += computeIgnoredBitmapRetainedSize(snapshot, leakingInstance);
        }
    } else {
      retainedSize = AnalysisResult.RETAINED_HEAP_SKIPPED;
    }

    return leakDetected(result.excludingKnownLeaks, className, leakTrace, retainedSize,
        since(analysisStartNanoTime));
}

总结

到这里我们对LeakCanary的分析就完成了,总结一下,LeakCanary一共做了三件事情:

  1. 首先会创建一个refWatcher,启动一个ActivityRefWatcher,用来监听Activity的回收情况。
  2. 通过ActivityLifecycleCallbacks把Activity的onDestroy生命周期关联。
  3. 最后在线程池中去开始分析我们的内存泄漏。