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渲染过程分析

相关推荐
REDcker33 分钟前
Android WebView 版本升级方案详解
android·音视频·实时音视频·webview·js·编解码
麦兜*35 分钟前
【springboot】图文详解Spring Boot自动配置原理:为什么@SpringBootApplication是核心?
android·java·spring boot·spring·spring cloud·tomcat
le1616161 小时前
Android 依赖种类及区别:远程仓库依赖、打包依赖、模块依赖、本地仓库依赖
android
lxysbly1 小时前
psp模拟器安卓版带金手指
android
云上凯歌2 小时前
02 Spring Boot企业级配置详解
android·spring boot·后端
hqiangtai2 小时前
Android 高级专家技术能力图谱
android·职场和发展
aqi002 小时前
FFmpeg开发笔记(九十七)国产的开源视频剪辑工具AndroidVideoEditor
android·ffmpeg·音视频·直播·流媒体
stevenzqzq2 小时前
Android Koin 注入入门教程
android·kotlin
炼金术3 小时前
SkyPlayer v1.1.0 - 在线视频播放功能更新
android·ffmpeg
用户276038157813 小时前
鲲鹏+昇腾:开启 AI for Science 新范式——基于PINN的流体仿真加速实践
android