FBKVOController源码剖析与学习

建议查看原文:https://www.jianshu.com/p/4a3f9fe13e5a(不定时更新)

源码剖析学习系列:(不断更新)

1、FBKVOController源码剖析与学习
2、MJRefresh源码剖析与学习
3、YYImage源码剖析与学习


FBKVOController是对KVO的封装,本文会分为两大部分:

一、针对FBKVOController进行源码解读,剖析其封装思路

二、针对源码,抽取其精要,模仿学习,变为己用

优势

相对于原生 API 优势

1、可以以数组形式,同时对 model 的多个 不同成员变量进行 KVO。

2、利用提供的 block,将 KVO 相关代码集中在一块,而不是四处散落。比较清晰,一目了然。

3、不需要在 dealloc 方法里取消对 object 的观察,当 FBKVOController 对象 dealloc,会自动取消观察。

使用

//1、在当前类创建一个KVO的控制器,并且指明监听者为当前类
// create KVO controller with observer
FBKVOController *KVOController = [FBKVOController controllerWithObserver:self];
self.KVOController = KVOController;

//2、监听对象
// observe clock date property
[self.KVOController observe:clock keyPath:@"date" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew block:^(ClockView *clockView, Clock *clock, NSDictionary *change) {
  // 更新UI
  // update clock view with new value
  clockView.date = change[NSKeyValueChangeNewKey];
}];

使用步骤很简短,我们关键是理解里面的封装。

FBKVOController

一、我们先看一下创建KVO controller实例的方法,以及销毁方法--(生命周期)

#pragma mark Lifecycle - 
//1、
+ (instancetype)controllerWithObserver:(nullable id)observer
{
  return [[self alloc] initWithObserver:observer];
}
//2、初始化observer,并依据retainObserved值决定内存策略
- (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];
    //初始化互斥锁
    pthread_mutex_init(&_lock, NULL);
  }
  return self;
}
//3、
- (instancetype)initWithObserver:(nullable id)observer
{
  return [self initWithObserver:observer retainObserved:YES];
}
//4、在dealloc注销所有监听并且销毁上面的互斥锁
- (void)dealloc
{
  [self unobserveAll];
  pthread_mutex_destroy(&_lock);
}

总结:1、NSPointerFunctionsStrongMemory创建了一个retain/release对象的集合,非常像常规的NSSet或NSArray。
NSPointerFunctionsWeakMemory使用等价的__weak来存储对象并自动移除被销毁的对象。
2、比较陌生的是 NSMapTable 。简单来说,它与 NSDictionary 类似。不同之处是 NSMapTable 可以自主控制 key / value 的内存管理策略。而 NSDictionary 的内存策略是固定为 copy。当 key 为 object 时, copy 的开销可能比较大!因此,在这里只能使用相对比较灵活的 NSMapTable。具体可以移步关于 NSMapTable

3、pthread_mutex:这是一种超级易用的互斥锁,使用的时候,只需要初始化一个 pthread_mutex_t,用 pthread_mutex_lock 来锁定, pthread_mutex_unlock 来解锁,当使用完成后,记得调用 pthread_mutex_destroy 来销毁锁

二、接下来看一下注册监听对象的方法

- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{
  NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block);
  if (nil == object || 0 == keyPath.length || NULL == block) {
    return;
  }
  //创建FBKVOInfo
  // create info
  _FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];
  
  //利用FBKVOInfo观察对象
  // observe object with info
  [self _observe:object info:info];
}

- (void)observe:(nullable id)object keyPaths:(NSArray<NSString *> *)keyPaths options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{
  NSAssert(0 != keyPaths.count && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPaths, block);
  if (nil == object || 0 == keyPaths.count || NULL == block) {
    return;
  }
//遍历每个keyPath,再递归
  for (NSString *keyPath in keyPaths) {
    [self observe:object keyPath:keyPath options:options block:block];
  }
}

使用断言,提示用户缺少必要参数;
为了避免保留循环,该block必须避免引用KVO控制器或其所有者。观察已经观察到的对象keyPath或nil的结果是没有操作的。

看一下FBKVOInfo的init方法

- (instancetype)initWithController:(FBKVOController *)controller
                           keyPath:(NSString *)keyPath
                           options:(NSKeyValueObservingOptions)options
                             block:(nullable FBKVONotificationBlock)block
                            action:(nullable SEL)action
                           context:(nullable void *)context
{
  self = [super init];
  if (nil != self) {
    _controller = controller;
    _block = [block copy];
    _keyPath = [keyPath copy];
    _options = options;
    _action = action;
    _context = context;
  }
  return self;
}

重写init方法,把值分别赋值给属性,对于为什么要if (nil != self),我认为,当应用程序在更有限的内存中运行,这是一个传统的编码建议。具体请看各位大神的回答--> In Objective-C why should I check if self = [super init] is not nil?

看一下观察FBInfo的方法

- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
  // lock
  pthread_mutex_lock(&_lock);
//1
  NSMutableSet *infos = [_objectInfosMap objectForKey:object];
//2
  // check for info existence
  _FBKVOInfo *existingInfo = [infos member:info];
  if (nil != existingInfo) {
    // observation info already exists; do not observe it again

    // unlock and return
    pthread_mutex_unlock(&_lock);
    return;
  }
//3
  // lazilly create set of infos
  if (nil == infos) {
    infos = [NSMutableSet set];
    [_objectInfosMap setObject:infos forKey:object];
  }

  // add info and oberve
  [infos addObject:info];

  // unlock prior to callout
  pthread_mutex_unlock(&_lock);
//4
  [[_FBKVOSharedController sharedController] observe:object info:info];
}

NSMutableSet是一个集合,它有几个特点:
1、没有顺序,所有元素并非按照加入顺序排列
2、重复元素只会添加一个,因此不用担心里面的元素有重复
NSMapTable是比Dicitionary更强大的一个类。我们定义一个Person类,用来记录人名,我们再创建一个Favourite类用来创建爱好对象,现在有Rose和Jack两个人,分别的爱好是ObjC和Swift,人和爱好必须要用对象实现,而且必须关联起来在一个表里,以便我们进行查询和记录。如果是以前的话需要自己建立一个Dictionary,把人名的name字段作为key,favourite的对象作为value。但是这样有一个问题,如果突然某一天,我Person里面增加了个字段age,我这个表还要记录每个人的年龄,供我以后来查询不同年龄段的人统计使用呢?这下就很尴尬了,因为Dicitionary没办法实现我们要的这个效果,不过没关系NSMapTable可以实现,详细请移步关于 NSMapTable

1、根据被观察的object获取其对应的infos set。这个主要作用在于避免多次对同一个keyPath添加多次观察,避免crash。因为每调用一次addObserverForKeyPath就要有一个对应的removeObserverForKey。

2、从infos set判断是不是已经观察此次info了,避免重复观察。

3、如果infos为空,就把object当做Key、infos当做Object存入 NSMapTable,[infos addObject:info];再把info与infos关联起来。这里听起来可能有点别扭,我做个比喻:object是上面所说的是Rose,infos爱好ObjC,而info则是他的age

4、使用了单例,将观察的信息及关系注册到_FBKVOSharedController中,并且调用iOS自带的KVO方法观察

_FBKVOSharedController作为一个传达者,用来接收和转发KVO通知

- (void)observe:(id)object info:(nullable _FBKVOInfo *)info
{
  if (nil == info) {
    return;
  }

  // register info
  pthread_mutex_lock(&_mutex);
  [_infos addObject:info];
  pthread_mutex_unlock(&_mutex);
  //1
  // add observer
  [object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];

  if (info->_state == _FBKVOInfoStateInitial) {
    info->_state = _FBKVOInfoStateObserving;
  } else if (info->_state == _FBKVOInfoStateNotObserving) {
    // this could happen when `NSKeyValueObservingOptionInitial` is one of the NSKeyValueObservingOptions,
    // and the observer is unregistered within the callback block.
    // at this time the object has been registered as an observer (in Foundation KVO),
    // so we can safely unobserve it.
    [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
  }
}

根据info的状态来选择添加或移除观察者

1、代表所有的观察信息都首先由FBKVOSharedController进行接收,随后进行转发。

//当属性的值发生变化时,自动调用此系统KVO方法


- (void)observeValueForKeyPath:(nullable NSString *)keyPath
                      ofObject:(nullable id)object
                        change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change
                       context:(nullable void *)context
{
  NSAssert(context, @"missing context keyPath:%@ object:%@ change:%@", keyPath, object, change);

  _FBKVOInfo *info;

  {
    // lookup context in registered infos, taking out a strong reference only if it exists
    pthread_mutex_lock(&_mutex);
    info = [_infos member:(__bridge id)context];
    pthread_mutex_unlock(&_mutex);
  }

  if (nil != info) {

    // take strong reference to controller
    FBKVOController *controller = info->_controller;
    if (nil != controller) {

      // take strong reference to observer
      id observer = controller.observer;
      if (nil != observer) {

        // dispatch custom block or action, fall back to default action
        if (info->_block) {
          NSDictionary<NSKeyValueChangeKey, id> *changeWithKeyPath = change;
          // add the keyPath to the change dictionary for clarity when mulitple keyPaths are being observed
          if (keyPath) {
            NSMutableDictionary<NSString *, id> *mChange = [NSMutableDictionary dictionaryWithObject:keyPath forKey:FBKVONotificationKeyPathKey];
            [mChange addEntriesFromDictionary:change];
            changeWithKeyPath = [mChange copy];
          }
          info->_block(observer, object, changeWithKeyPath);
        } else if (info->_action) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
          [observer performSelector:info->_action withObject:change withObject:object];
#pragma clang diagnostic pop
        } else {
          [observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context];
        }
      }
    }
  }
}

根据info的block回调或者actioin等等进行消息转发。

至此,对FBKVOController的源码剖析基本结束,下面是剖析后的学习

学习

1、NSHashTable
+ (instancetype)personWithName:(NSString *)name
{
    DWPerson *person = [[DWPerson alloc] init];
    person.name = name;
    //1、待会替换
    person.family = [[NSMutableArray alloc] init];
    [person.family addObject:person];
    return [person autorelease];
}
- (NSString *)description
{
    return [NSString stringWithFormat:@"%@'s retainCount is %lu",self.name,[self retainCount]];
}
- (void)dealloc
{
    self.name = nil;
    self.family = nil;
    [super dealloc];
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        DWPerson *person_1 = [DWPerson personWithName:@"iOS"];
        DWPerson *person_2 = [DWPerson personWithName:@"swift"];
        DWPerson *person_3 = [DWPerson personWithName:@"android"];
        DWPerson *person_4 = [DWPerson personWithName:@"java"];
        DWPerson *person_5 = [DWPerson personWithName:@"ruby"];
        
        id list = @[person_1, person_2, person_3, person_4, person_5];
        NSLog(@"%@",list);

    }
    return 0;
}

打印:

(
"iOS's retainCount is 3",
"swift's retainCount is 3",
"android's retainCount is 3",
"java's retainCount is 3",
"ruby's retainCount is 3"
)

可以看出每个person的retainCount为3,因为family持有person,person持有family,如果我们运用NSHashTable,则可以完美解决此问题

我们替换1中的代码,

+ (instancetype)personWithName:(NSString *)name
{
    DWPerson *person = [[DWPerson alloc] init];
    person.name = name;
    person.family = [NSHashTable hashTableWithOptions:NSHashTableWeakMemory];
    [person.family addObject:person];
    return [person autorelease];
}

打印:

(
"iOS's retainCount is 2",
"swift's retainCount is 2",
"android's retainCount is 2",
"java's retainCount is 2",
"ruby's retainCount is 2"

)
可看出,已解决循环引用

2、宏定义魔法

先看一下系统的KVO方法

[testPerson addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];

这样写keyPath,如果age属性不存在,也不会告知,导致后续的排查困难,但这种低级错误在FBKVOController不复存在,因为其使用了宏定义

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

该宏定义使用了C语言的逗号表达式,(3+5,6+8)称为逗号表达式,其求解过程先表达式1,后表达式2,整个表达式值是表达式2的值,如:(3+5,6+8)的值是14,a=(a=3 x 5,a x 4)的值是60,而(a=3 x 5,a x 4)的值是60, a的值是15。

使用逗号表达式,我觉得主要是为了FBKVOClassKeyPath

FBKVOClassKeyPath(DWPerson, name)==(((void)(NO && ((void)((DWPerson *)(nil)).name, NO)), #KEYPATH)),其会检查DWPerson中是否有name属性

3、自释放

FBKVOController通过自释放的机制来实现observer的自动移除,其实就是给observer的类中添加一个FBKVOController的成员变量,然后在FBKVOController中的dealloc移除observer,下面是个例子

#import "DWTestViewController.h"
#import "DWObserViewController.h"

@interface DWTestViewController ()
@property (nonatomic, strong) DWObserViewController *obserVC;
@end

@implementation DWTestViewController

- (instancetype)init
{
    self = [super init];
    if (nil != self) {
        _obserVC = [[DWObserViewController alloc] init];
        NSLog(@"DWTestVC创建");
        NSLog(@"DWObserVC创建");
    }
    return self;
}
#import "DWObserViewController.h"

@implementation DWObserViewController

- (void)dealloc {
    NSLog(@"DWObserVC跟着销毁");
}

打印:

2018-02-05 15:32:39.299859+0800 FBKVOController_Demo[6804:208216] DWTestVC创建
2018-02-05 15:32:39.300209+0800 FBKVOController_Demo[6804:208216] DWObserVC创建
2018-02-05 15:32:41.271585+0800 FBKVOController_Demo[6804:208216] DWTestVC销毁
2018-02-05 15:32:46.520148+0800 FBKVOController_Demo[6804:208216] DWObserVC跟着销毁

参考:

NSHashTable和NSMapTable用法

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,829评论 1 331
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,603评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,846评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,600评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,780评论 3 272
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,695评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,136评论 2 293
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,862评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,453评论 0 229
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,942评论 2 233
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,347评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,790评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,293评论 3 221
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,839评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,448评论 0 181
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,564评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,623评论 2 249