iOS 简单代码模拟 KVO 派生类实现方式

废话开篇:简单代码模拟 KVO 派生类实现方式,当然,有人会有疑问,“网上的例子多的是,看看就行,为什么还要写?”。其实个人理解的话有些知识点光看不行,写一写,再说一说会对自身的认知是有一个提升的。尤其是与掘友分享交流还能有新的感悟,共同学习进步

一、实现逻辑前的铺垫

1、如何理解派生类

其实“派生”个人理解它没有特别的含义,它也仅仅是个类,只不过它是为了服务某个特定场景而创建的,在实际开发中,系统产生的“派生”类是隐密起来的类,不会像 UIButton 这样的类呼之即来。

2、类到底是什么

类其实也是对象,可能话刚说完就会有人反驳,“不对!如果类是对象,那么,实例对象是什么?”。从代码逻辑上或许根本没有“”、“对象”的区分,只不过是为了让程序的更好的被理解人为的给某些特定的代码统称为load 方法的执行就理解为一个开辟了内存地址,而对应的内存区域内会根据程序设定保存着相关的属性列表、方法列表等。一个的实例对象的isa指针就是指向类的内存地址,所以,一个对象的可以响应的方法或者能够保存的属性都是通过isa找到类,去查找能否响应方法或者是否有对应属性。

如果你正在面试,或者正准备跳槽,不妨看看我精心总结的iOS大厂面试资料https://gitee.com/Mcci7/i-oser 来获取一份详细的大厂面试资料 为你的跳槽加薪多一份保障

这里说点别的,大家也知道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();
复制代码

运行结果:

[图片上传失败...(image-2507aa-1647873295887)]

可以看出,给 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

复制代码

很简单的一个类,声明 nameaddresscolor三个属性。为证明修改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

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

推荐阅读更多精彩内容