源码解析之–YYAsyncLayer异步绘制

news/2024/6/30 10:05:29

前言


YYAsyncLayer是异步绘制与显示的工具。最初是从YYKitDemo中接触到这个工具,为了保证列表滚动流畅,将视图绘制、以及图片解码等任务放到后台线程,在YYAsyncLayer之前还是想从YYKitDemo中性能优化说起,虽然些跑题了…


YYKitDemo


对于列表主要对两个代理方法的优化,一个与绘制显示有关,另一个与计算布局有关:


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;


常规逻辑可能觉得应该先调用tableView : cellForRowAtIndexPath :返回UITableViewCell对象,事实上调用顺序是先返回UITableViewCell的高度,是因为UITableView继承自UIScrollView,滑动范围由属性contentSize来确定,UITableView的滑动范围需要通过每一行的UITableViewCell的高度计算确定,复杂cell如果在列表滚动过程中计算可能会造成一定程度的卡顿。


假设有20条数据,当前屏幕显示5条,tableView : heightForRowAtIndexPath :方法会先执行20次返回所有高度并计算出滑动范围,tableView : cellForRowAtIndexPath :执行5次返回当前屏幕显示的cell个数。


TableViewOfPerformanceOptimization.png 


从图中简单看下流程,从网络请求返回JSON数据,将Cell的高度以及内部视图的布局封装为Layout对象,Cell显示之前在异步线程计算好所有布局对象,并存入数组,每次调用tableView: heightForRowAtIndexPath :只需要从数组中取出,可避免重复的布局计算。同时在调用tableView: cellForRowAtIndexPath :对Cell内部视图异步绘制布局,以及图片的异步绘制解码,这里就要说到今天的主角YYAsyncLayer。


YYAsyncLayer


首先介绍里面几个类:


  • YYAsyncLayer:继承自CALayer,绘制、创建绘制线程的部分都在这个类。

  • YYTransaction:用于创建RunloopObserver监听MainRunloop的空闲时间,并将YYTranaction对象存放到集合中。

  • YYSentinel:提供获取当前值的value(只读)属性,以及- (int32_t)increase自增加的方法返回一个新的value值,用于判断异步绘制任务是否被取消的工具。


AsyncDisplay.png


上图是整体异步绘制的实现思路,后面一步步说明。现在假设需要绘制Label,其实是继承自UIView,重写+ (Class)layerClass ,在需要重新绘制的地方调用下面方法,比如setter,layoutSubviews。


+ (Class)layerClass {

    return YYAsyncLayer.class;

}

- (void)setText:(NSString *)text {

    _text = text.copy;

    [[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];

}

- (void)layoutSubviews {

    [super layoutSubviews];

    [[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];

}


YYTransaction有selector、target的属性,selector其实就是contentsNeedUpdated方法,此时并不会立即在后台线程去更新显示,而是将YYTransaction对象本身提交保存在transactionSet的集合中,上图中所示。


+ (YYTransaction *)transactionWithTarget:(id)target selector:(SEL)selector{

    if (!target || !selector) return nil;

    YYTransaction *t = [YYTransaction new];

    t.target = target;

    t.selector = selector;

    return t;

}

- (void)commit {

    if (!_target || !_selector) return;

    YYTransactionSetup();

    [transactionSet addObject:self];

}


同时在YYTransaction.m中注册一个RunloopObserver,监听MainRunloop在kCFRunLoopCommonModes(包含kCFRunLoopDefaultMode、UITrackingRunLoopMode)下的kCFRunLoopBeforeWaiting和kCFRunLoopExit的状态,也就是说在一次Runloop空闲时去执行更新显示的操作。


kCFRunLoopBeforeWaiting:Runloop将要进入休眠。

kCFRunLoopExit:即将退出本次Runloop。


static void YYTransactionSetup() {

    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);

    });

}


下面是RunloopObserver的回调方法,从transactionSet取出transaction对象执行SEL的方法,分发到每一次Runloop执行,避免一次Runloop执行时间太长。


static void YYRunLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {

    if (transactionSet.count == 0) return;

    NSSet *currentSet = transactionSet;

    transactionSet = [NSMutableSet new];

    [currentSet enumerateObjectsUsingBlock:^(YYTransaction *transaction, BOOL *stop) {

#pragma clang diagnostic push

#pragma clang diagnostic ignored "-Warc-performSelector-leaks"

        [transaction.target performSelector:transaction.selector];

#pragma clang diagnostic pop

    }];

}


接下来是异步绘制,这里用了一个比较巧妙的方法处理,当使用GCD时提交大量并发任务到后台线程导致线程被锁住、休眠的情况,创建与程序当前激活CPU数量(activeProcessorCount)相同的串行队列,并限制MAX_QUEUE_COUNT,将队列存放在数组中。


YYAsyncLayer.m有一个方法YYAsyncLayerGetDisplayQueue来获取这个队列用于绘制(这部分YYKit中有独立的工具YYDispatchQueuePool)。创建队列中有一个参数是告诉队列执行任务的服务质量quality of service,在iOS8+之后相比之前系统有所不同。


  • iOS8之前队列优先级:


DISPATCH_QUEUE_PRIORITY_HIGH 2 高优先级

DISPATCH_QUEUE_PRIORITY_DEFAULT 0 默认优先级

DISPATCH_QUEUE_PRIORITY_LOW (-2) 低优先级

DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN 后台优先级


  • iOS8+之后:


QOS_CLASS_USER_INTERACTIVE 0x21, 用户交互(希望尽快完成,不要放太耗时操作)

QOS_CLASS_USER_INITIATED 0x19, 用户期望(不要放太耗时操作)

QOS_CLASS_DEFAULT 0x15, 默认(用来重置对列使用的)

QOS_CLASS_UTILITY 0x11, 实用工具(耗时操作,可以使用这个选项)

QOS_CLASS_BACKGROUND 0x09, 后台

QOS_CLASS_UNSPECIFIED 0x00, 未指定


/// Global display queue, used for content rendering.

static dispatch_queue_t YYAsyncLayerGetDisplayQueue() {

#ifdef YYDispatchQueuePool_h

    return YYDispatchQueueGetForQOS(NSQualityOfServiceUserInitiated);

#else

#define MAX_QUEUE_COUNT 16

 

    static int queueCount;

    static dispatch_queue_t queues[MAX_QUEUE_COUNT];            //存放队列的数组

    static dispatch_once_t onceToken;

    static int32_t counter = 0;

    dispatch_once(&onceToken, ^{

        //程序激活的处理器数量

        queueCount = (int)[NSProcessInfo processInfo].activeProcessorCount;        

        queueCount = queueCount  MAX_QUEUE_COUNT ? MAX_QUEUE_COUNT : queueCount);

        if ([UIDevice currentDevice].systemVersion.floatValue >= 8.0) {

            for (NSUInteger i = 0; i


接下来是关于绘制部分的代码,对外接口YYAsyncLayerDelegate代理中提供- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask方法用于回调绘制的代码,以及是否异步绘制的BOOl类型属性displaysAsynchronously,同时重写CALayer的display 方法来调用绘制的方法- (void)_displayAsync:(BOOL)async。

这里有必要了解关于后台的绘制任务何时会被取消,下面两种情况需要取消,并调用了YYSentinel的increase方法,使value值增加(线程安全):


  • 在视图调用setNeedsDisplay时说明视图的内容需要被更新,将当前的绘制任务取消,需要重新显示。

  • 以及视图被释放调用了dealloc方法。


在YYAsyncLayer.h中定义了YYAsyncLayerDisplayTask类,有三个block属性用于绘制的回调操作,从命名可以看出分别是将要绘制,正在绘制,以及绘制完成的回调,可以从block传入的参数BOOL(^isCancelled)(void)判断当前绘制是否被取消。


@property (nullable, nonatomic, copy) void (^willDisplay)(CALayer *layer);

@property (nullable, nonatomic, copy) void (^display)(CGContextRef context, CGSize size, BOOL(^isCancelled)(void));

@property (nullable, nonatomic, copy) void (^didDisplay)(CALayer *layer, BOOL finished);


下面是部分- (void)_displayAsync:(BOOL)async绘制的代码,主要是一些逻辑判断以及绘制函数,在异步执行之前通过YYAsyncLayerGetDisplayQueue创建的队列,这里通过YYSentinel判断当前的value是否等于之前的值,如果不相等,说明绘制任务被取消了,绘制过程会多次判断是否取消,如果是则return,保证被取消的任务能及时退出,如果绘制完毕则设置图片到layer.contents。


if (async) {  //异步

        if (task.willDisplay) task.willDisplay(self);

        YYSentinel *sentinel = _sentinel;

        int32_t value = sentinel.value;

        NSLog(@" --- %d ---", value);

        //判断当前计数是否等于之前计数

        BOOL (^isCancelled)() = ^BOOL() {

            return value != sentinel.value;

        };

 

        CGSize size = self.bounds.size;

        BOOL opaque = self.opaque;

        CGFloat scale = self.contentsScale;

        CGColorRef backgroundColor = (opaque && self.backgroundColor) ? CGColorRetain(self.backgroundColor) : NULL;

        if (size.width


最后


关于具体使用可以看看程序的示例,这是从YYAsyncLayer中学到的一些技巧,自己还试着简单实现一遍,项目中遇到的性能问题可也以依据这些思路去找到最合适的解决方案,挺想说一句阅读源码是件比较要耐心的事,但确实可以收获颇多。最近也有换环境工作的计划,坐标帝都,欢迎骚扰https://github.com/ShelinShelin


http://lihuaxi.xjx100.cn/news/235268.html

相关文章

排序算法 - 堆排序

堆排序是指利用堆这种数据结构所设计的一种排序算法。 类型:选择排序时间复杂度(最坏):O(nlogn)时间复杂度(最好):O(nlogn)时间复杂度(平均):O(nlogn)空间复杂…

swift3.0阿里百川反馈

闲言少叙 直接上不熟 1.导入自己工程阿里百川demo中的Util文件,并引用其中的头文件 2.剩余就是swift3.0代码.在自己需要的地方书写 (前提是你已经申请了APPKey) 3.代码 //调用意见反馈 func actionOpenFeedback(){ //key self.appKey "此处填写自己申请的key" s…

Notification与多线程

前几天与同事讨论到Notification在多线程下的转发问题,所以就此整理一下。 先来看看官方的文档,是这样写的: In a multithreaded application, notifications are always delivered in the thread in which the notification was posted, whi…

activemq 消息阻塞优化和消息确认机制优化

一、消息阻塞优化 1.activemq消费者在从待消费队列中获取消息是会先进行预读取,默认是1000条(prefetch1000)。这样很容易造成消息积压。 2.可以通过设置prefetch的默认值来调整预读取条数,java代码如下 //设置预读取为1ActiveMQPr…

自己常用网站

在线批量生产iOS图标 http://www.atool.org/ios_logo.ph iOS MVVM RAC从框架到实战 http://www.cnblogs.com/leixu/articles/5344335.html cocoapods安装流程及使用 http://blog.csdn.net/p_igmihu/article/details/52858375 Swift - 使用Alamofire通过HTTPS进行网络请求…

数据库索引-基本知识

为什么80%的码农都做不了架构师?>>> 数据库索引--基本知识 有许多因素会影响数据库性能。最明显的是数据量:您拥有的数据越多,数据库的速度就越慢。虽然有很多方法可以解决性能问题,但主要的解决方案是正确索引数据库…

RxSwift Runtime分析(利用OC消息转发实现IOS消息拦截)原理同ReactiveCocoa

简要介绍:这是一篇介绍IOS消息拦截的文章,来源于对RxSwift源码的分析,其原理是利用Object-c的消息转发(forwardInvocation:)来实现(ReactiveCocoa中也是这个原理,而且是RXSwift借鉴的RAC和MAZeroingWeakRef),阅读本文章…

swift3.0友盟分享

经过(一)的讲解,大家应该可以按照友盟提供的测试账号可以集成友盟分享了,友盟目前集合了18个APP共27种分享,可以授权的有10个App:微信、QQ、新浪微博、腾讯微博、人人网、豆瓣、Facebook、Twitter、Linkedi…