Android从屏幕刷新到View的绘制(二)之Choreographer、Vsync与屏幕刷新

news2024/11/24 19:31:06

0.相关分享:

Android从屏幕刷新到View的绘制(一)之 Window、WindowManager和WindowManagerService之间的关系
Android从屏幕刷新到View的绘制(二)之Choreographer、Vsync与屏幕刷新

1. 相关类

Choreographer 编舞者

Android4.1之后新增的机制,用于配合系统的 Vsync (Vertical Synchronization,垂直同步)信号,编舞者可以接受系统的 Vsync信号,统一管理引用的输入、动画、绘制等任务的执行时机,使得整体显得协调连贯。业界一般通过它来监控应用的帧率。

CallbackQueue

Choreographer添加的任务最后都被封装为CallbackRecord,以链表的形式保存在CallbackQueue中,Choreographer的mCallbackQueues是一个数组,长度为4,保存着四个CallbackQueue链表(Android8)。分别处理:输入、动画、遍历绘制等任务。

FrameDisplayEventReceiver

Android4.1之后,默认开启了Vsync功能,FrameDisplayEventReceiver用于接收Vsync事件,并将事件提交给looper(一般为主线程)。它会通过JNI创建一个IDisplayEventConnection的Vsync监听者。

FrameHandler

处理主线程中关于Vsync的事件。执行异步消息,有延迟的任务发延迟消息、不在原线程的发到原线程。

2. 源码分析

在前面的分析我们知道,Activity的onResume()执行完之后,会通知WMS进行window的添加。过程中会调用到ViewRootImpl的setView(),最后又会走到scheduleTraversals()方法。

另外,其实我们使用View.invalidate()、requestLayout()等方法时,最后都会走到ViewRootImpl的scheduleTraversals()方法。也就是说,所有UI的变化最后都会走到ViewRootImpl的scheduleTraversals()方法

在Android的framework源码阅读过程在,我们会遇到很多类如一一对应的 scheduleXXX() 与 performXXX(),这可以理解为“发起请求”以及得到响应之后的“执行任务”。在这里,scheduleTraversals() 就是进行发起绘制请求,等到 Vsync 信号到来之后,执行绘制performTraversals()。

先来挂一张流程图,可能会一脸懵,但看完后续源码解析后,这张流程图就一目了然了:

请添加图片描述

2.1 Choreographer的创建

在ViewRootImpl对象创建的时候,会获取到一个Choreographer实例:

//ViewRootImpl
Choreographer mChoreographer;
//ViewRootImpl实例是在添加window时创建的
public ViewRootImpl(Context context,Display display){
    //...
    mChoreographer = Choreographer.getInstance();
    //...
}

Choreographer维护了一个looper和handler:

//Choreograper
//通过ThreadLocal获取线程私有的Choreographer
private static final ThreadLocal<Choreographer> sThreadInstance =
    new ThreadLocal<Choreographer>() {
    @Override
    protected Choreographer initialValue() {
        //当前线程的Looper
        Looper looper = Looper.myLooper();
        if (looper == null) {
            throw new IllegalStateException("The current thread must have a looper!");
        }
        return new Choreographer(looper, VSYNC_SOURCE_APP);
    }
};

public static Choreographer getInstance() {
    return sThreadInstance.get();
}

我们来看到Choreographer的构造方法,它做了:

  1. 维护了所在线程的Looper
  2. 为该looper设置了一个FrameHandler,处理绘制消息
  3. Android4.1之后,还有一个Vsync监听者 FrameDisplayEventReceiver()
  4. 初始化了不同事件的回调队列
private Choreographer(Looper looper, int vsyncSource) {
    //所在线程的looper
    mLooper = looper;
    //handler消息处理,只能收到当前线程Looper的消息
    mHandler = new FrameHandler(looper);
    //Vsync消息监听者,Android4.1之后默认开启USE_VSYNC
    mDisplayEventReceiver = USE_VSYNC
        ? new FrameDisplayEventReceiver(looper, vsyncSource)
        : null;
    // 计算一帧的时间,Android手机屏幕是60Hz的刷新频率,就是16ms
    mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());

    //数组长度为4 (在android8.0中)
    mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
    for (int i = 0; i <= CALLBACK_LAST; i++) {
        mCallbackQueues[i] = new CallbackQueue();
    }
}

我们可以按经验来看着几个对象,猜测当Vsync信号到来时,可以进行刷新,FrameHandler收到了这个消息,回调给CallbackQueue中的各个View任务。

2.2 向Choreographer添加任务

mChoreographer.postCallback()就是向CallbackQueue中添加View任务的方法,这个方法有三个参数,第一个参数有四种类型(Android8中是四种类型),表示的是这个任务的类型:

//Choreographer
//输入事件,优先级最高
public static final int CALLBACK_INPUT = 0;
//动画任务
public static final int CALLBACK_ANIMATION = 1;
//绘制任务
public static final int CALLBACK_TRAVERSAL = 2;
//提交,优先级最低,最后执行
public static final int CALLBACK_COMMIT = 3;

我们之前谈到,所有UI的变化都走到了ViewRootImpl的scheduleTraversals()中,这个方法向Choreographer添加了任务申请:

//ViewRootImpl
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        //标志位防重复提交,也就是多次requestLayout、invalidate在一次绘制申请中,只会生效一次
        mTraversalScheduled = true;
        //添加同步屏障,挡住后续所有的同步消息,让Looper优先分发异步事件。Vsync来到时,发布的是异步消息,优先处理
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        //添加任务
        mChoreographer.postCallback(
            Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

来到scheduleTraversals(),核心做了几件事,这几件事都很重要,和后续有紧密联系:

  1. 将申请标志位置为true,避免重复提交,在一次绘制申请中,多次requestLayout/invalidate只会生效一次 我们知道invalidate等方法都只是一个申请,方法内没有数据变化,在Vsync到来之前,View的数据可以变化,最终只会生效最后的情况,所以没必要重复接收多个invalidate()请求,就好像你对妈妈说:“我饿了,我想吃一碗饭”,妈妈收到了,说“好的”,妈妈就去煮饭了,你又说“我饿了,我想吃两碗饭”,妈妈不会理会你,反正最后你吃几碗是几碗,妈妈直到你饿了已经去煮了
  2. 向Looper的MessageQueue中添加同步屏障,保证Vsync来到后异步消息能够立即执行。
  3. 向Choreographer添加mTraversalRunnable,注册绘制任务的callback

我们来看一下这个mTraversalRunnable具体做了什么来执行绘制:

//ViewRootImpl.java
final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        //只做了一件时,调用外部类,也就是ViewRootImpl对象的doTraversal()方法
        doTraversal();
    }
}
//ViewRootImpl只管绘制,所以TraversalRunnable订好了只做doTraversal()
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

void doTraversal() {
    if (mTraversalScheduled) {
        //标志设为false,可以继续接受scheduleTraversal()任务
        mTraversalScheduled = false;
        //取出Looper的MessageQueue中的同步屏障,恢复同步消息的分发
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        //进入ViewRootImpl下的DecorView的绘制流程
        performTraversals();
    }
}

这里有几个点注意:

  1. mTraversalScheduled字段又被设置为false,和之前的故事相照应:妈妈把电饭煲端出来了,你可以吃饭了,怎么吃是你的事了,妈妈很疼你,如果你还要吃,允许你再请妈妈煮饭
  2. 之前由于添加了同步屏障,为了保证Vsync来到后立即绘制,但屏蔽了正常的同步消息,所以这里需要把同步屏障取消掉
  3. Vsync到来之后,由于之前注册的回调,所以会执行TraversalRunnable->run()->ViewRootImpl.doTraversal()->performTraversals()->View的绘制流程。

那么这个回调在注册过程中发生了什么呢?

mChoreographer.postCallback()内部调用了postCallbackDelayed(),紧接着调用了postCallbackDelayedInternal():

//Choreographer
//这里的action,就是传入的Runnable,由于类型不一定,所以改成了Object
private void postCallbackDelayedInternal(int callbackType,
                                         Object action, Object token, long delayMillis) {
    
    synchronized (mLock) {
        //当前时间
        final long now = SystemClock.uptimeMillis();
        //执行时间
        final long dueTime = now + delayMillis;
        //将传入的action包装在CallbackRecord中,插入到队列里
        mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
		//如果时间已经到了,其实就是delayMillis==0的情况
        if (dueTime <= now) {
            //立即执行scheduleFrameLocked()
            scheduleFrameLocked(now);
        } else {
            //如果这个请求时延迟任务,那么就将其设置为异步任务,并添加上时间,放在Message中,发送Message。
            //复用了 MSG_DO_SCHEDULE_CALLBACK的message,将action传入
            Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
            msg.arg1 = callbackType;
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, dueTime);
        }
    }
}

这里主要做了几件事:

  1. 计算时间,如果需要立即执行,则调用scheduleFrameLocked()
  2. 如果是延迟消息,先去复用池获取一个msg,将action传入,这里的action是之前传入的mTraversalRunnable
  3. 由于这是绘制任务,为了通过同步屏障,我们需要设置消息为异步消息 msg.setAsynchronous(true)
  4. message需要定一个int what让handler来决定处理方式

CallbackQueue.addCallbackLocked()就是把dueTime、action、token(这里的token为null)等信息存入CallbackRecord后,放在CallbackQueue链表中。

我们先来看一下直接发送的 scheduleFrameLocked()

//Choreographer
private void scheduleFrameLocked(long now) {
    if (!mFrameScheduled) {
        //似乎成了一个约定,只要是schedule申请,就会有一个标志来表示是否已接受过这个申请。
        mFrameScheduled = true;
        if (USE_VSYNC) {//Android4.1之后默认开启Vsync垂直同步
            //线程判断
            if (isRunningOnLooperThreadLocked()) {
                //如果就是本线程发起的,就进入这里
                scheduleVsyncLocked();
            } else {
                //如果是其他线程调用的这个方法,则通过Message来将任务的线程切换到本线程执行。最终还是执行scheduleVsyncLocked()
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
                msg.setAsynchronous(true);
                mHandler.sendMessageAtFrontOfQueue(msg);
            }
        } else {
            //没有垂直同步,就直接doFrame()
            Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, nextFrameTime);
        }
    }
}

如果开启了Vsync,那么进入到scheduleVsyncLocked():

//Choreographer
private void scheduleVsyncLocked() {
    //调用了DisplayEventReceiver的方法
    mDisplayEventReceiver.scheduleVsync();
}
//DisplayEventReceiver
public void scheduleVsync() {
    //调用了本地方法
    nativeScheduleVsync(mReceiverPtr);

}
//这是一个本地方法
private static native void nativeScheduleVsync(long receiverPtr);

我们之前介绍直到,DisplayEventReceiver它会通过JNI创建一个IDisplayEventConnection的Vsync监听者。我们来到framework/base/core/jni包下看看:

//android_view_DisplayEventReceiver.cpp
static void nativeScheduleVsync(JNIEnv* env, jclass clazz, jlong receiverPtr) {
    sp<NativeDisplayEventReceiver> receiver =
        reinterpret_cast<NativeDisplayEventReceiver*>(receiverPtr);
    status_t status = receiver->scheduleVsync();
    if (status) {
        String8 message;
        message.appendFormat("Failed to schedule next vertical sync pulse.  status=%d", status);
        jniThrowRuntimeException(env, message.string());
    }
}

NativeDisplayEventReceiver继承自DisplayEventDispatcher

status_t DisplayEventDispatcher::scheduleVsync() {
    if (!mWaitingForVsync) {
        //...
        //又由Dispatcher让DisplayEventReceiver去requestNextVsync()
        status_t status = mReceiver.requestNextVsync();
        //...
        mWaitingForVsync = true;
    }
    return OK;
}

顾名思义,让DisplayEventReceiver去申请下一个Vsync,最后来到/gui/DisplayEventReceiver.cpp:

//gui/DisplayEventReceiver.cpp
status_t DisplayEventReceiver::requestNextVsync() {
    if (mEventConnection != NULL) {
        mEventConnection->requestNextVsync();
        return NO_ERROR;
    }
    return NO_INIT;
}

我们可以看到,DisplayEventReceiver通过IDisplayEventConnection对象去申请下一个Vsync

class BpDisplayEventConnection : public SafeBpInterface<IDisplayEventConnection> {
    void requestNextVsync() override {
  	 	callRemoteAsync<decltype(&IDisplayEventConnection::requestNextVsync)>(
            Tag::REQUEST_NEXT_VSYNC);
    }
}

看名字就知道,这个DisplayEventConnection只是一个binder引用,需要远程调用其实体去requestNextVsync,我们来看到BnDisplayEventConnection::onTransact是如何处理这个事务的:

status_t BnDisplayEventConnection::onTransact(uint32_t code, const Parcel& data, Parcel* reply,
                                              uint32_t flags) {
    if (code < IBinder::FIRST_CALL_TRANSACTION || code > static_cast<uint32_t>(Tag::LAST)) {
        return BBinder::onTransact(code, data, reply, flags);
    }
    auto tag = static_cast<Tag>(code);
    switch (tag) {
        case Tag::STEAL_RECEIVE_CHANNEL:
            return callLocal(data, reply, &IDisplayEventConnection::stealReceiveChannel);
        case Tag::SET_VSYNC_RATE:
            return callLocal(data, reply, &IDisplayEventConnection::setVsyncRate);
        case Tag::REQUEST_NEXT_VSYNC:
            //通过callLocalAsync()来request_next_vsync
            return callLocalAsync(data, reply, &IDisplayEventConnection::requestNextVsync);
    }
}

接下来来到SafeInterface.h中的callLocalAsync():

//SafeInterface.h
template <typename Method>
status_t callLocalAsync(const Parcel& data, Parcel* /*reply*/, Method method) {
   
    using ParamTuple = typename SafeInterface::ParamExtractor<Method>::ParamTuple;
    typename RawConverter<std::tuple<>, ParamTuple>::type rawArgs{};

    //发送parcel数据
    status_t error = InputReader<ParamTuple>{mLogTag}.readInputs(data, &rawArgs);
    
    //方法返回
    MethodCaller<ParamTuple>::callVoid(this, method, &rawArgs);
	
    return NO_ERROR;
}

我只跟到这里,这里应该只是发送Vsync通知SurfaceFlinger刷新的信号,下面两篇文章对Vsync做了更深层的底层探究

这有一个native层Vsync分发的文章: https://blog.csdn.net/Android062005/article/details/123090139

推荐一个SurfaceFlinger的学习文章 : https://www.jianshu.com/p/c954bcceb22a

本文只关注到,最后Vsync信号会被发送给DisplayEventReceiver::AttachedEvent::handleEvent():

//displayservice/DisplayEventReceiver.cpp
int DisplayEventReceiver::AttachedEvent::handleEvent(int fd, int events, void* /* data */) {
    
    constexpr size_t SIZE = 1;

    ssize_t n;
    FwkReceiver::Event buf[SIZE];
    while ((n = mFwkReceiver.getEvents(buf, SIZE)) > 0) {
        for (size_t i = 0; i < static_cast<size_t>(n); ++i) {
            const FwkReceiver::Event &event = buf[i];

            uint32_t type = event.header.type;
            uint64_t timestamp = event.header.timestamp;

            switch(buf[i].header.type) {
                case FwkReceiver::DISPLAY_EVENT_VSYNC: {
                    //回调onVsync()方法
                    mCallback->onVsync(timestamp, event.vsync.count);
                } break;
                case FwkReceiver::DISPLAY_EVENT_HOTPLUG: {
                    mCallback->onHotplug(timestamp, event.hotplug.connected);
                } break;
                default: {
                    LOG(ERROR) << "AttachedEvent handleEvent unknown type: " << type;
                }
            }
        }
    }

    return 1; // keep on going
}

最终回调到Java层的DisplayEventReceiver.onVsync(),跟了这么深,还记得DisplayEventReceiver在java层的实现类是谁么?真棒,是Choreographer中的内部类FrameDisplayEventReceiver,我们来看看它:

//Choreographer
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
    implements Runnable {
    private boolean mHavePendingVsync;
    private long mTimestampNanos;
    private int mFrame;

    public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
        super(looper, vsyncSource);
    }

    @Override
    public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
        
        if (builtInDisplayId != SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN) {
            scheduleVsync();
            return;
        }

        long now = System.nanoTime();
        if (timestampNanos > now) {
            timestampNanos = now;
        }

        if (mHavePendingVsync) {
            
        } else {
            mHavePendingVsync = true;
        }

        mTimestampNanos = timestampNanos;
        mFrame = frame;
        //将本身作为runnable传入msg,被调用执行后,会进入run()->doFrame()
        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);
    }
}

onVsync()方法中,将接收器本身作为Runnable传入异步消息msg,可以越过同步屏障,优先执行,但它并不一定立即执行,仅仅是越过同步屏障罢了,如果前面有较为耗时的操作,仍然需要等待,然后才会执行本次doFrame()。这也说明主线程不应当有耗时操作,否则就会影响屏幕输入事件、绘制等任务,出现ANR问题。

最后来到 doFrame() 我们来看看具体是如何逐步调用各种事件响应的:

//Choreographer
void doFrame(long frameTimeNanos, int frame) {
    final long startNanos;
    synchronized (mLock) {
        if (!mFrameScheduled) {
            return; // no work to do
        }

        //...
        // 预期执行时间
        long intendedFrameTimeNanos = frameTimeNanos;
        startNanos = System.nanoTime();
        // 超时时间是否超过一帧的时间(这是因为MessageQueue虽然添加了同步屏障,但是还是有正在执行的同步任务,导致doFrame延迟执行了)
        final long jitterNanos = startNanos - frameTimeNanos;
        if (jitterNanos >= mFrameIntervalNanos) {
            // 计算掉帧数
            final long skippedFrames = jitterNanos / mFrameIntervalNanos;
            if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
                // 掉帧超过30帧打印Log提示
                Log.i(TAG, "Skipped " + skippedFrames + " frames!  "
                      + "The application may be doing too much work on its main thread.");
            }
            final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
            //...
            frameTimeNanos = startNanos - lastFrameOffset;
        }
        //...
        mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
        // Frame标志位恢复
        mFrameScheduled = false;
        // 记录最后一帧时间
        mLastFrameTimeNanos = frameTimeNanos;
    }

    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);
		//接下来执行View的绘制
        mFrameInfo.markPerformTraversalsStart();
        doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
		//最后提交
        doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
    } 
    //...
}

接下来就到doCallbacks()把各个CallbackQueue中的任务回调执行,还记得吗?最初我们会提交任务到Choreographer,然后通过postCallback来等待回调,现在就是回调的时机了:

//Choreorapher
void doCallbacks(int callbackType, long frameTimeNanos) {
    CallbackRecord callbacks;
    synchronized (mLock) {
        //执行其中时间ok的任务,也就是查找到达执行时间的CallbackRecord
        callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(
            now / TimeUtils.NANOS_PER_MS);
        if (callbacks == null) {
            return;
        }
        mCallbacksRunning = true;
		//一些log。。。
    }
    try {
        //遍历执行CallbackRecord的run()方法,进一步调用里面的action的方法。例如传入的比如mTraversalRunnable的run()方法
        for (CallbackRecord c = callbacks; c != null; c = c.next) {
            //内部回调callback的run
            c.run(frameTimeNanos);
        }
    } finally {
        synchronized (mLock) {
            mCallbacksRunning = false;
            do {
                final CallbackRecord next = callbacks.next;
                //回收这个CallbackRecord
                recycleCallbackLocked(callbacks);
                callbacks = next;
            } while (callbacks != null);
        }
    }
}

我们最后再来看一下CallbackRecord这个类:

private static final class CallbackRecord {
    public CallbackRecord next;
    public long dueTime;
    public Object action; // Runnable or FrameCallback
    public Object token;

    @UnsupportedAppUsage
    public void run(long frameTimeNanos) {
        if (token == FRAME_CALLBACK_TOKEN) {
            // 通过postFrameCallback 或 postFrameCallbackDelayed,会执行这里
            ((FrameCallback)action).doFrame(frameTimeNanos);
        } else {
            //取出Runnable执行run()
            ((Runnable)action).run();
        }
    }
}

例如ViewRootImpl发起的绘制任务时传递token为null,所以进入到了mTraversalRunnable.run()。接下去就是我们熟悉的去除内存屏障,performTraversals()->View的绘制流程。

此外,我们注意到token还有可能是 FRAME_CALLBACK_TOKEN,这一般被用来计算丢帧情况。通过Choreographer.postFrameCallback()。

我们最后再来回顾流程图,应当是一目了然了:

请添加图片描述

  1. ViewRootImpl可以接受来自View的invalidate、requestLayout等请求

  2. ViewRootImpl通过scheduleTraversals()发起请求,向Choreographer编舞者添加绘制回调

  3. Choreographer通过FrameDisplayEventReceiver来注册接受Vsync信号。这是个DisplayEventReceiver的binder引用,是个BpBinder。通过Binder通信,让BnDisplayEventReceiver去接受Vsync信号。

  4. 可以注意到,虽然屏幕16ms刷新,但如果屏幕布局元素一直没有变化,那么就不会申请重绘,也就一直显示当前的页面。我们平时屏幕大部分时间还是静态的,所以不会那么高频地去进行刷新。除非我们在看电影、打游戏等需要持续变化的,才会高频地刷新屏幕。因为需要不断重绘元素或者画面。(此外,这里我做了一个测试,在动画过程中,执行耗时操作,并不会报ANR,只会跳帧/丢帧?)

    Choreographer: Skipped 599 frames!  The application may be doing too much work on its main thread.
    

    其实也是因为ANR只发生在:

    • Service Timeout:前台服务在20s内未执行完成
    • BroadcastQueue Timeout:前台广播在10s内未执行完成
    • ContentProvider Timeout:内容提供者在publish过超时10s;
    • InputDispatching Timeout:输入事件分发超时5s,包括按键和触摸事件。

    对于重绘、动画任务被主线程耗时操作堵住时不会报ANR,也是因为这个这两个事件超时执行了并不会报错,仅是在Choreographer中做了个JANK记录,打印出来。

    //Choreographer
    if (jitterNanos >= mFrameIntervalNanos) {
        final long skippedFrames = jitterNanos / mFrameIntervalNanos;
        if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
            Log.i(TAG, "Skipped " + skippedFrames + " frames!  "
                  + "The application may be doing too much work on its main thread.");
        }
        final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
        if (DEBUG_JANK) {
            Log.d(TAG, "Missed vsync by " + (jitterNanos * 0.000001f) + " ms "
                  + "which is more than the frame interval of "
                  + (mFrameIntervalNanos * 0.000001f) + " ms!  "
                  + "Skipping " + skippedFrames + " frames and setting frame "
                  + "time to " + (lastFrameOffset * 0.000001f) + " ms in the past.");
        }
        frameTimeNanos = startNanos - lastFrameOffset;
    

    但输入任务是有定时炸弹的,可以看到这篇文章:

    img

  5. 后续就进入到了View的绘制流程。具体如何绘制的,如何通过局部重绘来优化性能的,我们在View的绘制中探讨。

参考文献

https://blog.csdn.net/qq_34519487/article/details/113030181
https://www.jianshu.com/p/86d00bbdaf60
https://blog.csdn.net/Android062005/article/details/123090139

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/373816.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL创建表

在创建表时需要提前了解mysql里面的数据类型 常见的数据类型 创建表方式1&#xff1a; 格式&#xff1a; CREATE TABLE [IF NOT EXISTS] 表名( 字段1, 数据类型 [约束条件] [默认值], 字段2, 数据类型 [约束条件] [默认值], 字段3, 数据类型 [约束条件] [默认值], …… [表约束…

英语基础语法学习(B站英语电力公司)

1. 句子结构 五大基本句型&#xff1a; 主谓主谓宾主谓宾宾主谓宾宾补主系表 谓语&#xff1a; 一般来说&#xff0c;谓语是指主语发出的动作。&#xff08;动词&#xff09;但是很多句子是没有动作的&#xff0c;但是还是必须要有谓语。&#xff08;此时需要be动词&#x…

echo命令

这是一条内置命令。 输出指定的字符串 一、语法 echo [选项] [参数] 二、选项 -e&#xff1a;激活转义字符。 使用-e选项时&#xff0c;若字符串中出现以下字符&#xff0c;则特别加以处理&#xff0c;而不会将它当成一般文字输出&#xff1a; \a 发出警告声&#xff1b; \b 删…

k8s-yaml文件

文章目录一、K8S支持的文件格式1、yaml和json的主要区别2、YAML语言格式二、YAML1、查看 API 资源版本标签2、编写资源配置清单2.1 编写 nginx-test.yaml 资源配置清单2.2 创建资源对象2.3 查看创建的pod资源3、创建service服务对外提供访问并测试3.1 编写nginx-svc-test.yaml文…

pytorch入门2--数据预处理、线性代数的矩阵实现、求导

数据预处理是指将原始数据读取进来使得能用机器学习的方法进行处理。 首先介绍csv文件&#xff1a; CSV 代表逗号分隔值&#xff08;comma-separated values&#xff09;&#xff0c;CSV 文件就是使用逗号分隔数据的文本文件。 一个 CSV 文件包含一行或多行数据&#xff0c;每一…

尚硅谷nginx基础

nginx1. nginx安装1.1版本区别1.2安装步骤1.3 启动nginx1.4关于防火墙1.5 安装成系统服务1.6 配置nginx环境变量2. nginx基本使用2.1 基本运行原理2.2 nginx配置文件2.2.1 最小配置2.2.1.1 基本配置说明2.3 虚拟主机2.3.1域名、dns、ip地址的关系2.3.2IP地址和DNS地址的区别2.3…

Vue2 组件基础使用、父子组件之间的传值

一、什么是组件如画红框的这些区域都是由vue里的各种组件组成、提高复用信通常一个应用会以一棵嵌套的组件树的形式来组织&#xff1a;例如&#xff0c;你可能会有页头、侧边栏、内容区等组件&#xff0c;每个组件又包含了其它的像导航链接、博文之类的组件。为了能在模板中使用…

Mybatis中添加、查询、修改、删除

在Mybatis中添加数据的操作 编写相对应的SQL语句&#xff0c;并完成相关数据的对应关系 编写测试用例 需要提交事务 sqlSession commit() 这里需要注意的是mybatis是默认的是手动提交事务&#xff0c;如果不写的话会进行回滚&#xff0c;添加操作就不会被执行 或者在 如果…

15- TensorFlow基础 (TensorFlow系列) (深度学习)

知识要点 TensorFlow是深度学习领域使用最为广泛的一个Google的开源软件库 .TensorFlow中定义的数据叫做Tensor(张量), Tensor又分为常量和变量. 常量一旦定义值不能改变. 定义常量: t tf.constant([[1., 2., 3.], [4., 5., 6.]])定义变量: v tf.Variable([[1., 2., 3.], [4…

黑盒测试用例设计方法-边界值分析法

一、边界值定义 边界值分析法就是对输入或输出的边界值进行测试的一种黑盒测试方法。通常边界值分析法是作为对等价类划分法的补充&#xff0c;这种情况下&#xff0c;其测试用例来自等价类的边界。 长期的测试工作经验告诉我们&#xff0c;大量的错误是发生在输入或输出范围…

Vision Transformer(ViT)

1. 概述 Transformer[1]是Google在2017年提出的一种Seq2Seq结构的语言模型&#xff0c;在Transformer中首次使用Self-Atttention机制完全代替了基于RNN的模型结构&#xff0c;使得模型可以并行化训练&#xff0c;同时解决了在基于RNN模型中出现了长距离依赖问题&#xff0c;因…

TDG code

部分 数据集 参数设置 def setup_args(args None):args.algorithm_name TDG# args.algorithm_name HDGargs.user_num 1000000args.attribute_num 6args.domain_size 64args.epsilon 0.2args.dimension_query_volume 0.5args.query_num 20args.query_dimension 3运行…

leetcode 41~50 学习经历

leetcode 41~50 学习经历41. 缺失的第一个正数42. 接雨水43. 字符串相乘44. 通配符匹配45. 跳跃游戏 II46. 全排列47. 全排列 II48. 旋转图像49. 字母异位词分组50. Pow(x, n)小结41. 缺失的第一个正数 给你一个未排序的整数数组 nums &#xff0c;请你找出其中没有出现的最小的…

C语言数据结构(二)—— 受限线性表 【栈(Stack)、队列(Queue)】

在数据结构逻辑层次上细分&#xff0c;线性表可分为一般线性表和受限线性表。一般线性表也就是我们通常所说的“线性表”&#xff0c;可以自由的删除或添加结点。受限线性表主要包括栈和队列&#xff0c;受限表示对结点的操作受限制。一般线性表详解&#xff0c;请参考文章&…

数据结构基础之栈和队列

目录​​​​​​​ 前言 1、栈 2、队列 2.1、实现队列 2.2、循环队列 前言 上一篇中我们介绍了数据结构基础中的《动态数组》&#xff0c;本篇我们继续来学习两种基本的数据结构——栈和队列。 1、栈 特点&#xff1a;栈也是一种线性结构&#xff0c;相比数组&#xff…

(汇总记录)电机控制算法

1.S曲线应用电机加减速 电机控制 | S曲线加减速 - Tuple - 博客园 (cnblogs.com) 如要将S型曲线应用到电机的加减速控制上&#xff0c;需要将方程在X、Y坐标系进行平移&#xff0c;同时对曲线进行拉升变化&#xff1a;即 Y A B / ( 1 exp( -ax b ) ) &#xff0c;则根据该…

Pandas怎么添加数据列删除列

Pandas怎么添加数据列 1、直接赋值 # 1、直接赋值df.loc[:, "最高气温"] df["最高气温"].str.replace("℃", "").astype("int32")df.loc[:, "最低气温"] df["最低气温"].str.replace("℃"…

Java异常架构与异常关键字

Java异常简介 Java异常是Java提供的一种识别及响应错误的一致性机制。 Java异常机制可以使程序中异常处理代码和正常业务代码分离&#xff0c;保证程序代码更加优雅&#xff0c;并提高程序健壮性。在有效使用异常的情况下&#xff0c;异常能清晰的回答what, where, why这3个问…

【编程入门】N种编程语言做个应用市场(appstore)

背景 前面已输出多个系列&#xff1a; 《十余种编程语言做个计算器》 《十余种编程语言写2048小游戏》 《17种编程语言10种排序算法》 《十余种编程语言写博客系统》 《十余种编程语言写云笔记》 《N种编程语言做个记事本》 本系列做了个应用市场&#xff0c;支持下载安装安卓…

Bootstrap系列之导航

Bootstrap导航 可以在 ul 元素上添加 .nav类&#xff0c;在每个 li 选项上添加 .nav-item 类&#xff0c;在每个链接上添加 .nav-link 类: 基本的导航 <div class"container mt-3"><h2>导航</h2><p>简单的水平导航:</p><ul class&…