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

借使说书籍是人类进步的台阶,那么精良的开源代码正是程序员提高的桥梁。研读源码能够学学个中的框架和格局,
代码技巧,
算法等,然后不断计算运用,最后那么些会变成自身的事物,编制程序水平自然也增强了。

FBKVOController是Instagram开源的接口设计优雅的KVO框架。小编研读之后确实收益匪浅,本着学以致用的口径,小编借鉴其接口设计的不二法门贯彻了一套完整的小红点(推送音信)消除方案RJBadgeKit,
有趣味的校友能够参照一下。

鉴于方今早就有诸多关于FBKVOController源码分析的博文,本文少禽尝试从其它一个角度,以提炼和分析现实知识点的不二法门来计算FBKVOController中大家得以借鉴和学习的地点。

宏定义

一般在添加观望者的时候都亟待钦定2个考察路径(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;

共计有2个早先化函数,在那之中第三个和第多少个为便于早先化函数(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对象的那两特性子是同一的,那么她们尽管同1位,所以类似上面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. 一经不是同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会有1个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使用的内部1个地点:

- (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为每2个被observe的目的都生成了3个_ 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的代码量就算不多,但其框架流程,接口设计和代码中使用到的分寸技术点确实极具水平,希望本文化总同盟结和提纯的各个姿势能够让我们有局部赢得,也欢迎大家留言探讨。完。