老青菜

iOS Legacy Runtime

2017-08-10

最近在回顾 runtime 的知识,突然了解到以前还存在一个 Legacy版本。 官方原文如下:

There are two versions of the Objective-C runtime—“modern” and “legacy”. 
The modern version was introduced with Objective-C 2.0 and includes a number of new features.
The programming interface for the legacy version of the runtime is described in Objective-C 1 Runtime Reference.

runtime 其实有两个版本:Legacy(objc 1.0)、Modern版本(objc 2.0)。两者有什么区别呢?最显著特性的应该就是 non fragile ivar了, 官方原文如下:

The most notable new feature is that instance variables in the modern runtime are “non-fragile”:
In the legacy runtime, if you change the layout of instance variables in a class, you must recompile classes that inherit from it.
In the modern runtime, if you change the layout of instance variables in a class, you do not have to recompile classes that inherit from it.

翻译一下:在 Legacy 版本里,如果修改了基类的成员变量布局(比如增加、删除成员变量),子类必须需要重新编译;在 Modern 版本里,修改了基类的成员变量布局,子类不需要重新编译。
那么早期的 runtime class 结构是什么样的呢?打开 objc4-723 ,或者 下载源码

objc_object

objc_object objc 中的对象,这个结构体在两个版本中没有什么变化,打开 objc-private.h,找到结构体定义。

struct objc_object {
private:
    isa_t isa;
public:
    // ISA() assumes this is NOT a tagged pointer object
    Class ISA();
    // getIsa() allows this to be a tagged pointer object
    Class getIsa();
    //此处省略 ...
}
//isa_t 是一个联合结构体
union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }
    Class cls;
    uintptr_t bits;
}

isa : 指向 MetaClass(元类)的指针,这里不做深入分析。

objc_class

我们接着来看一下早期的 objc_class

///等同于objc class
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

打开 objc-runtime-old.h 或者 runtime.h 文件,找到老的结构体 objc_class 结构体定义

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
    Class _Nullable super_class            OBJC2_UNAVAILABLE;
    const char * _Nonnull name             OBJC2_UNAVAILABLE;
    long version                           OBJC2_UNAVAILABLE;
    long info                              OBJC2_UNAVAILABLE;
    long instance_size                     OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars      OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                   OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols   OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;

注释里明确说明了 objc2 不可用,我们先简单介绍一下结构体的几个成员:

  1. superclass: 指向父类的结构体指针(找父类变量和方法)。
  2. name:类名。
  3. version:版本号,默认为0。
  4. info:其他信息,运行期间的一些位标示。
  5. instance_size:类实例变量大小。

ivars

变量链表,包含了变量数量、占用的空间、变量数组的首地址,类似一个变量数组,objc2 中不可用。

struct objc_ivar_list {
    int ivar_count  OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space   OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_ivar ivar_list[1]  OBJC2_UNAVAILABLE;
}       OBJC2_UNAVAILABLE

objc_ivar:变量结构体,包含名称,类型,偏移字节和占用的空间。

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

ivars 在编译期间就确定了,运行时无法修改。按照官方的说法,如果父类增加、删除了成员变量,我们需要重新编译我们的子类。通俗的说,也就是如果 NSObject 增加了属性,我们需要重新编译所有的库,很明显这样太麻烦了。

methodLists

方法链表,objc2 中不可用。

struct objc_method_list {
    struct objc_method_list * _Nullable obsolete     OBJC2_UNAVAILABLE;
    int method_count                                 OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                        OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]                OBJC2_UNAVAILABLE;
}                                                    OBJC2_UNAVAILABLE;

cache

使用过的方法链表,objc2 中不可用。

typedef struct objc_cache *Cache      OBJC2_UNAVAILABLE;
struct objc_cache {
    unsigned int mask /* total = mask + 1 */    OBJC2_UNAVAILABLE;
    unsigned int occupied                       OBJC2_UNAVAILABLE;
    Method _Nullable buckets[1]                 OBJC2_UNAVAILABLE;
};

protocols

协议链表

struct objc_protocol_list {
    struct objc_protocol_list * _Nullable next;
    long count;
    __unsafe_unretained Protocol * _Nullable list[1];
};

总结

objc 中,成员变量的访问是通过对象地址 + 变量的偏移ivar_offset来访问的。
objc_ivar 结构体包含了偏移(ivar_offset)。而 objc_ivar_listobjc_ivar 链表)在编译期间就确定了,不能修改。那么问题就来了:
我们有两个 classMyObject 继承 NSObject,内存布局基本如下(先不管NSObject自身的变量占用的空间):

如果某个版本中,NSObject 布局变了,新增了两个变量,而子类没有重新编译,objc_ivar ivar_offset 没有更新,那么根据偏移值访问到的值就是错的。



不过好在 Objective-C 2.0 对内存布局做了优化,加入了 Non Fragile ivars 特性,解决了这个问题。

参考链接

Apple OpenSource objc4-723
Apple OpenSource Download
Apple Developer Documentation
深入解析 ObjC 中方法的结构
Non Fragile ivars

Tags: objc
使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

扫描二维码,分享此文章