原址
传统软件的UI绘制是依靠CPU来完成的,硬件加速就是将绘制任务交由GPU来执行。Android系统负责硬件加速的模块主要是HWUI,如下图所示:
Android HWUI硬件加速的底层实现是基于OpenGL ES
接口向GPU提交指令来完成绘制的。硬件加速的优势在于:
实现的源码位于目录android/platform/framework/base/libs/hwui
在Android 5上,ThreadedRenderer的出现减轻了主线程的负担,可以更快的响应用户的操作。
下面是一段Android5硬件加速的一个crash堆栈,从这个调用关系中,我们可以大致知道从View到RenderNode的调用流程。
android.view.GLES20Canvas.nDrawRenderNode(Native Method)
android.view.GLES20Canvas.drawRenderNode(GLES20Canvas.java:233)
android.view.View.draw(View.java:15041)
android.view.ViewGroup.drawChild(ViewGroup.java:3405)
android.view.ViewGroup.dispatchDraw(ViewGroup.java:3199)
android.view.View.updateDisplayListIfDirty(View.java:14057)
android.view.View.getDisplayList(View.java:14085)
android.view.View.draw(View.java:14852)
android.view.ViewGroup.drawChild(ViewGroup.java:3405)
android.view.ViewGroup.dispatchDraw(ViewGroup.java:3199)
android.view.View.updateDisplayListIfDirty(View.java:14057)
android.view.View.getDisplayList(View.java:14085)
android.view.View.draw(View.java:14852)
android.view.ViewGroup.drawChild(ViewGroup.java:3405)
再看下nDrawRenderNode函数,在DisplayListRenderer的drawRenderNode方法里把RendeNode的DrawOp记录在了DisplayListData里。
static jint android_view_GLES20Canvas_drawRenderNode(JNIEnv* env,
jobject clazz, jlong rendererPtr, jlong renderNodePtr,
jobject dirty, jint flags) {
DisplayListRenderer* renderer = reinterpret_cast(rendererPtr);
RenderNode* renderNode = reinterpret_cast(renderNodePtr);
...
status_t status = renderer->drawRenderNode(renderNode, bounds, flags);
...
return status;
}
下图是DisplayList里记录的DrawOp:
下图是Android 5.1的OpenGL Renderer执行显示列表的流程:
OpenGLRenderer类主要负责GL API的执行以及渲染状态的设置更新。
enum OpBatchId {
kOpBatch_None = 0, // Don't batch
kOpBatch_Bitmap,
kOpBatch_Patch,
kOpBatch_AlphaVertices,
kOpBatch_Vertices,
kOpBatch_AlphaMaskTexture,
kOpBatch_Text,
kOpBatch_ColorText,
kOpBatch_Count, // Add other batch ids before this
};
如上面代码所示,绘制的批次按文本、图片资源、几何图形等进行分类,分批绘制的效果如下图所示。
那么HWUI又是如何对各个DrawOp进行合并的呢?
Android 4.4之后,HWUI模块通过Deferred Display List剔除过绘制的区域,完成DrawOp的分批和合并。
我们来看下***位于DeferredDisplayList.cpp中MergingDrawBatch类的canMergeWith方法***:
bool canMergeWith(const DrawOp* op, const DeferredDisplayState* state) {
bool isTextBatch = getBatchId() == DeferredDisplayList::kOpBatch_Text ||
getBatchId() == DeferredDisplayList::kOpBatch_ColorText;
if (!isTextBatch || op->hasTextShadow()) {
//如果不是文本或者操作包含阴影,并且和当前的渲染状态的包围盒相交,则不能合并
if (intersects(state->mBounds)) return false;
}
const DeferredDisplayState* lhs = state;
const DeferredDisplayState* rhs = mOps[0].state;
//Alpha值(透明度)不相等,同样不能合并
if (!MathUtils::areEqual(lhs->mAlpha, rhs->mAlpha)) return false;
...
// 操作绑定的着色器不一样,也不能合并
if (op->mPaint && mOps[0].op->mPaint &&
op->mPaint->getShader() != mOps[0].op->mPaint->getShader()) {
return false;
}
...
return true;
}
DrawOp合并与否取决于DrawOp对应渲染管线的状态是否一致,一般是把相同状态的DrawOp进行合并,达到减少渲染状态切换的效果。
HWUI模块中,DrawOp批次合并的实现主要在DeferredDisplayList::addDrawOp方法,如下面片段所示:
void DeferredDisplayList::addDrawOp(OpenGLRenderer& renderer, DrawOp* op) {
...
// 获取渲染状态
DeferInfo deferInfo;
op->onDefer(renderer, deferInfo, *state);
...
// 剔除过绘制批次
if (CC_LIKELY(mAvoidOverdraw) && mBatches.size() &&
state->mClipSideFlags != kClipSide_ConservativeFull &&
deferInfo.opaqueOverBounds && state->mBounds.contains(mBounds)) {
// avoid overdraw by resetting drawing state + discarding drawing ops
discardDrawingBatches(mBatches.size() - 1);
resetBatchingState();
}
...
// 将新的DrawOp合并至现有批次中
// 首先,先找到现有批次中可以合并的Batch,我们希望这个批次是最新的
DrawBatch* targetBatch = NULL;
// insertion point of a new batch, will hopefully be immediately after similar batch
// (eventually, should be similar shader)
int insertBatchIndex = mBatches.size();
if (!mBatches.isEmpty()) {
if (state->mBounds.isEmpty()) {
// don't know the bounds for op, so add to last batch and start from scratch on next op
DrawBatch* b = new DrawBatch(deferInfo);
b->add(op, state, deferInfo.opaqueOverBounds);
mBatches.add(b);
resetBatchingState();
return;
}
if (deferInfo.mergeable) {
// Try to merge with any existing batch with same mergeId.
if (mMergingBatches[deferInfo.batchId].get(deferInfo.mergeId, targetBatch)) {
if (!((MergingDrawBatch*) targetBatch)->canMergeWith(op, state)) {
targetBatch = NULL;
}
}
} else {
// join with similar, non-merging batch
targetBatch = (DrawBatch*)mBatchLookup[deferInfo.batchId];
}
if (targetBatch || deferInfo.mergeable) {
// iterate back toward target to see if anything drawn since should overlap the new op
// if no target, merging ops still interate to find similar batch to insert after
for (int i = mBatches.size() - 1; i >= mEarliestBatchIndex; i--) {
DrawBatch* overBatch = (DrawBatch*)mBatches[i];
if (overBatch == targetBatch) break;
// TODO: also consider shader shared between batch types
if (deferInfo.batchId == overBatch->getBatchId()) {
insertBatchIndex = i + 1;
if (!targetBatch) break; // found insert position, quit
}
if (overBatch->intersects(state->mBounds)) {
// NOTE: it may be possible to optimize for special cases where two operations
// of the same batch/paint could swap order, such as with a non-mergeable
// (clipped) and a mergeable text operation
targetBatch = NULL;
break;
}
}
}
}
// 现有Batch中没有找到合适的Batch,我们只能创建新的批次并插入批次列表中
if (!targetBatch) {
if (deferInfo.mergeable) {
targetBatch = new MergingDrawBatch(deferInfo,
renderer.getViewportWidth(), renderer.getViewportHeight());
mMergingBatches[deferInfo.batchId].put(deferInfo.mergeId, targetBatch);
} else {
targetBatch = new DrawBatch(deferInfo);
mBatchLookup[deferInfo.batchId] = targetBatch;
}
DEFER_LOGD("creating %singBatch %p, bid %x, at %d",
deferInfo.mergeable ? "Merg" : "Draw",
targetBatch, deferInfo.batchId, insertBatchIndex);
mBatches.insertAt(targetBatch, insertBatchIndex);
}
targetBatch->add(op, state, deferInfo.opaqueOverBounds);
}
DisplayList记录了绘制操作和状态,主线程将它们提交至OpenGL渲染器,由渲染器执行最终的DrawCall。
分批次渲染以及合并DrawOp带来的好处是显而易见的,它大幅度的减少了OpenGL渲染状态的切换的开销(由于OpenGL ES的驱动实现的复杂性,每个GL的绑定调用以及GL状态的切换都会引起GL状态的检查以及同步操作)。
AssetAtlasService对资源的加载在做了专门的管理,它为了减少图片资源上传至GPU的操作,对对各资源进行合并,打包(Atlas算法实现在graphics/java/android/graphics/Atlas.java类里)成单张纹理进行上传,通过UvMapper重新映射纹理坐标至[0,1]区间,使UI依然能够正确绘制。
由于App的进程是从Zygonte进程fork出来的,所以AssetAtlasService加载的纹理资源可以共享给App。
ATLAS的使用减少了GPU显存的消耗,以及纹理单元(Texture Unit)绑定调用(Resource Binding)的开销。
文字的渲染也是一个令人头疼的问题,APP的文字渲染不像游戏中那样的简单,通常游戏发布的时候可以预先将固定的文字转换成单张的纹理,渲染文字时直接映射纹理坐标即可。Android底层对文字字形纹理数据进行了Cache。Android使用了Skia和第三方开源字体库(FreeType)对字体进行栅格化。
如果设备支持OpenGL ES 3.0,Android会使用PixelBufferObject绑定完成字体纹理的异步上传,这样做的好处在于通过映射方式(glMapBuffer)更加高效的上传纹理数据至GPU,具体实现在libs/hwui/PixelBuffer.cpp:
GpuPixelBuffer::GpuPixelBuffer(GLenum format, uint32_t width, uint32_t height):
PixelBuffer(format, width, height), mMappedPointer(0), mCaches(Caches::getInstance()) {
glGenBuffers(1, &mBuffer);
mCaches.bindPixelBuffer(mBuffer);
glBufferData(GL_PIXEL_UNPACK_BUFFER, getSize(), NULL, GL_DYNAMIC_DRAW);
mCaches.unbindPixelBuffer();
}
void GpuPixelBuffer::upload(uint32_t x, uint32_t y, uint32_t width, uint32_t height, int offset) {
// If the buffer is not mapped, unmap() will not bind it
mCaches.bindPixelBuffer(mBuffer);
unmap();
glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, width, height, mFormat,
GL_UNSIGNED_BYTE, reinterpret_cast<void*>(offset));
}
通过PBO减少字体纹理资源上传的等待时间,提高了界面流畅度。
在我们平常的需求开发中会遇到一些场景可以通过使用硬件加速来完成,比如对复杂的布局执行Alpha渐变的动画时,我们可以直接设置父布局LayerType为
LAYER_TYPE_HARDWARE
。
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
......
public void buildLayer() {
......
final AttachInfo attachInfo = mAttachInfo;
......
switch (mLayerType) {
case LAYER_TYPE_HARDWARE:
updateDisplayListIfDirty();
if (attachInfo.mHardwareRenderer != null && mRenderNode.isValid()) {
attachInfo.mHardwareRenderer.buildLayer(mRenderNode);
}
break;
case LAYER_TYPE_SOFTWARE:
buildDrawingCache(true);
break;
}
}
......
}
当View的LayerType设置为HARDWARE时,View就会绑定到一个OpenGL ES的FrameBufferObject上去,如果要对这个View进行Animation(Alpha,Rotation,etc.),会将这个FrameBufferObject渲染至Texture(RTT,Render To Texture),然后再进行动画的渲染,但是这里要注意到内存的占用会增加。
优化的点可以从API角度和GPU硬件特性展开:
在今年的SIGGRAPH上,Google已经公开支持Vulkan,它将成为下一代Android设备上的主要图形接口