为什么要造一个 UILabel ?( 复习两丫技术 )

本文探讨了为什么需要自定义UILabel,重点在于异步视图(Async View)的实现,利用RunLoop和Async Layer提高性能。详细介绍了如何创建异步图层,通过Run Loop在空闲时进行绘制,以避免影响帧率。此外,还介绍了YYLabel,一个功能强大的文本渲染工具,支持丰富的样式,并且在异步渲染基础上增加了布局管理。最后提出了关于获取CTFrame和CTLine布局信息的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

主要参照 YYKit

YYKit 博大精深,就像少林武功


a, Async View

为了异步 + runloop 空闲时绘制,

因为苹果的 UILabel 性能不够 6

1, Async Layer

思路: UI 操作,必须放在主线程,

然而图形处理,可以放在子线程,

( 开辟图形上下文,进行绘制,取出图片 )

最后一步,放在主线程,就好了

layer.contents = image

Custom View 中,
  • layer 类,重新制定为异步 layer

+ (Class)layerClass {

    return YYAsyncLayer.class;

}
  • 建立绘制任务

创建一个绘制任务,YYAsyncLayerDisplayTask

关键是里面的绘制方法 display

拿到异步图层 layer 创建的图形上下文,

调一下坐标系,( Core Text 的原点,在左下方 )

文本 map 为富文本,

富文本 map 为一帧,

一帧拆分为好多 CTLine,

一行一行地展示



- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask {


    // capture current state to display task

    NSString *text = _text;

    UIFont *fontX = _font;


    YYAsyncLayerDisplayTask *task = [YYAsyncLayerDisplayTask new];

    CGFloat h_h = self.bounds.size.height;

    CGFloat w_w = self.bounds.size.width;

    task.display = ^(CGContextRef context, CGSize size, BOOL(^isCancelled)(void)) {

        if (isCancelled()) return;

        //在这里由于绘制文字会颠倒

        [[NSOperationQueue mainQueue] addOperationWithBlock:^{

            CGContextSetTextMatrix(context, CGAffineTransformIdentity);

            CGContextTranslateCTM(context, 0, h_h);

            CGContextScaleCTM(context, 1.0, -1.0);

        }];

        NSAttributedString* str = [[NSAttributedString alloc] initWithString:text attributes:@{NSFontAttributeName: fontX, NSForegroundColorAttributeName: UIColor.blueColor}];

        CTFramesetterRef ref = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)str);

        CGPathRef path = CGPathCreateWithRect(CGRectMake(0, 0, w_w, 3000), nil);

        CTFrameRef pic = CTFramesetterCreateFrame(ref, CFRangeMake(0, 0), path, nil);

        CFArrayRef arr = CTFrameGetLines(pic);

        NSArray *array = (__bridge NSArray*)arr;

        int i = 0;

        int cnt = (int)array.count;

        CGPoint originsArray[cnt];

        CTFrameGetLineOrigins(pic, CFRangeMake(0, 0), originsArray);

        CGFloat y_y = h_h - 60;

        while (i < cnt) {

            NSLog(@"%f", originsArray[i].y);

            CTLineRef line = (__bridge CTLineRef)(array[i]);

            CGContextSetTextPosition(context, 0, y_y - i * 30);

            CTLineDraw(line, context);

            i += 1;

        }

    };


    return task;

}
Async Layer 中,
  • 启动绘制任务,

先处理下继承关系,

再执行上文提到的绘制任务



- (void)display {

    super.contents = super.contents;

    [self _displayAsync];

}

  • 执行绘制任务,

拿到任务,没有绘制内容,就算了

再判断,自身的大小 ( size ), 合不合规

大小 CGSize(1, 1), 就继续,

  • 子线程,先开辟图形上下文,

再处理背景色,

如果顺利,执行上文的绘制步骤,

从图形上下文中,取出 image, 交给 layer.contents



- (void)_displayAsync{

    __strong id<YYAsyncLayerDelegate> delegate = (id)self.delegate;

    YYAsyncLayerDisplayTask *task = [delegate newAsyncDisplayTask];

    if (!task.display) {

        self.contents = nil;

        return;

    }


        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);

                });

            }

        

            CGColorRelease(backgroundColor);

            return;

        }

        

        dispatch_async(YYAsyncLayerGetDisplayQueue(), ^{

            if (isCancelled()) {

                CGColorRelease(backgroundColor);

                return;

            }

            UIGraphicsBeginImageContextWithOptions(size, opaque, scale);

            CGContextRef context = UIGraphicsGetCurrentContext();

            if (opaque) {

                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();

                return;

            }

            UIImage *image = UIGraphicsGetImageFromCurrentImageContext();

            UIGraphicsEndImageContext();

            if (isCancelled()) {

                return;

            }

            dispatch_async(dispatch_get_main_queue(), ^{

                if (isCancelled() == NO) {

                    self.contents = (__bridge id)(image.CGImage);

                }

            });

        });

}


2, RunLoop
触发

设置样式后,不会立即触发,重绘

先保存起来

- (void)setText:(NSString *)text {

    _text = text.copy;

    [[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];

}

调用异步图层的绘制任务


- (void)contentsNeedUpdated {

    // do update

    [self.layer setNeedsDisplay];

}
事件的保存

先把方法调用的 target 和 action, 保存为对象

YYTransactionSetup(); 单例方法,初始化

把方法调用的对象,添加到集合

@implementation YYTransaction



+ (YYTransaction *)transactionWithTarget:(id)target selector:(SEL)selector{

    if (!target || !selector) return nil;

    YYTransaction *t = [YYTransaction new];

    t.target = target;

    t.selector = selector;

    return t;

}


- (void)commit {

    if (!_target || !_selector) return;

    YYTransactionSetup();

    [transactionSet addObject:self];

}
空闲的时候,把事情给办了,不影响帧率

下面的单例方法,初始化事件任务集合,

run loop 回调中,执行

不干涉, 主 runloop


static NSMutableSet *transactionSet = nil;



static void YYRunLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {

    if (transactionSet.count == 0) return;

    NSSet *currentSet = transactionSet;

    transactionSet = [NSMutableSet new];

    [currentSet enumerateObjectsUsingBlock:^(YYTransaction *transaction, BOOL *stop) {

        [transaction.target performSelector:transaction.selector];

    }];

}



static void YYTransactionSetup() {

    static dispatch_once_t onceToken;

    dispatch_once(&onceToken, ^{

        transactionSet = [NSMutableSet new];

        CFRunLoopRef runloop = CFRunLoopGetMain();

        CFRunLoopObserverRef observer;

        

        observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(),

                                           kCFRunLoopBeforeWaiting | kCFRunLoopExit,

                                           true, 

                                           0xFFFFFF,

                                           YYRunLoopObserverCallBack, NULL);

        CFRunLoopAddObserver(runloop, observer, kCFRunLoopCommonModes);

        CFRelease(observer);

    });

}

b, YYLabel

功能相当强大的渲染工具,

在上文异步渲染的基础上

支持各种样式

增加了一层抽象 YYTextLayout


  • YYLabel 中的绘制任务,

如果需要更新,就创建新的布局 layout ,

如果居中 / 底部对其,处理下 layout 布局

然后 layout 绘制

@implementation YYLabel


- (YYTextAsyncLayerDisplayTask *)newAsyncDisplayTask {


    // create display task

    YYTextAsyncLayerDisplayTask *task = [YYTextAsyncLayerDisplayTask new];


    task.display = ^(CGContextRef context, CGSize size, BOOL (^isCancelled)(void)) {

        if (isCancelled()) return;

        if (text.length == 0) return;

        

        YYTextLayout *drawLayout = layout;

        if (layoutNeedUpdate) {

            layout = [YYTextLayout layoutWithContainer:container text:text];

            shrinkLayout = [YYLabel _shrinkLayoutWithLayout:layout];

            if (isCancelled()) return;

            layoutUpdated = YES;

            drawLayout = shrinkLayout ? shrinkLayout : layout;

        }

        

        CGSize boundingSize = drawLayout.textBoundingSize;

        CGPoint point = CGPointZero;

        if (verticalAlignment == YYTextVerticalAlignmentCenter) {

            if (drawLayout.container.isVerticalForm) {

                point.x = -(size.width - boundingSize.width) * 0.5;

            } else {

                point.y = (size.height - boundingSize.height) * 0.5;

            }

        } else if (verticalAlignment == YYTextVerticalAlignmentBottom) {

            if (drawLayout.container.isVerticalForm) {

                point.x = -(size.width - boundingSize.width);

            } else {

                point.y = (size.height - boundingSize.height);

            }

        }

        point = YYTextCGPointPixelRound(point);

        [drawLayout drawInContext:context size:size point:point view:nil layer:nil debug:debug cancel:isCancelled];

    };



    return task;

}



@end
  • 绘制各种

先绘制背景,

其次画阴影,

下划线,

文字,

图片

边框


@implementation YYTextLayout



- (void)drawInContext:(CGContextRef)context

                 size:(CGSize)size

                point:(CGPoint)point

                 view:(UIView *)view

                layer:(CALayer *)layer

                debug:(YYTextDebugOption *)debug

                cancel:(BOOL (^)(void))cancel{

    @autoreleasepool {

        if (self.needDrawBlockBorder && context) {

            if (cancel && cancel()) return;

            YYTextDrawBlockBorder(self, context, size, point, cancel);

        }

        if (self.needDrawBackgroundBorder && context) {

            if (cancel && cancel()) return;

            YYTextDrawBorder(self, context, size, point, YYTextBorderTypeBackgound, cancel);

        }

        if (self.needDrawShadow && context) {

            if (cancel && cancel()) return;

            YYTextDrawShadow(self, context, size, point, cancel);

        }

        if (self.needDrawUnderline && context) {

            if (cancel && cancel()) return;

            YYTextDrawDecoration(self, context, size, point, YYTextDecorationTypeUnderline, cancel);

        }

        if (self.needDrawText && context) {

            if (cancel && cancel()) return;

            YYTextDrawText(self, context, size, point, cancel);

        }

        if (self.needDrawAttachment && (context || view || layer)) {

            if (cancel && cancel()) return;

            YYTextDrawAttachment(self, context, size, point, view, layer, cancel);

        }

        if (self.needDrawInnerShadow && context) {

            if (cancel && cancel()) return;

            YYTextDrawInnerShadow(self, context, size, point, cancel);

        }

        if (self.needDrawStrikethrough && context) {

            if (cancel && cancel()) return;

            YYTextDrawDecoration(self, context, size, point, YYTextDecorationTypeStrikethrough, cancel);

        }

        if (self.needDrawBorder && context) {

            if (cancel && cancel()) return;

            YYTextDrawBorder(self, context, size, point, YYTextBorderTypeNormal, cancel);

        }

        if (debug.needDrawDebug && context) {

            if (cancel && cancel()) return;

            YYTextDrawDebug(self, context, size, point, debug);

        }

    }

}

  • 进入绘制文字

还有图片

这里的绘制粒度,比较上文,

粒度更加的细

上文是 CTLine,

这里是 CTRun


// 注意条件判断,

// 与保存 / 恢复图形上下文
static void YYTextDrawAttachment(YYTextLayout *layout, CGContextRef context, CGSize size, CGPoint point, UIView *targetView, CALayer *targetLayer, BOOL (^cancel)(void)) {

    

    BOOL isVertical = layout.container.verticalForm;

    CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0;

    

    for (NSUInteger i = 0, max = layout.attachments.count; i < max; i++) {

        YYTextAttachment *a = layout.attachments[i];

        if (!a.content) continue;

        

        UIImage *image = nil;

        UIView *view = nil;

        CALayer *layer = nil;

        if ([a.content isKindOfClass:[UIImage class]]) {

            image = a.content;

        } else if ([a.content isKindOfClass:[UIView class]]) {

            view = a.content;

        } else if ([a.content isKindOfClass:[CALayer class]]) {

            layer = a.content;

        }

        if (!image && !view && !layer) continue;

        if (image && !context) continue;

        if (view && !targetView) continue;

        if (layer && !targetLayer) continue;

        if (cancel && cancel()) break;

        

        CGSize asize = image ? image.size : view ? view.frame.size : layer.frame.size;

        CGRect rect = ((NSValue *)layout.attachmentRects[i]).CGRectValue;

        if (isVertical) {

            rect = UIEdgeInsetsInsetRect(rect, UIEdgeInsetRotateVertical(a.contentInsets));

        } else {

            rect = UIEdgeInsetsInsetRect(rect, a.contentInsets);

        }

        rect = YYTextCGRectFitWithContentMode(rect, asize, a.contentMode);

        rect = YYTextCGRectPixelRound(rect);

        rect = CGRectStandardize(rect);

        rect.origin.x += point.x + verticalOffset;

        rect.origin.y += point.y;

        if (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);

            }

        } else if (view) {

            view.frame = rect;

            [targetView addSubview:view];

        } else if (layer) {

            layer.frame = rect;

            [targetLayer addSublayer:layer];

        }

    }

}
本文,最后一个问题:

上面 layout 的绘制信息,怎么取得的?

先拿文本,创建 CTFrame, CTFrame 中拿到 CTLine 数组

然后遍历每一行,与计算

@implementation YYTextLayout

+ (YYTextLayout *)layoutWithContainer:(YYTextContainer *)container text:(NSAttributedString *)text range:(NSRange)range {


    // ...

    ctSetter = CTFramesetterCreateWithAttributedString((CFTypeRef)text);

    if (!ctSetter) goto fail;

    ctFrame = CTFramesetterCreateFrame(ctSetter, YYTextCFRangeFromNSRange(range), cgPath, (CFTypeRef)frameAttrs);

    if (!ctFrame) goto fail;

    lines = [NSMutableArray new];

    ctLines = CTFrameGetLines(ctFrame);


   // ...
   
   
   
   for (NSUInteger i = 0, max = lines.count; i < max; i++) {

        YYTextLine *line = lines[i];

        if (truncatedLine && line.index == truncatedLine.index) line = truncatedLine;

        if (line.attachments.count > 0) {

            [attachments addObjectsFromArray:line.attachments];

            [attachmentRanges addObjectsFromArray:line.attachmentRanges];

            [attachmentRects addObjectsFromArray:line.attachmentRects];

            for (YYTextAttachment *attachment in line.attachments) {

                if (attachment.content) {

                    [attachmentContentsSet addObject:attachment.content];

                }

            }

        }

    }
    
    // ...
   
}

github repo

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值