YYWebImage源码分析
YYImage源码
YYModel源码解析
这段时间针对设计架构,解耦以及性能优化相关的知识点看了不少,前两者可以看MVVM和AOP编程思路,最后一个是昨天看的,YY大神的文章之前有看过,但是没有认真研究,这段时间根据源码再结合文章看了下,我真的是服,CF和CT框架都玩的那么6
YY的作者文章也有提到参考的是Facebook的开源框架AsyncDisplayKit,这里还是先看看YYText的源码分析,这东西扩展开来真的消化不了,而且一次性看太多容易吐,太多知识点了,核心思路都差不多,你可以把YYText异步渲染的思路理解为FaceBook ASDK实现的简单思路,把计算排版渲染的任务丢到后台线程,最后回调出来赋值给layer.content显示
- (void)display {
dispatch_async(backgroundQueue, ^{
CGContextRef ctx = CGBitmapContextCreate(...);
// draw in context...
CGImageRef img = CGBitmapContextCreateImage(ctx);
CFRelease(ctx);
dispatch_async(mainQueue, ^{
layer.contents = img;
});
});
}
看吧,大神都说就这个思路了,很简单啊,第一次看他的博客还真信了,就没去看了,现在回头再看
骗子,几千几万行代码,看完之后懵逼了,感觉和看完JSPatch一样,握草,竟然会有这样的人写出这样的代码,感觉自己写的就是一坨屎,赶紧记录下来,学习下知识点
YYText 是YYKit中的一个富文本显示,编辑组件,拥有YYLabel,YYTextView 两个控件。其中YYLabel类似于UILabel,但功能更为强大,支持异步文本渲染,更丰富的效果显示,支持UIImage,UIView, CALayer 文本附件,自定义强调文本范围,支持垂直文本显示等等。YYTextView 类似UITextView,除了兼容UITextView API,扩展了更多的CoreText 效果属性,支持高亮链接,支持自定义内部文本路径形状,支持图片拷贝,粘贴等等。下面是YYText 与 TextKit 的比较图
图还是要有的,这里可以很清楚的看出来,暴露给外部的YYLabel底部有三个核心,另外还有一个最重要的异步绘制Layer
YYAsyncLayer: YYLabel的异步渲染,通过YYAsyncLayerDisplayTask 回调渲染
YYTextLayout: YYLabel的布局管理类,也负责绘制(排版和绘制)管理下面两个
YYTextContainer: YYLabel的布局类 负责布局形状
NSAttributedString+YYText: YYLabel 所有效果属性设置 负责内容
YYAsyncLayer主要负责Hook disPlay方法,Delegate给YYLabel,让YYLabel的YYTextLayout先排版后根据Task的Block传递CGContextRef进行渲染,最后在YYAsyncLayer的主线程赋值content
YYAsyncLayer
是 CALayer的子类,通过设置 YYLabel 类方法 layerClass
返回自定义的 YYAsyncLayer
,重写了父类的 setNeedsDisplay
,(用来标记内容在下一帧到的时候渲染,执行display) display
实现 contents
自定义刷新。YYAsyncLayerDelegate
返回新的刷新任务 newAsyncDisplayTask
用于更新过程回调,返回到 YYLabel 进行文本渲染。其中 YYSentinel
是一个线程安全的原子递增计数器,用于判断更新是否取消。
YYLabel
实现了 YYAsyncLayerDelegate
代理方法 newAsyncDisplayTask
,回调处理3种文本渲染状态willDisplay ,display,didDisplay 。在渲染之前,移除不需要的文本附件,渲染完成后,添加需要的文本附件。渲染时,首先获取YYTextLayout
, 一般包含了 YYTextContainer
和 NSAttributedString
两部分, 分别负责文本展示的形状和内容。不管是渲染时和渲染完成后,最后都需要调用 YYTextLayout
的 核心绘制渲染方法
- (void) drawInContext:(CGContextRef)context
size:(CGSize)size
point:(CGPoint)point
view:(UIView *)view
layer:(CALayer *)layer
debug:(YYTextDebugOption *)debug
cancel:(BOOL (^)(void))cancel{
1.外层调用(YY自己加的NSAttribute属性自己看源码)
非异步
YYLabel *label = [YYLabel new];
label.attributedText = text;
label.width = self.view.width;
label.height = self.view.height - (kiOS7Later ? 64 : 44);
label.top = (kiOS7Later ? 64 : 0);
label.textAlignment = NSTextAlignmentCenter;
label.textVerticalAlignment = YYTextVerticalAlignmentCenter;
label.numberOfLines = 0;
label.backgroundColor = [UIColor colorWithWhite:0.933 alpha:1.000];
[self.view addSubview:label];
异步
YYLabel *label = [YYLabel new];
label.displaysAsynchronously = YES;
label.ignoreCommonProperties = YES;
label.backgroundColor = [UIColor blueColor];
label.origin = CGPointMake(100, 100);
// label.size = CGSizeMake(100, 100); //这段代码貌似会检测上下文,会跑完所有的ui相关才会进行下一步渲染 看看是否有排版
// display只有在size改变的时候才会调用--->渲染
[self.view addSubview:label];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Create attributed string.
NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:@"Some Text"];
text.yy_font = [UIFont systemFontOfSize:16];
text.yy_color = [UIColor grayColor];
[text yy_setColor:[UIColor redColor] range:NSMakeRange(0, 4)];
// Create text container
YYTextContainer *container = [YYTextContainer new];
container.size = CGSizeMake(100, CGFLOAT_MAX);
container.maximumNumberOfRows = 0;
// Generate a text layout.
YYTextLayout *layout = [YYTextLayout layoutWithContainer:container text:text];
dispatch_async(dispatch_get_main_queue(), ^{
// 1. 只有当空间的Size改变的时候才能调用display
// 2. 只有当Label有Size的时候调用排版就会调用Display方法 进行渲染
label.size = layout.textBoundingSize;
label.textLayout = layout;
});
});
2.YYLabel 属性setter内部原理
以attributeText为例,setter方法给内部_innerText私有变量赋值 除了基本的setter赋值以为,这里除了backgroundColor,其他setter方法都执行了下面三句代码
[self _setLayoutNeedUpdate];
[self _endTouch];
[self invalidateIntrinsicContentSize];
第一句表示设置了该属性都会有flag,表示下一帧刷新的时候需要重绘
- (void)_setLayoutNeedRedraw {
[self.layer setNeedsDisplay];
}
+ (Class)layerClass {
return [YYTextAsyncLayer class];
}
这里已经把YYLabel的Layer指向了YYTextAsyncLayer(这里重写了Display),标记后,会在刷新的强制所有指向这个Layer的View调用display,自定义渲染
第二句表示停止事件处理
第三句取消当前的size 在下一次布局中采用 - (CGSize)IntrinsicContentSize返回的新的size进行布局,可以理解为生成YYTextLayout进行排版
根据第一段非异步的调用,这里排版是在主线程的
// https://www.jianshu.com/p/515e12728138
/**
2. Intrinsic Content Size
Intrinsic这个词的意思,「本质的、固有的」。一个View的Intrinsic Content Size意指这个View想要舒舒服服地显示出来,
需要多大的size。对于一个numberOfLines为0的Label来说,它的preferredMaxLayoutWidth确定、font确定,
则它的intrinsicContentSize就定下来了。不是所有的View都有intrinsicContentSize,在自定义的View中,
可以覆盖intrinsicContentSize方法来返回Intrinsic Content Size,并可以通过调用invalidateIntrinsicContentSize来通知
布局系统在下一个布局过程采用新的Intrinsic Content Size。
*/
- (CGSize)intrinsicContentSize {
........省略部分代码
YYTextContainer *container = [_innerContainer copy];
container.size = containerSize;
// 通过container的大小 和 富文本 计算出Layout的大小size 排版
YYTextLayout *layout = [YYTextLayout layoutWithContainer:container text:_innerText];
return layout.textBoundingSize;
}
第二段异步开启之后只要设置两个属性即可,每次执行完setter方法,然后会把之前的content设置为nil,最后还是那执行三句代码
3.SetNeedsDisplay之后调用重写的display方法进行自定义渲染
上面的方法都会进行YYTextLayout的排版和重绘标记,因此就会在刷新的时候进入display
这里有个_displaysAsynchronously属性表示是否开线程进行异步渲染绘制
- (void)display {
super.contents = super.contents;
[self _displayAsync:_displaysAsynchronously];
}
- (void)_displayAsync:(BOOL)async {
__strong id delegate = (id)self.delegate;
YYTextAsyncLayerDisplayTask *task = [delegate newAsyncDisplayTask];
if (async) {
if (task.willDisplay) task.willDisplay(self);
_YYTextSentinel *sentinel = _sentinel;
int32_t value = sentinel.value;
BOOL (^isCancelled)() = ^BOOL() {
return value != sentinel.value;
};
dispatch_async(YYTextAsyncLayerGetDisplayQueue(), ^{
if (isCancelled()) {
CGColorRelease(backgroundColor);
return;
}
UIGraphicsBeginImageContextWithOptions(size, opaque, scale);
CGContextRef context = UIGraphicsGetCurrentContext();
if (opaque && context) {
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();
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);
}
});
});
} else {
[_sentinel increase];
if (task.willDisplay) task.willDisplay(self);
UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.opaque, self.contentsScale);
CGContextRef context = UIGraphicsGetCurrentContext();
if (self.opaque && context) {
CGSize size = self.bounds.size;
size.width *= self.contentsScale;
size.height *= self.contentsScale;
CGContextSaveGState(context); {
if (!self.backgroundColor || CGColorGetAlpha(self.backgroundColor) < 1) {
CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
CGContextAddRect(context, CGRectMake(0, 0, size.width, size.height));
CGContextFillPath(context);
}
if (self.backgroundColor) {
CGContextSetFillColorWithColor(context, self.backgroundColor);
CGContextAddRect(context, CGRectMake(0, 0, size.width, size.height));
CGContextFillPath(context);
}
} CGContextRestoreGState(context);
}
task.display(context, self.bounds.size, ^{return NO;});
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
self.contents = (__bridge id)(image.CGImage);
if (task.didDisplay) task.didDisplay(self, YES);
}
}
这里会执行YYTextAsyncLayer的Delegate方法,YYTextAsyncLayer的Delegate就是YYLabel,执行newAsyncDisplayTask,首先明白YYTextAsyncLayer是重绘的时候拦截display方法,在这里能获取到绘图上下文,把上下文代理给YYLabel,然后YYLabel根据是否更新之前排版好的YYTextLayout,调用drawInContext根据是否异步进行对应线程的绘制,这个时候context就已经被绘制好了,然后YYAsyncLayer就会从上下文中拿出对应的Image,在主线程显示到对应的Layer,而且最终的显示都是通过contents来显示上下文中渲染出来的图片,所以这里先代理给YYLabel,会创建一个task并返回,这里会按顺序执行包含三段Block,
willDisplay
、display
和didDisplay
给Layer显示过程中调用
核心绘制过程会执行displayBlock,传上下文和其他参数进行渲染
(YYTextAsyncLayerDisplayTask )newAsyncDisplayTask {
// 1
YYAsyncLayerDisplayTask task = [YYAsyncLayerDisplayTask new];
// 2
task.willDisplay = ^(CALayer *layer) {
// ...
}
// 3
task.display = ^(CGContextRef context, CGSize size, BOOL (^isCancelled)(void)) {
// ...
}
// 4
task.didDisplay = ^(CALayer *layer, BOOL finished) {// ...}return task;}
[drawLayout drawInContext:context size:size point:point view:nil layer:nil debug:debug cancel:isCancelled];
通过YYLabel执行的代理方法渲染之后,最后在YYTextAsyncLayer中执行self.contents = (__bridge id)(image.CGImage);获取绘制好的内容
上面的代码if下面就是异步绘制,else表示的就是普通的主线程绘制,如果任务执行久就会有很明显的卡顿
这里有个CancelBlock来取消上一次的任务,避免不必要的异步任务执行,通过OSAtomicIncrement32一个全局递增计数器实现点击打开链接
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
第一个是绘制渲染相关的,第二个是排版计算布局相关的。
https://www.jianshu.com/p/7a353962b3d8这个博主写的很好,借鉴下
优化第一步就是把cell高度也预排版在异步线程计算好,然后缓存成一个个拥有数据源的layout对象。
heightForRowAtIndexPath的时候直接取layout里面的cellHeight,cellForRowAtIndexPath渲染的时候,无非就是Lable和ImageView两个,Label可以把内容渲染在后台线程绘制成一张image,在主线程直接从上下文拿出对应的Image即可。ImageView就是在下载完图片的时候异步解压和处理。
这里的思路和YYText不同,如果你看过AsyncDisplayKit,这就是把一套逻辑拆分成两套渲染的方案
第一个是异步渲染,通过重写触发Display的方法开异步线程进行渲染
第二个是通过Runloop Observe来监听 kCFRunLoopBeforeWaiting | kCFRunLoopExit 这两个状态,而且把优先级设置为系统CA动画之后,在执行完系统动画之后再回到主线程去进行绘制渲染
简单看下实现流程
1.属性调用
YYTextView *textView = [YYTextView new];
textView.attributedText = text;
textView.textParser = [YYTextExampleEmailBindingParser new];
textView.size = self.view.size;
textView.textContainerInset = UIEdgeInsetsMake(10, 10, 10, 10);
textView.delegate = self;
if (kiOS7Later) {
textView.keyboardDismissMode = UIScrollViewKeyboardDismissModeInteractive;
}
textView.contentInset = UIEdgeInsetsMake(64, 0, 0, 0);
textView.scrollIndicatorInsets = textView.contentInset;
[self.view addSubview:textView];
self.textView = textView;
[self.textView becomeFirstResponder];
2.Setter内部标记 _commitUpdate
/// Update layout and selection before runloop sleep/end.
- (void)_commitUpdate {
#if !TARGET_INTERFACE_BUILDER
_state.needUpdate = YES;
[[YYTextTransaction transactionWithTarget:self selector:@selector(_updateIfNeeded)] commit];
#else
[self _update];
#endif
}
外部调用的属性设置都会进行标记,这里的标记和上面的YYText不同,不是setNeedsDisplay,而是把自己设置为回调Target,然后把需要执行的SEL传进去 YYTextTransaction (处理Runloop观察者回调)
+ (YYTextTransaction *)transactionWithTarget:(id)target selector:(SEL)selector{
if (!target || !selector) return nil;
YYTextTransaction *t = [YYTextTransaction new];
t.target = target;
t.selector = selector;
return t;
}
上面的类方法先设置好Target和SEL
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:^(YYTextTransaction *transaction, BOOL *stop) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[transaction.target performSelector:transaction.selector];
#pragma clang diagnostic pop
}];
}
static void YYTextTransactionSetup() {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
transactionSet = [NSMutableSet new];
CFRunLoopRef runloop = CFRunLoopGetMain();
CFRunLoopObserverRef observer;
observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(),
kCFRunLoopBeforeWaiting | kCFRunLoopExit,
true, // repeat
0xFFFFFF, // after CATransaction(2000000)
YYRunLoopObserverCallBack, NULL);
CFRunLoopAddObserver(runloop, observer, kCFRunLoopCommonModes);
CFRelease(observer);
});
}
然后通过commit 绑定Runloop的两个状态,在主线程中触发YYRunLoopObserverCallBack 方法,然后让Target执行SEL,也就是最终的方法,如下
/// Update layout and selection view immediately.
- (void)_update {
_state.needUpdate = NO;
[self _updateLayout];
[self _updateSelectionView];
}
之后的排版和渲染就会在YYTextContainerView里面调用DrawRect进行绘制,但是这个是主线程的。
对象创建 (对象的创建会分配内存、调整属性、甚至还有读取文件等操作,比较消耗 CPU 资源。尽量用轻量的对象代替重量的对象,可以对性能有所优化)
对象调整,(当视图层次调整时,UIView、CALayer 之间会出现很多方法调用与通知,所以在优化性能时,应该尽量避免调整视图层次、添加和移除视图。)
对象销毁,(对象的销毁虽然消耗资源不多,但累积起来也是不容忽视的。)
布局,(视图布局的计算是 App 中最为常见的消耗 CPU 资源的地方。如果能在后台线程提前计算好视图布局、并且对视图布局进行缓存,那么这个地方基本就不会产生性能问题了)
Autolayout,(Autolayout 是苹果本身提倡的技术,在大部分情况下也能很好的提升开发效率,但是 Autolayout 对于复杂视图来说常常会产生严重的性能问题)
文本计算,(如果一个界面中包含大量文本(比如微博微信朋友圈等),文本的宽高计算会占用很大一部分资源,并且不可避免)
文本渲染,(屏幕上能看到的所有文本内容控件,包括 UIWebView,在底层都是通过 CoreText 排版、绘制为 Bitmap 显示的。常见的文本控件 (UILabel、UITextView 等),其排版和绘制都是在主线程进行的,当显示大量文本时,CPU 的压力会非常大。对此解决方案只有一个,那就是自定义文本控件,用 TextKit 或最底层的 CoreText 对文本异步绘制。尽管这实现起来非常麻烦,但其带来的优势也非常大,CoreText 对象创建好后,能直接获取文本的宽高等信息,避免了多次计算(调整 UILabel 大小时算一遍、UILabel 绘制时内部再算一遍);CoreText 对象占用内存较少,可以缓存下来以备稍后多次渲染。) 这个其实就是YYText的核心
图片解码,(当你用 UIImage 或 CGImageSource 的那几个方法创建图片时,图片数据并不会立刻解码。图片设置到 UIImageView 或者 CALayer.contents 中去,并且 CALayer 被提交到 GPU 前,CGImage 中的数据才会得到解码。这一步是发生在主线程的,并且不可避免。如果想要绕开这个机制,常见的做法是在后台线程先把图片绘制到 CGBitmapContext 中,然后从 Bitmap 直接创建图片。目前常见的网络图片库都自带这个功能。)
图像绘制等操作图像的绘制通常是指用那些以 CG 开头的方法把图像绘制到画布中,然后从画布创建图片并显示这样一个过程。这个最常见的地方就是 [UIView drawRect:] 里面了。由于 CoreGraphic 方法通常都是线程安全的,所以图像的绘制可以很容易的放到后台线程进行。)
纹理渲染 (所有的 Bitmap,包括图片、文本、栅格化的内容,最终都要由内存提交到显存,绑定为 GPU Texture)
视图混合 (当多个视图(或者说 CALayer)重叠在一起显示时,GPU 会首先把他们混合到一起。)
图形生成 (CALayer 的 border、圆角、阴影、遮罩(mask),CASharpLayer 的矢量图形显示,通常会触发离屏渲染(offscreen rendering),而离屏渲染通常发生在 GPU 中)
上面两条都摘抄自YY博客
按照上面的原因和思路,YYText和YYTextView就处理了这些点,主要包括两个点
1.自定义View 把layerClass指向自定义Layer,然后在Layer上重写Display方法(通过setNeedsDisplay标记触发),自定义实现异步渲染,或者一开始把数据源直接异步排版,再通过渲染的时候进行异步绘制渲染
2.通过属性的设值把任务提交到 YYTextTransaction 类中的全局Set容器存储对象,然后在该类实现Runloop观察,通过观察kCFRunLoopBeforeWaiting | kCFRunLoopExit 两个方法,优先级低于系统动画,然后把排版和渲染的耗时操作在最后执行,保证用户事件和系统动画的流畅,把耗时操作放到Runloop即将进入休眠之前处理,这里不涉及到滑动,因此主线程绘制,把优先级低于Core Animation就能保证用户和系统优先。
这个才是终极的UI页面优化方案的集合体,很多思路都源自于这里
ASDK 对于绘制过程的优化有三部分:分别是栅格化子视图、绘制图像以及绘制文字。
它拦截了视图加入层级时发出的通知 - willMoveToWindow:
方法,然后手动调用 - setNeedsDisplay
,强制所有的 CALayer
执行 - display
更新内容;
然后将上面的操作全部抛入了后台的并发线程中,并在 Runloop 中注册回调,在每次 Runloop 结束时,对已经完成的事务进行 - commit
,以图片的形式直接传回对应的 layer.content
中,完成对内容的更新。
这里ASDK的做法是,触发绘制任务直接提交到后台并发队列里面,然后给一个观察者监听Runloop BeforeWaiting,根据key去队列里面拿会知道的image,如果有就返回主线程显示到layer.content中,但是YYTextView只是一个简单的主线程思路而已,他是把任务提交到set里面,在观察者Runloop即将休眠的时候拿出来在最后的优先级里面进行主线程绘制。
从它的实现来看,确实解决了很多昂贵的 CPU 以及 GPU 操作,有效地加快了视图的绘制和渲染,保证了主线程的流畅执行。
YY的作者也说从中学到了不少知识,他的优化实现方案可以理解为一个更容易理解的版本,有兴趣的可以看看ASDK的源码,这里有个大神介绍的很详细了
灯神
YYkit
CPU 和 GPU
简书介绍
OSAtomicIncrement32
YYText
流程方法介绍