从 NSObject 的初始化了解 isa
代替 isa 指针的是结构体 isa_t, 这个结构体中"包含"了当前对象指向的类的信息。
struct objc_object {
isa_t isa;
};
当 ObjC 为为一个对象分配内存,初始化实例变量后,在这些对象的实例变量的结构体中的第一个就是 isa。
在 ObjC 中 Class 的定义也是一个名为 objc_class 的结构体,如下:
struct objc_class : objc_object {
isa_t isa;
Class superclass;
cache_t cache;
class_data_bits_t bits;
};
由于 objc_class 结构体是继承自 objc_object 的,所以在这里显式地写出了 isa_t isa 这个成员变量。
因为在 Objective-C 中,对象的方法并没有存储于对象的结构体中(如果每一个对象都保存了自己能执行的方法,那么对内存的占用有极大的影响)。
当实例方法被调用时,它要通过自己持有的 isa 来查找对应的类,然后在这里的 class_data_bits_t 结构体中查找对应方法的实现。同时,每一个 objc_class 也有一个指向自己的父类的指针 super_class 用来查找继承的方法。
类与元类之间的关系的图需要注意
- rootclass 的 superclass 指向 nil
- root meta class 的 superclass 指向 rootclass
- root meta class 的 isa 指向自己
- 所有类的 meta class 的 isa 直接指向 root meta class
结构体 isa_t
#define ISA_MASK 0x00007ffffffffff8ULL
#define ISA_MAGIC_MASK 0x001f800000000001ULL
#define ISA_MAGIC_VALUE 0x001d800000000001ULL
#define RC_ONE (1ULL<<56)
#define RC_HALF (1ULL<<7)
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
struct {
uintptr_t indexed : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 44;
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 8;
};
};
其中的 isa_t、cls、 bits 还有结构体共用同一块地址空间。而 isa 总共会占据 64 位的内存空间(决定于其中的结构体)
struct {
uintptr_t indexed : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 44;
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 8;
};
indexed 表示 isa_t 的类型
- 0 表示 raw isa,也就是没有结构体的部分,访问对象的 isa 会直接返回一个指向 cls 的指针,也就是在 iPhone 迁移到 64 位系统之前时 isa 的类型。
- 1 表示当前 isa 不是指针,但是其中也有 cls 的信息,只是其中关于类的指针都是保存在 shiftcls 中。
magic
magic 的值为 0x3b 用于调试器判断当前对象是真的对象还是没有初始化的空间
has_cxx_dtor
这一位表示当前对象有 C++ 或者 ObjC 的析构器(destructor),如果没有析构器就会快速释放内存。
shiftcls
使用整个指针大小的内存来存储 isa 指针有些浪费,尤其在 64 位的 CPU 上。
在 ARM64 运行的 iOS 只使用了 33 位作为指针(与结构体中的 33 位无关,Mac OS 上为 47 位),而剩下的 31 位用于其它目的。
代码中的 object 对象的 isa 结构体中的内容是这样的:
其中红色的为类指针
因为我们使用结构体取代了原有的 isa 指针,所以要提供一个方法 ISA() 来返回类指针。
ISA() 方法
其中 ISA_MASK 是宏定义,这里通过掩码的方式获取类指针:
#define ISA_MASK 0x00007ffffffffff8ULL
inline Class
objc_object::ISA()
{
return (Class)(isa.bits & ISA_MASK);
}
其它 bits
- has_assoc
对象含有或者曾经含有关联引用,没有关联引用的可以更快地释放内存 - weakly_referenced
对象被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放 - deallocating
对象正在释放内存 - has_sidetable_rc
对象的引用计数太大了,存不下 - extra_rc
对象的引用计数超过 1,会存在这个这个里面,如果引用计数为 10,extra_rc 的值就为 9
ObjC 中类的结构图
-
isa
是指向元类的指针,不了解元类的可以看 Classes and Metaclasses -
super_class
指向当前类的父类 -
cache
用于缓存指针和vtable
,加速方法的调用 -
bits
就是存储类的方法、属性、遵循的协议等信息的地方
class_data_bits_t 结构体
ObjC 中 class_data_bits_t 的结构体,其中只含有一个 64 位的 bits 用于存储与类有关的信息。
在 objc_class 结构体中的注释写到 class_data_bits_t 相当于 class_rw_t 指针加上 rr/alloc 的标志。
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
它为我们提供了便捷方法用于返回其中的 class_rw_t * 指针:
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
将 bits 与 FAST_DATA_MASK 进行位运算,只取其中的 [3, 47] 位转换成 class_rw_t * 返回。
- isSwift()
FAST_IS_SWIFT 用于判断 Swift 类 - hasDefaultRR()
FAST_HAS_DEFAULT_RR 当前类或者父类含有默认的 retain/release/autorelease/retainCount/_tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference 方法
requiresRawIsa() - FAST_REQUIRES_RAW_ISA
当前类的实例需要 raw isa
执行 class_data_bits_t 结构体中的 data() 方法或者调用 objc_class 中的 data() 方法会返回同一个 class_rw_t * 指针。
class_rw_t 和 class_ro_t
ObjC 类中的属性、方法还有遵循的协议等信息都保存在 class_rw_t 中。
其中还有一个指向常量的指针 ro,其中存储了当前类在编译期就已经确定的属性、方法以及遵循的协议。
然后在加载 ObjC 运行时的过程中在 realizeClass 方法中:
- 从 class_data_bits_t 调用 data 方法,将结果从 class_rw_t 强制转换为 class_ro_t 指针
- 初始化一个 class_rw_t 结构体
- 设置结构体 ro 的值以及 flag
- 最后设置正确的 data
下图是 realizeClass 方法执行过后的类所占用内存的布局。
[图片上传失败...(image-ac188c-1593190339947)]
但是,在这段代码运行之后 class_rw_t 中的方法,属性以及协议列表均为空。这时需要 realizeClass 调用 methodizeClass 方法来将类自己实现的方法(包括分类)、属性和遵循的协议加载到 methods、 properties 和 protocols 列表中。
深度揭秘Runtime原理
创建实例对象时,会根据其对应的Class分配内存,内存构成是ivars+isa_t。并且实例变量不只包含当前Class的ivars,也会包含其继承链中的ivars。ivars的内存布局在编译时就已经决定,运行时需要根据ivars内存布局创建对象,所以Runtime不能动态修改ivars,会破坏已有内存布局。
ivar读写
在class_ro_t中ivars是const只读的,在image load时copy到class_rw_t中时,是不会copy ivars的,并且class_rw_t中并没有定义ivars的字段。
在访问某个成员变量时,直接通过isa_t找到对应的objc_class,并通过其class_ro_t的ivar list做地址偏移,查找对应的对象内存。正是由于这种方式,所以对象的内存地址是固定不可改变的。
方法传参
当调用实例变量的方法时,会通过objc_msgSend()发起调用,调用时会传入self和SEL。函数内部通过isa在类的内部查找方法列表对应的IMP,传入对应的参数并发起调用。如果调用的方法时涉及到当前对象的成员变量的访问,这时候就是在objc_msgSend()内部,通过类的ivar list判断地址偏移,取出ivar并传入调用的IMP中的。
调用super的方式时则调用objc_msgSendSuper()
函数实现,调用时将实例变量的父类传进去。但是需要注意的是,调用objc_msgSendSuper
函数时传入的对象,也是当前实例变量,所以是在向自己发送父类的消息。
程序加载
深度揭秘Runtime原理
在应用程序启动后,由dyld(the dynamic link editor)
进行程序的初始化操作。大概流程就像下面列出的步骤,其中第3、4、5步会执行多次,在ImageLoader
加载新的image
进内存后就会执行一次。
- 在引用程序启动后,由
dyld
将应用程序加载到二进制中,并完成一些文件的初始化操作。 -
Runtime
向dyld
中注册回调函数。 - 通过
ImageLoader
将所有image
加载到内存中。 -
dyld
在image
发生改变时,主动调用回调函数。 -
Runtime
接收到dyld
的函数回调,开始执行map_images
、load_images
等操作,并回调+load
方法。 - 调用
main()
函数,开始执行业务代码。
ImageLoader
是image
的加载器,image
可以理解为编译后的二进制。
下面是在Runtime
的map_images
函数打断点,观察回调情况的汇编代码。可以看出,调用是由dyld
发起的,由ImageLoader
通知dyld
进行调用。
关于
dyld
我并没有深入研究,有兴趣的同学可以到Github上下载源码研究一下。
动态加载
一个OC程序可以在运行过程中动态加载和链接新类或Category,新类或Category会加载到程序中,其处理方式和其他类是相同的。动态加载还可以做许多不同的事,动态加载允许应用程序进行自定义处理。
OC提供了objc_loadModules运行时函数,执行Mach-O中模块的动态加载,在上层NSBundle对象提供了更简单的访问API。
map images
在Runtime加载时,会调用_objc_init函数,并在内部注册三个函数指针。其中map_images函数是初始化的关键,内部完成了大量Runtime环境的初始化操作。
在map_images函数中,内部也是做了一个调用中转。然后调用到map_images_nolock函数,内部核心就是_read_images函数。
void map_images_nolock(unsigned mhCount, const char * const mhPaths[],
const struct mach_header * const mhdrs[])
{
if (hCount > 0) {
_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
}
}
在_read_images函数中完成了大量的初始化操作,函数内部代码量比较大,下面是精简版带注释的源代码。
先整体梳理一遍_read_images函数内部的逻辑:
- 加载所有类到类的gdb_objc_realized_classes表中。
- 对所有类做重映射。
- 将所有SEL都注册到namedSelectors表中。
- 修复函数指针遗留。
- 将所有Protocol都添加到protocol_map表中。
- 对所有Protocol做重映射。
- 初始化所有非懒加载的类,进行rw、ro等操作。
- 遍历已标记的懒加载的类,并做初始化操作。
- 处理所有Category,包括Class和Meta Class。
- 初始化所有未初始化的类。
load images
load类方法的调用时机比main函数还要靠前。load方法是由系统来调用的,并且在整个程序运行期间,只会调用一次,所以可以在load方法中执行一些只执行一次的操作。
你真的了解 load 方法么?
通过 _getObjc2NonlazyClassList 获取所有的类的列表之后,会通过 remapClass 获取类对应的指针,然后调用 schedule_class_load 递归地安排当前类和没有调用 + load 父类进入列表。
在执行 add_class_to_loadable_list(cls) 将当前类加入加载列表之前,会先把父类加入待加载的列表,保证父类在子类前调用 load 方法。
一般Method Swizzling都会放在load方法中执行,这样在执行main函数前,就可以对类方法进行交换。可以确保正式执行代码时,方法肯定是被交换过的。
如果对一个类添加Category后,并且重写其原有方法,这样会导致Category中的方法覆盖原类的方法。但是load方法却是例外,所有Category和原类的load方法都会被执行。
iOS +load 与 +initialize
调用时机比较早,运行环境有不确定因素。具体说来,在iOS上通常就是App启动时进行加载,但当load调用的时候,并不能保证所有类都加载完成且可用,必要时还要自己负责做auto release处理。
- 关于继承:对于一个类而言,没有+load方法实现就不会调用,不会考虑对NSObject的继承,就是不会沿用父类的+load。
- 父类和本类的调用:父类的方法优先于子类的方法。一个类的+load方法不用写明[super load],父类就会收到调用。
- 本类和Category的调用:本类的方法优先于类别(Category)中的方法。Category的+load也会收到调用,但顺序上在本类的+load调用之后。
- 对于有依赖关系的两个库中,被依赖的类的+load会优先调用。但在一个库之内,父、子类、类别之间调用有顺序,不同类之间调用顺序是不确定的。
- 不会直接触发initialize的调用。
在一个新的工程中,我们创建一个TestObject
类,并在其load
方法中打一个断点,看一下系统的调用堆栈。
从调用栈可以看出,是通过系统的动态链接器dyld
开始的调用,然后调到Runtime
的load_images
函数中。load_images
函数是通过_dyld_objc_notify_register
函数,将自己的函数指针注册给dyld
的。
在load_images函数中主要做了两件事,首先通过prepare_load_methods函数准备Class load list和Category load list,然后通过call_load_methods函数调用已经准备好的两个方法列表。
void load_images(const char *path __unused, const struct mach_header *mh)
{
if (!hasLoadMethods((const headerType *)mh)) return;
prepare_load_methods((const headerType *)mh);
call_load_methods();
}
initialize
iOS +load 与 +initialize
+initialize 方法的调用与普通方法的调用是一样的,走的都是发送消息的流程。换言之,如果子类没有实现 +initialize 方法,那么继承自父类的实现会被调用;如果一个类的分类实现了 +initialize 方法,那么就会对这个类中的实现造成覆盖。
- initialize的自然调用是在第一次主动使用当前类的时候。
- 在initialize方法收到调用时,运行环境基本健全。
- 关于继承:和load不同,即使子类不实现initialize方法,会把父类的实现继承过来调用一遍,就是会沿用父类的+initialize。(沿用父类的方法中,self还是指子类)
- 父类和本类的调用:子类的+initialize将要调用时会激发父类调用的+initialize方法,所以也不需要在子类写明[super initialize]。(本着除主动调用外,只会调用一次的原则,如果父类的+initialize方法调用过了,则不会再调用)
- 本类和Category的调用:Category中的+initialize方法会覆盖本类的方法,只执行一个Category的+initialize方法。
Category
Category如何加载
深入理解Objective-C:Category
Category被附加到类上面是在map_images的时候发生的。
大致 Category 加载、添加附加内容到对应的 Class 的调用链条:_objc_init -> map_images -> _read_images -> unattachedCategoriesForClass -> remethodizeClass -> attachCategories -> attachLists。
在new-ABI的标准下,_objc_init里面的调用的map_images最终会调用objc-runtime-new.mm里面的_read_images方法。
addUnattachedCategoryForClass只是把 类 和 Category 做一个关联映射,而remethodizeClass才是真正去处理。
而对于添加类的实例方法而言,又会去调用attachCategoryMethods这个方法。
attachCategoryMethods做的工作相对比较简单,它只是把所有Category的实例方法列表拼成了一个大的实例方法列表,然后转交给了attachMethodLists方法。
Category 原理解析
// Attach method lists and properties and protocols from categories to a class.
// Assumes the categories in cats are all loaded and sorted by load order,
// oldest categories first.
// Class 加载完之后会加载分类列表并添加到类结构体中去
static void
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return; // 判断有没有分类列表
// 打印需要替换的方法
if (PrintReplacedMethods) printReplacements(cls, cats);
// 是否为元类
bool isMeta = cls->isMetaClass();
// fixme rearrange to remove these intermediate allocations
// 分配相应的实例/类方法、属性、协议列表指针,相当于二维链表,一个分类对应一个一维链表
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists));
protocol_list_t **protolists = (protocol_list_t **)
malloc(cats->count * sizeof(*protolists));
// Count backwards through cats to get newest categories first
int mcount = 0;
int propcount = 0;
int protocount = 0;
int i = cats->count;
bool fromBundle = NO;
// 循环分类列表
while (i--) {
// 取出第 i 个分类
auto& entry = cats->list[I];
// 从分类里取出对应的实例/类方法表
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
// 将实例/类方法列表赋值到对应的二维链表中
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}
// 从分类里取出对应的实例/类属性列表,并加到对应的二维链表中
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
proplists[propcount++] = proplist;
}
// 从分类里取出遵守的协议列表,并加到对应的二维链表中
protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist;
}
}
// 遍历完分类后,取出类/元类加载到内存(堆区)的 class_rw_t 结构体
auto rw = cls->data();
// 准备方法列表:加锁扫描方法列表,将新方法放在每一个分类的方法前面(对每个分类方法进行排序)
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
// 添加方法到类/元类中
rw->methods.attachLists(mlists, mcount);
// 释放二维方法列表
free(mlists);
// 刷新方法缓存
if (flush_caches && mcount > 0) flushCaches(cls);
// 添加属性到类/元类中
rw->properties.attachLists(proplists, propcount);
// 释放二维属性列表
free(proplists);
// 添加遵守的协议到类/元类中
rw->protocols.attachLists(protolists, protocount);
// 释放二维协议列表
free(protolists);
}
Category的方法没有“完全替换掉”原来类已经有的方法,也就是说如果Category和原来类都有methodA,那么Category附加完成之后,类的方法列表里会有两个methodA
Category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休。
Category 加载原理
在 App 启动加载镜像文件时,会在 _read_images 函数间接调用到 attachCategories 函数,完成向类中添加 Category 的工作。原理就是向 class_rw_t 中的 method_array_t, property_array_t, protocol_array_t 数组中分别添加 method_list_t, property_list_t, protocol_list_t 指针。
在这里可以看具体的代码验证 通过Runtime源码了解Objective-C中的方法存储
Category和+load方法
在类的+load方法调用的时候,我们可以调用Category中声明的方法么?
可以调用,因为附加Category到类的工作会先于+load方法的执行-
这么些个+load方法,调用顺序是咋样的呢?
+load的执行顺序是先类,后Category,而Category的+load执行顺序是根据编译顺序决定的。
Category 与 Extension 的区别
Category 与 Extension 从使用代码层面上看是没什么区别。
@interface NSObject ()
//添加成员属性
@property (nonatomic, assign) NSInteger age;
//添加方法声明
- (void)agePerson;
@end
以上代码,声明了属性和方法。不过注意的是这里的属性是有成员变量的。而不是需要存储全局的 Hash 表中。其实本质上原因是 Extension 在编译时期就加载到类中,而 Category 却是在运行时期才加载到 Class 结构体中。
Category 可以向 Class 中添加实例方法、类方法、遵守的协议以及成员属性。不过,需要注意的是这里说的是属性不是成员变量,Category 是不可以添加成员变量的。
Category和关联对象
在 Category 中添加属性,就必须自己实现对应的 getter 和 setter 方法。而且不能生成成员变量,所以需要关联对象来存储这些值。
关联对象 AssociatedObject 完全解析
#import "DKObject+Category.h"
#import
@implementation DKObject (Category)
- (NSString *)categoryProperty {
return objc_getAssociatedObject(self, _cmd);
}
- (void)setCategoryProperty:(NSString *)categoryProperty {
objc_setAssociatedObject(self, @selector(categoryProperty), categoryProperty, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
使用了两个方法 objc_getAssociatedObject
以及 objc_setAssociatedObject
来模拟『属性』的存取方法,而使用关联对象模拟实例变量。
@selector(categoryProperty)
也就是参数中的 key,其实可以使用静态指针 static void *
类型的参数来代替,不过在这里,笔者强烈推荐使用 @selector(categoryProperty)
作为 key 传入。因为这种方法省略了声明参数的代码,并且能很好地保证 key 的唯一性。
关联政策是一组枚举常量:
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, /**< Specifies a weak reference to the associated object. */
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object.
* The association is not made atomically. */
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**< Specifies that the associated object is copied.
* The association is not made atomically. */
OBJC_ASSOCIATION_RETAIN = 01401, /**< Specifies a strong reference to the associated object.
* The association is made atomically. */
OBJC_ASSOCIATION_COPY = 01403 /**< Specifies that the associated object is copied.
* The association is made atomically. */
};
关联对象原理
关联对象方法 void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
实际上是调用 _object_set_associative_reference
方法进行关联。先看以下方法代码:
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// retain the new value (if any) outside the lock.
ObjcAssociation old_association(0, nil);
id new_value = value ? acquireValue(value, policy) : nil;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
// break any existing association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// secondary table exists
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
j->second = ObjcAssociation(policy, new_value);
} else {
(*refs)[key] = ObjcAssociation(policy, new_value);
}
} else {
// create the new association (first time).
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();
}
} else {
// setting the association to nil breaks the association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
refs->erase(j);
}
}
}
}
// release the old value (outside of the lock).
if (old_association.hasValue()) ReleaseValue()(old_association);
}
关联对象是由 AssociationsManager 管理,接下来看其代码:
// class AssociationsManager manages a lock / hash table singleton pair.
// Allocating an instance acquires the lock, and calling its assocations()
// method lazily allocates the hash table.
spinlock_t AssociationsManagerLock;
class AssociationsManager {
// associative references: object pointer -> PtrPtrHashMap.
static AssociationsHashMap *_map;
public:
AssociationsManager() { AssociationsManagerLock.lock(); }
~AssociationsManager() { AssociationsManagerLock.unlock(); }
AssociationsHashMap &associations() {
if (_map == NULL)
_map = new AssociationsHashMap();
return *_map;
}
};
AssociationsManager
是一个静态的全局 AssociationsHashMap
,用来存储所有的关联对象,key是对象的内存地址,value则是另一个 AssociationsHashMap
,其中存储了关联对象的 key-value。对象销毁的工作则交给 objc_destructInstance(id obj)
。
AssociationsManager
维护了 spinlock_t
和 AssociationsHashMap
的单例,初始化它的时候会调用 lock.lock()
方法,在析构时会调用 lock.unlock()
,而 associations
方法用于取得一个全局的 AssociationsHashMap
单例。
也就是说 AssociationsManager
通过持有一个自旋锁 spinlock_t
保证对 AssociationsHashMap
的操作是线程安全的,即每次只会有一个线程对 AssociationsHashMap 进行操作。
关联对象在内存中的存储
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [NSObject new];
objc_setAssociatedObject(obj, @selector(hello), @"Hello", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return 0;
}
instanceStart 和 instanceSize
entsize_list_tt 实现了 non-fragile 特性的数组结构。假如苹果在新版本的 SDK 中向 NSObject 类增加了一些内容,NSObject 的占据的内存区域会扩大,开发者以前编译出的二进制中的子类就会与新的 NSObject 内存有重叠部分。于是在编译期会给 instanceStart 和 instanceSize 赋值,确定好编译时每个类的所占内存区域起始偏移量和大小,这样只需将子类与基类的这两个变量作对比即可知道子类是否与基类有重叠,如果有,也可知道子类需要挪多少偏移量。