Android渲染流程分析

在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;
    }
}
  1. updateDisplayListIfDirty在满足以下三种情况之一时进行才会进行update,否则跳过update:
  • mPrivateFlags未设置PFLAG_DRAWING_CACHE_VALID
  • mPrivateFlags有PFLAG_INVALIDATED
  • renderNode.isValid()为false 的情况下(未绘制过、DetachedFromWindow)
  1. 如果RenderNode是有效的且mRecreateDisplayList为false,则表明其自身UI未发生变换,通过调用dispatchGetDisplayList()通知子View进行updateDisplayListIfDirty
  2. 在RenderNode.start()中调用GLES20RecordingCanvas.obtain(this)获取HardwareCanvas对象,在GLES20Canvas中对Canvas的drawXXX方法进行重写
  1. 对于LAYER_TYPE_SOFTWARE的View,创建Bitmap、Canvas(非HardwareCanvas,保存在ViewRootImpl的mAttachInfo中,实现复用)对象,调用draw(canvas)将UI通过CPU绘制在Bitmap上,最终调用hardwareCanvas的drawBitmap
  2. 对于硬件加速的View:如果View没有background且setWillNotDraw为true表明View自身不需要绘制,调用dispatchDraw(),不会执行onDraw()。
  3. renderNode.end()方法将canvas中的DisplayList保存在RenderNode中以更新渲染命令
  4. 在第一次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

参考资料

Hardware acceleration

How Android renders (Google I/O '18)

Android Performance Patterns: Android UI and the GPU

Android应用程序UI硬件加速渲染的Display List构建过程分析

Android应用程序UI硬件加速渲染的Display List渲染过程分析

相关推荐
Dnelic-2 小时前
【单元测试】【Android】JUnit 4 和 JUnit 5 的差异记录
android·junit·单元测试·android studio·自学笔记
Eastsea.Chen4 小时前
MTK Android12 user版本MtkLogger
android·framework
长亭外的少年11 小时前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
建群新人小猿14 小时前
会员等级经验问题
android·开发语言·前端·javascript·php
1024小神15 小时前
tauri2.0版本开发苹果ios和安卓android应用,环境搭建和最后编译为apk
android·ios·tauri
兰琛15 小时前
20241121 android中树结构列表(使用recyclerView实现)
android·gitee
Y多了个想法16 小时前
RK3568 android11 适配敦泰触摸屏 FocalTech-ft5526
android·rk3568·触摸屏·tp·敦泰·focaltech·ft5526
NotesChapter17 小时前
Android吸顶效果,并有着ViewPager左右切换
android
_祝你今天愉快18 小时前
分析android :The binary version of its metadata is 1.8.0, expected version is 1.5.
android
暮志未晚Webgl18 小时前
109. UE5 GAS RPG 实现检查点的存档功能
android·java·ue5