读源码涨姿势之优雅KVO实现

假如说书籍是全人类前进的台阶,那么地道的开源代码就是程序员提高的桥梁。研读源码可以学学其中的框架和格局,
代码技巧,
算法等,然后不断总括运用,最后这个会成为投机的东西,编程水平自然也提高了。

FBKVOController是非死不可开源的接口设计优雅的KVO框架。笔者研读之后确实收益匪浅,本着学以致用的规格,笔者借鉴其接口设计的艺术实现了一套完整的小红点(推送信息)解决方案RJBadgeKit,
有趣味的同室可以参照一下。

出于如今一度有众多关于FBKVOController源码分析的博文,本文会尝试从另外一个角度,以提炼和分析现实知识点的法门来总括FBKVOController中大家得以借鉴和读书的地点。

宏定义

常见在抬高观望者的时候都亟需指定一个观测路径(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通过自释放的体制来落实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的操作。

初始化

我们先来看望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宏,这样一旦运用方误用了系统默认的初步化函数时会给出警告,指示他应有利用模块指定的起始化接口方法。

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.

循环引用

俺们领悟,使用block的时候极易并发循环引用,平日使用方需要在block内部协调表明一个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来实现。

线程锁

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

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];
}

数据结构

KVOController是框架的对外接口类,作为KVO的官员,其具备了近来观看者对象和被观察者的KVO音讯。
观望者对象以weak特性存储在_observer中,而_objectInfosMap大校被观看者以key举办仓储,
value则存储了对应的_ FBKVOInfo聚集(图片引用自Draveness的博文)。

KVOController

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

FBKVOInfo

FBKVO的调用流程如下图所示,
FBKVOController的效率只是加上相应的被观看者记录,以及变更对应的FBKVOInfo消息,最后会由FBKVOSharedController
这些单例来调用系统KVO方法实现对性能的监听,并且在回调方法司令员事件分发给
KVO 的观看者。

C语言,调用流程

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的代码量即便不多,但其框架流程,接口设计和代码中行使到的微小技术点确实极具水平,希望本文总括和提炼的各类姿势可以让大家有部分取得,也欢迎我们留言研究。完。