LeakCanary源码笔记

LeakCanary

由Square开源的一款轻量级第三方内存泄漏检测工具
为什么需要LeakCanary框架:性能优化是衡量我们app质量的一大标准。
性能优化有如下几点:UI卡顿/ANR/内存泄漏/OOM/启动速度
内存泄漏没有直观的体现,长时间不断累积会导致OOM现象

为什么有内存泄漏

较长生命周期的对象持有了较短生命周期的引用导致较短生命周期对象无法被垃圾回收期回收。
预备知识:1).GcRoots ;2)四种引用类型
LeakCanary利用了我们的MAT去分析内存,然后解决内存泄漏。

GcRoots

所谓“GC roots”,或者说tracing GC的“根集合”,就是一组必须活跃的引用。
例如说,这些引用可能包括:
所有Java线程当前活跃的栈帧里指向GC堆里的对象的引用;换句话说,当前所有正在被调用的方法的引用类型的参数/局部变量/临时值。
VM的一些静态数据结构里指向GC堆里的对象的引用,例如说HotSpot VM里的Universe里有很多这样的引用。
JNI handles,包括global handles和local handles
(看情况)所有当前被加载的Java类
(看情况)Java类的引用类型静态变量
(看情况)Java类的运行时常量池里的引用类型常量(String或Class类型)
(看情况)String常量池(StringTable)里的引用

4钟引用

强引用:平常的new出来的对象
软引用:SoftRefrence内存不足,GC会回收
弱引用:weakrefrence,每次都会回收
虚引用:和没有是一样的

内存

1.栈(stack):放一些基本类型及对象的引用
2.堆(heap):存放我们new出来的对象及数组,有java虚拟机Gc管理,线程共享
3.方法区(method):静态区,包含我们所有的class对象以及静态变量

LeakCanary原理

流程:
1.手动触发GC然后分析强引用的GC引用链
2.如果存在GC引用链,说明有内存泄漏,会在你的手机上弹出提示框,创建一个app
3.记录了每一次内存泄漏的GC引用链,通过它可以直接定位到内存泄漏的未释放的对象

原理:watch一个即将要销毁的对象
ReferenceQueue:软引用/弱引用,对象被垃圾回收,java虚拟机就会把这个引用加入到与之关联的引用队列中去。
1.Activity Destroy之后将它放在一个WeakReference,通过registerActivityLifecycleCallbacks监听ondestory
2.这个WeakReference关联到一个ReferenceQueue中
3.查看ReferenceQueue是否存在Activity的引用
4.如果该Activity泄漏了,Dump出heap信息,然后再去分析泄漏路径

那么我们回顾一下传统的检测内存泄漏的方法:
1.GC后dump一份内存快照
2.频繁操作GC后再dump一份内存快照
3.重复并用mat工具对比对象增量
所以旧方法对劳动力和精确度都是一个考验啊,LeakCanay同样是用了内存快照,但是在精确对比之前还加入了弱引用算法,可以理解成是粗定位到精定位的一个过程。

LeakCanary源码分析
1.首先会创建一个refwatcher,启动一个ActivityRefWatcher
2.通过ActivityLifecycleCallbacks把Activity的ondestory生命周期相关联
3.最后在线程池中去开始分析我们的泄漏

public final class RefWatcher {
    public static final RefWatcher DISABLED = (new RefWatcherBuilder()).build();
//执行内存泄漏的监测
    private final WatchExecutor watchExecutor;
//查询是否调试,正在调试时不会执行内存泄漏的监测判断
    private final DebuggerControl debuggerControl;
//处理Gc,用于在判断内存泄漏前,给泄漏对象再一次机会,调用这个
//对象中的方法执行gc
    private final GcTrigger gcTrigger;
//dump出内存泄漏的堆文件
    private final HeapDumper heapDumper;
//持有待检测的以及已经产生了内存泄漏的key
    private final Set<String> retainedKeys;
//判断弱引用所持有的对象是否已经执行了垃圾回收
    private final ReferenceQueue<Object> queue;
//分析一些产生heap文件的回调
    private final Listener heapdumpListener;
//排除系统bug引起的内存泄漏
    private final ExcludedRefs excludedRefs;

public void watch(Object watchedReference, String referenceName) {
    if (this == DISABLED) {
      return;
    }
    final long watchStartNanoTime = System.nanoTime();
//待监测的key加入集合中
    String key = UUID.randomUUID().toString();
    retainedKeys.add(key);
//把对象加入到弱引用中
    final KeyedWeakReference reference =
        new KeyedWeakReference(watchedReference, key, referenceName, queue);
//开启异步线程分析刚创建的弱引用
    ensureGoneAsync(watchStartNanoTime, reference);
  }

//线程池中执行我们的runable
private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
    watchExecutor.execute(new Retryable() {
      @Override public Retryable.Result run() {
        return ensureGone(reference, watchStartNanoTime);
      }
    });
  }

//确保我们的activity是否已经进入到gone的状态(也就是被回收了),
//因为在dump我们的内存信息前(提示内存泄漏前),希望我们系统进
//过充分的垃圾回收
Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
    long gcStartNanoTime = System.nanoTime();
//计算从调用watch方法到我们调用了gc垃圾回收总共耗费的时间
    long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
//清除已经到达我们引用队列的弱引用,剩下的retainedKeys都是未回收
//的对象
    removeWeaklyReachableReferences();
//debug状态停止内存泄漏分析
    if (debuggerControl.isDebuggerAttached()) {
      // The debugger can create false leaks.
      return RETRY;
    }
//retainedKeys中未包含该key,直接返回
    if (gone(reference)) {
      return DONE;
    }
//在跑一次gc
    gcTrigger.runGc();
    removeWeaklyReachableReferences();
    if (!gone(reference)) {
      long startDumpHeap = System.nanoTime();
      long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
//dump出我们内存泄漏的文件
      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;
  }

}

.hprof转换snapshot

1.解析hprof文件,把这个文件封装成snapshot
2.根据弱引用和前面定义的key值,确定泄漏对象
3.找到最短泄漏路径,作为结果反馈出来,displayActivity展示结果
ps:如果在snapshot对象中没有找到怀疑泄漏的对象,证明这个对象没有内存泄漏,可能是误判或者是已经被gc回收了

上面找出了真正内存泄漏的activity,通过heapdumpListener.analyze开始真正的内存泄漏分析以及路径,analyze最终会调用runAnalysis方法

public final class HeapAnalyzerService extends IntentService {
    private static final String LISTENER_CLASS_EXTRA = "listener_class_extra";
    private static final String HEAPDUMP_EXTRA = "heapdump_extra";

//IntentService中startService每次都会回调onHandleIntent方法
//IntentService内部会启动HandlerThread线程
//HeapDump用来分析我们产生的堆文件
    public static void runAnalysis(Context context, HeapDump heapDump, Class<? extends AbstractAnalysisResultService> listenerServiceClass) {
        Intent intent = new Intent(context, HeapAnalyzerService.class);
        intent.putExtra("listener_class_extra", listenerServiceClass.getName());
        intent.putExtra("heapdump_extra", heapDump);
        context.startService(intent);
    }

    public HeapAnalyzerService() {
        super(HeapAnalyzerService.class.getSimpleName());
    }

    protected void onHandleIntent(Intent intent) {
        if(intent == null) {
            CanaryLog.d("HeapAnalyzerService received a null intent, ignoring.", new Object[0]);
        } else {
            String listenerClassName = intent.getStringExtra("listener_class_extra");
//HeapDump用来分析我们产生的堆文件
            HeapDump heapDump = (HeapDump)intent.getSerializableExtra("heapdump_extra");
//HeapAnalyzer 用来分析我们堆内存
            HeapAnalyzer heapAnalyzer = new HeapAnalyzer(heapDump.excludedRefs);
//内存分析结果,将我们之前的hprof文件解析成我们的内存快照snapshot
            AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey);
            AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);
        }
    }
}

heapAnalyzer.checkForLeak,将我们之前的hprof文件解析成我们的内存快照。

1.把.hpro转为Snapshot,包含我们所有对象引用的路径
2.优化gcroots
3.找出泄漏的对象/找出泄漏对象的最短路径

public AnalysisResult checkForLeak(File heapDumpFile, String referenceKey) {
        long analysisStartNanoTime = System.nanoTime();
        if(!heapDumpFile.exists()) {
            IllegalArgumentException e1 = new IllegalArgumentException("File does not exist: " + heapDumpFile);
            return AnalysisResult.failure(e1, this.since(analysisStartNanoTime));
        } else {
            try {
            //封装
                MemoryMappedFileBuffer e = new MemoryMappedFileBuffer(heapDumpFile);
            //解析器
                HprofParser parser = new HprofParser(e);
            //解析器解析
                Snapshot snapshot = parser.parse();
            //去重的Gcroots
                this.deduplicateGcRoots(snapshot);
            //根据我们需要检测的key值,查询我们解析结果中是否有我们的对象
                Instance leakingRef = this.findLeakingReference(referenceKey, snapshot);
                return leakingRef == null?AnalysisResult.noLeak(this.since(analysisStartNanoTime)):this.findLeakTrace(analysisStartNanoTime, snapshot, leakingRef);
            } catch (Throwable var9) {
                return AnalysisResult.failure(var9, this.since(analysisStartNanoTime));
            }
        }
    }

找到内存泄漏的引用

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

private Instance findLeakingReference(String key, Snapshot snapshot) {
//leakcanary通过弱引用来查找我们需要的对象的,需要检测的类我们构造了一
//个含有弱引用的对象,我们通过查找弱引用就可以查到我们内存泄漏的对象
        ClassObj refClass = snapshot.findClass(KeyedWeakReference.class.getName());
        ArrayList keysFound = new ArrayList();
        Iterator var5 = refClass.getInstancesList().iterator();

        while(var5.hasNext()) {
            Instance instance = (Instance)var5.next();
            List values = HahaHelper.classInstanceValues(instance);
            String keyCandidate = HahaHelper.asString(HahaHelper.fieldValue(values, "key"));
          //当key值相等时,说明找到了我们的监测对象,这个对象就是我们内存泄漏
         //的引用
            if(keyCandidate.equals(key)) {
                return (Instance)HahaHelper.fieldValue(values, "referent");
            }

            keysFound.add(keyCandidate);
        }

        throw new IllegalStateException("Could not find weak reference with key " + key + " in " + keysFound);
    }

根据找到的内存泄漏引用,来获取最短路径

private AnalysisResult findLeakTrace(long analysisStartNanoTime, Snapshot snapshot, Instance leakingRef) {
//通过分析我们的hprof文件,找到我们内存泄漏的点,判断的依据是Gcroot(表
//示不能被gc回收的对象),有很多类型,在leakcanary只需关注两种,一个是
//静态的,一个是这个对象被其他线程使用,其他线程正在运行。下面两行代码
//最重要的findPath这个方法就是判断上述两种情况
        ShortestPathFinder pathFinder = new ShortestPathFinder(this.excludedRefs);
        Result result = pathFinder.findPath(snapshot, leakingRef);
        if(result.leakingNode == null) {
//找不到内存泄漏的gcroot的判断
            return AnalysisResult.noLeak(this.since(analysisStartNanoTime));
        } else {
      //生成我们内存泄漏的调用栈,也就是展现在我们屏幕上的trace
            LeakTrace leakTrace = this.buildLeakTrace(result.leakingNode);
            String className = leakingRef.getClassObj().getClassName();
            snapshot.computeDominators();
            Instance leakingInstance = result.leakingNode.instance;
      //计算内存泄漏的空间大小
            long retainedSize = leakingInstance.getTotalRetainedSize();
            if(VERSION.SDK_INT <= 25) {
                retainedSize += this.computeIgnoredBitmapRetainedSize(snapshot, leakingInstance);
            }

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

LeakCanary补充_Application

1.Application是个单例
2.全局实例
3.启动Application时,系统会创建一个PID

使用场景:
1.初始化全局变量,环境配置变量
2.获取应用程序当前内存使用情况
3.监听应用程序内所有Activity的生命周期
4.监听应用程序配置信息的改变

源码分析:

level内存级别:
//内存不足,进程在后台进程列表的最后一个,马上要被清理
static final int TRIM_MEMORY_COMPLETE = 80;
//内存不足,进程在后台进程列表的中部,不会立即清理
static final int TRIM_MEMORY_MODERATE = 60;
//内存不足,后台进程
static final int TRIM_MEMORY_BACKGROUND = 40;
//内存不足,不可见,可能调用stop方法
static final int TRIM_MEMORY_UI_HIDDEN = 20;
//内存不足,进程优先级高
static final int TRIM_MEMORY_RUNNING_CRITICAL = 15;
//内存不足,优先级又高了点
static final int TRIM_MEMORY_RUNNING_LOW = 10;
//内存不足,更高
static final int TRIM_MEMORY_RUNNING_MODERATE = 5;


@CallSuper
    public void onTrimMemory(int level) {
        Object[] callbacks = collectComponentCallbacks();
        if (callbacks != null) {
            for (int i=0; i<callbacks.length; i++) {
                Object c = callbacks[i];
                if (c instanceof ComponentCallbacks2) {
                    ((ComponentCallbacks2)c).onTrimMemory(level);
                }
            }
        }
    }

//android4.0前用来检测,相当于level为TRIM_MEMORY_COMPLETE 
@CallSuper
    public void onLowMemory() {
        Object[] callbacks = collectComponentCallbacks();
        if (callbacks != null) {
            for (int i=0; i<callbacks.length; i++) {
                ((ComponentCallbacks)callbacks[i]).onLowMemory();
            }
        }
    }

//应用程序结束后会回调的方法
@CallSuper
    public void onTerminate() {
    }

//监听应用程序配置信息改变 
@CallSuper
    public void onConfigurationChanged(Configuration newConfig) {
        Object[] callbacks = collectComponentCallbacks();
        if (callbacks != null) {
            for (int i=0; i<callbacks.length; i++) {
                ((ComponentCallbacks)callbacks[i]).onConfigurationChanged(newConfig);
            }
        }
    }

LeakCanary补充_性能数据上报

性能解决思路,应用性能种类,各个性能数据指标

性能解决思路

1.监控性能情况:数据信息,收集上报 cpu,内存,卡顿,网络流量,页面加载
等,量化的角度分析app性能
2.根据上报信息修改代码迭代
3.持续监控并观察

应用性能

1.资源消耗。电量,流量等
2.流畅度

统计性能数据指标

1.网络请求流量
1).日常开发中可以通过tcpdump+Wireshark抓包测试法
2).TrafficStats类

2.冷启动
1).adbshell am start -W packagename/MainActivity冷启动时间
2).日志打印,起始时间终止时间,起点:Application的oncreate方法;终点:
MainActivity Oncreate

3.UI卡顿
1).Fps:值越高越流畅
Choreographer类获取帧率信息,设置framecallback,每帧在渲染的时候记录开始渲染的时间,这样在下一帧被处理的时候不仅能判断上一帧在渲染的时候是否出现掉帧现象,而且整个过程都是实时处理的
VSYNC同步信号,当发送信号通知整个界面重新绘制渲染,同步周期为16.6ms,每一次重绘的时候都会回调Choreographer的doframe方法,如果两次doframe进行绘制的时间间隔大于16.6ms,说明出现了卡顿。
缺点:Choreographer性能消耗比较大,描述卡顿不够准确,不能准确定位到堆栈信息

2).主线程ActivityThread消息处理时长
原理:主线程消息前后都会用logging打印消息,监测值跟阈值的对比;blockcanary监测UI卡顿的原理之一;根据不同设备设定不同的阈值
也有性能损耗,通过匹配日志字符串来找到卡顿信息,不过能准确拿到堆栈信息;字符串拼接会产生很多临时对象,临时对象会造成gc,频繁gc造成内存抖动造成性能问题

总结:两个方案结合,主线程的消息处理用在debug版本定位问题

4.内存占用
内存分为三大类:RAM物理内存,PSS,HEAP
RAM:一般意义的内存,一般只需要知道应用的总体占有就可以了
PSS:应用占用的实际物理内存,安卓应用会共享系统库,系统在计算PSS会平均到每个应用
heap:虚拟机的堆内存,虚拟机中有gc,频繁gc会造成性能影响

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

推荐阅读更多精彩内容