转载

基于 CoreText 实现的高性能 UITableView

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

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

基于 CoreText 实现的高性能 UITableView

然后是9.3系统iPhone5的真机效果

基于 CoreText 实现的高性能 UITableView

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:color lineBreakMode: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 + offset+ self.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); }

上述方法运用起来就是:

- (void)fillData:(CGContextRef)context {     [self.nickname drawInContext:context withPosition:(CGPoint){kTextXOffset, kSpec} andFont:kNicknameFont                     andTextColor:UIColorFromRGB(0x556c95) andHeight:self.nicknameSize.height                         andWidth:self.nicknameSize.width lineBreakMode:kCTLineBreakByTruncatingTail];     [self.drawer setText:self.contentString context:context contentSize:self.contentSize          backgroundColor:[UIColor whiteColor] font:kContentTextFont textColor:[UIColor blackColor]                    block:nil xOffset:kTextXOffset yOffset:kSpec * 2 + self.nicknameSize.height]; } - (void)fillContents:(NSArray *)array {     UIGraphicsBeginImageContextWithOptions(CGSizeMake(self.size.width, self.size.height), YES, 0);     CGContextRef context = UIGraphicsGetCurrentContext();     [UIColorFromRGB(0xffffff) set];     CGContextFillRect(context, CGRectMake(0, 0, self.size.width, self.size.height));     // 获取需要高亮的链接CGRect,并填充背景色     if (array) {         for (NSString *string in array) {             CGRect rect = CGRectFromString(string);             [UIColorFromRGB(0xe5e5e5) set];             CGContextFillRect(context, rect);         }     }     [self fillData:context];     UIImage *temp = UIGraphicsGetImageFromCurrentImageContext();     UIGraphicsEndImageContext();     self.contentView.layer.contents = (__bridge id _Nullable)(temp.CGImage); }

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

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, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {                                             self.contents = (__bridge id _Nullable)(image.CGImage);                                         });                                         if (_observer) {                                             CFRunLoopAddObserver(CFRunLoopGetMain(), _observer,  kCFRunLoopCommonModes);                                         }                                     }                                 });                                 self.originImage = image;                             }                         }]; }

其他比较简单就不展开。

3、显示小视频

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

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

- (void)main {     @autoreleasepool {         if (self.isCancelled) {             _newVideoFrameBlock = nil;             _decodeFinishedBlock = nil;             return;         }         AVURLAsset *asset = [AVURLAsset URLAssetWithURL:[[NSURL alloc] initFileURLWithPath:self.filePath] options:nil];         NSError *error;         AVAssetReader* reader = [[AVAssetReader alloc] initWithAsset:asset error:&error];         if (error) {             return;         }         NSArray* videoTracks = [asset tracksWithMediaType:AVMediaTypeVideo];         AVAssetTrack* videoTrack = [videoTracks objectAtIndex:0];         // 视频播放时,m_pixelFormatType=kCVPixelFormatType_32BGRA         // 其他用途,如视频压缩,m_pixelFormatType=kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange         int m_pixelFormatType = kCVPixelFormatType_32BGRA;         NSDictionary* options = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt: (int)m_pixelFormatType]                                                             forKey:(id)kCVPixelBufferPixelFormatTypeKey];         AVAssetReaderTrackOutput* videoReaderOutput = [[AVAssetReaderTrackOutput alloc]                 initWithTrack:videoTrack outputSettings:options];         [reader addOutput:videoReaderOutput];         [reader startReading];         // 要确保nominalFrameRate>0,之前出现过android拍的0帧视频         if (self.isCancelled) {             _newVideoFrameBlock = nil;             _decodeFinishedBlock = nil;             return;         }         while ([reader status] == AVAssetReaderStatusReading && videoTrack.nominalFrameRate > 0) {             if (self.isCancelled) {                 _newVideoFrameBlock = nil;                 _decodeFinishedBlock = nil;                 return;             }             CMSampleBufferRef sampleBuffer = [videoReaderOutput copyNextSampleBuffer];             CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);             // Lock the base address of the pixel buffer             CVPixelBufferLockBaseAddress(imageBuffer, 0);             // Get the number of bytes per row for the pixel buffer             size_t bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer);             // Get the pixel buffer width and height             size_t width = CVPixelBufferGetWidth(imageBuffer);             size_t height = CVPixelBufferGetHeight(imageBuffer);             //Generate image to edit`             unsigned char* pixel = (unsigned char *)CVPixelBufferGetBaseAddress(imageBuffer);             CGColorSpaceRef colorSpace=CGColorSpaceCreateDeviceRGB();             CGContextRef context=CGBitmapContextCreate(pixel, width, height, 8, bytesPerRow, colorSpace,                                                        kCGBitmapByteOrder32Little|kCGImageAlphaPremultipliedFirst);             if (context != NULL) {                 CGImageRef imageRef = CGBitmapContextCreateImage(context);                 CVPixelBufferUnlockBaseAddress(imageBuffer, 0);                 CGColorSpaceRelease(colorSpace);                 CGContextRelease(context);                 // 解码图片                 size_t width = CGImageGetWidth(imageRef);                 size_t height = CGImageGetHeight(imageRef);                 size_t bitsPerComponent = CGImageGetBitsPerComponent(imageRef);                 // CGImageGetBytesPerRow() calculates incorrectly in iOS 5.0, so defer to CGBitmapContextCreate                 size_t bytesPerRow = 0;                 CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();                 CGColorSpaceModel colorSpaceModel = CGColorSpaceGetModel(colorSpace);                 CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef);                 if (colorSpaceModel == kCGColorSpaceModelRGB) {                     uint32_t alpha = (bitmapInfo & kCGBitmapAlphaInfoMask); #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wassign-enum"                     if (alpha == kCGImageAlphaNone) {                         bitmapInfo &= ~kCGBitmapAlphaInfoMask;                         bitmapInfo |= kCGImageAlphaNoneSkipFirst;                     } else if (!(alpha == kCGImageAlphaNoneSkipFirst || alpha == kCGImageAlphaNoneSkipLast)) {                         bitmapInfo &= ~kCGBitmapAlphaInfoMask;                         bitmapInfo |= kCGImageAlphaPremultipliedFirst;                     } #pragma clang diagnostic pop                 }                 CGContextRef context = CGBitmapContextCreate(NULL, width, height, bitsPerComponent,                                                              bytesPerRow, colorSpace, bitmapInfo);                 CGColorSpaceRelease(colorSpace);                 if (!context) {                     if (self.newVideoFrameBlock) {                         dispatch_async(dispatch_get_main_queue(), ^{                             if (self.isCancelled) {                                 _newVideoFrameBlock = nil;                                 _decodeFinishedBlock = nil;                                 return;                             }                             self.newVideoFrameBlock(imageRef, self.filePath);                             CGImageRelease(imageRef);                         });                     }                 } else {                     CGContextDrawImage(context, CGRectMake(0.0f, 0.0f, width, height), imageRef);                     CGImageRef inflatedImageRef = CGBitmapContextCreateImage(context);                     CGContextRelease(context);                     if (self.newVideoFrameBlock) {                         dispatch_async(dispatch_get_main_queue(), ^{                             if (self.isCancelled) {                                 _newVideoFrameBlock = nil;                                 _decodeFinishedBlock = nil;                                 return;                             }                             self.newVideoFrameBlock(inflatedImageRef, self.filePath);                             CGImageRelease(inflatedImageRef);                         });                     }                     CGImageRelease(imageRef);                 }                 if(sampleBuffer) {                     CMSampleBufferInvalidate(sampleBuffer);                     CFRelease(sampleBuffer);                     sampleBuffer = NULL;                 } else {                     break;                 }             }             [NSThread sleepForTimeInterval:CMTimeGetSeconds(videoTrack.minFrameDuration)];         }         if (self.isCancelled) {             _newVideoFrameBlock = nil;             _decodeFinishedBlock = nil;             return;         }         if (self.decodeFinishedBlock) {             self.decodeFinishedBlock(self.filePath);         }     } }

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

具体的使用:

- (void)playVideoWithFilePath:(NSString *)filePath_ type:(NSString *)type {     @weakify(self)     [[VideoPlayerManager shareInstance] decodeVideo:filePath_                               withVideoPerDataBlock:^(CGImageRef imageData, NSString *filePath) {                                   @strongify(self)                                   if ([type isEqualToString:@"video"]) {                                       if ([filePath isEqualToString:self.filePath]) {                                           [self.sources.firstObject                                                   setContents:(__bridge id _Nullable)(imageData)];                                       }                                   }                               } decodeFinishBlock:^(NSString *filePath){                 [self playVideoWithFilePath:filePath type:type];             }]; }

4、其他

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

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event

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

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

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

本文作者:伯乐在线 - Hawk0620

正文到此结束
Loading...