在Android应用的开发中,我们经常会接触到invalidate()方法。由于Android对底层软硬件封装非常完善,我们只知道会调用到onDraw(),不清楚底层原理的意义。本文基于Android 5.1尝试从上层代码到底层硬件对invalidate流程进行简单介绍,将涉及到硬件加速、脏区计算、DisplayList构建与渲染、OpenGL渲染命令执行。
介绍
1. CPU、GPU与页面渲染
- CPU用于执行程序代码,主要负责逻辑控制,GPU主要用于处理图形运算。相比于CPU来说,GPU使用了并行设计,且拥有更多的算数逻辑单元
- 页面渲染是指将高级对象描述的元素(如:圆形、矩形、文本、图片等)转换像素点,在这个过程中包含大量的浮点数运算
2. 软件绘制、硬件加速
- 在Android4.0以前,系统还未对View绘制过程引入硬件加速,在这个方式具有以下两个缺点:1.脏区中的View都onDraw(),额外执行很多代码 2.主线程执行绘制工作,影响性能
- 在引入硬件加速后,应用程序仍然通过调用invalidate()去重新渲染View,但是与软件绘制不同的是CPU只记录绘制命令,GPU将这些命令进行渲染
流程分析
1. 构建脏区
java
public void invalidate() {
invalidate(true);
}
void invalidate(boolean invalidateCache) {
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
if (skipInvalidate()) {
return;
}
if (...) {
...
mPrivateFlags |= PFLAG_DIRTY;
if (invalidateCache) {
mPrivateFlags |= PFLAG_INVALIDATED;
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
}
final ViewParent p = mParent;
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
damage.set(l, t, r, b);
p.invalidateChild(this, damage);
}
}
}
在invalidateInternal中设置脏区为(0, 0, width, height),对该View取消PFLAG_DRAWN、PFLAG_DRAWING_CACHE_VALID,添加 PFLAG_INVALIDATED
ini
public final void invalidateChild(View child, final Rect dirty) {
ViewParent parent = this;
if (attachInfo != null) {
...
final int[] location = attachInfo.mInvalidateChildLocation;
location[CHILD_LEFT_INDEX] = child.mLeft;
location[CHILD_TOP_INDEX] = child.mTop;
...
do {
View view = null;
if (parent instanceof View) {
view = (View) parent;
}
...
parent = parent.invalidateChildInParent(location, dirty);
...
} while (parent != null);
}
}
在parent不为空的情况下一直调用invalidateChildInParent
ini
public ViewParent invalidateChildInParent(final int[] location, final Rect dirty{
if ((mGroupFlags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE)) !=
FLAG_OPTIMIZE_INVALIDATE) {
dirty.offset(location[CHILD_LEFT_INDEX] - mScrollX,
location[CHILD_TOP_INDEX] - mScrollY);
if ((mGroupFlags & FLAG_CLIP_CHILDREN) == 0) {
dirty.union(0, 0, mRight - mLeft, mBottom - mTop);
}
if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {
if (!dirty.intersect(0, 0, mRight - left, mBottom - top)) {
dirty.setEmpty();
}
}
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
location[CHILD_LEFT_INDEX] = left;
location[CHILD_TOP_INDEX] = top;
return mParent;
}
return null;
}
- 对脏区位置进行修正,即对Left和Right分别加上子View相对于父View的Left,对Top、Bottom分别加上子View相对于父View的Top。
- 如果父View设置FLAG_CLIP_CHILDREN,则将修正后的脏区限制在父View的区域内;如果没有设置,则将脏区与父View区域求并集
- 对父View取消PFLAG_DRAWING_CACHE_VALID
接着看一下ViewRootImpl的invalidateChildInParent,这个方法对传递上来的脏区与已有的mDirty区域求并集。
java
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
if (dirty == null) {
invalidate();
return null;
} else if (dirty.isEmpty() && !mIsAnimating) {
return null;
}
final Rect localDirty = mDirty;
localDirty.union(dirty.left, dirty.top, dirty.right, dirty.bottom);
if (!mWillDrawSoon && (intersected || mIsAnimating)) {
scheduleTraversals();
}
return null;
}
2. 注册帧信号
scheduleTraversals方法中通过Choreographer向底层注册了一次帧信号的回调,在收到VSYNC信号后开始执行performTraversals 。这个过程可以参考Animation动画绘制流程
3. 遍历View树
csharp
private void performDraw() {
...
try {
draw(fullRedrawNeeded);
} finally {
mIsDrawing = false;
}
}
typescript
private void draw(boolean fullRedrawNeeded) {
if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) {
...
dirty.setEmpty();
mAttachInfo.mHardwareRenderer.draw(mView, mAttachInfo, this);
} else {
...
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
}
}
}
在这里将软件绘制、硬件加速进行区分,可通过android:hardwareAccelerated="true"进行设置
scss
void draw(View view, AttachInfo attachInfo, HardwareDrawCallbacks callbacks) {
...
updateRootDisplayList(view, callbacks);
int syncResult = nSyncAndDrawFrame(mNativeProxy, frameTimeNanos,
recordDuration, view.getResources().getDisplayMetrics().density);
...
}
scss
private void updateRootDisplayList(View view, HardwareDrawCallbacks callbacks) {
updateViewTreeDisplayList(view);
if (mRootNodeNeedsUpdate || !mRootNode.isValid()) {
HardwareCanvas canvas = mRootNode.start(mSurfaceWidth, mSurfaceHeight);
try {
...
canvas.drawRenderNode(view.getDisplayList());
...
} finally {
mRootNode.end(canvas);
}
}
}
在该方法中,先更新DecorView的DisplayList。获取HardwareCanvas并将DecorView的DisplayList放入Canvas中(通过添加一个DrawRenderNodeOp的方式),最后调用mRootNode(这个RenderNode对应ViewRootImpl)的end方法,将canvas中的所有DisplayList放入mRootNode。
4. 构建DisplayList
ini
private void updateViewTreeDisplayList(View view) {
view.mPrivateFlags |= View.PFLAG_DRAWN;
view.mRecreateDisplayList = (view.mPrivateFlags & View.PFLAG_INVALIDATED)== View.PFLAG_INVALIDATED;
view.mPrivateFlags &= ~View.PFLAG_INVALIDATED;
view.updateDisplayListIfDirty();
view.mRecreateDisplayList = false;
}
scss
private void updateDisplayListIfDirty() {
if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0
|| !renderNode.isValid()
|| (mRecreateDisplayList)) {
if (renderNode.isValid()
&& !mRecreateDisplayList) {
mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
dispatchGetDisplayList();
return; // no work needed
}
...
final HardwareCanvas canvas = renderNode.start(width, height);
try {
final HardwareLayer layer = getHardwareLayer();
if (layer != null && layer.isValid()) {
canvas.drawHardwareLayer(layer, 0, 0, mLayerPaint);
} else if (layerType == LAYER_TYPE_SOFTWARE) {
buildDrawingCache(true);
Bitmap cache = getDrawingCache(true);
if (cache != null) {
canvas.drawBitmap(cache, 0, 0, mLayerPaint);
}
} else {
mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
dispatchDraw(canvas);
} else {
draw(canvas);
}
}
} finally {
renderNode.end(canvas);
setDisplayListProperties(renderNode);
}
} else {
mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
}
}
- updateDisplayListIfDirty在满足以下三种情况之一时进行才会进行update,否则跳过update:
- mPrivateFlags未设置PFLAG_DRAWING_CACHE_VALID
- mPrivateFlags有PFLAG_INVALIDATED
- renderNode.isValid()为false 的情况下(未绘制过、DetachedFromWindow)
- 如果RenderNode是有效的且mRecreateDisplayList为false,则表明其自身UI未发生变换,通过调用dispatchGetDisplayList()通知子View进行updateDisplayListIfDirty
- 在RenderNode.start()中调用GLES20RecordingCanvas.obtain(this)获取HardwareCanvas对象,在GLES20Canvas中对Canvas的drawXXX方法进行重写
- 对于LAYER_TYPE_SOFTWARE的View,创建Bitmap、Canvas(非HardwareCanvas,保存在ViewRootImpl的mAttachInfo中,实现复用)对象,调用draw(canvas)将UI通过CPU绘制在Bitmap上,最终调用hardwareCanvas的drawBitmap
- 对于硬件加速的View:如果View没有background且setWillNotDraw为true表明View自身不需要绘制,调用dispatchDraw(),不会执行onDraw()。
- renderNode.end()方法将canvas中的DisplayList保存在RenderNode中以更新渲染命令
- 在第一次draw时Canvas会调用drawRenderNode,将子View的DisplayList保存在父View中,因此RootRenderNode中保存了所有View绘制的DisplayList}
5. 渲染DisplayList
在Android5.0以后的设备上,系统引入了RenderThread减少UIThread的工作量。UIThread负责测量、布局、绘制,同步DisplayList给RenderThread并通知RenderThread开始渲染这一帧。
scss
int RenderProxy::syncAndDrawFrame() {
return mDrawFrameTask.drawFrame();
}
int DrawFrameTask::drawFrame() {
...
postAndWait();
...
}
void DrawFrameTask::postAndWait() {
AutoMutex _lock(mLock);
mRenderThread->queue().post([this]() { run(); });
mSignal.wait(mLock);
}
通过RenderProxy向RenderThread的任务队列中放入DrawFrameTask,并阻塞UIThread
scss
void DrawFrameTask::run() {
ATRACE_NAME("DrawFrame");
...
canUnblockUiThread = syncFrameState(info);
...
if (canUnblockUiThread) {
unblockUiThread();
}
...
context->draw();
...
}
syncFrameState将同步绘制命令到RenderThread中,再调用CanvasContext的draw()方法去绘制这一帧。
scss
void RenderNode::prepareTreeImpl(TreeObserver& observer, TreeInfo& info, bool functorsNeedLayer) {
info.damageAccumulator->pushTransform(this);
...
if (info.mode == TreeInfo::MODE_FULL) {
pushStagingPropertiesChanges(info);
}
...
if (info.mode == TreeInfo::MODE_FULL) {
pushStagingDisplayListChanges(observer, info);
}
if (mDisplayList) {
info.out.hasFunctors |= mDisplayList->hasFunctor();
bool isDirty = mDisplayList->prepareListAndChildren(
observer, info, childFunctorsNeedLayer,
[](RenderNode* child, TreeObserver& observer, TreeInfo& info,
bool functorsNeedLayer) {
child->prepareTreeImpl(observer, info, functorsNeedLayer);
});
if (isDirty) {
damageSelf(info);
}
}
...
info.damageAccumulator->popTransform();
}
- pushStagingPropertiesChanges将UIThread修改的mStagingProperties同步到mProperties
- pushStagingDisplayListChanges将UIThread修改的mStagingDisplayList同步到mDisplayList
- Native通过damageAccumulator记录当前处理的RenderNode,在damgeSelf中设置脏区为该View的left、top、right、bottom,在popTransform中调用join对脏区进行合并。
硬件加速渲染Case分析
布局如图,在View 1的OnClickListener中调用View1与View2的invalidate()方法:
- 硬件加速:只有View 1、View 2执行onDraw方法,GPU绘制区域显示为View 1与View 2的并集(在clipChildren为false时,GPU绘制区域显示为整个父View),关于GPU的工作可以看Doc版
- 非硬件加速:View 1、View 2、View 3均执行onDraw方法,GPU绘制区域为空
QA
1. 脏区的作用?
脏区这个概念在软件绘制中已经存,它的作用就是实现局部重绘
2. 为什么要在Native中重新计算脏区?
脏区会受到Scale、Translate等动画的影响,Java层计算的脏区不包括属性动画影响Matrix的脏区
3. setLayerType的作用?
在硬件加速中,CPU负责把View中的元素计算成纹理交给GPU进行栅格化,OpenGL ES可以把这些纹理保存在GPU Memory里面(通常包括图片、文字等),在下次需要渲染的时候直接从Memory进行获取渲染。设置LAYER_TYPE_HARDWARE 将会把该View在GPU中存储为纹理,通常在动画开始时设置为LAYER_TYPE_HARDWARE,结束时设置为LAYER_TYPE_NONE
4. 硬件加速的特点?
- GPU更适合并行计算,绘制效率高
- GPU中存储了一些纹理
- 5.0以后RenderThread减少UIThread工作量
- 部分API不兼容
5. GPU渲染顺序为什么和onDraw中代码不一致?
GPU会进行指令重排序,达到资源复用
总结
绘制场景 | 硬件加速 | 软件绘制 |
---|---|---|
页面首次加载 | UIThread创建DisplayList,RenderThread执行绘制命令,GPU渲染整个页面 | CPU渲染所有View |
执行Scale、Translate等属性动画 | 直接修改该RenderNode的属性,无需遍历 | CPU重绘脏区所有View |
invalidate、Animation动画 | UIThread更新此View的DisplayList(该View及每一级父View更新DisplayList),GPU渲染脏区 | CPU重绘脏区所有View |
参考资料
How Android renders (Google I/O '18)
Android Performance Patterns: Android UI and the GPU