Android 屏幕刷新机制

基本概念

首先,先来过一下一些基本概念,摘抄自网上文章android屏幕刷新显示机制

在一个典型的显示系统中,一般包括CPU、GPU、display三个部分, CPU负责计算数据,把计算好数据交给GPU,GPU会对图形数据进行渲染,渲染好后放到buffer里存起来,然后display(有的文章也叫屏幕或者显示器)负责把buffer里的数据呈现到屏幕上。

显示过程,简单的说就是CPU/GPU准备好数据,存入buffer,display每隔一段时间去buffer里取数据,然后显示出来。display读取的频率是固定的,比如每个16ms读一次,但是CPU/GPU写数据是完全无规律的。

上述内容概括一下,大体意思就是说,屏幕的刷新包括三个步骤:CPU 计算屏幕数据、GPU 进一步处理和缓存、最后 display 再将缓存中(buffer)的屏幕数据显示出来。

对于 Android 而言,第一个步骤:CPU 计算屏幕数据指的也就是 View 树的绘制过程,也就是 Activity 对应的视图树从根布局 DecorView 开始层层遍历每个 View,分别执行测量、布局、绘制三个操作的过程。

image.png

Display 这一行可以理解成屏幕,所以可以看到,底层是以固定的频率发出 VSync 信号的,而这个固定频率就是我们常说的每 16.6ms 发送一个 VSync 信号,至于什么叫 VSync 信号,我们可以不用深入去了解,只要清楚这个信号就是屏幕刷新的信号就可以了。

看图,CPU 蓝色的这行,CPU 这块的耗时其实就是我们 app 绘制当前 View 树的时间,而这段时间就跟我们自己写的代码有关系了,如果你的布局很复杂,层次嵌套很多,每一帧内需要刷新的 View 又很多时,那么每一帧的绘制耗时自然就会多一点。

看图,CPU 蓝色这行里也有一些数字,其实这些数字跟 Display 黄色的那一行里的数字是对应的,在 Display 里这些数字表示的是每一帧的画面,那么在 CPU 这一行里,其实就是在计算对应帧的画面数据,也叫屏幕数据。也就是说,在当前帧内,CPU 是在计算下一帧的屏幕画面数据,当屏幕刷新信号到的时候,屏幕就去将 CPU 计算的屏幕画面数据显示出来;同时 CPU 也接收到屏幕刷新信号,所以也开始去计算下一帧的屏幕画面数据。

CPU 跟 Display 是不同的硬件,它们是可以并行工作的。要理解的一点是,我们写的代码,只是控制让 CPU 在接收到屏幕刷新信号的时候开始去计算下一帧的画面工作。而底层在每一次屏幕刷新信号来的时候都会去切换这一帧的画面,这点我们是控制不了的,是底层的工作机制。

当我们的 app 界面没有必要再刷新时(比如用户不操作了,当前界面也没动画),这个时候,我们 app 是接收不到屏幕刷新信号的,所以也就不会让 CPU 去计算下一帧画面数据,但是底层仍然会以固定的频率来切换每一帧的画面,只是它后面切换的每一帧画面都一样,所以给我们的感觉就是屏幕没刷新。

ViewRootImpl 和DecorView的绑定

分析 View#invalidate() 时,也可以看到内部其实是有一个 do{}while() 循环来不断寻找 mParent,所以最终才会走到 ViewRootImpl 里去,那么可能大伙就会疑问了,为什么 DecorView 的 mParent 会是 ViewRootImpl 呢?换个问法也就是,在什么时候将 DevorView 和 ViewRootImpl 绑定起来?

Activity 的启动是在 ActivityThread 里完成的,handleLaunchActivity() 会依次间接的执行到 Activity 的 onCreate(), onStart(), onResume()。在执行完这些后 ActivityThread 会调用 WindowManager#addView(),而这个 addView() 最终其实是调用了 WindowManagerGlobal 的 addView() 方法。

这里初始化了一个 ViewRootImpl,然后调用了它的 setView() 方法,将 DevorView 作为参数传递了进去,这个方法里还调用了一个 requestLayout() 方法,进而又调用了一个 scheduleTraversals()

setView() 方法里调用了 DecorView 的 assignParent() 方法,参数是 ViewParent,而 ViewRootImpl 是实现了 ViewParent 接口的,所以在这里就将 DecorView 和 ViewRootImpl 绑定起来了。

每个Activity 的根布局都是 DecorView,而 DecorView 的 parent 又是 ViewRootImpl,所以在子 View 里执行 invalidate() 之类的操作,循环找 parent 时,最后都会走到 ViewRootImpl 里来。

即使是界面上一个小小的 View 发起了重绘请求时,都要层层走到 ViewRootImpl,由它来发起重绘请求,然后再由它来开始遍历 View 树,一直遍历到这个需要重绘的 View 再调用它的 onDraw()方法进行绘制。

总结一下:其实打开一个 Activity,当它的 onCreate---onResume 生命周期都走完后,才将它的 DecoView 与新建的一个 ViewRootImpl 对象绑定起来,同时开始安排一次遍历 View 任务也就是绘制 View 树的操作等待执行,然后将 DecoView 的 parent 设置成 ViewRootImpl 对象

ViewRootImpl#scheduleTraversals

    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();//往主线程的消息队列中发送一个同步屏障,拦截这个时刻之后所有的同步消息的执行,但不会拦截异步消息,以此来尽可能的保证当接收到屏幕刷新信号时可以尽可能第一时间处理遍历绘制 View 树的工作;

            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);//把 performTraversals() 封装到 Runnable 里面,然后调用 Choreographer 的 postCallback() 方法;postCallback() 方法会先将这个 Runnable 任务以当前时间戳放进一个待执行的队列里,然后如果当前是在主线程就会直接调用一个native 层方法,如果不是在主线程,会发一个最高优先级的 message 到主线程,让主线程第一时间调用这个 native 层的方法;
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }
View刷新.png
  1. 界面上任何一个 View 的刷新请求最终都会走到 ViewRootImpl 中的 scheduleTraversals() 里来安排一次遍历绘制 View 树的任务;
  2. scheduleTraversals() 会先过滤掉同一帧内的重复调用,在同一帧内只需要安排一次遍历绘制 View 树的任务即可,这个任务会在下一个屏幕刷新信号到来时调用 performTraversals() 遍历 View 树,遍历过程中会将所有需要刷新的 View 进行重绘;
  3. 接着 scheduleTraversals() 会往主线程的消息队列中发送一个同步屏障,拦截这个时刻之后所有的同步消息的执行,但不会拦截异步消息,以此来尽可能的保证当接收到屏幕刷新信号时可以尽可能第一时间处理遍历绘制 View 树的工作;
  4. 发完同步屏障后 scheduleTraversals() 才会开始安排一个遍历绘制 View 树的操作,作法是把 performTraversals() 封装到 Runnable 里面,然后调用 Choreographer 的 postCallback() 方法;
  5. postCallback() 方法会先将这个 Runnable 任务以当前时间戳放进一个待执行的队列里,然后如果当前是在主线程就会直接调用一个native 层方法,如果不是在主线程,会发一个最高优先级的 message 到主线程,让主线程第一时间调用这个 native 层的方法;
  6. native 层的这个方法是用来向底层注册监听下一个屏幕刷新信号,当下一个屏幕刷新信号发出时,底层就会回调 Choreographer 的onVsync() 方法来通知上层 app;
  7. onVsync() 方法被回调时,会往主线程的消息队列中发送一个执行 doFrame() 方法的消息,这个消息是异步消息,所以不会被同步屏障拦截住;
  8. doFrame() 方法会去取出之前放进待执行队列里的任务来执行,取出来的这个任务实际上是 ViewRootImpl 的 doTraversal() 操作;
  9. 上述第4步到第8步涉及到的消息都手动设置成了异步消息,所以不会受到同步屏障的拦截;
  10. doTraversal() 方法会先移除主线程的同步屏障,然后调用 performTraversals() 开始根据当前状态判断是否需要执行performMeasure() 测量、perfromLayout() 布局、performDraw() 绘制流程,在这几个流程中都会去遍历 View 树来刷新需要更新的View;

ref:

Android 屏幕刷新机制

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

推荐阅读更多精彩内容