动画窗口机制系列Android 高质量开发 — UI 优化

RenderThread:实现动画的异步渲染

2019-12-07  本文已影响0人  godliness

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


UI 优化系列专题

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

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 结构设计如下:

所以从 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 渲染框架的知识,感兴趣的朋友可以参考如下资料:


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

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


其他系列专题

上一篇下一篇

猜你喜欢

热点阅读