Drawing
Unnecessary efficiency considerations are often the root of the evils of performance problems. --william Allan Wulf
In the 12th chapter, "Curvature of velocity" we learn how to use instruments to diagnose core animation performance problems. There are a number of potential performance pitfalls when building an iOS app, but in this chapter we'll look at performance issues related to drawing .
Software drawings
The term drawing typically refers to a software drawing in the context of the core animation (meaning: drawings that are not supported by the GPU). In iOS, software drawings are usually done by the core graphics framework. However, in some necessary cases, it is much slower than the Core animation and Opengl,core graphics.
Software drawing is not only inefficient, it also consumes considerable memory. CALayer
just need some memory related to yourself: only its boarding map consumes a certain amount of memory space. Even if you assign contents
a picture directly to a property, you do not need to add an extra photo storage size. If the same picture is a property of multiple layers contents
, they will share the same block of memory instead of duplicating the memory block.
But once you have implemented the method in the CALayerDelegate
protocol -drawLayer:inContext:
or UIView
the -drawRect:
method in it (in fact, the former packaging method), the layer creates a drawing context, which is the size of the memory that the context needs to derive from this equation: layer width * Layer Height * 4 bytes, The units of the wide height are all pixels. For a full-screen layer on the retina ipad, the amount of memory is 2048*1526*4 bytes, which is equivalent to 12MB of RAM, which needs to be re-erased and redistributed each time the layer is redrawn.
Software drawings are expensive, and unless absolutely necessary, you should avoid redrawing your view. The secret to improving drawing performance is to avoid drawing as much as possible.
Vector graphics
One of the common reasons we use the core graphics for drawing is that vector graphics are not easily drawn with images or layer effects. Vector drawings contain these:
- Freeform (not just a rectangle)
- Slash or Curve
- Text
- Gradient
For example, listing 13.1 shows a basic line-drawing application. This app converts a user's touch gesture to a UIBezierPath
point on top and then draws it into a view. We UIView
DrawingView
implemented all the drawing logic in a subclass, in which case we didn't use the view controller. But if you like you can implement touch event handling in the View controller. Figure 13.1 Shows the result of the code run.
Listing 13.1 implements a simple drawing application with the core graphics
#import "DrawingView.h"@interfaceDrawingview () @property (nonatomic, strong) Uibezierpath*path;@end@implementationDrawingview- (void) awakefromnib{//Create a mutable pathSelf.path =[[Uibezierpath alloc] init]; Self.path.lineJoinStyle=Kcglinejoinround; Self.path.lineCapStyle=Kcglinecapround; ? Self.path.lineWidth=5;}- (void) Touchesbegan: (Nsset *) touches withevent: (Uievent *)Event{ //get the starting pointCgpoint point =[[Touches Anyobject] locationinview:self]; //move the path drawing cursor to the starting point[Self.path movetopoint:point];}- (void) touchesmoved: (Nsset *) touches withevent: (Uievent *)Event{ //get The current pointCgpoint point =[[Touches Anyobject] locationinview:self]; //Add a new line segment to our path[Self.path Addlinetopoint:point]; //Redraw the View[self setneedsdisplay];}- (void) DrawRect: (cgrect) rect{//Draw Path[[Uicolor Clearcolor] setfill]; [[Uicolor Redcolor] setstroke]; [Self.path stroke];}@end
Figure 13.1 Making a simple "sketch" with the core graphics
The problem with this implementation is that the more we draw, the slower the program will become. Because the entire Bezier path is redrawn each time the finger is moved ( UIBezierPath
), as the path becomes more complex, the work of each redraw increases, resulting in a decrease in the number of frames. It seems that we need a better way.
Core Animation provides specialized classes for drawing these graphic types and provides them with hardware support (the sixth chapter "proprietary layers" is mentioned in detail). CAShapeLayer
you can draw polygons, lines, and curves. CATextLayer
you can draw text. CAGradientLayer
used to draw gradients. These are generally faster than core graphics, and they also avoid creating a homestay map.
If you change the previous code a little bit and CAShapeLayer
replace the core Graphics, the performance will be improved (see listing 13.2). Although the drawing performance will continue to degrade as the path complexity increases, the frame rate difference is noticeable only when very very impetuous drawing.
Listing 13.2 Using the CAShapeLayer
re-implementation drawing app
#import "DrawingView.h"#import<QuartzCore/QuartzCore.h>@interfaceDrawingview () @property (nonatomic, strong) Uibezierpath*path;@end?@implementationDrawingview+(Class) layerclass{//This makes our view create a Cashapelayer//instead of a calayer for its backing layer return[Cashapelayerclass];}- (void) awakefromnib{//Create a mutable pathSelf.path =[[Uibezierpath alloc] init]; //Configure the LayerCashapelayer *shapelayer = (Cashapelayer *) Self.layer; Shapelayer.strokecolor=[Uicolor Redcolor]. Cgcolor; Shapelayer.fillcolor=[Uicolor Clearcolor]. Cgcolor; Shapelayer.linejoin=Kcalinejoinround; Shapelayer.linecap=Kcalinecapround; Shapelayer.linewidth=5;}- (void) Touchesbegan: (Nsset *) touches withevent: (Uievent *)Event{ //get the starting pointCgpoint point =[[Touches Anyobject] locationinview:self]; //move the path drawing cursor to the starting point[Self.path movetopoint:point];}- (void) touchesmoved: (Nsset *) touches withevent: (Uievent *)Event{ //get The current pointCgpoint point =[[Touches Anyobject] locationinview:self]; //Add a new line segment to our path[Self.path Addlinetopoint:point]; //update the layer with a copy of the path((Cashapelayer *) self.layer). Path =Self.path.CGPath;}@end
Dirty rectangles
Sometimes replacing the core graphics with CAShapeLayer
or other vector graphics layers is not so practical. For example, our drawing application: We use the line to complete the vector drawing perfectly. But imagine if we can further improve the performance of the application, let it work like a blackboard, and then use "chalk" to draw lines. The simplest way to simulate chalk is to use a "line brush" image and paste it where the user's finger touches it, but this method is CAShapeLayer
not possible.
We can create a separate layer for each "line brush", but it's a big problem to implement. The number of layers allowed to appear on the screen at the same time is about hundreds of, so we'll be out of it soon. In this case we don't have a choice, just use the core graphics (unless you want to do something more complicated with OpenGL).
The initial implementation of our "blackboard" application is shown in Listing 13.3, where we changed the previous version DrawingView
and replaced it with an array of brush positions UIBezierPath
. Figure 13.2 is the result of the operation
Listing 13.3 Simple blackboard-like applications
#import "DrawingView.h"#import<QuartzCore/QuartzCore.h>#defineBrush_size 32@interfaceDrawingview () @property (nonatomic, strong) Nsmutablearray*strokes;@end@implementationDrawingview- (void) awakefromnib{//Create arraySelf.strokes =[Nsmutablearray array];}- (void) Touchesbegan: (Nsset *) touches withevent: (Uievent *)Event{ //get the starting pointCgpoint point =[[Touches Anyobject] locationinview:self]; //Add Brush Stroke[self addbrushstrokeatpoint:point];}- (void) touchesmoved: (Nsset *) touches withevent: (Uievent *)Event{ //get the Touch pointCgpoint point =[[Touches Anyobject] locationinview:self]; //Add Brush Stroke[self addbrushstrokeatpoint:point];}- (void) Addbrushstrokeatpoint: (cgpoint) point{//Add brush stroke to array[Self.strokes addobject:[nsvalue valuewithcgpoint:point]; //needs redraw[self setneedsdisplay];}- (void) DrawRect: (cgrect) rect{//Redraw Strokes for(Nsvalue *valueinchself.strokes) {//Get PointCgpoint point =[value cgpointvalue]; //Get Brush rectCGRect brushrect = CGRectMake (point.x-brush_size/2, point.y-brush_size/2, Brush_size, brush_size); //Draw brush Stroke?[[UIImage imagenamed:@"Chalk.png"] Drawinrect:brushrect]; }}@end
Figure 13.2 Using a program to draw a simple "sketch"
This implementation is good on the simulator, but not so good on the real device. The problem is that we redraw the previous line brush every time the finger moves, even if the majority of the scene doesn't change. The more we plot, the slower we will be. As time increases each time the redraw takes more time, the number of frames also drops (see Figure 13.3), how do you improve performance?
Figure 13.3 Frame rate and line quality will decrease over time.
To reduce unnecessary drawing, Mac OS and iOS devices will differentiate the screen into areas that need to be redrawn and areas that do not need to be redrawn. The parts that need to be redrawn are called "dirty areas." In practice, given the complexity of clipping and blending of non-rectangular area boundaries, it is common to distinguish the rectangular position that contains the specified view, which is the "dirty rectangle".
When a view has been changed, TA may need to be redrawn. But in many cases, just part of the view has been changed, so redrawing the entire homestay map is a waste. But core animation usually doesn't know your custom drawing code, nor does it calculate the location of the dirty area on its own. However, you can indeed provide this information.
When you detect that the specified part of the specified view or layer needs to be redrawn, you call -setneedsdisplayinrect: to mark it, and then pass the affected rectangle as a parameter. This invokes the view's -drawrect: (or the layer proxy's -drawlayer:incontext: method) When a view is refreshed.
Incoming -drawLayer:inContext:
CGContext
parameters are automatically trimmed to fit the corresponding rectangle. To determine the size of the rectangle, you can use the CGContextGetClipBoundingBox()
method to get the size from the context. -drawRect()
the invocation is simpler because CGRect
it is passed directly as a parameter.
You should limit your drawing work to this rectangle. Any drawing outside this area will be ignored automatically, but it is not worth the time it takes to calculate and discard the CPU.
Instead of relying on the core graphics to redraw for you, cropping out your own drawing area may allow you to avoid unnecessary actions. That is to say, if your clipping logic is quite complex, let the core graphics do it, and remember: You do it when you can do it efficiently.
Listing 13.4 shows an -addBrushStrokeAtPoint:
upgraded version of a method that redraws only the nearby area of the current line brush. It also refreshes the vicinity of the front brush, which we can also use CGRectIntersectsRect()
to avoid redrawing any old line brushes so as not to overwrite the area that has been updated. This will significantly improve drawing efficiency (see figure 13.4).
Listing 13.4 -setNeedsDisplayInRect:
is used to reduce unnecessary drawing
- (void) Addbrushstrokeatpoint: (cgpoint) point{//Add brush stroke to array[Self.strokes addobject:[nsvalue valuewithcgpoint:point]; //set Dirty rect[self setneedsdisplayinrect:[self brushrectforpoint:point];}-(CGRect) Brushrectforpoint: (cgpoint) point{returnCGRectMake (point.x-brush_size/2, point.y-brush_size/2, Brush_size, brush_size);}- (void) DrawRect: (cgrect) rect{//Redraw Strokes for(Nsvalue *valueinchself.strokes) {//Get PointCgpoint point =[value cgpointvalue]; //Get Brush rectCGRect Brushrect =[self brushrectforpoint:point]; ? //Only draw brush stroke If it intersects dirty rect if(Cgrectintersectsrect (rect, brushrect)) {//Draw Brush Stroke[[UIImage imagenamed:@"Chalk.png"] Drawinrect:brushrect]; } }}
Figure 13.4 Better frame rate and smooth line
Drawing asynchronously
Uikit's single-threaded nature means that boarding maps are updated on the main thread, which means that drawing interrupts user interaction and even makes the entire app appear unresponsive. There's nothing we can do about it, but it's much better to avoid the user waiting for the drawing to finish.
There are some ways to do this: in some cases, we can presumably draw the content on another thread ahead of time, and then set the resulting picture directly to the content of the layer. This may not be convenient to implement, but it is feasible in certain situations. Core Animation offers some options: CATiledLayer
and drawsasynchronously properties.
Catiledlayer
We explored it briefly in the sixth chapter CATiledLayer
. In addition to splitting the layer again into a small, independent update (similar to the concept of automatic updating of dirty rectangles), CATiledLayer
There is an interesting feature: calling a method on multiple threads for each small block at the same time -drawLayer:inContext:
. This avoids blocking user interaction and being able to draw faster with multi-core new slices. Only one small piece CATiledLayer
is an easy way to implement an asynchronous update of a picture view.
drawsasynchronously
In IOS 6, Apple CALayer
has introduced this curious attribute, which changes the drawsAsynchronously
cgcontext of incoming -drawlayer:incontext: Allows Cgcontext to delay the execution of the draw command so that user interaction is not blocked.
It CATiledLayer
is not the same as using asynchronous drawing. Its own -drawlayer:incontext: The method is only called on the main thread, but Cgcontext does not wait for the end of each drawing command. Instead, it joins the command into the queue and, when the method returns, performs a real drawing on a per-background thread.
According to Apple's statement. This feature works best on views that require frequent redrawing (such as our drawing app, or something like that UITableViewCell
), and is not much help for layers that are drawn only once or rarely redrawn.
Summarize
In this chapter we discuss some of the performance challenges with the core graphics software drawing, and then explore some improvements: such as improving drawing performance or reducing the amount of paint that needs to be drawn.
In the 14th chapter, "Image Io", we will discuss the loading performance of the image.
[IOS animation]-calayer drawing efficiency