YYWebImage源码分析

YYImage源码

YYModel源码解析


前言

这段时间针对设计架构,解耦以及性能优化相关的知识点看了不少,前两者可以看MVVMAOP编程思路,最后一个是昨天看的,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 ,(用来标记内容在下一帧到的时候渲染,执行displaydisplay 实现 contents 自定义刷新。YYAsyncLayerDelegate返回新的刷新任务 newAsyncDisplayTask 用于更新过程回调,返回到 YYLabel 进行文本渲染。其中 YYSentinel是一个线程安全的原子递增计数器,用于判断更新是否取消。

 

YYTextLayout(负责排版和渲染)

YYLabel 实现了 YYAsyncLayerDelegate 代理方法 newAsyncDisplayTask,回调处理3种文本渲染状态willDisplay ,display,didDisplay 。在渲染之前,移除不需要的文本附件,渲染完成后,添加需要的文本附件。渲染时,首先获取YYTextLayout, 一般包含了 YYTextContainerNSAttributedString 两部分, 分别负责文本展示的形状和内容。不管是渲染时和渲染完成后,最后都需要调用 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<YYTextAsyncLayerDelegate> 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,

willDisplaydisplaydidDisplay
给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一个全局递增计数器实现点击打开链接

      

 

总结:YYLabel调用核心顺序

TableView优化思路:

- (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就是在下载完图片的时候异步解压和处理。

 

YYTextView

这里的思路和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资源消耗原因

对象创建 (对象的创建会分配内存、调整属性、甚至还有读取文件等操作,比较消耗 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 方法通常都是线程安全的,所以图像的绘制可以很容易的放到后台线程进行。

GPU资源消耗原因

纹理渲染 (所有的 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就能保证用户和系统优先。

 

AsyncDisplayKit

这个才是终极的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

流程方法介绍

 

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐