专栏首页码上积木来看“Android编舞者”怎么编舞

来看“Android编舞者”怎么编舞

前言

接上回,今天说说VSync机制的相关处理,也就是Choreographer类。

每次说到源码就很难表述,所以今天还是通过问题的方式,一步步解析这个“编舞者”

(本篇涉及到大量Handler知识点,如果忘记的朋友可以再翻翻我之前写的《Handler27问》- https://juejin.cn/post/6943048240291905549)

界面有绘制任务时候,会马上执行performTraversals吗?

当界面有绘制任务,一般是执行了两种方法:

  • View.invalidate
  • View.requestLayout

而这两个方法的的终点都是ViewRootImp类的scheduleTraversals方法,开始View树的绘制任务。

我们就从这里入手:

    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        }
    }

主要做了三件事:

  • 设置了mTraversalScheduled标识位为true。这个标示位的用法,很像我们防止按钮多次点击时候的逻辑,所以我们可以猜测下它就是为了防止某个时间段多次绘制任务的提交。
  • 给主线程的Handler加入了同步屏障。同步屏障我们之前说过了,它的存在是为了保证后续的异步消息优先执行。
  • 最后就是用到我们的Choreographer类,调用了postCallback方法。

我们先不看这个postCallback方法,而是看看传入的一个参数mTraversalRunnable

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

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

可以发现,在这个Runnable中执行了doTraversal,同样是三件事:

  • 设置mTraversalScheduled标识位为false。
  • 移除同步屏障
  • 执行performTraversals方法,开始绘制。

这不巧了吗,刚好对应了刚才的三件事。

也就是在这里,执行了performTraversals方法开始绘制View树,即layout、measure、draw

所以这个标识位我们可以断定了,就是防止在开始绘制之前的某个时间段内多次提交绘制需求。而且在这里移除了同步屏障,因为绘制任务已经在执行了,不用怕别的Handler消息任务来插队了。

那这个过程到底发生了什么呢?是马上执行了doTraversal方法吗?悬念自然就在postCallback方法中了,继续下一个问题。

Choreographer单例是怎么实现的?保存在哪里?

刚才我们看到了编舞者Choreographer的身影,那么它又是从哪里来的呢?

 public ViewRootImpl(Context context, Display display) {
        //...
        mChoreographer = Choreographer.getInstance();
    }


    //Choreographer.java
    private static final ThreadLocal<Choreographer> sThreadInstance =
            new ThreadLocal<Choreographer>() {
        @Override
        protected Choreographer initialValue() {
            Looper looper = Looper.myLooper();
            if (looper == null) {
                throw new IllegalStateException("The current thread must have a looper!");
            }
            Choreographer choreographer = new Choreographer(looper, VSync_SOURCE_APP);
            if (looper == Looper.getMainLooper()) {
                mMainInstance = choreographer;
            }
            return choreographer;
        }
    };

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

ViewRootImpl构造方法中获取了Choreographer实例,至于怎么获取的。。没错,又是它:ThreadLocal

细节就不说了,ThreadLocal主要是保证一个线程对应一个实例,Choreographer一般只存在于主线程,所以就是用于单例。

Choreographer怎么处理绘制任务呢?

好了,接着上面的说说postCallback方法,也就是绘制任务被交给Choreographer之后是怎么处理的呢?

    public void postCallback() {
        postCallbackDelayed(callbackType, action, token, 0);
    }
    public void postCallbackDelayed() {
        postCallbackDelayedInternal(callbackType, action, token, delayMillis);
    }

    private void postCallbackDelayedInternal() {

        synchronized (mLock) {
            final long now = SystemClock.uptimeMillis();
            final long dueTime = now + delayMillis;

            mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

            if (dueTime <= now) {
             //如果没有延时,马上预定下一个VSync信号
                scheduleFrameLocked(now);
            } else {
             //会在任务执行时间去预定VSync信号
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
                msg.arg1 = callbackType;
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, dueTime);
            }
        }
    }
  • 首先,看到delayMillis,就知道这个任务是可以延迟的,跟Handler很像,会把延迟后的时间记录下来,也就是任务的执行时间。
  • 然后,把这个任务带上执行时间加到了mCallbackQueues数组
  • 最后,判断了任务执行时间,如果没有延时就马上执行scheduleFrameLocked方法,否则就通过Handler延时执行任务。

继续看scheduleFrameLocked方法:

 private void scheduleFrameLocked(long now) {
        if (!mFrameScheduled) {
            mFrameScheduled = true;
            if (USE_VSync) {
                //开启了VSync
                if (isRunningOnLooperThreadLocked()) {
                    scheduleVSyncLocked();
                } else {
                    Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSync);
                    msg.setAsynchronous(true);
                    mHandler.sendMessageAtFrontOfQueue(msg);
                }
            } else {
             //没有开启VSync
                final long nextFrameTime = Math.max(
                        mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
                
                Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, nextFrameTime);
            }
        }
    }
  • 如果开启了VSync,会判断当前是否运行在主线程,如果不是主线程就通过Handler切换线程,执行MSG_DO_SCHEDULE_VSync消息。
  • 如果在主线程,就执行scheduleVSyncLocked方法,所以我们猜测MSG_DO_SCHEDULE_VSync消息应该也是执行这个方法。
  • 如果没有开启VSync,就会通过Handler执行MSG_DO_FRAME消息。

到此,我们知道了Choreographer把任务加到数组之后,就开始根据VSync执行了scheduleVSyncLocked方法,所以这个方法自然跟VSync也就是垂直同步信号有关了,继续看看。

Choreographer与VSync信号的关系?每次VSync信号都能接收到吗?

    private void scheduleVSyncLocked() {
        mDisplayEventReceiver.scheduleVSync();
    }

    public void scheduleVSync() {
        if (mReceiverPtr == 0) {
        } else {
            nativeScheduleVSync(mReceiverPtr);
        }
    }

    private final class FrameDisplayEventReceiver extends DisplayEventReceiver
            implements Runnable {
        
        @Override
        public void onVSync(long timestampNanos, long physicalDisplayId, int frame) {
         //...
        }

        @Override
        public void run() {
         //...
        }
    }

哎呀,这就没了?

所以绘制任务传到Choreographer之后,最终调用的是native方法nativeScheduleVSync

这个底层方法就不去细究了,它的目的就是注册VSync信号的监听。

nativeScheduleVSync() 会向 SurfaceFlinger 注册VSync信号的监听,VSync信号由 SurfaceFlinger 实 现并定时发送,当VSync信号来的时候就会回调 FrameDisplayEventReceiver#onVSync()

所以,当有绘制任务的时候,Choreographer会去注册VSync信号的监听,注册之后,下一个垂直同步信号到来的时候,才会传到Choreographer中的FrameDisplayEventReceiver#onVSync()方法。

也就是说,不需要绘制的时候,VSync信号是不会发到Choreographer这里的。虽然每隔16.6ms,就有一次VSync信号,但是只有需要绘制的时候,才会去订阅VSync信号,然后才开始真正的绘制工作。

好像剧透了?还没说到绘制工作呢。。

VSync信号到来的时候,Choreographer会怎么操作?

接下来就看看垂直同步信号来的时候,Choreographer做了啥。刚才说到会执行FrameDisplayEventReceiver#onVSync()方法。

private final class FrameDisplayEventReceiver extends DisplayEventReceiver
            implements Runnable {

        @Override
        public void onVSync(long timestampNanos, long physicalDisplayId, int frame) {

            Message msg = Message.obtain(mHandler, this);
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
        }

        @Override
        public void run() {
            doFrame(mTimestampNanos, mFrame);
        }
    }

这里要注意的是,这里的Runnable是包装成为Handler中的异步消息加到了消息队列中,而这个Handler也是主线程的Handler,待会会细说。

Runnable中的run方法,会执行到doFrame方法,这个doFrame我们就可以理解为这一帧要做的事情。

到这里,VSync信号机制其实就显现出来了,正常情况下,每隔16.6ms系统就会发送一次VSync信号。

  • 如果有绘制任务,就会通过Choreographer类进行下一次信号的监听
  • 当垂直同步信号来了,就会切换到主线程执行doFrame方法开始下一帧数据的绘制,也就是之前所说的CPU工作的开始。
  • 没有任务的情况下,VSync信号就不会发到Choreographer这里。

来个图:

最后看看doFrame方法:

void doFrame(long frameTimeNanos, int frame) {
        final long startNanos;
        synchronized (mLock) {
            if (!mFrameScheduled) {
                return; // no work to do
            }
            //VSync信号时间
            long intendedFrameTimeNanos = frameTimeNanos;
            //当前时间
            startNanos = System.nanoTime();
            //时间差
            final long jitterNanos = startNanos - frameTimeNanos;
            //如果当前时间大于Vsync信号时间
            if (jitterNanos >= mFrameIntervalNanos) {
             //就设置这一帧的时间为上一个Vsync信号时间
                final long skippedFrames = jitterNanos / mFrameIntervalNanos;
                final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
                frameTimeNanos = startNanos - lastFrameOffset;
            }

            //如果这一帧时间小于上次帧绘制时间,就重新申请新的VSync信号
            if (frameTimeNanos < mLastFrameTimeNanos) {
                scheduleVsyncLocked();
                return;
            }
        }

        try {
         //依次执行任务
            doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
            doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
            doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos);
            doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
            doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
        } 
        
    }

中间的一系列时间判断其实是对时间做校正,暂且按下不表,看看最后执行任务的时候干了啥,也就是doCallbacks方法。

 void doCallbacks(int callbackType, long frameTimeNanos) {
        CallbackRecord callbacks;

        callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(now / TimeUtils.NANOS_PER_MS);
        try {
            for (CallbackRecord c = callbacks; c != null; c = c.next) {
                c.run(frameTimeNanos);
            }
        } 
    }

    public void run(long frameTimeNanos) {
            if (token == FRAME_CALLBACK_TOKEN) {
                ((FrameCallback)action).doFrame(frameTimeNanos);
            } else {
                ((Runnable)action).run();
            }
    }

粗略的看下,可以得知其实是执行到任务中action的run方法,这个action很明显就是当初传进来的Runnable,所以最后就是执行到doTraversal方法,开始三大绘制流程。

终于,一个环闭合了,当然中间还有很多细节,我们慢慢看。

Choreographer内部的Handler是干什么的?

刚才说到很多次Choreographer内部的Handler,来看看它是何方神圣:

 private Choreographer(Looper looper, int vsyncSource) {
        mHandler = new FrameHandler(looper);
    }


 private final class FrameHandler extends Handler {
        public FrameHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_DO_FRAME:
                 //绘制新的一帧数据
                    doFrame(System.nanoTime(), 0);
                    break;
                case MSG_DO_SCHEDULE_VSYNC:
                 //申请VSYNC信号
                    doScheduleVsync();
                    break;
                case MSG_DO_SCHEDULE_CALLBACK:
                 //延迟预定VSync信号
                    doScheduleCallback(msg.arg1);
                    break;
            }
        }
    }

Choreographer的单例存在于主线程,所以这里的FrameHandler也是主线程的Handler。

所以这里的Handler有两个作用:

  • 1、作为切换到主线程,比如MSG_DO_FRAME消息。
  • 2、延时消息,比如之前说到的延迟预定VSync信号,发的就是MSG_DO_SCHEDULE_CALLBACK消息。

Choreographer中保存任务的数组结构是如何?

还记得刚才添加绘制任务的时候吗?调用了addCallbackLocked方法:

mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

进去看看里面的任务处理:

    mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
    for (int i = 0; i <= CALLBACK_LAST; i++) {
        mCallbackQueues[i] = new CallbackQueue();
    }

private final class CallbackQueue {
 //链表
    private CallbackRecord mHead;
    //添加链表节点
 public void addCallbackLocked(long dueTime, Object action, Object token) {
            CallbackRecord callback = obtainCallbackLocked(dueTime, action, token);
            CallbackRecord entry = mHead;
            if (entry == null) {
                mHead = callback;
                return;
            }
            //插入链表头部
            if (dueTime < entry.dueTime) {
                callback.next = entry;
                mHead = callback;
                return;
            }
            //找到合适的时间点插入
            while (entry.next != null) {
                if (dueTime < entry.next.dueTime) {
                    callback.next = entry.next;
                    break;
                }
                entry = entry.next;
            }
            entry.next = callback;
        }
    }

也?又是链表嗦。

没错,数组里面存储的是CallbackQueue对象,而CallbackQueue对象里面存储的就是链表。

那数组的序号callbackType又是什么呢?

 //输入任务
    public static final int CALLBACK_INPUT = 0;
    //动画任务
    public static final int CALLBACK_ANIMATION = 1;
    //插入更新动画任务
    public static final int CALLBACK_INSETS_ANIMATION = 2;
    //绘制任务
    public static final int CALLBACK_TRAVERSAL = 3;
    //提交任务
    public static final int CALLBACK_COMMIT = 4;

而我们的view树绘制任务传入的就是CALLBACK_TRAVERSAL

到这里,Choreographer中的数组结构我们算是搞清楚了,数组一共有五个对象,分别对应着每个任务类别,而每个对象都存储着一个任务链表,记录了任务的回调和时间。

而最后处理的时候,也是按照链表上元素的时间进行处理:

public CallbackRecord extractDueCallbacksLocked(long now) {
            CallbackRecord callbacks = mHead;
            if (callbacks == null || callbacks.dueTime > now) {
                return null;
            }

            CallbackRecord last = callbacks;
            CallbackRecord next = last.next;
            while (next != null) {
                if (next.dueTime > now) {
                    last.next = null;
                    break;
                }
                last = next;
                next = next.next;
            }
            mHead = next;
            return callbacks;
        }

通过遍历,把任务时间大于当前时间的任务都摘除掉,只剩下当前时间之前的所有任务,然后去遍历执行run方法。

其实这里我好奇的一点是,比如绘制任务CALLBACK_TRAVERSAL,明明同一帧时间内,scheduleTraversals只会执行一次。原因刚才说过了,通过mTraversalScheduled字段控制的。

void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
        }
    }

那么,为什么这里要用链表结构呢?还遍历链表进行处理。

于是我去全局搜索了CALLBACK_TRAVERSAL任务,结果还真发现了另外的地方也调用了:

//DisplayPowerState

    private void scheduleColorFadeDraw() {
        if (!mColorFadeDrawPending) {
            mColorFadeDrawPending = true;
            mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL,
                    mColorFadeDrawRunnable, null);
        }
    }

DisplayPowerState应该是控制电量相关View的。

虽然没有细看,但是说明界面的绘制应该不只是scheduleTraversals一种情况的,有了解的朋友可以来讨论下。

哪些情况会导致掉帧呢(开发角度考虑)?Choreographer又是怎么处理的?

接下来就看看实际会发生掉帧的情况。

1、发起了一次绘制任务,然后会申请VSync信号,当信号来的时候,将doFrame消息加到主线程的消息队列中,这时候主线程正在被其他任务占用,并且迟迟不能结束。**

我们画个图表示下这种情况:

可以看到,在VSync1之前我们就提交了绘制任务,按道理应该是VSync1到来的时候执行doFrame方法,但是被耗时任务耽误了,等到耗时任务执行完已经是VSync3之后的时间了,所以本该VSync2的时间点屏幕就可以显示第二张图片,结果被拖到了VSync4的时间点才显示第二张图片,发生了掉帧情况。

这时候执行onFrame会怎么处理呢?这就涉及到刚才没说到的时间校正了:

void doFrame(long frameTimeNanos, int frame) {
        final long startNanos;
        synchronized (mLock) {
            if (!mFrameScheduled) {
                return; // no work to do
            }
            //VSync信号时间
            long intendedFrameTimeNanos = frameTimeNanos;
            //当前时间
            startNanos = System.nanoTime();
            //时间差
            final long jitterNanos = startNanos - frameTimeNanos;
            //如果当前时间大于Vsync信号时间
            if (jitterNanos >= mFrameIntervalNanos) {
             //就设置这一帧的时间为上一个Vsync信号时间
                final long skippedFrames = jitterNanos / mFrameIntervalNanos;
                final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
                frameTimeNanos = startNanos - lastFrameOffset;
            }

            //如果这一帧时间小于上次帧绘制时间,就重新申请新的VSync信号
            if (frameTimeNanos < mLastFrameTimeNanos) {
                scheduleVsyncLocked();
                return;
            }

            mLastFrameTimeNanos = frameTimeNanos;
        }
    }

进入doFrame方法,frameTimeNanos应该为VSync1的时间,而当前时间是在VSync3之后,假设为VSync3+5ms

所以时间差 jitterNanos = 两帧时间+5ms ,上一帧时间偏差 lastFrameOffset = 5ms

而这里做的校正就是把要绘制的这一帧时间frameTimeNanos设置为最近一帧的时间,也就是VSync3的时间,然后赋值给mLastFrameTimeNanos

校正之后,就能保证帧的处理时间与VSync信号的时间是在一个节奏上的,保证帧处理的连贯性。

2、View树绘制的时间过长。

也就是doTraversal方法的执行时间过长,也就是doFrame本身的执行时间过长,再来张图片说明:

正常情况下,doFrame方法应该在一帧时间内,也就是16.6ms内完成绘制工作。但是如果绘制时间过长,跨过了几帧时间,那么新一帧的显示也就延后了,导致了掉帧情况。

这时候Choreographer又是怎么处理的呢?

void doFrame(long frameTimeNanos, int frame) {
        //...
        try {
         //依次执行任务
            doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
            doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
            doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos);
            doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
            doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
        } 
        
    }

可以看到,doFrame方法中,执行了一系列任务之后,还执行了一个CALLBACK_COMMIT任务,这个任务有点特殊,它包含了对doFrame方法执行完毕的一个处理:

void doCallbacks(int callbackType, long frameTimeNanos) {
        CallbackRecord callbacks;
        synchronized (mLock) {
            if (callbackType == Choreographer.CALLBACK_COMMIT) {
                final long jitterNanos = now - frameTimeNanos;
                if (jitterNanos >= 2 * mFrameIntervalNanos) {
                    final long lastFrameOffset = jitterNanos % mFrameIntervalNanos
                            + mFrameIntervalNanos;
                    frameTimeNanos = now - lastFrameOffset;
                    mLastFrameTimeNanos = frameTimeNanos;
                }
            }
        }

当任务为CALLBACK_COMMIT的时候,会再次判断当前时间和 绘制帧时间 的时间差,如果大于两个帧时间,就将绘制帧时间移动到 最近一个VSync时间点之前的一个VSync时间点,也就是倒数第二个VSync时间点。

从图中分析下:

从刚才的代码分析我们得知,frameTimeNanos每次会被设置为倒数第一个垂直信号的时间,也就是VSync1时间点。

现在在每次doFrame执行完毕后,如果发现doFrame耗时超过两帧时间,也就是图中黄色耗时,那么就把frameTimeNanos时间设置为倒数第二个垂直信号时间点,也就是VSync2时间点。

这又是为何呢?

还是为了保证帧处理时间的连贯性,同时又保证了帧时间的不重复。

例如在执行doFrame耗时方法的时候(叫他doFrame1方法),VSync3之前又申请了一个绘制任务,那么当VSync3来的时候,就向Handler中加了新的一个doFrame消息,我们叫他doFrame2消息。

  • 如果doFrame1方法执行完,不校正时间,那么帧时间frameTimeNanos还是为VSync1。而doFrame2执行的时候,帧时间为VSync3,就不连贯了。
  • 如果doFrame1方法执行完,校正时间为倒数第一个帧时间,也就是为VSync3。结果doFrame2执行的帧时间也是VSync3,就出现了重复的情况。

所以,考虑连贯性和不重复性,就设置为倒数第二个垂直信号的时间点为最后的帧时间。

可以通过代码统计掉帧情况吗?

可以,用到的是Choreographer的postFrameCallback方法。

Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
    @Override
    public void doFrame(long frameTimeNanos) {
       long starTime=System.nanoTime();
       Log.e(TAG,"starTime="+starTime+", frameTimeNanos="+frameTimeNanos+", frameDueTime="+(frameTimeNanos-starTime)/1000000);
    }
});

通过Choreographer的回调方法,我们可以获取每一帧的获取时间。我们可以进去看看原理:

public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
        postCallbackDelayedInternal(CALLBACK_ANIMATION,
                callback, FRAME_CALLBACK_TOKEN, delayMillis);
    }

    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) {
                ((FrameCallback)action).doFrame(frameTimeNanos);
            } else {
                ((Runnable)action).run();
            }
        }
    }

同样是发送了一个任务,不同的是token=FRAME_CALLBACK_TOKEN

所以当VSync信号来的时候,会执行回调方法doFrame,我们就可以在这里自己进行帧时间处理了。

总结:Choreographer是什么?

Choreographer到底是什么呢?

  • 是View绘制、动画等界面变动任务的接收和执行者。
  • 是可以对VSync信号进行预约和响应的监听者
  • 是同步VSync信号和绘制工作的编舞者。

参考

https://www.jianshu.com/p/0a54aa33ba7d https://juejin.cn/post/6864365886837686285

感谢大家的阅读,有一起学习的小伙伴可以关注下公众号—码上积木❤️ 每日一个知识点,建立完整体系架构。

本文分享自微信公众号 - 码上积木(Lzjimu),作者:积木zz

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2021-04-23

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Android系统的编舞者Choreographer

    上一篇文章 Android的16ms和垂直同步以及三重缓存 解释了手机流畅性的问题,并在文章中提到了在Android4.1中添加的Vsync。Choreogra...

    静默加载
  • Android 性能采集之Fps,Memory,Cpu

    各位大佬好久不见了,憋了一阵子发育了一下(主要是我在拼神龙斗士),基本上完成了简单的性能采集的Demo,分享一下相关的经验给各位吧。

    逮虾户
  • 【全球首届机器人春晚】“王菲”、“杰伦”跳广场舞、讲相声、下五子棋

    新智元推荐 来源:机器人家 编辑:艾霄葆 【新智元导读】大年三十,除了晚上央视的春节联欢晚会,还有另一场机器人春晚也同样值得期待。今天14:20 BTV...

    新智元
  • 说说Android的UI刷新机制的实现

    在Android中,一块Surface对应一块内存,当内存申请成功后,App端才有绘图的地方。由于Android的view绘制不是今天的重点,所以这里点到为止~

    砸漏
  • 给机器人编舞什么感觉?

    黄翊和机器人”库卡“ 给机器人编舞是一种怎样的体验?在黄翊眼中,名叫“库卡”的机器人有呼吸、有温度,和一只小狗,一头小象,或是一个孩子没什么分别。...

    机器人网
  • 波士顿动力机器人热舞背后藏着哪些秘密?工程副总裁揭秘

    一周前,波士顿动力上传了一段机器人跳舞的视频,人形机器人 Atlas、机器狗 Spot、双轮机器人 Handle 伴随着 The Contours 的歌曲《Do...

    机器之心
  • GTC '19 经典回顾 | 如何编排和创造二次元中的舞蹈?

    场景描述:日前于苏州举办的 GTC China 2019 上,网易雷火伏羲实验室介绍了其 AI 编舞项目,借此提升游戏中动画的表现。将传统的人工编舞技术赋予 A...

    HyperAI超神经
  • Wolfram语言与舞王的发明

    演唱会结束了,观众满意的散场了。而近三个小时的劲歌热舞,令迈克精疲力竭。道具和化妆师布什,一边忙着帮迈克卸妆,一边为迈克抹去脸上的汗珠,汗水浸透了他白色的背心。...

    WolframChina
  • 波士顿动力副总裁:机器人一天就学会了芭蕾舞,尚未使用机器学习技术

    新年之前,波士顿动力上传了一段机器人跳舞的视频引爆全网。现在这段视频仅在YouTube平台就已经被观看超过2500万次。

    代码医生工作室
  • 吃了这些数据集和模型,跟 AI 学跳舞,做 TensorFlowBoys

    场景描述:利用深度学习算法 GAN 可实现动作追踪与迁移,将某人物动作复制到其他人,应用到舞蹈领域,人人皆可成舞王。

    统计学家
  • 吃了这些数据集和模型,跟 AI 学跳舞,做 TensorFlowBoys

    场景描述:利用深度学习算法 GAN 可实现动作追踪与迁移,将某人物动作复制到其他人,应用到舞蹈领域,人人皆可成舞王。

    HyperAI超神经
  • LayaAir引擎学习经历

      由于公司任务安排,需要笔者先去了解一下LayaAir引擎库,以用来完成公司将要启动的大数据可视化项目,需要借助LayaAir引擎实现复杂的动画。笔者花两天时...

    饮水思源为名
  • 等候期间看AR太空秀,科切拉音乐节化身“朋克俱乐部”?

    恰逢科切拉音乐节正在举行(4月12-14日、4月19-21日),全世界的粉丝们都在期盼着能够在现场欣赏到最新的音乐榜单,并一睹顶级名流的风采。

    VRPinea
  • Silverlight初级教程-库

    Silverlight初级教程 库 flash中有库这个概念。库里可以放很多的影片剪辑“MC”,一个MC可以在很多的地方使用,修改了库中的MC所有用到这个MC...

    用户1172164
  • 硬核粉丝 | 清华双胞胎“YCY Dance Now”杀进超越杯编程大赛决赛

    从“黄蓉 AI 换脸 杨幂”、“首届杨超越编程大赛”、“cxk 流量或打篮球的技术视频”、到《996.ICU》等系列项目、再到现在最火的《复仇者联盟》,程序员才...

    数据派THU
  • 中国版初音未来,《热力舞伴》嗨爆Chinajoy 全场

    VRPinea
  • 用DensePose,教照片里的人学跳舞,系群体鬼畜 | ECCV 2018

    Facebook团队,把负责感知的多人姿势识别模型DensePose,与负责生成的深度生成网络结合起来。

    量子位
  • 7.11 VR扫描:Oculus VR或将低于399美元;美图与Natura推口红试妆服务

    VRPinea
  • 资源 | DanceNet:帮你生成会跳舞的小姐姐

    DanceNet 中最主要的三个模块是变分自编码器、LSTM 与 MDN。其中变分自编码器(VAE)是最常见的生成模型之一,它能以无监督的方式学习复杂的分布,因...

    机器之心

扫码关注云+社区

领取腾讯云代金券