AndroidQ UI刷新机制详解

本篇文章分析Android UI刷新机制,就是更新UI,做Android开发初期我们经常会听说不能在子线程更新UI,以及Activity的onCreate方法中获取不到View宽高的问题

我们先来说一下子线程不能更新UI的问题:

public class MainActivity extends AppCompatActivity {
    private Button button;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
              new Thread(new Runnable() {
                  @Override
                  public void run() {
                      button.setText("button");
                  }
              }).start();
            }
        });
    }
}

我们定义一个button,点击时修改button的文字,发现果然报错:很熟悉的错误,只能在创建视图的原始线程修改视图

03-24 18:17:17.974 25194 27234 E AndroidRuntime: FATAL EXCEPTION: Thread-2
03-24 18:17:17.974 25194 27234 E AndroidRuntime: Process: com.example.myapplication, PID: 25194
03-24 18:17:17.974 25194 27234 E AndroidRuntime: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
03-24 18:17:17.974 25194 27234 E AndroidRuntime: at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8464)
03-24 18:17:17.974 25194 27234 E AndroidRuntime: at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1468)

我们再把代码改成这样,不在button点击时修改它的text,而在onCreate中修改:

public class MainActivity extends AppCompatActivity {
    private Button button;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button = findViewById(R.id.button);
        new Thread(new Runnable() {
            @Override
            public void run() {
                button.setText("button");
            }
        }).start();
    }
}

我们发现这样就不报错了,而且text修改成功,所以我们如果在onCreate中更新UI的话随便在哪个线程都是OK的

为什么会这样?我们看下上面那段抛出的异常,在ViewRootImpl的requestLayout中调用checkThread来检查UI更新的线程的,那么onCreate中能在子线程更新UI,说明此时无法使用checkThread来检查线程,实际上此时ViewRootImpl并没有被创建

ViewRootImpl的创建是在Activity的onResume过程中,在onResume时会通过WindowManagerGlobal的addView方法将Activity的顶层视图DecoreView添加到WMS中,而在WindowManagerGlobal的addView方法中就会创建ViewRootImpl,并调用它的setView方法

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
          ...
            root = new ViewRootImpl(view.getContext(), display);
  			...
            root.setView(view, wparams, panelParentView);
         
            ....
    }

ViewRootImpl的setView方法中会调用requestLayout方法,此方法就会调用
checkThread检查当前线程,并进行View的测量,布局,绘制流程

@Override
    public void requestLayout() {
            ...
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

这也解决了为什么onCreate中无法获取View的宽高的问题,因为onResume时才对Activity的布局文件进行测量

好了,在checkThread检查完UI更新的线程是否合法之后就会调用scheduleTraversals触发UI的更新操作了,首先给一个结论,UI的更新不是同步的,而是将UI更新请求首先放入一个callback链表,等待底层的垂直同步脉冲信号(vsync),收到这个信号之后回调callback执行测量,布局,绘制操作,这个信号在底层由硬件发出,有些也是通过软件模拟的,标准情况下是每秒60次,即每秒刷新60帧,也就是我们常说的16.6ms需要完成一帧的绘制,不然就会卡顿

接下来进行代码分析

scheduleTraversals

    @UnsupportedAppUsage
    void scheduleTraversals() {
       //防止重复调用
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            .....
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            //通知底层即将进行画面渲染
           ......
    }

这个方法里面我们关注两个重点,1.设置同步屏障,2.post一个绘制请求

首先来看设置同步屏障,同步屏障其实就是向MessageQueue添加一个不带target的消息,作用是屏蔽handler的同步消息,只取出异步消息执行,默认handle都是同步消息,
关于同步屏障我在AndroidQ Handler消息机制(java层)这篇文章中详细分析过

接着看调用Choreographer的postCallback方法添加一个Callback,第一个参数是Callback的type,第二个参数是Callback执行的mTraversalRunnable,
我们后面再来看这个Runnable

final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
    final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

来看看Choreographer的postCallback方法

 public void postCallback(int callbackType, Runnable action, Object token) {
        postCallbackDelayed(callbackType, action, token, 0);
    }
    @TestApi
    public void postCallbackDelayed(int callbackType,
            Runnable action, Object token, long delayMillis) {
        if (action == null) {
            throw new IllegalArgumentException("action must not be null");
        }
        if (callbackType < 0 || callbackType > CALLBACK_LAST) {
            throw new IllegalArgumentException("callbackType is invalid");
        }

        postCallbackDelayedInternal(callbackType, action, token, delayMillis);
    }
    private void postCallbackDelayedInternal(int callbackType,
            Object action, Object token, long delayMillis) {
         ...
        synchronized (mLock) {
            final long now = SystemClock.uptimeMillis();
            final long dueTime = now + delayMillis;
            //(1)步骤1
            mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
             //(2)步骤2
            if (dueTime <= now) {
                scheduleFrameLocked(now);
            } else {
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
                msg.arg1 = callbackType;
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, dueTime);
            }
        }
    }

一通调用,就是一个参数的传递,postCallbackDelayedInternal中我们分两个步骤来分析,先看步骤1:
mCallbackQueues类型为CallbackQueue[],里面有四种类型的CallbackQueue,CallbackQueue是一个链表,就是一系列CALLBACK_XXXX类型请求的集合,按照执行的先后顺序排列,头部是即将执行的请求,我们只关心绘制类型请求,另外还有输入类型,动画类型等

/**
     * Callback type: Traversal callback.  Handles layout and draw.  Runs
     * after all other asynchronous messages have been handled.
     * @hide
     */
    public static final int CALLBACK_TRAVERSAL = 3;

我们传递过来的dueTime为0,需要立即执行,在来看步骤2:
其实如下不管是哪个分支最终都是通过handler发送MSG_DO_SCHEDULE_CALLBACK消息来处理的,只是是否延迟执行而已

 if (dueTime <= now) {
                scheduleFrameLocked(now);
            } else {
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
                msg.arg1 = callbackType;
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, dueTime);
            }

我们来看scheduleFrameLocked方法

scheduleFrameLocked

private void scheduleFrameLocked(long now) {
        //保证执行一次
        if (!mFrameScheduled) {
            mFrameScheduled = true;
            //是否使用VSYNC,现在手机一般都使用的
            if (USE_VSYNC) {
               ...
                if (isRunningOnLooperThreadLocked()) {
                    scheduleVsyncLocked();
                } else {
                    Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
                    msg.setAsynchronous(true);
                    mHandler.sendMessageAtFrontOfQueue(msg);
                }
            } else {
                //不使用VSYNC的情况
                ......
            }
        }
    }

上面代码的两个分支最终都是执行同样的方法scheduleVsyncLocked,只不过需要判断当前是否在Looper线程,我们理解成主线程就行了,如果不在就通过Handler消息到主线程执行,我们看Choreographer的代码会发现所有的Handler消息都被设置成了异步消息setAsynchronous(true),这样我们前面开启的同步屏障就起作用了,目的是UI的更新必须以最高优先级进行

scheduleVsyncLocked

@UnsupportedAppUsage
    private void scheduleVsyncLocked() {
        mDisplayEventReceiver.scheduleVsync();
    }

mDisplayEventReceiver类型为FrameDisplayEventReceiver,它是上层用来接收Vsync信号的,调用它的scheduleVsync方法向native层注册监听Vsync,需要注意Vsync的接收是注册一次才能接收一次

scheduleVsync

 public void scheduleVsync() {
        if (mReceiverPtr == 0) {
            Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "
                    + "receiver has already been disposed.");
        } else {
            nativeScheduleVsync(mReceiverPtr);
        }
    }

最终通过nativeScheduleVsync向native层注册Vsync的监听,关于Vsync的注册我们下一篇文章来分析

这里我们直接来看接收到Vsync之后的回调

private final class FrameDisplayEventReceiver extends DisplayEventReceiver
            implements Runnable {
			......
			@Override
        public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
            
            long now = System.nanoTime();
            
            .....
            
            mTimestampNanos = timestampNanos;
            mFrame = frame;
            Message msg = Message.obtain(mHandler, this);
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
        }

        @Override
        public void run() {
            mHavePendingVsync = false;
            doFrame(mTimestampNanos, mFrame);
        }
			......
}

接收到Vsync的回调接着就通过handler到UI线程执行Runnable的回调doFrame

doFrame

@UnsupportedAppUsage
    void doFrame(long frameTimeNanos, int frame) {
        final long startNanos;
        synchronized (mLock) {
            //这里有很多关于Vsync到来时间,frame时间计算,以及
            //跳frame相关的各种计算我就省略了
            .....
            

        try {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
            AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);

            mFrameInfo.markInputHandlingStart();
            doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);

            mFrameInfo.markAnimationsStart();
            doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
            doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos);

            mFrameInfo.markPerformTraversalsStart();
            doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);

            doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
        } finally {
            AnimationUtils.unlockAnimationClock();
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }

        ....
    }

最终此方法实际是通过doCallbacks方法执行我们最开始添加的绘制类型的回调的,我们关注传递的type为CALLBACK_TRAVERSAL的回调

doCallbacks

void doCallbacks(int callbackType, long frameTimeNanos) {
        CallbackRecord callbacks;
        synchronized (mLock) {
            //步骤1
            callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(
                    now / TimeUtils.NANOS_PER_MS);
            if (callbacks == null) {
                return;
            }

            //不关注这个类型
            if (callbackType == Choreographer.CALLBACK_COMMIT) {
               .....
            }
        }
        try {
            //步骤2
            for (CallbackRecord c = callbacks; c != null; c = c.next) {
               ...
                c.run(frameTimeNanos);
            }
        } finally {
           //收尾工作
            ....
        }
    }

为了代码简洁,删掉了不关注的部分,上面代码我分为两个部分来分析,先看步骤1:
对type为CALLBACK_TRAVERSAL的CallbackQueues执行extractDueCallbacksLocked方法,主要看看这个方法要干嘛的

extractDueCallbacksLocked

 public CallbackRecord extractDueCallbacksLocked(long now) {
            CallbackRecord callbacks = mHead;//头指针
            //如果链表为空或者链表中头指针指向的callback的执行时间都
            //大于当前时间则返回,因为还没到callback执行时间
            if (callbacks == null || callbacks.dueTime > now) {
                return null;
            }
            //last是头指针
            CallbackRecord last = callbacks;
            //next是头指针下一个Callback
            CallbackRecord next = last.next;
            //这种一看就是遍历链表操作
            while (next != null) {
                //如果下一个Callback的执行时间大于当前时间
                if (next.dueTime > now) {
                   //则需要断掉链表,因为这里只有头部Callback
                   //满足执行条件
                    last.next = null;
                    break;
                }
                //否则继续往后查找
                last = next;
                next = next.next;
            }
            //链表遍历完成之后,头指针重新指向next,这里的next可能为空,
            //为空代表链表所有Callback都需要执行,不为空则指向被断掉的链表
            mHead = next;
            return callbacks;
        }

总结一下extractDueCallbacksLocked的作用,此方法就是为了得到执行时间小于当前时间的Callback链表集合,得到之后会将链表断成两份,前面一份为需要马上执行的Callback,后面一份为还未到执行时间的Callback,这样保证了下一次绘制请求的Callback能够放在后面一个链表中,最早也在下一个Vsync到才执行

好了我们接着看步骤2,步骤2就比较简单了,就是遍历extractDueCallbacksLocked得到的链表,然后依次调用它们的run方法,我们看看CallbackRecord的run方法

 private static final class CallbackRecord {
        ......
        @UnsupportedAppUsage
        public void run(long frameTimeNanos) {
            if (token == FRAME_CALLBACK_TOKEN) {
                ((FrameCallback)action).doFrame(frameTimeNanos);
            } else {
                ((Runnable)action).run();
            }
        }
    }

这里的action就是我们最开始scheduleTraversals方法中post进去的,就是这个:

 final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }

调用doTraversal方法

doTraversal

void doTraversal() {
        .....
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            performTraversals();
           ....
        }
    }

这个方法先移除了我们开始添加的同步屏障,接着调用我们熟悉的performTraversals方法,开始了测量,布局,绘制流程

到这里UI刷新机制全部分析完了,我们做一个总结:

  1. 不管什么形式的UI更新,最终都会调用到ViewRootImpl的scheduleTraversals方法,这个方法会开启同步屏障,并且将UI的绘制请求post到一个callback数组中等待执行
  2. Choreographer中的DisplayEventReceiver会在UI线程中通过Handler异步消息向native层注册一个Vsync信号监听(nativeScheduleVsync)
  3. 在下一个Vsync到来时会回调FrameDisplayEventReceiver的onVsync方法,并且到UI线程中执行它的run方法,调用doFrame方法
  4. doFrame中通过doCallbacks获取到执行时间小于当前时间的一条CallbackRecord链表,并遍历链表执行最开始添加的Runnable的run方法
  5. 这个Runnable中会执行doTraversal,doTraversal中移除同步屏障,并执行performTraversals开始测量,布局,绘制流程

下一篇文章分析Vsync的监听与上层的接收

发布了42 篇原创文章 · 获赞 61 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_34211365/article/details/105093648
今日推荐