上篇主要是大致分析了一下关于alloc的底层源码的执行流程,但是相信大家在测试的过程中肯定会发现一些奇怪的地方,那么我们还是接下来继续探索。
一.LLVM优化
1.alloc之前做了什么?
老规矩还是先定义对象,alloc走起,断点搞起来
CTRL+step into单步往下走或者Debug - debug workflow - Always Show Disasssmbly
what?啥情况?为啥是alloc 变成了objc_alloc,既然如此继续给objc_alloc加断点
咦,不打不知道,原来是先走的objc_alloc,然后再在callAlloc里面并没有执行先执行_objc_rootAllocWithZone,而是先走的下面的objc_msgSend,重新执行allocWithZone:,然后再执行alloc,如图
那么问题就来了,为什么要这样做呢?
2.LLVM拦截优化
为了验证这个问题,只能全局搜索objc_alloc,去寻找objc_alloc与alloc之间是否存在某种关系,终于让我找到了
继续顺藤摸瓜,走起,原来是在_read_images中,执行的fixupMessageRef,继续摸😄
这里发现一段注释,大致意思如图所示,也就是说fixupMessageRef,是用于修复,或者说防止alloc有问题而存在的一种监测手段,当然似乎不止alloc,还有其他的几个方法,例如release,retain等等,接着摸啊摸😄
发现map_images_nolock,继续追本溯源
发现map_images,继续继续,gogogo
最终就探索到这里了,发现在_objc_init,中执行_dyld_objc_notify_register,取map_images函数的地址,大家可以自己断点查看验证一下,做一下了解,当然重心还是在为什么要这样做呢,接下我们一起看一下LLVM的源码
只是目前找到的一部分关于alloc的方法的底层拦截,alloc-objce_alloc-标记receiver(判断进不去)- objc_msgSend-alloc,所以callalloc会走两次
当然呢,下面其实还有关于release,retain等的拦截优化,其实LLVM底层方法拦截优化这块思路并没有很清晰,后续还会继续探索补充,目前就点到为止(这把尽力局😄)
二.内存分配计算原则
上篇里面有提到alloc的两个关键流程,其中有提到关于_class_createInstanceFromZone,代码如下
_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 = (id)calloc(1, size);
}
if (slowpath(!obj)) {
if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
return _objc_callBadAllocHandler(cls);
}
return nil;
}
//关联isa
if (!zone && fast) {
obj->initInstanceIsa(cls, hasCxxDtor);
} else {
//使用原始的isa 前提是对区域或RR做一些未知的操作
// 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);
}
那么接下来我们就探索一下,关于内存的分配问题。
- 首先先附一张关于常见各种数据类型的所占用内存大小(字节),32位和64位
1.对象的内存对齐
对象内存分配要点
- 对象成员变量8字节对齐
- 对象之间16字节对齐(意味着对象至少16字节)
- 每个对象都会关联一个isa指针(8字节),作为首地址
以下是objc源码里面objc-os.h文件中的关于内存对齐规则
#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
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
static inline size_t word_align(size_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
获取内存大小的三种方式
-
sizeof是一个操作符,不是函数,sizeof计算内存大小时,只是计算当前传入的数据所需内存空间大小,在编译器的编译阶段大小就已经确定
-
class_getInstanceSize runtime提供的api,用于获取类对象或者实例对象的成员变量所占内存大小总和(包括isa)
-
malloc_size 获取系统实际分配的内存大小(考虑内存对齐之后的大小)
话不多说先测试一波,如图所示
先引入#import <malloc/malloc.h> #import<objc/runtime.h>
这是创建的一个对象YCXPerson,根据内存计算方式应该是24字节,8(isa)+8(1+1+4)+8(4),我们来看一下打印结果
首地址isa指针,打印出来时YCXPerson,后面依次是5,97(a对应的ask码),98(b对应的ask码),100,与上面设置的值一一对应,当时我打印得时候是使用得16进制得格式,最后考虑对象对齐原则也就是32了,而代码中的打印结果8-24-32,也正是对应isa指针,对象内的成员变量大小总和,已经考虑对象对齐之后的实际分配内存大小
PS:对象的成员变量在计算内存大小时,不需要考虑声明的顺序问题,会自行优化,确保最大化的空间利用率
对象内部添加自定义对象会如何?
还是先定义2个对象,计算一下他们的大小
为了更好的理解,以上面2个对象,提供一个示意图
成员变量内部先以8字节对齐后,在以对象之间16字节对齐,接下来在添加自定义对象为成员变量
打印结果是:40
打印结果是:48
相信结果显而易见了,lgp也是只占用8字节内存大小,其实也很容易理解,毕竟只是一个指针,所以说只有当LGPerson类alloc的时候开辟空间后,p.lgp = [LGPerson alloc],这时指针lgp才会指向这块新开辟的内存空间
那么如果是结构体呢,内存又该是如何计算呢?
2.结构体内存对齐
对齐原则(来源逻辑教育Cooci老师 优秀的一批)
- 数据成员对齐规则:结构体(struct)或联合体(union)的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的存储位置要从该成员大小或成员的子成员大小(只要该成员有子成员,比如说是数组结构体等)的整数倍开始(比如int是4字节,则要从4的整数倍地址开始存储)min(当前开始的位置mn)m=9 n=4 9 10 11 12
- 结构体作为成员,如果一个结构体里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储(struct a里面有struct b,b里面有char,int,double等元素,那b应该从8的整数倍开始存储)
- 收尾工作:结构体的总大小,也就是sizeof的结果,.必须是其内部最大,成员的整数倍.不足的要补⻬。
实践是检验真理的唯一标准!源自:YCXPerson(容我皮一下)
相信通过我注释的计算方式,应该不难看出,成员之间内存对齐最终需要以内存最大成员为标准保证字节对齐的。
结构体嵌套
通过测试不难发现,其实结构体内部添加结构体只需要在原来的内存大小基础之上相加就行,比如struct1大小是24,sturct3除去struct1其余成员按照对齐原则是24,再加上struct1就是48,这样计算就方便多了,当然其实本质上,都是遵循了各自内部最大成员对齐原则的。
PS:结构体的成员变量在计算内存大小时,要考虑成员变量的顺序问题,按顺序依次计算
为了更好的理解,就以struct1和struct3为例,提供一个示意图参考:
3.得出结论后的疑惑
问题1:为什么要对齐?
目的是为了提高内存的读取效率,通过一种固定规律的读取方式自然是比无规则的读取要快捷,如果所有成员变量的存储地址都是紧密相联,而不同类型的成员变量字节大小不一,还有可能出现读取错误的可能,所以采用对齐的方式,一方面提高读取效率,另一方面也可以提高读取数据的容错空间
问题2:为什么会采用这种对齐方式呢?
首先要明白一点每个对象都有isa指针,用来存储对象内存地址的,占用8字节,那么类似于结构体一样,按照内部最大内存的成员为基准对齐的话,自然而然也就是8字节对齐,那么对象之前对齐方式16字节也就合乎情理了,这样既可以满足读取效率的提升,同时也能保证不过多的占用空间
三.mallco源码分析
- 工欲善其事,必先利其器,要分析就得有malloc源码
1.定位calloc,malloc_zone_calloc
首先我们之前在分析objc-818.2源码时提到过开辟内存空间的核心方法是calloc,malloc_zone_calloc
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 = (id)calloc(1, size);
}
if (slowpath(!obj)) {
if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
return _objc_callBadAllocHandler(cls);
}
return nil;
}
if (!zone && fast) {
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);
}
2.开始探索calloc
那么接下来进去看一看calloc,malloc_zone_calloc分别在做什么
相信不难看出最终都是走到calloc,但是走到这里你会发现看不到calloc做了什么?
这时候就应该想到LLDB断点调试打印一下看看有什么线索
果然有发现呀,过来一翘,又是这玩意…继续打印
终于有新发现了,层层套娃真滴难啊 ಥ_ಥ,单步往下走发现这里直接return p,那么这个函数_nano_malloc_check_clear就是关键所在了
此时此刻还有谁?我们再来看下segregated_size_to_fit
看这里看这里!原来在这里是有做内存对齐的哦
在重识alloc流程(上)篇中也提到过对象的内存对齐,如一下代码实例
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
static inline size_t word_align(size_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
那么通过内存的分配计算原则实践得出的结论和mallco从源码角度去分析内存的分配,得出的结论也就完全对上了,那么今天的内容就到这里结束了。
PS:后续精彩内容请看重识alloc流程(下)