阅读 170

OC底层原理初探之对象的本质(二)alloc探索中

一:LLVM拦截优化

话接上文,分析过alloc方法底层调用逻辑之后,本以为已经搞明白了alloc的底层调用流程,不成想看到debug里的函数调用栈,如下图 image.png 分析发现:

  1. alloc方法调用之前还调用了objc_alloccallAlloc方法,且整个调用顺序为objc_alloc->callAlloc->alloc->_objc_rootAlloc->callAlloc->_objc_rootAllocWithZone->_class_createInstanceFromZoneNSObject比较特殊,它的alloc方法调用顺序为objc_alloc->callAlloc->_objc_rootAllocWithZone->_class_createInstanceFromZone),如下图:

image.png image.png

  1. callAlloc方法调用了两次,且每次方法内部走的流程还不一样,而且我们通过control + command + step into这样跟流程的方式是没法进入objc_alloc和第一次callAlloc方法里面的。

那么objc_alloc方法和第一次callAlloc方法为什么会出现在调用流程里面呢?苹果到底为什么要这样做呢?下面就带着这些问题一起来探索。

首先需要找出objc_alloc方法是在什么时候、什么位置调用的,但是函数调用栈和汇编代码等方式都没有线索。只有使用最原始的办法:在源码工程里全局搜索objc_alloc方法,一个一个去找了。

皇天不负有心人,终于被我找到了线索,发现了一个修复函数fixupMessageRef,根据代码可知:if (msg->sel == @selector(alloc))的情况下,就讲IMP替换成objc_alloc

image.png

接下来继续顺藤摸瓜(逆向查找)找出调用顺序,看看程序在什么情况下进入这里替换IMP

  • 全局搜索fixupMessageRef,找到调用者_read_images

image.png

  • _read_images的注释可知调用者为map_images_nolock;

image.png

  • 根据注释可知map_images_nolock调用者可能为map_images,全局搜索map_images_nolock,确定调用者确实是map_images

image.png

  • 全局搜索map_images,找到了_objc_init里面的_dyld_objc_notify_register函数,继续搜索_objc_init_dyld_objc_notify_register函数的话不会有什么收获,所以据此判断,至此跟fixupMessageRef函数相关的调用流程逆向查找也就结束了。

image.png

  • 正向验证:根据上面探索的结果可以确定正向调用顺序为_objc_init->_dyld_objc_notify_register->map_images->map_images_nolock->_read_image->fixupMessageRef,在这些函数里设置断点,然后运行源码调试验证。

image.png

结论:通过逆向查找源码调用流程,正向运行程序验证,得出的结论是alloc方法一定会被替换成objc_alloc,但是却并不是在上述fixupMessageRef函数内修改的。那么苹果为什么会提供一个可能不会执行的修复函数呢?这里发散思维,难道在程序编译阶段就会有相应的类似操作,这里只是容错处理!

带着这样的猜想,接下来下载LLVM的源码,然后拖到Visual Studio Code里探索验证一下:

image.png

MachOVie验证.app的可执行文件,也可以发现在汇编阶段就已经存在objc_alloc符号:

image.png

最终结论:程序在LLVM编译阶段就已经完成了alloc->objc_alloc的替换,而且还替换了retainreleaseautorelease等等。至于为什么要hook这些函数,推测可能是对象的创建、释放等跟内存相关,所以系统做了相应的监控。

探索到这里还有一个问题困扰着我们,就是系统为什么要在objc源码内添加修复函数fixupMessageRef这个容错处理?什么情况下LLVM编译器会出错,从而触发fixupMessageRef函数?这个问题留待后续探索。

流程总结:

alloc等一系列特殊方法在编译阶段LLVM会对其进行hook,其中alloc被替换成了objc_alloc函数,这样在运行时声明一个XJPerson类的对象并且为其开辟内存时调用alloc方法,首先响应的就是objc_alloc,接着进入callAlloc,第一次永远不满足判断条件if (fastpath(!cls->ISA()->hasCustomAWZ()))从而触发((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc)),给XJPerson发送了alloc消息,这个时候alloc方法才真正被调用,然后进入_objc_rootAlloc->callAlloc->_objc_rootAllocWithZone->_class_createInstanceFromZone此方法里面做三件事:字节对齐开辟内存空间与对象绑定

流程图: image.png

二:对象内存的影响因素

探索方向:

  1. 空对象,不声明任何成员变量、属性和方法。

image.png

  1. 只声明属性(成员变量)。

image.png

  1. 只声明方法。

image.png

结论:

  • 不声明成员变量、属性和方法的情况下,类的实例对象的内存大小为NSObjectisa指针的8字节。
  • 方法不会对类的实例对象的内存大小产生任何影响,方法不存在对象内(实例方法在类的method_list,类方法在元类的method_list)。
  • 添加属性(成员变量)的情况下,类的实例对象的内存大小为各成员变量所占内存大小加上isa指针的8字节,然后再以8字节对齐。

三:字节对齐

8字节对齐算法:

image.png

(x + WORD_MASK) & ~WORD_MASKx是已知参数,类型是size_t,代表当前对象声明成员变量的字节数instanceSizeWORD_MASK是宏定义,64位系统下值是7,假设x = 8,那么表达式就是:

  (8 + 7) & ~7
= 15 & ~7       (7 = 0000 0111, ~7 = 1111 1000)
= 0000 1111 & 1111 1000
= 0000 1000
= 8
复制代码

16字节对齐算法:

image.png

(x + size_t(15)) & ~size_t(15)x是已知参数,类型是size_t,代表当前对象声明成员变量的字节数instanceSize,假设x = 21那么表达式就是:

  (21 + 15) & ~15
= 36 & ~15       (15 = 0000 1111, ~15 = 1111 0000)
= 0010 0100 & 1111 0000
= 0010 0000
= 32
复制代码

为什么需要内存对齐?

原理图解:

字节对齐.gif

四:结构体内存对齐

内存对齐原则:

  • 数据成员对齐规则:结构体(struct)或联合体(union)的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的存储位置要从该成员大小或成员的子成员大小(只要该成员有子成员,比如说是数组结构体等)的整数倍开始(比如int是4字节,则要从4的整数倍地址开始存储)。
  • 结构体作为成员,如果一个结构体里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储(struct a里面有struct b,b里面有char,int,double等元素,那b应该从8的整数倍开始存储)。
  • 收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员大小的整数倍,不足的要补齐。

image.png

补充资料:基础类型占用字节数 image.png

五:malloc探索

为什么要探索malloc

探索打印实例对象占用内存的时候,出现了意料之外的结果:

image.png

遂找到libmalloc-317.40.8源码,并在其中找到了核心代码:


#define SHIFT_NANO_QUANTUM		4
#define NANO_REGIME_QUANTA_SIZE	(1 << SHIFT_NANO_QUANTUM)	// 16

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
	}
	k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
	slot_bytes = k << SHIFT_NANO_QUANTUM;							// multiply by power of two quanta size
	*pKey = k - 1;													// Zero-based!

	return slot_bytes;
}
复制代码

malloc源码流程图:

image.png

结论:

  • 对象的内存,16字节对齐。
  • 成员变量,8字节对齐,相加不满8字节的优化放在一起,不足的补0。
  • 对象与对象,16字节对齐。
文章分类
iOS
文章标签