目录
写在前面
一、卡顿介绍及优化工具选择
1.1、卡顿问题介绍
1.2、优化工具选择
二、自动化卡顿检测方案及优化
2.1、为什么需要自动化卡顿检测
2.2、自动化卡顿检测方案原理
2.3、AndroidPerformanceMonitor
三、ANR实战分析
3.1、ANR介绍
3.2、ANR实战分析
3.3、线上ANR监控方案
四、应用界面秒开
4.1、界面秒开率统计
五、优雅监控耗时盲区
5.1、为什么会出现耗时盲区
5.2、耗时盲区监控线下方案
5.3、耗时盲区监控线上方案
最近过的有点累,身心俱疲,成年人的世界太难了,庆幸的是自己还是坚持着把今天的内容整理完了!
在上一篇中介绍了Android性能优化系列专栏中的布局优化——《你想知道的布局优化都在这里了》,今天就继续来说一下另外一个比较重要的性能优化点,也就是Android中的卡顿优化。
对于用户来说我们的应用当中的很多性能问题比如内存占用高、流量消耗快等不容易被发现,但是卡顿却很容易被直观的感受到,对于开发者来说,卡顿问题又难以定位,那么它究竟难在哪里呢?
卡顿问题难点:
①、CPU Profiler
使用方式:
②、Systrace
使用方式:
Systrace优点
③、StrictMode
现在到之前的Demo中来实际使用一下,找到我们的Application类,新增一个方法initStrictMode():
private void initStrictMode(){
if (DEV_MODE) {
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectCustomSlowCalls() //API等级11,使用StrictMode.noteSlowCode
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()// or .detectAll() for all detectable problems
.penaltyLog() //在Logcat 中打印违规异常信息
.build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.setClassInstanceLimit(FeedBean.class, 1)
.detectLeakedClosableObjects() //API等级11
.penaltyLog()
.build());
}
}
首先在这里加了一个标记位DEV_MODE,也就是只在线下开发的时候才会走到这个方法。对于线程策略使用方式就是StrictMode.setThreadPolicy,然后就是一些配置比如磁盘的读取、写入、网络监控等,如果出现了违规情况我们使用的是penaltyLog()方法在日志中打印出违规信息,这里你也可以选择别的方式。对于虚拟机策略这里是配置需要检测出Sqlite对象的泄露,并且这里还设置某个类的实例数量是x,如果大于x它应该会被检测出不合规。
我在这里从Looper.java的loop()方法的源码中截取了一段代码,大家看下:
// This must be in a local variable, in case a UI event sets the logger
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
......
此处省略一大段代码
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
它在Message执行的前后都打印了一段日志并且是不同的,所以我们可以通过这个来判断Message处理的开始和结束的时机。
具体的实现原理:
下面我们在项目中实际使用一下:
首先在application中进行初始化:
//BlockCanary初始化
BlockCanary.install(this,new AppBlockCanaryContext()).start();
这里入参有一个AppBlockCanaryContext,这个是我们自定义BlockCanary配置的一些信息:
public class AppBlockCanaryContext extends BlockCanaryContext {
public String provideQualifier() {
return "unknown";
}
public String provideUid() {
return "uid";
}
public String provideNetworkType() {
return "unknown";
}
public int provideMonitorDuration() {
return -1;
}
//设置卡顿阈值为500ms
public int provideBlockThreshold() {
return 500;
}
public int provideDumpInterval() {
return provideBlockThreshold();
}
public String providePath() {
return "/blockcanary/";
}
public boolean displayNotification() {
return true;
}
public boolean zip(File[] src, File dest) {
return false;
}
public void upload(File zippedFile) {
throw new UnsupportedOperationException();
}
public List concernPackages() {
return null;
}
public boolean filterNonConcernStack() {
return false;
}
public List provideWhiteList() {
LinkedList whiteList = new LinkedList<>();
whiteList.add("org.chromium");
return whiteList;
}
public boolean deleteFilesInWhiteList() {
return true;
}
public void onBlock(Context context, BlockInfo blockInfo) {
Log.i("jarchie","blockInfo "+blockInfo.toString());
}
}
然后在MainActivity中模拟一次卡顿,让当前线程休息2s,然后来看一下这个组件会不会通知我们:
try {
Thread.currentThread().sleep(2000);
}catch (InterruptedException e){
e.printStackTrace();
}
当我们把程序运行之后,会发现手机桌面上出现了一个Blocks的图标,这个玩意和之前我们使用LeakCanary的时候有点像哈,然后点进去果然发现了刚刚的Block信息,如下所示:
这里详细的打出了当前的CPU核心数、进程名、内存情况、block的堆栈信息等等,我们就可以根据这些堆栈找到对应哪个类的哪一行代码出现了问题,然后进行修改即可。
对于这种方案的总结如下:
这种方案网上有很多的使用资料,但是实际上它也是存在一定的问题的,自动检测方案的问题:
举个栗子:主线程在T1 T2时间段内发生了卡顿,卡顿检测方案获取卡顿堆栈的信息是T2时刻,但是实际情况可能是整个这一段时间之内某个函数的耗时过长导致的卡顿,捕获堆栈的时机此时该函数已经执行完成,所以在T2时刻捕获的堆栈信息并不能准确的反应现场情况。
自动检测方案优化
海量卡顿堆栈处理:高频卡顿上报量太大,会导致服务端有压力
ANR分类:Application Not Responding
ANR执行流程:
ANR解决套路:
接下来我们来模拟一次ANR的出现,回到项目中,首先在MainActivity中创建一个线程,并且让它持有当前Activity20秒,然后在主线程中我们再次申请这把锁,让它弹一个吐司,代码也都很简单,如下所示:
将项目跑起来,此时我操作系统的返回键,然后就真的出现了ANR异常,系统弹出了一个弹框询问你是继续等待还是关闭应用,点击关闭应用,此时traces.txt文件已经生成了。其实从代码中分析也可以看出添加的这些代码肯定会造成ANR异常,主线程要申请MainActivity.this这把锁,但此时这把锁是被我们开始创建的异步线程所持有着,必须要等到异步线程执行完成之后才能继续往下执行,意思也就是MainActivity的onCreate()方法要在这里卡顿20s。那么我们该如何将生成的traces.txt文件导出到我们本地进行分析呢?下面一起来看一下:
这里我们使用adb pull data/anr/traces.txt这个命令进行导出,如果你的手机和我的一样是高版本的,可能你会出现以下问题,它说找不到,这个问题真的很蛋疼,然后我使用adb shell命令,进到anr的目录下,查看该文件夹下的文件是有的:
此时我尝试直接pull这个文件名,很遗憾,依然不行,它会告诉你没有权限,这里可能需要root手机,我没有进行尝试,有条件的可以尝试一下root之后是否可以:
最后无奈我们使用adb bugreport这个命令,此命令导出一个zip的压缩包,这个过程可能会有点慢,耐心等待就OK了:
然后它会将文件导出到你命令行所在的当前目录下,找到压缩包解压之后,在FS/data/anr这个目录下就可以看到anr文件了:
然后我将我需要的anr文件打开,然后搜索应用包名,这里就看到了anr发生的地方以及具体的原因:
①、ANR-WatchDog
现在到Demo中实际应用一下,首先在build.gradle中引入这个库,然后在application中的onCreate()方法中作初始化操作:
new ANRWatchDog().start();
然后运行程序之后我这里按返回键进行按键事件交互,程序直接Crash掉了,这是ANR-WatchDog对于ANR的默认处理,然后来看下日志:
从日志我们可以看到它抛出的异常是在MainActivity的76行,同时告知我们main Thread的状态是blocked的,通过去代码中查找发现第76行正好就是发生锁冲突的地方:
②、ANR-WatchDog源码解析
下面我们跟着源码来看一下这个库它的实现原理是什么?这个库一共就两个类:ANRError和ANRWatchDog:
首先来看ANRWatchDog实际上是继承自Thread类,本质上它是一个线程,对于一个线程来说最重要的就是它的run()方法:
@Override
public void run() {
setName("|ANR-WatchDog|");
int lastTick;
int lastIgnored = -1;
while (!isInterrupted()) {
lastTick = _tick;
_uiHandler.post(_ticker);
。。。。省略部分代码,见下方分析中的代码
}
}
在run()方法中首先是对这个线程进行命名,接着声明了一个变量lastTick,然后进行while循环,在循环中它通过_uiHandler post了一个runnable即_ticker:
private final Runnable _ticker = new Runnable() {
@Override public void run() {
_tick = (_tick + 1) % Integer.MAX_VALUE;
}
};
这个runnable内部是进行+1的操作,接着这个线程就会sleep一段时间:
try {
Thread.sleep(_timeoutInterval);
}
catch (InterruptedException e) {
_interruptionListener.onInterrupted(e);
return ;
}
之后就会进行检测:
// If the main thread has not handled _ticker, it is blocked. ANR.
if (_tick == lastTick) {
if (!_ignoreDebugger && Debug.isDebuggerConnected()) {
if (_tick != lastIgnored)
Log.w("ANRWatchdog", "An ANR was detected but ignored because the debugger is connected (you can prevent this with setIgnoreDebugger(true))");
lastIgnored = _tick;
continue ;
}
ANRError error;
if (_namePrefix != null)
error = ANRError.New(_namePrefix, _logThreadsWithoutStackTrace);
else
error = ANRError.NewMainOnly();
_anrListener.onAppNotResponding(error);
return;
}
具体的是检测刚刚的runnable是否被执行,其实就是判断“+1”的操作有没有被执行,如果+1成功则判定runnable执行成功,说明主线程未发生卡顿,如果没有+1成功,则判定runnable未被执行,说明主线程已经发生了卡顿,并且它会拿到MainThread,通过主线程拿到堆栈信息,然后返回一个ANRError:
static ANRError NewMainOnly() {
final Thread mainThread = Looper.getMainLooper().getThread();
final StackTraceElement[] mainStackTrace = mainThread.getStackTrace();
return new ANRError(new $(getThreadTitle(mainThread), mainStackTrace).new _Thread(null));
}
然后通过anrListener调用onAppNotResponding()方法:即:_anrListener.onAppNotResponding(error);
public interface ANRListener {
public void onAppNotResponding(ANRError error);
}
这个方法的默认实现是直接将这个error给throw出去,这样就会导致程序崩溃:
private static final ANRListener DEFAULT_ANR_LISTENER = new ANRListener() {
@Override public void onAppNotResponding(ANRError error) {
throw error;
}
};
ANR-WatchDog的实现原理:
从上面我们知道ANR-Watchdog在每次发生ANR的时候,它是直接将异常抛出的,这样做其实是有一个问题的,就是每次发生ANR的时候应用都会异常崩溃掉,这其实给人的用户体验就很不好,那该怎么办呢?如果你继续阅读官方文档你会发现它有一个ANRListener,你可以覆写这个listener,在onAppNotResponding这个回调方法中自己实现ANR事件的定制化处理。好,如此一来我们就可以很方便的拿到ANR的堆栈信息,然后就可以上报服务端,进行日志的统计分析。
总结:①、非侵入式;②、弥补高版本无权限读取trace.txt文件问题;③、两种方式结合使用
区别:
应用界面秒开的实现方案:
这里来介绍一个开源方案:Lancet,它是一个轻量级的Android AOP框架:
下面我们来具体使用一下这个库,我们使用这个库来统计页面的onCreate()方法到onWindowsFocusChanged()方法之间的加载耗时情况:
①、添加依赖
这里大家可以参考github上的使用方式进行依赖的添加,主要是两个部分:工程的build.gradle和app module的build.gradle:
classpath 'me.ele:lancet-plugin:1.0.6' //工程的build.gradle
apply plugin: 'me.ele.lancet' //module的build.gradle
//lancet
compileOnly 'me.ele:lancet-base:1.0.6'
②、编写一个实体类,定义用于上述两个方法时间统计的成员变量:
public class ActivityLive {
public long mOnCreateTime;
public long mOnWindowsFocusChangedTime;
}
③、创建统计方法的工具类,在类中分别编写onCreate()和onWindowFocusChanged()方法,关于具体的注解的使用含义详见代码注释:
public class ActivityHooker {
public static ActivityLive mLive;
static {
mLive = new ActivityLive();
}
//@Insert:使用自己程序中自己的一些类需要添加,值这里就指定onCreate()方法,
//可配置项mayCreateSuper是当目标函数不存在的时候可以通过它来创建目标方法
//@TargetClass:框架知道要找的类是哪个,可配置项Scope.ALL:匹配value所指定的所有类的子类
@Insert(value = "onCreate",mayCreateSuper = true)
@TargetClass(value = "androidx.appcompat.app.AppCompatActivity", scope = Scope.ALL)
protected void onCreate(Bundle savedInstanceState) {
mLive.mOnCreateTime = System.currentTimeMillis();
Origin.callVoid(); //无返回值的调用
}
//注解含义同上面onCreate()
@Insert(value = "onWindowFocusChanged",mayCreateSuper = true)
@TargetClass(value = "androidx.appcompat.app.AppCompatActivity", scope = Scope.ALL)
public void onWindowFocusChanged(boolean hasFocus) {
mLive.mOnWindowsFocusChangedTime = System.currentTimeMillis();
Log.i("onWindowFocusChanged","---"+(mLive.mOnWindowsFocusChangedTime - mLive.mOnCreateTime));
Origin.callVoid();
}
}
下面运行程序来看下结果:
界面秒开监控纬度
对于一般的监控方案,它的监控指标只是一个大的范围,只是一个数据,比如:
如下代码所示,我首先在Activity的onCreate()方法中发送了一个msg,并且打印了一条日志
然后在列表展示的第一条同样打印一条日志:
最后输出的结果如下:
从执行结果来看,这个MSG是跑在Feed展示之前的,这个msg模拟的耗时是1s,此时用户看到界面的时间也就被往后延迟了1s。其实这个场景还是很常见的,因为我们可能由于某些业务需求在某个生命周期或者某个阶段及某些第三方的SDK中会做一些handler post的操作,这个操作很有可能会在列表展示之前被执行到,所以出现这种耗时的盲区,既普遍又不好排查。
耗时盲区监控难点
这种场景非常适合之前说过的一个工具,你能想到是什么吗?————答案是TraceView:
可行性方案:
嗯,写着写着天儿就黑了,又到了饭点了。OK,关于Android卡顿优化的部分,今天就先到这里了,后面如果有需要补充的,再进行补充吧,拜了个拜,下期再会!