iOS版的志愿实现剖析

志愿原理

对于志愿的原理,很多人都比较清楚了大概是这样子的:

假定我们自己的类是Object和它的对象obj,当obj发送addObserverForKeypath:keypath消息后,系统会做3件事情:

创建³³动态一个Object的子类,名字可自定义假设叫做Object_KVONotify。

同时,子类动态增加方法setKeypath:,动态添加的方法会绑定到一个Ç语言的函数。

调用object_setClass函数,将OBJ的类设置为Object_KVONotify。

这样做会相当于建立如下结构:

//Object@interfaceObject:NSObject@property(nonatomic,copy)NSString*keypath;@end@implementationObject-(void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void*)context{NSLog(@" --- Object observeValueForKeyPath:%@ ofObject:%@ change:%@ context:%@", keyPath, object, change, context);}-(NSString*) description{return[NSStringstringWithFormat:@"This is %@ instance keypath = %@",self.class,self.keypath];}@end//Object_KVONotify@interfaceObject_KVONotify:Object@endstaticvoiddynamicSetKeyPath(idobj, SEL sel,idv){... ...}@implementationObject_KVONotify-(void) setKeypath:(NSString*)keypath{dynamicSetKeyPath(self,@selector(setKeyPath:), keypath);}@end//objObject *obj = [[Object alloc] init];object_setClass(obj, Object_KVONotify.class);//上面2句其实相当于Object_KVONotify *obj = [[Object_KVONotify alloc] init]

这样一来,当我们调用

obj.keypath ="hello world";

实际上调用的是

dynamicSetKeyPath(self,@selector(setKeypath:), keypath);

此时dynamicSetKeyPath要做2件事情。

父调用类的setKeyPath:方法。

调用observeValueForKeyPath方法,触发回调。

所以dynamicSetKeyPath函数应该是这样的:

staticvoiddynamicSetKeyPath(idobj, SEL sel,idv){Method superMethod = class_getInstanceMethod(Object.class, sel);((void(*)(id, Method,id))method_invoke)(obj, superMethod, v);NSMutableDictionary* change = [[NSMutableDictionaryalloc] init];change[@"new"] = v;[obj observeValueForKeyPath:@"keypath"ofObject:obj change:change context:nil];}

或者这样

staticvoiddynamicSetKeyPath(idobj, SEL sel,idv){object_setClass(obj, Object.class);[obj setValue: v forKey:@"keyPath"];object_setClass(obj, Object_Notify.class);[(Object *)obj observeValueForKeyPath:@"keypath"ofObject: objChange:@{@"new":v} context:nil];}

在对象类中添加测试代码

+(void)test{Object*obj = [[Objectalloc] init];obj.keypath = @"inited";NSLog(@"%@", obj);object_setClass(obj, Object_KVONotify.class);obj.keypath = @"hello world";}

调用测试代码,产生输入如下

This isObjectinstance keypath = initedObjectobserveValueForKeyPath:keypath ofObject:This is Object_KVONotify instance keypath = hello world change:{new="hello world";} context:(null)

上述过程就是志愿具体流程及测试代码。具体的演示可以代码在这里找到。

志愿痛点

大家都知道,系统KVO略有点难用,主要因为这几点:

addObserver后,不会在对象释放时,自动释放,只能我们在dealloc中手动removeObserver。在这样的疏忽下情况忘记removeObserver可能会导致崩溃。另外,这个限制让我们无法在一个类中为其他类对象增加监听。

如果没有addObserver的英文不能removeObserver的,会崩溃。

不支持块。

重新实现KVO

要重新实现志愿,根据志愿原理,我们需要创建一个增加监听的函数,并在函数内做到:

动态创建当前类的的子类,名字带固定后缀_NotifyKVO。

同时,子类动态增加方法setXXXX:,动态添加的方法会绑定到一个Ç语言的函数。

调用object_setClass函数,将OBJ的类设置为XXXX_NotifyKVO。

首先我们创建一个NSObject的的分类,添加创建志愿方法。

@implementationNSObject(BlockKVO)-(void) addObserverForKeyPath:(NSString*)keyPath option:(NSKeyValueObservingOptions)option block:((^)(idobj,NSDictionary *change))block{//self.blockKVO是通过associate与NSObject对象绑定的//这样我们就把所有逻辑转移到了BlockKVO这个类中[self.blockKVO addObserver:selfforKeyPath:keyPath option:option block:block];}//这里覆盖了系统的KVO监听,里面仅仅调用了添加监听时的block//这样做,可以让系统的KVO监听方法也能收到通过blockKVO添加的事件。-(void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void*)context{BlockKVOItem *item = [self.blockKVO itemWithKeyPath:keyPath];if(item.block) {item.block(self, keyPath, change);}}@end

由于我们有很多参数和状态需要存储,而OC的类别中保存属性是很麻烦的。

所以我们将创建一个新的类来处理所有的绑定逻辑,这就需要将所有参数及对象本身传递到这个类对象中。

请仔细阅读代码中的注释。

@implementationBlockKVO//这里的参数obj就是需要kvo的对象,这个函数很重要,它做到了2件事//1 为obj的class 创建一个以`_NotifyKVO`为后缀的子类//2. 将obj的class指向XXX_NotifyKVO这个子类//搞这么多幺蛾子的好处是实现了AOP,原有的类没有任何改变,obj仍然能访问原类的所有属性方法,而且obj可以通过扩展XXX_NotifyKVO方法,增加功能,也能修改原来类的行为,而不会影响原来类的结构。-(void) initKVOClassWithObj:(id) obj{if(self.srcClass ==nil){self.srcClass = [objclass];//添加子类NSString*dynamicClassName = [NSStringstringWithFormat:@"%@_NotifyKVO",NSStringFromClass(self.srcClass)];Class dynamicClass =NSClassFromString(dynamicClassName);if(!dynamicClass) {dynamicClass = objc_allocateClassPair(self.srcClass, dynamicClassName.UTF8String,0);objc_registerClassPair(dynamicClass);}self.dynamicClass = dynamicClass;//将obj的类换成新创建的子类,否则不会调到dynamicSetKeyPathobject_setClass(obj, dynamicClass);}}//这个方法是从原类中接收参数的,它只做2件事://1. 收到参数后,保存到observers字典中。//2. 根据keyPath,添加setter方法。-(void) addObserver: (id) obj forKeyPath:(NSString*)keyPath option:(NSKeyValueObservingOptions)option block:(void(^)(idobj,NSString*keyPath,NSDictionary *change))block{[selfinitKVOClassWithObj:obj];if(self.observers ==nil){self.observers = [[NSMutableDictionaryalloc] init];}if(self.observers[keyPath] !=nil){return;}//添加方法SEL methodSel = getSetSelector(keyPath);class_addMethod(self.dynamicClass, methodSel, (IMP)dynamicSetKeyPath,"v@:@");//保存BlockKVOItem *item = [[BlockKVOItem alloc] init];item.obj = obj;item.keyPath = keyPath;item.options = option;item.block = block;self.observers[keyPath] = item;}@end

会我们注意到class_addMethod方法,最后一个参数是一个奇怪的字符串。这个字符串是为了表示所添加方法的类型,包括返回值类型和所有参数类型。

这东西又叫做TypeEncoding,为啥有这个东西呢?

我们知道,OC是动态语言,它发送消息是要通过SEL去查找函数的,一旦找到了函数我们再去调用它就不是动态调用了,而是静态调用。

静态调用参数的数量和类型就很重要了。参数数量和类型其中任意一个对不上都会导致程序出错。

对于class_addMethod函数来说,TypeEncoding可以为添加的方法标记出它的返回值类型,参数个数和每个参数的类型。

上面的“v @:@”表示的是,所添加的函数指针,返回值为虚,有3个参数,第一个参数是id,第二个参数是SEL,第三个参数是id。很简单。

OC的类property可以很多种类型,不仅仅是id。所以如果想为不同类型调用class_addMethod,就要编写不同的TypeEncoding。

列一下常用的TypeEncoding:( 更多细节查阅点这里TypeEncoding

“v @:q”=> setKeyPath :(很长)

“v @:c”=> setKeyPath:(char)

“v @:{CGSize = dd}”=> setKeypPath:(CGSize)

通过上述代码,我们当对象的调用再setKeyPath:方法的时候,调用实际上的的英文dynamicSetKeyPath函数,我们看一下它的实现:

//这个函数的定义符合我们定义的typeencoding:"v@:@"staticvoiddynamicSetKeyPath(idobj, SEL sel,idvalue){BlockKVO *blockKVO = [obj blockKVO];//这里肯定不会为空,习惯性防御写法if(blockKVO !=nil) {//根据SEL获取keyPathNSString*keypath = getKeyPath(sel);//获取到注册KVO时传入的参数,包括block啥的。BlockKVOItem *item = [blockKVO itemWithKeyPath:keypath];//这里先将obj的class恢复,否则会陷入循环object_setClass(obj, blockKVO.srcClass);//获取旧值idoldValue = [obj valueForKey:keypath];//设置新值[obj setValue:value forKey: keypath];//设置成子类object_setClass(obj, blockKVO.dynamicClass);//将oldValue和newValue通过observerValueForKeyPath:ofObject:change:方法通知给调用方(调用了block)NSMutableDictionary* change = [[NSMutableDictionaryalloc] init];if(item.options &NSKeyValueObservingOptionNew){change[@"old"] = oldValue;}if(item.options &NSKeyValueObservingOptionOld) {change[@"new"] = value;}[obj observeValueForKeyPath:keypath ofObject:obj change:change context:nil];}}

这样,每次我们调用setKeyPath:的时候,前面注册的KVO监听的块都会被调用。

整个KVO流程就完成了。

当然,如果实现完整的志愿,上面的代码是不够的你还需要解决如下问题:

不同类型的属性支持

setValue:forKey:处理,弱变量可以通过这个函数处理。

线程安全(如果你只在主线程使用,则不必要)

动态创建类的释放

其他可能出现的问题

作者:hard_man

链接:https://www.jianshu.com/p/2a2a03681814

來源:简书

简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

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

推荐阅读更多精彩内容