前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android卡顿优化 | AndroidPerformanceMonitor(BlockCanary)源码详析(真的很详细哦!)

Android卡顿优化 | AndroidPerformanceMonitor(BlockCanary)源码详析(真的很详细哦!)

作者头像
凌川江雪
发布2020-04-09 15:35:35
1.4K0
发布2020-04-09 15:35:35
举报
文章被收录于专栏:李蔚蓬的专栏

为了另外一篇性能优化实战方案讲解博客的结构清晰和篇幅, 我们“断章取义”,把框架的源码解析部分搬到这边哈~ 项目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**类,源码如下:

代码语言:javascript
复制
/*
 * 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** 框架配置类的超类,提供给外部进行集成和配置信息:

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
文件存储
文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档