建议先看下我之前的Objc4-818底层探索(一):alloc探索(一)
首先补齐一些lldb
命令, 有助于后面探索
x
: 相应的表示打印
4
: 打印4个, 内存段
g
: 代表8字节
w
: 代表4字节
x
: 一般末尾x表示16进制打印
举个例子:
-
x/5gx test
: 以5个片段, 16字节读出内存段 -
0x101b4743
: 内存段地址 -
0x011d80010000834d
: 内存段第一个默认是isa
- 从第二个开始内存段为属性一些值, 留意下即使属性没赋值也会给开辟内存空间, 不然后续赋值不知道内存空间有多少
接来下补齐些上篇文章遗留的问题 Objc4-818底层探索(一):alloc探索(一)
问题1: 为什么对象alloc会先走objc_alloc
当我们step into
时候会发现alloc
→_objc_rootAlloc
→callAlloc
, 但是我们实际走的时候发现走的却是objc_alloc
我们搜索下objc_alloc
, 在objc-runtime-new.mm
→ fixupMessageRef
可找到
/***********************************************************************
* fixupMessageRef
* Repairs an old vtable dispatch call site.
* vtable dispatch itself is not supported.
**********************************************************************/
static void
fixupMessageRef(message_ref_t *msg)
{
msg->sel = sel_registerName((const char *)msg->sel);
if (msg->imp == &objc_msgSend_fixup) {
if (msg->sel == @selector(alloc)) {
msg->imp = (IMP)&objc_alloc;
} else if (msg->sel == @selector(allocWithZone:)) {
msg->imp = (IMP)&objc_allocWithZone;
} else if (msg->sel == @selector(retain)) {
msg->imp = (IMP)&objc_retain;
} else if (msg->sel == @selector(release)) {
msg->imp = (IMP)&objc_release;
} else if (msg->sel == @selector(autorelease)) {
msg->imp = (IMP)&objc_autorelease;
} else {
msg->imp = &objc_msgSend_fixedup;
}
}
else if (msg->imp == &objc_msgSendSuper2_fixup) {
msg->imp = &objc_msgSendSuper2_fixedup;
}
else if (msg->imp == &objc_msgSend_stret_fixup) {
msg->imp = &objc_msgSend_stret_fixedup;
}
else if (msg->imp == &objc_msgSendSuper2_stret_fixup) {
msg->imp = &objc_msgSendSuper2_stret_fixedup;
}
#if defined(__i386__) || defined(__x86_64__)
else if (msg->imp == &objc_msgSend_fpret_fixup) {
msg->imp = &objc_msgSend_fpret_fixedup;
}
#endif
#if defined(__x86_64__)
else if (msg->imp == &objc_msgSend_fp2ret_fixup) {
msg->imp = &objc_msgSend_fp2ret_fixedup;
}
#endif
}
这里先普及下sel
和 imp
sel
: 方法编号, 可理解成一本书的目录, 可通过对应名称找到页码
imp
: 函数指针地址, 可以理解成一本书的页面, 方便找到具体的实现函数
然后看下fixupMessageRef(message_ref_t *msg)
这个方法, 其中有这个,
if (msg->sel == @selector(alloc)) {
msg->imp = (IMP)&objc_alloc;
}
如果sel == @selector(alloc)
则msg->imp = (IMP)&objc_alloc
, 即 如果方法为alloc
我就让你去走objc_alloc
方法。
但是要留意下 这个方法是fixup
, 修正方法
。如果没有问题是不会走这个修正
的, 有问题才会走修正方法。这个方法源码其实在我们编译器LLVM
里面
LLVM
LLVM
是构架编译器的框架系统
,以C++编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time),对开发者保持开放,并兼容已有脚本。
这块简单介绍一些架构概念:
前端编译器(Frontend)
: 编译器的前端任务是解析源代码
, 会进行词法分析
、语法分析
、语义分析
。优化器(Optimizer)
: 负责各种优化, 改善代码的运行时间,如消除冗余计算等后端编译器(Backkend)/ 代码生成器(CodeGenerator)
: 将代码映射到目标指令集,生成机器语言,并进行机器相关的代码优化(目标指不同操作系统)。
iOS编译器架构
Objective C / C / C++
使用的编译器前端是Clang
,Swift
是swift
,后端都是LLVM
。
继续回到alloc
, 他的流程顺序依次是 OC
→Clang
→LLVM
→机器代码
。那么 alloc
方法一进LLVM
, 后端 LLVM
判断alloc, 这位是我们尊贵的VIP的顾客, 必须给走特殊方法
, 所以走特殊的处理流程。
接下来我们看下alloc
在llvm
是怎么处理的
全局搜索alloc
, 可在CGObjC.cpp
找到tryGenerateSpecializedMessageSend
方法
这个方法是llvm
针对于一些传入的sel方法, 做了一些不同的特殊处理, 其中就可以看到alloc
。这里可看到, llvm
判断如果进入的是alloc
方法, 那么去执行EmitObjCAlloc
, 我们点击进入EmitObjCAlloc
可看到llvm
让它去执行了objc_alloc
方法
由此alloc
会走到objc+alloc
, 其实这部分是由llvm
做的特殊消息处理逻辑。
第一次与第二次的区别:
第一次是系统级别的alloc
, 而第二次是消息转发objc_msgSend
发起的alloc
。其中objc_msgSend
走慢速查找, 针对于当前类的ISA
做了些处理, 所以第二次会走if (fastpath(!cls->ISA()->hasCustomAWZ()))
这个判断。(这块后续补充)
首先我们先看个例子, SATest.h
里面创建一些属性
main
里面调用一下, 并读取
- 冒号
:
左边的0x101c04100
,0x101c04110
...是内存段的地址, 第一个0x101c04100
是alloc
开辟内存的首地址, 也是SATest *test
的地址。 - 冒号
:
右边的0x011d8001000083b5
,0x0000001200006261
...是内存段的值(po 读取一下对应内存段也可以看到) - 每片内存段占
8
字节 0x61 = 97 = ASCII码中的 a
- 浮点数要读取比较特殊
e -f f -- XXX
或``p/f XXX`, 当然我们也用一些函数方法, 还原下
void lg_float2HEX(float f){
union uuf { float f; char s[4]; } uf;
uf.f = f;
printf("0x");
for (int i=3;i>=0;i--)
printf("%02x", 0xff & uf.s[i]);
printf("\n");
}
void lg_double2HEX(double d){
union uud { double d; char s[8];} ud;
ud.d = d;
printf("0x");
for (int i=7;i>=0;i--)
printf("%02x",0xff & ud.s[i]);
printf("\n");
}
可能问题就来了, 为什么会这样拍, 而且int age, char a, b为什么会放到一起, 接下来我们就探索下
内存对齐
内存对齐原则
每个平台的编辑器都有自己的
对齐系数
, 程序员也可以通过预编译命令#pragma pack(n), n=1,2,4,8,16来改变这一系数, 其n就是你指定的对齐系数
。在ios中xcode默认为8
, 即8
字节对齐数据成员对齐可以理解为
min(m, n)
公式, 其中m
表示当前成员开始位置,n
表示当前成员所需要的位数。如果满足m
整除n
(m % n == 0
),n
从m
位置开始存储, 反之m
循环+1
, 直至可以整除, 从而确定了当前成员位置。数组成员为
结构体
, 当结构体嵌套结构体
时, "成员"的结构体的自身长度为"成员"结构体中最大成员的内存大小
, 例如: 结构体a嵌套结构体b,b中有char、int、double等,则b的自身长度为8结构体
的内存大小必须为结构体最大成员内存大小
的整数倍
, 不足需要补齐
先熟悉下 ios中数据类型的占用内存大小, 方便后面的例子计算
例子
struct Struct1 {
double a;
char b;
int c;
short d;
}struct1;
struct Struct2 {
double a;
int b;
char c;
short d;
}struct2;
struct Struct3 {
double a;
int b;
char c;
short d;
int e;
struct Struct1 str;
}struct3;
NSLog(@"结构体1占用的内存大小 %lu", sizeof(struct1));
NSLog(@"结构体2占用的内存大小 %lu", sizeof(struct2));
NSLog(@"结构体3占用的内存大小 %lu", sizeof(struct3));
解题思路
结构体1
a
: double类型占8字节, 即 0~7存double a
b
: char类型占1字节, 因为 8 % 1 = 0, 即8存char b
c
: int类型占4字节,
9 % 4 != 0, 不满足 +1,
10 % 4 != 0, 不满足 +1,
11 % 4 != 0, 不满足 +1,
12 % 4 == 0, 满足, 即, 12~15存 int c
d
: short类型占2字节, 16 % 4 != 0, 满足 +1, 即16, 17存short d
结构体1
需要内存18
字节, 由于最大字节数为8
(double的8), 结构体必须是8的倍数, 所以需要向上取整, 固最终结果为24
结构体2
a
: double类型占8字节, 即 0~7存double a
b
: int类型占4字节, 8 % 4 == 0, 8~11存储int b
c
: char类型占1字节, 因为 12 % 1 = 0, 即9存char c
d
: short类型占2字节
13 % 2 != 0, 不满足 +1,
14 % 2 == 0, 满足, 即14, 15存short d
结构体2
需要内存16
字节, 由于最大字节数为8
(double的8), 结构体必须是8的倍数, 16满足8的倍数, 固最终结果为16
之前的问题解决了, 我们接下来看下结构体嵌套结构体
结构体3
前四个跟结构体2一样, 从 int e;
开始
e
: int类型占4字节, 16 % 4 == 0, 16~19存储int b
str
: 结构体类型, 就是结构体1, 结构体1占24字节, 留意下, 当结构体嵌套结构体
时, "成员"的结构体的自身长度为"成员"结构体中最大成员的内存大小
, 最大成员为double的8, 那么我们要找到位置满足, 位置 % 8 == 0, 开始存放结构体1
20 % 8 != 0, 不满足 +1,
21 % 8 != 0, 不满足 +1,
22 % 8 != 0, 不满足 +1,
23 % 8 != 0, 不满足 +1,
24 % 8 == 0, 不满足, 24~47存struct Struct1 str
结构体3
需要内存48
字节, 由于最大字节数为8
, 48满足8的倍数, 固最终结果为48
验证结果
上面的例子也可看出结构体内存大小与结构体成员内存大小的顺序有关:
如果是结构体中数据成员是根据内存
从小到大
的顺序定义的,根据内存对齐规则来计算结构体内存大小,需要增加有较大的内存padding即内存占位符,才能满足内存对齐规则,比较浪费内存如果是结构体中数据成员是根据内存
从大到小
的顺序定义的,根据内存对齐规则来计算结构体内存大小,我们只需要补齐少量内存padding即可满足堆存对齐规则,这种方式就是苹果中采用的,利用空间换时间,将类中的属性进行重排,来达到优化内存的目的
属性重排
接下来看一下之前那int age
, 与char a
char b
排到一起的0x0000001200006261
问题
-
0x00000012
: 18 -
62
: 0x62 = 98 = b -
61
: 0x61 = 97 = a
首先这里排在一起, 是苹果系统自动帮我们优化处理的, 做了内存优化属性重排
。
虽然大部分的内存都是通过固定的内存块进行读取,尽管苹果会采用对齐
的方式处理优化,但并不是所有的内存都可以进行浪费的,苹果会自动对属性进行重排,以此来优化内存。
还是先看了例子
例子1
里面涉及三个读取内存知识点sizof
, class_getInstanceSize
, malloc_size
sizof
-
sizeof
: 对象类型占用大小, 里面可以放基础数据类型
,对象
,指针
等, 比如
int a, sizeof(a) = 4;
short b, sizeof(b) = 2;
NSString *c, sizeof(c) = 8如果放入
基础数据类型
, 则直接读出数据类型大小如果放入是
NSObject/继承NSObject
对象, 直接读它本质
,NSObject本质
是结构体(struct objc_class : objc_object)
, 对象在64bit
下为8字节。
class_getInstanceSize
-
class_getInstanceSize
: 实例对象中成员变量的内存大小。 -
class_getInstanceSize
是runtime提供的api
, 用于获取类的实例对象所占用的内存空间的大小, 并返回具体的字节数。
malloc_size
malloc_size
: 系统实际分配内存大小, 这个是由系统完成的, 涉及16字节内存对齐
。
那么上面的例子
*test 指针类型, 64位占8字节; 对象里面只有isa, 所以class_getInstanceSize
占用8字节; 而系统默认16字节对齐分配内存, 所以malloc_size
为16
例子2
我们再加一个成员变量看一下
#import
@interface SATest : NSObject {
NSString *a;
}
@end
可看到class_getInstanceSize
变了(isa + NSString = 8 +8 = 16), 说明成员变量大小的确影响实际开辟内存大小
例子3
我们再加个属性
#import
@interface SATest : NSObject {
NSString *a;
}
@property (nonatomic, strong) NSString *b;
@end
可看到class_getInstanceSize
也变了(isa + NSString + NSString = 8 + 8 + 8 = 24), 说明属性也会影响内存大小。malloc_size
系统实际分配内存大小, 要满足16字节内存对齐
。
例子4
我们再加个方法
@interface SATest : NSObject {
NSString *a;
}
@property (nonatomic, strong) NSString *b;
- (void)sayHello;
@end
可看到class_getInstanceSize
没有变, 说明方法不会影响内存大小(类方法也一样)
其实属性在底层也是成员变量 + set/get方法, 所以影响开辟内存大小的只有成员变量
malloc_size
接下来我们看下malloc_size
, 看下底层, 看下它是怎么样进行16字节对齐
malloc
代码在libmalloc
里面
我们先在main
中, 加一个calloc
跟一下内容
#import
#import
int main(int argc, const char * argv[]) {
@autoreleasepool {
void *p = calloc(1, 40);
NSLog(@"%lu",malloc_size(p));
NSLog(@"Hello, World!");
}
return 0;
}
void *
calloc(size_t num_items, size_t size)
{
return _malloc_zone_calloc(default_zone, num_items, size, MZ_POSIX);
}
MALLOC_NOINLINE
static void *
_malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size,
malloc_zone_options_t mzo)
{
MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);
void *ptr;
if (malloc_check_start) {
internal_check();
}
// 关键代码, 因为最后需要返回的是ptr
ptr = zone->calloc(zone, num_items, size);
if (os_unlikely(malloc_logger)) {
malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE | MALLOC_LOG_TYPE_CLEARED, (uintptr_t)zone,
(uintptr_t)(num_items * size), 0, (uintptr_t)ptr, 0);
}
MALLOC_TRACE(TRACE_calloc | DBG_FUNC_END, (uintptr_t)zone, num_items, size, (uintptr_t)ptr);
if (os_unlikely(ptr == NULL)) {
malloc_set_errno_fast(mzo, ENOMEM);
}
return ptr;
}
void *(* MALLOC_ZONE_FN_PTR(calloc))(struct _malloc_zone_t *zone, size_t num_items, size_t size); /* same as malloc, but block returned is set to zero */
跟到这里我们发现没法继续了, 找不到下一层了。
当断点进入这块时候,
1.可走汇编, 汇编能看出他走了什么方法
2.可通过lldb命令 zone->calloc
查看他存在哪里。因为有赋值就会有存储值, 有存储就能打印
接下来, 查找
default_zone_calloc
方法
有些时候po读不出来, 我们也可以p
一下, 读完整
因为不会超过NANO_MAX_SIZE
值(超过系统会发生问题), 所以其中关键代码是p
这里
void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
if (p) {
return p;
因为void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
, 所以我们要找_nano_malloc_check_clear
方法, 点击进入
static void *
_nano_malloc_check_clear(nanozone_t *nanozone, size_t size, boolean_t cleared_requested)
{
MALLOC_TRACE(TRACE_nano_malloc, (uintptr_t)nanozone, size, cleared_requested, 0);
void *ptr;
size_t slot_key;
size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); // Note slot_key is set here
mag_index_t mag_index = nano_mag_index(nanozone);
nano_meta_admin_t pMeta = &(nanozone->meta_data[mag_index][slot_key]);
ptr = OSAtomicDequeue(&(pMeta->slot_LIFO), offsetof(struct chained_block_s, next));
if (ptr) {
unsigned debug_flags = nanozone->debug_flags;
#if NANO_FREE_DEQUEUE_DILIGENCE
size_t gotSize;
nano_blk_addr_t p; // the compiler holds this in a register
p.addr = (uint64_t)ptr; // Begin the dissection of ptr
if (NANOZONE_SIGNATURE != p.fields.nano_signature) {
malloc_zone_error(debug_flags, true,
"Invalid signature for pointer %p dequeued from free list\n",
ptr);
}
if (mag_index != p.fields.nano_mag_index) {
malloc_zone_error(debug_flags, true,
"Mismatched magazine for pointer %p dequeued from free list\n",
ptr);
}
gotSize = _nano_vet_and_size_of_free(nanozone, ptr);
if (0 == gotSize) {
malloc_zone_error(debug_flags, true,
"Invalid pointer %p dequeued from free list\n", ptr);
}
if (gotSize != slot_bytes) {
malloc_zone_error(debug_flags, true,
"Mismatched size for pointer %p dequeued from free list\n",
ptr);
}
if (!_nano_block_has_canary_value(nanozone, ptr)) {
malloc_zone_error(debug_flags, true,
"Heap corruption detected, free list canary is damaged for %p\n"
"*** Incorrect guard value: %lu\n", ptr,
((chained_block_t)ptr)->double_free_guard);
}
#if defined(DEBUG)
void *next = (void *)(((chained_block_t)ptr)->next);
if (next) {
p.addr = (uint64_t)next; // Begin the dissection of next
if (NANOZONE_SIGNATURE != p.fields.nano_signature) {
malloc_zone_error(debug_flags, true,
"Invalid next signature for pointer %p dequeued from free "
"list, next = %p\n", ptr, "next");
}
if (mag_index != p.fields.nano_mag_index) {
malloc_zone_error(debug_flags, true,
"Mismatched next magazine for pointer %p dequeued from "
"free list, next = %p\n", ptr, next);
}
gotSize = _nano_vet_and_size_of_free(nanozone, next);
if (0 == gotSize) {
malloc_zone_error(debug_flags, true,
"Invalid next for pointer %p dequeued from free list, "
"next = %p\n", ptr, next);
}
if (gotSize != slot_bytes) {
malloc_zone_error(debug_flags, true,
"Mismatched next size for pointer %p dequeued from free "
"list, next = %p\n", ptr, next);
}
}
#endif /* DEBUG */
#endif /* NANO_FREE_DEQUEUE_DILIGENCE */
((chained_block_t)ptr)->double_free_guard = 0;
((chained_block_t)ptr)->next = NULL; // clear out next pointer to protect free list
} else {
ptr = segregated_next_block(nanozone, pMeta, slot_bytes, mag_index);
}
if (cleared_requested && ptr) {
memset(ptr, 0, slot_bytes); // TODO: Needs a memory barrier after memset to ensure zeroes land first?
}
return ptr;
}
这块代码很长, 我们找到关键代码即可, 因为返回的是ptr
, 我们就寻找ptr
赋值地方就可以。关键代码ptr = segregated_next_block(nanozone, pMeta, slot_bytes, mag_index);
也可进去看一下不过是一些很长回调, 我们实际要获取的是大小, 所以其实关键要找到是slot_bytes
, 关键代码size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); // Note slot_key is set here
#define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM) // 16
#define SHIFT_NANO_QUANTUM 4
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;
}
segregated_size_to_fit
这里边可看到对传入的size做了
- size + 16 - 1 >> 4 再 <<4操作 后4位抹零, 即
16字节对齐
例如1: size = 8, 8 + 15 = 23 = 0001 1000
0001 0111
左移4位
0000 0001
右移4位
0001 0000 = 16
例如2: size = 23, 23 + 15 = 38 = 0010 0110
0010 0110
左移4位
0000 0010
右移4位
0010 0000 = 32
所以malloc_size
其实做了16字节
对齐操作