健壮的实例变量 (Non Fragile ivars)和脆弱的实例变量(Fragile ivars)

1,232 阅读3分钟

1、Non Fragile ivars

objc_class 结构体

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;

objc_ivar结构体

struct objc_ivar {
    char *ivar_name                                          OBJC2_UNAVAILABLE;
    char *ivar_type                                          OBJC2_UNAVAILABLE;
    int ivar_offset                                          OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
}                                                            OBJC2_UNAVAILABLE;

对象地址 + ivar偏移字节
在编译类时,编译器生成了一个 ivar 布局,显示了在类中从哪可以访问ivars 对 ivar 的访问就可以通过 对象地址 + ivar偏移字节的方法

父类增加实例变量

苹果更新了NSObject类,发布新版本的系统,当增加了父类的ivar,这个时候布局就出错了,就不得不重新编译子类来恢复兼容性。 (那如果是在线上运行的app,升级系统后就没办法运行了)

兼容父类的变化

使用 Non Fragile ivars时,Runtime会进行检测来调整类中新增的 ivar 的偏移量。 这样就可以通过对象地址 + 基类大小 + ivar偏移字节 的方法来计算出ivar相应的地址,并访问到相应的ivar。(即使升级iOS系统,之前的app也能正常运行)

2、Non Fragile ivars的意义

Objective-C的库从此具有了**“二进制兼容性”**。 比如在项目里用了第三方提供的静态库SDK,包含一些.h和一个.a文件。当iOS SDK的版本从11升到了12,都不需要更新这个SDK。虽然iOS SDK版本升级时,苹果在等基类中加入了更多的成员变量,但是以前发布的静态库SDK不需要重新编译还能正常使用。

iOS从一开始就是用的modern runtime。 以前的Mac开发者每次MacOS发布新版本,都要重新编译自己的程序,跟着发布新版本。

3、引申-为什么OC类不能动态添加成员变量

既然允许用Category给类增加方法和属性,那为什么不允许增加成员变量? 在Objective-C提供的runtime函数中,确实有一个class_addIvar()函数用于给类添加成员变量,但是文档中特别说明:

This function may only be called after objc_allocateClassPair and before objc_registerClassPair. Adding an instance variable to an existing class is not supported.

这个函数只能在“构建一个类的过程中”调用。 一旦完成类定义,就不能再添加成员变量了。 经过编译的类在程序启动后就被runtime加载,没有机会调用addIvar。 程序在运行时动态构建的类需要在调用objc_registerClassPair之后才可以被使用,同样没有机会再添加成员变量。

为基类动态增加成员变量会导致所有已创建出的子类实例都无法使用。

那为什么runtime允许动态添加方法和属性,而不会引发问题呢?

因为方法和属性并不“属于”类实例,而成员变量“属于”类实例。我们所说的“类实例”概念,指的是一块内存区域,包含了isa指针和所有的成员变量。 所以假如允许动态修改类成员变量布局,已经创建出的类实例就不符合类定义了,变成了无效对象。 但方法定义是在objc_class中管理的,不管如何增删类方法,都不影响类实例的内存布局,已经创建出的类实例仍然可正常使用。