【objc4-NSObject】(一):本质、size、alloc、

852 阅读21分钟

NSObject

探索NSObject的本质可以有很多方法,查看objc4(本文使用版本:objc4-818.2)的源码,还可以通过编译器将OC代码转成C++代码,还可以通过断点的方式进入汇编一步一步跟,下面结合起来看一下NSObject的本质。

代码转译

  1. 前期准备,首先新建一个命令行工程,在main.m添加一个Person类,代码如下: image.png
  2. 对代码进行转译,可以窥探一些内部实现,通过命令行进入当前main.m所在文件,输入一下命令(命令详细内容不做陈述,可自行Google): xcrun --sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o arm64-main.mm 这样就会生成一个对应的arm64-main.mm文件。到此我们的转译就完成了。

探索本质

Objective-C中的对象本质是什么?

NSObject

arm64-main.mm中搜索NSObject,找到了如下代码:

struct NSObject_IMPL {
    Class isa;
};

struct 结构体,IMPL其实就是implementation的缩写,所以此处应该是NSObject的数据结构实现->结构体。 其实上面的结构体等同于objc_object的结构体,

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

Class

NSObject有一个Class类型的isaClass就是:

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

Class本质就是指向objc_class结构体的指针,在想查看objc_class是什么,就要查看objc4的源码了。

struct objc_class : objc_object {
    objc_class(const objc_class&) = delete;
    objc_class(objc_class&&) = delete;
    void operator=(const objc_class&) = delete;
    void operator=(objc_class&&) = delete;
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    ... 下面就是一些函数了
}

可以看到objc_class是一个继承自objc_object的结构体,也就是说Class也可以看成是一个OC对象,结构里面的delete是C++11里的特性,具体可查看c++11 类默认函数的控制:"=default" 和 "=delete"函数

自定义类

自定义的Person类被转译成了这样:

struct Person_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
};

也是一个结构体,里面存放了一个NSObject_IMPL的结构体,等同于下面的定义:

 struct Person_IMPL {
     Class isa;
 };

到这里可以看到,OC对象是基于结构体来实现的。

其他

id

typedef struct objc_object *id;

id是objc_object结构体的指针,和NSObject还是有一定区别的。

SEL

typedef struct objc_selector *SEL;

对象创建的流程

对象创建流程中会有slowpath、fastpath的使用:

#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))

其原理可查看__builtin_expect 说明 这篇文章。

关键的方法

alloc

+ (id)alloc {
    return _objc_rootAlloc(self);
}

NSObject的类方法alloc,会向下调用_objc_rootAlloc,并把当前类传入。

objc_alloc

// Calls [cls alloc].
id
objc_alloc(Class cls)
{
    return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}

方法直接调用callAlloc,查看此处传入的参数allocWithZone为false。

callAlloc

// Call [cls alloc] or [cls allocWithZone:nil], with appropriate
// shortcutting optimizations.
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
    if (slowpath(checkNil && !cls)) return nil;
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        return _objc_rootAllocWithZone(cls, nil);
    }
#endif
    // No shortcuts available.
    if (allocWithZone) {
        return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
    }
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}

这个方法对应的分支就比较多了:

  1. 第一个分支:#if __OBJC2__,这个预预编译肯定会进,最新版的runtime 就是OBJC2版本,所以里面的代码肯定会执行;
  2. 第二个分支:检查传入的类是否存在
    • 存在:继续向下执行;
    • 不存在:直接返回nil
  3. 第三个分支:查看当前类是否有allocWithZone方法
    • 有:不进入代码块,继续向下执行;
    • 没有:进入代码块,直接调用_objc_rootAllocWithZone
  4. 第四个分支:根据传入的allocWithZone参数判断是否要通过objc_msgSend调用allocWithZone方法;
  5. 第五个分支:通过objc_msgSend调用类的alloc方法;

_objc_rootAllocWithZone

NEVER_INLINE
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
    // allocWithZone under __OBJC2__ ignores the zone parameter
    return _class_createInstanceFromZone(cls, 0, nil,
                                         OBJECT_CONSTRUCT_CALL_BADALLOC);
}

直接调用_class_createInstanceFromZone方法。

_class_createInstanceFromZone

/***********************************************************************
* class_createInstance
* fixme
* Locking: none
*
* Note: this function has been carefully written so that the fastpath
* takes no branch.
**********************************************************************/
static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
                              int construct_flags = OBJECT_CONSTRUCT_NONE,
                              bool cxxConstruct = true,
                              size_t *outAllocatedSize = nil)
{
    ASSERT(cls->isRealized());
    // Read class's info bits all at once for performance
    bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();
    size_t size;
    
    size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;
    
    id obj;
    if (zone) {
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        obj = (id)calloc(1, size);
    }
    
    if (slowpath(!obj)) {
        if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
            return _objc_callBadAllocHandler(cls);
        }
        return nil;
    }

    if (!zone && fast) {
        obj->initInstanceIsa(cls, hasCxxDtor);
    } else {
        // Use raw pointer isa on the assumption that they might be
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

    if (fastpath(!hasCxxCtor)) {
        return obj;
    }
    
    construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
    return object_cxxConstructFromClass(obj, cls, construct_flags);
}

这个方法有3个步骤,计算类在内存中占用大小,根据大小进行开辟内存空间,在进行isa指针绑定,其他的是一些异常边界处理和C++扩展的处理。

创建流程

上面介绍了对象创建的几个关键方法,此处简单介绍一下对象创建流程,objc-4官网源码无法运行,所以使用了objc_debug。下面是创建流程走到最后一步_class_createInstanceFromZone的栈追踪。获取方式:在_class_createInstanceFromZone方法内加上断点,当代码运行到此断点时,在lldb调试输入 bt 即可获得。可以追溯代码运行过程。当然还可以通过断点调试一步一步查看。

* thread #1, queue = 'com.apple.main-thread', stop reason = step over
  * frame #0: 0x00000001002fc124 libobjc.A.dylib`_objc_rootAllocWithZone [inlined] _class_createInstanceFromZone(cls=Person, extraBytes=0, zone=0x0000000000000000, construct_flags=2, cxxConstruct=true, outAllocatedSize=0x0000000000000000) at objc-runtime-new.mm:7986:9
    frame #1: 0x00000001002fc040 libobjc.A.dylib`_objc_rootAllocWithZone(cls=Person, zone=0x0000000000000000) at objc-runtime-new.mm:8021
    frame #2: 0x00000001002fbc5c libobjc.A.dylib`_objc_rootAlloc [inlined] callAlloc(cls=Person, checkNil=false, allocWithZone=true) at NSObject.mm:1932:16
    frame #3: 0x00000001002fbbf0 libobjc.A.dylib`_objc_rootAlloc(cls=Person) at NSObject.mm:1949
    frame #4: 0x000000010035b838 libobjc.A.dylib`+[NSObject alloc](self=Person, _cmd="alloc") at NSObject.mm:2547:12
    frame #5: 0x0000000100359a1c libobjc.A.dylib`objc_alloc [inlined] callAlloc(cls=Person, checkNil=true, allocWithZone=false) at NSObject.mm:1940:12
    frame #6: 0x000000010035996c libobjc.A.dylib`objc_alloc(cls=Person) at NSObject.mm:1956
    frame #7: 0x0000000100003c64 KCObjcBuild`main(argc=1, argv=0x000000016fdff4a0) at main.m:24:28 [opt]
    frame #8: 0x000000019e445430 libdyld.dylib`start + 4

简单介绍一些上面这些数据该如何解读,拿frame #6举例,当前要执行的是Person *person = [Person alloc];方法:

  • #7:后面0x000...是方法的内存地址,
  • libobjc.A.dylib是方法所在库;
  • objc_alloc是方法名,(`后面就是方法名)。
  • (cls=Person),括号内是执行方法要传入的参数
  • at后面是方法在源文件的位置,没有源码的话是没有该数据的。
  • [inlined],内联函数,优化频繁调用的函数大量消耗栈空间(栈内存)的问题。 我们要从frame #6开始查看。

frame #6 - objc_alloc

  • 调用方法:objc_alloc
  • 传入参数:传入的是Person
  • 调用说明:此处是编译器做了优化,直接从[Person alloc]到了此处。

frame #5 - callAlloc

  • 调用方法:callAlloc
  • 传入参数:
    1. cls=Person, Person类
    2. checkNil=true, 执行判空处理
    3. allocWithZone=false, 不执行allocWithZone:方法。
  • 调用说明:这是第一次进入callAlloc方法,根据传入的参数在看该方法内的执行逻辑,要么会执行_objc_rootAllocWithZone,要么会通过objc_msgSend调用alloc方法。通过下一帧frame #4可以看到,是走到alloc方法。

frame #4 - alloc

  • 调用方法:+[NSObject alloc]
  • 传入参数:(以下为OC方法默认追加参数,分别放在第一、第二位)
    1. self=Person,类。
    2. _cmd="alloc", 当前执行的方法。
  • 调用说明:此处调用的是+[NSObject alloc]方法,是_objc_rootAllocWithZone调起的。

frame #3 - _objc_rootAlloc

  • 调用方法:_objc_rootAlloc
  • 传入参数:传入的是Person类。
  • 调用说明:alloc内直接调用该方法。

frame #2 - callAlloc

  • 调用方法:callAlloc
  • 传入参数:
    1. cls=Person Person类
    2. checkNil=false 不进行判空处理
    3. allocWithZone=true 需要执行allocWithZone:方法
  • 调用说明:此处是通过_objc_rootAlloc内调起的callAlloc方法,这是第二次进入callAlloc方法,此处传入的参数与frame #5传入的参数有些区别,checkNil变成了false,这样可以快速跳过cls的异常处理。allocWithZone变成了true,但是最后并没有通过objc_msgSend调用alloc方法。(而是调用了_objc_rootAllocWithZone方法,传入参数:cls=Person, zone=0x0000000000000000。)

frame #1 - _objc_rootAllocWithZone

  • 调用方法:_objc_rootAllocWithZone
  • 传入参数:
    1. cls=Person Person类
    2. zone=0x0000000000000000 指向nil的内存地址
  • 调用说明:方法内会直接调用_class_createInstanceFromZone方法。

frame #0 - _class_createInstanceFromZone

  • 调用方法:_class_createInstanceFromZone
  • 传入参数:
    1. cls=Person, 类
    2. extraBytes=0, 是否需要申请额外的内存空间
    3. zone=0x0000000000000000, 是否需要通过malloc_zone_calloc方式开辟内存
    4. construct_flags=2, 创建失败的处理
    5. cxxConstruct=true, 是否需要调用C++的构造函数,还要依赖于当前类是否实现构造函数。
    6. outAllocatedSize=0x0000000000000000, 抛出当前对象创建所需要的内存空间大小。
  • 调用说明:这是创建流程的最后一步,计算大小、开辟空间、绑定isa指针。

流程图

没有实现allocWithZone方法流程图如下: NSObject.png

疑问

  1. 为什么callAlloc方法会执行两次?
  2. 编译器是如何优化alloc方法的(从alloc直接调用到了objc_alloc),目的是什么? llvmalloc进行了hook,调用alloc是会先调用objc_alloc,此时会标记objc_alloc已经被调用过,接下来会走到callAlloc方法,里面会通过objc_msgSend调用alloc方法,由于之前的标记,此时不会在调用objc_alloc方法。

再此hook猜测应该是要做一些内存上的监控吧。

对象在内存中所占的大小

想要了解对象在内存中的大小,我们先要了解内存大小的单位,以及基本数据类型在内存中所占大小,以及对象的本质,是如何表示一个对象的。

位(bit):

计算机内最小的存储单元是位(bit),一只能用0或者1来表示,即二进制(还有八进制、十六进制,一个十六进制包含4个二进制,也就是4bit)。如果用一个二进制表示十进制里的3,应该是11

字节(byte):

一个字节包含8,里面有8个bit,8个二进制数据。这里就可以引申一个问题,为什么int的取值范围是-2147483648(-2^31)~2147483647(2^31-1)?

这里解释的不太对,详细的可以查看下面的内容

一个int数据在内存中占有4个字节(32位)int存储的是整数,其中也包括负数,表示数据的正负需要一位,首位是0表示正数,首位是1表示负数,那么就只剩下31位去真正表示数据了。

  • -2的二进制是 1001
  • -1的二进制是 1111
  • 0的二进制是 0000
  • 1的二进制是 0001 这样算下来int的最大值就不是2^32,而需要减少一位了,也就是最大值为2^31-1,最小值就是-2^32.还会有疑问为啥最大值要做-1操作,而最小值不用?
  • 最大值:二进制0111(这里的首位0表示正整数)能表示的数是0~7,也就是0~2^3-1,这样31位二进制能表示的值就是0~2^31-1,所以最大值就是2^31-1(2147483647)。
  • 最小值:整数里的负数不是从0开始的,而是从-1开始的,-1的二进制是10,此时最后一位二进制的0是表示1,那二进制1111(这里的首位1表示负整数)表示的数时-1~-8,也就是-1~-2^3,所以最小值也就是-2^31。 最后总结:整数的基本数据类型的取值范围要看是否有符号
  • 无符号0~2^n-1(因为是无符号,所以不需要首位表示正负数了)
  • 有符号-2^(n-1)~2^(n-1)-1 其中n为该数据类型在内存中所占的bit总数。

基本数据类型

下表是基础数据类型在内存中占用大小(字节)

类型32位64位
BOOL11
char11
unsigned char11
short22
unsigned short22
int44
unsigned int44
long48
unsigned long48
long long88
NSInteger48
float44
double88
CGFloat48
指针48

结构体

如果想要创建一个对象,首页我们要知道当前对象在内存中需要开辟多少空间,占用多少字节,上面已经介绍过了OC对象是基于结构体来实现的,想要知道一个对象占用多少空间,就要知道该对象转换后的结构体在内存中占用多少空间,结构体大小要遵循一个内存对齐原则:

  1. 结构体变量的起始地址能够被其最宽的成员大小整除;

  2. 结构体每个成员相对于起始地址的偏移能够被其自身大小整除,如果不能则在前一个成员后面补充字节;

  3. 结构体总体大小能够被最宽的成员的大小整除,如不能则在后面补充字节; 下面举例说明一下:

    struct x1 {
        int a;
        char b;
        char c;
    } x1;
    

    假如内存从0x00开始:

    • int aint类型在内存中占用4个字节,成员变量a占用0x000x34个字节
    • 成员变量char b和成员变量char c分别各占用1个字节,这样成员变量b占用0x04,成员变量c占用0x05
    • 基于规则3,需要在0x05后补充字节,补充到总体大小能被最宽成员变量a(4)整除,所以需要填充0x060x07两个字节
    • 最后结构体x1占用8个字节
    struct x2 {
        char a;
        int b;
        char c;
    } x2;
    

    假如内存从0x00开始:

    • char achar类型在内存中占用1个字节,成员变量a占用0x00
    • int bint类型在内存中占用4个字节,基于规则2,当前成员变量b的起始地址的偏移为1,并不能被自身大小(4)整除,所以要在成员变量a后面填充0x010x020x03,从0x04开始距离起始地址的偏移为4,能被4整除,成员变量b就站用了0x040x07
    • 最后成员变量c占用0x08,基于规则3,需要填充后3个字节0x090x011
    • 最后结构体x2占用12个字节
    struct x3 {
        char a;
        char b;
        int c;
    } x3;
    

    假如内存从0x00开始:

    • 成员变量char a和成员变量char b分别各占用1个字节,成员变量a占用0x00,成员变量b占用0x01
    • 基于规则2,当前成员变量b的起始地址的偏移为2,并不能被自身大小(4)整除,所以要在成员变量b后面填充0x020x03
    • int cint类型在内存中占用4个字节,占用0x040x07
    • 最后结构体x2占用8个字节 根据上面的分析,不难得出上面例子三个结构体的内存布局如下: image.png 最后输出内容:
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            NSLog(@"x1 is %lu",sizeof(x1));  // 输出8
            NSLog(@"x2 is %lu",sizeof(x2));  // 输出12
            NSLog(@"x3 is %lu",sizeof(x3));  // 输出8
        }
        return 0;
    }
    

    在以后定义结构时,最优的方式是将内存占用最小的数据类型放在前面,这样可以较少一些内存开销。

OC对象

oc中有两个方法可以查看对象所占内存大小,一个是在runtime库中,一个是在malloc库中,方法分别是:

#import <objc/runtime.h>
/** 
* Returns the size of instances of a class.
* 
* @param cls A class object.
* 
* @return The size in bytes of instances of the class \e cls, or \c 0 if \e cls is \c Nil.
*/
size_t class_getInstanceSize(Class _Nullable cls)

查看注释,当前方法需要传入一个Class,会返回当前类实例对象在内存中占用大小。

#import <malloc/malloc.h>
/* Returns size of given ptr */
extern size_t malloc_size(const void *ptr)

当前方法是传入一个指针,返回当前指针所指向的内容在内存中所占用大小。

下面看看NSObject对象占用内存大小:

image.png 结合NSObject_IMPL结构体,结构体内就一个指向objc_class结构体的指针,指针在内存(64位)中占用8字节,而malloc_size返回的是16。

class_getInstanceSize

先看看objc4的源码是如何实现class_getInstanceSize方法的:

  1. class_getInstanceSize()实现:

    size_t class_getInstanceSize(Class cls)
    {
        if (!cls) return 0;
        return cls->alignedInstanceSize();
    }
    

    与注释描述的一样,如果传入一个nil,会返回0。

  2. alignedInstanceSize()的实现:

    // Class's ivar size rounded up to a pointer-size boundary.
    uint32_t alignedInstanceSize() const {
        return word_align(unalignedInstanceSize());
    }
    

    该方法是获取当前类里的成员变量的大小,并且做了字节对齐的操作。

    • unalignedInstanceSize()的实现:

      // May be unaligned depending on class's ivars.
      uint32_t unalignedInstanceSize() const {
          ASSERT(isRealized());
          return data()->ro()->instanceSize;
      }
      

      获取类成员变量的大小,内存是否对齐要依赖类里面的对象,如果成员变量只有1个指针,加上类原有的的isa,那当前情况下是对齐的。如果只有一个int类型的数据,那就不是对齐的。

    • word_align()的实现:

      // 64位下,
      // define WORD_MASK 7UL 
      static inline uint32_t word_align(uint32_t x) {
          return (x + WORD_MASK) & ~WORD_MASK;
      }
      

      这是WORD_MASK+1对齐的算法,比如当前是WORD_MASK为7,那就会进行8对齐,换算为8的倍数。 比如当前传入7,7 + 7 = 14; 14的二进制为 00001110 7的二进制为 00000111 在取反之后 11111000 在做&运算: 00001110 & 11111000 ---------- 00001000 最后结果的十进制为8.

class_getInstanceSize方法实质就是取当前类里的成员变量在内存中所占大小,并且要按8字节进行对齐。

可以进行以下测试,自定义一个类Person,里面加一个age的属性,最新的Xcode属性会自动生成成员变量、setter、getter,然后通过class_getInstanceSize方法获取大小,应该是16

image.png 可以看到输出就是16

下面看看Person类的实例在内存中的布局,可通过两种方式查看,一种是通过lldblldb查看内存信息)命令查看,另一种是通过Xcode中Debug->Debug Workflow->View Memory查看。 代码如下: image.png

  1. lldb命令: image.pngPerson创建的实例对象p在内存中占用16个字节,前8个字节存放的是对象的isa指针,指向的是Person的类对象(通过类对象内存地址0x1可以确定iOS中类对象是存放在全局区的, [iOS] 内存五大区),后8个字节存放的是成员变量age的值10。通过isa & 上一个mask可以获取类对象地址,可先自行搜索一下。x/4gx (isa&ISA_MASK)可以查看类对象的内存信息,可以自行尝试。
  2. View Memory: image.png iOS 设备的处理器是基于 ARM 架构的,默认是采用小端模式,所以读取数据的时候要从右向左读取。isa指针就是:0x0100000100b5d4a1,而成员变量age就是:0x000000000000000a在这可以推测出,一个实例对象里面只存储了成员变量,实例对象的方法并没有存储到对象内,对象的方法是存储到了类对象内。

可以自行尝试在添加一个属性,查看内存大小以及存储会发生什么变化。

malloc_size

malloc_sizelibmalloc提供的API,用于获取系统实际分配的内存大小。

oc中开辟内存方式:

使用的方式:
void *calloc(size_t __count, size_t __size);`
在内存中动态地分配`count`个长度为`size`的连续空间,

objc-4里的代码:
obj = (id)calloc(1, size);

传入的count是1,而size是一个计算出来的值。如果我们找到这个size是怎么算出来的,就可以了解NSObject实例对象大小了。

instanceSize

在对象创建过程中最后一步会走到_class_createInstanceFromZone,此处会通过instanceSize方法计算出对象开辟内存所占用的空间大小,在此,我们只要搞懂instanceSize里的代码就知道为什么NSObject在内存中要站用16个字节 了。具体实现代码如下:

inline size_t instanceSize(size_t extraBytes) const {
    if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
        return cache.fastInstanceSize(extraBytes);
    }
    size_t size = alignedInstanceSize() + extraBytes;
    // CF requires all objects be at least 16 bytes.
    if (size < 16) size = 16;
    return size;
}

首先会去从缓存(cache_t)中取InstanceSize,取不到在进行16字节补齐

  • fastInstanceSize

      size_t fastInstanceSize(size_t extra) const
      {
          ASSERT(hasFastInstanceSize(extra));
          if (__builtin_constant_p(extra) && extra == 0) {
              return _flags & FAST_CACHE_ALLOC_MASK16;
          } else {
              size_t size = _flags & FAST_CACHE_ALLOC_MASK;
              // remove the FAST_CACHE_ALLOC_DELTA16 that was added
              // by setFastInstanceSize
              return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
          }
      }
    

暂时只看最后的return align16(size + extra - FAST_CACHE_ALLOC_DELTA16),最后是一个16对齐的算法。

  • align16

     static inline size_t align16(size_t x) {
         return (x + size_t(15)) & ~size_t(15);
     }
    

和前面的word_align是一样的原理。

OC对象是16字节对齐

继承自NSObject的类,所生成的实例在内存占用大小是按照16字节对齐的。下面简单测试一下: 声明了一个Person类,自带一个isa指针,占用8字节,里面有一个name属性,指针类型占用8字节,一个age属性,int类型占用4字节,转换成结构体后,按照结构齐对齐原则应该是共占用24字节,也就是class_getInstanceSize获取到的大小,又根据runtime内存对齐原则,最后生成的对象应该是占用32字节,也就是malloc_size获取到的大小。

image.png 代码中nameage属性都是没有进行赋值操作的,最后也会占用内存,如果一些Model或其他实体类内有大量属性的,可以通过其他方式进行存储减少一些内存占用。

查看libmalloc内的源码,在开辟内存时,里面也会进行16字节对齐。

疑问:

  1. cache_t 是如何关联InstanceSize的,还存储了哪些数据?
  2. runtime有一个16字节对齐的计算,libmalloc内也有,这样不是重复计算了吗?
  3. 结构体、OC对象内部为什么要做内存对齐?是否可以理解这是一个典型的空间换时间的策略?下面参考资料里的内存对齐可以解答。

参考资料:

  1. C/C++内存对齐详解
  2. 如何理解 struct 的内存对齐?
  3. C语言字节对齐、结构体对齐最详细的解释
  4. objc4源码
  5. objc4可运行的源码
  6. __builtin_expect 说明
  7. [inlined],内联函数
  8. [iOS] 内存五大区
  9. c++11 类默认函数的控制:"=default" 和 "=delete"函数