神奇的 BlocksKit(1):源码分析(上)

高能预警:本篇文章非常长,因为 BlocksKit 的实现还是比较复杂和有意的。这篇文章不是为了剖析 iOS 开发中的 block 的实现以及它是如何组成甚至使用的,如果你想通过这篇文章来了解 block 的实现,它并不能帮到你。

Block 到底是什么?这可能是困扰很多 iOS 初学者的一个问题。如果你在 Google 上搜索类似的问题时,可以查找到几十万条结果,block 在 iOS 开发中有着非常重要的地位,而且它的作用也越来越重要。



概述

这篇文章仅对 BlocksKit v2.2.5 的源代码进行分析,从框架的内部理解下面的功能是如何实现的:

  • 为 NSArray、 NSDictionary 和 NSSet 等集合类型以及对应的可变集合类型 NSMutableArray、NSMutableDictionary 和 NSMutableSet 添加 bk_each: 等方法完成对集合中元素的快速遍历
  • 使用 block 对 NSObject 对象 KVO
  • 为 UIView 对象添加 bk_whenTapped: 等方法快速添加手势
  • 使用 block 替换 UIKit 中的 delegate ,涉及到核心模块 DynamicDelegate。

BlocksKit 框架中包括但不仅限于上述的功能,这篇文章是对 v2.2.5 版本源代码的分析,其它版本的功能不会在本篇文章中具体讨论。

如何提供简洁的遍历方法

BlocksKit 实现的最简单的功能就是为集合类型添加方法遍历集合中的元素。

[@[@1,@2,@3] bk_each:^(id obj) {

NSLog(@"%@",obj);

}];

这段代码非常简单,我们可以使用 enumerateObjectsUsingBlock: 方法替代 bk_each: 方法:

[@[@1,@2,@3] enumerateObjectsUsingBlock:^(id obj,NSUInteger idx,BOOL *stop) {

NSLog(@"%@",obj);

}];

2016-03-05 16:02:57.295 Draveness[10725:453402] 1

2016-03-05 16:02:57.296 Draveness[10725:453402] 2

2016-03-05 16:02:57.297 Draveness[10725:453402] 3

这部分代码的实现也没什么难度:

- (void)bk_each:(void (^)(id obj))block

{

NSParameterAssert(block != nil);

[self enumerateObjectsUsingBlock:^(id obj,NSUInteger idx,BOOL *stop) {

block(obj);

}];

}

它在 block 执行前会判断传进来的 block 是否为空,然后就是调用遍历方法,把数组中的每一个 obj 传给 block。

BlocksKit 在这些集合类中所添加的一些方法在 Ruby、Haskell 等语言中也同样存在。如果你接触过上面的语言,理解这里方法的功能也就更容易了,不过这不是这篇文章关注的主要内容。

// NSArray+BlocksKit.h

- (void)bk_each:(void (^)(id obj))block;

- (void)bk_apply:(void (^)(id obj))block;

- (id)bk_match:(BOOL (^)(id obj))block;

- (NSArray *)bk_select:(BOOL (^)(id obj))block;

- (NSArray *)bk_reject:(BOOL (^)(id obj))block;

- (NSArray *)bk_map:(id (^)(id obj))block;

- (id)bk_reduce:(id)initial withBlock:(id (^)(id sum,id obj))block;

- (NSInteger)bk_reduceInteger:(NSInteger)initial withBlock:(NSInteger(^)(NSInteger result,id obj))block;

- (CGFloat)bk_reduceFloat:(CGFloat)inital withBlock:(CGFloat(^)(CGFloat result,id obj))block;

- (BOOL)bk_any:(BOOL (^)(id obj))block;

- (BOOL)bk_none:(BOOL (^)(id obj))block;

- (BOOL)bk_all:(BOOL (^)(id obj))block;

- (BOOL)bk_corresponds:(NSArray *)list withBlock:(BOOL (^)(id obj1,id obj2))block;

NSObject 上的魔法

NSObject 是 iOS 中的『上帝类』。

在 NSObject 上添加的方法几乎会添加到 Cocoa Touch 中的所有类上,关于 NSObject 的讨论和总共分为以下三部分进行:

  1. AssociatedObject
  2. BlockExecution
  3. BlockObservation

添加 AssociatedObject

经常跟 runtime 打交道的人不可能不知道 AssociatedObject ,当我们想要为一个已经存在的类添加属性时,就需要用到 AssociatedObject 为类添加属性,而 BlocksKit 提供了更简单的方法来实现,不需要新建一个分类。

NSObject *test = [[NSObject alloc] init];

[test bk_associateValue:@"Draveness" withKey:@" name"];

NSLog(@"%@",[test bk_associatedValueForKey:@"name"]);

2016-03-05 16:02:25.761 Draveness[10699:452125] Draveness

这里我们使用了 bk_associateValue:withKey: 和 bk_associatedValueForKey: 两个方法设置和获取 name 对应的值Draveness.

- (void)bk_associateValue:(id)value withKey:(const void *)key

{

objc_setAssociatedObject(self,key,value,OBJC_ASSOCIATION_RETAIN_NONATOMIC);

}

这里的 OBJC_ASSOCIATION_RETAIN_NONATOMIC 表示当前属性为 retain nonatomic 的,还有其它的参数如下:

/**

* Policies related to associative references.

* These are options to objc_setAssociatedObject()

*/

typedef OBJC_ENUM(uintptr_t,objc_AssociationPolicy) {

OBJC_ASSOCIATION_ASSIGN = 0,          /**< Specifies a weak reference to the associated object. */

OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,/**< Specifies a strong reference to the associated object.

*   The association is not made atomically. */

OBJC_ASSOCIATION_COPY_NONATOMIC = 3,  /**< Specifies that the associated object is copied.

*   The association is not made atomically. */

OBJC_ASSOCIATION_RETAIN = 01401,      /**< Specifies a strong reference to the associated object.

*   The association is made atomically. */

OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.

*   The association is made atomically. */

};

上面的这个 NS_ENUM 也没什么好说的,需要注意的是这里没有 weak 属性。

BlocksKit 通过另一种方式实现了『弱属性』:

- (void)bk_weaklyAssociateValue:(__autoreleasing id)value withKey:(const void *)key

{

_BKWeakAssociatedObject *assoc = objc_getAssociatedObject(self,key);

if (!assoc) {

assoc = [_BKWeakAssociatedObject new];

[self bk_associateValue:assoc withKey:key];

}

assoc.value = value;

}

在这里先获取了一个 _BKWeakAssociatedObject 对象 assoc,然后更新这个对象的属性 value。

因为直接使用 AssociatedObject 不能为对象添加弱属性,所以在这里添加了一个对象,然后让这个对象持有一个弱属性:

@interface _BKWeakAssociatedObject : NSObject

@property (nonatomic,weak) id value;

@end

@implementation _BKWeakAssociatedObject

@end

这就是 BlocksKit 实现弱属性的方法,我觉得这个实现的方法还是比较简洁的。

getter 方法的实现也非常类似:

- (id)bk_associatedValueForKey:(const void *)key

{

id value = objc_getAssociatedObject(self,key);

if (value && [value isKindOfClass:[_BKWeakAssociatedObject class]]) {

return [(_BKWeakAssociatedObject *)value value];

}

return value;

}

在任意对象上执行 block

通过这个类提供的一些接口,可以在任意对象上快速执行线程安全、异步的 block,而且这些 block 也可以在执行之前取消。

- (id <NSObject,NSCopying>)bk_performOnQueue:(dispatch_queue_t)queue afterDelay:(NSTimeInterval)delay usingBlock:(void (^)(id obj))block

{

NSParameterAssert(block != nil);

return BKDispatchCancellableBlock(queue,delay,^{

block(self);

});

}

判断 block 是否为空在这里都是细枝末节,这个方法中最关键的也就是它返回了一个可以取消的 block,而这个 block 就是用静态函数 BKDispatchCancellableBlock 生成的。

static id <NSObject,NSCopying> BKDispatchCancellableBlock(dispatch_queue_t queue,NSTimeInterval delay,void(^block)(void)) {

dispatch_time_t time = BKTimeDelay(delay);

#if DISPATCH_CANCELLATION_SUPPORTED

if (BKSupportsDispatchCancellation()) {

dispatch_block_t ret = dispatch_block_create(0,block);

dispatch_after(time,queue,ret);

return ret;

}

#endif

__block BOOL cancelled = NO;

void (^wrapper)(BOOL) = ^(BOOL cancel) {

if (cancel) {

cancelled = YES;

return;

}

if (!cancelled) block();

};

dispatch_after(time,queue,^{

wrapper(NO);

});

return wrapper;

}

这个函数首先会执行 BKSupportsDispatchCancellation 来判断当前平台和版本是否支持使用 GCD 取消 block,当然一般都是支持的:

  • 函数返回的是 YES,那么在 block 被派发到指定队列之后就会返回这个 dispatch_block_t 类型的 block
  • 函数返回的是 NO,那么就会就会手动包装一个可以取消的 block,具体实现的部分如下:

__block BOOL cancelled = NO;

void (^wrapper)(BOOL) = ^(BOOL cancel) {

if (cancel) {

cancelled = YES;

return;

}

if (!cancelled) block();

};

dispatch_after(time,queue,^{

wrapper(NO);

});

return wrapper;

上面这部分代码就先创建一个 wrapper block,然后派发到指定队列,派发到指定队列的这个 block 是一定会执行的,但是怎么取消这个 block 呢?

如果当前 block 没有执行,我们在外面调用一次 wrapper(YES) 时,block 内部的 cancelled 变量就会被设置为 YES,所以 block 就不会执行。

  1. dispatch_after — cancelled = NO
  2. wrapper(YES) — cancelled = YES
  3. wrapper(NO) — cancelled = YES block 不会执行

这是实现取消的关键部分:

+ (void)bk_cancelBlock:(id <NSObject,NSCopying>)block

{

NSParameterAssert(block != nil);

#if DISPATCH_CANCELLATION_SUPPORTED

if (BKSupportsDispatchCancellation()) {

dispatch_block_cancel((dispatch_block_t)block);

return;

}

#endif

void (^wrapper)(BOOL) = (void(^)(BOOL))block;

wrapper(YES);

}

  • GCD 支持取消 block,那么直接调用 dispatch_block_cancel 函数取消 block
  • GCD 不支持取消 block 那么调用一次 wrapper(YES)

使用 Block 封装 KVO

BlocksKit 对 KVO 的封装由两部分组成:

  1. NSObject 的分类负责提供便利方法
  2. 私有类 _BKObserver 具体实现原生的 KVO 功能

提供接口并在 dealloc 时停止 BlockObservation

NSObject+BKBlockObservation 这个分类中的大部分接口都会调用这个方法:

- (void)bk_addObserverForKeyPaths:(NSArray *)keyPaths identifier:(NSString *)identifier options:(NSKeyValueObservingOptions)options context:(BKObserverContext)context task:(id)task

{

#1: 检查参数,省略

#2: 使用神奇的方法在分类中覆写 dealloc

NSMutableDictionary *dict;

_BKObserver *observer = [[_BKObserver alloc] initWithObservee:self keyPaths:keyPaths context:context task:task];

[observer startObservingWithOptions:options];

#3: 惰性初始化 bk_observerBlocks 也就是下面的 dict,省略

dict[identifier] = observer;

}

我们不会在这里讨论 #1、#3 部分,再详细阅读 #2 部分代码之前,先来看一下这个省略了绝大部分细节的核心方法。

使用传入方法的参数创建了一个 _BKObserver 对象,然后调用 startObservingWithOptions: 方法开始 KVO 观测相应的属性,然后以 {identifier,obeserver} 的形式存到字典中保存。

这里实在没什么新意,我们在下一小节中会介绍 startObservingWithOptions: 这一方法。

在分类中调剂 dealloc 方法

这个问题我觉得是非常值得讨论的一个问题,也是我最近在写框架时遇到很棘手的一个问题。

当我们在分类中注册一些通知或者使用 KVO 时,很有可能会找不到注销这些通知的时机。

因为在分类中是无法直接实现 dealloc 方法的。 在 iOS8 以及之前的版本,如果某个对象被释放了,但是刚对象的注册的通知没有被移除,那么当事件再次发生,就会向已经释放的对象发出通知,整个程序就会崩溃。

这里解决的办法就十分的巧妙:

Class classToSwizzle = self.class;

// 获取所有修改过 dealloc 方法的类

NSMutableSet *classes = self.class.bk_observedClassesHash;

// 保证互斥避免 classes 出现难以预测的结果

@synchronized (classes) {

// 获取当前类名,并判断是否修改过 dealloc 方法以减少这部分代码的调用次数

NSString *className = NSStringFromClass(classToSwizzle);

if (![classes containsObject:className]) {

// 这里的 sel_registerName 方法会返回 dealloc 的 selector,因为 dealloc 已经注册过

SEL deallocSelector = sel_registerName("dealloc");

__block void (*originalDealloc)(__unsafe_unretained id,SEL) = NULL;

// 实现新的 dealloc 方法

id newDealloc = ^(__unsafe_unretained id objSelf) {

//在方法 dealloc 之前移除所有 observer

[objSelf bk_removeAllBlockObservers];

if (originalDealloc == NULL) {

// 如果原有的 dealloc 方法没有被找到就会查找父类的 dealloc 方法,调用父类的 dealloc 方法

struct objc_super superInfo = {

.receiver = objSelf,

.super_class = class_getSuperclass(classToSwizzle)

};

void (*msgSend)(struct objc_super *,SEL) = (__typeof__(msgSend))objc_msgSendSuper;

msgSend(&superInfo,deallocSelector);

} else {

// 如果 dealloc 方法被找到就会直接调用该方法,并传入参数

originalDealloc(objSelf,deallocSelector);

}

};

// 构建选择子实现 IMP

IMP newDeallocIMP = imp_implementationWithBlock(newDealloc);

// 向当前类添加方法,但是多半不会成功,因为类已经有 dealloc 方法

if (!class_addMethod(classToSwizzle,deallocSelector,newDeallocIMP,"[email protected]:")) {

// 获取原有 dealloc 实例方法

Method deallocMethod = class_getInstanceMethod(classToSwizzle,deallocSelector);

// 存储 dealloc 方法实现防止在 set 的过程中调用该方法

originalDealloc = (void(*)(__unsafe_unretained id,SEL))method_getImplementation(deallocMethod);

// 重新设置 dealloc 方法的实现,并存储到 originalDealloc 防止方法实现改变

originalDealloc = (void(*)(__unsafe_unretained id,SEL))method_setImplementation(deallocMethod,newDeallocIMP);

}

// 将当前类名添加到已经改变的类的集合中

[classes addObject:className];

}

}

这部分代码的执行顺序如下:

  1. 首先调用 bk_observedClassesHash 类方法获取所有修改过 dealloc 方法的类的集合 classes
  2. 使用 @synchronized (classes) 保证互斥,避免同时修改 classes 集合的类过多出现意料之外的结果
  3. 判断即将调剂方法的类 classToSwizzle 是否调剂过 dealloc 方法
  4. 如果 dealloc 方法没有调剂过,就会通过 sel_registerName(“dealloc”) 方法获取选择子,这行代码并不会真正注册dealloc 选择子而是会获取 dealloc 的选择子,具体原因可以看这个方法的实现 sel_registerName
  5. 在新的 dealloc 中添加移除 Observer 的方法, 再调用原有的 dealloc

id newDealloc = ^(__unsafe_unretained id objSelf) {

[objSelf bk_removeAllBlockObservers];

if (originalDealloc == NULL) {

struct objc_super superInfo = {

.receiver = objSelf,

.super_class = class_getSuperclass(classToSwizzle)

};

void (*msgSend)(struct objc_super *,SEL) = (__typeof__(msgSend))objc_msgSendSuper;

msgSend(&superInfo,deallocSelector);

} else {

originalDealloc(objSelf,deallocSelector);

}

};

IMP newDeallocIMP = imp_implementationWithBlock(newDealloc);

  1. 调用 bk_removeAllBlockObservers 方法移除所有观察者,也就是这段代码的最终目的
  2. 根据 originalDealloc 是否为空,决定是向父类发送消息,还是直接调用 originalDealloc 并传入 objSelf,deallocSelector 作为参数

6.在我们获得了新 dealloc 方法的选择子和 IMP 时,就要改变原有的 dealloc的实现了

if (!class_addMethod(classToSwizzle,deallocSelector,newDeallocIMP,"[email protected]:")) {

// The class already contains a method implementation.

Method deallocMethod = class_getInstanceMethod(classToSwizzle,deallocSelector);

// We need to store original implementation before setting new implementation

// in case method is called at the time of setting.

originalDealloc = (void(*)(__unsafe_unretained id,SEL))method_getImplementation(deallocMethod);

// We need to store original implementation again,in case it just changed.

originalDealloc = (void(*)(__unsafe_unretained id,SEL))method_setImplementation(deallocMethod,newDeallocIMP);

}

  1. 调用 class_addMethod 方法为当前类添加选择子为 dealloc 的方法(当然 99.99% 的可能不会成功)
  2. 获取原有的 dealloc 实例方法
  3. 将原有的实现保存到 originalDealloc 中,防止使用 method_setImplementation 重新设置该方法的过程中调用dealloc 导致无方法可用
  4. 重新设置 dealloc 方法的实现。同样,将实现存储到 originalDealloc 中防止实现改变

关于在分类中调剂 dealloc 方法的这部分到这里就结束了,下一节将继续分析私有类 _BKObserver。

时间: 2024-10-03 08:35:30

神奇的 BlocksKit(1):源码分析(上)的相关文章

memcached学习笔记——存储命令源码分析上

原创文章,转载请标明,谢谢. 上一篇分析过memcached的连接模型,了解memcached是如何高效处理客户端连接,这一篇分析memcached源码中的process_update_command函数,探究memcached客户端的set命令,解读memcached是如何解析客户端文本命令,剖析memcached的内存管理,LRU算法是如何工作等等. 解析客户端文本命令 客户端向memcached server发出set操作,memcached server读取客户端的命令,客户端的连接状态

axios 源码分析(上) 使用方法

axios是一个基于Promise 用于浏览器和 nodejs 的 HTTP 客户端,它可以在浏览器和node环境下运行,在github上已经有六七万个星了,axios使用很方便,很多人在使用他,vue官方也推荐使用axios了,技术这东西还是随主流吧,大家都用肯定有它的特长所在. axios现在最新的版本的是v0.19.0,实现代码也很好理解.我们本节先说一下它的使用方法,然后来分析一下它的实现源码 我们可以使用两种方式来创建一个axios实例: ·一种是直接调用axios(config) ·

知识小罐头08(tomcat8启动源码分析 上)

前面好几篇都说的是一个请求是怎么到servlet中的service方法的,这一篇我们来看看Tomcat8是怎么启动并且初始化其中的组件的? 相信看了前面几篇的小伙伴应该对Tomcat中的各个组件不陌生了,所以我们就可以加快一点速度: 简单说一下Tomcat启动流程,首先是设置一下各种类加载器,然后加载server.xml配置文件,然后初始化Container各个容器,最后就是连接器Connector. 1.批处理文件 双击startup.bat文件启动tomcat,其实内部就是跳转到catali

Hadoop-2.4.1学习之Map任务源码分析(上)

众所周知,Mapper是MapReduce编程模式中最重要的环节之一(另一个当然是Reducer了).在Hadoop-2.x版本中虽然不再有JobTracker和TaskTracker,但Mapper任务的功能却没有变化,本篇文章将结合源代码深入分析Mapper任务时如何执行的,包括处理InputSplit,mapper的输出.对输出分类等.在进行分析之前先明确几个概念:作业.任务.任务的阶段和任务的状态,可以将作业理解为要最终实现的功能或目的,比如统计单词的数量,而任务就是对该作业的拆分,只负

Linux驱动修炼之道-SPI驱动框架源码分析(上)【转】

转自:http://blog.csdn.net/lanmanck/article/details/6895318 SPI驱动架构,以前用过,不过没这个详细,跟各位一起分享: 来自:http://blog.csdn.net/woshixingaaa/article/details/6574215 SPI协议是一种同步的串行数据连接标准,由摩托罗拉公司命名,可工作于全双工模式.相关通讯设备可工作于m/s模式.主设备发起数据帧,允许多个从设备的存在.每个从设备 有独立的片选信号,SPI一般来说是四线串

SOFA 源码分析 —— 服务发布过程

前言 SOFA 包含了 RPC 框架,底层通信框架是 bolt ,基于 Netty 4,今天将通过 SOFA-RPC 源码中的例子,看看他是如何发布一个服务的. 示例代码 下面的代码在 com.alipay.sofa.rpc.quickstart.QuickStartServer 类下. ServerConfig serverConfig = new ServerConfig() .setProtocol("bolt") // 设置一个协议,默认bolt .setPort(9696)

Dubbo 源码分析 - 集群容错之 Directory

1. 简介 前面文章分析了服务的导出与引用过程,从本篇文章开始,我将开始分析 Dubbo 集群容错方面的源码.这部分源码包含四个部分,分别是服务目录 Directory.服务路由 Router.集群 Cluster 和负载均衡 LoadBalance.这几个部分的源码逻辑比较独立,我会分四篇文章进行分析.本篇文章作为集群容错的开篇文章,将和大家一起分析服务目录相关的源码.在进行深入分析之前,我们先来了解一下服务目录是什么.服务目录中存储了一些和服务提供者有关的信息,通过服务目录,服务消费者可获取

Android 上千实例源码分析以及开源分析

Android 上千实例源码分析以及开源分析(百度云分享) 要下载的直接翻到最后吧,项目实例有点多. 首先 介绍几本书籍(下载包中)吧. 01_Android系统概述 02_Android系统的开发综述 03_Android的Linux内核与驱动程序 04_Android的底层库和程序 05_Android的JAVA虚拟机和JAVA环境 06_Android的GUI系统 07_Android的Audio系统 08_Android的Video 输入输出系统 09_Android的多媒体系统 10_

Docker源码分析(七):Docker Container网络 (上)

1.前言(什么是Docker Container) 如今,Docker技术大行其道,大家在尝试以及玩转Docker的同时,肯定离不开一个概念,那就是“容器”或者“Docker Container”.那么我们首先从实现的角度来看看“容器”或者“Docker Container”到底为何物. 逐渐熟悉Docker之后,大家肯定会深深得感受到:应用程序在Docker Container内部的部署与运行非常便捷,只要有Dockerfile,应用一键式的部署运行绝对不是天方夜谭: Docker Conta

Hadoop之HDFS原理及文件上传下载源码分析(上)

HDFS原理 首先说明下,hadoop的各种搭建方式不再介绍,相信各位玩hadoop的同学随便都能搭出来. 楼主的环境: 操作系统:Ubuntu 15.10 hadoop版本:2.7.3 HA:否(随便搭了个伪分布式) 文件上传 下图描述了Client向HDFS上传一个200M大小的日志文件的大致过程: 首先,Client发起文件上传请求,即通过RPC与NameNode建立通讯. NameNode与各DataNode使用心跳机制来获取DataNode信息.NameNode收到Client请求后,