美文网首页
YYLabel是如何绘制文本的?

YYLabel是如何绘制文本的?

作者: 迷路的安然和无恙 | 来源:发表于2018-07-15 17:14 被阅读207次
/**
 The text displayed by the label. Default is nil.
 Set a new value to this property also replaces the text in `attributedText`.
 Get the value returns the plain text in `attributedText`.
 */
@property (nullable, nonatomic, copy) NSString *text;

YYLabel给的注释是,给Text赋值之后,Get时返回的实际是一个attributedText
那么YYLabel是怎么在UIView上把文字显示上去的呢?
YYLabeltextSetter如下

/**
*开始添加注释解释 刚开始读代码的时候,很疑惑text被作者赋到哪里去了,实际上
[_innerText replaceCharactersInRange:NSMakeRange(0, _innerText.length) withString:text ? text : @""];
_innerText就是text的接受者,是一个NSMutableAttributerString类型
*/ 
- (void)setText:(NSString *)text {
    if (_text == text || [_text isEqualToString:text]) return;
    _text = text.copy;
    BOOL needAddAttributes = _innerText.length == 0 && text.length > 0;
    [_innerText replaceCharactersInRange:NSMakeRange(0, _innerText.length) withString:text ? text : @""];
// 这里作者是为了解决一个Bug 这是作者的解释 https://github.com/ibireme/YYText/issues/104 
    [_innerText removeDiscontinuousAttributesInRange:NSMakeRange(0, _innerText.length)];
// 是否需要添加属性 这个咱不管他就当 NO
    if (needAddAttributes) {
        _innerText.font = _font;
        _innerText.color = _textColor;
        _innerText.shadow = [self _shadowFromProperties];
        _innerText.alignment = _textAlignment;
        switch (_lineBreakMode) {
            case NSLineBreakByWordWrapping:
            case NSLineBreakByCharWrapping:
            case NSLineBreakByClipping: {
                _innerText.lineBreakMode = _lineBreakMode;
            } break;
            case NSLineBreakByTruncatingHead:
            case NSLineBreakByTruncatingTail:
            case NSLineBreakByTruncatingMiddle: {
                _innerText.lineBreakMode = NSLineBreakByWordWrapping;
            } break;
            default: break;
        }
    }
// 这个里面是一系列的正则,判断串中是否有能够匹配到的、特定条件的内容,匹配到了,会执行特定的操作
// 因为在匹配前会把所有的Attribute移除,所以最后还需要重新对外面给的Attribute添加回去。
    if ([_textParser parseText:_innerText selectedRange:NULL]) {
        // 由此执行的update操作
        [self _updateOuterTextProperties];
    }
// 这里决定这个文字如何显示 才是重点
    if (!_ignoreCommonProperties) {// 该值默认为NO,
// 如果是异步展示
        if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
// 清空当前Layer中Content   CGImageRef image = (__bridge_retained CGImageRef)(self.layer.contents);
    self.layer.contents = nil;
// 实际上当前layer是YYAsyncLayer 其内容最终是以绘制出来的image展示的
            [self _clearContents];
        }

        [self _setLayoutNeedUpdate];// 此方法触发当前Layer的重绘 重点关注此方法
        [self _endTouch];// 暂时忽略此方法
        [self invalidateIntrinsicContentSize];// UIKit API 告知UIView其内容size已发生变化
    }
}

最终的显示是由_setLayoutNeedUpdate决定的,此方法如下:

// 这 
- (void)_setLayoutNeedUpdate {
    _state.layoutNeedUpdate = YES;
// 清空
    [self _clearInnerLayout];
// 重绘
    [self _setLayoutNeedRedraw];
}

_setLayoutNeedRedraw又下

- (void)_setLayoutNeedRedraw {
// 触发layer重绘
    [self.layer setNeedsDisplay];
}

下面开始重点看YYAsyncLayer,在调用setNeedsDisplay之后,layer中都做了什么?
YYAsyncLayer继承自CALayer,在调用setNeedsDisplay时,会触发layer重绘。
作者的注释如下

 The YYAsyncLayer class is a subclass of CALayer used for render contents asynchronously.
 
 @discussion When the layer need update it's contents, it will ask the delegate 
 for a async display task to render the contents in a background queue.

当layer需要更新其内容时,会让其代理在异步线程中渲染其内容
// 是否异步渲染,默认为YES
@property BOOL displaysAsynchronously;
/**
 The YYAsyncLayer's delegate protocol. The delegate of the YYAsyncLayer (typically a UIView)
 must implements the method in this protocol.
 */
@protocol YYAsyncLayerDelegate <NSObject>
@required
/// This method is called to return a new display task when the layer's contents need update.
- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask;
@end

YYAsyncLayer中的内容需要被更新时,会调用该代理方法,返回了一个YYAsyncLayerDisplayTask对象,包含了绘制过程中的几种状态

// 在开始渲染内容之前,会在主线程调用该block 参数就是当前的layer
@property (nullable, nonatomic, copy) void (^willDisplay)(CALayer *layer);
// 在绘制的时候被调用
@property (nullable, nonatomic, copy) void (^display)(CGContextRef context, CGSize size, BOOL(^isCancelled)(void));
// 绘制完成后被调用
@property (nullable, nonatomic, copy) void (^didDisplay)(CALayer *layer, BOOL finished);

再来看YYAsyncLayer中的内部实现,调用setNeedsDisplay会触发CALayer的重绘,那重绘又会调用哪个方法呢?

// 苹果文档解释是 Reloads the content of this layer. 
// Do not call this method directly. 像UIView的draw方法一样,外部不能直接调用该方法
// 但是该方法可以在子类中被重写 YYAsyncLayer中重写了该方法,也就是YYLabel中的文本内容在这里被渲染出来
- (void)display;
// _displayAsync中实现了内容的渲染
- (void)display {
    super.contents = super.contents;
    [self _displayAsync:_displaysAsynchronously];
}
// 开始逐行解析 参数async 为是否异步渲染
- (void)_displayAsync:(BOOL)async {
// 获取delegate
    __strong id<YYAsyncLayerDelegate> delegate = (id)self.delegate;
// 获取YYAsyncLayerDisplayTask对象
    YYAsyncLayerDisplayTask *task = [delegate newAsyncDisplayTask];
// 判断外部是否给与了绘制的上下文等信息 如果没有会进入if语句 直接return。
    if (!task.display) {
// 回到给代理 当前layer层
        if (task.willDisplay) task.willDisplay(self);
// 清空contents
        self.contents = nil;
// 完毕
        if (task.didDisplay) task.didDisplay(self, YES);
        return;
    }
    // 执行到此处 说明外部给了相应的上下文、size的信息 如果是异步绘制会进入下面的if语句
    if (async) {
        if (task.willDisplay) task.willDisplay(self);
        YYSentinel *sentinel = _sentinel;
        int32_t value = sentinel.value;
        BOOL (^isCancelled)() = ^BOOL() {
            return value != sentinel.value;
        };
        CGSize size = self.bounds.size;
        BOOL opaque = self.opaque;
        CGFloat scale = self.contentsScale;
        CGColorRef backgroundColor = (opaque && self.backgroundColor) ? CGColorRetain(self.backgroundColor) : NULL;
        if (size.width < 1 || size.height < 1) {
            CGImageRef image = (__bridge_retained CGImageRef)(self.contents);
            self.contents = nil;
            if (image) {
                dispatch_async(YYAsyncLayerGetReleaseQueue(), ^{
                    CFRelease(image);
                });
            }
            if (task.didDisplay) task.didDisplay(self, YES);
            CGColorRelease(backgroundColor);
            return;
        }
        // 异步绘制从这里开始
        dispatch_async(YYAsyncLayerGetDisplayQueue(), ^{
            if (isCancelled()) {
                CGColorRelease(backgroundColor);
                return;
            }
// 开启图形上下文
            UIGraphicsBeginImageContextWithOptions(size, opaque, scale);
// 获取当前上下文
            CGContextRef context = UIGraphicsGetCurrentContext();
            if (opaque && context) {
// CGContextSaveGState函数的作用是将当前图形状态推入堆栈。
// 之后对图形状态所做的修改会影响随后的描画操作,但不影响存储在堆栈中的拷贝
// 在修改完成后可以通过CGContextRestoreGState函数把堆栈顶部的状态弹出,返回到之前的图形状态。
// 这种推入和弹出的方式是回到之前图形状态的快速方法,避免逐个撤消所有的状态修改
// 这也是将某些状态(比如裁剪路径)恢复到原有设置的唯一方式。
                CGContextSaveGState(context); {
// 没有背景色或有透明度 会默认设置为白色背景色
                    if (!backgroundColor || CGColorGetAlpha(backgroundColor) < 1) {
                        CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
                        CGContextAddRect(context, CGRectMake(0, 0, size.width * scale, size.height * scale));
                        CGContextFillPath(context);
                    }
                    if (backgroundColor) {
                        CGContextSetFillColorWithColor(context, backgroundColor);
                        CGContextAddRect(context, CGRectMake(0, 0, size.width * scale, size.height * scale));
                        CGContextFillPath(context);
                    }
                } CGContextRestoreGState(context);
                CGColorRelease(backgroundColor);
            }
            task.display(context, size, isCancelled);
// 如果被取消 会直接关闭当前上下文 结束绘制
            if (isCancelled()) {
                UIGraphicsEndImageContext();
                dispatch_async(dispatch_get_main_queue(), ^{
                    if (task.didDisplay) task.didDisplay(self, NO);
                });
                return;
            }
// 没有被取消 会获取当前绘制的UIImage
            UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
            UIGraphicsEndImageContext();
            if (isCancelled()) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    if (task.didDisplay) task.didDisplay(self, NO);
                });
                return;
            }
            dispatch_async(dispatch_get_main_queue(), ^{
                if (isCancelled()) {
                    if (task.didDisplay) task.didDisplay(self, NO);
                } else {
// 最终在主线程中 将绘制好的文本图片给layer的contents进行显示 至此文本绘制完成 
// contents为什么可以显示图片?这是苹果文档/** Layer content properties and methods. **/
/* An object providing the contents of the layer, typically a CGImageRef,
 * but may be something else. (For example, NSImage objects are
 * supported on Mac OS X 10.6 and later.) Default value is nil.
 * Animatable. */
                    self.contents = (__bridge id)(image.CGImage);
                    if (task.didDisplay) task.didDisplay(self, YES);
                }
            });
        });
    } else {
// 非异步绘制 就是在主线程执行的上述操作
        [_sentinel increase];
        if (task.willDisplay) task.willDisplay(self);
        UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.opaque, self.contentsScale);
        CGContextRef context = UIGraphicsGetCurrentContext();
        if (self.opaque && context) {
            CGSize size = self.bounds.size;
            size.width *= self.contentsScale;
            size.height *= self.contentsScale;
            CGContextSaveGState(context); {
                if (!self.backgroundColor || CGColorGetAlpha(self.backgroundColor) < 1) {
                    CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
                    CGContextAddRect(context, CGRectMake(0, 0, size.width, size.height));
                    CGContextFillPath(context);
                }
                if (self.backgroundColor) {
                    CGContextSetFillColorWithColor(context, self.backgroundColor);
                    CGContextAddRect(context, CGRectMake(0, 0, size.width, size.height));
                    CGContextFillPath(context);
                }
            } CGContextRestoreGState(context);
        }
        task.display(context, self.bounds.size, ^{return NO;});
        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        self.contents = (__bridge id)(image.CGImage);
        if (task.didDisplay) task.didDisplay(self, YES);
    }
}

上述内容是一行一行代码对YYAsyncLayer进行的分析,从YYLabeltext赋值,到最后显示。
其实到这里,还是会有疑惑。文本呢?为什么没有看到相应的文本的赋值?
这时候需要重新回到YYLabel中看到。

// YYLabel的layer被替换为YYAsynvLayer
+ (Class)layerClass {
    return [YYAsyncLayer class];
}

补充一点,在iOS中每个View在init的时候,会同时创建一个CALayer,该Layer实际展示的内容就是最终UIView所能展示的内容,文本的渲染顺序是UIVIew -> init - > CALayer -> init ->display -> UIView -> drawRect,UIView作为CALayer的代理,如下

   ************
   *  UIView  *    // UIView更像是一个内容显示(Layer)和
                   // 事件(UIResponder)处理的管理者
   ************
   ↓          ↓ 
   ↓          ↓
UIResponder  CALayer

那么文本的最终渲染就是在Layer层上,但是我还是没明白,文本是怎么给Layer并让Layer绘制的呢?在YYAsyncLayer中,只看到了绘制,并没有看到传入的要绘制的内容。

相关文章

网友评论

      本文标题:YYLabel是如何绘制文本的?

      本文链接:https://www.haomeiwen.com/subject/krqupftx.html