Crash 防护方案(五):KVO

原文 : 与佳期的个人博客(gonghonglou.com)

Apple 使用了 isa 混写(isa-swizzling)来实现 KVO 。当观察对象 A 时,KVO 机制动态创建一个新的名为:NSKVONotifying_A 的新类,该类继承自对象 A 的本类,Apple 还重写了该类的 -class 方法,返回父类,即对象 A 的本类。且 KVO 为 NSKVONotifying_A 重写观察属性的 setter 方法,setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象属性值的更改情况。
isa 指针的作用:每个对象都有 isa 指针,指向该对象的类,它告诉 Runtime 系统这个对象的类是什么。所以对象注册为观察者时,isa 指针指向新子类,那么这个被观察的对象就变成新子类的对象(或实例)了。 因而在该对象上对 setter 的调用就会调用已重写的 setter,从而激活键值通知机制。

我们可以通过断点看到,被观察者对象的 isa 指针已经变成了 NSKVONotifying_ 开头的类:


kvo-isa.png

对于 KVO 使用不当的话很容易出现 Crash,比如添加和移除观察不对应,重复 removeObserver: 或者移除一个不存在的观察者就会造成 Crash,尤其是在多线程操作时防不胜防:

2019-07-13 17:50:14.805177+0800 GHLCrashGuard_Example[77448:5047850] *** Terminating app due to uncaught exception 'NSRangeException', reason: 'Cannot remove an observer <GHLKVOViewController 0x7fd072c17720> for the key path "name" from <GHLTestObject 0x60000106c160> because it is not registered as an observer.'

为了避免这种重复添加或者重复移除观察造成的崩溃,可以对 KVO 包装一层。创建一个额外的观察者对象,所有的添加观察和移除观察都通过这个额外的对象,这样在添加和移除的时候就可以做安全判断了。
FaceBook 出品的 KVOController 就是做的这样的事情。

self.kvo = [FBKVOController controllerWithObserver:self];
[self.kvo observe:self.obj keyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {

   NSLog(@"oldName:%@", [change objectForKey:NSKeyValueChangeOldKey]);
   NSLog(@"newName:%@", [change objectForKey:NSKeyValueChangeNewKey]);
}];

FBKVOController 的关键主要就在以下三个方法上:

实例化

+ (instancetype)controllerWithObserver:(nullable id)observer;
创建 FBKVOController 对象,主要做了两件事:1、存储了观察者 _observer,2、创建了 _objectInfosMap,用于存储被观察对象的信息。

添加观察

- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block;

1、

// create info
_FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];

主要在创建 _FBKVOInfo 对象,存储
FBKVOController(存储着观察者 _observer)
keyPath(观察属性)
options(观察时机)
block(回调)

2、

- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
  // lock
  pthread_mutex_lock(&_lock);

  NSMutableSet *infos = [_objectInfosMap objectForKey:object];

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

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

  [[_FBKVOSharedController sharedController] observe:object info:info];
}

添加观察者之前做的判断,避免重复添加观察。并且添加了 pthread_mutex_lock 互斥锁保证线程安全。

3、

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

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

调用系统方法 addObserver: 添加观察者,并且在这后判断了 info->_state 如果是非观察状态则执行 removeObserver:

移除观察

1、

- (void)_unobserve:(id)object info:(_FBKVOInfo *)info
{
  // lock
  pthread_mutex_lock(&_lock);

  // get observation infos
  NSMutableSet *infos = [_objectInfosMap objectForKey:object];

  // lookup registered info instance
  _FBKVOInfo *registeredInfo = [infos member:info];

  if (nil != registeredInfo) {
    [infos removeObject:registeredInfo];

    // remove no longer used infos
    if (0 == infos.count) {
      [_objectInfosMap removeObjectForKey:object];
    }
  }

  // unlock
  pthread_mutex_unlock(&_lock);

  // unobserve
  [[_FBKVOSharedController sharedController] unobserve:object info:registeredInfo];
}

同样是做了安全判断,并通过 pthread_mutex_lock 锁保证线程安全

2、

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

  // unregister info
  pthread_mutex_lock(&_mutex);
  [_infos removeObject:info];
  pthread_mutex_unlock(&_mutex);

  // remove observer
  if (info->_state == _FBKVOInfoStateObserving) {
    [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
  }
  info->_state = _FBKVOInfoStateNotObserving;
}

最后一步调用系统方法 removeObserver: 移除观察者,info->_state 设置为非观察状态

Demo 地址:GHLCrashGuard

后记

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

推荐阅读更多精彩内容

  • 上半年有段时间做了一个项目,项目中聊天界面用到了音频播放,涉及到进度条,当时做android时候处理的不太好,由于...
    DaZenD阅读 3,009评论 0 26
  • 该文章属于刘小壮原创,转载请注明:刘小壮[//www.greatytc.com/u/2de707c93d...
    刘小壮阅读 48,187评论 35 227
  • [深入浅出Cocoa]详解键值观察(KVO)及其实现机理罗朝辉 (http://www.cppblog.com/k...
    Crazy2015阅读 677评论 0 1
  • KVO原理浅析 KVO,即Key-Value Observing,官方文档中的介绍是 Key-value obse...
    wilsonhan阅读 1,686评论 1 7
  • 春茶季,高端货,我们屯了不少,这一切都要归功于看老领导的计划周全! 没有做好预算,不敢压货的门店,急得直...
    小稀里糊涂阅读 151评论 0 0