App卡顿-从零了解到系统解决

一、 卡顿有哪些场景

    首先,回想下在什么情况下你会觉得某个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系统上我们稍微深入一点,得到一个更详细的流程。
Android显示原理-从View到手机屏幕上的图像

    从上述一张图绘制流程图可以看到,系统绘制一张图比较耗时间的点在于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 感官优化
用户觉得慢,这个问题是“用户觉得慢”,不一定是真的慢。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,245评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,749评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,960评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,575评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,668评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,670评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,664评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,422评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,864评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,178评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,340评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,015评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,646评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,265评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,494评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,261评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,206评论 2 352

推荐阅读更多精彩内容