有兴趣的朋友可以观看我的上一篇文章《OC对象原理探究之alloc探索》
一、对象的内存空间
先给HPerson
添加对象:
打上断点,运行,然后在lldb
中输入x p
,显示对象p
的内存分布:
0x108f079c0
是对象p
的内存首地址,接下来就是对象p
的内存。
iOS端为小端模式
,所有需要倒着读取:
内存打印出来是什么呢?
是isa
!
为什么没有打印出isa
呢?
因为要&ISA_MASK
:
# if __arm64__
// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e.
# if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
# define ISA_MASK 0x007ffffffffffff8ULL
# else
# define ISA_MASK 0x0000000ffffffff8ULL
# endif
这里使用的是模拟器,所以&0x0000000ffffffff8
:
现在就正确的打印出isa
了!
后面的0
则为对象的属性的存储空间!
进入debug
查看一下内存:
输入内存首地址:
发现即使没有给属性赋值,依旧会开辟内存!
给属性赋值后再运行:
x/5gx
为格式化输出!
x/nuf <addr>
n表示要显示的内存单元的个数
—————
u表示一个地址单元的长度:
b表示单字节
h表示双字节
w表示四字节
g表示八字节
—————
f表示显示方式,可取如下值:
x按十六进制格式显示变量
d按十进制格式显示变量
u按十进制格式显示无符号整型
o按八进制格式显示变量
t按二进制格式显示变量
a按十六进制格式显示变量
i指令地址格式
c按字符格式显示变量
f按浮点数格式显示变量
如果把height
改为BOOL
类型:
就会发现age
和height
放在了一起!
这是苹果的底层对内存进行了优化,即内存对齐
!
二、对象的内存影响因素
先引入runtime
:
#import <objc/runtime.h>
然后打印对象实例大小
:
发现大小为32
。
我们再删除一些属性
,重新打印:
发现大小为16
!
说明属性
对类的大小
是有影响的!
加上成员变量
后再打印:
发现大小为24
。
说明成员变量
对类的大小
也是有影响的!
那么方法
是否会影响类的大小
呢?
添加方法
:
发现类的大小
依旧是24
!
说明方法
对类的大小
是没有影响的!
结论:只有成员变量会影响类的大小!
如下图所示:
三、结构体内存对齐
1、各类型所用字节:
2、内存对⻬的原则:
1:数据成员对⻬规则:结构(struct)(或联合(union))的数据成员,第
⼀个数据成员放在offset为0的地⽅,以后每个数据成员存储的起始位置要
从该成员⼤⼩或者成员的⼦成员⼤⼩(只要该成员有⼦成员,⽐如说是数组,
结构体等)的整数倍开始(⽐如int为4字节,则要从4的整数倍地址开始存
储。
min(当前开始的位置m n) m = 9 n = 4
9 10 11 12
2:结构体作为成员:如果⼀个结构⾥有某些结构体成员,则结构体成员要从
其内部最⼤元素⼤⼩的整数倍地址开始存储.(struct a⾥存有struct b,b
⾥有char,int ,double等元素,那b应该从8的整数倍开始存储.)
3:收尾⼯作:结构体的总⼤⼩,也就是sizeof的结果,.必须是其内部最⼤
成员的整数倍.不⾜的要补⻬。
3、分析结构体:
先看看这2个结构体
:
结构体1:
struct LGStruct1 {
double a;
char b;
int c;
short d;
}struct1;
结构体2:
struct LGStruct2 {
double a;
int b;
char c;
short d;
}struct2;
这2个结构体
占用的字节
是否一样呢?
我们来分析一下:
结构体1:
double为8字节,从8的整数倍开始,占用8字节,所以a在[0-7]
char为1字节,从1的整数倍开始,占用1字节,所以b在[8]
int为4字节,从4的整数倍开始,占用4字节,所以9、10、11空出,c在[12-15]
short为2字节,从2的整数倍开始,占用2字节,所以d在[16-17]
所用结构体1
一共18
字节。
但是根据内存对齐原则
,结构体的总⼤⼩为内部最⼤成员的整数倍,即24
!
结构体2:
double为8字节,从8的整数倍开始,占用8字节,所以a在[0-7]
int为4字节,从4的整数倍开始,占用4字节,所以b在[8-11]
char为1字节,从1的整数倍开始,占用1字节,所以c在[12]
short为2字节,从2的整数倍开始,占用2字节,所以13空出,d在[14-15]
所用结构体2
一共16
字节。
根据内存对齐原则
,总大小为16
!
再来看看结构体3:
struct LGStruct3 {
double a;
int b;
char c;
short d;
int e;
struct LGStruct1 str;
}struct3;
这种结构体
嵌套的结构体
又该怎么分析呢?
double为8字节,从8的整数倍开始,占用8字节,所以a在[0-7]
int为4字节,从4的整数倍开始,占用4字节,所以b在[8-11]
char为1字节,从1的整数倍开始,占用1字节,所以c在[12]
short为2字节,从2的整数倍开始,占用2字节,所以13空出,d在[14-15]
int为4字节,从4的整数倍开始,占用4字节,所以e在[16-19]
LGStruct1为24字节,LGStruct1内最大成员为8字节,所以从8的整数倍开始,占24字节,所以str在[24-47]
所用结构体3
一共48
字节。
根据内存对齐原则
,总大小为48
!
我们看看打印的结果吧:
和我们分析的一致!
那么我们平时写属性
的时候需要注意顺序
吗?
来看看对象
的内存分布
就知道了:
发现内存分布
并没有按照属性
顺序排序!
xcode
已经帮我们做了优化
了!
也可以进行二进制类型的重排
、数据类型的重排
等等。
4、为什么需要内存对齐?
一个结构体有char、int、long
类型:
如果没有内存对齐:
处理器
读取数据需要先读1字节
,再读4字节
,再读8自己
,多次读取,效率低下!
如果有内存对齐:
处理器读取数据只需要8字节、8字节
的去读就可以了!方便读取!效率非常高!
所以内存对齐就是空间换时间!提高效率!
四、malloc源码引入
依旧是Hperson
:
引入malloc.h
#import <malloc/malloc.h>
看看这个打印结果:
3个值分别是什么呢?
sizeof 是否是 8 + 8 + 4 + 8 = 28 ?还是 32 ?又或者是40呢?
class_getInstanceSize 呢?
malloc_size 呢?
来看看打印结果
:
分别为8、40、48
!
为什么呢?
p为指针,指针的大小为8!
class_getInstanceSize([HPerson class])获取的是HPerson类的大小!
HPerson类有isa、name、nickName、age、height,共5个成员变量!所以大小为40!
那么malloc_size
又是什么呢?
我们点进去看看:
发现没有实现
!
再看看路径
:
去苹果的源代码目录下载源码:
然后可以参考《libmalloc源码浅谈》进行编译!
五、malloc分析探索思路
在上一篇文章《OC对象原理探究之alloc探索》中我们已经知道了calloc
是开辟内存的方法。
现在我们就来探索一下calloc
!
先在源码的main.h
中使用calloc
方法:
跟进去看看:
void *
calloc(size_t num_items, size_t size)
{
return _malloc_zone_calloc(default_zone, num_items, size, MZ_POSIX);
}
再进入到_malloc_zone_calloc
方法:
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 = 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;
}
那么这段代码的重点在哪呢?
先看return
,再根据return
寻找:
跟着calloc
进去:
发现只是一个声明
!
接下来怎么探索呢?
全局搜索calloc
:
发现有赋值
,即有存储值
,那么就可以打印
!
断上断点进行打印
:
全局搜索找到default_zone_calloc
进去:
static void *
default_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
zone = runtime_default_zone();
return zone->calloc(zone, num_items, size);
}
继续断点打印
:
全局搜索找到nano_calloc
进去:
static void *
nano_calloc(nanozone_t *nanozone, size_t num_items, size_t size)
{
size_t total_bytes;
if (calloc_get_size(num_items, size, 0, &total_bytes)) {
return NULL;
}
if (total_bytes <= NANO_MAX_SIZE) {
void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
if (p) {
return p;
} else {
/* FALLTHROUGH to helper zone */
}
}
malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone);
return zone->calloc(zone, 1, total_bytes);
}
那么,nano_calloc
方法的重点又在哪呢?
先看return
!
第一个return NULL;
返回空
,这种肯定有问题,可以忽略!
那么return p;
和return zone->calloc(zone, 1, total_bytes);
会走哪个呢?
在return p;
前有个if (total_bytes <= NANO_MAX_SIZE)
可得出,正常情况肯定是走return p;
!
然后进入创建p
的_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;
}
这种很长的代码应该怎么解读呢?
就从小玩到大!
把if
折叠起来看:
这就很清楚了:存在ptr
就会怎么怎么样,不存在ptr
又会怎么怎么样!
先看看存在ptr
会做什么!
一看就发现if
里面都是调用malloc_zone_error
方法,很明显,这个方法是报错
的方法!
再看看不存在ptr
会做什么!
进入segregated_next_block
方法:
static MALLOC_INLINE void *
segregated_next_block(nanozone_t *nanozone, nano_meta_admin_t pMeta, size_t slot_bytes, unsigned int mag_index)
{
while (1) {
uintptr_t theLimit = pMeta->slot_limit_addr; // Capture the slot limit that bounds slot_bump_addr right now
uintptr_t b = OSAtomicAdd64Barrier(slot_bytes, (volatile int64_t *)&(pMeta->slot_bump_addr));
b -= slot_bytes; // Atomic op returned addr of *next* free block. Subtract to get addr for *this* allocation.
if (b < theLimit) { // Did we stay within the bound of the present slot allocation?
return (void *)b; // Yep, so the slot_bump_addr this thread incremented is good to go
} else {
if (pMeta->slot_exhausted) { // exhausted all the bands availble for this slot?
pMeta->slot_bump_addr = theLimit;
return 0; // We're toast
} else {
// One thread will grow the heap, others will see its been grown and retry allocation
_malloc_lock_lock(&nanozone->band_resupply_lock[mag_index]);
// re-check state now that we've taken the lock
if (pMeta->slot_exhausted) {
_malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]);
return 0; // Toast
} else if (b < pMeta->slot_limit_addr) {
_malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]);
continue; // ... the slot was successfully grown by first-taker (not us). Now try again.
} else if (segregated_band_grow(nanozone, pMeta, slot_bytes, mag_index)) {
_malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]);
continue; // ... the slot has been successfully grown by us. Now try again.
} else {
pMeta->slot_exhausted = TRUE;
pMeta->slot_bump_addr = theLimit;
_malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]);
return 0;
}
}
}
}
}
发现是一个死循环
!
里面有一个注释
:
// Did we stay within the bound of the present slot allocation?
即可知segregated_next_block
方法是在寻找可用内存空间
!
但是我们的重点是在找这个对象的大小
!
那么是哪个决定了大小
呢?
在上一个方法_nano_malloc_check_clear
方法里面:
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
于是我们进入segregated_size_to_fit
方法:
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;
}
再看看NANO_REGIME_QUANTA_SIZE
:
#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)
得到了确定大小
的公式
:
k = size + 15
然后k先右移4位再左移4位
即向上取16的整数倍!
这就是内存对齐的公式
!即16字节对齐
!
和上一章的字节对齐公式
类似!
字节对齐
即为向上取8
的整数倍,也就是先右移3位
再左移3位
!
所以:
对象的内存对齐是16字节对齐!
成员变量的对齐是8字节对齐!
六、对象内存对齐原理
对象的内存对齐
为什么是16
字节呢?
比如有3个对象的大小都为8 * 3 = 24
字节!
如果内存对齐是8
字节的话,这3个对象就会紧密相连
!
容易造成访问错误
!
如果内存对齐是16
字节的话,就会有空余
,提高容错率
!
同时,对象来自于NSObject
,而NSObject
自身有一个isa
成员变量,所以再加任意一个成员变量的对象的大小至少都是16
字节!
所以对象的内存对齐
是按照16
字节对齐!