最近一年多时间里,在某k12在线教育培训机构工作,期间参与了集团超级板书的开发, 其实核心就是 传输获取不同的点,绘制到canvas上. 中间也碰到了一些困惑和优化上的难题,听了组内一些在这方面比较有经验的大佬的分享. 但所了解都非常零碎,不成体系. 所以也暗下决心, 计划花一定时间去整理一下这些,其中首当其冲的就是Android view层硬件加速的全流程,利用周末记录一下流程(本人水平有限,如有错误之处,请不吝指正):
硬件加速绘制和软件绘制的区别
首先,需要先了解一下Android 硬件加速绘制和软件绘制的区别:
硬件加速绘制: 是先使用cpu去主线程去遍历RenderNode树, 通过canvas.drawRenderNode(). 把将要绘制的信息写入c++层的SkdisplayList里的DisplayListData中 , 然后去请求RenderThread去执行将所有RenderNode绘制的流程.
软件绘制: 是直接在surface上lock一个canvas,然后去遍历view树的脏区域,使用canvas绘制内容,整个流程是在MainThread中通过cpu实现的.
硬件加速涉及到的核心类的结构
因为流程较长,涉及到核心类比较多,所以前面先简单罗列一下各个类,混个脸熟:
java层:
RecordingCanvas.java 伪代码
RecordingCanvas extends DisplayListCanvas extends BaseRecordingCanvas extends BaseRecordingCanvas extends Canvas {
.....
}
RenderNode.java 伪代码
RenderNode {
RecordingCanvas mCurrentRecordingCanvas;
....
}
可以看出: RecordingCanvas 和 RenderNode在java层都没有很多属性,大部分属性都定义在native层
native层:
SkiaRecordingCanvas.cpp
SkiaRecordingCanvas.cpp : SkiaCanvas : Canvas {
//native层,保存renderNode信息的核心类
RecordingCanvas mRecorder;
//renderNode的绘制信息,及renderNode树保存数据结构的指针
std::unique_ptr<SkiaDisplayList> mDisplayList;
//记录view层级的屏障
StartReorderBarrierDrawable* mCurrentBarrier;
....
}
RecordingCanvas.cpp
RecordingCanvas.cpp : SkCanvasVirtualEnforcer<SkNoDrawCanvas> {
DisplayListData* fDL;
.....
}
SkiaDisplayList.cpp, RenderNodeDrawable.cpp
SkiaDisplayList {
DisplayListData mDisplayList
//是一个双向队列,用来记录,子节点renderNode
std::deque<RenderNodeDrawable> mChildNodes;
.....
}
//记录renderNode的子节点
RenderNodeDrawable {
//每个节点的指针
sp<RenderNode> mRenderNode;
...
}
ReorderBarrierDrawable.cpp
每一层ViewGroup,开始时中插入的屏障对象
StartReorderBarrierDrawable: SkDrawable {
int mEndChildIndex;
int mBeginChildIndex;
FatVector<RenderNodeDrawable*, 16> mChildren;
SkiaDisplayList* mDisplayList;
...
}
每一层ViewGroup,结束时中插入的屏障对象
EndReorderBarrierDrawable : SkDrawable {
...
}
RenderNode.cpp, DisplayList.h, android_view_ThreadedRenderer.cpp
SkiaDisplayList {
//是指RenderNode的宽高等信息的存储对象
RenderProperties mProperties
//就是SkiaDisplayList
DisplayList* mDisplayList
….
}
DisplayList.h {
DisplayList = SkiaDisplayList
....
}
/**
RootRenderNode 只能通过ThreadedRender.java 构造方法中的
mRootNode = RenderNode.*adopt*(*nCreateRootRenderNode*());创建出来,
类似View中根节点decorView的概念.但是需要注意 decorView并不是一个RootRenderNode,
而是一个普通的RenderNode
**/
RootRenderNode : RenderNode, ErrorHandler
}
代码执行流程
说完了各个类的概念,我们要开始看一下RenderNode是如何生成DisplayListData中的数据的过程了. 首先, viewRootImpl里面的触发绘制的逻辑,我们就不捋了, 直接开始从硬件绘制流程的起点开始:ThreadedRenderer中的draw()
/**
* Draws the specified view.
*
* @param view The view to draw.
* @param attachInfo AttachInfo tied to the specified view.
*/
void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks) {
final Choreographer choreographer = attachInfo.mViewRootImpl.mChoreographer;
//给ChoreGrapher设置一个开始绘制的标记
choreographer.mFrameInfo.markDrawStart();
//调用了mDecorView的 updateDisplayListIfDirty(),
//如果view树中存在dirty区域,则创建新的renderNode,并保存,此过程在主线程中执行
updateRootDisplayList(view, callbacks);
//处理正在执行动画的RenderNode
// register animating rendernodes which started animating prior to renderer
// creation, which is typical for animators started prior to first draw
if (attachInfo.mPendingAnimatingRenderNodes != null) {
final int count = attachInfo.mPendingAnimatingRenderNodes.size();
for (int i = 0; i < count; i++) {
registerAnimatingRenderNode(
attachInfo.mPendingAnimatingRenderNodes.get(i));
}
attachInfo.mPendingAnimatingRenderNodes.clear();
// We don't need this anymore as subsequent calls to
// ViewRootImpl#attachRenderNodeAnimator will go directly to us.
attachInfo.mPendingAnimatingRenderNodes = null;
}
//请求绘制renderNode,此过程在RenderThread中执行
int syncResult = syncAndDrawFrame(choreographer.mFrameInfo);
//如果绘制结果返回SYNC_LOST_SURFACE_REWARD_IF_FOUND标志位,说明可能Surface无效或者出错,则关闭硬件渲染,再次请求使用cpu方式绘制。
if ((syncResult & SYNC_LOST_SURFACE_REWARD_IF_FOUND) != 0) {
setEnabled(false);
attachInfo.mViewRootImpl.mSurface.release();
// Invalidate since we failed to draw. This should fetch a Surface
// if it is still needed or do nothing if we are no longer drawing
attachInfo.mViewRootImpl.invalidate();
}
// 如果SYNC_INVALIDATE_REQUIRED返回了,说明还需要再刷新一次
if ((syncResult & SYNC_REDRAW_REQUESTED) != 0) {
attachInfo.mViewRootImpl.invalidate();
}
}
可以看出比较核心的方法有两个:
1,updateRootDisplayList(view, callbacks);
2,syncAndDrawFrame(choreographer.mFrameInfo);
构建RenderNode树
我们来看一下updateRootDisplayList(view, callbacks)方法 `
private void updateRootDisplayList(View view, DrawCallbacks callbacks) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Record View#draw()");
//从decorView开始请求构建RenderNode结构
updateViewTreeDisplayList(view);
// Consume and set the frame callback after we dispatch draw to the view above, but before
// onPostDraw below which may reset the callback for the next frame. This ensures that
// updates to the frame callback during scroll handling will also apply in this frame.
if (mNextRtFrameCallbacks != null) {
final ArrayList<FrameDrawingCallback> frameCallbacks = mNextRtFrameCallbacks;
mNextRtFrameCallbacks = null;
setFrameCallback(frame -> {
for (int i = 0; i < frameCallbacks.size(); ++i) {
frameCallbacks.get(i).onFrameDraw(frame);
}
});
}
if (mRootNodeNeedsUpdate || !mRootNode.hasDisplayList()) {
//通过根节点,RootNode来生成一个RecordingCanvas,来进行renderNode中每次绘制的记录
RecordingCanvas canvas = mRootNode.beginRecording(mSurfaceWidth, mSurfaceHeight);
try {
final int saveCount = canvas.save();
canvas.translate(mInsetLeft, mInsetTop);
callbacks.onPreDraw(canvas);
canvas.enableZ();
canvas.drawRenderNode(view.updateDisplayListIfDirty());
canvas.disableZ();
callbacks.onPostDraw(canvas);
canvas.restoreToCount(saveCount);
mRootNodeNeedsUpdate = false;
} finally {
mRootNode.endRecording();
}
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
附一个生成ReenderNode数据全流程图
可以看出,每个View都有自己的RenderNode, 当自己被标记为dirty需要绘制时,会根据自己的RenderNode,创建一个RecordingCanvas, 然后通过这个canvas执行onDraw()操作,最后保存在SKiaRecordCanvas的SKiaDisplayList
1, 具体onDraw()里面的每次操作是怎么保存起来对应到native层的呢?
1,RenderNode在beginRecording()时,会从一个长度为25的pool里先尝试取一个RecordingCanvas,去到就reset()一下,取不到就创建,创建流程如上.所以每次onDraw里的操作其实都是保存在了这一级RenderNode对应创建的DisplayListData里.
2,创建java层的RecordingCanvas, 会在native层创建一个SkiaRecordingCanvas. SkiaRecordingCanvas中有一个成员变量 RecordingCanvas.cpp对象,这个对象又继承了SkNoDrawCanvas.
3,SkiaRecordingCanvas在构造方法会调用 initDisplayList(),然后会将RecordingCanvas:SkNoDrawCanvas传递过去.这样硬件加速下每次小的操作比较DrawText()等于是在SkNoDrawCanvas上执行的.流程如下
4,根据3可知, View层onDraw中自定义的draw操作其实每次都会执行到RecordingCanvas.cpp中,并调用onDrawXXX()系列方法.这些最终都会调用DisPlayListData中的drawXXX(),并调用push方法通过 fBytes保存起来.
2,View树上的RenderNode是怎么建立关系的呢?
1,view层draw方法 在调用完 onDraw()后,会使用DecorView的Canvas执行drawRenderNode操作.执行完成之后,子RenderNode就和DecorView建立起来联系了
`
void SkiaRecordingCanvas::drawRenderNode(uirenderer::RenderNode* renderNode) {
// Record the child node. Drawable dtor will be invoked when mChildNodes deque is cleared.
//将子View的RenderNode放入双向队列,队列需要传递的是一个RenderNodeDrawable,这个是怎么生成相应对象的?
mDisplayList->mChildNodes.emplace_back(renderNode, asSkCanvas(), true, mCurrentBarrier);
//拿到刚才加入的RenderNodeDrawable
auto& renderNodeDrawable = mDisplayList->mChildNodes.back();
if (Properties::getRenderPipelineType() == RenderPipelineType::SkiaVulkan) {
// Put Vulkan WebViews with non-rectangular clips in a HW layer
renderNode->mutateStagingProperties().setClipMayBeComplex(mRecorder.isClipMayBeComplex());
}
//RenderNodeDrawable是继承自SkDrawable的,所以可以直接使用SkCanvas去添加,最终就保存在了当前SkiaRecordingCanvas中的SkiaDisplayList中
drawDrawable(&renderNodeDrawable);
// use staging property, since recording on UI thread
if (renderNode->stagingProperties().isProjectionReceiver()) {
mDisplayList->mProjectionReceiver = &renderNodeDrawable;
}
}
3,RenderNode既然是保存在SkiaDisplayList中的,哪它是怎么保存我们view的层级结构的呢?
1,RecordingCanvas,java 中有一个方法nInsertReorderBarrier(),每次ViewGroup,draw()开始都会调用这个方法insertReorderBarrier(true), 在SkiaRecordingCanvas.finishRecording()中,也会调一下这个方法的结束insertReorderBarrier(false).
void SkiaRecordingCanvas::insertReorderBarrier(bool enableReorder) {
if (mCurrentBarrier && enableReorder) {
// Already in a re-order section, nothing to do
return;
}
if (nullptr != mCurrentBarrier) {
// finish off the existing chunk
//需要关闭,就会向SkiaDisplayList中加入一个继承自SkDrawable的EndReorderBarrierDrawable
SkDrawable* drawable =
mDisplayList->allocateDrawable<EndReorderBarrierDrawable>(mCurrentBarrier);
mCurrentBarrier = nullptr;
drawDrawable(drawable);
}
//需要开启,就会向SkiaDisplayList中加入一个继承自SkDrawable的StartReorderBarrierDrawable
if (enableReorder) {
mCurrentBarrier =
mDisplayList->allocateDrawable<StartReorderBarrierDrawable>(mDisplayList.get());
drawDrawable(mCurrentBarrier);
}
}
绘制流程
1,接下来,终于到了绘制流程了
1, syncAndDrawFrame(choreographer.mFrameInfo)方法执行;首先借用大佬们一张图.
,里面又涉及到了一些新的类:
RenderThread 硬件渲染线程,所有的渲染任务都会在该线程中使用硬件渲染线程的Looper进行。 RenderThread里面的threadloop,这位大佬重点分析了RenderThread,基于Android9的, Android10在这块目前没发现有太大的改动. 有兴趣可以去看一下 www.jianshu.com/p/c84bfa909… `
bool RenderThread::threadLoop() {
setpriority(PRIO_PROCESS, 0, PRIORITY_DISPLAY);
Looper::setForThread(mLooper);
if (gOnStartHook) {
gOnStartHook("RenderThread");
}
initThreadLocals();
while (true) {
waitForWork();
processQueue();
if (mPendingRegistrationFrameCallbacks.size() && !mFrameCallbackTaskPending) {
drainDisplayEventQueue();
mFrameCallbacks.insert(mPendingRegistrationFrameCallbacks.begin(),
mPendingRegistrationFrameCallbacks.end());
mPendingRegistrationFrameCallbacks.clear();
requestVsync();
}
if (!mFrameCallbackTaskPending && !mVsyncRequested && mFrameCallbacks.size()) {
// TODO: Clean this up. This is working around an issue where a combination
// of bad timing and slow drawing can result in dropping a stale vsync
// on the floor (correct!) but fails to schedule to listen for the
// next vsync (oops), so none of the callbacks are run.
requestVsync();
}
}
return false;
}
` 2,绘制执行流程
先上一张流程图
看一下DrawFrameTask的run()方法
void DrawFrameTask::run() {
ATRACE_NAME("DrawFrame");
bool canUnblockUiThread;
bool canDrawThisFrame;
{
TreeInfo info(TreeInfo::MODE_FULL, *mContext);
canUnblockUiThread = syncFrameState(info);
canDrawThisFrame = info.out.canDrawThisFrame;
if (mFrameCompleteCallback) {
mContext->addFrameCompleteListener(std::move(mFrameCompleteCallback));
mFrameCompleteCallback = nullptr;
}
}
// Grab a copy of everything we need
CanvasContext* context = mContext;
std::function<void(int64_t)> callback = std::move(mFrameCallback);
mFrameCallback = nullptr;
// From this point on anything in "this" is *UNSAFE TO ACCESS*
if (canUnblockUiThread) {
unblockUiThread();
}
// Even if we aren't drawing this vsync pulse the next frame number will still be accurate
if (CC_UNLIKELY(callback)) {
context->enqueueFrameWork(
[callback, frameNr = context->getFrameNumber()]() { callback(frameNr); });
}
if (CC_LIKELY(canDrawThisFrame)) {
context->draw();
} else {
// wait on fences so tasks don't overlap next frame
context->waitOnFences();
}
if (!canUnblockUiThread) {
unblockUiThread();
}
}