iOS开发-KVO下的isa-swizzling

前言

对于KVO技术,开发中使用较多,能够监听值的改变。

探讨下KVO的技术实现,KVO接口如下:

@interface NSObject(NSKeyValueObserverRegistration)

/* Register or deregister as an observer of the value at a key path relative to the receiver. The options determine what is included in observer notifications and when they're sent, as described above, and the context is passed in observer notifications as described above. You should use -removeObserver:forKeyPath:context: instead of -removeObserver:forKeyPath: whenever possible because it allows you to more precisely specify your intent. When the same observer is registered for the same key path multiple times, but with different context pointers each time, -removeObserver:forKeyPath: has to guess at the context pointer when deciding what exactly to remove, and it can guess wrong.
*/
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

@end

有如下问题:

  1. KVO是如何实现监听属性值变化的
  2. addObserver是添加观察者,那么它的引用计数会不会改变呢?

探索

1. 引用问题

有如下代码:

    KVOObject *myobj = [[KVOObject alloc] init];
    myobj.name = @"";
    
    //注释部分1
//    KVOObserver *observer = [[KVOObserver alloc] init];
//    NSLog(@"before add %@",[observer valueForKey:@"retainCount"]);
//    [myobj addObserver:observer forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
//    NSLog(@"after add %@",[observer valueForKey:@"retainCount"]);
//    myobj.name = @"yanfang";
    
    NSLog(@"before block %ld",(long)CFGetRetainCount((__bridge CFTypeRef)(myobj)));
    
    dispatch_block_t block = ^{
        NSLog(@"do action");
        myobj.name = @"yanfang2";
        NSLog(@"in block %ld",(long)CFGetRetainCount((__bridge CFTypeRef)(myobj)));
    };
    NSLog(@"after block %ld",(long)CFGetRetainCount((__bridge CFTypeRef)(myobj)));
    
    NSLog(@"block %@",block);
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), block);

执行并没有问题,myobj会被block捕获,引用计数+1,block执行完之后myobj释放。

当我们将注释部分恢复时,就出现了崩溃EXC_BAD_ACCESS,那么既然不是myobj的问题,那么就是observer提前释放了,而myobj保持了他的_unsafe_unretain的指针,也就不是weak类型指针,导致了崩溃。这和NSNotifcationCenter崩溃的原因应该一致。

2. KVO原理

我们通过addObserver前后打印其isa指针

    printf("before %s \n",object_getClassName(myobj));
    [myobj addObserver:observer forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    printf("after %s \n",object_getClassName(myobj));

输出如下:

before KVOObject 
after NSKVONotifying_KVOObject 

这里其实就使用了isa-swizzling,原理是,将KVOObjectisa修改为NSKVONotifying_KVOObject类型(NSKVONotifying_KVOObject继承自KVOObject),这样子runtime进行消息发送时,首先检查的是isa,通过isa,判断其是NSKVONotifying_KVOObject类型,就去NSKVONotifying_KVOObject的方法列表中去找。这样就达到了将原类方法映射到新类上的目的。

在这里插入图片描述

hook框架Aspect也是利用了这一原理,进行了isa-swizzling

关于如何改变isa指针,runtime源码中有如下接口可以参考:

/***********************************************************************
* object_getClass.
* Locking: None. If you add locking, tell gdb (rdar://7516456).
**********************************************************************/
Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}


/***********************************************************************
* object_setClass.
**********************************************************************/
Class object_setClass(id obj, Class cls)
{
    if (!obj) return nil;

    // Prevent a deadlock between the weak reference machinery
    // and the +initialize machinery by ensuring that no 
    // weakly-referenced object has an un-+initialized isa.
    // Unresolved future classes are not so protected.
    if (!cls->isFuture()  &&  !cls->isInitialized()) {
        _class_initialize(_class_getNonMetaClass(cls, nil));
    }

    return obj->changeIsa(cls);
}

如何关闭KVO

重写automaticallyNotifiesObserversForKey,这个方法会在调用addObserver:...方法时调用

+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
    if ([key isEqualToString:@"name"]) {
        return NO;
    }else{
        return [super automaticallyNotifiesObserversForKey:key];
    }
}

isa是如何恢复的

有没有想过修改其isa之后,怎么恢复呢?蒙圈了吧 = =,这里贴出 GNUStep 实现

@implementation NSObject (NSKeyValueObserverRegistration)

- (void) addObserver: (NSObject*)anObserver
	  forKeyPath: (NSString*)aPath
	     options: (NSKeyValueObservingOptions)options
	     context: (void*)aContext
{
  GSKVOInfo             *info;
  GSKVOReplacement      *r;
  NSKeyValueObservationForwarder *forwarder;
  NSRange               dot;

  setup();
  [kvoLock lock];

  // Use the original class
  r = replacementForClass([self class]);

  /*
   * Get the existing observation information, creating it (and changing
   * the receiver to start key-value-observing by switching its class)
   * if necessary.
   */
  info = (GSKVOInfo*)[self observationInfo];
  if (info == nil)
    {
      info = [[GSKVOInfo alloc] initWithInstance: self];
      [self setObservationInfo: info];
      object_setClass(self, [r replacement]);
    }

  /*
   * Now add the observer.
   */
  dot = [aPath rangeOfString:@"."];
  if (dot.location != NSNotFound)
    {
      forwarder = [[NSKeyValueObservationForwarder alloc]
        initWithKeyPath: aPath
	       ofObject: self
	     withTarget: anObserver
		context: aContext];
      [info addObserver: anObserver
             forKeyPath: aPath
                options: options
                context: forwarder];
    }
  else
    {
      [r overrideSetterFor: aPath];
      [info addObserver: anObserver
             forKeyPath: aPath
                options: options
                context: aContext];
    }

  [kvoLock unlock];
}

- (void) removeObserver: (NSObject*)anObserver forKeyPath: (NSString*)aPath
{
  GSKVOInfo	*info;
  id            forwarder;

  /*
   * Get the observation information and remove this observation.
   */
  info = (GSKVOInfo*)[self observationInfo];
  forwarder = [info contextForObserver: anObserver ofKeyPath: aPath];
  [info removeObserver: anObserver forKeyPath: aPath];
  if ([info isUnobserved] == YES)
    {
      /*
       * The instance is no longer being observed ... so we can
       * turn off key-value-observing for it.
       */
      object_setClass(self, [self class]);
      IF_NO_GC(AUTORELEASE(info);)
      [self setObservationInfo: nil];
    }
  if ([aPath rangeOfString:@"."].location != NSNotFound)
    [forwarder finalize];
}

@end

使用的 object_setClass(self, [self class]); 来还原,这为啥管用? KVO因为重写了其 class 方法,返回的是原来的类型 ,让你没有感觉isa被替换掉,所以 [self class] 这里就没有问题。


Demo链接
链接:https://pan.baidu.com/s/1VZBzclWnK4w5_1Vrcd4TSQ
密码:md43

参考文章
https://www.jianshu.com/p/d509c78c59bd
https://www.jianshu.com/p/badf5cac0130


时时刻刻关注着她,往往输得很惨,所以慎用KVO哦

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值