YYKit series source code analysis article:

  • YYText source code analysis: CoreText and asynchronous drawing
  • YYModel source code analysis: focus on performance
  • YYCache source code analysis: highlights
  • YYImage source code analysis: image processing skills
  • YYAsyncLayer source code analysis: asynchronous drawing
  • YYWebImage source code analysis: thread processing and caching strategy

preface

YYText is a well-known rich text framework in the industry. Based on CoreText, it does a lot of infrastructure and implements two upper view components: YYLabel and YYTextView. Like other YYKit components, YYText performs well in terms of performance and is surprisingly powerful, arguably the best in the industry.

Mention YYText, we all know its core optimization point: asynchronous drawing, however, this is just the tip of the iceberg, YYText in the most complex and most length is based on CoreText of various calculations, have to say, the source of a large number of calculations is very easy to dazzle.

To understand YYText in depth or follow this article, you must know the basics of CoreText and be patient. Framework code is very large, this article mainly explains the framework based on CoreText of the basic part of the underlying, not too much to explain the details of YYLabel and YYTextView.

I. Overview of the framework

YYText GitHub

Most iOS UI components must be drawn in the main thread. When the drawing pressure is too high, the interface will get stuck. Thanks to multi-threading technology, we can draw graphics in the asynchronous thread to reduce the main thread pressure.

Core idea of YYText: create a graphics context in the asynchronous thread, then draw rich text using CoreText, draw pictures, shadows, borders and so on using CoreGraphics, and finally put the completed bitmap into the main thread for display.

The steps seem simple enough, but the source code that involves drawing CoreText and CoreGraphics requires a lot of code to calculate positions, which is one of the main points of this article. In the interest of brevity, I’ll skip over some technical details, such as vertical text layout logic and some weird BUG fixing code.

I hope readers can first understand the CoreText foundation (CoreText official introduction), here put two structure diagram for easy understanding (figure will be biased) :

CoreText related utility classes

1, YYTextRunDelegate

Insert the key into the rich text for kCTRunDelegateAttributeName CTRunDelegateRef instance can customize the size of a region, usually use this way to set aside a period of blank, behind can fill pictures to reach by the effect of the mixed. Creating CTRunDelegateRef requires a series of function names that are cumbersome to use. The framework uses a class to encapsulate them to reduce the cost of using them:

@interface YYTextRunDelegate : NSObject <NSCopying, NSCoding>
...
@property (nonatomic) CGFloat ascent;
@property (nonatomic) CGFloat descent;
@property (nonatomic) CGFloat width;
@end
Copy the code
static void DeallocCallback(void *ref) {
    YYTextRunDelegate *self = (__bridge_transfer YYTextRunDelegate *)(ref);
    self = nil; // release
}
static CGFloat GetAscentCallback(void *ref) {
    YYTextRunDelegate *self = (__bridge YYTextRunDelegate *)(ref);
    returnself.ascent; }... @implementation YYTextRunDelegate - (CTRunDelegateRef)CTRunDelegate CF_RETURNS_RETAINED { CTRunDelegateCallbacks callbacks; callbacks.dealloc = DeallocCallback; callbacks.getAscent = GetAscentCallback; .returnCTRunDelegateCreate(&callbacks, (__bridge_retained void *)(self.copy)); }...Copy the code

Create a CTRunDelegateCreate() with a __bridge_retained transfer of memory management and hold a YYTextRunDelegate object. There are several static functions in this class as callbacks, such as the ascent property of the holding object as the return value when the GetAscentCallback() function is called.

How to free YYTextRunDelegate held by CTRunDelegateRef? The answer is in the DeallocCallback() callback that goes when CTRunDelegateRef releases, transferring memory management privileges to a YYTextRunDelegate local variable that automatically manages memory.

** Note 2: CTRunDelegateCreate(&callbacks, (__bridge_retained void *)(self.copy)) executes a copy of self. What’s the point of doing that? The first reaction might be to think that CTRunDelegateRef holds a copy of self to avoid circular references, however this method does not make self hold an instance after CTRunDelegateCreate(), so there is no circular reference problem. In fact, it should just create a copy and keep the configuration data secure when the method returns (avoiding accidental external changes).

2, YYTextLine

Create a rich text that takes CTLineRef and CTRunRef as well as some structured data (such as Ascent Descent). CTRunRef does not contain a lot of data content, so the framework does not make a special class to wrap it. Use YYTextLine to packaging CTLineRef save some data to the back of the calculation, such as using CTLineGetTypographicBounds (…). ; Ascent Descent leading, etc.

Calculates the line position and size

_bounds = CGRectMake(_position.x, _position.y - _ascent, _lineWidth, _ascent + _descent);
_bounds.origin.x += _firstGlyphPos;
Copy the code

_position means that the coordinates of the origin point of line in the context are converted to the value of UIKit coordinate system, then combined with the above structure figure 2 analysis: Y – _ascent is the minimum y value of line and _ascent + _descent is the line height (leading is not included).

Here the minimum X value is added with _firstGlyphPos, which is the offset of the current line’s first run relative to line by CTRunGetPositions(…). ; Calculate that there may be a scenario where the origin position of line is offset from the position of the first run (I did not simulate this situation).

Find all placeholder runs

In fact, this is to find the CTRunDelegateRef, each CTRunDelegateRef corresponds to a YYTextAttachment, which represents an attachment (image, UIView, CALayer), the implementation will be explained separately later. All you need to know here is that the basic principle is to occupy the CTRunDelegateRef and fill it with YYTextAttachment.

When iterating through a run inside a line, it is crucial to calculate the position and size of the run if the run contains YYTextAttachment indicating that it is a placeholder run (so that the attachment can be filled into the correct position later).

runPosition.x += _position.x;
runPosition.y = _position.y - runPosition.y;
runTypoBounds = CGRectMake(runPosition.x, runPosition.y - ascent, runWidth, ascent + descent);
Copy the code

X + _position.x indicates the x position of run relative to the graphics context, and the same goes for the following.

Finally, the YYTextAttachment object and the run location size information are cached (the implementation logic will be analyzed later).

3, YYTextContainer

Create CTFrameRef using CTFramesetterCreateFrame(…) This method requires a CGPathRef parameter. For ease of use, the framework abstractions a YYTextContainer class with the following key attributes:

@property CGSize size;
@property UIEdgeInsets insets;
@property (nullable, copy) UIBezierPath *path;
@property (nullable, copy) NSArray<UIBezierPath *> *exclusionPaths;
Copy the code

Users can simply use CGSize to specify the size of rich text, or use UIBezierPath, a function of automatic memory management, to specify paths and exclusionPaths.

┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ < -- -- -- -- -- -- -- container │ │ │ asdfasdfasdfasdfasdfa < -- -- -- -- -- -- -- -- -- -- -- -- container insets │ Asdfasdfa asdfasdFA │ asdfas ASDASD │ ASDFA <----------------------- Container Exclusion Path │ asdfas adfasd │ Asdfasdfa asdfasdfa │ │ asdfasdfasdfasdfasdfa │ │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘Copy the code

CoreText supports hollowing out effect and is controlled by this exclusion Path. Property access to this class is thread-safe, with some fine fault tolerance.

YYTextLayout core computing class

YYTextLayout contains almost all the information of a rich text layout, and also puts a lot of drawing related C code in this file, so this file is very large. Let’s ignore these drawing codes, YYTextLayout is mainly used to calculate various data and prepare for the subsequent drawing.

The core is calculated in + (YYTextLayout *)layoutWithContainer:(YYTextContainer *)container text:(NSAttributedString *)text range:(NSRange)range; In the initialization method, which provides the data base for the various query calculations that follow, let’s look at what this over 500 line initialization method does.

1. Calculate the drawing path and the position rectangle of the path

CGPathRef is the main logic calculated based on YYTextContainer. To avoid negative value of matrix attribute, CGRectMartial Arts (…) is used. To correct. Due to the difference between UIKit and CoreText coordinates, the resulting matrix first does a coordinate inversion:

rect = CGRectApplyAffineTransform(rect, CGAffineTransformMakeScale(1, -1));
cgPath = CGPathCreateWithRect(rect, NULL);
Copy the code

or

CGAffineTransform trans = CGAffineTransformMakeScale(1, -1);
CGMutablePathRef transPath = CGPathCreateMutableCopyByTransformingPath(path, &trans);
Copy the code

They’re all the same, they’re all rotated 180 degrees along the x axis, and some of you might be wondering, didn’t UIKit switch to CoreText not only rotated 180 degrees, but also shifted the height of the drawing area? There is one less operation because the framework uses CTRunDraw(…). Iterate over the draw Run, using CGContextSetTextPosition(…) before drawing the Run. Specify the position (the position computed by line relative to the drawing area), so it is meaningless whether the y coordinate of this place is correct.

Calculation of the rectangle size and position of the path:



pathBox = (CGRect){50, 50, 100, 100}
pathBox
cgPathBox.origin
origin

2. Initialize CTFramesetterRef and CTFrameRef

This step is very simple, using the two functions is done: CTFramesetterCreateWithAttributedString (…). CTFramesetterCreateFrame(…) . It is worth noting that the framework supports several CTFrameRef properties, such as kCTFramePathWidthAttributeName, these properties are also through YYTextContainer configuration.

3. Calculate the total line frame and the number of lines

Now that we’ve created a rich text CTFrameRef, we just need to iterate through all the lines, and we can see the following code to get the size of each line:

// CoreText coordinate system
CGPoint ctLineOrigin = lineOrigins[i]; 
// UIKit coordinate system
CGPoint position;
position.x = cgPathBox.origin.x + ctLineOrigin.x;
position.y = cgPathBox.size.height + cgPathBox.origin.y - ctLineOrigin.y;

YYTextLine *line = [YYTextLine lineWithCTLine:ctLine position:position vertical:isVerticalForm];
CGRect rect = line.bounds;
Copy the code

LineOrigins: CTFrameGetLineOrigins(…) So you need to convert it to UIKit coordinates for easy calculation. This is the calculated offset of the actual drawing rectangle. The resulting position is the point relative to the graphics context. Then use this point to initialize YYTextLine. This gives you the position and size of the current line: rect.

Then, use CGRectUnion(…) The textBoundingRect () function merges the rects of each line to produce a minimum position rectangle containing all lines.

Count the number of lines in line

Instead of a single line occupying a row, a row may have two lines when there are excluded paths:

Therefore, you need to calculate the row of each line to provide a basis for many subsequent calculations, such as the maximum line limit.

When the height of the current line is greater than that of the last line, if y0 of the current line is greater than the baseline and y1 is less than the baseline, no line break is performed.

If the height of the current line is smaller than that of the last line, and y0 of the last line is greater than the baseline and y1 is less than the baseline, no line break is performed.

4. Get the upper and lower bounds array of rows

typedef struct {
    CGFloat head;
    CGFloat foot;
} YYRowEdge;
Copy the code

Declare a YYRowEdge *lineRowsEdge = NULL; The array, YYRowEdge, represents the upper and lower boundaries of each row. If the current line and the last line are the same line, select the maximum upper and lower boundary shared by line and last line:

lastHead = MIN(lastHead, rect.origin.y);
lastFoot = MAX(lastFoot, rect.origin.y + rect.size.height);
Copy the code

If the current line and the last line are different, take the upper and lower bounds of the current line:

lastHead = rect.origin.y;
lastFoot = lastHead + rect.size.height;
Copy the code

The end result could be something like this:

There will be a gap between foot1 and head2, the gap is the line spacing, the frame processing is to divide the gap evenly:

5. Calculate the total size of the drawing area

The position rectangle pathBox of the drawing path has been calculated above, which is only the size of the actual drawing area. If the line width or edge margin of YYTextContainer is set, the total size of the actual drawing area required by the service will be larger:

The blue filled area in the figure is the actual drawing area pathBox, and the total size of the drawing area should be the range covered by the blue border (please ignore the small gap between lines). With the help of a CGRectInset. (…). UIEdgeInsetsInsetRect(…) Chinese martial Arts Contains some martial arts martial arts. Cgrectmartial Arts contains some martial arts martial arts. Correct for negative values.

6. Line truncation

When rich text exceeds the limit, you may want to make an ellipsis at the end of the last line that can be displayed: aAAA… .

First there’s an NSAttributedString *truncationToken; The token can be customized, and the framework also has a default, which is a… Ellipsis, and then concatenate this truncationToken to the last line:

NSMutableAttributedString *lastLineText = [text attributedSubstringFromRange:lastLine.range].mutableCopy;
[lastLineText appendAttributedString:truncationToken];
Copy the code

Of course, such lastLineText will exceed the scope of the drawing area, so the way to use the system to provide CTLineCreateTruncatedLine (…). This method returns a CTLineRef, which is converted to YYTextLine and truncatedLine as an attribute of YYTextLayout.

This means that the truncation of YYText is always at the end of rich text, and there is only one.

7. Cache various BOOL values

Iterating over a rich text object, caches a series of BOOL values:

void (^block)(NSDictionary *attrs, NSRange range, BOOL *stop) = ^(NSDictionary *attrs, NSRange range, BOOL *stop) {
    if (attrs[YYTextHighlightAttributeName]) layout.containsHighlight = YES;
    if (attrs[YYTextBlockBorderAttributeName]) layout.needDrawBlockBorder = YES;
    if (attrs[YYTextBackgroundBorderAttributeName]) layout.needDrawBackgroundBorder = YES;
    if(attrs[YYTextShadowAttributeName] || attrs[NSShadowAttributeName]) layout.needDrawShadow = YES; . }; [layout.text enumerateAttributesInRange:visibleRange options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:block];Copy the code

Can guess, YYTextBlockBorderAttributeName is YYText custom rich text attributes, such as when initializing YYTextLayout whether will rich text contains a custom key cached.

Imagine if these BOOL values were not used here, then the framework would have to iterate over if there was a custom key and perform custom draw logic if there was. That is, this traversal must be done, either at initialization or at drawing time.

According to the framework, YYTextLayout initialization and drawing can be performed in the main thread or asynchronous drawing, so the purpose here is not to put the traversal logic into the asynchronous thread, but for caching.

After caching these BOOL values when initializing YYTextLayout, the second drawing does not need to be iterated again to optimize performance.

8. Merge all attachments

As mentioned above, when YYTextLine is initialized, all the attachment and its related location information will be loaded into the array, so it will traverse all the lines and merge the attachment related array together, so the subsequent drawing will not need to traverse the line to obtain the attachment.

9, summary

In addition to YYTextLayout initialization methods, there are also a series of Query methods under the #pragma Mark-Query tag, which are based on the above initialization calculation data. Pragma Mark-draw will be discussed later.

YYTextLayout initialization method is very long. I tried to break it down and found that it would be more complicated. The reason is that this initialization method contains a lot of memory that needs to be managed manually, such as CGPathRef CTFramesetterRef CTFrameRef, etc.

One might say, where do I need to subtract one from the reference count, just manually release?

But the reality is more complicated because the entire initialization process can be interrupted at any time. Such as calloc (…). Creating memory may fail, CGPathCreateMutableCopy(…) Creating a path can fail, so in any case failure requires interruption of initialization, something like this would be written:

if (failed) {
    CFRelease(...);
    free(...);
    ...
    return nil;
}
Copy the code

And this is where you have to free up all of the previously manually managed memory, which can drive you crazy when you have too much code.

So the author uses a clever method, goto:

fail:
    if (cgPath) CFRelease(cgPath);
    if(lineOrigins) free(lineOrigins); .return nil;
Copy the code

So, when something fails, just write:

if (failed) {
    goto fail;
}
Copy the code

In this scenario, the use of goto is really appropriate.

4. Customize rich text attributes

As we know, NSMutableAttributedString object using the addAttribute: value: range: such as a series of methods can add rich text effects, these effects have three elements: name (key), value (the value), the scope. YYText also extends some of its own names (YYTextAttribute files) :

UIKIT_EXTERN NSString *const YYTextAttachmentAttributeName; UIKIT_EXTERN NSString *const YYTextHighlightAttributeName; .Copy the code

Of course for all the key to create a corresponding value (class), such as the corresponding YYTextHighlight YYTextHighlightAttributeName. But the custom key CoreText is not recognized, so what happens inside the framework?

NSDictionary *attrs = (id)CTRunGetAttributes(run);
id anyValue = attrs[anyKey];
if (anyValue) { ... }
Copy the code

This is a simple way of iterating through rich text to find out if a run contains a custom key, and then doing the drawing logic accordingly.

1, text and text mix

Most of the user-defined attributes of YYText are “decorative” text, so it is only necessary to determine whether the corresponding key is included when drawing, and make the corresponding drawing logic if so. But one custom property is special:

YYTextAttachmentAttributeName : YYTextAttachment
Copy the code

Set a CTRunDelegateRef (UIImage, UIView, CALayer) when setting the custom property.

NSMutableAttributedString *atr = [[NSMutableAttributedString alloc] initWithString:YYTextAttachmentToken]; YYTextAttachment *attach = [YYTextAttachment new]; attach.content = content; // UIImage, UIView, CALayer... [atr yy_setTextAttachment:attach range:NSMakeRange(0, atr.length)]; YYTextRunDelegate *delegate = [YYTextRunDelegate new]; . CTRunDelegateRef delegateRef = delegate.CTRunDelegate; [atr yy_setRunDelegate:delegateRef range:NSMakeRange(0, atr.length)];Copy the code

(1) Alignment

When adding pictures, there are many alignment ways in a business. How to align is controlled by adjusting CTRunDelegateRef’s Ascent Descent. The framework has three alignment ways: up, down, and center.

R:

Make the ascent of placeholder run always equal the ascent of the text (pasted baseline if placeholder run is too short).

R:

Descent: Make placeholder RUN always equal to text descent (baseline if placeholder RUN is too short).

In the middle:

The calculation of the middle is relatively complex and requires that the midpoint of the placeholder run be aligned with the midpoint of the text (see figure). In this figure, yOffset + (height of the placeholder run) * 0.5 equals the ascent of the placeholder run (baseline if the placeholder run is too short).

Of course, the picture above could be UIView CALayer. Now that we have the run placeholder, we need to draw UIImage UIView CALayer into the corresponding empty space.

(2) Draw attachments

Draw logic in YYTextLayout under the method YYTextDrawattroar (…) UIViewContentMode is used to fill the image according to the size of the placeholder run set at the beginning, and then call the CoreGraphics API to draw the image:

CGImageRef ref = image.CGImage;
if (ref) {
    CGContextSaveGState(context);
    CGContextTranslateCTM(context, 0, CGRectGetMaxY(rect) + CGRectGetMinY(rect));
    CGContextScaleCTM(context, 1, -1);
    CGContextDrawImage(context, rect, ref);
    CGContextRestoreGState(context);
}
Copy the code

If the attachment type is UIView CALayer, then we need to pass in additional parent view and parent layer respectively: TargetView targetLayer, and then simply add UIView to targetView or CALayer to targetLayer.

2. Click highlight

YYTextHighlightAttributeName : YYTextHighlight
Copy the code

YYTextHighlight contains click and long press callbacks, as well as some property configuration. In YYLabel, the trigger logic is written as follows:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
Copy the code

There are a lot of complicated calculations involved in determining the location of the CGPoint in the rich text that you click on, which are not expanded here.

When YYTextHighlight that should be triggered is found, replace the specific YYTextLine with the highlighted YYTextLine and then redraw. When the hand is released, the normal YYTextLine will be switched.

This is how clicking on highlight works, essentially replacing YYTextLine to update the layout.

Five, asynchronous drawing

For other custom properties, they are basically drawn using the CoreGraphics API, such as borders and shadows. Of course, CoreText comes with many effects, and YYText has made some improvements and extensions.

You can see that every drawing method has a Block to cancel or not cancel, Static void YYTextDrawShadow(YYTextLayout * Layout, CGContextRef Context, CGSize size, CGPoint point, BOOL (^cancel)(void)); . This cancel is used to determine whether the drawing needs to be cancelled. In this way, it can be interrupted at any position of a drawing and cancel useless drawing tasks in time to improve efficiency.

YYText rich text can be drawn asynchronously or in the main thread. The layout class and its related calculation can be created in any thread, and suitable strategies can be selected according to business requirements.

The specific implementation is somewhat complex, so the specific principle of asynchronous drawing can see the author of a special blog: YYAsyncLayer source code analysis: Asynchronous drawing YYAsyncLayer is a component extracted from YYText. The core is a CALayer subclass that supports asynchronous drawing. I believe that after reading the analysis of YYAsyncLayer, I will have a deeper understanding of asynchronous drawing.

After the language

YYText is indeed too heavy, this paper only takes the focus on the basic part of the analysis, in addition to a lot of calculation and logic, interested in your own research.

From the point of view of the code quality, YYText is almost impeccable, the details are very good, the logic code is very refined, the author tried to write part of the logic code, found that the optimization and half a day back to the source code 😂, had to admire the author’s skills.

So far, the author has read YYKit most of the source code, has been repeatedly impressed by the author’s code skills, almost every sentence of code can withstand scrutiny, the author also more profound understanding of the performance optimization, understand the optimization to start from the details.

Suddenly remembered the author and a good friend of the joke. In good times:

“It’s a really neat and exciting technique.”