玩转Leakcanary内存泄露分析

转自我的csdn博客,转载请注明出处。

Leakcanary是square推出的内存泄露分析工具,使用很简单,可谓“傻瓜式”应用。

但其内部原理实现直的深究学习,今天我们就层层剖析其使用方式、源码实现,了解一下大牛是如何写代码的。

前期知识点准备:内存泄漏 GC回收原理 java四种引用等等。

一、使用方法

1、gradle中添加依赖,目前最新版本为1.6.1

debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.1'//debug版本

releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.1'//发布版本,将该工具失效,避免影响线上业务

2、Application类中调用 LeakCanary.install(this);即可以开启该工具监控Activity内存泄漏,其他对象fragmen、service等其他对象的监控需要在对应类的中调用refWatcher.watch(this);

public class MyApplacition extends Application {

    static MyApplacition instance;

    private RefWatcher refWatcher;

    public static RefWatcher getRefWatcher(Context context) {

        return instance.refWatcher;

    }

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


public class MyFragment extends Fragment {

    @Override

    public void onDestroy() {

        super.onDestroy();

        RefWatcher refWatcher = Myapplacition.getRefWatcher(getActivity());

        refWatcher.watch(this);

//      RefWatcher refWatcher = LeakCanary.installedRefWatcher();//1.6.1版本中提供该方法获取refWatcher对象,之前的版本需要按上面方法自行获取。另,android8.0以上工具中添加了fragment的生命周期监测,不需要再添加此处代码。

    }

二、源码分析

在讲源码之前,先上一段栗子,

void refTest(){

    A a = new A();//a为强引用

    ReferenceQueue queue = new ReferenceQueue();

    WeakReference aa = new WeakReference(a, queue);//aa为弱引用

    a = null;

    Runtime.getRuntime().gc();//通知系统GC

    System.runFinalization();//强制系统回收已经没有强引用的对象

    Reference poll = null;

    while ((poll = queue.poll()) != null) {

        Log.i(TAG,"Reference"+poll.toString());

    }

}

以上这段代码中,强引用a置为null,则A对象只有aa这个弱引用存在。之后手动触发GC,log中可以看到aa的弱引用已经放到了引用队列中,说明A对象已经被回收。Leakcanary就是(1)利用此原理初步定位内存泄漏对象后,(2)再调用系统接口dump出堆转储文件快照.hprof,(3)调用haha库分析该文件解析出最短引用路径,(4)提示给用户的。

如图所示


1、初步定位内存泄漏对象

(1)//在调用LeakCanary的intsall方法之后,会调用buildAndInstall()生成refWatcher对象

public RefWatcher buildAndInstall() {

  if (LeakCanaryInternals.installedRefWatcher != null) {

    throw new UnsupportedOperationException("buildAndInstall() should only be called once.");

  }

  RefWatcher refWatcher = build();//构造模式,获取RefWatcher对象

  if (refWatcher != DISABLED) {

    if (watchActivities) {//监控activity,默认为true

      ActivityRefWatcher.install(context, refWatcher);

    }

    if (watchFragments) {//监控fragment,默认为true

      FragmentRefWatcher.Helper.install(context, refWatcher);

    }

  }

  LeakCanaryInternals.installedRefWatcher = refWatcher;

  return refWatcher;

}

(2)//refWatcher开始监控Activity、Fragment等对象

public final class ActivityRefWatcher {

  public static void installOnIcsPlus(Application application, RefWatcher refWatcher) {

    install(application, refWatcher);

  }

  public static void install(Context context, RefWatcher refWatcher) {

    Application application = (Application) context.getApplicationContext();

    ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);

    application.registerActivityLifecycleCallbacks(activityRefWatcher.lifecycleCallbacks);

  }

  private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =

      new ActivityLifecycleCallbacksAdapter() {

        @Override public void onActivityDestroyed(Activity activity) {

          refWatcher.watch(activity);

        }

      };

public interface FragmentRefWatcher {

  void watchFragments(Activity activity);

  final class Helper {

    private static final String SUPPORT_FRAGMENT_REF_WATCHER_CLASS_NAME =

        "com.squareup.leakcanary.internal.SupportFragmentRefWatcher";

    public static void install(Context context, RefWatcher refWatcher) {

ListfragmentRefWatchers = new ArrayList<>();

      if (SDK_INT >= O) {

        fragmentRefWatchers.add(new AndroidOFragmentRefWatcher(refWatcher));

      }

(3)//Android8.0以上引入了fragment的生命周期,用户不需要在onDestroy中自行调用

@RequiresApi(Build.VERSION_CODES.O) //

class AndroidOFragmentRefWatcher implements FragmentRefWatcher {

  private final RefWatcher refWatcher;

  AndroidOFragmentRefWatcher(RefWatcher refWatcher) {

    this.refWatcher = refWatcher;

  }

  private final FragmentManager.FragmentLifecycleCallbacks fragmentLifecycleCallbacks =

      new FragmentManager.FragmentLifecycleCallbacks() {

        @Override

        public void onFragmentDestroyed(FragmentManager fm, Fragment fragment) {

          refWatcher.watch(fragment);

        }

      };

(4)//生成唯一key标识对象,并建立该对象的弱引用关联到引用队列,如上述栗子中所示。

//其中retainedKeys为CopyOnWriteArraySet类型,解决并发读写问题

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

}

(5)//watchExecutor子线程中进行分析

  private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {

    watchExecutor.execute(new Retryable() {

      @Override public Retryable.Result run() {

        return ensureGone(reference, watchStartNanoTime);

      }

    });

  }

(6)//初步分析定位出泄漏对象

  @SuppressWarnings("ReferenceEquality") // Explicitly checking for named null.

  Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {

    long gcStartNanoTime = System.nanoTime();

    long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);

    removeWeaklyReachableReferences();//retainedKeys中,移除未泄露的对象(弱引用已被GC添加到引用队列的对象)

    if (debuggerControl.isDebuggerAttached()) {

      // The debugger can create false leaks.

      return RETRY;

    }

    if (gone(reference)) {//未泄露则返回,结束此次分析

      return DONE;

    }

    gcTrigger.runGc();//手动GC

    removeWeaklyReachableReferences();//再次移除未泄漏对象

    if (!gone(reference)) {//初步确认该对象内存泄漏

      long startDumpHeap = System.nanoTime();

      long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);

      File heapDumpFile = heapDumper.dumpHeap();//生成hprof文件

      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);//分析hprof文件

    }

    return DONE;

  }

  private boolean gone(KeyedWeakReference reference) {

    return !retainedKeys.contains(reference.key);

  }

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

    }

  }

2、调用系统接口dump出堆转储文件快照.hprof

@SuppressWarnings("ReferenceEquality") // Explicitly checking for named null.

@Override public File dumpHeap() {

  File heapDumpFile = leakDirectoryProvider.newHeapDumpFile();

  if (heapDumpFile == RETRY_LATER) {

    return RETRY_LATER;

  }

FutureResultwaitingForToast = new FutureResult<>();

  showToast(waitingForToast);//在开始生成hprof文件之前,在主线程中显示toast,如附图

  if (!waitingForToast.wait(5, SECONDS)) {//等待主线程完成toast的展示

    CanaryLog.d("Did not dump heap, too much time waiting for Toast.");

    return RETRY_LATER;

  }

  Notification.Builder builder = new Notification.Builder(context)

      .setContentTitle(context.getString(R.string.leak_canary_notification_dumping));

  Notification notification = LeakCanaryInternals.buildNotification(context, builder);

  NotificationManager notificationManager =

      (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);

  int notificationId = (int) SystemClock.uptimeMillis();

  notificationManager.notify(notificationId, notification);

  Toast toast = waitingForToast.get();

  try {

    Debug.dumpHprofData(heapDumpFile.getAbsolutePath());//调用系统方法生成.hprof文件

    cancelToast(toast);

    notificationManager.cancel(notificationId);

    return heapDumpFile;

  } catch (Exception e) {

    CanaryLog.d(e, "Could not dump heap");

    // Abort heap dump

    return RETRY_LATER;

  }

}

private void showToast(final FutureResultwaitingForToast) {

  mainHandler.post(new Runnable() {

    @Override public void run() {

      final Toast toast = new Toast(context);

      toast.setGravity(Gravity.CENTER_VERTICAL, 0, 0);

      toast.setDuration(Toast.LENGTH_LONG);

      LayoutInflater inflater = LayoutInflater.from(context);

      toast.setView(inflater.inflate(R.layout.leak_canary_heap_dump_toast, null));

      show(toast);

      // Waiting for Idle to make sure Toast gets rendered.

      Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {

        @Override public boolean queueIdle() {

          waitingForToast.set(toast);

          return false;

        }

      });

    }

  });

}


3、调用haha库分析该文件解析出最短引用路径

和MAT分析.hprof文件类似,此处调用haha开源库分析出最短引用路径。

haha库的github路径https://github.com/square/haha

4、提示用户

将分析出的结果最终通过DisplayLeakService在状态栏提示展示出来,通知用户。点击通知调起DisplayLeakActivity显示泄露信息,即最短引用路径。这两给类是另开进程的,在桌面上可以看到leakcanry的图标就是在DisplayLeakActivity中配置的,至于另开进程的原因,应该是为了避免占用主应用的内存。

三、示例

1、线程泄露

在ativity中调用此方法,然后按返回键结束activity。

void startAsyncWork() {

    // This runnable is an anonymous class and therefore has a hidden reference to the outer

    // class MainActivity. If the activity gets destroyed before the thread finishes (e.g. rotation),

    // the activity instance will leak.

    Runnable work = new Runnable() {

        @Override public void run() {

            // Do some slow work in background

            SystemClock.sleep(20000);

        }

    };

    Thread thread=new Thread(work);

    thread.start();

}

测试结果如下:


结果中显示MainActivity存在泄漏,原因是被this指针引用,this指针就是java中非静态内部类对外部类默认的引用,this指针被thread持有,而thread里面有耗时任务。所以在Activity结束的时候,因为thread的耗时任务没有执行完,导致Activity不能销毁,出现泄漏。

2、handler泄露

Activity中,直接new handler发送延迟消息,出现泄漏。改为弱引用,则不出现泄漏,原理同上面栗子。

public class HandlerActivity extends Activity {

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.handlerac_layout);

        handler.sendEmptyMessageDelayed(0, 10 * 60 * 1000);

//        new WeakHandler(this).sendEmptyMessageDelayed(0, 10 * 60 * 1000);

        findViewById(R.id.gc).setOnClickListener(new View.OnClickListener() {

            @Override

            public void onClick(View view) {

                finish();

            }

        });

    }

    Handler handler = new Handler(){

        @Override

        public void handleMessage(Message msg) {

            super.handleMessage(msg);

            Log.i("HandlerActivity","接收消息") ;

        }

    };

    private static class WeakHandler extends Handler {

WeakReferenceweakReference;

        public WeakHandler(HandlerActivity activity) {

weakReference = new WeakReference(activity);

        }

        @Override

        public void handleMessage(Message msg) {

            HandlerActivity activity = weakReference.get();

            if (activity != null && activity.tvHandler != null) {

                Log.i("HandlerActivity","接收消息") ;

            }

        }

    }

测试结果如下:

this指针被Message.target即handler对象持有,在new handler时会关联到当前线程的looper,而looper创建了messagequeue对象,所以handler最终被messagequeue持有。栗子中发送了延时任务,所以Activity结束时,因为MessageQueue队列中的消息没有结束,导致Activity泄漏。


3、单例泄漏

4、io资源未关闭

5、注册未反注册

6、静态activity、静态view等

其他栗子类似,也是常见的一些泄漏问题。

四、其他

leakcanar中一些比较好的用法,也值得我们借鉴一下

1、自定义结果处理

使用中可以继承DisplayLeakService 自定义分析结果处理方式,比如上传服务器等,只需要将自定义的sevice传到rewatcher对象中,因为其参数类型设计为边界上限的泛型Class listenerServiceClass,在开发中可以借鉴这种方式,提升代码的扩展性。

//继承类

public class LeakUploadService extends DisplayLeakService {

    static final String TAG="ReferenceQueue";

    @Override

    protected void afterDefaultHandling(HeapDump heapDump, AnalysisResult result, String leakInfo) {

        if (!result.leakFound || result.excludedLeak) {

            return;

        }

        Log.i(TAG,"leakInfo"+leakInfo);

//传参

RefWatcher refWatcher = LeakCanary.refWatcher(this)

        .listenerServiceClass(LeakUploadService.class)

        .buildAndInstall();

2、多线程同步

源码中的showtoast方法,使用了CountDownLatch实现主线程和子线程之间的同步。

public final class FutureResult{

private final AtomicReferenceresultHolder;

  private final CountDownLatch latch;

  public FutureResult() {

resultHolder = new AtomicReference<>();

    latch = new CountDownLatch(1);//子线程等待主线程展示toast,计数为1

  }

  public boolean wait(long timeout, TimeUnit unit) {

    try {

      return latch.await(timeout, unit);//计数结束

    } catch (InterruptedException e) {

      throw new RuntimeException("Did not expect thread to be interrupted", e);

    }

  }

  public T get() {

    if (latch.getCount() > 0) {

      throw new IllegalStateException("Call wait() and check its result");

    }

    return resultHolder.get();

  }

  public void set(T result) {

    resultHolder.set(result);

    latch.countDown();

  }

}

3、并发读写

CopyOnWrite的读写数据,解决并发读写问题

retainedKeys = new CopyOnWriteArraySet<>();

4、当前进程判断

因为heap文件的分析服务、结果处理服务、结果展示activity都是新开进程的,二新的进程会触发application的oncreate,所以调用该方法判断当前进程是否为leakcanary新开的进程。

public static boolean isInServiceProcess(Context context, Class serviceClass) {

  PackageManager packageManager = context.getPackageManager();

  PackageInfo packageInfo;

try{

    packageInfo = packageManager.getPackageInfo(context.getPackageName(), GET_SERVICES);

}catch(Exceptione) {

CanaryLog.d(e,"Could not get package info for %s", context.getPackageName());

returnfalse;

  }

  String mainProcess = packageInfo.applicationInfo.processName;

ComponentName component =newComponentName(context, serviceClass);

  ServiceInfo serviceInfo;

try{

serviceInfo = packageManager.getServiceInfo(component,0);

}catch(PackageManager.NameNotFoundException ignored) {

// Service is disabled.

returnfalse;

  }

if(serviceInfo.processName.equals(mainProcess)) {

CanaryLog.d("Did not expect service %s to run in main process %s", serviceClass, mainProcess);

// Technically we are in the service process, but we're not in the service dedicated process.

returnfalse;

  }

  int myPid = android.os.Process.myPid();

  ActivityManager activityManager =

      (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);

ActivityManager.RunningAppProcessInfo myProcess =null;

List runningProcesses;

try{

    runningProcesses = activityManager.getRunningAppProcesses();

}catch(SecurityExceptionexception) {

// https://github.com/square/leakcanary/issues/948

CanaryLog.d("Could not get running app processes %d",exception);

returnfalse;

  }

if(runningProcesses !=null) {

for(ActivityManager.RunningAppProcessInfo process : runningProcesses) {

if(process.pid == myPid) {

        myProcess = process;

break;

      }

    }

  }

if(myProcess ==null) {

CanaryLog.d("Could not find running process for %d", myPid);

returnfalse;

  }

returnmyProcess.processName.equals(serviceInfo.processName);

}

5、构造模式

类似dialog的builder,rewatcher对象采用了构造模式,通过rewatcherbuilder生成。


6、IdleHandler

源码中多处使用idlehandler,是一种很巧妙的用法,首先触发UI更新操作,然后等待主线程空闲,则说明主线程已经完成UI更新操作,继而执行下一步操作。

// Waiting for Idle to make sure Toast gets rendered.

Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {

  @Override public boolean queueIdle() {

    waitingForToast.set(toast);

    return false;

  }

});

7、手动gc

源码中使用该方法保证只有弱引用的对象被回收,即首先调用Runtime.gc(),等待100ms后,再调用System.runFinalization()强制系统回收已经没有强引用的对象释放内存,并确保该对象的弱引用被添加到引用队列。

public interface GcTrigger {

  GcTrigger DEFAULT = new GcTrigger() {

    @Override public void runGc() {

      // Code taken from AOSP FinalizationTest:

      // https://android.googlesource.com/platform/libcore/+/master/support/src/test/java/libcore/

      // java/lang/ref/FinalizationTester.java

      // System.gc() does not garbage collect every time. Runtime.gc() is

      // more likely to perfom a gc.

      Runtime.getRuntime().gc();

      enqueueReferences();

      System.runFinalization();

    }

8、 监控对象类型

rewatcher的watch方法入参是object类型,所以本质上是可以监控任意对象类型的,关键在于监控的时机,像activity、service、fragmen是有生命周期的,可以在ondestroy时开始监控,其他的对象类型用户可以选择合适的时机调用该方法进行监控,所以网上一般说的leakcanary只能监控activity是不准确的。

public void watch(Object watchedReference) {

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容