读源码涨姿势之优雅KVO完成

NSHashTable & NSMapTable

NSHashTable可以知道为更宽广意义上的NSMutableSet,
与膝下比较NSMapTable主要有如下特征:

  • NSHashTable是可变的, 没有不可变版本
  • 可以弱引用所蕴藏的元素, 当元素释放后会自动被移除
  • 可以在添加元素的时候复制元素后再存放

与NSMutableSet相同之处(与NSMutableArray分化之处)则是:

  • 要素都是无序存放的
  • 根据hashisEqual来对元素进行相比较
  • 不会存放相同的因素

至于相比,大家要先区分==运算符和isEqual方法:

UIColor *color1 = [UIColor colorWithRed:0.5 green:0.5 blue:0.5 alpha:1.0];
UIColor *color2 = [UIColor colorWithRed:0.5 green:0.5 blue:0.5 alpha:1.0];

在地点的言传身教中color1 == color2返回false,
[color1 isEqual:color2]却是再次来到true,
原因在于==是一向的指针相比,明显color1和color2的地方是例外的,而isEqual则是判断其颜色内容是还是不是同样。

看似的还包含NSString isEqualToString / NSDate isEqualToDate

回去NSHashTable中来,
NSHashTable可以随心所欲的囤积指针并且采用指针的唯一性来进行hash同一性检查(检查成员元素是还是不是有再一次)和自查自纠操作(isEqual),
当然大家也足以重写hash/isEqual方法来设定元素比较和十分的规则(其实isEqual是NSObject定义的Protocol).
大家来看下边这些示例:

@interface Person : NSObject

@property (nonatomic,   copy) NSString *name;
@property (nonatomic, strong) NSDate   *birthday;

+ (instancetype)personWithName:(NSString *)name birthday:(NSDate *)date;

@end

咱俩定义一个Person类,其包含name和birthday三个特性,在我们的例行认知下,假诺四个Persion对象的那四个特性是平等的,那么她们不怕同一个人,所以类似下边UIColor的事例,大家须要重写下Person的isEqual函数:

- (BOOL)isEqual:(id)object
 {
    if (nil == object) {
        return NO;
    }
    if (self == object) {
        return YES;
    }
    if (![object isKindOfClass:[Person class]]) {
        return NO;
    }    
    return [self isEqualToPerson:(Person *)object];
}

- (BOOL)isEqualToPerson:(Person *)person
 {
    if (!person) return NO;

    BOOL haveEqualNames     = (!self.name && !person.name) || [self.name isEqualToString:person.name];
    BOOL haveEqualBirthdays = (!self.birthday && !person.birthday) || [self.birthday isEqualToDate:person.birthday];

    return haveEqualNames && haveEqualBirthdays;
}

此间的isEqual函数的兑现分为四步,也是大家引进的best pratice:

  1. 看清目的是或不是为空
  2. 判定是或不是同样对象(内存地址是还是不是等于)
  3. 比方不是同一个class那自然不是一模一样对象
  4. 认清目标的各属性值是不是等于

Person *person1 = [Person personWithName:@"Ryan Jin" birthday:self.date];
Person *person2 = [Person personWithName:@"Ryan Jin" birthday:self.date];

现今借使判断[person1 isEqual person2]就是回去true了,不过怎么觉得缺了点什么,
hash好像还没用到呀,难道不必要重写hash方法吧?

答案当然是要求,当成员被投入到NSHashTable(也席卷NSSet)中时,会被分配一个hash值,以标识该成员在会聚中的位置,通过这么些岗位标识可以极大的提拔成员查找的功能(那也是为何NSHashTable
查找元素的快慢会快于NSArray).

由于NSHashTable/NSSet在添比索素的时候会就行判等操作,当某个元素已经存在时不会再也添加,那个判等的操作包蕴两步:

  1. 七个分子的hash值是不是等于,如不相等则即时判断为不一致因素
  2. 若hash值相等,则再判断isEqual是不是再次来到一致

唯有三个要素的hashisEqual都为同样的状态下才看清为同一对象。好了,精通了那一个规则,大家来重写下Person的hash方法:

- (NSUInteger)hash {
    return [self.name hash] ^ [self.birthday hash]; // best practice
}

鉴于系统的NSString和NSDate在情节同样的情状下会回去相同的hash值,所以那边的特等实践是回到各属性的位或运算。那边须要注意的是无法大致的归来[super hash],
因为默许的hash值为该对象的内存地址,所以地点的person1person2它们的[super hash]是见仁见智的,所以会被判定为分裂的因素,而大家想要完结的是当Person的各属性一致的时候它们即为同一元素。

NSHashTable/NSSet在添法郎素和判断某个元素是或不是留存(member:/containsObject:)时会调用hash方法,
此外NSDictionary在寻找key时(key为非字符串对象),
也会利用hash值来抓实查找效用

大家来看下FBKVO里面用到NSHashTable地方:

NSHashTable *infos = [[NSHashTable alloc] initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];

这边开始化了一个NSHashTable,
存放类型为NSPointerFunctionsWeakMemory即弱持有成员元素且当元素释放后活动从NSHashTable移除。此外,判等连串为NSPointerFunctionsObjectPointerPersonality即直接选取指针地址是不是等于来判定。倘使类型设置为NSPointerFunctionsObjectPersonality则使用地点所描述hash和isEqual来判断。

FBKVO那边使用直接指针地址进行元素比较的原委是单例_FBKVOSharedControllerinfos内部所存放的_FBKVOInfo都是从FBKVOController流传过来的,已经经过了判等操作,不会冒出雷同的靶子,所以_infos拍卖那些_FBKVOInfo要素直接用指针相比较就好了,没必要再去调用hashisEqual方法。另外NSPointerFunctionsWeakMemory安装是为了在_FBKVOInfo放活后活动从_infos里面移除它,
_FBKVOInfo都不存在了,放在其中也没意义了。从那边可以见到FBKVO设计的真正很仔细。

NSMapTable可以了解为更普遍意义上的NSMutableDictionary,
其各项特征和NSHashTable基本相同:

  • NSMapTable是可变的, 没有不可变版本
  • 可以弱引用持有keys和values, 当key或value释放后存储的实体会被移除
  • NSMapTable可以在添加value的时候对value举行复制

NSMapTable *keyToObjectMapping = [NSMapTable mapTableWithKeyOptions:NSMapTableCopyIn
                                                       valueOptions:NSMapTableStrongMemory];

一经按上边这么设置NSMapTable会和NSMutableDictionary用起来完全等同: 复制
key,
并对它的object引用计数加一。同样,我们也来看下FBKVO使用NSMapTable的地点:

- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
{
  self = [super init];
  if (nil != self) {
    _observer = observer;
    NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
    _objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
  }
  return self;
}

那边定义了一个_objectInfosMap: key为被观看的对象,
value则为寄放着_FBKVOInfo的NSMutableSet,
那边在开始化的时候伸张了retainObserved变量用来标记是还是不是将key强引用,具体调用示例如下:

[self.KVOController observe:self.photos                        
                    keyPath:@"count"
                    options:NSKeyValueObservingOptionNew
                      block:^(id observer, id object, NSDictionary *change) {
    // observer -> RJViewController -> __weak self
    // object   -> self.photos    
    // change   -> NSKeyValueChangeKey + FBKVONotificationKeyPathKey    
}];

默许情状下对key(被观望的目标)也就是那边的self.photos做强引用,然而如果我们observe的对象为self本身,那么是不可能做强引用持有的,否则就循环引用了。所以我们在这么些意况下必要retainObserved传入NO,
那也是干什么NSObject+FBKVOController会有一个KVOController和KVOControllerNonRetaining了。

至于_objectInfoMap的value,
因为是NSMutableSet所以直接选取默许的强引用持有,那边我们可能有疑难,为啥value值又选拔了NSMutableSet而不用NSHashTable呢?原因是此处并不须要弱引用持有各样_FBKVOInfo对象,而且多数状态下利用NSMutableSet尤其便利,比如NSHashTable就从未有过enumerateObjectsUsingBlock的枚举方法。而NSSet相比较NSArray的分别是前者无序存放,且使用hash查找元素功效快,可是后者比前者添日币素的快慢快很多,所以在甄选接纳哪个容器的时候须求基于具体情形来抉择。

FBKVO对_FBKVOInfo的hash和isEqual进行了重写,以keyPath来进行判等。所以那边对于value设置为NSPointerFunctionsObjectPersonality以hash/isEqual举行判断,而key值(被观望者)则设置为NSPointerFunctionsObjectPointerPersonality直接以指针地址做判定。

终极大家平素引用Mattt大神对于怎么样时候用NSMapTable几时用NSDictionary的注解来收场这一小节:

As always, it’s important to remember that programming is not about
being clever: always approach a problem from the highest viable level
of abstraction. NSSet and NSDictionary are great classes. For 99% of
problems, they are undoubtedly the correct tool for the job. If,
however, your problem has any of the particular memory management
constraints described above, then NSHashTable & NSMapTable may be
worth a look.

宏定义

万般在添加观看者的时候都要求指定一个观看路径(keyPath),
那几个路子是一贯以字符串的法门提供的,比如大家有个类RJPhoto的对象photo,
要求考察它的name路径:

[self.KVOController observe:photo keyPath:@"name"];

若果字符串拼写错误,或者被observe的目的没有name其一特性,编译器并不会报错,唯有等到运行时才会意识难题。大家来看下FBKVOController是怎么通过宏定义来解决这么些题材的:

#define FBKVOKeyPath(KEYPATH) \
@(((void)(NO && ((void)KEYPATH, NO)), \
({ const char *fbkvokeypath = strchr(#KEYPATH, '.'); NSCAssert(fbkvokeypath, @"Provided key path is invalid."); fbkvokeypath + 1; })))

#define FBKVOClassKeyPath(CLASS, KEYPATH) \
@(((void)(NO && ((void)((CLASS *)(nil)).KEYPATH, NO)), #KEYPATH))

有了那五个宏,被观看者的keyPath可以经过宏传入,其利益在于该宏会举行编译检查和代码提醒,即使keyPath不存在或者拼写错误,会唤醒错误。

[self.KVOController observe:photo keyPath:FBKVOKeyPath(photo.name)];
[self.KVOController observe:photo keyPath:FBKVOClassKeyPath(RJPhoto, name)];

地点的宏是如何是好到编译检查和代码提示的吗?我们先分析第四个绝相比较较复杂的宏FBKVOClassKeyPath,
其总体是一个C语言的逗号表明式,逗号表明式的格式: e.g.
int a = (b, c);逗号表明式取后边的值,故而a将被赋值成c,
此时b在赋值运算中就被忽略了,没有被运用,所以编译器会交到警告,为了破除这些warning大家需要在b前边加上(void)做个品类强转操作。

逗号表达式的前项和NO举行了与操作,这几个重大是为着让编译器忽略第三个值,因为我们实在赋值的是表达式前边的值。预编译的时候看见了NO,
就会急忙的跳过判断标准。我猜你见到那儿肯定会意外了,既然要不经意,那干什么还要用个逗号表明式呢,直接赋值不就好了?

此间根本是对传播的首先个参数CLASS的对象(CLASS *)(nil)和第三个正要输入的KEYPATH做了.操作,那也多亏为什么输入第三个参数时编辑器会付出正确的代码提示(只要是作为表明式的一有的,
Xcode自动会提示)。倘诺传入的KEYPATH不是CLASS对象的属性,那么(CLASS *)(nil).KEYPATH就不是一个合法的表明式,所以自然编译就不会由此了。

FBKVOKeyPath接受一个参数,前半段和上面是如出一辙的,差其他是逗号表达式的后一段strchr(# photo.name, '.') + 1,
函数strchar是C语言中的函数,用来探寻某字符在字符串中首次出现的岗位,那里用来在photo.name(注意眼前加了#字符串化)中找找.并发的职分,再添加1就是回来.后面keyPath的地点了。也就是strchr('photo.name', '.')回到的是一个C字符串,那几个字符串从找到'photo.name'中为'.'的字符最先未来,即'name'.

那边还用到了断言宏NSCAssert(x, y), xBOOL值, y为字符串类型,
xNO时暴发断言退出并打印y字符串内容.
须求留意的是NSCAssert在C语言函数下采纳,
NSAssert则是Objective-C函数下利用

关于宏定义的详细分解以及地点所述的切近宏定义的应用和剖析,可以参考作者的博文Hello,
宏定义魔法世界

FBKVOController是脸谱开源的接口设计优雅的KVO框架。小编研读之后确实受益匪浅,本着学以致用的规格,作者借鉴其接口设计的点子完结了一套完整的小红点(推送新闻)解决方案RJBadgeKit,
有趣味的同窗可以参照一下。

若是说书籍是全人类前行的阶梯,那么完美的开源代码就是程序员进步的大桥。研读源码可以学学其中的框架和格局,
代码技巧,
算法等,然后不断统计运用,最终那么些会成为自己的事物,编程水平自然也增强了。

由于近年来一度有众多有关FBKVOController源码分析的博文,本文种尝试从别的一个角度,以提炼和剖析具体知识点的点子来总计FBKVOController中大家得以借鉴和读书的地方。

线程锁

FBKVO使用pthread_mutex_t作为线程锁,关于iOS各类线程锁的牵线可以参考这篇博文。大家一直来看下FBKVO使用的其中一个地点:

- (void)_unobserveAll
{
  // lock
  pthread_mutex_lock(&_lock);

  NSMapTable *objectInfoMaps = [_objectInfosMap copy];

  // clear table and map
  [_objectInfosMap removeAllObjects];

  // unlock
  pthread_mutex_unlock(&_lock);

  _FBKVOSharedController *shareController = [_FBKVOSharedController sharedController];

  for (id object in objectInfoMaps) {
    // unobserve each registered object and infos
    NSSet *infos = [objectInfoMaps objectForKey:object];
    [shareController unobserve:object infos:infos];
  }
}

锁的主导尺度是有所对公共数据拍卖的地点都急需加锁,上边的代码中_objectInfosMap为大局的NSMapTable,
对其修改操作(Add/Remove)都急需加锁,
FBKVO那边的操作很值得借鉴,先拷贝一份临时变量,然后将_objectInfosMap清空,这一步在锁中间操作,之后被拷贝出的那份非全局或者说非共享变量再去做相应的存续操作。

那样的话尽管有多个线程访问_unobserveAll也不会有其它难点,因为唯有首先个线程会访问到_objectInfosMap,
第一个线程等解锁后再去拜谒时_objectInfosMap早就为空了,拷贝的对象objectInfoMaps也理所当然为空,所以锁并不须要加满整个_unobserveAll函数范围。

万一急需选择互斥类型的pthread_mutex_t锁,比如在递归函数中加锁,那须要将pthread_mutex_t伊始化为互斥类型:

pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&_mutex, &attr);
pthread_mutexattr_destroy(&attr);

锁使用完后记得要在dealloc里面销毁掉:

- (void)dealloc {
    pthread_mutex_destroy(&_mutex);
}

数据结构

KVOController是框架的对外接口类,作为KVO的领导,其所有了现阶段观望者对象和被阅览者的KVO音讯。
观望者对象以weak美学原理,属性存储在_observer中,而_objectInfosMap中校被观望者以key举行仓储,
value则存储了相应的_ FBKVOInfo聚集(图片引用自Draveness的博文)。

KVOController

FBKVO为每一个被observe的对象都生成了一个_ FBKVOInfo目标,该对象存储了富有与KVO相关的音讯,包罗路径,回调等等。

FBKVOInfo

FBKVO的调用流程如下图所示,
FBKVOController的听从只是增进相应的被观看者记录,以及变更对应的FBKVOInfo音信,最后会由FBKVOSharedController
那几个单例来调用系统KVO方法达成对性能的监听,并且在回调方法少将事件分发给
KVO 的观看者。

调用流程

FBKVO中还有一个相比较有趣的地点是用_来分别内部接口和表面接口:

- (void)_unobserve:(id)object info:(_FBKVOInfo *)info // -> private method
- (void)unobserve:(nullable id)object keyPath:(NSString *)keyPath // -> public method

席卷类名也是如此:

@interface FBKVOController : NSObject        // -> public 
@interface _FBKVOInfo : NSObject             // -> internal 
@interface _FBKVOSharedController : NSObject // -> internal 

FBKVO的代码量就算不多,但其框架流程,接口设计和代码中利用到的细小技术点确实极具水平,希望本文总计和提纯的各个姿势可以让大家有部分收获,也欢迎大家留言研究。完。

DEBUG描述

DEBUG描述(debugDescription)其实和description是一样的效率,只是debugDescription是在Xcode控制台里使用po命令的时候调用呈现的。倘诺没有落到实处debugDescription方法,那么打印该对象的时候偏偏显示内存地址,而不会浮现该目的的逐条属性值。

- (NSString *)debugDescription
{
  NSMutableString *s = [NSMutableString stringWithFormat:@"<%@:%p keyPath:%@", NSStringFromClass([self class]), self, _keyPath];
  if (0 != _options) {
    [s appendFormat:@" options:%@", describe_options(_options)];
  }
  if (NULL != _action) {
    [s appendFormat:@" action:%@", NSStringFromSelector(_action)];
  }
  if (NULL != _context) {
    [s appendFormat:@" context:%p", _context];
  }
  if (NULL != _block) {
    [s appendFormat:@" block:%p", _block];
  }
  [s appendString:@">"];
  return s;
}

上面是FBKVO实现的_FBKVOInfo的debugDescription,
将逐条属性值拼接成字符串展现出来。那如若某个对象有N多少个属性,这样一个个拼接会卓殊麻烦,那种气象下可以接纳runtime来动态获取属性并重回:

- (NSString *)debugDescription // prefer super class
{ 
    NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];

    // fetch class's all properties
    uint count;
    objc_property_t *properties = class_copyPropertyList([self class], &count);

    // loop to get each property via KVC
    for (int i = 0; i<count; i++) {
        objc_property_t property = properties[I];
        NSString *name = @(property_getName(property));
        id value = [self valueForKey:name]?:@"nil"; // default nil string
        [dictionary setObject:value forKey:name]; // add to dicionary
    }
    free(properties);

    return [NSString stringWithFormat:@"<%@-%p> -- %@",[self class],self,dictionary];
}

自释放

FBKVOController通过自释放的机制来贯彻observer的机关移除,具体来说就是给observer添加一个FBKVOController的积极分子变量,比如:

#import "RJViewController.h"
#import "KVOController.h"

@interface RJViewController ()

@property (nonatomic, strong) FBKVOController *kvoController;

@end

@implementation RJViewController

- (instancetype)init
{
  self = [super init];
  if (nil != self) {
      _kvoController = [FBKVOController controllerWithObserver:self];
  }
  return self;
}

观察者RJViewController概念了一个FBKVOController的积极分子变量kvoController,
RJViewController获释后,其成员变量kvoController也会相应释放,FBKVO自动移除观望者的trick就是在FBKVOControllerdealloc里面做remove
observer的操作。

巡回引用

大家知道,使用block的时候极易出现循环引用,经常使用方需求在block内部团结声美赞臣(Meadjohnson)(Karicare)个weak化的self来幸免那个题材。那有没有主意省去这一步呢?是的,FBKVO在接口设计的时候也设想到了那个题材,解决形式是在block回调接口伸张一个observer参数,而这个observer在FBKVOController内部做了weak化处理,在上头示例中,id observer尽管观看者(即RJViewController *observer),也就是早先化接口时传出的self,
所以在block内部直接使用[observer doSomething]来代替[self doSomething]即可幸免循环引用的难题。

FBKVO防止循环引用的宏图真正很精妙,大家来接着看下边这几个意况:

[self.KVOController observe:self.photos                        
                    keyPath:@"count"
                    options:NSKeyValueObservingOptionNew
                      block:^(id observer, id object, NSDictionary *change) {
    // observer -> RJViewController -> __weak self
    // object   -> self.photos    
    // [self doSomething] -> [observer doSomething]
    [self.KVOController unobserve:self.photos keyPath:@"count"]
}];

这里在block里面用self调用了unobserve措施,依照大家以前的敞亮,这那边肯定会产出循环引用了,应该改成:

[observer.KVOController unobserve:observer.photos keyPath:@"count"]

但实际情形是在这些境况下,即接纳self也不会挑起循环引用,那是干吗呢?原因是做了unobserve操作后,存储KVO信息的_FBKVOInfo会被假释掉,那样它所指向的当前那些block也会被置为nil,
这样block引用self那么些链端就被打破了,也就不会见世循环引用的难点了。所以打破循环引用除了在block内使用weakSelf外,也可以在事件处理完后将近期的block置为nil来实现。

初始化

大家先来看望FBKVOController是怎么样提供起首化接口的:

+ (instancetype)controllerWithObserver:(nullable id)observer;
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithObserver:(nullable id)observer;

计算有3个早先化函数,其中第二个和首个为便于发轫化函数(convenience
initializer),
首个添加了NS_DESIGNATED_INITIALIZER宏,为指定初阶化函数(designated
initializer).

伊始化接口的条条框框:
a) 指定开头化方法必须调用父类的指定开头化方法
b)
便利初步化方法必须调用其余的开头化方法,直到最终指向指定起初化方法
c) 具有指定初步化方法的子类必须兑现所有父类的指定开头化方法

知道了自释放的规律,伊始化的平整就很扎眼了,FBKVOController必须作为observer的分子变量存在。那即便使用方忽视了那些规则或者不想这么繁琐,有没有更简单的格局吗?有!大家来看下FBKVO是怎么提供最简化convenience
initializer的:

@interface NSObject (FBKVOController)

@property (nonatomic, strong) FBKVOController *KVOController;
@property (nonatomic, strong) FBKVOController *KVOControllerNonRetaining;

@end

FBKVOController创制了NSObject的Category,
通过AssociateObject给NSObject提供一个Retain和nonRetain的KVOController(那里其实也是在成员变量KVOController的Get函数里面调用了+ controllerWithObserver方式)。所以任意observer都可以直接调用observer.KVOController来动态变化一个FBKVOController对象,非凡便利!

到那儿看起来先导化接口已经很完整了,但类似还有个难题,万一使用方不按套路直接来个种类默许的开头化函数[[FBKVOController alloc] init]或者[FBKVOController new]那Observer岂不是就从未有过了。怎么办才能提示使用方不要调用系统的起先化函数呢?

/**
 @abstract Allocates memory and initializes a new instance into it.
 @warning This method is unavaialble. Please use `controllerWithObserver:` instead.
 */
- (instancetype)init NS_UNAVAILABLE;

+ (instancetype)new NS_UNAVAILABLE;

答案就是在那个默许开首化函数后边加上NS_UNAVAILABLE宏,那样只要拔取方误用了系统默认的初阶化函数时会给出警告,提醒他应有选取模块指定的开始化接口方法。