Android 屏幕刷新原理笔记

概述

屏幕刷新包括三个步骤:

  • CPU计算屏幕数据,,把计算好数据交给GPU。
  • GPU会对图形数据进行渲染,渲染好后放到buffer里存起来。
  • 接下来display负责把buffer里的数据呈现到屏幕上。

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

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

也就是说,我们常说的 Android 每隔 16.6ms 刷新一次屏幕其实是指:底层以固定的频率,比如每 16.6ms 将 buffer 里的屏幕数据显示出来。

在这里插入图片描述

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

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

  • 我们常说的 Android 每隔 16.6 ms 刷新一次屏幕其实是指底层会以这个固定频率来切换每一帧的画面。
  • 这个每一帧的画面也就是我们的 app 绘制视图树(View 树)计算而来的,这个工作是交由 CPU 处理,耗时的长短取决于我们写的代码:布局复不复杂,层次深不深,同一帧内刷新的 View 的数量多不多。
  • CPU 绘制视图树来计算下一帧画面数据的工作是在屏幕刷新信号来的时候才开始工作的,而当这个工作处理完毕后,也就是下一帧的画面数据已经全部计算完毕,也不会马上显示到屏幕上,而是会等下一个屏幕刷新信号来的时候再交由底层将计算完毕的屏幕画面数据显示出来。
  • 当我们的 app 界面不需要刷新时(用户无操作,界面无动画),app 就接收不到屏幕刷新信号所以也就不会让 CPU 再去绘制视图树计算画面数据工作,但是底层仍然会每隔 16.6 ms 切换下一帧的画面,只是这个下一帧画面一直是相同的内容。

为什么界面不刷新时 app 就接收不到屏幕刷新信号了?为什么绘制视图树计算下一帧画面的工作会是在屏幕刷新信号来的时候才开始的?

源码

ViewRootImpl 与 DecorView 的绑定.

View#invalidate() 是请求重绘的一个操作,所以我们切入点可以从这个方法开始一步步跟下去。

Android 设备呈现到界面上的大多数情况下都是一个 Activity,真正承载视图的是一个 Window,每个 Window 都有一个 DecorView,我们调用 setContentView() 其实是将我们自己写的布局文件添加到以 DecorView 为根布局的一个 ViewGroup 里,构成一颗 View 树。

每个 Activity 对应一颗以 DecorView 为根布局的 View 树,但其实 DecorView 还有 mParent,而且就是 ViewRootImpl,而且每个界面上的 View 的刷新,绘制,点击事件的分发其实都是由 ViewRootImpl 作为发起者的,由 ViewRootImpl 控制这些操作从 DecorView 开始遍历 View 树去分发处理。

ViewRootImpl 与 DecorView 的绑定

跟着 invalidate() 一步步往下走的时候,发现最后跟到了 ViewRootImpl#scheduleTraversals() 就停止了。

Android 设备呈现到界面上的大多数情况下都是一个 Activity,真正承载视图的是一个 Window,每个 Window 都有一个 DecorView,我们调用 setContentView() 其实是将我们自己写的布局文件添加到以 DecorView 为根布局的一个 ViewGroup 里,构成一颗 View 树。

每个 Activity 对应一颗以 DecorView 为根布局的 View 树,但其实 DecorView 还有 mParent,而且就是 ViewRootImpl,而且每个界面上的 View 的刷新,绘制,点击事件的分发其实都是由 ViewRootImpl 作为发起者的,由 ViewRootImpl 控制这些操作从 DecorView 开始遍历 View 树去分发处理。

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

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

//WindowManagerGlobal#addView
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
   
    ...
    ViewRootImpl root;
    ...
    synchronized (mLock) {
   
        ...
        //1. 实例化一个 ViewRootImpl对象
        root = new ViewRootImpl(view.getContext(), display);
        ...
        mViews.add(view);
        mRoots.add(root);
        ...
    }
    try {
   
        //2. 调用ViewRootImpl的setView(),并将DecorView作为参数传递进去
        root.setView(view, wparams, panelParentView);
    }...  
}

WindowManager 维护着所有 Activity 的 DecorView 和 ViewRootImpl。这里初始化了一个 ViewRootImpl,然后调用了它的 setView() 方法,将 DevorView 作为参数传递了进去。所以看看 ViewRootImpl 中的 setView() 做了什么:

//ViewRootImpl#setView
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
   
    synchronized (this) {
   
        if (mView == null) {
   
            //1. view 是 DecorView
            mView = view;
            ...
            //2.发起布局请求
            requestLayout();
            ...
            //3.将当前ViewRootImpl对象this,作为参数调用了DecorView的assignParent
            view.assignParent(this);
            ...
        }
    }
}

在 setView() 方法里调用了 DecorView 的 assignParent() 方法,所以去看看 View 的这个方法:

//View#assignParent
void assignParent(ViewParent parent) {
   
    if (mParent == null) {
   
        mParent = null;
    } else if (parent == null) {
   
        mParent = null;
    } else {
   
        throw new RunTimeException("view " + this + " is already has a parent")
    }
}

参数是 ViewParent,而 ViewRootImpl 是实现了 ViewParent 接口的,所以在这里就将 DecorView 和 ViewRootImpl 绑定起来了。每个Activity 的根布局都是 DecorView,而 DecorView 的 parent 又是 ViewRootImpl,所以在子 View 里执行 invalidate() 之类的操作,循环找 parent 时,最后都会走到 ViewRootImpl 里来。

跟界面刷新相关的方法里应该都会有一个循环找 parent 的方法,或者是不断调用 parent 的方法,这样最终才都会走到 ViewRootImpl 里,也就是说实际上 View 的刷新都是由 ViewRootImpl 来控制的。

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

重新看回 ViewRootImpl 的 setView() 这个方法,这个方法里还调用了一个 requestLayout() 方法:

//ViewRootImpl#requestLayout
@Override
public void requestLayout() {
   
    if (!mHandingLayoutInLayoutRequest) {
   
        //1.检查该操作是否是在主线程中执行
        checkThread();
        mLayoutRequested = true;
        //2.安排一次遍历绘制View树的任务
        scheduleTraversals();
    }
}

这里调用了一个 scheduleTraversals(),还记得当 View 发起重绘操作 invalidate() 时,最后也调用了 scheduleTraversals() 这个方法么。其实这个方法就是屏幕刷新的关键,它是安排一次绘制 View 树的任务等待执行,具体后面再说。

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

这也就是为什么在 onCreate—onResume 里获取不到 View 宽高的原因,因为在这个时刻 ViewRootImpl 甚至都还没创建,更不用说是否已经执行过测量操作了。

还可以得到一点信息是,一个 Activity 界面的绘制,其实是在 onResume() 之后才开始的。

ViewRootImpl # scheduleTraversals

调用一个 View 的 invalidate() 请求重绘操作,内部原来是要层层通知到 ViewRootImpl 的 scheduleTraversals() 里去。

//ViewRootImpl#scheduleTraversals
void scheduleTraversals() {
   
    if (!mTraversalScheduled) {
   
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值