• 0

  • 476

  • Favorite

KVO原理浅析

None

关注Linux

1 month ago

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_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:方法时的各项传参。

简单总结

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

类图

顺序图

看完上述的过程,基本明白了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中需要避免的问题。

免责声明:文章版权归原作者所有,其内容与观点不代表Unitimes立场,亦不构成任何投资意见或建议。

Linux中文社区

476

Relevant articles

未登录头像

No more data