欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

初识Runtime之KVO实现原理

程序员文章站 2024-03-24 13:51:40
...

Runtime运行时是在iOS代码运行时将其翻译成对应的C语言程序。
Runtime 运行时是苹果提供的一个API,属于一个C语言的库。那么在iOS开发中,Runtime运行时有什么用呢?
1. 利用Runtime运行时,在程序的运行过程中,动态创建一个类。
2. 利用Runtime运行时,在程序的运行过程中,动态修改一个类的属性/方法。
3. 利用Runtime运行时,遍历一个类的所有的属性和方法。
用的时候需要导入头文件: objc/runtime.h/objc/message.h(包含了objc/runtime.h)(需要加上尖括号,由于这个编辑器的原因,直接加上尖括号,导致无法显示)
Method:成员方法
Ivar :成员变量

上面是对Runtime运行时的一个简单概括,我之前一直只是知道有Runtime这个东西的存在,但是具体实例一直没有研究过,感觉跟Runtime相关的更多的是长篇大论的理论,所以总是不能很好的去理解Runtime具体是怎么工作和应用的。最近看了Hank老师的公开课,感觉明了了很多,下面就介绍一下用Runtime运行时来实现KVO的一个过程,基本步骤如下:

  1. 动态添加被监听对象类的一个子类;
  2. 在该子类中动态添加被监听的属性的set方法,即重写父类的属性的set方法;
  3. 当调用被监听的属性的set方法且改属性的值发生改变的时候通知外界。

特别注意:
一个类的属性赋值形式为object.property = …
一个类的成员变量赋值形式为object->MemberVariable = …
KVO不能观察到成员变量的值的变化,因为KVO是动态添加了一个被监听对象类的子类,然后重写了被监听对象类的属性的set方法,在该方法中来做的通知,所以只有调用set方法才能被检测到。

我这篇文章是看了Hank老师的公开课后整理的,那我先总结一下Hank老师讲的内容,下面是我做的笔记:

KVO原理和实现过程说明(非代码,最后面会给出我自己实现KVO的完整demo):
Person * _p = [[Person alloc] init];
[_p addObserver:self forKeyPath:@“age” options:NSKeyValueObservingOptionNew context:nil];
只要添加这行代码,调用时,runtime会动态添加一个Person类的子类 NSKVONotifying_Person类,然后在这个类中重写age属性的set方法,并把_p的真实类型改为NSKVONotifying_Person,然后一旦调用_p.age就是调用子类的age的set方法,一旦子类的age的值发生改变就会通知-(void)observeValueForKeyPath:ofObject:change: context;

KVO重写的set方法:
初识Runtime之KVO实现原理

willChangeValueForKey和didChangeValueForKey都会通知–(void)observeValueForKeyPath:ofObject:change: context;方法,change是dictionary,里面放的是keyPath的旧值和新值,willChangeValueForKey会把旧值给过去,didChangeValueForKey会把新值给过去

以上是我听了Hank老师的视频总结的(如果理解有错请指教),然后我自己就想验证一下,但是写到重写set方法的时候,调用了willChangeValueForKey和didChangeValueForKey方法,然而并没有收到通知,原因我还没弄明白,但是我查了一些资料,一些只是说调用willChangeValueForKey和didChangeValueForKey方法就会触发通知,但是并没有给出具体代码。还有看到实现了KVO的,但是并不是调用的willChangeValueForKey和didChangeValueForKey方法,所以我的这个demo也没有调用willChangeValueForKey和didChangeValueForKey方法,如果有用willChangeValueForKey和didChangeValueForKey方法来实现的希望可以分享下代码。我的demo地址为KVO_Runtime_Demo

有兴趣的可以看一下demo的源码,下面我会根据我的demo的代码来说一下KVO的实现过程:
首先创建一个Person类和一个NSObject类的分类(因为需要所有类都可以调用,可参考原生KVO的addObserver方法),demo目录结构如下:
初识Runtime之KVO实现原理
Person类代码如下:

Person.h
#import <Foundation/Foundation.h>

@interface Person : NSObject

@property(nonatomic,assign) int age;

@end

Person.m
#import "Person.h"

@implementation Person

-(void)setAge:(int)age{
    NSLog(@"我是Person的setAge");
    //自定义操作
    age = 10 + age;
    _age = age;
}

@end

Person类是用来测试的类,即充当被监听的类,我们下面要监听一下Person类的age属性的变化。
直接调用系统的addObserver:self forKeyPath:options:context:方法是可以的,我们这里是要探讨KVO内部的实现,那么我们就在NSObject+ZZ_KVO这个分类里自己写一个添加观察者的方法,然后自己去用Runtime运行时来实现监听的功能。
我在NSObject+ZZ_KVO中定义一个添加监听的方法(不建议直接重写系统的addObserver:self forKeyPath:options:context:,加上前缀以作为区别):

-(void)ZZ_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context

然后在ViewController中定义一个Person对象,并让该Person对象调用ZZ_addObserver方法

_p = [[Person alloc] init];
    [_p ZZ_addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];

NSObject+ZZ_KVO中的代码如下:

NSObject+ZZ_KVO.h
#import <Foundation/Foundation.h>

@interface NSObject (ZZ_KVO)

-(void)ZZ_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;

@end

NSObject+ZZ_KVO.m

#import "NSObject+ZZ_KVO.h"
#import <objc/message.h>

@implementation NSObject (ZZ_KVO)
//添加观察者方法
-(void)ZZ_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context{
    //动态添加一个类
    NSString * oldClassName = NSStringFromClass([self class]);
    NSString * newClassName = [@"ZZKVO_" stringByAppendingString:oldClassName];
    //OC转C
    const char * newName = [newClassName UTF8String];
    //定义一个类
    Class MyClass = objc_allocateClassPair([self class], newName, 0);
    //重写setAge方法
    class_addMethod(MyClass, @selector(setAge:), (IMP)setAge, "");
    //注册这个类
    objc_registerClassPair(MyClass);
    //改变isa指针,让self指向子类
    object_setClass(self, MyClass);
    //给对象绑定观察者对象
    objc_setAssociatedObject(self, @"observer", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

/*
 有默认参数
void setAge(id self,SEL _cmd,int age){
    NSLog(@"哥们来啦~~~");
}
 */

//id self,SEL _cmd,默认参数
//子类重写父类的setAge方法,OC中一般都会先调用[super method],这样如果父类的setAge方法里对age值做过什么处理,子类里面也会做一样的处理,所以在这里,也需要先调用父类的setAge方法
void setAge(id self,SEL _cmd,int age){
    //子类自己
    id class = [self class];
    //父类
    id superClass = class_getSuperclass([self class]);
    //让自己指向父类
    object_setClass(self, superClass);
    //存放age改变之前的值
    NSMutableDictionary * changeDic = [NSMutableDictionary dictionaryWithCapacity:0];
    unsigned int oldOutCount = 0;
    //由于已经让self指向了自己的父类,所以在取属性的时候,可以直接用[self class]拿到age
    Ivar * oldivars = class_copyIvarList([self class], &oldOutCount);
    for (unsigned int i = 0; i < oldOutCount; i ++) {
        Ivar ivar = oldivars[i];
        const char * name = ivar_getName(ivar);
        NSString * nameString = [[NSString alloc] initWithUTF8String:name];
        if ([nameString isEqualToString:@"_age"]) {
            [changeDic setValue:[self valueForKey:nameString] forKey:[NSString stringWithFormat:@"old%@",nameString]];
        }
    }
    free(oldivars);
    //调用父类的setAge方法,类似于OC的[super setAge:age]
    //objc_getAssociatedObject(<#id object#>, <#const void *key#>)这个方法需要设置xcode的一个属性才能用,默认的无法提示出这个方法并且会报错
    objc_msgSend(self, @selector(setAge:),age);
    //让self指向自己
    object_setClass(self, class);
    NSLog(@"我是ZZKVO_Person的setAge");
    unsigned int newOutCount = 0;
    //已经让self指向了自己,所以如果还是直接用[self class]的话是拿不到age属性的,因为age是父类的,并且现在这个子类的属性列表为空,容易在取值的时候造成崩溃,所以用superClass
    Ivar * newivars = class_copyIvarList(superClass, &newOutCount);
    for (unsigned int i = 0; i < newOutCount; i ++) {
        Ivar ivar = newivars[i];
        const char * name = ivar_getName(ivar);
        NSString * nameString = [[NSString alloc] initWithUTF8String:name];
        if ([nameString isEqualToString:@"_age"]) {
            [changeDic setValue:[self valueForKey:nameString] forKey:[NSString stringWithFormat:@"new%@",nameString]];
        }
    }
    free(newivars);
    if ([[changeDic objectForKey:@"old_age"] intValue] != [[changeDic objectForKey:@"new_age"] intValue]) {
        //通知外界
        id observer = objc_getAssociatedObject(self, @"observer");
        [observer ZZ_observeValueForKeyPath:@"age" ofObject:self change:changeDic context:nil];
    }

}

-(void)ZZ_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{

}

然后我在NSObject+ZZ_KVO中的addObserve方法中打上断点,来看一下在ZZ_addObserver方法中self的类型:
初识Runtime之KVO实现原理
可以看到这个时候self的类型是Person,经过改变isa那行代码之后:
初识Runtime之KVO实现原理
可以看到self的类型变为我动态加的一个类了,这也跟Hank老师在视频中讲的吻合,也验证了KVO的实现步骤。系统KVO执行时添加的子类的前缀为NSKVONotifying_,这个可以行验证,也可以看Hank老师的视频,我这里就不再验证。
最后我是在ViewController中的touchBegin方法中调用了_p.age给age属性赋值,然后我们可以看一下打印的结果:
第一次touch(即age变化)
初识Runtime之KVO实现原理
然后再touch一次
初识Runtime之KVO实现原理
这样就简单的实现了对Person类的age属性值的变化的一个监测。