一、alloc的真实流程
1.1 实际探索中发现alloc流程底层是先调用objc_alloc
先下个断点看下alloc实际调用的是什么方法
断住alloc方法
查看汇编代码
汇编代码
可以看到汇编代码实际调用的是objc_alloc,这就很奇怪了,源代码里明明是alloc调用的_objc_rootAlloc 上源码
+ (id)alloc {
return _objc_rootAlloc(self);
}
1.2分析objc_alloc的调用流程
这是为啥呢,来搜一下objc_alloc吧看看能不能找到线索
image.png
在objc-runtime-new.mm文件里找到fixupMessageRef(message_ref_t *msg)函数里修改了alloc的imp 再看下调用方可以发现是在_read_images方法里调用的通过方法名和注释知道fixupMessageRef(message_ref_t *msg)函数是用来fix up objc_msgSend_fixup 修复消息错误的 也就是修复之前对alloc的hook
image.png
继续往上查找调用链发现有两个调用链
_objc_init_image
->_read_images
->fixupMessageRef
_objc_init
->_dyld_objc_notify_register
(取map_images的地址作为参数调用) ->map_images
->map_images_nolock
->_read_images
->fixupMessageRef
image的加载 和 _objc_init调用都是dyld加载应用程序的时候执行的,而在这个早的时机就需要修复之前的hook,那对alloc的hook只能更早,猜测是在编译阶段就hook了
1.3验证真实的alloc流程
llvm源码中对alloc的hook在llvm的源码中也找到了对alloc hook的代码,证实了我们的猜想。
结论:
llvm
在编译阶段对alloc做了hook,创建对象时第一次调用alloc方法会进入objc_alloc方法然后进入callAlloc方法callAlloc方法里通过objc_msgSend再次调用alloc方法这次调用才会走alloc的正常流程(即iOS底层原理探究01-alloc底层原理里所说的流程)
二、结构体内存对齐
2.1对象大小的影响因素探索
先来看看源码静态分析
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.2结构体内存对齐
结构体内存对齐
各数据类型占用的内存结构体内存对齐原则
- :数据成员对⻬规则:结构(struct)(或联合(union))的数据成员,第
⼀个数据成员放在offset为0的地⽅,以后每个数据成员存储的起始位置要
从该成员⼤⼩或者成员的⼦成员⼤⼩(只要该成员有⼦成员,⽐如说是数组,
结构体等)的整数倍开始(⽐如int为4字节,则要从4的整数倍地址开始存
储。 min(当前开始的位置m n) m = 9 n = 4
9 10 11 12- 结构体作为成员:如果⼀个结构⾥有某些结构体成员,则结构体成员要从
其内部最⼤元素⼤⼩的整数倍地址开始存储.(struct a⾥存有struct b,b
⾥有char,int ,double等元素,那b应该从8的整数倍开始存储.)- 收尾⼯作:结构体的总⼤⼩,也就是sizeof的结果,.必须是其内部最⼤
成员的整数倍.不⾜的要补⻬。
根据对齐原则分析结构体的内存大小
struct LGStruct1 {
double a; // 8 [0 7]
char b; // 1 [8]
int c; // 4 (9 10 11 [12 13 14 15]
short d; // 2 [16 17] 24
}struct1;
struct LGStruct2 {
double a; // 8 [0 7]
int b; // 4 [8 9 10 11]
char c; // 1 [12]
short d; // 2 (13 [14 15] 16
}struct2;
struct LGStruct3 {
double a; // 8 [0 ~ 7]
int b; // 4 [8 9 10 11 12]
char c; // 1 [13]
short d; // 2 [14 15]
int e; // 4 [16 17 18 19]
struct LGStruct1 str; // 24 (20 21 22 23 [24 ~ 48)
}struct3;
struct1中a占用0 ~ 7八个字节,b占用第8个位置一个字节,c因为是4字节的要从4的整数倍开始存所以9、10、11位置留空要从12位置开始存占用12 ~ 15四字节,因为16位置是2的整数倍所以d占用16 ~ 17两字节,最后因为struct1中最大的成员是double类型的a占用8字节 所以整个结构体的大小要是8的整数倍,即整个结构体要占用24个字节 ,同理可以得到struct2 大小是16 struct3大小是 48
接下来我们验证一下
NSLog(@"%lu-%lu-%lu",sizeof(struct1),sizeof(struct2),sizeof(struct3));
打印结果为
内存大小验证
打印结果跟我们的分析是一致的
最后我们在通过一个问题引出下一篇对malloc的探索
image.png
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface LGPerson : NSObject
//隐藏的isa 8
@property (nonatomic, copy) NSString *name;//8
@property (nonatomic, copy) NSString *nickName;//8
@property (nonatomic, assign) int age;//4
@property (nonatomic, assign) long height;//8
@end
NS_ASSUME_NONNULL_END
上面是LGPerson的定义 对象的本质是结构体指针(下一篇会详细讲),通过结构体字节对齐的规则计算得到LGPerson底层的结构体应该占用40个字节
但是打印结果为啥是8 - 40 - 48呢,
- 首先8是打印的person它是一个指针,8字节没有问题
- class_getInstanceSize([LGPerson class])返回类对象至少需要多少空间,打印的是40 通过我们的计算也没有问题
- malloc_size((__bridge const void *)(person)) 返回的是实际分配的内存空间,打印的是48,这就有问题了系统为啥要分配48呢,这个问题我们下一篇详细分析一下(因为malloc分配内存的时候是16字节对齐的,这里提前给懒人剧透一下)
补充:内存打印指令
x /nuf <addr>
n表示要显示的内存单元的个数
u表示一个地址单元的长度:
b表示单字节
h表示双字节
w表示四字节
g表示八字节
f表示显示方式,可取如下值:
x按十六进制格式显示变量
d按十进制格式显示变量
u按十进制格式显示无符号整型
o按八进制格式显示变量
t按二进制格式显示变量
a按十六进制格式显示变量
i指令地址格式
c按字符格式显示变量
f按浮点数格式显示变量
网友评论