android内存优化之Leakcanary浅谈

1.Java内存概要

在java内存模型中,一般分为5个部分,栈(stack),堆(heap),方法区(method),本地方法区(native method),程序计数器。其实我们比较熟悉是栈,堆和方法区。

主要存储基本数据类型和引用类型

主要存储对象类型,一个虚拟机只有一个,所有线程共享,由虚拟机GC管理

方法区

又称静态区,主要存储class对象和静态变量,一个虚拟机只有一个,所有线程是共享

2.内存泄漏的原因与后果

对象有自己的生命周期,大部分情况都是比较短暂的,当对象的生命周期结束后,由于一些特殊原因导致该对象无法被虚拟机回收,这就是内存泄漏。当内存泄漏到了一定数值导致应用占用的内存达到阀值,这时虚拟机就会报出OutOfMenoryError的错误,从而程序崩溃。在android平台中,由于移动端的特点,这个问题将会更加明显。

3.android常见的内存泄漏情景

单例模式的内存泄漏

单例对象的生命周期一般是从第一次创建使用到应用结束,虽然不能完全和应用的生命周期保持一致,但是也是和应用同步消亡的。而在android中使用单例中,往往会把上下文对象(Context)传给单例对象,一般会把Activity对象传进去,这时就产生了矛盾,两者的生命周期是不一致的,这种不一致就会导致Activity对象onDestroy以后不能被虚拟机回收。处理的方法简单,只需要调用Context的getApplicationContext,把对象转称application的上下文对象,这样两者消亡的生命周期就一致了,就不会内存泄漏了。(注:这里不用考虑单例的写法,不管是饿汉,懒汉,以及静态单例或者枚举写法都是一样)
修改前代码:

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

非静态内部类创建静态变量

非静态内部类会隐式持有外部类的实例,如果用非静态内部类创建静态变量就会导致外部类的实例不能被及时回收。打个比方,如果这个外部类是Activity,非静态内部类是Adapter,在Activity中有个Adapter类型的静态变量mAdapter,Adapter隐式持有Activity实例,mAdapter由于是静态的,将和应用消失的生命周期一致,当Actviity销毁后所占有的内存就不会被虚拟机回收造成类内存泄漏。修改的方式也比较简单:1.把内部类修饰成静态的,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的内存泄漏

handler的内存泄漏场景本质上和第二种是一样的,所以原因也是大同小异的,先看一段代码

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

这段代码是不是非常熟悉,很多时候我们代码就是这么写的,一般情况下,这段代码也是不会产生内存泄漏的,但是出现下面的场景时就会内存泄漏。

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

问题代码的逻辑是点击按钮通过handler发送一条需要延迟5分钟的message到消息队列,然后退出当前activity。那为什么这样就会引起内存泄漏呢?由于mHandler是非静态的变量,他持有了Activity的实例,在handler,messageQueue,Loop机制中,handler既是发送者又是消费者,所以当handler在某些业务场景下不能跟activity生命周期产生同步就会引起actviity的内存泄漏。修改的方式跟第二种类型,要将mHandler修饰成static。

线程的内存泄漏

这种场景和第2,3情况是类似的,就是线程持有当前activity的实例,当activity要销毁时,线程还没有完成任务,这样就导致了activity的内存泄漏,下面我们用thread来举个例子。

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

这样的写法在我们日常开发中极为常见,为了方便快速直接在逻辑中起个线程区完成异步任务,这样的写法会导致线程管理混乱,更会引起activity的内存泄漏。当线程中的任务耗时过长,已经大大超出activity的生命周期范围中效果就更加明显。原因跟上两种雷同,thread匿名内部类持也有了activity的实例,本身的任务又不能及时完成,影响activity内存回收。修改的方式原则就是将线程和当前activity脱离干系,交与专门的线程管理类操作。

Webview的内存泄漏

现在的app的开发多多少少会涉及混合开发,当webview加载的页面过多过于复杂,或者web页面中js代码不太完善时,webview就会占有大量内存,同时也不可避免会出现内存泄漏的情况。解决的方案就是给webview一个单独的进程,当webview所在的activity销毁时,直接杀掉这个单独开启的进程。

AndroidManifeast.xml

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

此方案的注意点:
1.app内进程间通信需要通过广播和aidl交互
2.数据持久化需要通过ContentProvider进行处理

4.Java引用类型

在讲Leakcanary之前,先复习下java中4中引用类型和引用队列

强引用(StrongReference)

我们一般情况下使用的都是强引用,申请内存后,如果不手动设置成Null,虚拟机是你会回收此内存,就算内存不足也不会回收,哪怕报出OOM

软引用(SoftReference)

在虚拟机内存足够的情况下,扫描到软引用是不会被GC回收的,但是内存不足的情况下也会被GC回收

弱引用(WeakReference)

不管虚拟机内存是否足够,只要被GC扫描到,就会被回收

虚引用(PhantomReference)

形同虚设的引用,不会决定对象的生命周期,很少涉及

引用队列(ReferenceQueue)

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

5.Leakcanary的介绍及使用

Leakcanary是square公司开源的一款内存监测框架,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集成工作结束,然后我们写一个内存泄漏代码也实验下。我们选择上面介绍过的handler内存泄漏的代码去执行。执行完代码逻辑,发现手机里面不仅有我们实验的app,还有一个名字叫做Leaks的图标,进入Leaks后发现真的发现检测到了内存泄漏的activity,引起的原因是MessageQueue,果然厉害。然后我们把handler的解决方法试下,运行后发现Leaks里面空空如也,这说明我们修复了之前的内存泄漏。


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

6.Leakcanary源码及原理(Activity为例)

Leakcanary的原理就是监控应用中所有activity生命周期中的onDestroy方法,在onDestroy中把activity转成弱引用保存在引用队列中,然后框架会不断遍历分析引用队列,检测到内存地址是否可达,并输出内存泄漏的最短路径。
开始看源码,我们从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();
  }

此方法会创建并返回RefWatcher对象,这对象是Leakcanary的核心,buildAndInstall方法是该方法的核心,我们继续跟进去看

  /**
   * 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;
  }

真正创建RefWatcher对象就是这里的build方法,Leakcanary支持监控Activity和Fragment的内存泄漏情况,我们这里主要是研究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此方法是系统方法,用来监控app内部所有Activity的生命周期。
那我们就来看看activityRefWatcher.lifecycleCallbacks的实现是什么?

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

只实现了onActivityDestroyed回调,接着跟进去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);
  }

这个方法的核心是给每个观察的引用产生一个随机的key,保存在集合中,并把引用转成弱引用保存在引用队列中,ensureGoneAsync方法是核心的工作方法,作用是启动工作线程异步的去分析内存泄漏的情况。

  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方法的作用就是确保能回收的引用从队列中删除,如果引用还在队列中没有被回收就会生成heap文件,然后分析这个heap文件。真正分析heap文件的工作在HeapAnalyzerService服务中。

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

整个框架中最核心的方法出现了heapAnalyzer.checkForLeak,通过这个方法就能判断是否内存泄漏,并输出结果。

  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文件读到内存缓存中HprofBuffer,然后通过HprofParser 进行解析内容,产生Snapshot快照,deduplicateGcRoots去除重复的GcRoot,findLeakingReference方法是通过之前产生的key,在快照中查找我们真正检测的引用是否存在,如果不存在说明无内存泄漏,如果存在,就找出内存泄漏的路径,输出结果。

以上就是Leakcanary检测内存泄漏的完整流程。第一遍看框架源码主要还是梳理执行流程,抓核心,放细节。如果对Leakcanary这个框架设计感兴趣不妨多看几遍源码。这边就不做展开了。

你可能感兴趣的:(android内存优化之Leakcanary浅谈)