iOS两丫技术之UILabel性能不够的解决方法

来自:网络
时间:2023-07-25
阅读:
目录

主要参照 YYKit

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

Async View

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

因为苹果的 UILabel 性能不够 6

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

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

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

返回顶部
顶部