分析实现-实现KVO

原文链接

基于观察者设计模式,苹果实现了notificationkvo两套监听机制,两者都实现了一对多的监听支持。通知在设计上暴露了notificationCenter这个中心类,通过公开的接口和数据类型,不难猜测出其实现方式。但KVO仅在NSObject中暴露了几个接口,同时缺乏必要的中间类,文档中也只有模糊的介绍,这让人不由地对其实现机制产生兴趣。

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

翻译过来就是:KVO是通过一种称作isa-swizzling的机制实现的,这个机制会在被观察对象的属性被监听时修改对象的isa指针,让指针指向一个中间类而非对象自身的类。

isa

通过文档的描述,可以得出isa指针是KVO的实现机制中最为核心的变量,那么什么是isa指针?如果你能使用英语而非拼音来书写代码,那么一定能够明白Objective-C翻译过来就是C语言的面向对象。换句话说:

OC的所有对象都是封装于C语言的结构体

虽然可以想象到,使用struct来实现面向对象的特性必然是一个十分复杂的过程,但继承的实现我们可以轻易的想象出来:在自身结构内部预留父结构体的变量。打个比方,NSObject的结构体为objc_object,存储了一个isa指针,假如存在子类Person,翻阅objc-private可以确定子类的结构组成:

typedef struct objc_object *id;
typedef struct objc_class *Class;
struct objc_object {
    Class isa;
};

struct objc_class : objc_object {
    // Class isa;
    Class superClass;
    cache_t cache;
    class_data_bits_t bits;
    ......
}

由于id类型属于通配类型,可以用来指向所有OC中的对象,根据其实现结构来看,可以说每一个OC对象都存在一个isa指针用来表示对象类型信息:

isa-swizzling

函数object_setClass提供了修改isa指针的手段,前面已经提到了isa用来表示对象的所属类型,那么交换isa指针可以看做是修改对象的所属类型:

/// NSObject.mm
- (Class)class {
    return object_getClass(self);
}

/// code
id obj = [NSObject new];
NSLog(@"-class: %@, object_getClass: %@", NSStringFromClass([obj class]), NSStringFromClass(object_getClass(obj)));

object_setClass(obj, [NSString class]);
NSLog(@"-class: %@, object_getClass: %@", NSStringFromClass([obj class]), NSStringFromClass(object_getClass(obj)));

/// log
2018-01-25 09:58:46.870577+0800 Test[11398:955919] -class: NSObject, object_getClass: NSObject
2018-01-25 09:58:46.870743+0800 Test[11398:955919] -class: NSString, object_getClass: NSString

方法(+/-)(Class)class的实现中采用object_getclass函数获取对象的所属类型,由于class方法存在被重写来误导使用者的可能性,可以直接调用object_getclass来获取正确的对象类型,通过这个函数可以窥见KVO的实现:

- (void)test {
    id obj = [TestObj new];
    NSLog(@"-class: %@, object_getClass: %@", NSStringFromClass([obj class]), NSStringFromClass(object_getClass(obj)));
    [obj addObserver: [NSObject new] forKeyPath: @"val" options: NSKeyValueObservingOptionNew context: nil];
    NSLog(@"-class: %@, object_getClass: %@", NSStringFromClass([obj class]), NSStringFromClass(object_getClass(obj)));
    
    Class realClass = object_getClass(obj);
    NSLog(@"%@", NSStringFromClass(class_getSuperclass(realClass)));
}

// log
2018-01-25 10:03:24.832764+0800 Test[11398:955919] -class: TestObj, object_getClass: TestObj
2018-01-25 10:03:24.833267+0800 Test[11398:955919] -class: TestObj, object_getClass: NSKVONotifying_TestObj
2018-01-25 10:03:24.833283+0800 Test[11398:955919] realClass's super class is: TestObj

mock in iOS中我曾经提到过要完全模拟一个对象包括两种手段:inherit或者isa_swizzling,结合苹果官方文档的说明,很明显苹果采用了后者。

type-encode

KVO的实现基础之一是被监控对象必须拥有相应的setter方法,换句话说只有ivar的类是无法进行监控的:

@interface UnableObservedClasss : NSobject
{
@public
    id _val1;
    id _val2;
}

@end

在监控过程中,KVO生成的新子类需要重写setter的实现,在属性发生修改的上下文插入执行回调的代码:

- (void)setVal: (id)val {
    [self willChangeValueForKey: @"val"];
    [super setVal: val];
    [self didChangeValueForKey: @"val"];
}

要实现一套通用的KVO机制时,是不能预设什么类型的property会被监控,因此如果无法区分监控属性的类型,是无法动态的去生成setter,我们需要使用到type encoding机制来协助完成这一工作。OC使用特定的字符编码表示某一种具体的数据类型,使用@encode([obj class])可以获取变量类型所对应的字符编码。下面列出官方文档中的编码对应表:

编码 类型
c char
i int
s short
l long
q long long
C unsigned char
I unsigned int
S unsigned short
L unsigned long
Q unsigned long long
f float
d double
B bool _Bool
v void
* char*
@ id
# Class
: SEL
[array type] array
{name=type...} struct
(name=type...) union
bnum a bit field of num bits
^type pointer to type
? unknown

对于单个property来说,通过property_copyAttributeList函数可以获取property的修饰符信息和类型信息,所有信息采用结构体进行映射表示:

typedef struct {
    const char * _Nonnull name;     /// 修饰编码
    const char * _Nonnull value;    /// 具体内容
} objc_property_attribute_t;

有两个重要的修饰编码:T表示类型编码,通过匹配编码表确认类型;S表示属性含有setter,可以动态的生成KVO的方法

实现

参照YYModel对于属性setter的封装实现:

/// 获取监控的属性
objc_property_t getKVOProperty(Class cls, NSString *keyPath) {
    if (!keyPath || !cls) {
        return NULL;
    }
    
    objc_property_t res = NULL;
    unsigned int count = 0;
    const char *property_name = keyPath.UTF8String;
    objc_property_t *properties = class_copyPropertyList(cls, &count);
    
    for (unsigned int idx = 0; idx < count; idx++) {
        objc_property_t property = properties[idx];
        if (strcmp(property_name, property_getName(property)) == 0) {
            res = property;
            break;
        }
    }
    free(properties);
    return res;
}

/// 检测属性是否存在setter方法
BOOL ifPropertyHasSetter(objc_property_t property) {
    BOOL res = NO;
    unsigned int attrCount;
    objc_property_attribute_t *attrs = property_copyAttributeList(property, &attrCount);
    
    for (unsigned int idx = 0; idx < attrCount; idx++) {
        if (attrs[idx].name[0] == 'S') {
            res = YES;
        }
    }
    free(attrs);
    return res;
}

/// 获取属性的数据类型
YYEncodingType getPropertyType(objc_property_t) {
    unsigned int attrCount;
    YYEncodingType type = YYEncodingTypeUnknown;
    objc_property_attribute_t *attrs = property_copyAttributeList(property, &attrCount);
    
    for (unsigned int idx = 0; idx < attrCount; idx++) {
        if (attrs[idx].name[0] == 'T') {
            type = YYEncodingGetType(attrs[idx].value);
        }
    }
    free(attrs);
    return type;
}

/// 根据setter名称获取属性名
NSString *getPropertyNameFromSelector(SEL selector) {
    NSString *selName = [NSStringFromSelector(selector) substringFromIndex: 3];
    NSString *firstAlpha = [[selName substringToIndex: 1] lowercaseString];
    return [selName stringByReplacingCharactersInRange: NSMakeRange(0, 1) withString: firstAlpha];
}

/// 根据属性名获取setter名称
SEL getSetterFromKeyPath(NSString *keyPath) {
    NSString *firstAlpha = [[keyPath substringToIndex: 1] uppercaseString];
    NSString *selName = [NSString stringWithFormat: @"set%@", [keyPath stringByReplacingCharactersInRange: NSMakeRange(0,  1) withString: firstAlpha]];
    return NSSelectorFromString(selName);
}

/// 设置bool属性的kvo setter
static void setBoolVal(id self, SEL _cmd, BOOL val) {
    NSString *name = getPropertyNameFromSelector(_cmd);
    void (*objc_msgSendKVO)(void *, SEL, NSString *) = (void *)objc_msgSend;
    void (*objc_msgSendSuperKVO)(void *, SEL, BOOL) = (void *)objc_msgSendSuper;
    
    objc_msgSendKVO(self, @selector(willChangeValueForKey:), val);
    objc_msgSendSuperKVO(self, _cmd, val);
    objc_msgSendKVO(self, @selector(didChangeValueForKey:), val);
}

/// KVO实现
static void addObserver(id observedObj, id observer, NSString *keyPath) {
    objc_property_t observedProperty = getKVOProperty([observedObj class], keyPath);
    if (!ifPropertyHasSetter(observedProperty)) {
        return;
    }
    
    NSString *kvoClassName = [@"SLObserved_" stringByAppendString: NSStringFromClass([observedObj class])];
    Class kvoClass = NSClassFromString(kvoClassName);
    if (!kvoClass)) {
        kvoClass = objc_allocateClassPair([observedObj class], kvoClassName.UTF8String, NULL);
        
        Class(^classBlock)(id) = ^Class(id self) {
            return class_getSuperclass([self class]);
        };
        class_addMethod(kvoClass, @selector(class), imp_implementationWithBlock(classBlock), method_getTypeEncoding(class_getMethodImplementation([observedObj class], @selector(class))));
        objc_registerClassPair(kvoClass);
    }
    
    YYEncodingType type = getPropertyType(observedProperty);
    SEL setter = getSetterFromKeyPath(observedProperty);
    switch (type) {
        case YYEncodingTypeBool: {
            class_addMethod(kvoClass, setter, (IMP)setBoolVal, method_getTypeEncoding(class_getMethodImplementation([observedObj class], setter)));
        }   break;
        ......
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,690评论 0 9
  • 上半年有段时间做了一个项目,项目中聊天界面用到了音频播放,涉及到进度条,当时做android时候处理的不太好,由于...
    DaZenD阅读 3,015评论 0 26
  • 序言在iOS开发中,苹果提供了许多机制给我们进行回调。KVO(key-value-observing)是一种十分有...
    陌尚煙雨遙阅读 475评论 0 0
  • 在iOS开发中,苹果提供了许多机制给我们进行回调。KVO(key-value-observing)是一种十分有趣的...
    流沙3333阅读 351评论 0 0
  • iOS--KVO的实现原理与具体应用 长时间不用容易忘,这篇文章挺好的.转载自看本文分为2个部分:概念与应用。概念...
    超_iOS阅读 1,436评论 0 17