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
,说明父类新增了成员变量,子类的成员变量需要进行偏移.
在上图的例子中,当MyObject
的instanceStart
小于NSObject
的instanceSize
,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的,这时候类结构已经确定下来了.如果这时进行成员变量的添加,那么当子类加载的时候,就会出现文章之前描述的内存覆盖的现象.