第2课-OC对象原理上-1

第2课-OC对象原理上-1

[TOC]

1.1 alloc对象的指针地址和内存

首先我们看下面代码的执行

ZBPerson *p1 = [ZBPerson alloc];
ZBPerson *p2 = [p1 init];
ZBPerson *p3 = [p1 init];

NSLog(@"%@-%p-%p", p1, p1, &p1);
NSLog(@"%@-%p-%p", p2, p2, &p2);
NSLog(@"%@-%p-%p", p3, p3, &p3);

打印如下:

2022-03-18 19:49:27.611250+0800 001-alloc&init探索-Demo[86438:3286512] -0x6000027c0360-0x7ff7b43f2e58
2022-03-18 19:49:27.611430+0800 001-alloc&init探索-Demo[86438:3286512] -0x6000027c0360-0x7ff7b43f2e50
2022-03-18 19:49:27.611564+0800 001-alloc&init探索-Demo[86438:3286512] -0x6000027c0360-0x7ff7b43f2e48

通过以上代码可以得到:

  • p1执行alloc之后在堆上分配了一块内存,0x6000027c0360,p1指向该内存地址的对象
  • p1,p2,p3指针相同,指向同一个对象
  • 在栈上连续分配了3块内存用来保存变量p1,p2,p3

第2课-OC对象原理上-1_第1张图片

那么接下来我们探索alloc函数底层具体执行了什么?那我们怎么探索底层源码呢?

1.2 底层探索的三种方法

1.2.1 符号断点

在探索之前,我们先了解一下Xcode的符号断点符号断点(Symbolic Breakpoint)

Symbolic Breakpoint为符号断点,可以针对某一个方法(函数)设置断点并暂停执行;有时候,我们并不清楚会在什么情况下调用某一个函数,那我们可以通过符号断点来跟踪获取调用该函数的程序堆栈

Xcode中添加符号断点如下 第2课-OC对象原理上-1_第2张图片

添加之后,会弹出如下设置页面 第2课-OC对象原理上-1_第3张图片

各字段含义如下: Symbol:填入你想设置断点的方法 (例如:-[NSException raise],-号是实例方法,+号是类方法)。 你也可以输入:objc_exception_throw malloc_error_break //跟踪调试释放了2次的对象 -[NSObject doesNotRecognizeSelector:] //向某个object发送没有的方法

Module:填入要设置断点的方法或函数是否在位于dylib中,默认不填。

Conditon:填入条件,例如:(BOOL)[item isEqualToString:@“test”]前面的(BOOL)是必须的。否则console会提示类型不符合,导致条件不能生效。意思是item(NSString)是test时停下。 同样可以写一下判断的方法比如用来确定类类型的isKindOfClass:,确定对象在继承体系中的位置的isMemberOfClass:,判断一个对象是否能接收某个特定消息的respondsToSelector:,判断一个对象是否遵循某个协议的conformsToProtocol:,以及提供方法实现地址的methodForSelector:等。

Ignore:忽略几次。

Action:可在程序断点执行后增加额外动作(Applescript,捕捉动画帧速,调试器命令(lldb),输入log记录,终端命令(shell),播放声音) 例如:Debugger Commond中可填入 po item 输出 item变量的值 bt 表示输出 方法调用堆栈信息

1.2.2 方式1:使用符号断点探索底层函数的执行

首先在alloc函数地方打个断点,执行alloc函数时按住ctrl点击调试小箭头,即可进入到底层函数 第2课-OC对象原理上-1_第4张图片 第2课-OC对象原理上-1_第5张图片 第2课-OC对象原理上-1_第6张图片

如果我们想要进一步了解objc_alloc函数的底层代码,那么我们打一个符号断点,查看底层代码的具体执行逻辑

第2课-OC对象原理上-1_第7张图片 接着再次执行断点我们就可以看到 第2课-OC对象原理上-1_第8张图片 objc_alloc实际会继续向下执行_objc_rootAllocWithZoneobjc_msgSend函数 其中上图中libobjc.a.dylib就是OC底层的动态库,所以我们得出如下结论:

1. alloc函数的执行最后是执行libobjc.a.dylib动态库里面objc_alloc函数

如果我们还想继续查看_objc_rootAllocWithZoneobjc_msgSend函数的底层逻辑,那么可以再将这两个函数设置符号断点,进一步查看底层代码逻辑。

第2课-OC对象原理上-1_第9张图片

1.2.3 方式2:通过汇编探索底层函数的执行

首先我们需要在断点处,打开汇编设置 第2课-OC对象原理上-1_第10张图片

接下来我们就可以看到汇编源码了 第2课-OC对象原理上-1_第11张图片

从汇编结果中我们可以看到,断点下一行 0x10b54fe0a <+58>: callq 0x10b5503c2 ; symbol stub for: objc_alloc 虽然我们看不懂汇编,但是通过call->symbol stub for: objc_alloc我们也可以大致理解,接下来函数会调用objc_alloc这个符号所代表的的函数。

接着ctrl一步步执行,最后还是会执行到OC的动态库 第2课-OC对象原理上-1_第12张图片

1.2.4 方式3:直接通过符号断点确定底层函数的执行

例如我们直接对alloc打上符号断点 第2课-OC对象原理上-1_第13张图片

执行之后可以直接得到libobjc.A.dylib+[NSObject alloc]:` 第2课-OC对象原理上-1_第14张图片

1.2.5 混合方式:汇编结合源码调试分析

我们到苹果开源官网下载底层源码。 https://opensource.apple.com/ https://opensource.apple.com/tarballs/

我们可以搜索objc 第2课-OC对象原理上-1_第15张图片

或者通过另一个网站下载 第2课-OC对象原理上-1_第16张图片

我们这里下载objc4-818.2这个库

下载成功之后打开这个库文件 第2课-OC对象原理上-1_第17张图片

然后在源码中搜索 alloc { 可以得到 第2课-OC对象原理上-1_第18张图片

  • 首先alloc定位到了NSObjec.mm文件,这里是一个Objective-c++汇编文件

  • 执行alloc会执行_objc_rootAlloc

      _objc_rootAlloc(Class cls)
      {
          return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
      }
  • 接着执行callAlloc 第2课-OC对象原理上-1_第19张图片 这里OBJC2是指的OC底层2.0版本的源码,当然还有1.0版本,这里做了版本控制。

    接下来的代码执行,如果不清楚到底走哪个分支,那么可以通过将callAlloc函数设置符号断点来调试查看函数接下来具体执行的内容。

          // 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));

    那么回到我们之前的ZBPerson断点处,我们添加一个_objc_rootAlloc的符号断点,再次执行 第2课-OC对象原理上-1_第20张图片 其中我们使用register read指令读取寄存器的内容。

  • regsiter read: 读取寄存器的内容

  • register read x0: 读取某个寄存器的内容

通过上面的汇编代码我们可以知道函数的执行顺序为, _objc_rootAlloc==》_objc_rootAllocWithZone==》objc_msgSend

所以我们回过头来在看源码:

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));
}

所以#if __OBJC2__分支的代码一定运行了。

1.3 源代码调试&编译优化

1.3.1 源代码编译调试

文章参照:https://juejin.cn/post/6844903959161733133

虽然通过上面的手段,我们能够检测到底层的执行函数,但是具体的实现逻辑还是不清楚。如果能够直接编译调试,像我们自己的代码直接 LLDB 调试,流程跟踪。那简直不要太爽,于是我编译了现在最新的iOS_objc4-838.1

首先下载源码库,源码库的下载上面已经讲过,这里不再重复。 下载链接:https://opensource.apple.com/releases/

编译源码库之前,你需要准备: Mac OS 12.2 Xcode 13.2 objc4-838.1

第2课-OC对象原理上-1_第21张图片

开始配置

配置好的源代码推荐: https://github.com/LGCooci/KCCbjc4_debug

问题:编译objc源码的时候遇到Xcode error: unable to find sdk 'macosx.internal' 第2课-OC对象原理上-1_第22张图片

解决方案: Build Settings -> Base SDK -> macOS 第2课-OC对象原理上-1_第23张图片

问题:文件查漏补缺 第2课-OC对象原理上-1_第24张图片

解决方案:

  • 大家可以通过 Apple source 在 xnu-8019.61.5/bsd/sys/reason.h 路径自行下载
  • 还可以通过谷歌中输入reason.h site:opensource.apple.com 定向检索第2课-OC对象原理上-1_第25张图片

把找到的文件加入到工程里面。例如:

  • 我在根目录创建了一个 ZBCommon 文件
  • 创建 sys 文件
  • 把 reason.h 文件加入进去

第2课-OC对象原理上-1_第26张图片 目前还不行,一定给我们的工程设置文件检索路径

第2课-OC对象原理上-1_第27张图片

这里之所以这么配置是因为,源代码中通过# include 进行引用,所以我们需要在ZBCommon目录下载配置一个sys文件夹,然后将ZBCommon的检索路径配置到项目中。

其他文件找不到,通过上面相同的方法进行配置

’mach-o/dyld_priv.h’ file not found 下载dyld库

在 dyld库/include/mach-o 目录下找到 dyld_priv.h,copy 到 ZBCommon/mach-o/

file not found

下载库libplatform

在 libplatform库/private/os 目录下找到 lock_private.h,copy 到 ZBCommon/mach-o/

’os/feature_private.h’ file not found

直接注释报错处,此头文件引用

dyld_priv.h 文件中,error: expected ‘,’ extern dyld_platform_t dyld_get_base_platform(dyld_platform_t platform) __API_AVAILABLE(macos(10.14), ios(12.0), watchos(5.0), tvos(12.0), bridgeos(3.0))

删除此文件中报错方法中的 bridgeos(3.0) 参数。

Use of undeclared identifier 'dyld_platform_version_macOS_10_13’

直接注释掉报错处,如下代码

if (!dyld_program_sdk_at_least(dyld_platform_version_macOS_10_13)) {
    DisableInitializeForkSafety = true;
    if (PrintInitializing) {
        _objc_inform("INITIALIZE: disabling +initialize fork "
                     "safety enforcement because the app is "
                     "too old.)");
    }
}

Use of undeclared identifier 'dyld_fall_2020_os_versions’

注释掉如下代码:

if (!dyld_program_sdk_at_least(dyld_fall_2020_os_versions))
DisableAutoreleaseCoalescingLRU = true;

Use of undeclared identifier ‘CRGetCrashLogMessage’

解决方法:target -> Build Settings -> Preprocessor Macros 添加LIBC_NO_LIBCRASHREPORTERCLIENT,如下图。

第2课-OC对象原理上-1_第28张图片

Mismatch in debug-ness macros 错误

直接将代码注释掉

objc-runtime.mm 文件中报 Use of undeclared identifier 'objc4’ 直接注释掉如下代码

if (!os_feature_enabled_simple(objc4, preoptimizedCaches, true)) {
    DisablePreoptCaches = true;
}

Use of undeclared identifier 'dyld_platform_version_macOS_10_11’

直接注释掉如下代码

if (dyld_get_active_platform() == PLATFORM_MACOS && !dyld_program_sdk_at_least(dyld_platform_version_macOS_10_11)) {
    DisableNonpointerIsa = true;
    if (PrintRawIsa) {
        _objc_inform("RAW ISA: disabling non-pointer isa because "
                     "the app is too old.");
    }
}

’Cambria/Traps.h’ file not found 或者 ’Cambria/Cambria.h’ file not found

注释掉报错出对类的引用

Use of undeclared identifier 'oah_is_current_process_translated’

第2课-OC对象原理上-1_第29张图片 修改为 第2课-OC对象原理上-1_第30张图片

Use of undeclared identifier 'dyld_fall_2018_os_versions’

在 objc-runtime-new.mm 文件的报错处,注释掉 initializeTaggedPointerObfuscator 方法内的实现。

’os/feature_private.h’ file not found

直接注释报错处,此头文件引用

’os/reason_private.h’ file not found

在 xnu/libkern/os/ 目录下找到 reason_private.h,copy 到 ZBCommon/os/

’os/variant_private.h’ file not found

在 Libc/os/ 目录下找到 variant_private.h,copy 到 ZBCommon/os/

Use of undeclared identifier 'dyld_platform_version_bridgeOS_2_0' 直接注释掉老sdk兼容代码

if (DebugPoolAllocation || sdkIsAtLeast(10_12, 10_0, 10_0, 3_0, 2_0)) {
    // OBJC_DEBUG_POOL_ALLOCATION or new SDK. Bad pop is fatal.
    _objc_fatal
        ("Invalid or prematurely-freed autorelease pool %p.", token);
}

variant_private.h文件中报API_AVAILABLE(macosx(10.15), ios(13.0), tvos(13.0), watchos(6.0), bridgeos(4.0)) Expected ','

删除此文件中报错方法中的 bridgeos(4.0) 参数

’_static_assert’ declared as an array with a negative size 注释掉报错处的代码

STATIC_ASSERT((~ISA_MASK & MACH_VM_MAX_ADDRESS) == 0  ||  
ISA_MASK + sizeof(void*) == MACH_VM_MAX_ADDRESS);

lCrashReporterClient 编译不到

library not found for -lCrashReporterClient

在 Other Linker Flags 中删除 -lCrashReporterClient ( Debug 和 Release 都删了) 第2课-OC对象原理上-1_第31张图片

library not found for -loah 同理、你懂我意思吧?看上一步:删除 -loah即可。

error: SDK “macosx.internal” cannot be located.

第2课-OC对象原理上-1_第32张图片

至此,终于编译成功了,享受成功的喜悦吧!

接下来我们进行objc编译调试

1.3.2 objc创建Target测试

  • 新建一个Target: ZBTest

    第2课-OC对象原理上-1_第33张图片

  • 绑定二进制依赖关系 第2课-OC对象原理上-1_第34张图片

    注意: 此时你在底层源码中打断点,会发觉断点无效。需要在 Build Settings 中将 Enable Hardened Runtime 设为 NO。

    第2课-OC对象原理上-1_第35张图片

    Build Phases --> Compile Source中,将main文件移至第一位 第2课-OC对象原理上-1_第36张图片

**如果设置之后还是无法调试,那么可以删除Target,重新创建一个Target进行调试**

1.3.3 编译器优化

首先我们看下面一段代码

int lgSum (int a, int b) {
    return a+b;
}
int main(int argc, char * argv[]) {
    int a = 10;
    int b = 20;
    int c = a+b;
    NSLog(@"查看编译器优化情况:%d",c);
    return 0;
}

打开汇编,查看运行结果 第2课-OC对象原理上-1_第37张图片

这里我们有一个问题?为什么保存变量a,b是w8呢,而不是x8呢,我们知道在arm64架构中,一般都是x开头。

这里主要是因为w表示4字节,x表示8字节,这里存储a,b这样简单的数,w足够了,完全没必要使用x

同样我们还能得出另一个结论就是,计算结果保存到了寄存器的第一个x0中。

接下来我们进行一些配置 target->build Setting -> optimization Level: 第2课-OC对象原理上-1_第38张图片

再次运行代码观察

第2课-OC对象原理上-1_第39张图片 很显然,汇编代码明显减少了,这就是因为我们上面的设置对汇编进行了优化。其中有一些不必要的变量直接被优化掉了。

Optimization Level各选项:

None[-O0] 不优化: 在这种设置下, 编译器的目标是降低编译消耗,保证调试时输出期望的结果。 程序的语句之间是独立的:如果在程序的停在某一行的断点出,我们可以给任何变量赋新值抑或是将程序计数器指向方法中的任何一个语句,并且能得到一个和源码完全一致的运行结果。

Fast[-O1] 大函数所需的编译时间和内存消耗都会稍微增加: 在这种设置下,编译器会尝试减小代码文件的大小,减少执行时间,但并不执行需要大量编译时间的优化。 在苹果的编译器中,在优化过程中,严格别名,块重排和块间的调度都会被默认禁止掉。此优化级别提供了良好的调试体验,堆栈使用率也提高,并且代码质量优于None[-O0]。

Faster[-O2] 编译器执行所有不涉及时间空间交换的所有的支持的优化选项: 是更高的性能优化Fast[-O1]。在这种设置下,编译器不会进行循环展开、函数内联或寄存器重命名。 和Fast[-O1]项相比,此设置会增加编译时间,降低调试体验,并可能导致代码大小增加,但是会提高生成代码的性能。

Fastest[-O3] 在开启Fast[-O1]项支持的所有优化项的同时,开启函数内联和寄存器重命名选项: 是更高的性能优化Faster[-O2],指示编译器优化所生成代码的性能,而忽略所生成代码的大小,有可能会导致二进制文件变大。还会降低调试体验。

Fastest, Smallest[-Os] 在不显著增加代码大小的情况下尽量提供高性能: 这个设置开启了Fast[-O1]项中的所有不增加代码大小的优化选项,并会进一步的执行可以减小代码大小的优化。 增加的代码大小小于Fastest[-O3]。与Fast[-O1]相比,它还会降低调试体验。

Fastest, Aggressive, Optimizations[-Ofast]与Fastest, Smallest[-Os] 相比该级别还执行其他更激进的优化: 这个设置开启了Fastest[-O3]中的所有优化选项,同时也开启了可能会打破严格编译标准的积极优化,但并不会影响运行良好的代码。 该级别会降低调试体验,并可能导致代码大小增加。

Smallest, Aggressive Size Optimizations [-Oz] 不使用LTO的情况下减小代码大小: 与-Os相似,指示编译器仅针对代码大小进行优化,而忽略性能优化,这可能会导致代码变慢。

目前项目中主要使用的是Fastest,smallest,这个选项可以开启那些不增加代码大小的全部优化,并且可以让可执行文件尽可能的小。

再看我们之前[ZBPerson alloc]执行过程

第2课-OC对象原理上-1_第40张图片

我们设置了

  1. _objc_rootAllocWithZone
  2. callAlloc
  3. _objc_rootAlloc

三个符号断点,其中运行发现,只执行了1和3,2并没有执行,这里不执行的原因就是编译器的优化造成的。将2给省略了。

1.4 alloc的主线流程

在讲alloc流程之前,我们先复习一下下面的知识

1.4.1 OC代码中的字节对齐

结论: 1.在OC对象中,isa指针永远排在成员变量的最前面 2. 一个对象里面的成员变量如果是固定写死的,而不是通过属性自动生成的,那么字节对齐就会按照写死的顺序进行对齐;如果该对象是继承对象,那么子类对象会按照isa->父类成员变量->子类成员变量的顺序进行字节对齐;如果对象内部包含其他对象指针,那么对象指针按照普通指针计算(64位系统8字节) 3.一个对象里面的成员变量如果是通过属性生成的,那么成员变量的顺序会按照变量类型所占字节的大小从低向高排序。排序目的是为了方便提高访问效率的同时,能够优化存储,减少浪费。如果该对象是继承对象,那么子类对象会按照isa->父类排序的成员变量->子类排序的成员变量的顺序进行字节对齐;如果对象内部包含其他对象指针,那么对象指针按照普通指针计算(64位系统8字节) 4. 一个对象如果既包含写死的成员变量,也包含属性,那么属性自动生成的成员变量会按照变量所占字节大小从低向高排序,排序之后会放在写死的成员变量后面;如果该对象是继承对象,那么会按照上述规则先对父类的成员变量进行排序,然后再对子类的成员变量进行排序

1.4.1.1 isa指针占8字节

假设我们有如下代码:

@interface ZBPerson : NSObject
@end

在main函数中执行 [ZBPerson alloc] 第2课-OC对象原理上-1_第41张图片

我们使用x p1打印变量p1的内存地址,我们可以看到前8个字节3d 83 00 00 01 80 1d 01已经有了内容,为什么前面这8字节已经有了值,后面的全是0呢?那么如何查看前面这8个字节的内容呢。 我们可以通过 po 0x011d80010000833d查看指针内容,注意这里拼接的地址是将前面8个字节的内容从后往前组合而来。因为ios设备中采用的是小端模式

(lldb) po 0x011d80010000833d
80361110145893181

到这里,我们还无法查看到具体的,我们还需要将上面的地址与ISA_MASK进行&与运算 第2课-OC对象原理上-1_第42张图片

(lldb) po 0x011d80010000833d & 0x0000000ffffffff8ULL
ZBPerson

所以我们得到了ZBPerson类,到这里我们也就明白了,其实0x011d80010000833d分配的这8个字节就是存储的isa指针,因为ZBPerson继承NSObject,而NSObject默认有个isa的成员变量 第2课-OC对象原理上-1_第43张图片 所以,一旦一个对象执行alloc函数之后,就会分配内存,系统会将isa指针与当前类绑定,isa指针占用8字节内存。

1.4.1.2 普通对象字节对齐
1.4.1.2.1 普通对象成员变量字节对齐

实例1

@interface Person1 : NSObject
{
    @public
    double _a;
    char _b;
    int _c;
    short _d;
}
@end

@interface Person2 : NSObject
{
@public
    double _a;
    int _b;
    char _c;
    short _d;
}
@end

main函数调用如下:

Person1 *person1 = [[Person1 alloc] init];
person1->_a = 1.0;
person1->_b = 'a';
person1->_c = 2;
person1->_d = 3;
Person2 *person2 = [[Person2 alloc] init];
person2->_a = 10.0;
person2->_b = 20;
person2->_c = 'b';
person2->_d = 30;
NSLog(@"%lu - %lu", class_getInstanceSize([person1 class]), malloc_size((__bridge const void *)(person1)));
NSLog(@"%lu - %lu", class_getInstanceSize([person2 class]), malloc_size((__bridge const void *)(person2)));

// 打印
2022-03-24 15:19:56.672398+0800 001-内存对齐原则-Demo[91720:7275398] 32 - 32
2022-03-24 15:19:57.265459+0800 001-内存对齐原则-Demo[91720:7275398] 24 - 32

我们分析一下字节对齐过程: Person1:

  • isa指针: 8字节 内存分配8字节,补齐0字节 [0, 7]
  • double _a;8字节 内存分配8字节,补齐0字节 [8, 15]
  • char _b; 1字节 内存分配4字节,补齐3字节 [16, 19]
  • int _c; 4字节 内存分配4字节,补齐0字节 [20, 23]
  • short _d; 2字节 内存分配8字节,补齐6字节 [24, 31] 所以内存对齐之后一共32字节

我们验证一下 第2课-OC对象原理上-1_第44张图片

注意: 变量a是double类型,默认是16进制,无法通过po 地址直接打印,需要使用e -f f -- 地址 或者 p/f 地址命令进行打印

person1内存分配如下:

  • person1->isa: 8字节 [0x600000e283e0, 0x600000e283e7]
  • person1->_a: 8字节 [0x600000e283e8, 0x600000e283eF]
  • person1->_b: 4字节 [0x600000e283F0, 0x600000e283F3]
  • person1->_c: 4字节 [0x600000e283F4, 0x600000e283F7]
  • person1->_d: 8字节 [0x600000e283F8, 0x600000e283FF] 验证通过

Person2:

  • isa指针: 8字节 内存分配8字节,补齐0字节 [0, 7]
  • double _a;8字节 内存分配8字节,补齐0字节 [8, 15]
  • int _b; 4字节 内存分配4字节,补齐0字节 [16, 19]
  • char _c; 1字节 内存分配2字节,补齐1字节 [20, 21]
  • short _d; 2字节 内存分配2字节,补齐0字节 [22, 23] 所以内存对齐之后一共24字节, maclloc是16字节对齐,所以是16*2 = 32字节

我们验证一下 第2课-OC对象原理上-1_第45张图片

person1内存分配如下:

  • person1->isa: 8字节 [0x600000e28440, 0x600000e28447]
  • person1->_a: 8字节 [0x600000e28448, 0x600000e2844F]
  • person1->_b: 4字节 [0x600000e28450, 0x600000e28453]
  • person1->_c: 2字节 [0x600000e28454, 0x600000e28455]
  • person1->_d: 2字节 [0x600000e28456, 0x600000e28457] 验证通过

结论: 1. 在字节对齐的过程中,如果对象里面的成员变量顺序已经固定,那么就会按照这个顺序进行字节对齐 2. 在字节对齐的过程中,isa指针永远排在成员变量的最前面

1.4.1.2.1 普通对象属性字节对齐

将上面Person1、Person2对象的所有成员变量都换成属性

@interface Person1 : NSObject
@property (nonatomic, assign) double a;
@property (nonatomic, assign) char b;
@property (nonatomic, assign) int c;
@property (nonatomic, assign) short d;
@end
@interface Person2 : NSObject
@property (nonatomic, assign) double a;
@property (nonatomic, assign) int b;
@property (nonatomic, assign) char c;
@property (nonatomic, assign) short d;
@end

main函数调用

Person1 *person1 = [[Person1 alloc] init];
person1.a = 1.0;
person1.b = 'a';
person1.c = 2;
person1.d = 3;
Person2 *person2 = [[Person2 alloc] init];
person2.a = 10.0;
person2.b = 20;
person2.c = 'b';
person2.d = 30;
NSLog(@"%lu - %lu", class_getInstanceSize([person1 class]), malloc_size((__bridge const void *)(person1)));
NSLog(@"%lu - %lu", class_getInstanceSize([person2 class]), malloc_size((__bridge const void *)(person2)));

// 打印
2022-03-24 15:46:48.488430+0800 001-内存对齐原则-Demo[92197:7296205] 24 - 32
2022-03-24 15:46:49.103490+0800 001-内存对齐原则-Demo[92197:7296205] 24 - 32

我们分析一下: 第2课-OC对象原理上-1_第46张图片 我们尝试分析一下上面打印出来的内存

  • 0x0000000109788b58:这个应该是isa指针,8字节
  • 0x0000000200030061:注意这个肯定是字节对齐优化过后的数据,我们按照从后往前来分析
    • 0x0061:应该是字符'a',也就是变量b,2字节
    • 0x0003: 数字3,变量d,2字节
    • 0x00000002:数字2,变量c,4字节
  • 0x3ff0000000000000:这个应该是double类型的a,8字节

也就是成员变量的顺序为

{
    Class isa;// isa成员变量在第1位
    char _b;
    short _d;
    int _c;
    double _a;// 剩下的成员变量按照字节数从低到高排序
}

那么person1的内存分配应该是:

  • person1->isa: 8字节 [0x6000015280a0, 0x6000015280a7]
  • person1->_b: 2字节 [0x6000015280a8, 0x6000015280a9]
  • person1->_d: 2字节 [0x6000015280aa, 0x6000015280ab]
  • person1->_c: 4字节 [0x6000015280ac, 0x6000015280af]
  • person1->_a: 8字节 [0x6000015280B0, 0x6000015280B7]

一共24字节

接下来我们通过lldb验证一下 第2课-OC对象原理上-1_第47张图片 验证通过。

同理我们分析person2,也能得出一样的结果。

由此我们发现,成员变量内存的分布并非完全按照属性顺序排列,而是会做一定的优化,比如 b、d、c 就会被合并到一起,因为它们加起来也不足 8 字节。这是系统帮我们做的优化,其目的是为了方便提高访问效率的同时,能够优化存储,减少浪费。

结论: 1.在字节对齐的过程中,isa指针永远排在成员变量的最前面 2.对象里面的属性在生成成员变量时,成员变量的顺序会按照变量类型所占字节的大小从低向高排序。其目的是为了方便提高访问效率的同时,能够优化存储,减少浪费。

1.4.1.3 嵌套对象字节对齐
1.4.1.3.1 嵌套对象成员变量字节对齐

假设有如下对象

@interface Person1 : NSObject
{
    @public
    double _a;
    char _b;
    int _c;
    short _d;
}
@end

@interface Person2 : NSObject
{
@public
    Person1 *_p1;
    double _a;
    int _b;
    char _c;
    short _d;
}
@end

main函数调用

Person1 *person1 = [[Person1 alloc] init];
person1->_a = 1.f;
person1->_b = 'a';
person1->_c = 2;
person1->_d = 3;
Person2 *person2 = [[Person2 alloc] init];
person2->_p1 = person1;
person2->_a = 10.f;
person2->_b = 'b';
person2->_c = 20;
person2->_d = 30;
NSLog(@"%lu - %lu", class_getInstanceSize([person2 class]), malloc_size((__bridge const void *)(person2)));

// 打印
2022-03-24 17:32:15.482387+0800 001-内存对齐原则-Demo[94061:7372665] 32 - 32 

person2内存对齐过程分析:

  • isa指针: 8字节 内存分配8字节,补齐0字节 [0, 7]
  • _p1指针: 8字节 内存分配8字节,补齐0字节 [8, 15]
  • double _a;8字节 内存分配8字节,补齐0字节 [16, 23]
  • int _b; 4字节 内存分配4字节,补齐0字节 [24, 27]
  • char _c; 1字节 内存分配2字节,补齐1字节 [28, 29]
  • short _d; 2字节 内存分配2字节,补齐0字节 [30, 31] 所以内存对齐之后一共32字节, maclloc是16字节对齐,所以是32字节

验证一下 第2课-OC对象原理上-1_第48张图片 验证通过

结论: 与普通对象相同,对象指针按照8字节计算

1.4.1.3.1 嵌套对象属性字节对齐

有如下程序:

@interface Person1 : NSObject
@property (nonatomic, assign) double a;
@property (nonatomic, assign) char b;
@property (nonatomic, assign) int c;
@property (nonatomic, assign) short d;
@property (nonatomic, strong) Person2 *p2;
@end

@interface Person2 : NSObject
@property (nonatomic, assign) double a;
@property (nonatomic, assign) int b;
@property (nonatomic, assign) char c;
@property (nonatomic, assign) short d;
@end

main函数调用

Person2 *person2 = [[Person2 alloc] init];
person2.a = 10.0;
person2.b = 2;
person2.c = 'a';
person2.d = 3;
Person1 *person1 = [[Person1 alloc] init];
person1.a = 1.0;
person1.b = 'a';
person1.c = 2;
person1.d = 3;
person1.p2 = person2;
NSLog(@"%lu - %lu", class_getInstanceSize([person1 class]), malloc_size((__bridge const void *)(person1)));
2022-03-24 17:54:25.752309+0800 001-内存对齐原则-Demo[94576:7394261] 32 - 32

lldb打印如下: 第2课-OC对象原理上-1_第49张图片 所以说实际分配如下:

  • isa指针: 8字节 内存分配8字节,补齐0字节 [0, 7]
  • char _b; 1字节 内存分配2字节,补齐1字节 [8, 9]
  • short _d; 2字节 内存分配2字节,补齐0字节 [10, 11]
  • int _c; 4字节 内存分配4字节,补齐0字节 [12, 15]
  • double _a;8字节 内存分配8字节,补齐0字节 [16, 23]
  • _p1指针: 8字节 内存分配8字节,补齐0字节 [24, 31] 所以内存对齐之后一共32字节, maclloc是16字节对齐,所以是32字节

结论: 与普通对象相同,对象指针按照8字节计算

1.4.1.4 继承对象字节对齐
1.4.1.4.1 继承对象成员变量字节对齐

有如下程序:

@interface Person1 : Person2
{
    @public
    double _a;
    char _b;
    int _c;
    short _d;
}
@end

@interface Person2 : NSObject
{
@public
    double _e;
    int _f;
    char _g;
    short _h;
}
@end

main函数调用

Person1 *person1 = [[Person1 alloc] init];
person1->_a = 1.0;
person1->_b = 'a';
person1->_c = 2;
person1->_d = 3;
person1->_e = 10.0;
person1->_f = 20;
person1->_g = 'b';
person1->_h = 30;
NSLog(@"%lu - %lu", class_getInstanceSize([person1 class]), malloc_size((__bridge const void *)(person1)));

// 打印
2022-03-24 19:21:24.474334+0800 001-内存对齐原则-Demo[96313:7465705] 48 - 48

lldb分析如下: 第2课-OC对象原理上-1_第50张图片

也就是Person1对象内部的成员变量顺序为:

{
    Class isa; // isa指针永远在首位
    double _e;
    int _f;
    char _g;
    short _h;
    double _a;
    char _b;
    int _c;
    short _d;
}

所以说实际分配如下:

  • isa指针: 8字节 内存分配8字节,补齐0字节 [0, 7]
  • double _e; 8字节 内存分配8字节,补齐0字节 [8, 15]
  • int _f; 4字节 内存分配4字节,补齐0字节 [16, 19]
  • char _g; 1字节 内存分配2字节,补齐1字节 [20, 21]
  • short _h; 2字节 内存分配2字节,补齐0字节 [22, 23]
  • double _a; 8字节 内存分配8字节,补齐0字节 [24, 31]
  • char _b; 1字节 内存分配4字节,补齐3字节 [32, 35]
  • int _c; 4字节 内存分配4字节,补齐0字节 [36, 39]
  • short _d; 2字节 内存分配8字节,补齐6字节 [40, 47]

所以内存对齐之后一共48字节, maclloc是16字节对齐,所以是48字节

结论: 1. 在内存对齐过程中,isa指针永远排在成员变量的首位 2. 子类的成员变量会按照父类在前,子类在后的顺序进行字节对齐

1.4.1.4.2 继承对象属性字节对齐

有如下程序:

@interface Person1 : Person2
@property (nonatomic, assign) double a;
@property (nonatomic, assign) char b;
@property (nonatomic, assign) int c;
@property (nonatomic, assign) short d;
@end

@interface Person2 : NSObject
@property (nonatomic, assign) double e;
@property (nonatomic, assign) int f;
@property (nonatomic, assign) char g;
@property (nonatomic, assign) short h;
@end

main函数调用

Person1 *person1 = [[Person1 alloc] init];
person1.a = 1.0;
person1.b = 'a';
person1.c = 2;
person1.d = 3;
person1.e = 10.0;
person1.f = 20;
person1.g = 'b';
person1.h = 30;
NSLog(@"%lu - %lu", class_getInstanceSize([person1 class]), malloc_size((__bridge const void *)(person1)));

2022-03-24 18:35:55.218624+0800 001-内存对齐原则-Demo[95491:7431598] 40 - 48

lldb分析如下: 第2课-OC对象原理上-1_第51张图片

也就是Person1对象内部的成员变量顺序为:

{
    Class isa; // isa指针永远在首位
    char g;
    short h;
    int f;
    double e;// 注意上面的efgh是父类的成员变量
    char b;
    short d;
    int c;
    double a;
}

所以说实际分配如下:

  • isa指针: 8字节 内存分配8字节,补齐0字节 [0, 7]
  • char g; 1字节 内存分配2字节,补齐1字节 [8, 9]
  • short h; 2字节 内存分配2字节,补齐0字节 [10, 11]
  • int f; 4字节 内存分配4字节,补齐0字节 [12, 15]
  • double e; 8字节 内存分配8字节,补齐0字节 [16, 23]
  • char b; 1字节 内存分配2字节,补齐1字节 [24, 25]
  • short d; 2字节 内存分配2字节,补齐0字节 [26, 27]
  • int c; 4字节 内存分配4字节,补齐0字节 [28, 31]
  • double a; 8字节 内存分配8字节,补齐0字节 [32, 39]

所以内存对齐之后一共40字节, maclloc是16字节对齐,所以是48字节

结论: 1. 在内存对齐过程中,isa指针永远排在成员变量的首位 2. 继承对象的属性在生成成员变量时候,会按照变量类型所占字节的大小从低向高的顺序,先排列生成父类成员变量,再生成子类成员变量,然后按照排序好的成员变量来进行字节对齐

1.4.1.4.3 继承对象成员变量+属性字节对齐

有如下程序:

@interface Person1 : Person2
{
    @public
    double _a;
    char _b;
}
@property (nonatomic, assign) int c;
@property (nonatomic, assign) short d;
@end

@interface Person2 : NSObject
{
@public
    char _g;
    short _h;
}
@property (nonatomic, assign) double e;
@property (nonatomic, assign) int f;
@end

main函数调用:

Person1 *person1 = [[Person1 alloc] init];
person1->_a = 1.0;
person1->_b = 'a';
person1.c = 2;
person1.d = 3;
person1.e = 10.0;
person1.f = 20;
person1->_g = 'b';
person1->_h = 30;
NSLog(@"%lu - %lu", class_getInstanceSize([person1 class]), malloc_size((__bridge const void *)(person1)));
// 打印
2022-03-24 20:02:07.472950+0800 001-内存对齐原则-Demo[97118:7499711] 40 - 48

lldv分析 第2课-OC对象原理上-1_第52张图片

也就是Person1对象内部的成员变量顺序为:

{
    Class isa; // isa指针永远在首位
    char _g;
    short _h;
    int _f;
    double _e;// 注意上面的efgh是父类的成员变量
    double _a;
    char _b;
    short _d;
    int _c;

}

所以说实际分配如下:

  • isa指针: 8字节 内存分配8字节,补齐0字节 [0, 7]
  • char g; 1字节 内存分配2字节,补齐1字节 [8, 9]
  • short h; 2字节 内存分配2字节,补齐0字节 [10, 11]
  • int f; 4字节 内存分配4字节,补齐0字节 [12, 15]
  • double e; 8字节 内存分配8字节,补齐0字节 [16, 23]
  • double a; 8字节 内存分配8字节,补齐0字节 [24, 31]
  • char b; 1字节 内存分配2字节,补齐1字节 [32, 33]
  • short d; 2字节 内存分配2字节,补齐0字节 [34, 35]
  • int c; 4字节 内存分配4字节,补齐0字节 [36, 39]

所以内存对齐之后一共40字节, maclloc是16字节对齐,所以是48字节

结论: 1. 在内存对齐过程中,isa指针永远排在成员变量的首位 2. 一个对象如果既包含写死的成员变量,也包含属性,那么属性自动生成的成员变量会按照变量所占字节大小从低向高排序,排序之后会放在写死的成员变量后面;如果该对象是子类对象,那么会按照上述规则先对父类的成员变量进行排序,然后再对子类的成员变量进行排序

1.4.2 alloc执行的流程

废话不说,直接上图

1.4.2.1 alloc执行2次
  1. 当我们执行[Person alloc]时,代码执行顺序为alloc —> objc_alloc --> callocAlloc --> objc_msgSend --> alloc 第2课-OC对象原理上-1_第53张图片 第2课-OC对象原理上-1_第54张图片 通过objc_msgSend发送消息再次调用[NSObject alloc]。 注意这里有一个问题,alloc为什么执行2次呢? 第2课-OC对象原理上-1_第55张图片 这里的原因是:

    1. 苹果的llvm编译器对alloc函数进行了优化

    2. 当llvm编译到alloc方法时,会hook,然后去执行objc_alloc,并且标记receiver(首次会走进去,一旦执行objc_alloc之后就不会再二次走进去了)

    3. 调用完objc_alloc然后再去执行objc_msgSend发送消息,再次执行alloc,此时就直接执行[NSObject alloc]函数了

      如果想看具体的优化过程,可以查看苹果llvm官方源码 https://github.com/apple/llvm-project,下载下来可以使用vscode打开

  1. 接下来执行[NSObject alloc]函数,之后的调用顺序为alloc-->_objc_rootAlloc-->callAlloc-->_objc_rootAllocWithZone-->_class_createInstanceFromZone 第2课-OC对象原理上-1_第56张图片 第2课-OC对象原理上-1_第57张图片 第2课-OC对象原理上-1_第58张图片 第2课-OC对象原理上-1_第59张图片
1.4.2.2 _class_createInstanceFromZone函数

_class_createInstanceFromZone函数,该函数主要做了3件事情 第2课-OC对象原理上-1_第60张图片

  1. 通过字节对齐算法,计算要分配的内存大小
  2. 向系统开辟内存,返回地址指针
  3. 关联isa指针到对应的类
1.4.2.3 通过字节对齐算法,计算要分配的内存大小
1.4.2.3.1 知识点1:计算要分配的内存是通过对象的成员变量来计算的

结论:

  1. 在计算一个实例对象要分配的内存大小时,是通过该对象内部的成员变量(无论是在.h文件还是在.m文件)来计算的。实例变量的大小=类成员变量的大小
  2. 方法,无论是对象方法,还是类方法,不占用实例变量的大小;属性之所以会影响内存大小,是因为属性会默认生成成员变量

接下来我们具体分析一下: 假设我们有一个没有成员变量的person对象

@interface ZBPerson : NSObject
@end

通过检测[ZBPerson alloc]过程我们可以发现unalignedInstanceSize在计算未对齐的实例变量大小时,返回8。 第2课-OC对象原理上-1_第61张图片

通过unalignedInstanceSize函数我们知道实例变量的大小=类成员变量的大小(May be unaligned depending on class's ivars)。但是我们的ZBPerson对象明明没有成员变量啊。为什么还是返回8呢?原因就是ZBPerson对象继承自NSObject,NSObject里面有一个isa的成员变量,所以这里返回的8就是isa的大小,之所以是8字节,是因为isa是一个结构体指针,在64位系统中,一个指针占8字节。

@interface NSObject  {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

而Class的本质是objc_class结构体指针

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

而objc_class继承自objc_object结构体

struct objc_class : objc_object {
....
}
struct objc_object {
private:
    isa_t isa;

public:

    // ISA() assumes this is NOT a tagged pointer object
    Class ISA(bool authenticated = false);

    // rawISA() assumes this is NOT a tagged pointer object or a non pointer ISA
    Class rawISA();

    // getIsa() allows this to be a tagged pointer object
    Class getIsa();

    uintptr_t isaBits() const;

    // initIsa() should be used to init the isa of new objects only.
    // If this object already has an isa, use changeIsa() for correctness.
    // initInstanceIsa(): objects with no custom RR/AWZ
    // initClassIsa(): class objects
    // initProtocolIsa(): protocol objects
    // initIsa(): other objects
    void initIsa(Class cls /*nonpointer=false*/);
    void initClassIsa(Class cls /*nonpointer=maybe*/);
    void initProtocolIsa(Class cls /*nonpointer=maybe*/);
    void initInstanceIsa(Class cls, bool hasCxxDtor);

    // changeIsa() should be used to change the isa of existing objects.
    // If this is a new object, use initIsa() for performance.
    Class changeIsa(Class newCls);

    bool hasNonpointerIsa();
    bool isTaggedPointer();
    bool isBasicTaggedPointer();
    bool isExtTaggedPointer();
    bool isClass();

    // object may have associated objects?
    bool hasAssociatedObjects();
    void setHasAssociatedObjects();

    // object may be weakly referenced?
    bool isWeaklyReferenced();
    void setWeaklyReferenced_nolock();

    // object may have -.cxx_destruct implementation?
    bool hasCxxDtor();

    // Optimized calls to retain/release methods
    id retain();
    void release();
    id autorelease();

    // Implementations of retain/release methods
    id rootRetain();
    bool rootRelease();
    id rootAutorelease();
    bool rootTryRetain();
    bool rootReleaseShouldDealloc();
    uintptr_t rootRetainCount();

    // Implementation of dealloc methods
    bool rootIsDeallocating();
    void clearDeallocating();
    void rootDealloc();

private:
    void initIsa(Class newCls, bool nonpointer, bool hasCxxDtor);

    // Slow paths for inline control
    id rootAutorelease2();
    uintptr_t overrelease_error();

#if SUPPORT_NONPOINTER_ISA
    // Controls what parts of root{Retain,Release} to emit/inline
    // - Full means the full (slow) implementation
    // - Fast means the fastpaths only
    // - FastOrMsgSend means the fastpaths but checking whether we should call
    //   -retain/-release or Swift, for the usage of objc_{retain,release}
    enum class RRVariant {
        Full,
        Fast,
        FastOrMsgSend,
    };

    // Unified retain count manipulation for nonpointer isa
    inline id rootRetain(bool tryRetain, RRVariant variant);
    inline bool rootRelease(bool performDealloc, RRVariant variant);
    id rootRetain_overflow(bool tryRetain);
    uintptr_t rootRelease_underflow(bool performDealloc);

    void clearDeallocating_slow();

    // Side table retain count overflow for nonpointer isa
    struct SidetableBorrow { size_t borrowed, remaining; };

    void sidetable_lock();
    void sidetable_unlock();

    void sidetable_moveExtraRC_nolock(size_t extra_rc, bool isDeallocating, bool weaklyReferenced);
    bool sidetable_addExtraRC_nolock(size_t delta_rc);
    SidetableBorrow sidetable_subExtraRC_nolock(size_t delta_rc);
    size_t sidetable_getExtraRC_nolock();
    void sidetable_clearExtraRC_nolock();
#endif

    // Side-table-only retain count
    bool sidetable_isDeallocating();
    void sidetable_clearDeallocating();

    bool sidetable_isWeaklyReferenced();
    void sidetable_setWeaklyReferenced_nolock();

    id sidetable_retain(bool locked = false);
    id sidetable_retain_slow(SideTable& table);

    uintptr_t sidetable_release(bool locked = false, bool performDealloc = true);
    uintptr_t sidetable_release_slow(SideTable& table, bool performDealloc = true);

    bool sidetable_tryRetain();

    uintptr_t sidetable_retainCount();
#if DEBUG
    bool sidetable_present();
#endif
};

通过上面一个例子可能还无法证明我们的结论,接下来我们验证一下实例变量的大小=类成员变量的大小这个结论 假设我们有

@interface ZBPerson : NSObject
@property (nonatomic, copy) NSString *name; ///< 名字
@end

main函数调用:

ZBPerson *person = [ZBPerson alloc];
NSLog(@"personSize = %lu", class_getInstanceSize([ZBPerson class]));
2022-03-22 11:13:30.166681+0800 002-系统内存开辟分析-Demo[55867:5774093] personSize = 16

我们分析一下:

  • name属性,会生成一个_name成员变量,指针类型,占用8字节
  • isa指针,占用8字节

所以一共16字节,内存对齐之后16字节,所以返回16

接着我们在person对象中添加属性,成员变量和方法

@interface ZBPerson : NSObject
{
    int _age; // 年龄
}
@property (nonatomic, copy) NSString *name; ///< 名字
@property (nonatomic, copy) NSString *nickName; // 昵称
//@property (nonatomic, assign) int age; ///< 年龄
@property (nonatomic, assign) double height; ///< 身高

- (void)objectMethod;
+ (void)classMethod;
@end

@implementation ZBPerson
- (void)objectMethod
{

}
+ (void)classMethod
{

}

main函数

ZBPerson *person = [ZBPerson alloc];
NSLog(@"personSize = %lu", class_getInstanceSize([ZBPerson class]));
2022-03-22 11:16:57.680721+0800 002-系统内存开辟分析-Demo[55961:5777855] personSize = 40

我们分析一下:

  • name属性,会生成一个_name成员变量,字符串指针类型,占用8字节
  • nickName属性,会生成一个_nickName成员变量,字符串指针类型,占用8字节
  • _age成员变量,int类型,占用4字节
  • height属性,会生成一个_height成员变量,double类型,占用8字节
  • isa指针,8字节

一共36字节,字节对齐之后40字节,所以打印40,注意这里是先将所有成员变量的大小加起来然后再执行8字节对齐算法。这里一定要注意和结构体对齐算法的区分。

我们通过结果进一步验证了我们的结论

1.4.2.3.2 知识点2:对象实例大小的计算采用8字节对齐算法对未对齐的字节进行对齐

在通过对象的成员变量计算出对象所占用的字节大小之后,还需要对字节进行对齐,对齐算法是8字节对齐算法,具体代码如下。

uint32_t alignedInstanceSize() const {
    return word_align(unalignedInstanceSize());
}

static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}

#   define WORD_MASK 7UL

word_align算法就是核心的字节对齐算法,这里的x就是我们传进来的8,(x + WORD_MASK) & ~WORD_MASK就是算法的执行过程,其中WORD_MASK=7UL(无符号长整形7),该算法就是取8的整数倍,8字节对齐。接下来我们看一下运算过程

x = 1
1机器码: 00000001
7机器码: 00000111
~7机器码:11111000
1+7:00001000
(1+7)&~7: 00001000=8

x = 2
2机器码: 00000010
7机器码: 00000111
~7机器码:11111000
2+7:00001001
(2+7)&~7: 00001000=8

x = 8
8机器码: 00001000
7机器码: 00000111
~7机器码:11111000
8+7:00001111
(8+7)&~7: 00001000=8

x = 9
9机器码: 00001001
7机器码: 00000111
~7机器码:11111000
9+7:00010000
(9+7)&~7: 00010000=16

x = 11
11机器码: 00001011
7机器码: 00000111
~7机器码:11111000
11+7:00010010
(11+7)&~7: 00010000=16

x = 17
17机器码: 00010001
7机器码: 00000111
~7机器码:11111000
17+7:00011000
(17+7)&~7: 00011000=24

通过以上运算可以得出结论:

  • 无论x是多少,经过该算法之后,结果一定会是8的整数倍,例如8,16,24等
  • 如果这里的WORD_MASK=15UL,也就意味进行16字节对齐,该算法结果会是16的整数倍

另外上面的算法可以换成一种等价的算法 即

  • (x + 7) & ~7 === (x + 7) >> 3 << 3
  • (x + 15) & ~15 === (x + 15) >> 4 << 4
  • (x + WORD_MASK) & ~WORD_MASK === (x + WORD_MASK) >> A << A 其中 2^A - 1 = WORD_MASK

例如:

x = 8
8机器码: 00001000
7机器码: 00000111
~7机器码:11111000
8+7:00001111
(8+7)&~7: 00001000=8

8+7 >> 3:00000001
8+7 >> 3 << 3: 00001000=8


x = 8
8机器码: 00001000
15机器码: 00001111
~15机器码:11110000
8+15:00010111
(8+15)&~15: 00010000=16

8+15 >> 4:00000001
8+15 >> 4 << 4: 00010000=16
1.4.2.3.3 知识点3:instanceSize方法体内,最少开辟16字节内存

如果不走缓存,最少开辟16字节内存 第2课-OC对象原理上-1_第62张图片

如果走缓存逻辑,通过align16做了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;
}

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);
    }
}

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

注意这里采用的是16字节对齐。读到这里可能有些小伙伴就糊涂了,刚才不是讲的8字节对齐吗,怎么到这里变成16字节了。这里有一个知识点:

  • 我们讲的8字节对齐算法是针对对象内部的成员变量的
  • 这里的16字节对齐算法是对针对象而言的,也就是对象与对象之间的内存大小计算通过16字节对齐算法,具体为什么?后面我们会讲到。这里只需要留意下。

你可能感兴趣的:(iOS底层原理)