OC对象原理探究(上)
APP启动流程探索
创建空工程代码如下,并且添加符号断点命名如下图
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"alloc 探索");
Person *p1 = [Person alloc];
Person *p2 = [p1 init];
Person *p3 = [p1 init];
}

运行工程查看堆栈信息

红色为app启动过程
: _dyld_start(dyld开始加载) -> dyld::main -> dyld_initialzeMainExecutable、ImageLoader...等等,表示主程序由_dyld_start开始到main等为启动做准备,包括加载动态库,共享内存,全局C++函数的析构,还有一系列的初始化,注册回调函数都在此步骤内完成。
蓝色为对象加载过程
: App启动一系列函数 -> libSystem_initializer -> libdispatch_init -> GCD环境的准备 -> _objc_init
OC对象初始化
分析alloc源码之前,先来查看三个变量内容
、内存地址
的区别:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"alloc 探索");
Person *p1 = [Person alloc];
Person *p2 = [p1 init];
Person *p3 = [p1 init];
NSLog(@"%@-%p",p1,p1);
NSLog(@"%@-%p",p2,p2);
NSLog(@"%@-%p",p3,p3);
}
<!-- 打印信息 -->
alloc 探索
2021-07-07 23:16:11.578015+0800 001-alloc&init探索[5266:540842] <Person: 0x600001994710>-0x600001994710
2021-07-07 23:16:11.578220+0800 001-alloc&init探索[5266:540842] <Person: 0x600001994710>-0x600001994710
2021-07-07 23:16:11.578378+0800 001-alloc&init探索[5266:540842] <Person: 0x600001994710>-0x600001994710
得出结论:
- 三个对象指向的是同一个内存空间,所以其
内容
和内存地址
是相同的 - p1 alloc之后拥有了
内存
,并且拥有了指针的指向
- init未对指针进行任何操作
<!-- 查看指针地址 -->
Person *p1 = [Person alloc];
Person *p2 = [p1 init];
Person *p3 = [p1 init];
NSLog(@"%@-%p-%p",p1,p1,&p1);
NSLog(@"%@-%p-%p",p2,p2,&p2);
NSLog(@"%@-%p-%p",p3,p3,&p3);
<!-- 打印信息 -->
alloc 探索
2021-07-08 20:57:54.778216+0800 001-alloc&init探索[7619:869162] <Person: 0x600001720490>-0x600001720490-0x7ffeee444028
2021-07-08 20:57:54.778464+0800 001-alloc&init探索[7619:869162] <Person: 0x600001720490>-0x600001720490-0x7ffeee444020
2021-07-08 20:57:54.778699+0800 001-alloc&init探索[7619:869162] <Person: 0x600001720490>-0x600001720490-0x7ffeee444018
由上图指针地址0x7ffeee444028 0x7ffeee444020 0x7ffeee444018得出结论:
-
*p1、*p2、*p3
属于栈上内存地址 -
*p1、*p2、*p3
是连续的地址空间,每个相隔8字节(解释:0x18+0x8=0x20、0x20+0x8=0x28)
图形详解:内存、指针的关系

下面探索alloc做了什么?init做了什么?
探索源码的三种方法
这里使用的模拟器
,也可以使用真机
探索
方法一
- 代码中打上断点

- 将工程运行,停在断点处之后,按住
control + Step into
进入到汇编代码


- 对看到的
objc_alloc
添加符号断点

- 按住
control + Step into
向下走

这里看到了libobjc.A.dylib objc_alloc
,也看到了接下来会调用的方法_objc_rootAllocWithZone,objc_msgSend
,我们就找到了objc_alloc底层源码来自于哪个动态库,为向下探索提供了线索!
方法二:通过汇编流程查看
- 选择菜单栏
Debug->Debug wrokflow->Always Show Disassembly
打开汇编模式,打上断点同上,运行工程

- 按住
control + Step into
找到objc_alloc

- 对看到的
objc_alloc
添加符号断点(同方式一) - 按住
control + Step into
向下走(同方式一)
方法三:直接通过已知符号断点设定,直接进入
- 打断点同上,运行工程至断点处
- 现在我们只知道
alloc
符号,直接添加alloc符号断点

- 点击跳过上面断点


直接锁定libobjc.A.dylib +[NSObject alloc]
,为找到objc_alloc底层源码来自于哪个动态库提供了线索!
汇编结合底层源码调试分析
苹果开源源码汇总: https://opensource.apple.com
Source Browser -> 找到objc4
这里查看的源码是objc4-818.2.tar.gz

- 打开源码项目
objc4-818.2
,搜索alloc查看alloc源码执行的详细流程:

- 进入
_objc_rootAlloc
方法

- 进入
callAlloc
方法

- 这里有
#if __OBJC2__
判断,如何验证执行_objc_rootAllocWithZone
还是执行objc_msgSend
? 对上面的每一个方法添加符号断点进行验证,最终发现先执行_objc_rootAllocWithZone

- 进入
_class_createInstanceFromZone
方法

alloc + init 整体源码的探索流程

编译器优化
编译器优化设置 BuildSetting -> Optimization level(GCC_OPTIMIZATION_LEVEL)
指定生成的代码针对速度和二进制大小进行优化的程度
设置 | 参数 |
---|---|
None[-O0] | 编译器不会优化代码。编译器的目标是蒋迪编译成本并使调试产生预期的结果,通常在Debug模式下使用。 |
Fast[-O,O1] | 快速,优化编译器需要编译的时间更久,对大型函数需要更多的内存。编译器会尝试减少代码大小和执行时间,而不执行任何需要大量编译时间的优化。 |
Faster[-O2] | 更快速,编译器执行几乎所有不涉及空间速度权衡的受支持优化。使用此设置,编译器不会执行循环展开或函数内联或寄存器命名,次设置会增加编译时间和生成代码的性能。 |
Fastest[-O3] | 设置指定的所有优化,并打开函数内联和寄存器重命名选项,此设置可能会产更大的二进制文件 |
Fastest,Smallest[-Os] | 最快、最小,此设置启用所有通常不会增加代码大小的更快的优化,它还会做减少代码大小的进一步优化 |
创建新工程编写代码如下,并打开汇编模式Debug->Debug wrokflow->Always Show Disassembly
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
//MARK: - 测试函数
int lgSum(int a, int b){
return a+b;
}
int main(int argc, char * argv[]) {
int a = 10;
int b = 20;
int c = lgSum(a, b);
NSLog(@"查看编译器优化情况:%d",c);
return 0;
}
优化模式
-
None[-O0]
不优化的情况下所有信息在寄存器中显示完整,分别打印a、b、计算前后x0寄存器的值结果如下:

Fastest,Smallest[-Os]

执行结果:优化掉了a、b两个变量,甚至连lgSum函数都被优化掉了,只剩下了一个结果0x1e存在w8寄存器中。
得出结论:
- 由于选择了Fastest,Smallest[-Os]优化方案,导致lgSum函数没有了,同理callAlloc函数也是一样的。
alloc的主线流程
alloc源码的核心操作主要分为三部分
-
cls->instanceSize
:计算需要开辟的内存空间大小 -
calloc
:申请内存,返回地址指针 -
obj->initInstanceIsa
:将 类 与 isa 关联
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对象进行新地址的赋值
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对象与cls对象进行绑定关联
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);
}
cls->instanceSize:计算所需内存大小
-
instanceSize
这个函数决定开辟内存,进入到这个函数,首先判断是否有缓存 - 如果有执行
cache.fastInstanceSize
函数,打断点执行到align16
,这个方法是16字节对齐
算法。执行完成之后内存开辟结束,获得该对象内存大小。 - 如果没有缓存,会执行
alignedInstanceSize
函数,执行word_align
函数,此函数的参数是函数unalignedInstanceSize
,而这个函数通过data()->ro()->instanceSize
获取到对象的实例大小,也就是说,最终开辟内存空间的大小是根据对象的成员变量大小
决定的。
默认情况下,不创建任何成员变量,类开辟的内存空间是8字节,因为继承NSObject
造成的,NSObject内有成员变量isa,由于isa的类型是结构体指针,所以isa是8字节,所以创建一个新的对象,没有任何成员变量,默认内存大小是8字节
字节对齐及其原理
字节对齐优势以空间换取时间
- 8字节来自于NSObject对象的isa结构体指针
- 不满16等于16
- 如果大于16会根据对象在内存分布中的特性来决定(根据传入的x,取x的整数倍),如果传入8,最后得到的是8的倍数
#ifdef __LP64__
# define WORD_SHIFT 3UL
# define WORD_MASK 7UL
# define WORD_BITS 64
#else
# define WORD_SHIFT 2UL
# define WORD_MASK 3UL
# define WORD_BITS 32
#endif
//字节对齐算法
//define WORD_MASK = 7
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
(8 + 7) & ~7 -> 15 & ~7 -> 8字节对齐,取8的整数
,这里为什么是8的倍数?而不是16 32的倍数?因为只有8字节的指针,double类型,没有16字节或者32字节的数据类型
对象的内存对齐
<!-- Person.h -- >
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic) int age;
@property (nonatomic) long height;
@property (nonatomic, copy) NSString *nickName;
- (void)saySomething;
@end
NS_ASSUME_NONNULL_END
<!-- Person.m -- >
#import "Person.h"
@implementation Person
- (void)saySomething{
NSLog(@"%s",__func__);
}
@end
<!-- main.m类中使用Person类 -- >
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [Person alloc];
NSLog(@"%@",p);
}
return 0;
}
- 添加断点

- lldb调试打印
<!-- 源码中查看ISA_MASK -->
# if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
# define ISA_MASK 0x007ffffffffffff8ULL
# define ISA_MAGIC_MASK 0x0000000000000001ULL
# define ISA_MAGIC_VALUE 0x0000000000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 0
# define ISA_BITFIELD

<!-- main.m类中使用Person类 -- >
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [Person alloc];
p.name = @"hello";
p.nickName = @"h";
p.age = 18;
p.height = 180;
NSLog(@"%@",p);
}
return 0;
}
<!-- NSLog打印这一行添加断点,进行lldb调试 -->
// x/4gx 表示 以16进制的格式打印4段
(lldb) x/4gx p
0x1006781a0: 0x011d800100008489 0x0000000000000012
0x1006781b0: 0x0000000100004010 0x00000000000000b4
(lldb) po 0x0000000000000012
18
(lldb) po 0x0000000100004010
hello
(lldb) po 0x00000000000000b4
180
<!-- 修改Person类height属性为Bool -- >
@property (nonatomic) long height; -> @property (nonatomic) BOOL height;
<!-- 修改p对象height为1,打断点运行 -- >
p.height = 1;
(lldb) x/4gx p
0x101b477a0: 0x011d800100008489 0x0000001200000001
0x101b477b0: 0x0000000100004010 0x0000000100004030
(lldb) po 0x0000001200000001
77309411329
(lldb) po 0x00000012
18
(lldb) po 0x00000001
1
通过上面打印我们发现int age
与 BOOL height
放在同一处开辟的8字节内存空间中,这就是内存对齐
,编译器进行了优化。