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

简介: Android卡顿优化 | AndroidPerformanceMonitor(BlockCanary)源码详析(真的很详细哦!)
为了另外一篇性能优化实战方案讲解博客的结构清晰和篇幅,
我们“断章取义”,把框架的源码解析部分搬到这边哈~

项目GitHub

目录
1. 监控周期的 定义
2. dump模块 / 关于.log文件
3. 采集堆栈周期的 设定
4. 框架的 配置存储类 以及 文件系统操作封装
5. 文件写入过程(生成.log文件的源码)
6. 上传文件
7. 设计模式、技巧
8. 框架中各个主要类的功能划分

**1. 【监控周期的 定义】
blockCanary打印一轮信息的周期,
是从主线程一轮Message(任务)分发处理开始,到这个Message(任务)分发处理结束结束,为一轮信息;
这个周期我们也可以成为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 单例模式,不用多说,
刚刚提到BlockCanaryBlockCanaryInternals里边都用到了;
7.2 回调机制设计:
内部接口,供给回调:
定义内部接口的类,“抽象调用”回调接口方法:接口暴露给外部,在外部实现回调:
8 .框架中各个主要类的功能划分
-BlockCanary 提供给外部使用的,负责框架整体的方法调度;整体的、最顶层的调度;

-BlockCanaryInternals
 封装控制 周期性采集堆栈信息打印、输入的关键逻辑;
(卡顿判定阈值采集信息周期 的配置,都在这里首先被使用)
(注意这里的onBlockEvent() 回调方法
 封装文件操作模块(创建文件、创建文件目录、获取相关路径等等 这些
从SD卡根目录到存储.log文件目录 这个级别的处理,往下的目录下文件单位级别的处理,交给LogWriter)等核心逻辑;
 调用了LogWriter.save()进行log文件存储等;
 创建CpuSamplerStackSample实例,用于协助完成周期性采集

-LogWriter 封装了文件流写入、处理等逻辑;

-LooperMonitor协助完成周期性采集
【主要是阻塞任务始末的各种调度,即面向卡顿阈值
当然,调度的内容也包括对周期性采集启闭调度!!!!】;
如上,
&println()有点像闹钟的角色,
它在主线程的任务分发dispatchMessage前后分别被调用一次
它在采集周期开始的时候,就记录下开始时间
在阻塞任务完成之后,会再次被调用,记录下结束时间

&isBlock():借助println()中记录的关于主线程任务分发开始时间结束时间
来判断阻塞的时间是不是大于我们设定的或者默认卡顿判定时间
如果是,调用notifyBlockEvent(),间接调用到回调方法 onBlockEvent()
这个方法上面说了,在BlockCanaryInternals 的构造器中被具体实现了,
可以调用LogWriter 最终输出.log文件;

&startDump()stopDump()
我们可以看到在println()中还有startDump()stopDump()这两个方法,
分别也是在主线程任务分发开始结束时,随着println()被调用而被调用;startDump()stopDump()的内容正是控制两个Sample类的启闭
-CpuSamplerStackSample
同样负责协助完成周期性采集
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来开启;
而这里处理这个RunnableHandler正是
HandlerThreadFactory.getTimerThreadHandler()
HandlerThreadFactory是框架的提供的一个内部线程类,
源码解析如下,使用了工厂模式吼:
如此便可以获得,绑定了工作线程(子线程)的LooperHandler
有了这个Handler就可以处理刚刚说的Runnable任务单元了;

【2. HandlerRunnable任务单元启闭是在哪个地方?】
当然是在AbstractSampler提供的start()stop()里边了;
CpuSamplerStackSample都会继承自AbstractSampler,自然也就继承了这start()stop()
最后上面讲过了,
LooperMonitorprintln()中,
startDump()stopDump()会被调用,
而在startDump()stopDump()中,
CpuSamplerStackSample实例的start()stop()也会被调用了,
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
从而控制了周期采集信息工作线程(子线程)任务单元【上述的Runnable实例】的启闭
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!


【3. doSample()的实现】
CpuSamplerStackSample都继承自AbstractSample
使用的都是从AbstractSample继承过来的Runnable实例

前面说过这个Runnable单元start()之后,
将会每隔一个采集周期,就执行一次run()和其中的doSample()
进行堆栈信息CPU信息的周期性采集工作;是这样的,
然后CpuSamplerStackSample通过对父类抽象方法doSample()做了不同的实现,
使得各自循环处理的任务内容不同罢了;
CpuSampler的面向CPU信息的处理,
StackSample则对堆栈信息的收集;】


-BlockCanaryContext 框架配置类的超类,提供给外部进行集成和配置信息:**

相关文章
|
17天前
|
XML Java Android开发
Android实现自定义进度条(源码+解析)
Android实现自定义进度条(源码+解析)
49 1
|
17天前
|
Java Android开发
Android反编译查看源码
Android反编译查看源码
22 0
|
20天前
|
缓存 监控 Java
构建高效Android应用:从优化用户体验到提升性能
在竞争激烈的移动应用市场中,为用户提供流畅和高效的体验是至关重要的。本文深入探讨了如何通过多种技术手段来优化Android应用的性能,包括UI响应性、内存管理和多线程处理。同时,我们还将讨论如何利用最新的Android框架和工具来诊断和解决性能瓶颈。通过实例分析和最佳实践,读者将能够理解并实施必要的优化策略,以确保他们的应用在保持响应迅速的同时,还能够有效地利用系统资源。
|
26天前
|
调度 数据库 Android开发
构建高效Android应用:Kotlin协程的实践与优化
在Android开发领域,Kotlin以其简洁的语法和平台友好性成为了开发的首选语言。其中,Kotlin协程作为处理异步任务的强大工具,它通过提供轻量级的线程管理机制,使得开发者能够在不阻塞主线程的情况下执行后台任务,从而提升应用性能和用户体验。本文将深入探讨Kotlin协程的核心概念,并通过实例演示如何在实际的Android应用中有效地使用协程进行网络请求、数据库操作以及UI的流畅更新。同时,我们还将讨论协程的调试技巧和常见问题的解决方法,以帮助开发者避免常见的陷阱,构建更加健壮和高效的Android应用。
35 4
|
29天前
|
数据库 Android开发 开发者
构建高效Android应用:采用Kotlin协程优化网络请求处理
【2月更文挑战第30天】 在移动应用开发领域,网络请求的处理是影响用户体验的关键环节。针对Android平台,利用Kotlin协程能够极大提升异步任务处理的效率和简洁性。本文将探讨如何通过Kotlin协程优化Android应用中的网络请求处理流程,包括协程的基本概念、网络请求的异步执行以及错误处理等方面,旨在帮助开发者构建更加流畅和响应迅速的Android应用。
|
18天前
|
Java Android开发 开发者
构建高效Android应用:Kotlin协程的实践与优化
在响应式编程范式日益盛行的今天,Kotlin协程作为一种轻量级的线程管理解决方案,为Android开发带来了性能和效率的双重提升。本文旨在探讨Kotlin协程的核心概念、实践方法及其在Android应用中的优化策略,帮助开发者构建更加流畅和高效的应用程序。通过深入分析协程的原理与应用场景,结合实际案例,本文将指导读者如何优雅地解决异步任务处理,避免阻塞UI线程,从而优化用户体验。
|
1天前
|
缓存 移动开发 Android开发
构建高效Android应用:从优化用户体验到提升性能表现
【4月更文挑战第18天】 在移动开发的世界中,打造一个既快速又流畅的Android应用并非易事。本文深入探讨了如何通过一系列创新的技术策略来提升应用性能和用户体验。我们将从用户界面(UI)设计的简约性原则出发,探索响应式布局和Material Design的实践,再深入剖析后台任务处理、内存管理和电池寿命优化的技巧。此外,文中还将讨论最新的Android Jetpack组件如何帮助开发者更高效地构建高质量的应用。此内容不仅适合经验丰富的开发者深化理解,也适合初学者构建起对Android高效开发的基础认识。
2 0
|
12天前
|
XML 开发工具 Android开发
构建高效的安卓应用:使用Jetpack Compose优化UI开发
【4月更文挑战第7天】 随着Android开发不断进化,开发者面临着提高应用性能与简化UI构建流程的双重挑战。本文将探讨如何使用Jetpack Compose这一现代UI工具包来优化安卓应用的开发流程,并提升用户界面的流畅性与一致性。通过介绍Jetpack Compose的核心概念、与传统方法的区别以及实际集成步骤,我们旨在提供一种高效且可靠的解决方案,以帮助开发者构建响应迅速且用户体验优良的安卓应用。
|
26天前
|
缓存 前端开发 Android开发
构建高效Android应用:从设计原则到性能优化
随着移动设备成为我们日常生活不可或缺的一部分,开发一个流畅且响应迅速的Android应用变得至关重要。本文将探讨如何通过遵循Android设计原则和实施细致的性能优化策略来构建高效的Android应用程序。我们将深入分析应用架构的选择、内存管理的要点以及UI设计的优化,旨在为开发人员提供一套实用的指导方针,帮助他们提升应用的整体性能和用户体验。
|
29天前
|
监控 Java Android开发
构建高效Android应用:从内存管理到性能优化
【2月更文挑战第30天】 在移动开发领域,打造一个流畅且响应迅速的Android应用是每个开发者追求的目标。本文将深入探讨如何通过有效的内存管理和细致的性能调优来提升应用效率。我们将从分析内存泄露的根本原因出发,讨论垃圾回收机制,并探索多种内存优化策略。接着,文中将介绍多线程编程的最佳实践和UI渲染的关键技巧。最后,我们将通过一系列实用的性能测试工具和方法,帮助开发者监控、定位并解决性能瓶颈。这些技术的综合运用,将指导读者构建出更快速、更稳定、用户体验更佳的Android应用。