用了这么多次KVO,你真的理解了吗?
-
KVO的实现
-
窥探isa指针
说在前面
KVO作为观察者模式的一种实现,为Cocoa框架中实现Binding的一部分,在ReactiveCocoa框架未出现之前为MVVM模式的实现提供了基础。不过KVO饱受诟病,提供的API不易维护,严重依赖String,不过还好有ReactiveCocoa框架:)
KVO的实现
KVO的实现基于Runtime,在文档《Objective-C Runtime Programming Guide》中有这么一句:
The runtime system acts as a kind of operating system for the Objective-C language
Runtime为Objective-C扮演了一种操作系统的角色。OC中各种黑魔法均通过强大的Runtime来实现,Runtime让OC这一门上古语言迎来了第二春。KVO自然也得通过Runtime来实现。
观察之前
在添加观察者之前对象的isa
指针指向了原始类。isa
指针用于告诉Runtime该对象是属于哪个类。在这个阶段被观察对象仍然属于原始类
观察之后
在添加观察者以后:
-
系统通过Runtime动态的创建一个中间类,继承自原始类
-
实现中间类的四个方法
-
class 返回值为原始类
-
setter 用于通知观察者值已经发生改变
-
_isKVOA 私有方法_isKVOA 是用来标示该类是一个KVO 机制声称的类
-
delloc 处理一些收尾工作
-
-
将被观察对象的isa指针指向中间类
至此,被观察对象就神奇的变成了原始类的子类的实例
下面我们用代码一一进行验证
#import <objc/runtime.h>
#import <objc/objc.h>
@interface ViewController ()
@property (nonatomic, strong)Person *person;
@property (nonatomic, assign)IMP originalMethod;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [Person person];
//纪录原始方法地址
self.originalMethod = method_getImplementation(class_getInstanceMethod(object_getClass(self.person),@selector(setName:)));
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
//打印对象信息
[self logObjectInfo:(__bridge struct objc_object *)(self.person)];
//延时改变观察值
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.person.name = @"changeName";
});
}
- (void)logObjectInfo:(struct objc_object * )object
{
Class object_class = [(__bridge id)object class];
NSLog(@"--------------\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\nclass:%s\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\nisa:%s\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\nisa_superClass:%@\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\nisa_methods:%@\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\nIMP_original_setName:%p\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\nIMP_setName:%p\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n----------------",
class_getName(object_class),
class_getName(object->isa),
class_getSuperclass(object->isa),
[[self classMethodsList:object->isa] componentsJoinedByString:@" | "],
self.originalMethod,
method_getImplementation(class_getInstanceMethod(object->isa,@selector(setName:))));
}
- (NSArray *)classMethodsList:(Class )class
{
NSMutableArray *array = [NSMutableArray array];
int methodCount = 0;
Method *methodList = class_copyMethodList(class, &methodCount);
int i;
for(i = 0; i < methodCount; i++) {
[array addObject: NSStringFromSelector(method_getName(methodList[i]))];
}
free(methodList);
return array;
}
struct objc_object *
其实是id
的全称,打开 objc/object.h
头文件可以看到一行定义
typedef struct objc_object *id;
打印结果:
2016-10-27 21:08:34.327 KVO-深入理解[4815:1764521]
--------------------------------------------------------------------------
class: Person |
--------------------------------------------------------------------------
isa: NSKVONotifying_Person |
--------------------------------------------------------------------------
isa_superClass: Person |
--------------------------------------------------------------------------
isa_methods: setName: | class | dealloc | _isKVOA |
--------------------------------------------------------------------------
IMP_original_setName: 0x10b44f7a0 |
--------------------------------------------------------------------------
IMP_setName: 0x10b5524ed |
--------------------------------------------------------------------------
苹果为了最大限度的还原被观察对象可谓是用心良苦啊,重写了class
方法以后返回的是Person
类,只有通过isa指针才能获取到真实类NSKVONotifying_Person
.通过对比两个setName
方法的地址,可以判断setName方法被重写了。
窥探isa指针
神奇的isa
指针可以起到改变对象身份的作用,我们来扒扒isa
指针的定义:
- isa 是一个Class 类型的指针. 每个实例对象有个isa的指针,他指向对象的类,而Class里也有个isa的指针, 指向meteClass(元类)。元类保存了类方法的列表。当类方法被调用时,先会从本身查找类方法的实现,如果没有,元类会向他父类查找该方法。同时注意的是:元类(meteClass)也是类,它也是对象。元类也有isa指针,它的isa指针最终指向的是一个根元类(root meteClass).根元类的isa指针指向本身;
在runtime头文件中可以找到对class 以及 object 的定义
//苹果限制了这一部分的使用,如果是Objective-C 2.0编译将不通过
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
对象的本质其实就是一个结构体,类也是一个结构体,可以看到类和对象都有一枚isa指针。不过在arm64架构的设备中,object的isa已经不再是一个指针了!这是因为64位架构中,runtime为了节约空间,会将其余的空间用于储存析构状态,引用计数,被其他 weak 变量引用情况,
下面列出了一些isa 的结构体定义
(最低有效位)
1 bit indexed //0代表原始isa,1代表非指针isa
1 bit has_assoc //对象是否拥有关联对象,如果没有对象可以析构的更快
1 bit has_cxx_dtor //对象是否拥有c++或者ARC析构函数,如果没有对象可以析构的更快
30 bits shiftcls // 类指针
9 bits magic //固定值为 0xd2,用于在调试时分辨真实对象是否未初始化
1 bit weakly_referenced //对象是否有过 weak 对象,如果没有,则析构时更快
1 bit deallocating //对象是否正在析构
1 bit has_sidetable_rc //对象的引用计数值是否过大无法存储在 isa 中
19 bits extra_rc //存储引用计数值减一后的结果
(最高有效位)