当前位置: 首页 > 编程日记 > 正文

基于 CoreText 实现的高性能 UITableView


来源:伯乐在线 - Hawk0620 

如有好文章投稿,请点击 → 这里了解详情

如需转载,发送「转载」二字查看说明


引起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


相关文章:

.net程序员面试题

1&#xff0e; 在web页面中不论显示器的分辨率的大小是800600&#xff0c;还是1024768使得页面都显示在屏幕中间。 答&#xff1a;Margin:auto; 2&#xff0e; 请利用正则表达式验证一个字符串是否为数字串 &#xff08;请用csharp&#xff09;。 1 public static b…

Android深度探索与驱动开发(一)

Android移植可分为两个部分&#xff1a;应用移植和系统移植。应用移植是指将应用程序移植到某一特定硬件平台上。由于不同硬件平台之间的差异&#xff0c;Android SDK API也有可能存在差异&#xff0c;或者将应用程序从低版本Android移植到高版本的Android上。为了保证应用程序…

【廖雪峰Python学习笔记】面向对象高级编程

文章目录为实例和类绑定属性和方法\_\_slots__限制当前class实例&#xff0c;对子类不管用property装饰器&#xff0c;将getter方法变成属性多重继承 —— 一个子类可同时获得多个父类的所有功能[Mixin]定制类枚举类元类为实例和类绑定属性和方法 创建类 class Animal(object…

5分钟 搭建免费个人博客

五分钟倒数已经可以计时了。 三步完成免费个人博客搭建&#xff0c; 这是一篇小白也能看懂的文章&#xff0c;本文主要针对mac OS &#xff0c;Windows 除了软件安装方式和命令有些区别&#xff08;装了git bash也一样&#xff09;&#xff0c;其他基本一样。 你可能会经常看到…

基数排序之算法

一、定义基数排序是一种非比较型整数排序算法&#xff0c;其原理是将整数按位数切割成不同的数字&#xff0c;然后按每个位数分别比较。由于整数也可以表达字符串&#xff08;比如名字或日期&#xff09;和特定格式的浮点数&#xff0c;所以基数排序也不是只能使用于整数。二、…

MyBaties学习记录

typeAliases详解&#xff1a; 类型别名是为 Java 类型设置一个短的名字。它只和 XML 配置有关&#xff0c;存在的意义仅在于用来减少类完全限定名的冗余&#xff1b;&#xff08;也就是为类型设置简称&#xff09; 实例: 通过包名称加上简称调用代替&#xff1b; 注解表示: // …

【廖雪峰Python学习笔记】错误、调试、测试

文章目录错误处理调试单元测试unitcase文档测试错误类型程序编写问题bug – 字符类型错误等用户输入错误 – 输入不符合规定的字符串异常&#xff0c;程序运行时无法预测 – 磁盘满了&#xff0c;无法写 错误处理 错误处理机制&#xff1a;try…except…finally… try运行可…

iOS开发——手势识别器(用手势实现图片旋转和缩小放大)

iOS开发中&#xff0c;除了有关触摸的这组方法来控制用户的手指触控外&#xff0c;还可以用UIGestureRecognize的衍生类来进行判断&#xff0c;方便了开发。 UIGestureRecognize的子类类别有以下几种&#xff1a; UITapGestureRecognizer //轻拍识别器UIPinchGestureRecognize…

直播APP常用动画效果

作者: 落影loyinglin 地址: http://www.jianshu.com/p/a9a201ed3aa8 介绍 记录、总结开发遇到一些问题&#xff0c;大家一起交流学习。 这次带来&#xff0c;对直播APP的常用动画总结。 效果展示 下面是一个很多平台都有的入门豪华礼物动画——烟花。 一个复杂的礼物动画&…

windows8下安装Visual Studio2008

windows8下安装Visual Studio2008是一个比较麻烦的事情&#xff0c;不过经过我3个小时的奋斗终于安装成功了。这是我安装Visual Studio 2008过程中遇到的最复杂的一次。 下面我用图解的方式&#xff0c;一步一步的说明安装Visual Studio2008的过程。 第一步&#xff1a;因为win…

[SDK文档]SDK简介

文档链接&#xff1a;https://docs.growingio.com/docs/sdk-integration SDK工作方式 主要内容&#xff1a;GIO采集内容&#xff0c;数据安全措施&#xff0c;针对数据采集的控制项 JS SDK 添加GIO跟踪代码于<head>...</head> 之间异步加载&#xff0c;不影响网…

mysql主从库配置ps:mysql5.6

1 Mysql cluster版本主从服务器搭建实践 主从的作用&#xff1a;MySQL的主从服务器可以满足同步数据库&#xff0c;同步表&#xff0c;同步表内容&#xff0c;也可以指定仅同步某个数据库或某个表&#xff0c;还可以排除不同步某个数据库某个表。 同步原理&#xff1a;主从数据…

XCODE 4.5 IOS多语言设置

转&#xff1a;http://blog.csdn.net/samuelltk/article/details/8480403 前些天升级到Xcode4.5&#xff0c;现在正在用Xcode4.5IOS6开发项目&#xff0c;当使用国际化时&#xff0c;遇到了一点问题&#xff0c;之前版本Xcode上新建Localizable.strings后&#xff0c;添加语言的…

socket第三方库 AsyncSocket(源码注释解读.转)

作者 OneTea 关注 2016.09.19 11:33* 字数 0 阅读 83评论 0喜欢 1#import <Foundation/Foundation.h> class AsyncSocket;//async异步的 synchro同步 class AsyncReadPacket; class AsyncWritePacket; //extern来说可以理解为扩展吧是这样的是从一个类扩展到另一个类中的…

【每日一学】复杂度分析

文章目录目标什么是数据结构复杂度分析目标 建立时间复杂度、空间复杂度意识&#xff0c;写出高质量的代码能够设计基础架构提高编程技能训练逻辑思维 什么是数据结构 广义&#xff1a;一组数据的存储结构 | 操作数据的一种方法 解决问题&#xff1a;如何更快更省的处理数据…

noip2010提高组3题题解 by rLq

本题地址http://www.luogu.org/problem/show?pid1525 关押罪犯 题目描述 S 城现有两座监狱&#xff0c;一共关押着N 名罪犯&#xff0c;编号分别为1~N。他们之间的关系自然也极不和谐。很多罪犯之间甚至积怨已久&#xff0c;如果客观条件具备则随时可能爆发冲突。我们用“怨气…

hdu 1306(字符串匹配)

题目链接&#xff1a;http://acm.hdu.edu.cn/showproblem.php?pid1306 思路&#xff1a;一开始还以为是求最长公共序列呢。。。仔细一看&#xff0c;orz.....就是求两个串匹配时公共部分字符最多相同的个数。。。 View Code 1 #define _CRT_SECURE_NO_WARNINGS2 #include<…

iOS之使用CoreImage进行人脸识别

更新 &#xff1a;应各位朋友的需求&#xff0c;补上了OC版本的demo&#xff0c; OC版下载地址 另外附上 : swift版下载地址 CoreImage是Cocoa Touch中一个强大的API&#xff0c;也是iOS SDK中的关键部分&#xff0c;不过它经常被忽视。在本篇教程中&#xff0c;我会带大家一起…

[HTTP协议]入门篇

文章目录http的前世今生1. 史前时期2. 创世纪3. 从产生到发展HTTP是什么与HTTP相关的各种概念与HTTP相关的技术TCP/IP协议栈http的前世今生 1. 史前时期 20世纪60年代&#xff0c;美国国防部高等研究计划署ARPA建立ARPA网&#xff0c;四个分布在各地的节点20世纪70年代&#…

CSS中实现DIV容器垂直居中

1.vertical-align&#xff1a;middle 垂直对齐 如表格元素中的<td>、<th>、<caption>等&#xff0c;而像<DIV>、<span>这样的元素是没有valign特性的&#xff0c;因此使用vertical-align对它们不起作用。 2.text-align:center 文本水平居中 一、…

如何制作自己的CocoaPod库

作者 OneTea 关注 2016.12.29 18:02* 字数 848 阅读 102评论 0喜欢 6制作流程图&#xff1a; 流程图1.将代码托管在github上 1.1本地代码 如图&#xff1a; Snip20161228_7.png在github上创建 并上传 Snip20161228_3.png切换到本地项目cd xxx路径后 用git命令行 &#xff08;…

【HTTP协议】域名

1. 域名的出现 IP协议将物理网卡的MAC地址抽象转化为4位数字数字化的IP地址对人不友好&#xff0c;需要友好的域名便于人类识别标记 2. 域名的形式 域名是一个有层次的结构——一串用’.分隔的多个单词【主机名.二级域名.顶级域名】最左边是主机名【eg&#xff1a;www提供万…

iOS 多级下拉菜单

前言 App 常用控件 -- 多级下拉菜单, 如团购类, 房屋类, 对数据进行筛选. 有一级, 二级, 三级, 再多就不会以这种样式,呈现给用户了. 作者就简单聊一下 多级下拉菜单 二级下拉筛选菜单.png一 目标 默认显示一个 TableView, 点击数据后, 添加第二个TableView, 并实现大小变化第二…

fork有啥用

#include <stdio.h>#include <sys/types.h>#include <unistd.h>int main(){ pid_t pid1; pid_t pid2; pid1 fork(); pid2 fork(); printf("pid1:%d, pid2:%d\n", pid1, pid2);}输出&#xff1a;pid1:3411, pid2:3412 //父进…

Html Agility Pack基础类介绍及运用

Html Agility Pack 源码中的类大概有28个左右&#xff0c;其实不算一个很复杂的类库&#xff0c;但它的功能确不弱&#xff0c;为解析DOM已经提供了足够强大的功能支持&#xff0c;可以跟jQuery操作DOM媲美&#xff1a;&#xff09; 基础类和基础方法介绍 Html Agility Pack最常…

【Python自动化测试】setuptools

setuptools Python标准的打包分发工具使用简单的setup.py文件&#xff0c;将Python应用打包 最基础的setup.py文件 #!/usr/bin/env python3 # -*- coding: utf-8 -*- from setuptools import setup setup(nameMyDemo, # 应用名version1.0, # 版本号packages[myd…

企业级-Mysql双主互备高可用负载均衡架构(基于GTID主从复制模式)(原创)

前言&#xff1a;原理与思想这里选用GTID主从复制模式Mysql主从复制模式&#xff0c;是为了更加确保主从复制的正确性、健康性与易配性。这里做的是两服务器A,B各有Mysql实例3310&#xff0c;两个实例间互为主从主从复制模式采用GTID主从复制模式&#xff0c;在服务器A,B上配置…

Objective-C自动生成文档工具:appledoc

作者 iOS_小松哥 关注 2016.12.13 15:47* 字数 919 阅读 727评论 10喜欢 35由于最近琐事比较多&#xff0c;所以好久没有写文章了。今天我们聊一聊Objective-C自动生成文档。 做项目的人多了&#xff0c;就需要文档了。手工写文档是一件苦差事&#xff0c;但是我们也有从源码中…

void main()是错的!

很多人甚至市面上的一些书籍&#xff0c;都使用了void main( )&#xff0c;其实这是错误的。C/C中从来没有定义过void main( )。C之父Bjarne Stroustrup在他的主页上的FAQ中明确地写着The definition void main( ) { /* ... */ } is not and never has been C, nor has it even…

Some tips

VScode自动换行 Code -> Perference -> Setting [ “editor.wordWrap”: “on” ]