前言
本文旨在学习YYText的布局&&调用渲染的过程,由于作者使用了CoreText接口和CoreGraphic的知识较多,学习过程中补充了两个库的知识!文章将从基本开始,最后切入YYText的布局和使用离线渲染的原理。离线渲染搭配SDWebImage的使用,能非常有效的处理掉帧问题(屏幕卡顿), 是一个非常值得学习的知识!特别是在没有TableView实现不了的界面的时代!
学习过程
1.CoreText 和 CoreGraphic 的学习总结
2.CoreText的高度计算与普遍的高度计算
3.CoreText文字渲染过程
5.YYText的学习记录
6.YYAsynLayer的学习总结
7.总结
内容
1.CoreText 和 CoreGraphic 的学习总结
两个库的位置
CoreText是一个负责管理文本布局的库,细化到每一个字每一个位置的尺寸,甚至自定义字体。
CoreGraphics是负责渲染的库,是Quartz 2D的一个高级绘图引擎,文本显示就需要它来实现显示
CoreText负责布局调用绘制接口,CoreGraphic负责绘制,下面针对两个库做简单描述,库中函数都是C接口
CoreText
Core Text关键类
富文本,使用NSAttributedString(CFAttributedStringRef)- (instancetype)initWithString:(NSString *)str attributes:(nullable NSDictionary<NSString *, id> *)attrs
第二个参数attributes:包含了详细的文字排版信息。详细的信息可以在<CoreText/CTStringAttributes.h>
或者#<Foundation/NSAttributedString.h>
文件中看到。
根据AttributedString可以创建CTFramesetter(CoreText布局类)
CTFramesetterRef CTFramesetterCreateWithAttributedString(
CFAttributedStringRef attrString ) CT_AVAILABLE(macos(10.5), ios(3.2), watchos(2.0), tvos(9.0));
CTFramesetter
通过CFAttributedString(NSAttributedString)创建,作用相当于一个生产CTFrame的工厂。
CTFrame
每个CTFrame可以看做一个段落 每个CTFrame由多个CTLine组成。可以使用它进行绘制
CTLine
代表一个line 一行文字中可能有多个CTLine组成,单每一个CTLine一定在同一行内。 一个CTLine有多个CTRun组成。也可以使用它进行绘制。
CTRun
一个连续的有着相同的attributes和direction的字形(glyph)的集合,是最小的字形绘制单元。
最后附上字体描述图
CoreGraphic
绘制过程分为CPU阶段,OpenGL ES阶段,GPU阶段。 布局和CoreGraphic的处理属于CPU阶段。这里重点描述。
OpenGL ES是对图层进行取色,采样,生成纹理,绑定数据,生成前后帧缓存。
GPU用来采集图片和形状,运行变换,应用文理和混合,最终把它们输送到屏幕上
Core Graphics绘制
如果对视图实现了drawRect:
或drawLayer:inContext:
方法,或者 CALayerDelegate 的 方法,那么在绘制任何东 西之前都会产生一个巨大的性能开销。为了支持对图层内容的任意绘制,Core Animation必须创建一个内存中等大小的寄宿图片。然后一旦绘制结束之后, 必须把图片数据通过IPC传到渲染服务器。在此基础上,Core Graphics绘制就会变得十分缓慢,所以在一个对性能十分挑剔的场景下这样做十分不好。
2.CoreText的高度计算与普遍的高度计算
普通的高度计算代码
NSDictionary *attribute = @{NSFontAttributeName:[UIFont systemFontOfSize:20]};
CGSize retSize = [str boundingRectWithSize:CGSizeMake(width,100000000) options:NSStringDrawingUsesLineFragmentOrigin attributes:attribute context:nil].size;
return retSize.height;
CoreText高度计算代码
if (!string && string.length == 0) {
return 0;
}
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)string);
CGRect drawingRect = CGRectMake(0, 0, width, 100000);
drawingRect = UIEdgeInsetsInsetRect(drawingRect, UIEdgeInsetsMake(50, 2, 2, 2));
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, drawingRect);
CTFrameRef textFrame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
CGPathRelease(path);
CFRelease(framesetter);
_linesArray = (NSArray *)CTFrameGetLines(textFrame);
CGPoint origins[[_linesArray count]];
CTFrameGetLineOrigins(textFrame, CFRangeMake(0, 0), origins);
int lastLineRow = (int)_linesArray.count - 1;
CGFloat line_y = (CGFloat)origins[lastLineRow].y; //最后一行line的原点y坐标
CGFloat ascent;
CGFloat descent;
CGFloat leading;
CTLineRef lastLine = (__bridge CTLineRef)[_linesArray objectAtIndex:lastLineRow];
CTLineGetTypographicBounds(lastLine, &ascent, &descent, &leading);
CGFloat totalHeight = 100000-line_y + ( ascent + descent);
return totalHeight;
两种高度计算实际测试,400个长度的字符串,系统字体20号,普通高度计算使用时间为13-18ms,而coreText使用2-4ms。从性能上来说,coretext更快!可是有一点需要注意的是,使用CoreText计算的高度比普通计算的高度小,差异在行间距&&字间距,所以,CoreText计算高度前,属性字符串需要先设置行间距和字间距。
3.CoreText文字渲染过程
绘制的过程需要一个CGContextRef对象,我们俗称的画布,画布可以在drawRect:方法通过UIGraphicsGetCurrentContext()获取当前画布进行绘制,而YYText是在YYAsynLayer中触发display(YYLabel调用self.layer setNeedsDisplay触发对应Layer的display方法)创建并获取当前画布。
UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.opaque, self.contentsScale);
CGContextRef context = UIGraphicsGetCurrentContext();
绘制代码如下
CGContextSaveGState(context); {
CGContextTranslateCTM(context, point.x, point.y);
CGContextTranslateCTM(context, 0, size.height);
CGContextScaleCTM(context, 1, -1);
BOOL isVertical = layout.container.verticalForm;
CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0;
NSArray *lines = layout.lines;
for (NSUInteger l = 0, lMax = lines.count; l < lMax; l++) {
YYTextLine *line = lines[l];
if (layout.truncatedLine && layout.truncatedLine.index == line.index) line = layout.truncatedLine;
NSArray *lineRunRanges = line.verticalRotateRange;
CGFloat posX = line.position.x + verticalOffset;
CGFloat posY = size.height - line.position.y;
CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine);
for (NSUInteger r = 0, rMax = CFArrayGetCount(runs); r < rMax; r++) {
CTRunRef run = CFArrayGetValueAtIndex(runs, r);
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextSetTextPosition(context, posX, posY);
YYTextDrawRun(line, run, context, size, isVertical, lineRunRanges[r], verticalOffset);
}
if (cancel && cancel()) break;
}
// Use this to draw frame for test/debug.
// CGContextTranslateCTM(context, verticalOffset, size.height);
// CTFrameDraw(layout.frame, context);
} CGContextRestoreGState(context);
YYTextDrawRun方法里面对Run的属性进行了处理,方法内部还有一段处理过程,但是最终绘制的方法是CoreText开放的绘制函数,函数名称如下:
CTFrameDraw(textFrame, c);
CTLineDraw(line, c);
CTRunDraw(run, c, CFRangeMake(0, 0));
CTFontDrawGlyphs(runFont, glyphs + g, &zeroPoint, 1, context);
代码中每个对象是有关联的,对应上面CoreText关键类的图示,CTLine从CTFrame获取,CTRun从CTLine获取,以此类推。当然绘制前是需要设置位置的CGContextSetTextPosition。最终所有需要绘制的内容能精确地跟计算的高度一直。在这个过程中,使用创建Context方式,需要将layer的content设置一下,这也是YYText的实现方式,
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
self.contents = (__bridge id)(image.CGImage);
上面截图中的self是CALayer或它的子类对象,这几行代码对应创建的代码,和addSubView有相似的地方。也是CoreGraphic 绘制描述中的缩略图,注意代码里,有一个EndImageContext是对应BeginImageContext的,有开就要有关!
到这里,CoreText的编辑&&调用绘制接口和运用CoreGraphic显示到屏幕上,大致的过程描述已经描述完毕,下面我们先使用以上的知识构建一个简单的CoreText绘制过程
上面是简单的绘制过程
1.在ViewController使用CoreText计算字符串的高度,并且添加一个自定义的Label,设置对应的高度。
2.重写自定义Label的+ (Class)layerClass方法,将Label的Layer指向自定义的Layer,自定义的layer在Label添加,或者触发layer setNeedsDisplay方法会触发display方法,从而创建一个ContextRef回调给ViewController。
3.ViewController使用回调的ContextRef,使用CoreText的绘制方法,在CoreText绘制,绘制完成后,Layer获取UIGraphicsGetImageFromCurrentImageContext();将layer的content赋值image,从而完成绘制,显示内容。
以上直接&&简单的一个绘制过程,但是一点都不优雅也不可能让别人可以方便运用,因为在ViewController有太多的代码,计算高度,绘制等!下面我们要学习YYText的设计思想。
5.YYText的学习记录
我认为,主体上,YYText的主要的实现过程与我们的描述是一样的(当然YYText还包含很多功能,包括处理了附件,边框绘制,阴影,属性字符串列别,队列池,异步渲染等,里面的编写风格也是值得学习的)
NSAttributedString提供内容,TextContainer设置排布空间,关系代码如下:
YYTextContainer *container = [YYTextContainer containerWithSize:CGSizeMake(kScreenWidth, kCellHeight)];
YYTextLayout *layout = [YYTextLayout layoutWithContainer:container text:text];
上面代码Layout包含了内容和布局信息,如果赋值给YYLabel的textLayout属性,将不需要再计算文字区域,而直接异步绘制,并且显示!
YYTextLayout对象管理着NSAttributedString和YYTextContainer对象,根据属性字符串的设置和Container的条件通过CoreText的方法计算出了所有的字符串数据,数据包含有很多信息,都是只读属性,有YYLine,以及对应的间距等信息!而YYLine内有CTLine 和其它间距信息!多层关联!
在绘制阶段,绘制的方法也放在了TextLayout内,都是C函数,函数名如下
在我们使用YYText的时候,我们可以从这里做切入口,将不用的代码,属性&&方法进行删减优化,提高项目维护的效率!
下图是YYText中YYLabel的描绘过程
过程中主要工作是TextLayout来完成,但是触发回调的是通过了YYLayer中调用DisplayTask的block回调来完成计算高度和调用渲染接口,YYLabel拥有TextLayout和AttributedString,对外YYLabel只需要开放NSAttributedString即可,这样,别人用起来就方便了,其中YYLayer还有离线渲染的设计,下个话题会描述!
YYText中围绕NSAttributedString+YYText做了很强大的设置字符串样式的简便方法(如阴影,加粗,空心,附件等),应用时,只要针对输入源对应设置即可!基本能满足90%以上的文本绘制!
具体请看YYKit的Demo。
6.YYAsynLayer的学习记录
YYKit中Demo中的Async Display中可查看效果,效果非常明显,未开启异步时,FPS会卡到15左右,离我们60的目标非常远,卡顿也非常明显,体验很差!开启异步绘制后,基本上FPS都稳定在60! 如此巨大的表现,究竟区别在哪里?
区别在哪里?
我认为主要有2个区别:
区别点 | 非异步 | 异步 |
绘制线程 | 主线程 | 子线程(多串行队列并发(例子中设置最大16个队列)) |
绘制任务过程 | 启动渲染就要直到结束 | 内容更新上一个任务就取消 |
首先我们看看YYAsyncLayer对应Demo的执行过程图,然后针对这两点展开学习。
上图流程上与YYLabel绘制的图(文章中上一张蓝色图)其实是一致的,只是Demo中先对属性字符进行了一轮计算赋值,在Demo中,作者有描述,这些计算最好放在子线程!这部分是可以优化的!异步绘制的差异点主要表现在YYAsyncLayerDisplayTask的 3个协议方法里面,分别是 willdisplay, display, 和 didplay, 其中willdisplay和diddisplay是在主线程完成的, 而display的绘制逻辑是在子线程完成,最后在主线程完成Layer的content赋值 self.contents = (__bridge id)(image.CGImage);完成绘制显示!
从上面的描述,我们可以知道,CoreText调用绘制接口的代码放在了子线程,tableView在滚动的时候就不用等待每个Label的绘制代码都完成了才可以滚动,优化了滚动的失帧情况!
这时,还有一个问题,TableView的Cell是一个复用的机制,那么Cell上面的Label也是复用,如果用户剧烈滚动,Cell上的Label对象快速设置内容,虽然绘制发生在子线程,还是会耗损性能,YYText的处理方案是以下代码:
BOOL (^isCancelled)(void) = ^BOOL() {
return value != sentinel.value;
};
sentinel是一个计数器,代码设置每次Layer重绘的时候(触发display方法)都会加1,只要改变绘制内容就加1,但是value在上一个绘制还没完成时还是上一次的值,代码中每次回调前都会调用判断,出现value 和当前已加1的sentinel的value不一样时取消上一次的绘制,回收性能!如下图:
task.display(context, size, isCancelled);
if (isCancelled()) {
UIGraphicsEndImageContext();
dispatch_async(dispatch_get_main_queue(), ^{
if (task.didDisplay) task.didDisplay(self, NO);
});
return;
}
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
if (isCancelled()) {
dispatch_async(dispatch_get_main_queue(), ^{
if (task.didDisplay) task.didDisplay(self, NO);
});
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
if (isCancelled()) {
if (task.didDisplay) task.didDisplay(self, NO);
} else {
self.contents = (__bridge id)(image.CGImage);
if (task.didDisplay) task.didDisplay(self, YES);
}
});
以上就是异步绘制的理解!
7.总结
通过这次学习TText框架的过程,整理了CoreText和CoreGraphic的认识,巩固了框架设计的方式,认识了C函数在OC内的使用过程!回顾了autoreleasepool和drawrect绘画的知识!学习了如isCancelled判断技巧,创建队列池,增加属性字符串内容,函数可先声明变量等技巧!后面将合理使用离线渲染和YYText强大的属性字符串显示功能!
YYTextView流程纪要(比YYLabel多了丰富的输入交互)