RenderThread:实现动画的异步渲染

作者: godliness | 来源:发表于2019-12-07 14:22 被阅读0次

    UI 优化系列专题,来聊一聊 Android 渲染相关知识,主要涉及 UI 渲染背景知识如何优化 UI 渲染两部分内容。


    UI 优化系列专题
    • UI 渲染背景知识

    View 绘制流程之 setContentView() 到底做了什么?
    View 绘制流程之 DecorView 添加至窗口的过程
    深入 Activity 三部曲(3)View 绘制流程
    Android 之 LayoutInflater 全面解析
    关于渲染,你需要了解什么?
    Android 之 Choreographer 详细分析

    • 如何优化 UI 渲染

    Android 之如何优化 UI 渲染(上)
    Android 之如何优化 UI 渲染(下)


    在 Android 中使用动画是非常常见的,无论是使用补间动画还是属性动画,都离不开 View 的绘制任务。我们知道 Android UI 绘制任务“都”是在主线程中完成的。那异步绘制是否可行呢?答案是肯定的,其关键就是今天要介绍的 RenderThread,对于 RenderThread 可能很多人对它并不了解,接下来我将教会大家如何利用 RenderThread 实现动画的异步渲染。

    什么是 RenderThread ?

    大家是否曾注意过,Android 在 5.0 之后对动画的支持更加炫酷了,但是 UI 绘制并没有因此受到影响,反而更加流畅。这其中很大的功劳源自于 RenderThread 的变化。在介绍 RenderThread 之前,我们需要先来了解下 Android 系统 UI 渲染的演进之路。

    在 Android 3.0 之前(或者没有启用硬件加速时),系统都会使用软件方式来渲染 UI。但是由于 CPU 在结构设计上的差异,对于图形处理并不是那么高效。这个过程完全没有利用 GPU 的图形高性能。

    CPU 和 GPU 结构设计如下:

    • 从结构图可以看出,CPU 的控制器较为复杂,而 ALU 数量较少,因此 CPU 更擅长各种复杂的逻辑运算,但不擅长数学尤其是浮点运算。而 GPU 的设计正是为实现大量数学运算。GPU 的控制器比较简单,但包含大量 ALU。GPU 中的 ALU 使用了并行设计,且具有较多的浮点运算单元。可以帮助我们加快栅格化操作。

    所以从 Android 3.0 开始,Android 开始支持硬件加速,但是到 Android 4.0 时才默认开启硬件加速。有关 Android 渲染框架详细内容,你可以参考《关于 UI 渲染,你需要了解什么?》。

    优化是无止境的,Google 在 2012 年的 I/O 大会上宣布了 Project Butter 黄油计划,并且在 Android 4.1 中正式开启了这个机制。Project Butter 主要包含三个组成部分,VSYNC、Triple Buffer 和 Choreographer。有关它们的详细分析,你可以参考如下资料:

    经过 Android 4.1 的 Project Butter 黄油计划之后,Android 的渲染性能有了很大的改善。不过你有没有注意到这样一个问题,虽然利用了 GPU 的图形高性能运算,但是从计算 DisplayList,到通过 GPU 绘制到 Frame Buffer,整个计算和绘制都在 UI 主线程中完成

    UI 线程“既当爹又当妈”,任务过于繁重。如果整个渲染过程比较耗时,可能造成无法响应用户的操作,进而出现卡顿的情况。GPU 对图形的绘制渲染能力更胜一筹,如果使用 GPU 并在不同线程绘制渲染图形,那么整个流程会更加顺畅。

    正因如此,在 Android 5.0 引入两个比较大的改变。一个是引入了 RenderNode 的概念,它对 DisplayList 及一些 View 显示属性都做了进一步封装。另一个是引入了 RenderThread,所有的 GL 命令执行都放到这个线程上,渲染线程在 RenderNode 中存有渲染帧的所有信息,可以做一些 View 的异步渲染任务,这样即便主线程有耗时操作的时候也可以保证渲染的流畅性。

    至此,我们已经知道 RenderThread 是 Android 5.0 之后的产物,用于分担主线程绘制任务的渲染线程。UI 可以进行异步绘制,那动画可以异步似乎也成为可能。所以,带着疑问,接下来我们还要对其进行一番探索实践,看如何利用 RenderThread 实现动画的异步渲染。

    原理探索

    经过查看官方文档,得知 RenderThread 目前仅支持两种动画的完全渲染工作(RenderThread 的文档介绍真的是少之又少)。

    1. ViewPreportyAnimator
    2. CircularReveal

    关于 CircularReveal(揭露动画)的使用比较简单且功能较为单一,在此不做过多的探索,今天我们着重探索下 ViewPropertyAnimator。

    final View view = findViewById(R.id.button);
    final ViewPropertyAnimator animator = view.animate().scaleY(2).setDuration(1000);
    animator.start();
    

    通过 View 的 animate() 即可创建 ViewPropertyAnimator 动画,注意它并不是 Animator 的子类。其内部提供了缩放、位移、透明度相关方法。

    public class ViewPropertyAnimator {
    
        /**
         * A RenderThread-driven backend that may intercept startAnimation
         */
        private ViewPropertyAnimatorRT mRTBackend;
    
        public ViewPropertyAnimator scaleX(float value) {
            animateProperty(SCALE_X, value);
            return this;
        }
    
         // ... 省略 scaleY
    
        public ViewPropertyAnimator translationX(float value) {
            animateProperty(TRANSLATION_X, value);
            return this;
        }
    
         // ... 省略 translationY
    
        public ViewPropertyAnimator alpha(float value) {
            animateProperty(ALPHA, value);
            return this;
        }
    
         /**
           *  开始动画
           */
        private void startAnimation() {
            // 是否能够通过 ReanderThread 渲染关键在这里
            if (mRTBackend != null && mRTBackend.startAnimation(this)) {
                // 使用 RenderThread 异步渲染动画
                return;
            }
            // 否则将会降解为普通熟悉动画
            ValueAnimator animator = ValueAnimator.ofFloat(1.0f);
           
            // ......
            animator.start();
        }
    }
    

    我们需要重点关注的是 startAnimator 方法,在该方法首先对 mRTBackend 进行了判断,它的实际类型是 ViewPropertyAnimatorRT,如果不为 null,则由它来执行动画。如果 if 条件不成立,也就是此时不支持 RenderThread 完全渲染。很明显 RenderThread 渲染动画和 ViewPropertyAnimatorRT 有直接关系。

    class ViewPropertyAnimatorRT {
    
        .....
    
        ViewPropertyAnimatorRT(View view) {
            mView = view;
        }
    
        public boolean startAnimation(ViewPropertyAnimator parent) {
            cancelAnimators(parent.mPendingAnimations);
            // 关键在这里判断是否成立
            if (!canHandleAnimator(parent)) {
                return false;
            }
            // 执行 RenderThread 异步渲染动画
            doStartAnimation(parent);
            return true;
        }
    
        ......
    }
    

    可以看到 startAnimation 方法先通过 canHandleAnimator 方法判断是否成立,如果不成立返回 false,此时回到 ViewPropertyAnimator 动画将会退化成普通属性动画。否则执行 doStartAnimation 方法。

    我们先看下 canHandleAnimator 的判断条件,它的参数是 ViewPropertyAnimator:

    private boolean canHandleAnimator(ViewPropertyAnimator parent) {
    
          if (parent.getUpdateListener() != null) {
              return false;
          }
          if (parent.getListener() != null) {
              // TODO support
              return false;
          }
          if (!mView.isHardwareAccelerated()) {
              // TODO handle this maybe?
              return false;
          }
          if (parent.hasActions()) {
              return false;
          }
          // Here goes nothing...
          return true;
    }
    

    可以看出代码逻辑是比较清楚了,① 是否支持硬件加速(Android 在 3.0 开始支持硬件加速,在 4.0 默认开启),② 是否设置了监听 Listener 或 UpdateListener,或者设置了 Action(监听动画开始、结束)都会导致 canHandleAnimator 方法返回 false,从而导致 doStartAnimator 方法无法执行。在此我们得到一个非常重要的条件是不进行任何监听器设置,确保 canHandleAnimator 返回 true

    下面接着看 doStartAnimation 方法,执行 doStartAnimation 方法表示动画将被 RenderThread 执行。

    private void doStartAnimation(ViewPropertyAnimator parent) {
         int size = parent.mPendingAnimations.size();
    
        // 启动延迟时间
         long startDelay = parent.getStartDelay();
         // duration 执行时间
         long duration = parent.getDuration();
         // 插值器
         TimeInterpolator interpolator = parent.getInterpolator();
         if (interpolator == null) {
             // Documented to be LinearInterpolator in ValueAnimator.setInterpolator
             // 默认线性插值器
             interpolator = sLinearInterpolator;
         }
         if (!RenderNodeAnimator.isNativeInterpolator(interpolator)) {
             interpolator = new FallbackLUTInterpolator(interpolator, duration);
         }
         for (int i = 0; i < size; i++) {
             NameValuesHolder holder = parent.mPendingAnimations.get(i);
             int property = RenderNodeAnimator.mapViewPropertyToRenderProperty(holder.mNameConstant);
    
             final float finalValue = holder.mFromValue + holder.mDeltaValue;
             // 对于每个动画属性都创建了RenderNodeAnimaor
             RenderNodeAnimator animator = new RenderNodeAnimator(property, finalValue);
             animator.setStartDelay(startDelay);
             animator.setDuration(duration);
             animator.setInterpolator(interpolator);
             animator.setTarget(mView);
             animator.start();
    
             mAnimators[property] = animator;
         }
    
         parent.mPendingAnimations.clear();
    }
    

    ViewPropertyAnimator 的 mPendingAniations 保存了动画的每个属性。doStartAnimation 方法为每个动画属性都创建了一个 RenderNodeAnimator,然后将对应的动画参数也设置给了 RenderNodeAnimator,此处就完成了动画和属性的绑定。

    接下来我们要跟踪下 RendernodeAnimator,

    public class RenderNodeAnimator extends Animator {
    
        public void setTarget (View view) {
            mViewTarget = view;
            setTarget (mViewTarget.mRenderNode);
        }
    
        private void setTarget (RenderNode node){
            ......
            mTarget = node;
            mTarget.addAnimator(this);
        }
    }
    

    setTarget 方法将当前 View 的 RenderNode 和 RenderNodeAnimator 通过 addAnimator 进行绑定。在 RenderNode 的 addAnimator 方法通过 Native 方法 nAddAnimator 将其注册到 AnimatorManager 中。

    public class RenderNode {
    
        public void addAnimator(RenderNodeAnimator animator) {
            if (mOwningView == null || mOwningView.mAttachInfo == null) {
                throw new IllegalStateException("Cannot start this animator on a detached view!");
            }
            // Native 方法注册到AnimatorManager
            nAddAnimator(mNativeRenderNode, animator.getNativeAnimator());
            mOwningView.mAttachInfo.mViewRootImpl.registerAnimatingRenderNode(this);
        }
    
    }
    

    nAddAnimator 方法实现如下:

    static void android_view_RenderNode_addAnimator (JNIEnv* env, jobject clazz, jlong renderNodePtr, jlong animatorPtr ){
        RenderNode* renderNode = reinterpret_cast<RenderNode*> (renderNodePtr);
        RenderPropertyAnimator* animator = reinterpret_cast<RenderPropertyAnimator*> (animatorPtr);
        renderNode -> addanimator(animator);
    }
    
    void RenderNode :: addAnimator (const sp<BaseRenderNodeAnimaor>& animator){
        // 添加到 AnimatorManager
        mAnimatorManager.addAnimator(animator);
    }
    

    至此,我们清楚了动画是如何被添加到 AnimatorManager 中。根据其官方文档的介绍,后续 AnimatorManager 和 RenderThread 的操作交由系统处理,进而让 RenderThread 去完全管理动画,实现由 RenderThread 渲染动画。


    代码实践

    通过上面原理探索阶段,为了能够让动画顺利交给 RenderThread,除了不能设置任何回调且 View 支持硬件加速(Android 4.0 之后默认支持)之外,还必须必须满足 ViewPropertyAnimatorRT 不为 null,它是让动画交由 RenderThread 的关键。

    但是翻阅源码,并未发现任何创建该对象的地方。此时我们需要一些特殊的操作以达到预期的效果。通过查看源码,发现 ViewPropertyAnimatorRT 属于包保护级别,而且没有被 @hide(Android P 之后也没有关系),所以我们直接采用反射的方式完成。

    ps:国外一篇博客中介绍:每个组件都是隐藏的,因此要使用它们,必须通过反射获得对所有需要类 / 方法的引用。

    为 View 创建 ViewPropertyAnimatorRT 对象:

    private static Object createViewPropertyAnimatorRT(View view) {
        try {
            final Class<?> animRtCalzz = Class.forName("android.view.ViewPropertyAnimatorRT");
            final Constructor<?> animRtConstructor = animRtCalzz.getDeclaredConstructor(View.class);
            animRtConstructor.setAccessible(true);
            return animRtConstructor.newInstance(view);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    

    然后将 ViewPropertyAnimatorRT 设置到对应的 ViewPropertyAnimator:

    private static void setViewPropertyAnimatorRT(ViewPropertyAnimator animator, Object rt) {
        try {
            final Class<?> animClazz = Class.forName("android.view.ViewPropertyAnimator");
            final Field animRtField = animClazz.getDeclaredField("mRTBackend");
            animRtField.setAccessible(true);
            animRtField.set(animator, rt);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    

    在动画执行前需要先执行上述步骤以满足相关条件:

    final View view = findViewById(R.id.button);
    final ViewPropertyAnimator animator = view.animate().scaleY(2).setDuration(1000);
    // 必须在 start 之前
    AsyncAnimHelper.onStartBefore(animator, view);
    animator.start();
    

    设置两种动画分别在执行 1s 后,让主线程休眠 2s(模拟主线程卡顿)。可以很明显看到普通属性动画,在主线程阻塞的时候,会出现丢帧卡顿现象。而使用 RenderThread 渲染的动画即使阻塞了主线程仍然不受影响,如下图所示(上面控件为普通属性动画):


    以上便是关于 RenderThread 实现动画的异步渲染的探索和实践,文中如果不妥或有更好的分析结果,欢迎您的分享留言或指正。

    Android 渲染框架非常庞大,并且演进的非常快,更多 Android 渲染框架的知识,感兴趣的朋友可以参考如下资料:


    最后,如果你有更好的分析结果或实践方案,欢迎您的分享留言或指正。

    文章如果对你有帮助,请留个赞吧!


    其他系列专题

    相关文章

      网友评论

        本文标题:RenderThread:实现动画的异步渲染

        本文链接:https://www.haomeiwen.com/subject/yxtowctx.html