欢迎阅读iOS探索系列(按序阅读食用效果更加)
- iOS探索 alloc流程
- iOS探索 内存对齐&malloc源码
- iOS探索 isa初始化&指向分析
- iOS探索 类的结构分析
- iOS探索 cache_t分析
- iOS探索 方法的本质和方法查找流程
- iOS探索 动态方法解析和消息转发机制
- iOS探索 浅尝辄止dyld加载流程
- iOS探索 类的加载过程
- iOS探索 分类、类拓展的加载过程
- iOS探索 isa面试题分析
- iOS探索 runtime面试题分析
- iOS探索 KVC原理及自定义
- iOS探索 KVO原理及自定义
- iOS探索 多线程原理
- iOS探索 多线程之GCD应用
- iOS探索 多线程之GCD底层分析
- iOS探索 多线程之NSOperation
- iOS探索 多线程面试题分析
- iOS探索 细数iOS中的那些锁
- iOS探索 全方位解读Block
写在前面
在iOS探索 alloc流程一文中讲了底层对象创建的流程,那么本文将来探索下对象中的属性在内存中的排列
一、探索目标
1.测试代码
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>
@interface FXPerson : NSObject
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) long height;
@property (nonatomic, assign) char c1;
@property (nonatomic, assign) char c2;
@end
@implementation FXPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
FXPerson *p = [FXPerson alloc];
p.name = @"Felix";
p.age = 20;
p.height = 180;
p.c1 = 'a';
p.c2 = 'b';
NSLog(@"\nsizeof——%lu\nclass_getInstanceSize——%lu\nmalloc_size——%lu", sizeof([p class]), class_getInstanceSize([p class]), malloc_size((__bridge const void *)(p)));
}
return 0;
}
**注:如果对象创建了没去赋值属性——它会是内存假地址
2.LLDB调试命令等预备知识
①x 对象
表示以16进制打印对象内存地址(x表示16进制)
因为iOS是小端模式
(数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中——反过来存放数据)所以要倒着读数据
(lldb) x p
0x10060eea0: c5 13 00 00 01 80 1d 00 61 62 00 00 00 00 00 00 ........ab......
0x10060eeb0: 14 00 00 00 00 00 00 00 50 10 00 00 01 00 00 00 ........P.......
②x/4gx 对象
表示输出4个16进制的8字节地址空间(x表示16进制,4表示4个,g表示8字节为单位,等同于x/4xg 对象
)
(lldb) x/4gx p
0x10060eea0: 0x001d8001000013c5 0x0000000000006261
0x10060eeb0: 0x0000000000000014 0x0000000100001050
左边是内存地址,右边两段是内存值
③po
与p
:p表示"expression"——打印对象指针;而po是"expression -O"——打印对象本身
(lldb) p p
(FXPerson *) $0 = 0x0000000101857750
(lldb) po p
<FXPerson: 0x101857750>
④Xcode查看内存地址 debug->Debug Workflow->view memory
有些操作可能用不到,读者可以自行拓展
3.修改代码查看内存
FXPerson
类中先声明name
,再声明age
(lldb) x/6gx p
0x10062c380: 0x001d8001000013c5 0x0000000000006261
0x10062c390: 0x0000000100001050 0x0000000000000014
0x10062c3a0: 0x00000000000000b4 0x0000000000000000
FXPerson
类中先声明age
,再声明name
(lldb) x/6gx p
0x100538e00: 0x001d8001000013c5 0x0000000000006261
0x100538e10: 0x0000000000000014 0x0000000100001050
0x100538e20: 0x00000000000000b4 0x0000000000000000
根据我们的计算机基础和LLDB指令,可以发现
- 第一段不知道是啥
- 第二段中62、63分别是
a
、b
的ASCII编码 - 第三段中的14是
20
的十六进制 - 第四段中po出来是
Felix
- 第五段是
180
4.查看控制台输出
sizeof——8
class_getInstanceSize——40
malloc_size——48
5.去掉声明属性查看控制台输出
FXPerson
类中不声明任何属性
sizeof——8
class_getInstanceSize——8
malloc_size——16
6.提出问题
Q1:为什么声明属性的前后会影响对象的内存排列呢?
Q2:sizeof、class_getInstanceSize、malloc_size分别是什么?
Q3:不是说对象最少为16字节,为什么class_getInstanceSize还能输出8字节?
二、内存对齐
1.二进制重排
二进制重排——将最经常执行的代码或最需要关键执行的代码(如启动阶段的顺序调用)聚合在一起,将无关紧要的代码放在较低的优先级,形成一个更紧凑的__TEXT段
2.内存优化
如果按照对象默认声明的属性顺序进行内存分配,在进行属性的8字节对齐时会浪费大量的内存空间,所以这里系统会把对象的属性重新排列,以此来最大化利用我们的内存空间——与二进制重排有着异曲同工之妙
3.sizeof、class_getInstanceSize、malloc_size
sizeof
:它是一个运算符,在编译时就可以获取类型所占内存的大小
class_getInstanceSize
:依赖于<objc/runtime.h>
,返回创建一个实例对象所需内存大小
malloc_size
:依赖于<malloc/malloc.h>
,返回系统实际分配的内存大小
关于class_getInstanceSize还能输出8字节
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
对于class_getInstanceSize为什么会返回8字节,这是一个易错题!!!! 在objc源码中搜索class_getInstanceSize,会发现它只做了字节对齐——8字节对齐,而alloc一文中讲过的至少为16字节代码不包含在class_getInstanceSize调用栈中——if (size < 16) size = 16;
4.内存对齐原则
对象的属性要内存对齐,而对象本身也需要进行内存对齐
- 数据成员对齐原则: 结构(struct)(或联合(union))的数据成员,第 一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要 从该成员大小或者成员的子成员大小
- 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从 其内部最大元素大小的整数倍地址开始存储
- 收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大 成员的整数倍,不足的要补⻬
5.举个栗子
struct struct1 {
char a;
double b;
int c;
short d;
} str1;
struct struct2 {
double b;
char a;
int c;
short d;
} str2;
struct struct3 {
double b;
int c;
char a;
short d;
} str3;
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"%lu——%lu——%lu", sizeof(str1), sizeof(str2), sizeof(str3));
}
return 0;
}
输出结果为24——24——16
已知(64位)char为1字节,double为8字节,int为4字节,short为2字节
内存对齐原则其实可以简单理解为min(m,n)
——m为当前开始的位置,n为所占位数。当m是n的整数倍时,条件满足;否则m位空余,m+1,继续min算法。
如str1
中的b
,一开始为min(1,8)
,不满足条件直至min(8,8)
,所以它在第8位坐下了,占据8个格子
如str2
中的c
,一开始为min(9,4)
,不满足条件直至min(12,4)
,所以它在第12位坐下了,占据4个格子
如str3
中的d
,一开始为min(13,2)
,不满足条件直至min(14,2)
,所以它在第14位坐下了,占据2个格子
三、malloc流程
关于内存开辟,还有一个历史遗留性问题——
alloc
在底层申请内存空间时调用了obj = (id)calloc(1, size)
。之前只有objc源码
我们无从下手,现在我们可以通过libmalloc源码
来一探究竟
1.calloc
在libmalloc源码
中新建target,按照objc源码
中的方式调用
void *p = calloc(1, 40);
2.malloc_zone_calloc
void *
calloc(size_t num_items, size_t size)
{
void *retval;
retval = malloc_zone_calloc(default_zone, num_items, size);
if (retval == NULL) {
errno = ENOMEM;
}
return retval;
}
根据return retval
可知retval
是核心内容,所以去看看malloc_zone_calloc
3 default_zone_calloc
void *
malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);
void *ptr;
if (malloc_check_start && (malloc_check_counter++ >= malloc_check_start)) {
internal_check();
}
ptr = zone->calloc(zone, num_items, size);
if (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);
return ptr;
}
根据return ptr
可知ptr
是重点,但是ptr = zone->calloc(zone, num_items, size);
跟进去会看到让人一串摸不到头脑的代码
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 */
3.1 方法一——分析zone
已知zone是malloc_zone_t
类型的,在第二步中retval = malloc_zone_calloc(default_zone, num_items, size);
中传递的第一个参数zone又是default_zone
,跟踪进去会发现它是一个静态变量
static malloc_zone_t *default_zone = &virtual_default_zone.malloc_zone;
static virtual_default_zone_t virtual_default_zone
__attribute__((section("__DATA,__v_zone")))
__attribute__((aligned(PAGE_MAX_SIZE))) = {
NULL,
NULL,
default_zone_size,
default_zone_malloc,
default_zone_calloc,
default_zone_valloc,
default_zone_free,
default_zone_realloc,
default_zone_destroy,
DEFAULT_MALLOC_ZONE_STRING,
default_zone_batch_malloc,
default_zone_batch_free,
&default_zone_introspect,
10,
default_zone_memalign,
default_zone_free_definite_size,
default_zone_pressure_relief,
default_zone_malloc_claimed_address,
};
初步推测zone->alloc
是default_zone_calloc
3.2 方法二——控制台打印
有时候打印也是阅读源码的一种方法——由打印可知实际调用default_zone_calloc
3.3 结论
只要思想不滑坡,方法总比困难多
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);
}
4.nano_malloc
好不容易从malloc_zone_calloc
找到了default_zone_calloc
,然后又是熟悉的味道——zone->calloc(zone, num_items, size)
继续打印试试
5._nano_malloc_check_clear
nano_malloc(nanozone_t *nanozone, size_t size)
{
if (size <= NANO_MAX_SIZE) {
void *p = _nano_malloc_check_clear(nanozone, size, 0);
if (p) {
return p;
} else {
/* FALLTHROUGH to helper zone */
}
}
malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone);
return zone->malloc(zone, size);
}
shift+command+O
来到nano_malloc
分析:这个方法中有两个return
和一句注释/* FALLTHROUGH to helper zone */——进入辅助区域
,即正常情况下走if判断(如果要开辟的空间小于 NANO_MAX_SIZE 则进行nanozone_t的malloc)NANO_MAX_SIZE=256
6.segregated_size_to_fit
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) {
...
} 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-else只走其一。再仔细想想,我们是带着目的来看源码的——malloc_size
中的48是怎么来的。这里有多个size_t类
,断点调试看了下的size
是我们传进来的40,而slot_bytes
刚好是我们的目标48,那我们就来看下40->48
是怎么来的
7. 16字节对齐
static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
// size = 40
size_t k, slot_bytes;
if (0 == size) {
size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
}
// 40 + 16-1 >> 4 << 4
// 40 - 16*3 = 48
//
// 16
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;
}
分析:size 是 40,在经过 (40 + 16 - 1) >> 4 << 4 操作后,结果为48,也就是16的整数倍——即16字节对齐
8.malloc总结
对象的属性是8字节对齐
对象是16字节对齐
- 因为内存是连续的,通过 16 字节对齐规避风险和容错,防止访问溢出
- 同时,也提高了寻址访问效率,也就是空间换时间
9.malloc部分流程图
写在后面
关于写文章,我喜欢先系统性的学一遍,将整体大纲写下来,但是有可能在后续学习中会有新的感悟,会不定时将文章更新,但文章主要知识点都是正确无误的