Non Fragile ivars

3 篇文章 0 订阅

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在运行期也能动态的修改偏移量.

来源:http://www.jianshu.com/p/3b219ab86b09

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值