基于 CoreText 实现的高性能 UITableView

引起UITableView卡顿比较常见的原因有cell的层级过多、cell中有触发离屏渲染的代码(譬如:cornerRadius、maskToBounds 同时使用)、像素是否对齐、是否使用UITableView自动计算cell高度的方法等。本文将从cell层级出发,以一个仿朋友圈的demo来讲述如何让列表保持顺滑,项目的源码可在文末获得。不可否认的是,过早的优化是魔鬼,请在项目出现性能瓶颈再考虑优化。


首先看看reveal上页面层级的效果图



1、绘制文本


使用core text可以将文本绘制在一个CGContextRef上,最后再通过UIGraphicsGetImageFromCurrentImageContext()生成图片,再将图片赋值给cell.contentView.layer,从而达到减少cell层级的目的。


绘制普通文本(譬如用户昵称)在context上,相关注释在代码里:


(void)drawInContext:(CGContextRef)context withPosition:(CGPoint)p andFont:(UIFont *)font andTextColor:(UIColor *)color andHeight:(float)height andWidth:(float)width lineBreakMode:(CTLineBreakMode)lineBreakMode {

    CGSize size = CGSizeMake(width, height);

    // 翻转坐标系

    CGContextSetTextMatrix(context,CGAffineTransformIdentity);

    CGContextTranslateCTM(context,0,height);

    CGContextScaleCTM(context,1.0,-1.0);

 

    NSMutableDictionary * attributes = [StringAttributes attributeFont:font andTextColor:colorlineBreakMode:lineBreakMode];

 

    // 创建绘制区域(路径)

    CGMutablePathRef path = CGPathCreateMutable();

    CGPathAddRect(path,NULL,CGRectMake(p.x, height-p.y-size.height,(size.width),(size.height)));

 

    // 创建AttributedString

    NSMutableAttributedString *attributedStr = [[NSMutableAttributedString alloc] initWithString:self attributes:attributes];

    CFAttributedStringRef attributedString = (__bridge CFAttributedStringRef)attributedStr;

 

    // 绘制frame

    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributedString);

    CTFrameRef ctframe = CTFramesetterCreateFrame(framesetter, CFRangeMake(0,0),path,NULL);

    CTFrameDraw(ctframe,context);

    CGPathRelease(path);

    CFRelease(framesetter);

    CFRelease(ctframe);

    [[attributedStr mutableString] setString:@""];

    CGContextSetTextMatrix(context,CGAffineTransformIdentity);

    CGContextTranslateCTM(context,0, height);

    CGContextScaleCTM(context,1.0,-1.0);

}


绘制朋友圈内容文本(带链接)在context上,这里我还没有去实现文本多了会折叠的效果,与上面普通文本不同的是这里需要创建带链接的AttributeString和CTLineRef的逐行绘制:


(NSMutableAttributedString *)highlightText:(NSMutableAttributedString *)coloredString{

    // 创建带高亮的AttributedString

    NSString* string = coloredString.string;

    NSRange range = NSMakeRange(0,[string length]);

    NSDataDetector *linkDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:nil];

    NSArray *matches = [linkDetector matchesInString:string options:0 range:range];

 

    for(NSTextCheckingResult* match in matches) {

        [self.ranges addObject:NSStringFromRange(match.range)];

        UIColor *highlightColor = UIColorFromRGB(0x297bc1);

        [coloredString addAttribute:(NSString*)kCTForegroundColorAttributeName

                              value:(id)highlightColor.CGColor range:match.range];

    }

 

    return coloredString;

}

 

(void)drawFramesetter:(CTFramesetterRef)framesetter

       attributedString:(NSAttributedString *)attributedString

              textRange:(CFRange)textRange

                 inRect:(CGRect)rect

                context:(CGContextRef)c {

    CGMutablePathRef path = CGPathCreateMutable();

    CGPathAddRect(path, NULL, rect);

    CTFrameRef frame = CTFramesetterCreateFrame(framesetter, textRange, path, NULL);

 

    CGFloat ContentHeight = CGRectGetHeight(rect);

    CFArrayRef lines = CTFrameGetLines(frame);

    NSInteger numberOfLines = CFArrayGetCount(lines);

 

    CGPoint lineOrigins[numberOfLines];

    CTFrameGetLineOrigins(frame, CFRangeMake(0, numberOfLines), lineOrigins);

 

    // 遍历每一行

    for (CFIndex lineIndex = 0; lineIndex < numberOfLines; lineIndex++) {

        CGPoint lineOrigin = lineOrigins[lineIndex];

        CTLineRef line = CFArrayGetValueAtIndex(lines, lineIndex);

 

        CGFloat descent = 0.0f, ascent = 0.0f, lineLeading = 0.0f;

        CTLineGetTypographicBounds((CTLineRef)line, &ascent, &descent, &lineLeading);

 

        CGFloat penOffset = (CGFloat)CTLineGetPenOffsetForFlush(line, NSTextAlignmentLeft, rect.size.width);

        CGFloat y = lineOrigin.y - descent - self.font.descender;

 

        // 设置每一行位置

        CGContextSetTextPosition(c, penOffset + self.xOffset, y - self.yOffset);

        CTLineDraw(line, c);

 

        // CTRunRef同一行中文本的不同样式,包括颜色、字体等,此处用途为处理链接高亮

        CFArrayRef runs = CTLineGetGlyphRuns(line);

        for (int j = 0; j < CFArrayGetCount(runs); j++) {

            CGFloat runAscent, runDescent, lineLeading1;

 

            CTRunRef run = CFArrayGetValueAtIndex(runs, j);

            NSDictionary *attributes = (__bridge NSDictionary*)CTRunGetAttributes(run);

            // 判断是不是链接

            if (!CGColorEqualToColor((__bridge CGColorRef)([attributes valueForKey:@"CTForegroundColor"]),self.textColor.CGColor)) {

                CFRange range = CTRunGetStringRange(run);

                float offset = CTLineGetOffsetForStringIndex(line, range.location, NULL);

 

                // 得到链接的CGRect

                CGRect runRect;

                runRect.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0,0), &runAscent, &runDescent,&lineLeading1);

                runRect.size.height = self.font.lineHeight;

                runRect.origin.x = lineOrigin.x + offsetself.xOffset;

                runRect.origin.y = lineOrigin.y;

                runRect.origin.y -= descent + self.yOffset;

 

                // 因为坐标系被翻转,链接正常的坐标需要通过CGAffineTransform计算得到

                CGAffineTransform transform = CGAffineTransformMakeTranslation(0, ContentHeight);

                transform = CGAffineTransformScale(transform, 1.f, -1.f);

                CGRect flipRect = CGRectApplyAffineTransform(runRect, transform);

 

                // 保存是链接的CGRect

                NSRange nRange = NSMakeRange(range.location, range.length);

                self.framesDict[NSStringFromRange(nRange)] = [NSValue valueWithCGRect:flipRect];

 

                // 保存同一条链接的不同CGRect,用于点击时背景色处理

                for (NSString *rangeString in self.ranges) {

                    NSRange range = NSRangeFromString(rangeString);

                    if (NSLocationInRange(nRange.location, range)) {

                        NSMutableArray *array = self.relationDict[rangeString];

                        if (array) {

                            [array addObject:NSStringFromCGRect(flipRect)];

                            self.relationDict[rangeString] = array;

                        } else {

                            self.relationDict[rangeString] = [NSMutableArray arrayWithObject:NSStringFromCGRect(flipRect)];

                        }

                    }

                }

 

            }

        }

    }

 

    CFRelease(frame);

    CFRelease(path);

}


上述方法运用起来就是:



这样就完成了文本的显示。


2、显示图片


图片包括用户头像和朋友圈的内容,这里只是将CALayer添加到contentView.layer上,具体做法是继承了CALayer,实现部分功能。


通过链接显示图片:


(void)setContentsWithURLString:(NSString *)urlString {

 

    self.contents = (__bridge id _Nullable)([UIImage imageNamed:@"placeholder"].CGImage);

    @weakify(self)

    SDWebImageManager *manager = [SDWebImageManager sharedManager];

    [manager downloadImageWithURL:[NSURL URLWithString:urlString]

                          options:SDWebImageCacheMemoryOnly

                         progress:nil

                        completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {

                            if (image) {

                                @strongify(self)

                                dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

                                    if (!_observer) {

 

                                        _observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault,kCFRunLoopBeforeWaiting | kCFRunLoopExit, false, POPAnimationApplyRunLoopOrder, ^(CFRunLoopObserverRefobserver, CFRunLoopActivity activity) {

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

                                        });

 

                                        if (_observer) {

                                            CFRunLoopAddObserver(CFRunLoopGetMain(), _observer,  kCFRunLoopCommonModes);

                                        }

                                    }

                                });

                                self.originImage = image;

                            }

                        }];

}


其他比较简单就不展开。


3、显示小视频


之前的一篇文章简单讲了怎么自己做一个播放器,这里就派上用场了。而显示小视频封面图片的CALayer同样在显示小视频的时候可以复用。


这里使用了NSOperationQueue来保障播放视频的流畅性,具体继承NSOperation的VideoDecodeOperation相关代码如下:



解码图片是因为UIImage在界面需要显示的时候才开始解码,这样可能会造成主线程的卡顿,所以在子线程对其进行解压缩处理。


具体的使用:




4、其他


1、触摸交互是覆盖了以下方法实现:


(void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event

(void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event

(void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event


2、页面上FPS的测量是使用了YYKit项目中的YYFPSLabel。


3、测试数据是微博找的,其中小视频是Gif快手。


本文的代码在https://github.com/hawk0620/PYQFeedDemo


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值