Non Fragile ivars

ivar结构体

runtime的源码中,可以看到类结构体中有成员变量的列表.(class_ro_t也是属于类结构体中的一个成员,不过需要通过non-pointer isas来访问).

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
    // ....
    const ivar_list_t * ivars;
    // ....
};

每个成员变量又是一个结构体.

struct ivar_list_t {
    uint32_t entsize;//总大小
    uint32_t count;//个数
    ivar_t first;//第一个变量的结构体
};

ivar_t结构体的布局如下.

struct ivar_t {
    int32_t *offset;//在实例对象中的偏移
    const char *name;//变量名
    const char *type;//变量类型
    //....
}

假设MyObject类(继承自NSObject)有两个数组属性.那么类的成员属性列表(ivar_list_t)看起来很可能是这样的:

子类并不是通过基类ivar_list_t中的entsize总大小得到子类自己的成员变量在实例对象中的偏移,而是在编译器,这点后面详谈.

二进制兼容性

Objective-C的runtime分为两个版本.一个是legacy版本,一个是modern版本.modern版本是新的runtime版本,它跟随着Objective-C 2.0一起推出的,增加了许多新的特性.

最重要的特性是成员变量(ivar)在modern runtime中是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.

怎么理解这两句话呢?

在旧版本的runtime中,MyObject类的布局如图,MyObject的成员变量排在基类NSObject的成员后面,这点上面也提到过.

如果苹果更新了SDK版本,假设NSObject增加了两个成员变量.原来写的程序将无法正常运作,因为MyObject类成员变量布局在编译期已经确定了,这时两个成员变量和基类的内存区域重叠了.在重新编译代码之前,程序无法在新版本的系统上运行.列举一个更通俗一点的情况,当我们在静态库中使用了继承自NSObject的类,如果这个第三方静态库没有重新编译的话,程序可能就废了...这时只能等待作者更新,或者更换一个第三方库.

这时modern runtime带着新特性Non Fragile ivars登场了.

runtime在加载MyObject类的时候(注:runtime加载类是在main函数跑起来之前),会计算基类的大小.runtime在运行期判断子类的instanceStart大小和父类instanceSize大小(关于这两个成员请看文章开头展示的结构体内容),如果子类的instanceStart小于父类的instanceSize,说明父类新增了成员变量,子类的成员变量需要进行偏移.

在上图的例子中,当MyObjectinstanceStart小于NSObjectinstanceSize,MyObject在编译器确定的结构体将会动态调整成员变量偏移,因此程序无需重新编译,就能在新版本的系统上运行.

因此,这个特性让OC的库具有了二进制兼容性,即稳固的ABI.

runtime实现

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
    // ....
    const uint8_t * ivarLayout;
    const ivar_list_t * ivars;
    // ....
};

该结构体中的instanceStart,和instanceSize在编译器都会被编译器赋值,成为runtime运行期动态调整成员变量的判断依据.

static void reconcileInstanceVariables(Class cls, Class supercls, const class_ro_t*& ro) 
{
  // ....
  // 当子类的instanceStart小于父类的instanceSize时,说明需要调整
    if (ro->instanceStart < super_ro->instanceSize) {
        // Superclass has changed size. This class's ivars must move.
        // Also slide layout bits in parallel.
        // This code is incapable of compacting the subclass to 
        //   compensate for a superclass that shrunk, so don't do that.
        // ....
        // 让只读区域可写
        class_ro_t *ro_w = make_ro_writeable(rw);
        ro = rw->ro;
        // 调整成员变量
        moveIvars(ro_w, super_ro->instanceSize, 
                  mergeLayouts ? &ivarBitmap : nil, 
                  mergeLayouts ? &weakBitmap : nil);
        // layoutsChanged标识布局改变了,ivarLayout需要改变
        layoutsChanged = YES;
    } 
  // ....
}

重点看看moveIvars的实现,简化如下:

static void moveIvars(class_ro_t *ro, uint32_t superSize, 
                      layout_bitmap *ivarBitmap, layout_bitmap *weakBitmap)
{
    // 纪录偏移
    uint32_t diff;
    // 偏移是父类的instanceSize减去子类的instanceStart
    diff = superSize - ro->instanceStart;
    // ....
  
    // Slide all of this class's ivars en masse
    // 遍历子类的所有成员变量
    for (i = 0; i < ro->ivars->count; i++) {
        // 拿到第i个成员变量
        ivar_t *ivar = ivar_list_nth(ro->ivars, i);
        // 得到原来记录的偏移量
        uint32_t oldOffset = (uint32_t)*ivar->offset;
        // 在原来的基础上加上额外的偏移量
        uint32_t newOffset = oldOffset + diff;
        *ivar->offset = newOffset;
    }
    // 最后,别忘了instanceStart和instanceSize也要加偏移
     *(uint32_t *)&ro->instanceStart += diff;
     *(uint32_t *)&ro->instanceSize += diff; 
}

我们注意到ivar_t中的offset是个int *指针,而不是一个int类型的变量,之所以要这样设计,就是为了不让偏移在编译器固定死,让runtime在运行期也能动态的修改偏移量.

不能动态添加成员变量的原因

我们知道,在设计分类的时候,是不能够在分类中添加成员变量的,那么这是为什么呢?
从上述的角度来看,这是因为:

分类是在主类之后被加载到runtime的,这时候类结构已经确定下来了.如果这时进行成员变量的添加,那么当子类加载的时候,就会出现文章之前描述的内存覆盖的现象.

推荐阅读更多精彩内容

  • 文中的实验代码我放在了这个项目中。 以下内容是我通过整理[这篇博客] (http://yulingtianxia....
    茗涙阅读 528评论 0 6
  • 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的...
    西木阅读 29,145评论 33 467
  • 作者:梁实秋 钟表上的时针是在慢慢的移动着的,移动的如此之慢,使你几乎不感觉到它的移动,人的年纪也是这样的,一年又...
    闫宇正面管教阅读 1,183评论 0 1
  • 2、ORACEL 数据类型 对应NUMBER类型的事例: NUMBER类型 对于日起类型,可以使用sysdate内...
    闲不住的李先森阅读 109评论 0 0
  • 任凭绵绵长江水流啊流, 浸透了江南, 许你半生不辜负, 思念就打湿了油纸伞。 落不尽咸咸的梅子雨, 倔强的人儿, ...
    东方予城阅读 184评论 0 0
  • 心如虚空广宇厦,内涵和玉小金屋。 卷藏锦囊善妙法,展现开篇吉祥疏。 人人和平心共处,和平世界丛林树。 众生安乐无伤...
    真如自在阅读 183评论 0 3