用了这么多次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 //存储引用计数值减一后的结果
(最高有效位)  

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

推荐阅读更多精彩内容