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 之 Choreographer 详细分析
- Android 之 Project Butter 详细介绍
经过 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 的文档介绍真的是少之又少)。
- ViewPreportyAnimator
- 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 (renderNodePtr);
RenderPropertyAnimator* animator = reinterpret_cast (animatorPtr);
renderNode -> addanimator(animator);
}
void RenderNode :: addAnimator (const sp& 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 渲染框架的知识,感兴趣的朋友可以参考如下资料:
- Android 之如何优化 UI 渲染(上)
- Android 之如何优化 UI 渲染(下)
- Android 之理解 VSYNC 信号
- Android 之 ViewTreeObserver 全面解析
- 深入 Activity 三部曲(3)之 View 绘制流程
最后,如果你有更好的分析结果或实践方案,欢迎您的分享留言或指正。
文章如果对你有帮助,请留个赞吧!
其他系列专题
- Android 存储优化系列专题
- Android 对象序列化之追求完美的 Serial
- Android 之不要滥用 SharedPreferences(上)
- Android 存储选项之 SQLite 优化那些事儿
- Android 对象序列化之你不知道的 Serializable