KVO源码浅析

KVO原理浅析

KVO,即Key-Value Observing,官方文档中的介绍是

Key-value observing is a mechanism that allows objects to be notified of changes to specified properties of other objects.

KVO是一种允许指定的对象的属性被改变时通知观察者的机制。

KVO在网上的评价褒贬不一,它提供了非常简单的使用方式,但同时在使用过程中又有许多坑需要避免,但不管怎样,KVO是一个有意思的功能,有必要从实现角度去了解一下,学习它的思想。

一、KVO的基本使用

简单看看如何使用KVO

1. 订阅属性

对一个对象添加观察,订阅其属性的变化代码如下:

[person addObserver:self forKeyPath:@"name" options:0 context:NULL];

这里options可以传入多个参数,决定回调函数中change的内容。

NSKeyValueObservingOptionNew: 指示change字典中包含新属性值;
NSKeyValueObservingOptionOld: 指示change字典中包含旧属性值;
NSKeyValueObservingOptionInitial: 在添加订阅时就会发送一条通知
NSKeyValueObservingOptionPrior: 在修改属性前会发送一条通知

2. 响应消息

观察类必须实现该方法,否则可能会crash,抛出异常。在该方法中处理传入的数据,或者调用父类的该方法实现。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if (context == XXXX) {
        NSLog(@"keyPath : %@, object : %@", keyPath, object);
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

3. 取消订阅

如果不remove,会导致内存泄漏以及中间类无法被销毁。

- (void)removeObserver:(NSObject *)observer 
            forKeyPath:(NSString *)keyPath 
               context:(void *)context;

KVO的简单使用只需要这三个函数,如此几步操作就可以实现观察一个属性的功能,来看看KVO的实现原理吧。

二、KVO的原理

苹果开发者文档中,对于KVO原理的解释只有短短几句

Automatic key-value observing is implemented using a technique called isa-swizzling.

When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.

You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

这里简单来说,就是KVO的实现使用了一种叫做isa-swizzling的技术,主要的原理是当观察一个对象的属性时,这个对象的isa指针将会被修改,isa指针会指向一个中间类,这个中间类是被观察对象的子类,它重写了被观察属性的setter方法。同时为了隐藏中间类,修改了这个中间类的class方法,使它返回的是被观察对象的类而不是这个中间类。

通过一张图来理解KVO的实现原理

KVO源类与中间类关系.png

在KVO_MyObject类中,重写了一些方法,举例如

  • setter方法:在重写的setter方法中调用了willChangeValueForKey:方法和didChangeValueForKey:方法,在这两个方法中则会调用observeValueForKeyPath:ofObject:change:context方法,这个方法就是接受通知的回调方法。
  • class方法:重写的class方法返回的是MyObject类对象而不是KVO_MyObject类对象,其目的是欺骗使用者。

1. 通过打印侧面验证原理

而KVO的源码不是开源的,所以这里根据国外大神Mike Ash的博客,通过控制台打印日志来验证KVO的实现细节,通过runtime的方法来查看被观察的对象在运行时的isa指针到底指向的是哪个类。

@interface TestClass : NSObject

@property int x;
@property int y;
@property int z;
@end
    
@implementation TestClass
@end
    
//通过runtime的方法获取当前类对象中的所有方法名
static NSArray *ClassMethodsNames(Class c)
{
    NSMutableArray *array = [NSMutableArray array];
    unsigned int methodCount = 0;
    Method *methodList = class_copyMethodList(c, &methodCount);
    unsigned int i;
    for(i = 0; i < methodCount; i++) {
        [array addObject:NSStringFromSelector(method_getName(methodList[i]))];
    }
    free(methodList);
    return array;
}

//打印对象的相关信息,主要是看传入对象实际上的类对象
static void PrintDescription(NSString *name, id obj)
{
    NSString *str = [NSString stringWithFormat:
        @"%@: %@\n\tNSObject class %s\n\tlibobjc class %s\n\timplements methods <%@>",
        name,
        obj,
        class_getName([obj class]),//KVO会修改中间类的class方法,返回的不是中间类而是源类达到欺骗使用者的目的
        class_getName(object_getClass(obj)),//class方法会欺骗你,但isa指针不会,通过object_getClass获取isa指针的内容,实际指向的就是中间类
        [ClassMethodsNames(object_getClass(obj)) componentsJoinedByString:@", "]];
    printf("%s\n", [str UTF8String]);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        TestClass *x = [[TestClass alloc] init];
        TestClass *y = [[TestClass alloc] init];
        TestClass *xy = [[TestClass alloc] init];
        TestClass *control = [[TestClass alloc] init];
        
        [x addObserver:x forKeyPath:@"x" options:0 context:NULL];
        [xy addObserver:xy forKeyPath:@"x" options:0  context:NULL];
        [y addObserver:y forKeyPath:@"y" options:0  context:NULL];
        [xy addObserver:xy forKeyPath:@"y" options:0  context:NULL];
        
        PrintDescription(@"control", control);
        PrintDescription(@"x", x);
        PrintDescription(@"y", y);
        PrintDescription(@"xy", xy);
        
        //这里在Mike Ash的博客(2009年)中打印的结果是一样的,而在现在的环境中测试时不一样的
        //经过查看源码,methodForSelector方法中实际调用的就是object_getMethodImplementation函数
        //在object_getMethodImplementation函数中,使用的是obj->getIsa()而不是重写的class函数,所以返回的是中间类中的函数
        printf("Using NSObject methods, normal setX: is %p, overridden setX: is %p\n",
               [control methodForSelector:@selector(setX:)],
               [x methodForSelector:@selector(setX:)]);
        printf("Using libobjc functions, normal setX: is %p, overridden setX: is %p\n",
               method_getImplementation(class_getInstanceMethod(object_getClass(control),
                                                                @selector(setX:))),
               method_getImplementation(class_getInstanceMethod(object_getClass(x),
                                                                @selector(setX:))));
    }
    return 0;
}

上面代码中,定义了一个TestClass类,其中有三个int属性x,y,z。在main函数中定义了4个TestClass对象,并为其添加了对属性的观察,之后通过打印函数打印4个对象一些属性。

在代码的打印函数中,打印了class函数以及通过object_getClass获取的对象的isa指针指向的类对象。根据官方文档给出的实现原理,对添加了观察的对象调用class函数,打印的应仍是该对象的类对象,在本例中应该就是TestClass,而通过Runtime的object_getClass函数获取的isa指针指向的类对象,则就是这个神秘的中间类了。

在代码最后的两行打印语句中,对比了没有添加观察的对象control的setX:方法和添加了观察的对象x的setX:的方法地址。我们知道,两个相同类的实例对象中的实例方法,是从同一个类对象中获取的,所以地址也应该是一样,而这里被观察的对象的属性的setter方法被复写了,所以打印结果应该是对象control和对象x的setX:方法的地址是不一样的。

来看看打印结果,打印结果如下:

control: <TestClass: 0x10058aa10>
    NSObject class TestClass
    libobjc class TestClass
    implements methods <observeValueForKeyPath:ofObject:change:context:, z, x, y, setX:, setY:, setZ:>

x: <TestClass: 0x100587ac0>
    NSObject class TestClass
    libobjc class NSKVONotifying_TestClass
    implements methods <setY:, setX:, class, dealloc, _isKVOA>

y: <TestClass: 0x100589cf0>
    NSObject class TestClass
    libobjc class NSKVONotifying_TestClass
    implements methods <setY:, setX:, class, dealloc, _isKVOA>

xy: <TestClass: 0x10058a9f0>
    NSObject class TestClass
    libobjc class NSKVONotifying_TestClass
    implements methods <setY:, setX:, class, dealloc, _isKVOA>

Using NSObject methods, normal setX: is 0x100001530, overridden setX: is 0x7fff2a0fecc5
Using libobjc functions, normal setX: is 0x100001530, overridden setX: is 0x7fff2a0fecc5

上面的打印结果中,首先打印的是control对象的信息,代码中没有对control对象添加观察,所以control对象的class方法与isa指针均指向TestClass类对象。

之后打印x,y,xy三个对象的信息,可以看到不同的地方,isa指针指向的类均为NSKVONotifiying_TestClass类对象,这个类就是神秘的中间类,并且它是TestClass类的派生类,并且类中实现的方法也不同了(这是必然),其中包括被观察的属性x,y的setter方法,class方法,dealloc方法以及_isKOA方法。

这里注意到,在NSKVONotifying_TestClass类中并没有重写属性z的setter方法,而且对于x、y和xy这三个实例对象,是同一个NSKVONotifying_TestClass类的实例对象,哪怕x对象并没有观察属性y,y对象也没有观察属性x,但他们的类对象中同时实现了属性x和y的setter方法。这会牺牲一些效率(在setter方法中需要增加额外的判断语句),如果不这样做,那就需要对x、y以及xy这三个对象生成三个不同的子类,显然苹果认为牺牲部分效率比动态生成更多的子类要更好一些。

在打印结果的最后,有两行打印输出了control对象和x对象的setX方法的地址,这里用了两种方法,分别是NSObject的methodForSelector方法,以及Runtime的class_getInstanceMethod方法。这里Mike Ash提到,第一种方法会显示相同的结果,是因为methodForSelector方法中使用了被重写的class方法,所以得到的是TestClass类中的方法,而我的测试结果与Mike所说不一样,通过查看runtime的源码,可以看到methodForSelector:方法调用了object_getMethodImplementation函数,在这个函数中,实际返回的是obj->getIsa()的结果,也就是isa指针指向的类对象

- (IMP)methodForSelector:(SEL)sel {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return object_getMethodImplementation(self, sel);
}

IMP object_getMethodImplementation(id obj, SEL name)
{
    Class cls = (obj ? obj->getIsa() : nil);
    return class_getMethodImplementation(cls, name);
}

这里可以通过在XCode的调试控制台中输入语句来打印setX:方法地址实际对应的函数,如下所示:

(lldb) print (IMP)0x7fff2a0fecc5
(IMP) $1 = 0x00007fff2a0fecc5 (Foundation`_NSSetIntValueAndNotify)

可以看到,实际上被修改的setX:方法是_NSSetIntValueAndNotify函数,这像是一种实现了观察者通知行为的私有函数。这里对Foundation使用了nm -a命令,得到了一个完整的私有函数列表,命令如下

nm -a /System/Library/Frameworks/Foundation.framework/Versions/Current/Foundation 

得到结果如下:

    0013df80 t __NSSetBoolValueAndNotify
    000a0480 t __NSSetCharValueAndNotify
    0013e120 t __NSSetDoubleValueAndNotify
    0013e1f0 t __NSSetFloatValueAndNotify
    000e3550 t __NSSetIntValueAndNotify
    0013e390 t __NSSetLongLongValueAndNotify
    0013e2c0 t __NSSetLongValueAndNotify
    00089df0 t __NSSetObjectValueAndNotify
    0013e6f0 t __NSSetPointValueAndNotify
    0013e7d0 t __NSSetRangeValueAndNotify
    0013e8b0 t __NSSetRectValueAndNotify
    0013e550 t __NSSetShortValueAndNotify
    0008ab20 t __NSSetSizeValueAndNotify
    0013e050 t __NSSetUnsignedCharValueAndNotify
    0009fcd0 t __NSSetUnsignedIntValueAndNotify
    0013e470 t __NSSetUnsignedLongLongValueAndNotify
    0009fc00 t __NSSetUnsignedLongValueAndNotify
    0013e620 t __NSSetUnsignedShortValueAndNotify

可以注意到,苹果为大部分原生类型以及结构体实现了通知类函数,对于Objective-C的对象只需要一个__NSSetObjectValueAndNotify足矣,但对于其他类型却需要不同的函数去实现。在这个列表中并没有完全实现,例如对于long double、_Bool、CFTypeRef以及除了上面已经实现的Range、Rect结构体之外的结构体都没有实现相应的函数,这意味着这些属性都不符合自动KVO的条件。

在博客的评论中有人指出,如果属性是Rect等结构体,则会使用一种新的函数__forwarding_prep_0__,是_forwardInvocation:技术的一部分,这表示KVO使用了NSInvocation来包装传入的参数,也就是说只要forwarding技术支持的类型都可以被自动KVO。

经过我的测试,目前至少有两种属性是无法被自动KVO的,一个是C类型的指针,如void *,另一个也就是Mike Ash提到的long double(目前原因未考证),其余如long long或者long int都是可以被自动KVO的。

2. GNUStep的实现方法

GNUStep是一个成熟的框架,适用于高级GUI桌面应用程序和服务器应用程序,它将Cocoa Objective-C软件库,以自由软件方式重新实现,能够运行在Linux和windows操作系统上。

GNUStep的Foundation与apple的API相同,虽然具体实现可能不一样,但仍旧有借鉴意义。

查看KVO的实现代码

从Github下载GNUStep base的源码后,可以找到NSKyeValueObsering的头文件和实现文件,这里就是KVO的实现了。

我们从KVO中第一个调用的方法addObserver:开始跟踪,可以发现在头文件中有两个地方声明了addObserver:方法,其中一个是在NSObject类的NSKeyValueObserverRegistration类别中,另一个是在NSArray的类别中,对于一个继承自NSObject的类调用的addObserver:方法,调用的显然是NSObject的类别中的方法,进入实现文件,看看它的实现吧,代码如下:

- (void) addObserver: (NSObject*)anObserver
          forKeyPath: (NSString*)aPath
             options: (NSKeyValueObservingOptions)options
             context: (void*)aContext
{
  GSKVOInfo             *info;//存储一个实例对象对应的观察的相关信息
  GSKVOReplacement      *r;//存储类与中间类的映射
  
  // 1. 通过当前类创建一个中间类,返回一个GSKVOReplacement对象
  r = replacementForClass([self class]);

  // 2. 查看该对象是否已经绑定ObservationInfo,有则说明该类已经被KVO且已经是中间类,
  //    如果没有则初始化GSKVOInfo对象,并将该对象的类替换为第一步返回的中间类
  info = (GSKVOInfo*)[self observationInfo];
  if (info == nil)
    {
      info = [[GSKVOInfo alloc] initWithInstance: self];
      [self setObservationInfo: info];
      object_setClass(self, [r replacement]);
    }

   // 3. 重写setter方法
   [r overrideSetterFor: aPath];
   // 4. 将观察者和观察信息添加到全局MapTable中
   [info addObserver: anObserver forKeyPath: aPath options: options context: aContext];
}

上述代码经过了一定的精简,最终可以看到,在addObserver方法中,经过4个主要步骤,实现了KVO的属性观察的添加,一步一步来看。

第一步:创建中间类
 r = replacementForClass([self class]);

找到replacementForClass函数的定义

static GSKVOReplacement * replacementForClass(Class c)
{
  GSKVOReplacement *r;
  //从全局classTable中查询该类是否已经生成中间类,若有则直接返回
  r = (GSKVOReplacement*)NSMapGet(classTable, (void*)c);
  if (r == nil)
    { //用该类初始化GSKVOReplacement
      r = [[GSKVOReplacement alloc] initWithClass: c];
      //添加到全局表中
      NSMapInsert(classTable, (void*)c, (void*)r);
    }
  return r;
}

可以看到,这个函数是为了返回一个GSKVOReplacement类的实例对象,该类存储了被观察的对象的类型和生成的中间类的类型以及被观察的属性Set集合。

replacementForClass函数中,首先从全局的classTable表中查询该类是否已经生成过GSKVOReplacement对象,如果有的话,则不需重复创建,直接返回r,如果没有,则需要创建一个新的GSKVOReplacement对象,并添加到全局表中,这里classTable是一个私有的全局变量NSMapTable类型。

接下来看一看这个GSKVOReplacement类的初始化函数中做了哪些工作。

- (id) initWithClass: (Class)aClass
{
  NSValue       *template;
  NSString      *superName;
  NSString      *name;
  
  original = aClass;

  //创建一个源类的派生类,并以GSKVOBase类为模板类重写派生类中的部分方法。
  superName = NSStringFromClass(original);//获取源类的名称
  name = [@"GSKVO" stringByAppendingString: superName];//生成中间类的名字,添加前缀
  template = GSObjCMakeClass(name, superName, nil);//创建一个类(该类并没有加入runtime,需要调用GSObjcAddClasses()函数)
  GSObjCAddClasses([NSArray arrayWithObject: template]);//将类加入到runtime中
  replacement = NSClassFromString(name);//存储中间类
  GSObjCAddClassBehavior(replacement, baseClass);//以baseClass类为模板为中间类添加方法

  return self;
}

initWithClass:方法中,以源类的名字为基础,创建了一个新的中间类,并以baseClass对象为模板,重写了一部分类方法,最后将源类与中间类存储起来。

这里baseClass对象被初始化为GSKVOBase类,该类重写了class方法、dealloc方法、KVC方法以及supercalss方法,这里仅看一下重写的class方法实现:

- (Class) class
{
  return class_getSuperclass(object_getClass(self));
}

在class方法中,返回了中间类的父类,也就是源类,达到了隐藏中间类的目的。

第二步:创建GSKVOInfo
info = (GSKVOInfo*)[self observationInfo];
if (info == nil)
{
    info = [[GSKVOInfo alloc] initWithInstance: self];
    [self setObservationInfo: info];
    object_setClass(self, [r replacement]);
}

实现文件内的私有的全局变量有一个NSMapTable类型的infoTable表,用来存储每一个被观察的实例对象和ObservationInfo的映射。

这一步也是先调用实例对象的observationInfo方法,该方法定义在NSObjectNSKeyValueObservingCustomization类别里,具体实现如下:

//该函数从infoTable表中查询self映射的info对象并返回。
- (void*) observationInfo
{
  void  *info;
  info = NSMapGet(infoTable, (void*)self);
  return info;
}

而如果返回的info为空,则说明该对象是第一次调用addObserver:方法,需要初始化GSKVOInfo对象,创建一个GSKVOInfo对象,调用setObservationInfo:方法将info添加到全局映射表中,同时也是最重要的一步,调用runtime函数object_setClassself的类更换成前面生成的中间类。

这一步结束时,被观察的类对象就已经变成中间类对象了,并且除了setter方法,其他方法均已经被重写。

第三步:重写setter方法
[r overrideSetterFor: aPath];

这一步调用的是GSKVOReplacement提供的方法overrideSetterFor:,传参是keyPath,也就是被观察的属性名称。这个方法的实现非常长,主要逻辑如下:

  1. 首先根据传参aPath生成setter方法的SEL,有两种,一种是setXXX,另一种是_setXXX,只要这两者在源类中已经实现,就会重写该函数。
  2. 根据被观察属性的类型,获取对应setter方法的IMP,所有支持的数据类型的setter方法在GSKVOSetter模板类中已经实现好。
  3. SELIMP通过runtime函数class_addMethod()添加到中间类中。

这里有一个模板类叫做GSKVOSetter,针对不同的数据类型,都有一个不同的setter方法实现,列举其中一个方法:

- (void) setterChar: (unsigned char)val
{
  NSString  *key;
  Class     c = [self class];
  void      (*imp)(id,SEL,unsigned char);
  //_cmd获取源类对象的set方法
  imp = (void (*)(id,SEL,unsigned char))[c instanceMethodForSelector: _cmd];

  key = newKey(_cmd);//通过选择器获取key(比如选择器名称是_setStr,得到str)
  if ([c automaticallyNotifiesObserversForKey: key] == YES)//实际上这里总是返回YES,在子类中可以重写
    {
      [self willChangeValueForKey: key];
      (*imp)(self, _cmd, val);
      [self didChangeValueForKey: key];
    }
  else
    {
      (*imp)(self, _cmd, val);
    }
  RELEASE(key);
}

在运行时,该方法的IMP会和overrideSetterFor方法中生成的SEL绑定并添加到中间类中。在该方法中通过_cmd变量获取[self class]也就是源类的setter方法的IMP,然后调用三个函数,分别是:

  1. willChangeValueForKey:用于保存旧数据以及发送修改数据之前的通知(需要设置options)
  2. 源类的setter方法
  3. didChangeValueForKey:用于保存新数据以及发送修改数据之后的通知(默认通知)

这里willChangeValueForKey:didChangeValueForKey会根据调用addObserver:forKeyPath:options:context方法的传参options来决定调用哪些操作,如在回调函数中的字典change传参中设置keyoldnew的value,以及在修改变量前和修改变量后的通知操作。

GSKVOSetter中的其余方法分别对不同数据类型实现了不同的setter方法,如chardoublefloatintlong以及RectPointSizeRange等结构体类型。

第四步:更新GSKVOInfo中的数据
[info addObserver: anObserver forKeyPath: aPath options: options context: aContext];

在第四步中,更新到GSKVOInfo对象中的相关信息,以及如果设置了NSKeyValueObservingOptionInitial,也就是希望在调用addObserver:…方法时就收到一个通知,则会在第四步中发送该通知。

这里有几个存储整个KVO过程的观察信息的类:

  • GSKVOInfo:存储某一个实例对象相关的所有观察信息,在全局表中与每一个被观察的实例对象一一映射。
  • GSKVOPathInfo:存储观察者的集合和数据变化的集合。
  • GSKVOObservation:存储最初调用addObserver:方法时的各项传参。
KVO类图-0710323.png
简单总结

通过UML图描述GNUStep中的KVO的实现

类图

GNUStepKVO类图.png

顺序图

GNUStepKVO顺序图.png

看完上述的过程,基本明白了KVO的实现原理,GNUStep中实现的方式,与苹果官方文档中所描述的一致,通过在运行时创建一个中间类,修改被观察类的isa指针指向这个中间类,重写部分实例方法以达到通知修改属性的目的。

GNUStep的实现中,使用文件内的私有全局映射表存储了源类与中间类的对应关系以及实例对象与观察信息的对应关系。使用了两个模板类GSKVOBaseGSKVOSetter分别提供了中间类的模板以及相关setter方法的模板。

当上面4步执行完成后,被观察的类对象实际上就不是原来的类了,也就是isa指针指向了中间类对象,并且在调用setter方法中,会根据传入的options变量进行相应的操作,通过这些过程,便可以做到无需添加额外的代码,方便的实现KVO了!

三、使用KVO的注意点

KVO被很多人吐槽过,总结一下KVO的一些槽点以及使用中需要注意的地方

1. 不支持传入回调函数且只有一个不可修改签名的回调方法
  • 添加KVO观察需要调用addObserver:forKeyPath:options:context方法,该方法不能传入自定义回调函数或者Block。
  • 所有的回调相应只能在observerValueForKeyPath...方法中处理,需要判断当前传入的参数是否为自己所需要的,如果KVO处理的事情多而繁杂,势必会造成该方法代码特别长。
2. NSString类型的KeyPath容易出错

KVO中的keyPath必须是NSString,不能依赖于编译器进行检查,同时如果修改了变量名,这里无法被自动修改,如通过Refactor->Rename菜单修改变量名。这里可以使用NSStringFromSelector(@selector(contentSize))来替代直接使用变量名,但这种方法对于如scrollView.contentOffset这样的keyPath是没用的。

3. 子类会拦截父类的实现

若父类也实现了回调函数,则需要在子类的实现中判断是否为自己需要的消息,如果不是,则需要手动调用父类的回调函数。使用context指针存储一个静态指针来判断是否为自己的消息不失为一个较好的方法。

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    if (context == CURRENT_POINTER && object == _tableView && [keyPath isEqualToString:@"contentSize"]) {
        [self configureView];
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}
4. 重复remove会导致crash

每一个addObserver要对应一个removeObserver,对一次观察执行两次removeObserver:方法则会导致crash。这里如果对同一个属性添加了多个观察,那么在通知和remove的时候均会按照倒序来执行,或者也可以通过Context指针来指定remove。

什么时候用KVO

1. 如果苹果要求使用KVO的话

AVPlayer类,苹果在文档中建议可以使用KVO对通用状态进行观察,如currentItem或者回放的rate

2. 为其他人设计API的时候

如果你在为他人设计一个库,而且你想监听scrollViewDidScroll:通知,同时因为库的使用者可能会去代理scrollView,这时可以使用KVO去监听contentOffset属性的变化。

好用的框架KVOController

KVOController是Facebook推出的一个开源框架,使用起来甚至比KVO更容易,并且支持传入自定义的回调函数或者Block,同时保证了线程安全,最后也不需要手动移除观察者。在Github中可以下载到源码,其中有详细的使用说明。

四、总结

因为苹果没有开源KVO的实现,所以我们只能通过其他办法来查看KVO的细节,本文首先通过代码运行过程中打印类的信息来验证KVO的一些实现机制,其次通过GNUStep的源码来学习了KVO的一种可能的实现,虽然与实际源码有区别,但从源码中仍然学习到很多知识,最后总结了一些使用KVO中需要避免的问题。

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,670评论 0 9
  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,242评论 8 265
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,065评论 1 32
  • OC语言基础 1.类与对象 类方法 OC的类方法只有2种:静态方法和实例方法两种 在OC中,只要方法声明在@int...
    奇异果好补阅读 4,228评论 0 11
  • 汇中原 总有是有人说:“缘分”是注定的,早在五月份就准备好了去深圳学习这次思维导图双证的课程,订好酒店,机票一切就...
    DONSTUDIO阅读 552评论 5 3