iOS兩丫技術之UILabel性能不夠的解決方法
主要參照 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
到此這篇關於iOS兩丫技術之UILabel性能不夠的解決方法的文章就介紹到這瞭,更多相關iOS UILabe內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!