首页 文章详情

纳尼?动画还能在非UI线程执行?

刘望舒 | 456 2021-03-07 15:09 0 0 0
UniSMS (合一短信)

作者:godliness

https://www.jianshu.com/p/70789a2eb9de

1.概述

在 Android 中使用动画是非常常见的,无论是使用补间动画还是属性动画,都离不开 View 的绘制任务。我们知道 Android UI 绘制任务“都”是在主线程中完成的。


那异步绘制是否可行呢?


答案是肯定的,其关键就是今天要介绍的 RenderThread,对于 RenderThread 可能很多人对它并不了解,接下来我将教会大家如何利用 RenderThread 实现动画的异步渲染。

2.什么是 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 时才默认开启硬件加速。


优化是无止境的,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 实现动画的异步渲染。

    3.原理探索

    经过查看官方文档,得知 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 渲染动画。

    4.代码实践

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


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


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

    https://medium.com/@workingkills/understanding-the-renderthread-4dc17bcaf979


    为 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 实现动画的异步渲染的探索和实践,文中如果不妥或有更好的分析结果,欢迎您的分享留言或指正。


    ·················END·················

    推荐阅读

    耗时2年,Android进阶三部曲第三部《Android进阶指北》出版!

    『BATcoder』做了多年安卓还没编译过源码?一个视频带你玩转!

    『BATcoder』是时候下载Android11系统源码和内核源码了!

    推荐我的技术博客

    推荐一下我的独立博客: liuwangshu.cn ,内含Android最强原创知识体系,一直在更新,欢迎体验和收藏!

    BATcoder技术群,让一部分人先进大厂

    你好,我是刘望舒,被百度百科收录的腾讯云TVP专家,著有三本技术畅销书,蝉联四届电子工业出版社年度优秀作者,谷歌开发者社区特邀讲师。


    前华为面试官,现大厂技术负责人。


    欢迎添加我的微信 henglimogan ,备注:BATcoder,加入BATcoder技术群。



    明天见(。・ω・。)

    good-icon 0
    favorite-icon 0
    收藏
    回复数量: 0
      暂无评论~~
      Ctrl+Enter