Android卡顿优化 | AndroidPerformanceMonitor(BlockCanary)源码详析(真的很详细哦!)
- 2020 年 4 月 9 日
- 筆記
为了另外一篇性能优化实战方案讲解博客的结构清晰和篇幅, 我们“断章取义”,把框架的源码解析部分搬到这边哈~ 项目GitHub
目录 1. 监控周期的 定义 2. dump模块 / 关于.log文件 3. 采集堆栈周期的 设定 4. 框架的 配置存储类 以及 文件系统操作封装 5. 文件写入过程(生成.log文件的源码) 6. 上传文件 7. 设计模式、技巧 8. 框架中各个主要类的功能划分
1. 【监控周期的 定义】 blockCanary打印一轮信息的周期, 是从主线程一轮阻塞的开始
开始,到阻塞的结束
结束,为一轮信息; 这个周期我们也可以成为BlockCanary
的监控周期/监控时间段
;
2. 【dump模块 / 关于.log文件】 这一个周期的信息,除了展现在通知
处,还会展示在logcat
处, 同时框架封装了dump模块
, 即框架会把我们这一轮信息,在手机(移动终端)内存中, 输出成一个.log
文件; 【当然,前提是在终端需要给这个APP授权
,允许APP读写内存
】 存放.log
文件的目录名,我们可以在上面提到的配置类
中自定义
:
如这里定义成blockcanary
,
在终端生成的文件与目录便是这样:
3. 【采集堆栈周期的 设定】 我们说过配置类
中,这个函数可以指定认定为卡顿的阈值时间
:
这里指定为500ms
,使得刚刚那个2s
的阻塞被确定为卡顿问题
;
其实还有一个函数,
用于指定在一个监控周期
内,采集数据的周期
!!!:
这里返回的同样是500ms
,
即从线程阻塞
开始,每500ms
采集一次数据,
给出一个阻塞问题出现的根源
;
而刚刚那个卡顿问题
阻塞的时间是2s
,
那毫无疑问我们可以猜到,刚刚那个.log
文件的内容里边,
有2s/500ms = 4
次采集的堆栈信息
!!
但是一个监控周期/log文件
只打印一次现场的详细信息
:
如果设置为250ms
,那便是有2s/250ms = 8
次采集的堆栈信息
了:
4. 【框架的 配置存储类 以及 文件系统操作封装】 框架准备了一个存储配置的类,用于存储响应的配置: 配置存储类:
–getPath()
:拿到sd卡根目录到存储log文件夹的目录路径
;!!!!!!!!
–detectedBlockDirectory()
:返回file类型
的 存储log文件
的文件夹
的目录
(如果没有这个文件夹,就创建文件夹,再返回file类型
的这个文件夹);!!!!!!!!!!
–getLogFiles()
:
如果detectedBlockDirectory()
返回的那个存储log文件
的文件夹
的目录
存在的话,
就把这个目录下所有的.log
文件过滤提取出来,
并存储在一个File[](即File数组)
里边,最后返回这个File数组
;!!!!!!!
–getLogFiles()
中的listFiles()
是JDK中的方法,
用来返回文件夹类型的File类实例
其 对应文件夹中(对应目录下)
所有的文件,
这里用的是它的重载方法,
就是传入一个过滤器,可以过滤掉不需要的文件;!!!!!!!
–BlockLogFileFilter
是过滤器,用于过滤出.log
文件;
###下面稍微实战一下这个文件封装:
呐我们在MainActivity的onCreate中,使用getLogFiles()
,
功能是刚说的获取BlockCanary生成的所有.log
文件,以.log
文件的形式返回,
完了我们把它打印出来:
运行之后,呐,毫无悬念,BlockCanary生成的所有.log
文件都被打印出来了:
拿到了文件, 意味着我们可以在适当的时机, 将之上传到服务器处理!!!
5. 【文件写入过程(生成.log文件的源码)】
- 一切要从框架的初始化开始说起:
install()
做了什么,install()
里边,初始化了BlockCanaryContext和Notification等的一些对象, 重要的,最后return
调用了,get()
; 有点单例的味道哈,BlockCanary
的构造方法是私有的(下图可以见得),get()
正是返回一个BlockCanary
实例, 当然new这一下也就调用了BlockCanary
的构造方法; 哦~BlockCanary
的构造方法中, 调用了BlockCanaryInternals.getInstance();
, 拿到一个BlockCanaryInternals
实例,赋给类中的全局变量!
BlockCanaryInternals.getInstance();
同样是使用了单例模式, 返回一个BlockCanaryInternals
实例:
同样也是new时候调用了BlockCanaryInternals
的构造方法:
可以看到BlockCanaryInternals
的构造方法中 出现了关于配置信息存储类
以及文件的写入逻辑
了; LogWriter.save(blockInfo.toString());
注意这里传入的是配置信息的字符串
,接着是LogWriter.save()
,这里的str便是刚刚的blockInfo.toString()
,即配置信息;
往下还有一层save(一参对应刚刚的字符串"looper",二参为Block字符串信息【最早是来自BlockCanaryInternals中的LogWriter.save(blockInfo.toString());中的 blockInfo.toString() 】)
:
可以看到.log
文件名的命名规则的就是定义在这里了, 往.log文件
写入的输入流逻辑,也都在这里了;
对比一下刚刚实验的结果,也就是实际生成的.log文件
的文件名
, 可见文件名跟上面save()
方法中定义好的规则是一样的,无误;
这两个在表头的字符串格式化器, 第一个是用来给.log文件
命名的,.log文件名
中的时间序列来自这里; 第二个是在save()函数中,用来写入文件的, 用时间来区分堆栈信息的每一次收集:
下面这个方法是用来构造zip文件实例的, 给出一个文件名,再构造一个成对应的File实例;
这个则是用来删除本框架生成的所有log文件的:
其他的很容易看懂,就不多说了;
6.【上传文件】 首先框架想得很周到哈,它已经为我们封装了一个Uploader
类,源码如下:
/* * Copyright (C) 2016 MarkZhai (http://zhaiyifan.cn). * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ ... final class Uploader { private static final String TAG = "Uploader"; private static final SimpleDateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US); private Uploader() { throw new InstantiationError("Must not instantiate this class"); } private static File zip() { String timeString = Long.toString(System.currentTimeMillis()); try { timeString = FORMAT.format(new Date()); } catch (Throwable e) { Log.e(TAG, "zip: ", e); } File zippedFile = LogWriter.generateTempZip("BlockCanary-" + timeString); BlockCanaryInternals.getContext().zip(BlockCanaryInternals.getLogFiles(), zippedFile); LogWriter.deleteAll(); return zippedFile; } public static void zipAndUpload() { HandlerThreadFactory.getWriteLogThreadHandler().post(new Runnable() { @Override public void run() { final File file = zip(); if (file.exists()) { BlockCanaryInternals.getContext().upload(file); } } }); } }
都封装成zip文件了,想得很周到很齐全吼, 点一下这个upload
,又回到配置类BlockCanaryContext
这儿来,
或者可以参考一下 这篇博客!!!!!!!,
可以在后台开启一个线程,定时扫描并上传。
或者
可以利用一下刚刚提到的 框架的文件系统操作封装 ,
再结合 自定义网络请求逻辑,
把文件上传到服务器也是ok的!
7. 设计模式、技巧: 7.1 单例模式,不用多说, 刚刚提到BlockCanary
和BlockCanaryInternals
里边都用到了; 7.2 回调机制设计: 内部接口,供给回调:
定义内部接口的类,“抽象调用”回调接口方法:
接口暴露给外部,在外部实现回调:
8 .框架中各个主要类的功能划分
–BlockCanary
提供给外部使用的,负责框架整体的方法调度;整体的、最顶层的调度;
–BlockCanaryInternals
封装控制 周期性采集堆栈信息
并打印、输入
的关键逻辑;
(卡顿判定阈值
、采集信息周期
的配置,都在这里首先被使用)
(注意这里的onBlockEvent() 回调方法
)
封装文件操作模块(创建文件、创建文件目录、获取相关路径等等 这些
从SD卡根目录到存储.log
文件目录 这个级别的处理,往下的目录下文件单位级别的处理,交给LogWriter
)等核心逻辑;
调用了LogWriter.save()
进行log文件存储等;
创建CpuSampler
、StackSample
实例,用于协助完成周期性采集
;
–LogWriter
封装了文件流
的写入、处理
等逻辑;
–LooperMonitor
协助完成周期性采集
【主要是阻塞任务始末的各种调度,即面向卡顿阈值
;
当然,调度的内容也包括对周期性采集
的启闭调度
!!!!】;
如上,
&println()
有点像闹钟的角色,
它在主线程的任务分发dispatchMessage前后
分别被调用一次
;
它在采集周期开始的时候
,就记录下开始时间
,
在阻塞任务完成之后
,会再次被调用,记录下结束时间
,
&isBlock()
:借助println()
中记录的关于主线程任务分发
的开始时间
和结束时间
,
来判断阻塞的时间
是不是大于我们设定
的或者默认
的卡顿判定时间
,
如果是,调用notifyBlockEvent()
,间接调用到回调方法 onBlockEvent()
,
这个方法上面说了,在BlockCanaryInternals
的构造器中被具体实现了,
可以调用LogWriter
最终输出.log
文件;
&startDump()
和stopDump()
:
我们可以看到在println()
中还有startDump()
和stopDump()
这两个方法,
分别也是在主线程任务分发
的开始
和结束
时,随着println()
被调用而被调用;
而startDump()
和stopDump()
的内容正是控制两个Sample
类的启闭
:
–CpuSampler
、StackSample
同样负责协助完成周期性采集
【CpuSampler
的逻辑主要是面向CPU信息
的处理,而
StackSample
的逻辑主要是对堆栈信息
的收集;
他们都继承自AbstractSample
】
首先在上面的源码我们可以看到,
在BlockCanaryInternals
的构造器中,
就分别创建了一个CpuSampler
(参数正为采集堆栈信息周期
属性)和一个StackSample
实例(参数为采集堆栈信息周期
属性):
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
这个参数一路上走,经过CpuSampler
的构造器,
最终是在CpuSampler
的父类AbstractSampler
中被使用!!!!!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
我们可以看到在AbstractSampler
中,
AbstractSampler
构造器接收了采集堆栈信息周期
属性,
同时准备了一个Runnable任务单元,
任务run()中做了两件事,
第一件事是调用抽象方法doSample()
;
第二件事是基于这个采集堆栈周期
属性这个Runnable单元
,
创建一个循环定时任务
!!!!!!!!!!!!!!!!!!!!!!!!!
即,
这个Runnable单元
被start()
之后,
将会每隔一个采集周期
,就执行一次run()
和其中的doSample()
;
进行堆栈信息
和CPU信息
的周期性采集工作;
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
这便是BlockCanary
能够周期采集堆栈信息
的根源!!!!!!!!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
那接下来我们可以展开三个点,
解决这个三个疑问点,脉络就理得差不多了:
【1. 由哪个Handler
来处理这个Runnable
】
我们知道,
Android中的多线程任务单元
可以由一个Handler
去post或者postDelayed一个Runnable
来开启;
而这里处理这个Runnable
的Handler
正是
HandlerThreadFactory.getTimerThreadHandler()
,
HandlerThreadFactory
是框架的提供的一个内部线程类,
源码解析如下,使用了工厂模式吼:
如此便可以获得,绑定了工作线程(子线程)的Looper
的 Handler
;
有了这个Handler
就可以处理刚刚说的Runnable
任务单元了;
【2. Handler
对Runnable任务单元
的启闭
是在哪个地方?】
当然是在AbstractSampler
提供的start()
和stop()
里边了;
CpuSampler
而StackSample
都会继承自AbstractSampler
,自然也就继承了这start()
和stop()
;
最后上面讲过了,
在LooperMonitor
的println()
中,
startDump()
和stopDump()
会被调用,
而在startDump()
和stopDump()
中,
CpuSampler
和StackSample
实例的start()
和stop()
也会被调用了,
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
从而控制了周期采集信息
的工作线程(子线程)任务单元
【上述的Runnable实例
】的启闭
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
【3. doSample()
的实现】
CpuSampler
、StackSample
都继承自AbstractSample
,
使用的都是从AbstractSample
继承过来的Runnable实例
;
前面说过这个Runnable单元
被start()
之后,
将会每隔一个采集周期
,就执行一次run()
和其中的doSample()
;
进行堆栈信息
和CPU信息
的周期性采集工作;
是这样的,
然后CpuSampler
、StackSample
通过对父类抽象方法doSample()
做了不同的实现,
使得各自循环处理的任务内容不同罢了;
【CpuSampler
的面向CPU信息
的处理,
而StackSample
则对堆栈信息
的收集;】
–BlockCanaryContext
框架配置类的超类,提供给外部进行集成和配置信息: