Android内存性能分析

工具:Android Studio、MAT(Memory Analyzer Tool)

第三方库:LeakCanary

1、Android Profiler基础用法

首先附上Android Profilder的官方使用说明地址:

https://developer.android.google.cn/studio/profile/memory-profiler.html

Android Profiler可以实时查看app的cpu、memory和network状态,这里只说memory,简单写个用GridView加载大量网络图片的例子,对于加载到的bitmap对象分别采用HashMap和LruCache进行保存,观察内存变化。

当使用HashMap时,观察Android Profiler中memory的变化

可以看出内存不停增加,最终程序oom;

接着用LruCache替换HashMap,LruCahe的配置如下:

    // 获取应用程序最大可用内存
    int maxMemory = (int) Runtime.getRuntime().maxMemory();
    int cacheSize = maxMemory / 8;
    // 设置图片缓存大小为程序最大可用内存的1/8
    mMemoryCache = new android.util.LruCache<String, Bitmap>(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap bitmap) {
            return bitmap.getByteCount();
        }
    };

可以看出,当内存达到一定的值时得到了回收。

*AndroidManifest.xml文件中可以设置 android:largeHeap="true"来增加app的可使用内存,实验发现默认情况下nexus5手机android6.0系统当内存达到300多M时产生oom,如果设置该选项内存达到500多M时才会oom,但是设置该值会导致gc时间变长从而一定程度上影响性能,所以要斟酌使用。

*ActivityManager获取的内存值定义在系统rom中,编译时已经写入系统,而Runtime类可以获得运行时的内存数据,也是Android profiler显示的动态图数据。

02-02 20:39:17.158 14633-14633/com.blogxuan.oomdemo2 I/OOMDEMO: 机型:AOSP on HammerHead am.getMemoryClass()为192
02-02 20:39:17.158 14633-14633/com.blogxuan.oomdemo2 I/OOMDEMO: 机型:AOSP on HammerHead am.getLargeMemoryClass()为512
02-02 20:39:17.158 14633-14633/com.blogxuan.oomdemo2 I/OOMDEMO: 机型:AOSP on HammerHeadRuntime.getRuntime().maxMemory()为:536870912

android:largeHeap="false"时

02-02 20:44:46.550 17492-17492/com.blogxuan.oomdemo2 I/OOMDEMO: 机型:AOSP on HammerHeadRuntime.getRuntime().maxMemory()为:201326592

2、使用Android Profiler进行内存泄露分析。

一个简单的内存泄露例子

public class TestActivity extends Activity {
    TextView textView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        textView = (TextView) findViewById(R.id.test_text_view);
        TestDataModel.getInstance().setRetainedTextView(textView);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
    }
}

TestDataModel的class文件

public class TestDataModel {

    private static TestDataModel sInstance;
    private TextView mRetainedTextView;

    public static TestDataModel getInstance() {
        if (sInstance == null) {
            sInstance = new TestDataModel();
        }
        return sInstance;
    }

    public void setRetainedTextView(TextView textView) {
        mRetainedTextView = textView;
    }
}

在例子中,当TestActivity finish后,由于静态变量sInstance引用了textView,而textView引用了TestActivity的context,最终导致TestActivity对象不能被回收产生了内存泄露。

找出本应该回收但是并没有回收的对象(也就是发生了泄露的对象):

通过分析操作前和操作后的堆中的对象分配,首先找出堆计数异常大或者本该回收但是却没得到回收的对象,然后查看具体是哪些对象引用了他们导致他们没能回收。

也可以从Android Studio中将堆转储保存为hprof文件来用其他工具(比如MAT)进行分析,比如上面的例子,TestActivity打开前保存一二hprof文件,然后打开后再finish,然后再在android studio中手动gc一下,再导出hprof文件。接着在MAT中对比这两个堆文件。结果如下图:

可见TestDataModel对象和TestActivity对象并没有被回收,这是因为静态的sInstance生命周期跟application一样,而sInstance引用了textView,textView引用了TestActivity的context,因此TestActivity也没有被回收。

3、使用LeakCanary进行内存泄露分析。

LeakCanary是square公司开源的一个检测内存泄露的工具库,使用很简单,只要在gradle中依赖一下,然后在application中注册一下就行了。

github地址:https://github.com/square/leakcanary

使用LeakCanary检测内存泄露实例:

public class TestActivity2 extends Activity {
    TextView textView;
    Message msg;
    Handler mLeakyHandler= new Handler(){
        @Override
        public void handleMessage(Message msg) {
        }
    };
    Message msg;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        msg = Message.obtain();
        mLeakyHandler.sendMessageDelayed(msg, 40000);
    }
}

上面的例子中TestActivity打开后马上关闭,会产生内存泄露,LeakCanary会有如下内存泄露提示

这是因为handler发送一个消息给MessageQueue后,存在一条这样的引用链,主线程→mainLooper→MessageQueue→Message→handler→TestActivity,当activity destroy后,message还存在消息队列里,因此activity对象得不得回收。

要使activity能得到回收,只要在activity关闭后在这个引用链的任意一处切断都行,有如下三种方法:

1.切断Message和messageQueue的联系,即在activity的onDestroy中调用

mLeakyHandler.removeCallbacksAndMessages(null);

这个方法可以移除消息队列中的消息从而切断Message和MessageQueue的联系,使activity得到回收。

2.切断Message和handler的联系,可以通过在activity的onDestroy中调用

 msg.setTarget(null);

来实现,即把handler置空。

3.由于非静态内部类或匿名类会持有外部类的引用,即handler持有activity的引用,因此可以通过把handler声明为静态内部类来切断handler对activity实例的引用,但是静态内部类的对象是不能调用外部类的实例变量的,因此可以通过传递一个外部activity实例的WeakReference给handler使用,如下所示:

    public static class MyHandler extends Handler
    {
        private WeakReference<Context> reference;

        public MyHandler(Context activity)
        {
            reference = new WeakReference<Context>(activity);
        }

        @Override
        public void handleMessage(Message msg)
        {
            TestActivity2 mTestActivity = (TestActivity2) reference.get();
            if (reference.get() != null)
            {
                mTestActivity.textView.setText("hello");
            }
        }
    }

handler泄露的危害:有的人会说通常情况不会使用handler发送延迟太久的消息,而且消息达到延迟的时间后,activity不就可以自动被回收了么,因此handler泄露也没什么打不了啊,但是考虑这样一个情景,通常我们都会在onDestory里处理一些资源回收的操作,如果handler的handleMessage里面使用到了某些资源,而这些资源被回收了,那就会出问题了,最常见的就是产生空指针导致程序崩溃。

3、LeakCanary原理简单介绍。

leakCanary内存泄露检测完整时序图。

代码中我们只要简单使用

LeakCanary.install(this);

就能完成LeakCanary的初始化。

  public static RefWatcher install(Application application) {
    return refWatcher(application).listenerServiceClass(DisplayLeakService.class)
        .excludedRefs(AndroidExcludedRefs.createAppDefaults().build())
        .buildAndInstall();
  }

主要看下buildAndInstall这个方法

  public RefWatcher buildAndInstall() {
    RefWatcher refWatcher = build();
    if (refWatcher != DISABLED) {
      LeakCanary.enableDisplayLeakActivity(context);
      ActivityRefWatcher.install((Application) context, refWatcher);
    }
    return refWatcher;
  }

里面调用了ActivityRefWatcher.install方法

public static void install(Application application, RefWatcher refWatcher) {
    new ActivityRefWatcher(application, refWatcher).watchActivities();
}

里面调用了watchActivities这个方法

  public void watchActivities() {
    // Make sure you don't get installed twice.
    stopWatchingActivities();
    application.registerActivityLifecycleCallbacks(lifecycleCallbacks);
  }

lifecycleCallbacks定义如下:

private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
  new Application.ActivityLifecycleCallbacks() {
    @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
    }

    @Override public void onActivityStarted(Activity activity) {
    }

    @Override public void onActivityResumed(Activity activity) {
    }

    @Override public void onActivityPaused(Activity activity) {
    }

    @Override public void onActivityStopped(Activity activity) {
    }

    @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
    }

    @Override public void onActivityDestroyed(Activity activity) {
      ActivityRefWatcher.this.onActivityDestroyed(activity);
    }
  };

这是application提供的一个方法,可以用来监听我们应用中所有activity的生命周期,LeakCanay就是在这里通过监控所有activity的onDestroyed方法来检测内存泄露的。

继续进入ActivityRefWatcher.this.onActivityDestroyed方法,如下所示:

  void onActivityDestroyed(Activity activity) {
    refWatcher.watch(activity);
  }

LeakCanay内存泄露检测的核心就是这个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);
  }

retainedKeys是一个Set类型的集合,用来记录被监控的所有还在内存的对象(这里是刚调用了onDestoryed方法的activity对象),KeyedWeakReference继承自WeakReference,作用是用一个弱引用来引用被监控的对象(activity),queue是一个ReferenceQueue队列,作用后面说,

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);
      heapdumpListener.analyze(
          new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,
              gcDurationMs, heapDumpDurationMs));
    }
    return DONE;
  }

ensureGone是LeakCanay内存泄露检测主要逻辑。

  private void removeWeaklyReachableReferences() {
    // WeakReferences are enqueued as soon as the object to which they point to becomes weakly
    // reachable. This is before finalization or garbage collection has actually happened.
    KeyedWeakReference ref;
    while ((ref = (KeyedWeakReference) queue.poll()) != null) {
      retainedKeys.remove(ref.key);
    }
  }

首先从removeWeaklyReachableReferences这个方法说起,从上面的注释我们可以看出ReferenceQueue的作用(如果一个对象变成弱引用,那么这个对象入队),也就是如果我们的activity被finish了,正常情况下这个activity是应该进入这个队列了,因此当我们通过queue.poll()把这个activity对象取出来,retainedKeys中就移除这个activity,表示这个activity被回收了,而gone(reference)这个方法就是来判断retainedKeys中是否还存在这个对象,代码如下

  private boolean gone(KeyedWeakReference reference) {
    return !retainedKeys.contains(reference.key);
  }

removeWeaklyReachableReferences和gone方法都执行了两次,如果第一次对象就从retainedKeys移除了,那就是表示对象已经成功被回收了,如果没有,则调用gcTrigger.runGc()执行一次gc操作,再进一步确认对象是否还存在,如果还存在则猜测发生了内存泄露,因此取出dump文件进一步分析,对dump文件的分析使用了squareup的另一个haha库,如果确实出现了泄露,则找出泄露对象的引用路径通知用户。如有需要我们也可以导出LeakCanay生成的dump文件使用其他工具比如MAT,Android Studio进行分析。

*看LeakCanary源码时发现使用到了一个net.ltgt.gradle:gradle-errorprone-plugin的库,通过这个库,如果我们的handler有内存泄露的隐患,直接在运行app时就会报错,因此我们可以在项目中引用这个库来提前发现一些问题,从而避免一些安全隐患。

推荐书籍

《Android群英传:神兵利器》

《高性能Android应用开发》