准备工作
汇编基础
三种寻找源码的方式
alloc 方法的底层调用流程
alloc -> objc_alloc -> callAlloc -> objc_msgSend ->
alloc -> objc_rootAlloc -> callAlloc -> _objc_rootAllocWithZone ->
_class_createInstanceFromZone
追踪 alloc
实例化一个对象往往是通过[[xxx alloc] init]
,那么 alloc 和 init 的区别在哪?,将两个方法分开调用,并用 2 个指针引用
User *user = [User alloc];
User *user1 = [user init];
User *user2 = [user init];
打断点控制输出 user1 和 user2 的内存地址,发现内存地址是一样的:
说明 init 方法不会去开辟内存空间。在 alloc 方法这行打上断点,开启汇编调试,运行代码(注意使用 control + step into,否则会跳过这行代码):
; symbol stub for: objc_alloc
: 意思是该地址保存的是 objc_alloc 方法的符号。
查看 objc 源码(地址在附录),搜索 objc_alloc
发现,fixupMessageRef
调用 alloc
时,使用的实现是objc_alloc
。(sel 代表方法名)
回到自己的项目添加一个 objc_alloc
符号断点,再次调试 alloc 方法,发现跳过了_objc_rootAllocWithZone
,直接到了objc_msgSend
,先不管,打印一下寄存器 x0 和 x1,确实是 alloc 方法。
- x0寄存器存放函数返回的值,这里返回 User 的一个实例对象
- x1寄存器 参数传递给函数,并将函数结果返回,传的参数是 “alloc”
接着增加一个[NSObject alloc]
的符号断点(这是所有 alloc 方法的最初实现):
并通过 step into 跳转到方法_objc_rootAlloc
内部,这一次 step over 能执行到_objc_rootAllocWithZone
了:
去源码看一下_objc_rootAllocWithZone
:
方法返回的是 id 类型,return 调用的是_class_createInstanceFromZone
,跳转到该方法:
可以看到返回的是 obj,也就是返回实例对象。下面去验证对象是通过该方法返回的,回到自己的项目汇编调试,通过 step into 进入:
ret、retab、retaa
都代表函数返回,并打印寄存器,确实是返回实例对象。
结论:
追踪 init
增加 init 方法的符号断点 [NSObject init]
,进入汇编调试:
源码查找 init
,_objc_rootInit
只是返回了 obj
结论:
init 只是返回了自身, init 作为工厂方法,目的是让子类继承并重写。比如 NSArray 继承 NSObject, 重写了 init 方法。
追踪 new
@implementation User
- (instancetype)init {
if ([super init]) {
self.name = @"Gin";
}
return self;
}
@end
通过调试发现通过 new 方法出来的对象也调用了 init:
找到源码 NSObject 类里的 new 方法,发现也是调用了 calloc,加上 init 方法
回到自己项目调试 new 方法,按住 control + Step into 发现实际调用的是 objc_opt_new
:
查找源码发现,如果不是__OBJC2__
,也会通过objc_msgSend
转发给源码中的 new 方法:
结论:new = alloc + init
优化等级
之前追踪 alloc 时发现的 _class_createInstanceFromZone
并没有出现在汇编调试里。这涉及到编译器优化,在 Xcode 找到以下设置:Optimization Level
debug 模式下优化等级默认是 None,先调试以下代码:
- (void)viewDidLoad {
[super viewDidLoad];
int a = 111;
int b = 222;
// 此处打断点
}
汇编调试可以看到数字:
接下来优化等级调整和 release 一样,汇编调试下:(对应的 Target 一定要选对再改设置)
发现少了那两个 w8 寄存器存储代码的变量值,这个就是编译优化的效果,代码中声明了变量却没有使用,编译时就被干掉了。没使用的函数也是同理。
- (void)viewDidLoad {
[super viewDidLoad];
int result = add(1111, 222);
// 断点
}
int add(int a, int b) {
return a + b;
}
没开优化:
优化后:虽然代码里调用了 add
方法,但是返回值没有被使用,就会被优化
接下来使用以下方法的返回值:
- (void)viewDidLoad {
[super viewDidLoad];
int result = add(1111, 222);
NSLog(@"result = %d", result);
// 断点
}
int add(int a, int b) {
return a + b;
}
调试发现,编译器直接在编译时把结果计算好了,相当于把函数里的实现直接替换到代码中:
源码调试 alloc
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>
#import "User.h"
int test (User *user) {
NSLog(@"%zu",class_getInstanceSize(user.class));
NSLog(@"%zu",malloc_size((__bridge const void *)(user)));
return 0;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
User *user = [User alloc]; // 断点
test(user);
}
return 0;
}
objc_alloc
callAlloc
,此时走的是底下的分支,说明前面的分支条件不满足,验证了前面汇编调试跳过了_objc_rootAllocWithZone
alloc
_objc_rootAlloc
,参数 cls 不为 nil
callAlloc
,这次走的另外一个分支
_objc_rootAllocWithZone
_class_createInstanceFromZone
,返回 obj,运行结束
但是,汇编调试时没有发现
callAlloc
和_class_createInstanceFromZone
的符号调用,这就是编译器优化的效果,哪怕优化水平是 None
通过源码了解了 alloc 的调用顺序:
对象的创建
_class_createInstanceFromZone
返回对象,查看该方法源码:
_class_createInstancesFromZone
中使用cls->instanceSize()
计算所需内存的大小
- 通过计算出 size,并且最小字节为 16字节;
alignedInstanceSize
是字节对齐(64位系统下是8字节)
模拟 8 字节对齐:
int align_8 (int byte) {
// byte + 7 : 为了得到超过8字节的部分,
// 例如 (9 + 7) / 8 = 2, 返回 2 * 8 = 16字节
return (byte + 7) / 8 * 8;
}
内存对齐
苹果官方的实现是:根据 64 或 32 位系统,进行8字节或4字节对齐。
这里 & ~WORD_MASK
怎么理解?在 64 位系统下,先进行非运算:
~WORD_MASK = ~7UL = ~0111 = 1000;
再进行与运算:0 与上任何数都是0,只有 1 & 1 = 1。例如 5 对齐:
(x + WORD_MASK) & ~WORD_MASK = (0101 + 0111) & 1000 = 1100 & 1000 = 1000 // 8
通过一个函数兼容 64 位和 32 位系统下的字节对齐,这就是精妙的地方。回到之前的 instanceSize
方法,在字节对齐钱,如果有缓存,会进入另外一个方法cache.fastInstanceSize()
中,实现如下:
可以看到 align16
,也就是 16 字节对齐,那么究竟是多少字节对齐的?这里只是计算出需要的大小,最终的创建从_class_createInstanceFromZone
中找到 obj = (id)calloc(1, size);
这里传入 size 去计算。
calloc
objc 源码中没有 calloc,需要到 libmalloc
源码中查找实现:
关键方法在于 segregated_size_to_fit
(直接是这个结论,具体怎么跳转的逻辑很深)
这也是个字节对齐的算法:
#define NANO_MAX_SIZE 256 /* Buckets sized {16, 32, 48, ..., 256} */
#define SHIFT_NANO_QUANTUM 4
#define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM) // 16
#define NANO_QUANTA_MASK (NANO_REGIME_QUANTA_SIZE - 1)
#define NANO_SIZE_CLASSES (NANO_MAX_SIZE/NANO_REGIME_QUANTA_SIZE)
static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
size_t k, slot_bytes;
if (0 == size) {
size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
}
// size + 15,然后右移 4 位
k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
// 再左移 4 位
slot_bytes = k << SHIFT_NANO_QUANTUM; // multiply by power of two quanta size
*pKey = k - 1; // Zero-based!
return slot_bytes;
}
size + 15,然后右移 4 位,再左移 4 位,也就是进行 16 字节对齐。
对象的本质
- 使用
clang
命令编译文件
clang -rewrite-objc main.m
就可得到 main.cpp 文件 - 搜索对象名
- 再搜索
NSObject_IMPL
所以对象的本质是 objc_object 结构体,⾥⾯存储 isa 指针和成员变量的值。
结构体的内存对齐方式
- 数据成员对⻬规则:结构(struct)的第⼀个数据成员放在 offset 为 0 的地⽅,以后每个数据成员存储的起始位置要从该成员⼤⼩或者成员的⼦成员⼤⼩的整数倍开始(⽐如 int 为 4 字节,则要从4的整数倍地址开始存储)。
- 结构体作为成员:如果⼀个结构⾥有某些结构体成员,则结构体成员要从其内部最⼤元素⼤⼩的整数倍地址开始存储.(struct a ⾥存有struct b,b ⾥有 char, int , double等元素,那 b应该从 8 的整数倍开始存储)。
- 收尾⼯作: 结构体的总⼤⼩,也就是sizeof的结果必须是其内部最⼤成员的整数倍,不⾜的要补⻬。
类型 | 32位 | 64位 |
---|---|---|
BOOL | 1 | 1 |
char | 1 | 1 |
unsigned char | 1 | 1 |
short | 2 | 2 |
unsigned short | 2 | 2 |
int | 4 | 4 |
unsigned int | 4 | 4 |
long | 4 | 8 |
unsigned long | 4 | 8 |
long long | 8 | 8 |
NSInteger | 4 | 8 |
float | 4 | 4 |
double | 8 | 8 |
CGFloat | 4 | 8 |
指针 | 4 | 8 |
struct StructOne {
// 8 字节,0~7
double a;
// 1 字节,9
char b;
// 4 字节,从 4 的整数倍开始,12~15
int c;
// 2 字节,16~17
short d;
// 总字节按最大成员字节的整数倍,所以占 24 字节
}structOne;
struct StructTwo {
// 8 字节,0~7
double a;
// 4 字节,8~11
int b;
// 1 字节,12
char c;
// 2 字节,14~15
short d;
// 总共占 16 字节
}structTwo;
struct StructThree {
// 8 字节,0~7
double a;
// 4 字节,8~11
int b;
// 1 字节,12
char c;
// 2 字节,14~15
short d;
// 4 字节,16~19
int e;
// 24 字节,且最大成员占 8 字节,24~47
struct StructOne myStruct;
// 总的大小按最大成员所占的 8 字节对齐(结构体不算基本类型),所以占 48 字节
}structThree;
NSLog(@"n structOne'size = %lu;n structOne'size = %lu;n structOne'size = %lu;n",
sizeof(structOne),sizeof(structTwo),sizeof(structThree));
附录
objc 源码下载
原文地址:https://blog.csdn.net/Jian_Ze/article/details/124369110
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.7code.cn/show_9991.html
如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱:suwngjj01@126.com进行投诉反馈,一经查实,立即删除!