写在前面的废话

研究RenderNode的起因…

作为一个代码编辑器的开发者,我十分关注我写的View的绘制速度。
于是国庆节放假,心血来潮测试自己编辑器和EditText绘制的性能。发现一段代码,在两个View中显示出来几乎没什么区别,但是TextView的绘制比咱快多了,这是为什么呢?我尝试一段一段注释自己的除了文本以外元素的绘制代码。从屏蔽代码区划线,到屏蔽空白字符的绘制,最后甚至把代码高亮都换成了普普通通的文本绘制,但是就是比不上EditText。笔者一下子就十分纳闷了,于是开始摸TextView的绘制部分。

摸索TextView源码…

下面以Andoird Q的源码为例。
打开onDraw()方法翻了一会,看到了调用Layout绘制的代码,不过伴随着的还有mEditor:

1
2
3
4
5
6
Path highlight = getUpdatedHighlightPath();
if (mEditor != null) {
mEditor.onDraw(canvas, layout, highlight, mHighlightPaint, cursorOffsetVertical);
} else {
layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
}

mEditor是在TextView能编辑文本的时候创建的,显然我应该看Editor.java了(但是我还是先看了一下Layout,可能我是sb吧)
然后转到Editor#onDraw:
本来以为会很长却意外地短,一下子就看到再次调方法绘制:

1
2
3
4
5
6
if (mTextView.canHaveDisplayList() && canvas.isHardwareAccelerated()) {
drawHardwareAccelerated(canvas, layout, highlight, highlightPaint,
cursorOffsetVertical);
} else {
layout.draw(canvas, highlight, highlightPaint, cursorOffsetVertical);
}

HardwareAccelerated实在是一个敏感的字眼,于是又看Editor#drawHardwareAccelerated,追踪到drawHardwareAcceleratedInner:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
RenderNode blockDisplayList = mTextRenderNodes[blockIndex].renderNode;
if (mTextRenderNodes[blockIndex].needsToBeShifted || blockDisplayListIsInvalid) {
final int blockBeginLine = blockInfoIndex == 0 ?
0 : blockEndLines[blockInfoIndex - 1] + 1;
final int top = layout.getLineTop(blockBeginLine);
final int bottom = layout.getLineBottom(blockEndLine);
int left = 0;
int right = mTextView.getWidth();
if (mTextView.getHorizontallyScrolling()) {
float min = Float.MAX_VALUE;
float max = Float.MIN_VALUE;
for (int line = blockBeginLine; line <= blockEndLine; line++) {
min = Math.min(min, layout.getLineLeft(line));
max = Math.max(max, layout.getLineRight(line));
}
left = (int) min;
right = (int) (max + 0.5f);
}

// Rebuild display list if it is invalid
if (blockDisplayListIsInvalid) {
final RecordingCanvas recordingCanvas = blockDisplayList.beginRecording(
right - left, bottom - top);
try {
// drawText is always relative to TextView's origin, this translation
// brings this range of text back to the top left corner of the viewport
recordingCanvas.translate(-left, -top);
layout.drawText(recordingCanvas, blockBeginLine, blockEndLine);
mTextRenderNodes[blockIndex].isDirty = false;
// No need to untranslate, previous context is popped after
// drawDisplayList
} finally {
blockDisplayList.endRecording();
// Same as drawDisplayList below, handled by our TextView's parent
blockDisplayList.setClipToBounds(false);
}
}

// Valid display list only needs to update its drawing location.
blockDisplayList.setLeftTopRightBottom(left, top, right, bottom);
mTextRenderNodes[blockIndex].needsToBeShifted = false;
}
((RecordingCanvas) canvas).drawRenderNode(blockDisplayList);

看到DisplayList、RenderNode,我感觉大有搞头。经过一段时间的翻阅后,发现RenderNode在Android Q(API 29)上成为了公开的API,位于android.graphics下。在这之前,RenderNode是android.view下的Hidden API,而且看了一下Android L的代码和Android P的代码相差还挺大的,反射也不好反射。不过我还是尝试在Android R上使用它。

RenderNode

应该没人翻译文档吧?稍微翻译一下…

文档

RenderNode可用于构建硬件加速绘制体系。每个RenderNode既包含一个DisplayList也包含一系列影响其DisplayList如何绘制在屏幕上的属性。RenderNode在默认情况下在所有View上被内部使用,而并不是直接被使用。
RenderNode可用于将一个复杂的绘制场景分开成为更小的部分。这些部分就可以被单独地以更小地开销更新。更新这一场景地一部分只需要更新少数的RenderNode的DisplayList或者属性,而不是重绘所有。一个RenderNode只需要在它的内容被改变时重新录制它的DisplayList。RenderNode可以在不重新录制的情况下通过它的属性进行一些变换。
比如说,一个文本编辑器或许会保存每一段到它自己的RenderNode中。这样当用户插入或删除一些字符的时候,只有受影响的段落的DisplayList需要被重新录制。

硬件加速

RenderNode可以在RecordingCanvas上被绘制。这不受软件绘制支持。请确保你正在使用来绘制RenderNode的Canvas时启用硬件加速的。你可以通过Canvas.isHardwareAccelerated()来确认硬件加速是否可用。

创建RenderNode

1
2
3
4
5
6
7
8
9
RenderNode renderNode = new RenderNode("myRenderNode");
renderNode.setPosition(0, 0, 50, 50); // 设置大小为50*50
RecordingCanvas canvas = renderNode.beginRecording();
try {
// 在RenderNode的Canvas上绘制
canvas.drawRect(...);
} finally {
renderNode.endRecording();
}

在View中绘制RenderNode

1
2
3
4
5
6
7
8
9
10
protected void onDraw(Canvas canvas) {
if (canvas.isHardwareAccelerated()) {
// 检查RenderNode是否有显示列表。如果没有,先重新绘制RenderNode
if (!myRenderNode.hasDisplayList()) {
updateDisplayList(myRenderNode);
}
// 将RenderNode画到Canvas上
canvas.drawRenderNode(myRenderNode);
}
}

释放资源

这一步并不是必须的,但是如果你想要尽快释放被DisplayList占用的资源,我们推荐你这么做。最主要的是它或许包含的Bitmap。

1
2
// 舍弃DisplayList,释放持有的资源
renderNode.discardDisplayList();

属性

除此以外,RenderNode提供了许多属性,比如setScaleX(float)setTranslationX(float)。他们可以用来影响一切已经记录的绘制命令。例如,这些属性可以被用来移动大量的图像,而不需要重新通过一个一个单独调用canvas.drawBitmap()来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private void createDisplayList() {
mRenderNode = new RenderNode("MyRenderNode");
mRenderNode.setPosition(0, 0, width, height);
RecordingCanvas canvas = mRenderNode.beginRecording();
try {
for (Bitmap b : mBitmaps) {
canvas.drawBitmap(b, 0.0f, 0.0f, null);
canvas.translate(0.0f, b.getHeight());
}
} finally {
mRenderNode.endRecording();
}
}

protected void onDraw(Canvas canvas) {
if (canvas.isHardwareAccelerated())
canvas.drawRenderNode(mRenderNode);
}
}

private void moveContentBy(int x) {
// 这将移动所有记录在RenderNode中的图像向右边x像素,然后重绘这个View
// 所有记录的操作不需要被重新发行,只有onDraw()会被调用而且很快就执行完成
mRenderNode.offsetLeftAndRight(x);
invalidate();
}

注:此处笔者觉得用处不大,没翻译
A few of the properties may at first appear redundant, such as setElevation(float) and setTranslationZ(float). The reason for these duplicates are to allow for a separation between static & transient usages. For example consider a button that raises from 2dp to 8dp when pressed. To achieve that an application may decide to setElevation(2dip), and then on press to animate setTranslationZ to 6dip. Combined this achieves the final desired 8dip value, but the animation need only concern itself with animating the lift from press without needing to know the initial starting value. setTranslationX(float) and setTranslationY(float) are similarly provided for animation uses despite the functional overlap with setPosition(Rect).
The RenderNode’s transform matrix is computed at render time as follows:

1
2
3
4
5
6
Matrix transform = new Matrix();
transform.setTranslate(renderNode.getTranslationX(), renderNode.getTranslationY());
transform.preRotate(renderNode.getRotationZ(),
renderNode.getPivotX(), renderNode.getPivotY());
transform.preScale(renderNode.getScaleX(), renderNode.getScaleY(),
renderNode.getPivotX(), renderNode.getPivotY());

The current canvas transform matrix, which is translated to the RenderNode’s position, is then multiplied by the RenderNode’s transform matrix. Therefore the ordering of calling property setters does not affect the result. That is to say that:

1
2
renderNode.setTranslationX(100);
renderNode.setScaleX(100);

is equivalent to:

1
2
renderNode.setScaleX(100);
renderNode.setTranslationX(100);

线程调度

RenderNode可以在任何线程被创建并使用,但它不是线程安全的。只有一个线程可以在某一时间与RenderNode交互。建议RenderNode只在相同的线程被使用,即它将要被绘制的线程。比如自定义View使用RenderNode时,只在UI线程使用RenderNode。
RenderNode的许多方法返回一个布尔值来指示它是否需要被重绘。典型的比如:

1
2
3
4
5
6
7
public void translateTo(int x, int y) {
boolean needsUpdate = myRenderNode.setTranslationX(x);
needsUpdate |= myRenderNode.setTranslationY(y);
if (needsUpdate) {
myOwningView.invalidate();
}
}

这会比显式地通过getTransationX()比较检查快,因为这最小化了移动到地JNI开销。是否需要重绘取决于这个RenderNode如何被绘制。如果它被绘制到View上,那么只需要重绘View就行了。如果它被绘制到一个直接使用Surface.lockHardwareCanvas()的Canvas上,那么它需要被重绘,通过调用Surface.lockHardwareCanvas(),绘制根RenderNode或者其它被顶级内容需要的,并调用Surface.unlockCanvasAndPost(canvas)

几个提示

构造器

构造器的name可以随意,null也可以。文档说是调试用的。

绘制

绘制到View的Canvas上之前,一定要记得检查这个Canvas是否开启硬件加速,如果没有开启,那么RenderNode无法被绘制并且会抛出UnsupportedOperationException。
同时也要检查RenderNode是否还有DisplayList,通过RenderNode.hasDisplayList()。如果返回false,需要重新录制在RenderNode上的绘制操作,否则绘制出的RenderNode是空的。
RenderNode似乎如果在上一次绘制操作中没有被使用,其DisplayList就会被回收,需要重新录制操作。笔者因为不知道这个傻傻地调了一个小时,最后才发现是没有DisplayList。

实例

HwAcceleratedRenderer.java