准备工作

汇编基础

  1. b bl: 跳转指令方法调用
  2. ret: 函数返回
  3. ; : 注释

三种寻找源码方式

1.断点
2.符号断点
在这里插入图片描述在这里插入图片描述

  1. 通过汇编
    在Xcode菜单栏选择Debug->Debug WorkFlow->Always Show Disassembly,即可显示汇编代码
    在这里插入图片描述

alloc 方法底层调用流程

alloc -> objc_alloc -> callAlloc -> objc_msgSend -> 
alloc -> objc_rootAlloc -> callAlloc -> _objc_rootAllocWithZone ->
_class_createInstanceFromZone

追踪 alloc

实例一个对象往往是通过[[xxx alloc] init],那么 allocinit区别在哪?,将两个方法分开调用,并用 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 方法。

结论:

alloc 方法返回的就是实例对象

追踪 init

增加 init 方法的符号断点 [NSObject init]进入汇编调试
在这里插入图片描述
源码查找 init_objc_rootInit 只是返回了 obj
在这里插入图片描述
在这里插入图片描述

结论:

init 只是返回了自身, init 作为工厂方法,目的是让子类继承并重写比如 NSArray 继承 NSObject, 重写了 init 方法。

追踪 new

重写 init 方法,并给默认值

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

但是,汇编调试时没有发现 callAlloc_class_createInstanceFromZone符号调用,这就是编译器优化效果,哪怕优化水平是 None

通过源码了解了 alloc 的调用顺序
在这里插入图片描述

对象的创建

_class_createInstanceFromZone 返回对象,查看该方法源码:

int align_8 (int byte) {
    // byte + 7 : 为了得到超过8字节的部分,
    // 例如 (9 + 7) / 8 = 2, 返回 2 * 8 = 16字节
    return (byte + 7) / 8 * 8;
}

内存对齐

苹果官方的实现是:根据 64 或 32 位系统,进行8字节或4字节对齐
在这里插入图片描述
这里 &amp; ~WORD_MASK 怎么理解?在 64 位系统下,先进行非运算

~WORD_MASK = ~7UL = ~0111 = 1000;

再进行与运算:0 与上任何数都是0,只有 1 &amp; 1 = 1。例如 5 对齐

(x + WORD_MASK) &amp; ~WORD_MASK = (0101 + 0111) &amp; 1000 = 1100 &amp; 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 字节对齐。

  • 为什么要字节对齐?
    • 字节是内存的容量单位。但是,CPU 在读取内存的时候,却不是以字节为单位来读取的,⽽是以“块”为单位读取的,所以⼤家也经常听到⼀块内存,“块”的⼤⼩也就是内存存取的⼒度。如果不对⻬的话,在我们频繁的存取内存的时候,CPU 就需要花费⼤量的精⼒去分辨你要读取多少字节,这就会造成 CPU 的效率低下,如果想要 CPU 能够⾼效读取数据,那就需要找⼀个规范,这个规范就是字节对⻬。
  • 为什么对象内部成员变量是以 8 字节对⻬,系统实际分配的内存以 16 字节对⻬?
    • 空间时间苹果采取16字节对⻬,是因为 OC 的对象中,第⼀位叫 isa 指针,它是必然存在的,⽽且它就占了8位字节,就算对象中没有其他的属性了,也⼀定有⼀ isa,那对象就⾄少要占⽤ 8 位字节。如果以 8 位字节对⻬的话,如果连续的两块内存都是没有属性的对象,那么它们的内存空间就会完全的挨在⼀起,是容易混乱的。以 16 字节为⼀块,这就保证了 CPU 在读取的时候,按照块读取就可以,效率更⾼,同时还不容易混乱。

对象的本质

  1. 使用 clang 命令编译文件
    在这里插入图片描述 clang -rewrite-objc main.m
    就可得到 main.cpp 文件
  2. 搜索对象名
    在这里插入图片描述
  3. 搜索 NSObject_IMPL
    在这里插入图片描述
    所以对象的本质是 objc_object 结构体,⾥⾯存储 isa 指针和成员变量的值。

结构体的内存对齐方式

  1. 数据成员对⻬规则:结构(struct)的第⼀个数成员放在 offset 为 0 的地⽅,以后每个数据成员存储的起始位置要从该成员⼤⼩或者成员的⼦成员⼤⼩的整数倍开始(⽐如 int 为 4 字节,则要从4的整数倍地址开始存储)。
  2. 结构体作为成员:如果⼀个结构⾥有某些结构体成员,则结构体成员要从其内部最⼤元素⼤⼩的整数倍地址开始存储.(struct a ⾥存有struct b,b ⾥有 char, int , double元素,那 b应该从 8 的整数倍开始存储)。
  3. 收尾⼯作: 结构体的总⼤⼩,也就是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 源码下载

objc源码地址

原文地址:https://blog.csdn.net/Jian_Ze/article/details/124369110

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。

如若转载,请注明出处:http://www.7code.cn/show_9991.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱:suwngjj01@126.com进行投诉反馈,一经查实,立即删除

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注