preface
The Draw process involves a lot of knowledge and is divided into three parts
- Draw process of Android custom View (Part 1)
- Draw ing process of Android custom View (in progress)
- Draw process of Android custom View (Part 2)
The first part analyzes the relevant knowledge of hardware acceleration:
Draw ing process of Android custom View (in progress)
This article will deeply analyze hardware accelerated rendering and software rendering from the perspective of code.
Through this article, you will learn:
1. Software drawing process
2. Hardware accelerated rendering process
2. Influence of LayerType on painting
3. Where did Canvas come from
4. Drawing process family photo
Software drawing process
As mentioned in the previous article, in viewrootimpl - > Draw (XX), software rendering and hardware accelerated rendering go their separate ways:
The above figure is the entrance for Window to distinguish hardware accelerated rendering from software rendering.
From easy to difficult, let's take a look at the software drawing process.
drawSoftware(xx)
#ViewRootImpl.java private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, boolean scalingRequired, Rect dirty, Rect surfaceInsets) { //Hold canvas final Canvas canvas; ... try { ... //Apply for a canvas object whose initial size is dirty canvas = mSurface.lockCanvas(dirty); //Set density canvas.setDensity(mDensity); } catch (Surface.OutOfResourcesException e) { ... } catch (IllegalArgumentException e) { ... return false; } finally { ... } try { //Does the canvas need to be moved canvas.translate(-xoff, -yoff); //mView is the RootView added to the Window //For windows opened by Activity and Dialog, mView is known as DecorView //rootView draw() method mView.draw(canvas); } finally { try { //Submit the painted content to the Surface surface.unlockCanvasAndPost(canvas); } catch (IllegalArgumentException e) { ... } } return true; }
The key functions of the above methods are as follows:
1. Request a Canvas object from the Surface, which is of compatible Canvas type
2, after getting Canvas, call View.draw(Canvas) to start drawing RootView.
3. After the entire ViewTree is drawn, submit the content to the Surface
Note: RootView is only a pronoun, not the name of a View.
For some common rootviews, please move to: The source of Android input events (1)
For the View.draw(xx) method: Draw process of Android custom View (Part 1) It has been analyzed in detail. Combined with the above codes, it is shown in the figure below:
It can be seen that the software drawing has the following characteristics:
- Recursively call the draw(xx) method of the sub layout from the RootView until each qualified View is drawn
- During drawing, all views hold the same Canvas object
Introduce question 1: since all views have the same Canvas, how to determine the starting point and ending point of each View?
This problem will be analyzed later.
Hardware accelerated rendering process
outline
Software rendering is to write a series of Canvas operations into Bitmap. For hardware accelerated rendering, each View has a RenderNode. When rendering is needed, a RecordingCanvas is obtained from RenderNode. Like software rendering, it also calls a series of APIs of Canvas, However, these API calls are recorded as a series of operation behaviors and stored in the DisplayList. When a View recording ends, give the DisplayList to RenderNode. At this time, the drawing steps have been recorded in RenderNode. At this point, the hardware drawing for a single View is completed. This process is also known as the construction process of DisplayList.
Call procedure analysis
Let's take a look at the entry of hardware acceleration:
#ThreadedRenderer.java void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks) { ... //(1) --- > recording operation //Update DisplayList of root View //From here, record the drawing operation in the DisplayList //The final record is in the rootRenderNode updateRootDisplayList(view, callbacks); //(2) --- > rendering //Render what you paint //Proxy is used as a bridge, and proxy is associated with rootRenderNode //Therefore, the drawing operation recorded in the previous step is finally handed over to a separate thread for rendering int syncResult = syncAndDrawFrame(choreographer.mFrameInfo); ... }
Focus on the recording process, and then analyze it:
#ThreadedRenderer.java private void updateRootDisplayList(View view, DrawCallbacks callbacks) { //Traverse the ViewTree and build the DisplayList updateViewTreeDisplayList(view); //When the ViewTree DisplayList is built //At first, mRootNode did not have a DisplayList if (mRootNodeNeedsUpdate || !mRootNode.hasDisplayList()) { //Apply for Canvas RecordingCanvas canvas = mRootNode.beginRecording(mSurfaceWidth, mSurfaceHeight); try { ... //View. Updatedisdisplaylistifdirty() returns the renderNode associated with RootView //Now hang the RootView renderNode under the canvas, so that all rendernodes can be connected in series canvas.drawRenderNode(view.updateDisplayListIfDirty()); ... mRootNodeNeedsUpdate = false; } finally { //Finally, hang the DisplayList under renderNode mRootNode.endRecording(); } } } private void updateViewTreeDisplayList(View view) { //Mark that the View has been drawn view.mPrivateFlags |= View.PFLAG_DRAWN; //Mrecreatedisplaylist -- > indicates whether the View needs to rebuild the DisplayList, that is, re record. More frankly, whether the Draw process needs to be followed //If pflag is marked_ Invalid flag, that is, if the View needs to be refreshed, it needs to be rebuilt view.mRecreateDisplayList = (view.mPrivateFlags & View.PFLAG_INVALIDATED) == View.PFLAG_INVALIDATED; //Clear the original value view.mPrivateFlags &= ~View.PFLAG_INVALIDATED; //If necessary, update the DisplayList of the View view.updateDisplayListIfDirty(); //The View has been rebuilt. No more rebuilding is needed view.mRecreateDisplayList = false; }
The above calls the method in View: updatedisisplaylistifdirty().
As the name suggests, if necessary, update the DisplayList of the View.
#View.java public RenderNode updateDisplayListIfDirty() { //When each View is constructed, a RenderNode:mRenderNode is created, which is called a render node final RenderNode renderNode = mRenderNode; //Whether hardware acceleration is supported is determined by judging View.AttachInfo.mThreadedRenderer if (!canHaveDisplayList()) { return renderNode; } //Remove the tag of the View //1. Rendering cache is invalid. 2. The rendering node has no DisplayList. 3. The rendering node has DisplayList, but it needs to be updated //If one of the three conditions is met, enter the condition code block if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0 || !renderNode.hasDisplayList() || (mRecreateDisplayList)) { //If there is a DisplayList and the DisplayList does not need to be updated, it means that the View does not need to go through the Draw process again if (renderNode.hasDisplayList() && !mRecreateDisplayList) { //Mark that the View has been drawn and the cache is valid mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID; mPrivateFlags &= ~PFLAG_DIRTY_MASK; //Continue to see if the sub layout needs to build a DisplayList dispatchGetDisplayList(); //---------(1) return renderNode; // no work needed } //If the above conditions are not met, the View needs to build a DisplayList mRecreateDisplayList = true; //The coordinates of the View determined in the layout process are used at this time int width = mRight - mLeft; int height = mBottom - mTop; //Gets the currently set layerType int layerType = getLayerType(); //Obtain the Canvas object from renderNode, and initialize the Canvas size to the View size //This Canvas is a RecordingCanvas type, which is simply understood as a Canvas used for recording final RecordingCanvas canvas = renderNode.beginRecording(width, height); try { //layerType has three values //In case of software drawing cache if (layerType == LAYER_TYPE_SOFTWARE) { //---------(2) //Then build the cache buildDrawingCache(true); //In fact, it is to write the drawing operation into the Bitmap Bitmap cache = getDrawingCache(true); if (cache != null) { //Draw the Bitmap into Canvas canvas.drawBitmap(cache, 0, 0, mLayerPaint); } } else { //If the software drawing cache is not set //Generally used with Scroller sliding computeScroll(); //Mscollx and mscolly are the rolling distance //When mScrollX is positive, the canvas moves to the left and the drawn content moves to the left, which is the fundamental reason why the View content moves to the left when scroll is positive canvas.translate(-mScrollX, -mScrollY); mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID; mPrivateFlags &= ~PFLAG_DIRTY_MASK; //---------(3) if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) { //The View does not need to draw its own content (including content, foreground, background, etc.) //Directly initiate the request to draw the sub layout dispatchDraw(canvas); drawAutofilledHighlight(canvas); if (mOverlay != null && !mOverlay.isEmpty()) { mOverlay.getOverlayView().draw(canvas); } if (debugDraw()) { debugDrawFocus(canvas); } } else { //You need to draw yourself draw(canvas); } } } finally { //Finally, end the canvas recording and give the result of the recording: DisplayList to renderNode //---------(4) renderNode.endRecording(); setDisplayListProperties(renderNode); } } else { //If the three conditions are not met, it is considered that the drawing has been completed mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID; mPrivateFlags &= ~PFLAG_DIRTY_MASK; } //Return renderNode to the previous layer //---------(5) return renderNode; }
Five important points are listed in the notes to analyze one by one:
(1)
dispatchGetDisplayList()
This method is not implemented in View. It is implemented in ViewGroup as follows:
#ViewGroup.java protected void dispatchGetDisplayList() { final int count = mChildrenCount; final View[] children = mChildren; for (int i = 0; i < count; i++) { //Traversal sub layout final View child = children[i]; if (((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null)) { //Rebuild sub layout DisplayList recreateChildDisplayList(child); } } ... } private void recreateChildDisplayList(View child) { //Determine whether reconstruction is needed child.mRecreateDisplayList = (child.mPrivateFlags & PFLAG_INVALIDATED) != 0; child.mPrivateFlags &= ~PFLAG_INVALIDATED; //Call the sub layout reconstruction method child.updateDisplayListIfDirty(); child.mRecreateDisplayList = false; }
It can be seen that the functions of dispatchGetDisplayList are as follows:
Traverse the sub layouts and call their reconstruction methods
In this way, start from the RootView and call updatedisisplaylistifdirty() recursively. If the sub layout needs to rebuild the DisplayList, re record the drawing operation. Otherwise, continue to find out whether the sub layout needs to rebuild the DisplayList.
(2)
buildDrawingCache(xx) is used to draw off screen cache, which will be described in detail later.
(3)
Skip drawing this paragraph for reference: Why didn't Android ViewGroup onDraw call
(4)
Hardware accelerated drawing is marked with start, recording and end:
1. renderNode generates Canvas for drawing – > beginrecoding. This is the beginning.
2. Call Canvas.drawXX() – > to record specific things. This is the recording process
3. renderNode finish drawing – > endrecording(), get the recording result: DisplayList from Canvas, and assign the result to renderNode, which is the end of recording
(5)
It can be seen from point 4 that the recorded results have been stored in the RenderNode, and the RenderNode needs to be returned. The RenderNode will be hung in the Canvas of the parent layout, that is, the Canvas of the parent layout already holds the DisplayList recorded by the child layout.
To simplify, use a diagram to show the hardware accelerated rendering process of a single View:
ViewTree hardware acceleration process:
Obviously, the hardware accelerated rendering process is the process of building the DisplayList. The DisplayList is built from the RootView recursive sub layout. When the whole DisplayList is built, it can be rendered, and the rendering thread is handed over to the GPU for processing, which greatly liberates the CPU.
Influence of LayerType on painting
The above describes the processes of software rendering and hardware accelerated rendering respectively. The starting point of analysis is whether the Window supports hardware acceleration and takes different branches.
From RootView to traversing all descendant views, they are either software drawn or hardware accelerated drawn. What will happen if the hardware acceleration of a View is disabled in the middle of hardware accelerated drawing? We mentioned earlier that hardware acceleration can be disabled by setting View - > LayerType. Next, we will analyze the impact of LayerType on the drawing process.
from Draw process of Android custom View (Part 1)
The analysis shows that no matter software rendering or hardware accelerated rendering, it will follow a common process:
draw(xx)->dispatchDraw(xx)->draw(x1,x2,x3)->draw(xx)...
This is also a recursive call process.
For a single View, where are the differences between software rendering and hardware acceleration?
The answer is: draw(x1,x2,x3) method
View soft and hard drawing bifurcation points
#View.java boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) { //Does canvas support hardware acceleration //The default canvas does not support hardware acceleration //RecordingCanvas supports hardware acceleration final boolean hardwareAcceleratedCanvas = canvas.isHardwareAccelerated(); //Whether to use RenderNode to draw, that is, whether the View supports hardware acceleration //The condition that the View supports hardware acceleration is: canvas supports hardware acceleration + the Window supports hardware acceleration boolean drawingWithRenderNode = mAttachInfo != null && mAttachInfo.mHardwareAccelerated && hardwareAcceleratedCanvas; //Animation related ... if (hardwareAcceleratedCanvas) { //canvas supports hardware acceleration. You need to check whether you need to rebuild the DisplayList mRecreateDisplayList = (mPrivateFlags & PFLAG_INVALIDATED) != 0; mPrivateFlags &= ~PFLAG_INVALIDATED; } RenderNode renderNode = null; Bitmap cache = null; //Get LayerType. The default type of View is None int layerType = getLayerType(); //------>(1) if (layerType == LAYER_TYPE_SOFTWARE || !drawingWithRenderNode) { //1. Off screen software rendering cache is set. 2. View does not support hardware accelerated rendering //The two meet one if (layerType != LAYER_TYPE_NONE) { //Software cache or hardware cache may be set //At this time, the hardware cache is used as the software cache layerType = LAYER_TYPE_SOFTWARE; //Draw to software cache //------>(2) buildDrawingCache(true); } //Fetch software cache cache = getDrawingCache(true); } if (drawingWithRenderNode) { //----->(3) //The View supports hardware acceleration //Then try to build the DisplayList and return renderNode renderNode = updateDisplayListIfDirty(); if (!renderNode.hasDisplayList()) { //I seldom go here renderNode = null; drawingWithRenderNode = false; } } int sx = 0; int sy = 0; if (!drawingWithRenderNode) { computeScroll(); //Offset content when hardware acceleration is not used sx = mScrollX; sy = mScrollY; } //Pay attention to these two marks, which will be used below //1. There is software cache. 2. Hardware acceleration is not supported. If both are established at the same time, it indicates that software cache is used for drawing final boolean drawingWithDrawingCache = cache != null && !drawingWithRenderNode; //1. There is no software cache. 2. Hardware acceleration is not supported. If both are established at the same time, it means that software rendering is used final boolean offsetForScroll = cache == null && !drawingWithRenderNode; if (offsetForScroll) { //------>(4) //For software drawing, move the canvas according to the View offset and content offset //The content scroll offset is included canvas.translate(mLeft - sx, mTop - sy); } else { if (!drawingWithRenderNode) { //------>(5) //If hardware acceleration is not supported, it may be software cache drawing //At this time, you also need to shift the canvas, but you don't need to consider the content scrolling offset canvas.translate(mLeft, mTop); } ... } ... if (!drawingWithRenderNode) { //Hardware acceleration is not supported if ((parentFlags & ViewGroup.FLAG_CLIP_CHILDREN) != 0 && cache == null) { //Reduce the canvas and limit the canvas display area, which is why the child layout display cannot exceed the parent layout area if (offsetForScroll) { //If it is software drawing, the rolling distance is reduced //------>(6) canvas.clipRect(sx, sy, sx + getWidth(), sy + getHeight()); } else { //Otherwise, the rolling distance does not need to be considered if (!scalingRequired || cache == null) { canvas.clipRect(0, 0, getWidth(), getHeight()); } else { canvas.clipRect(0, 0, cache.getWidth(), cache.getHeight()); } } } ... } if (!drawingWithDrawingCache) { //Draw without software cache if (drawingWithRenderNode) { //Support hardware acceleration mPrivateFlags &= ~PFLAG_DIRTY_MASK; //Hang the renderNode of the View under the Canvas of the parent layout, and a connection is established here ((RecordingCanvas) canvas).drawRenderNode(renderNode); } else { //For software drawing, a drawing request is initiated: dispatchdraw (canvas) & draw (canvas); //------>(7) if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) { mPrivateFlags &= ~PFLAG_DIRTY_MASK; dispatchDraw(canvas); } else { draw(canvas); } } } else if (cache != null) { //Software drawing cache exists mPrivateFlags &= ~PFLAG_DIRTY_MASK; if (layerType == LAYER_TYPE_NONE || mLayerPaint == null) { ... //If the cache type is not set, the software drawing cache is written to the bitmap of canvas canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint); } else { ... canvas.drawBitmap(cache, 0.0f, 0.0f, mLayerPaint); } } ... //The View has been built mRecreateDisplayList = false; return more; }
The judgment in this method is chaotic, and seven important points are extracted:
(1)
As long as off screen software cache is set or hardware acceleration is not supported, you need to use software cache to draw.
(3)
Hardware accelerated rendering is used as long as hardware acceleration is supported. Combination (1), do you feel a little contradictory? Consider one of the conditions in (1): off screen software cache is set and hardware acceleration is supported. According to the logic in (1), software cache rendering is enabled at this time. So (3) isn't it unnecessary to continue to accelerate rendering with hardware?
Review the snippet in updatedisisplaylistifdirty():
if (layerType == LAYER_TYPE_SOFTWARE) { ... //Software cache drawing buildDrawingCache(true); } else { //Hardware Drawing ... }
Here the judge again.
(4)(5)
Canvas displacement
For software rendering, the Canvas is displaced, and the displacement distance considers the offset of View itself and the offset of View content.
For software cache rendering, the Canvas is displaced, and only the offset of the View itself is considered.
For hardware accelerated rendering, the displacement of Canvas is not seen.
In fact, for software cached rendering and hardware accelerated rendering, Canvas displacement includes both View itself offset and View content offset. It's just not in the above code.
For software cache drawing:
In builddrawingcacheimpl (XX) - > canvas.translate (- mscollx, - mscolly); Content offset.
For hardware accelerated rendering:
Offset the View itself in layout (XX) - > mrendernode.setlefttoprightbottom (mleft, MTop, mright, mbottom).
Update displaylistifdirty (XX) - > canvas. Translate (- mscollx, - mscolly); Content offset.
Therefore, regardless of software rendering / software cache rendering / hardware accelerated rendering, the three displace the Canvas, including the offset of View itself and the offset of content.
The above also explains question 1.
(6)
Canvas reduction
For software rendering, Canvas reduction includes View content offset.
For software cache drawing, Canvas is drawn into Bitmap.
For hardware accelerated rendering, trim in setdisplaylistproperties (XX) - > rendernode. Setcliptobounds (XX).
(7)
If it is software drawing, directly call dispatchDraw(xx)/draw(xx) to initiate drawing.
The draw(x1,x2,x3) method is used to determine the drawing method used by the View:
1. Hardware accelerated rendering
2. Software drawing
3. Software cache drawing
Software cache drawing
Let's see how to build a software cache:
#View.java public void buildDrawingCache(boolean autoScale) { //If the cache is marked invalid or empty if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0 || (autoScale ? mDrawingCache == null : mUnscaledDrawingCache == null)) { try { //Build cache buildDrawingCacheImpl(autoScale); } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } } } private void buildDrawingCacheImpl(boolean autoScale) { int width = mRight - mLeft; int height = mBottom - mTop; ... boolean clear = true; Bitmap bitmap = autoScale ? mDrawingCache : mUnscaledDrawingCache; //If the bitmap does not exist or the size of the bitmap is inconsistent with that of the View, it is created ... Canvas canvas; if (attachInfo != null) { canvas = attachInfo.mCanvas; if (canvas == null) { //For the first time, there was no Canvas in AttachInfo canvas = new Canvas(); } //Associated bitmap canvas.setBitmap(bitmap); attachInfo.mCanvas = null; } else { //I seldom go here canvas = new Canvas(bitmap); } computeScroll(); final int restoreCount = canvas.save(); //Scroll and PAN based on content canvas.translate(-mScrollX, -mScrollY); mPrivateFlags |= PFLAG_DRAWN; if (mAttachInfo == null || !mAttachInfo.mHardwareAccelerated || mLayerType != LAYER_TYPE_NONE) { //Mark to indicate that the software drawing cache is in effect mPrivateFlags |= PFLAG_DRAWING_CACHE_VALID; } //Similarly, call the public method if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) { mPrivateFlags &= ~PFLAG_DIRTY_MASK; dispatchDraw(canvas); drawAutofilledHighlight(canvas); if (mOverlay != null && !mOverlay.isEmpty()) { mOverlay.getOverlayView().draw(canvas); } } else { draw(canvas); } canvas.restoreToCount(restoreCount); canvas.setBitmap(null); if (attachInfo != null) { //Record it and use it directly next time attachInfo.mCanvas = canvas; } }
In this way, the software cache is built, and the results are stored in Bitmap, which can be obtained through the following methods:
#View.java public Bitmap getDrawingCache(boolean autoScale) { //Disable software caching //Not prohibited by default if ((mViewFlags & WILL_NOT_CACHE_DRAWING) == WILL_NOT_CACHE_DRAWING) { return null; } if ((mViewFlags & DRAWING_CACHE_ENABLED) == DRAWING_CACHE_ENABLED) { //Whether the software cache drawing is enabled. It is not enabled by default //Build cache buildDrawingCache(autoScale); } //Return cache to return autoScale ? mDrawingCache : mUnscaledDrawingCache; }
This method can be used to get the page of the View.
Make a summary:
At the beginning, the hardware accelerated drawing process and the software drawing process go their own way without affecting each other.
1. When using software drawing, if the off screen cache type is set: software cache, the software drawing is invalid and only the software cache is used for drawing. If the hardware cache type is set, it is also drawn as software cache.
2. When using hardware accelerated rendering, if the off screen cache type is set: software cache, the hardware accelerated rendering fails and only the software cache is used for rendering. This is why setting software cache can disable hardware acceleration.
3. The drawing results of the software cache are saved in the bitmap, and the bitmap will eventually be drawn to the Canvas of the parent layout.
No matter which drawing type is used, the common calling method will be used: draw(xx)/dispatchDraw(xx).
Therefore, the drawing type is transparent for us to override onDraw(xx).
Where did Canvas come from
Software drawing
Start with viewrootimpl - > drawsoftware (XX) by:
canvas = mSurface.lockCanvas(dirty);
Canvas generated. The canvas is passed to all sub layouts through the View.draw(xx) method, so in this case, the entire ViewTree shares the same canvas object. The canvas type is CompatibleCanvas.
Hardware accelerated rendering
Start with view - > updatedisisplaylistifdirty (XX) by:
final RecordingCanvas canvas = renderNode.beginRecording(width, height);
Canvas generated. It can be seen that canvas is regenerated for each View that supports hardware acceleration. The canvas type is RecordingCanvas.
Software cache drawing
Start with view - > builddrawingcacheimpl (XX) by:
canvas = new Canvas();
The Canvas is generated and recorded in AttachInfo. It will be used the next time the View software cache is built. It can be seen that for each View using software cache, a new Canvas is generated. Of course, if AttachInfo is available, it can be reused.
Canvas out of View
The above three have a common feature: the generated Canvas is finally connected with the Surface, so the content drawn through these canvases can finally be displayed on the screen.
Can I directly construct a Canvas that is separated from the View? The answer is yes.
private void buildCanvas(int width, int height) { Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(); canvas.setBitmap(bitmap); //draw canvas.drawXX(xx); ... }
As shown above, create a canvas and bitmap and associate them. Finally, call Canvas to draw API, and the result will be saved in Bitmap. This process is actually the method used by software cache rendering.
Of course, after we get the Bitmap, we want it to be easier to display. As long as it is associated with the View, it can be displayed on the screen. Associating to a View actually uses the Canvas associated with the View to draw the generated Bitmap on it,
Drawing process family photo
The drawing process is shown in the figure:
Simple software rendering and hardware accelerated rendering:
Drawing when software cache is set:
This concludes the Draw process series.
The source code of this article is based on Android 10.