一、 卡顿有哪些场景
首先,回想下在什么情况下你会觉得某个App很卡,不妨设想现在从手机桌面打开一个App-简书。
1 当App启动后很久才进入主页面,你会觉得卡;
2 当主页面内容很久才展示完全,你会觉得卡;
3 当在列表页面滑动时出现停顿,你会觉得卡;
4 当点击某篇文章停留很久才跳转,你会觉得卡;
5 当点击订阅按钮,很久才有订阅成功反馈,你会觉得卡;
总结提炼下,你会发现卡顿这种视觉感知问题归根到底是事件处理和UI展示的综合消耗时间超出了用户感官系统的期待时间。精确一点就可给出如下定义:
二、卡顿定义
在能够感知的视觉场景中,当事件处理(思考)和UI展示(表达)的综合消耗时间超过用户视觉系统的最大期待时间,我们就说出现了卡顿。
在卡顿描述中有提到一个概念用户视觉系统的期待时间,这个期待时间是主观的,但要小于大多数用户的期待时间,它在一定条件下又是客观的。比如当点击订阅按钮,App会弹出订阅成功或者订阅失败弹框,有人等待1s也没觉得有问题,也有人超过0.4s就感觉体验很不爽,但我们开发者要关注的是所有用户的期待时间,所以阈值一定是让大多数用户感觉爽的,若点击App中任意按钮、视图,都在0.1s内给出反馈,这样基本上99%的人都是感觉-哇哦~你们App反应好快。
除了上述事件反馈时间,还有一个我们人类视觉系统硬件带来的期待时间,那就是连续动画中单个画面的渲染时间。连续的动画中一个画面的如果在视觉暂留时间内没有渲染好,显示系统将会展示上一帧页面,那么对于用户来说就是发生了卡顿。通常用fps衡量渲染速度,fps(frame per second)是一秒钟系统的渲染页面的总次数。渲染速度越快,平均一帧渲染时间就越短,我们就感觉越丝滑。但由于人类的视觉暂留时间基本都大于16.6ms,所以我们单帧渲染时间小于16.6ms(也就是帧率60fps以上)就可以使大多数人感觉非常流畅。
通过卡顿的定义,我们找到了解决卡顿的关键两要素:
1 事件数据处理时间
2 UI渲染时间
我们要做的是在用户能够感知的使用场景中,给出优化事件处理时间和UI渲染时间的方案。
结合用户感知最多的卡顿场景,可以得出一个较全面的卡顿解决方案:
三、最常见卡顿的解决方案
3.1 App冷启动优化
冷启动具体时间段界定
startTime:用户点击桌面图标开始
点击桌面图标->点击事件回调到桌面(Launcher) App->Launcher处理点击事件,收集该图标相关的信息,发起intent调用->跨进程调用AMS启动对应的进程
然后再ActivityStarter打印log:
2021-12-16 14:18:50.402 24772-24772/com.example.demo V/JG: launcher onClick start
2021-12-16 14:18:50.403 534-945/system_process I/ActivityTaskManager: START u0 {flg=0x10000000 cmp=com.jingang.lifechange/.SplashActivity} from uid 10164
2021-12-16 14:18:50.449 534-563/system_process I/ActivityManager: Start proc 24932:com.jingang.lifechange/u0a162 for pre-top-activity {com.jingang.lifechange/com.jingang.lifechange.SplashActivity}
//第一句log是我们模拟桌面App的点击事件;第二句log是AMS开始启动SplashActivity,第三句log是ams发现该Activity所在的进程未启动去启动进程。
从点击桌面到这两个log打印,过程有很多步骤,但这些步骤里面没有耗时操作,一般情况下非常短暂(2ms内),而且没有log,所以一般情况下把上述第二句logSTART u0 ~*的时间当作App冷启动的开始时间。当然一些特殊情况需要定位问题到底在哪边需要精确定位时间,我们就要想办法去无限接近用户点击桌面图标的时间,这个需要去了解Android 输入系统或观察Launcher app点击日志(基本没有),以后介绍。
但只有代码运行到自己的App进程之后,我们才能有所作为,所以还要记录下冷启动时候App最早收到回调的时间点-这也是优化的起点时间:
public class MainApplication extends Application {
private static final String TAG = "lifeCycle:"+MainApplication.class.getName();
@Override
protected void attachBaseContext(Context base) {
Log.v(TAG,"attachBaseContext");
super.attachBaseContext(base);
}
}
endTime:第一个页面主体内容展示出来结束
如果有splash页面,那就是Home页面主体展示出来结束。
home页面主体展示出来,比较精确的就是取第一帧图像绘制出来的时间。
@Override
protected void onResume() {
super.onResume();
Log.v(getTag(),"onResume");
getWindow().getDecorView().getViewTreeObserver().addOnDrawListener(new ViewTreeObserver.OnDrawListener() {
@Override
public void onDraw() {
Log.v(getTag(),"onDecorView draw , draw time");
}
});
}
当然也可以利用系统log,如下的第二句log
2021-12-16 15:19:35.979 28022-28022/com.jingang.lifechange V/lifeCycle:MainActivity: onDecorView draw , draw time 0
2021-12-16 15:19:36.010 534-561/system_process I/ActivityTaskManager: Displayed com.jingang.lifechange/.MainActivity: +1s863ms
2021-12-16 15:19:36.018 28022-28022/com.jingang.lifechange V/lifeCycle:MainActivity: onDecorView draw , draw time 1
2021-12-16 15:19:36.040 28022-28022/com.jingang.lifechange V/lifeCycle:MainActivity: onDecorView draw , draw time 2
冷启启动时间指标
4s以内良,8s以外差————用户点击后桌面图标后, 心里开始 数1 、2、 3 到4页面还没有出来,用户开始着急,数到7、8没出来用户一般就放弃了。也就是上面我们记录的结束时间减去开始时间最好不好超过4s。
冷启动优化方案
原则: 视觉优化、异步、 懒加载、协调加载顺序
实现:
1)用户点击桌面应用图标——到正式展示出来假如只有4S,但4S内出现了点击无反应、黑屏、白屏,那用户主观感觉也是非常糟糕。所以我们把视觉优化放到第一个要优化的项目,最常见的就是把冷启动的Activity增加一个带有背景(闪屏图)的主体(Theme)。
2)把没有必要放在UI线程中的初始化任务放入其他线程;
3)把没有必要在应用启动时的初始化任务移动到真正使用之前,或者利用应用线程空余时间进行;
4)注意很多初始化任务是有顺序的,在优化过程中这些顺序要注意保持;
如果是一个大型项目,app启动初始化任务特别多,有很多初始化任务之间有加载顺序问题,初始化也不一定在UI线程进行,那我们可以大干一场,构建一个App启动器。App启动器是一个任务调度工具类,可以把不同的初始化任务按照顺序在不同线程中执行,从而使App启动正确而高效,做好之后也可以在不同项目中复用。这个以后再探讨如何设计和实现。
3.2 页面跳转卡顿优化
页面跳转时间段界定
startTime:发起页面用户事件
endTime:打开页面首帧加载完成
页面跳转时间指标
1s内- 页面秒开,无他页面秒开基本称为一个用户在App内操作的一个潜在标准,当然跟用户主观感受有关。
页面跳转优化概览
普通模式下:Activity A跳转Activity B生命周期
A onPause()->B onCreate()-> B onStart()->B onResume()->A onStop()
B页面的首帧加载是在B onResume()回调后进行View的测量、布局、绘制,同本文冷启动结束的时间节点,可以参考上面进行。
由页面跳转的计算开始结束时间点,可得如下具体点优化点:
1)页面跳转发起页面Activity A,onPause()内尽量减少UI线程耗时操作,可提升这个页面打开其他页面的速度;
2)页面跳转发起页面Activity A,onStop()的UI线程耗时操作,虽然不会使页面跳转看起来加快,但因为onStop是这个用户操作最后一个环节,所以减少耗时操作可以减少出现ANR的概率;
3)被打开页面Activity B 在onCreate()\onStart()\onResume()方法中尽量减少UI线程的耗时操作,提升这个页面被打开的速度 。比如一些耗时操作移动到idleHandler中;
4)对Activity B 的View层级、布局、绘制等进行优化也可以加快页面B的打开速度,这个会在接下来章节继续展开。
原则
根据不同模式下Activity启动,涉及到的生命周期变化进行跟踪优化。
3.3 页面滑动,属性动画、帧动画等动画
动画的本质是什么?
动画和视频的本质都是按一定顺序快速展示的一组图片,因为人眼的视觉暂留原理就形成了连续移动的感觉。
图片的本质是什么?
一张图片的本质是一组像素点
图片的这一组像素点如何得来
1 现成图片:各种图片格式,虽然可能有压缩,但一张jpg,png等格式的图片本质就是一组像素点。
显示的时候就是把这组像素点从网络、硬盘等地方读入到内存,由内存完成一些校验工作,并缓存起来,等待特定时机(接收到vsync信号时)写入到显示器缓存区-从而显示出来。
2 由数据生成
操作系统都提供一套用户定义图像api--比如Android的显示系统中View就是提供给用户自定义图像的api,用户按照一定规范调用api描述自己想要的图像,系统在特定时机(接收到vsync信号时)就把这种用户规范的描述转换成一组像素点,写入显示器缓存,从而显示出来。
我们上面谈到的页面滑动、属性动画都属于第二种情况——图片是由操作系统根据用户的描述一步步生成,所以接下来的重点是讨论这种情况如何显示和优化。
计算机显示一张图片,其实跟我们自己找人画一张画像然后送给朋友非常相似,都是由我们把想画的场景告诉画家,画家取纸张绘制,最后我们拿到画作,去展示给心爱的人。
在android系统上我们稍微深入一点,得到一个更详细的流程。
从上述一张图绘制流程图可以看到,系统绘制一张图比较耗时间的点在于1) 读取和转换用户描述 ,2)绘制(包含渲染)
除此之外,我们大多数绘制工作是要在ui线程进行,虽然android系统有消息屏障机制可保证绘制任务优先级很高,但是ui线程并不能把当前正在执行的任务终止,所以在进行绘制流程时候,遇到UI线程中有耗时很多的任务,也会导致绘制被推迟,从而造成卡顿、丢帧等。
所以我们优化从以下两个方面进行:
1、布局优化
布局优化就是指是采用尽量节约的方式指达到同样的显示效果。比如减少 View 层级,这样会加快 View 的循环遍历过程,比如view层级优化可以减少view;去除不必要的背景(背景是单独绘制),可以使绘制内容减少;减少View 的过度绘制;提前把xml转换成java代码,减少解析时间等;
2、 减少UI线程中耗时任务和异常阻塞
前面有讲到过UI线程会处理完当前任务,才进行绘制,如果在我们申请绘制的时候有耗时任务在执行,那势必会影响正常显示,android系统三缓冲机制,所以原则上一个ui线程任务超过16*3=48ms的时候就会对绘制任务造成影响,所以我们要减少这些任务。在full GC的时候也停止ui线程,影响绘制造成卡顿。
如何检测
1 布局优化检测
Hierarchy Viewer、开发者模式过度绘制开关等
2 ui耗时任务检测
通过类似下面的计算出ui线程每个任务执行的时间,找到耗时比较长的时间进行优化。
getMainLooper().setMessageLogging(new LogPrinter(Log.INFO,"uiThread"));
可以通过hook等方法,插入定位问题所需要的信息。
五 思考题目
系统有哪些监控措施是处理卡顿的?
答:ANR 、strictmode
本文分析的可全面?
答:并没有, 我们只是找了启动流程中最可能有问题的地方拿出来分析讲解,不代表其他地方没有问题,比如跨进程通信 、AMS状态、当前系统cpu和内存使用状态等。
关于流畅度未来
1 卡顿预测
基于对用户行为的洞察,可以预测到接下来会该显示哪些内容,提前进行数据初始化,甚至提前走完所有绘制步骤。
2 分工与异步
对绘制过程再进行重新解读,将当前顺序执行的再次进行分工,充分利用当前多cpu和gpu架构。
3 显示效果与功耗更加平衡
动态多线程,动态开启gpu
4 感官优化
用户觉得慢,这个问题是“用户觉得慢”,不一定是真的慢。