iOS 简单代码模拟 KVO 派生类实现方式
废话开篇:简单代码模拟 KVO 派生类实现方式,当然,有人会有疑问,“网上的例子多的是,看看就行,为什么还要写?”。其实个人理解的话有些知识点光看不行,写一写,再说一说会对自身的认知是有一个提升的。尤其是与掘友分享交流还能有新的感悟,共同学习进步。
一、实现逻辑前的铺垫
1、如何理解派生类
其实“派生”个人理解它没有特别的含义,它也仅仅是个类,只不过它是为了服务某个特定场景而创建的,在实际开发中,系统产生的“派生”类是隐密起来的类,不会像 UIButton 这样的类呼之即来。
2、类到底是什么
类其实也是对象,可能话刚说完就会有人反驳,“不对!如果类是对象,那么,实例对象是什么?”。从代码逻辑上或许根本没有“类”、“对象”的区分,只不过是为了让程序的更好的被理解人为的给某些特定的代码统称为类。load 方法的执行就理解为一个类开辟了内存地址,而类对应的内存区域内会根据程序设定保存着相关的属性列表、方法列表等。一个类的实例对象的isa指针就是指向类的内存地址,所以,一个对象的可以响应的方法或者能够保存的属性都是通过isa找到类,去查找能否响应方法或者是否有对应属性。
这里说点别的,大家也知道JS下也有原型链的概念,展示一段代码:
//动物类 var Animal = function(){ this.run = function(){ console.log('我是animal类,我在跑'); } }; var animal = new Animal(); animal.eat = function(){ console.log('我是animal类,我在吃饭'); } //人类 var Person = function(){}; //设置animal为Person类的父类 Person.prototype = animal; var person = new Person(); //执行父类存在的方法 person.run(); person.eat(); 复制代码
运行结果:
可以看出,给 Person 这个类设置prototype的值为一个Animal类的实例对象。那么,person 的实例对象就可以继承Animal类的所有方法。
3、修改isa指针会发生什么
isa指针是实例对象的一个默认“成员变量”,它保存的是类的地址。修改isa后仅仅是修改了类的指向,并不会对实例对象已开辟的内存数据存在影响。创建一个“派生”类后,通过runtime对“派生”类的结构进行动态调整,再将实例对象的isa指针指向“派生”类,那么,实例对象就会响应“派生”类一切新增方法。为了保证运行的稳定性,“派生”类最好继承自当前实例对象所属类。
二、代码简单实现
1、创建Animal类
@interface Animal : NSObject @property(nonatomic,strong) NSString * name; @property(nonatomic,strong) NSString * address; @property(nonatomic,strong) NSString * color; @end @implementation Animal - (instancetype)init { if (self = [super init]) { self.color = @"红色"; } return self; } - (void)eat { NSLog(@"我是动物类,我在吃饭"); } - (void)setName:(NSString *)name { _name = name; NSLog(@"我是原setName方法\n"); } - (void)dealloc { NSLog(@"动物%@销毁了",self); } @end 复制代码
很简单的一个类,声明 name、 address、 color三个属性。为证明修改isa指针不影响实例对象的内存数据,在初始化的时候就对 Animal 的颜色进行红色设置。
2、创建 NSObject+WSLObserver 分类
(1)NSObject+WSLObserver.h
#import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface NSObject (WSLObserver) //添加观察者 - (void)addWSLObserverWithKey:(NSString *)key callBack:(void(^)(id _self,id obj))callBack; @end NS_ASSUME_NONNULL_END 复制代码
这里就 addWSLObserverWithKey 方法,参数 key 是要观察的属性,callBack 是修改属性的时候进行的回调通知。
(2)NSObject+WSLObserver.m
#import "NSObject+WSLObserver.h" #import <objc/runtime.h> #import <objc/message.h> static char * callBackDicStr; @implementation NSObject (WSLObserver) //C 方法实现 void setValue(id self,SEL _sel,id value){ Class currentClass = [self class]; //将isa指向父类 object_setClass(self, class_getSuperclass([self class])); //执行父类方法 ((void (*) (id, SEL, id)) (void *)objc_msgSend)(self, _sel, value); //设置为派生类 object_setClass(self, currentClass); //执行派生类方法 NSMutableDictionary * callBackDic = objc_getAssociatedObject(self, &callBackDicStr); void(^callBack)(id,id) = (void(^)(id,id))callBackDic[NSStringFromSelector(_sel)]; if (callBack) { callBack(self,value); } } - (void)addWSLObserverWithKey:(NSString *)key callBack:(void(^)(id _self,id obj))callBack { //当前类 Class class = [self class]; //派生类 Class newClass = class; //判断key值是否存在 if (![self checkIvarIsExist:class key:key]) { NSLog(@"暂无 %@ 属性",key); return; } //创建派生类 NSString * classStr = NSStringFromClass(class); NSString * newClassStr = [NSString stringWithFormat:@"WSLDerived%@",classStr]; char * newClassChar = (char*) [newClassStr UTF8String]; //是否为派生类 BOOL isDerived = [NSStringFromClass(class) hasPrefix:@"WSLDerived"]; if (!isDerived) { //派生类 newClass = objc_allocateClassPair(class,newClassChar,0); //修改isa指针 object_setClass(self, newClass); } //重写观察属性的 setter 方法 NSString * setKey = @""; if (key.length > 0) { setKey = [key stringByReplacingCharactersInRange:NSMakeRange(0,1) withString:[[key substringToIndex:1] capitalizedString]]; } else { return; } //关联属性 NSMutableDictionary * callBackDic = objc_getAssociatedObject(self, &callBackDicStr); if (!callBackDic) { callBackDic = [[NSMutableDictionary alloc] init]; objc_setAssociatedObject(self,&callBackDicStr,callBackDic, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } callBackDic[[NSString stringWithFormat:@"set%@:",setKey]] = callBack; //添加方法 SEL setSel = NSSelectorFromString([NSString stringWithFormat:@"set%@:",setKey]); class_addMethod(newClass, setSel, (IMP)setValue,"v@:@"); } //判断key值是否存在 - (BOOL)checkIvarIsExist:(Class)class key:(NSString *)key { BOOL isExist = NO; unsigned int outCount, i; //当前类 Ivar * ivars = class_copyIvarList(class, &outCount); for (i = 0; i < outCount; i++){ Ivar ivar = ivars[i]; NSString * ivarName = [[NSString alloc] initWithCString:ivar_getName(ivar) encoding:NSUTF8StringEncoding]; if ([ivarName isEqualToString:[NSString stringWithFormat:@"_%@",key]]) { free(ivars); return YES; } } //父类递归判断 if (!isExist) { if ([class superclass]) { isExist = [self checkIvarIsExist:[class superclass] key:key]; } } return isExist; } 复制代码
首先,开头声明了一个 setValue c语言函数,这里方法名字其实无关紧要,目的是要它的IMP。它的方法体部分主要是来实现调用父类的set方法及自身关联对象保存的回调callBack的执行。其实就相当于重写了set方法并调用super方法。
再次,addWSLObserverWithKey方法里,创建了一个继承自实例对象所属类的派生类,修改isa指针指向派生类,为派生类添加了对于需要观察属性的set方法。将addWSLObserverWithKey方法传进来的回调闭包保存在一个NSMutableDictionary类型的关联属性下,以 set方法名作为key,以回调callBack作为value。
最后,animal 在做属性赋值的时候,进行的其实是派生类的set方法。
三、外部调用
逻辑代码:
Animal * animal = [[Animal alloc] init]; NSLog(@"修改isa指针前 animal = %@",animal); [animal addWSLObserverWithKey:@"name" callBack:^(id _Nonnull _self, id _Nonnull obj) { NSLog(@"\n name = %@ \n",obj); }]; [animal addWSLObserverWithKey:@"address" callBack:^(id _Nonnull _self, id _Nonnull obj) { Animal * animal = (Animal *)_self; NSLog(@"\n self.color = %@;address = %@ \n",animal.color,obj); }]; animal.name = @"cat"; animal.address = @"拉尼亚凯亚超星系群"; NSLog(@"修改isa指针后 animal = %@",animal); 复制代码
打印结果:
可以看到,修改isa并没影响对象的内存地址,并且对象的 color 属性也没有收到任何影响。
四、总结
有朋友会指出,到对象销毁,isa 指针也没有变回去,这不符合要求。是的,其实是可以变回去的,就是不再一开始设置观察属性的时候就进行isa修改,而是交换一下原类属性的set方法实现,在执行设置的时候进行修改isa指针,在设置完了再换回到原类的isa指针。但是如果这样做那么着实没有创建派生类的意义了,或许系统有更多的考虑而自身认知水平的有限。
还有就是不要盲目的使用runtime的交换方法实现(method_exchangeImplementations)的API,最好是保证当前类两个方法都存在,不存在的进行创建,因为,当交换的两个方法一个属于父类的话,那么,父类再调用交换完的方法时会因为找不到实现而崩溃,因为实现写在子类里了。
学习总结,大神勿笑[抱拳][抱拳][抱拳]
作者:头疼脑胀的代码搬运工
链接:https://juejin.cn/post/7056216238581612558