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

本文详细探讨了Objective-C中`alloc`函数的底层实现,包括通过符号断点、汇编代码以及源码调试等方法探索`alloc`的执行流程。文章详细讲解了内存分配、对象字节对齐的原理,包括普通对象、嵌套对象和继承对象的字节对齐规则,并通过实例分析了成员变量和属性在内存中的布局。同时,文章还介绍了源代码编译调试技巧,以及编译优化对字节对齐和内存分配的影响。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

第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] <ZBPerson: 0x6000027c0360>-0x6000027c0360-0x7ff7b43f2e58
2022-03-18 19:49:27.611430+0800 001-alloc&init探索-Demo[86438:3286512] <ZBPerson: 0x6000027c0360>-0x6000027c0360-0x7ff7b43f2e50
2022-03-18 19:49:27.611564+0800 001-alloc&init探索-Demo[86438:3286512] <ZBPerson: 0x6000027c0360>-0x6000027c0360-0x7ff7b43f2e48

通过以上代码可以得到:

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

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

1.2 底层探索的三种方法

1.2.1 符号断点

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

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

Xcode中添加符号断点如下

添加之后,会弹出如下设置页面

各字段含义如下: 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点击调试小箭头,即可进入到底层函数

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

接着再次执行断点我们就可以看到 objc_alloc实际会继续向下执行_objc_rootAllocWithZoneobjc_msgSend函数 其中上图中libobjc.a.dylib就是OC底层的动态库,所以我们得出如下结论:

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

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

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

首先我们需要在断点处,打开汇编设置

接下来我们就可以看到汇编源码了

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

接着ctrl一步步执行,最后还是会执行到OC的动态库

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

例如我们直接对alloc打上符号断点

执行之后可以直接得到libobjc.A.dylib+[NSObject alloc]:`

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

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

我们可以搜索objc

或者通过另一个网站下载

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

下载成功之后打开这个库文件

然后在源码中搜索 alloc { 可以得到

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

  • 执行alloc会执行_objc_rootAlloc

      _objc_rootAlloc(Class cls)
      {
          return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
      }
  • 接着执行callAlloc 这里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的符号断点,再次执行 其中我们使用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

开始配置

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

问题:编译objc源码的时候遇到Xcode error: unable to find sdk 'macosx.internal'

解决方案: Build Settings -> Base SDK -> macOS

问题:文件查漏补缺

解决方案:

  • 大家可以通过 Apple source 在 xnu-8019.61.5/bsd/sys/reason.h 路径自行下载
  • 还可以通过谷歌中输入reason.h site:opensource.apple.com 定向检索

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

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

目前还不行,一定给我们的工程设置文件检索路径

这里之所以这么配置是因为,源代码中通过# include <sys/reason.h>进行引用,所以我们需要在ZBC

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值