物来则应,物去不留 ~
发现问题:
objc无法编译? 调试问题?alloc -> objc_alloc?instanceSize大小计算?8 + 8 +4 -> 32的大小, 为啥不是24 ?x/4gx?[LGPerson alloc]源码编译,为啥先跑objc_alloc(Class cls){...}函数,而不直接是+ (id)alloc {...}呢???
一、Objc 编译问题
1. 编译设置选择KCObjcBuild
2. 编译失败,不能通过
①: Build Settings -> enable hardened runtime -> NO
②: Build phase -> denpendenice -> objc
3. M1电脑错误解决
错误位置
解决方式
- 注释
objc-cache.mm文件中的void cache_t::init()函数中的部分代码1067、1068行号如下:
_objc_fatal("task_restartable_ranges_register failed (result 0x%x: %s)",
kr, mach_error_string(kr));
- 注释
objc-runtime-new.mm文件中的第176、177行号代码块
STATIC_ASSERT((~ISA_MASK & MACH_VM_MAX_ADDRESS) == 0 ||
ISA_MASK + sizeof(void*) == MACH_VM_MAX_ADDRESS);
二、LLVM 优化 alloc 优化
1. alloc 处标记符号断点
2. 查看汇编代码
通过汇编代码可查看下一步实际调用的是 objc_alloc 函数,而查看objc源码点击下一步调用的是alloc函数,为什么呢?
3. 分析 alloc 函数调用顺序
3.1 分析结果
通过源码快速查找,在objc-runtime-new.mm文件中,找到在fixupMessageRef函数里面修改了alloc的imp,往前推理,查看是在_read_images (类的加载映射当前文件) 里面调用的 fixupMessageRef函数,通过调用函数的注释说明,_read_images调用fixupMessageRef是用来修复消息发送信息
通过 _read_images继续往前推理,可得出如下调用顺序:
fixupMessageRef<-_read_images<-_objc_init_imagefixupMessageRef<-_read_images<-map_images_nolock<-map_images<-_dyld_objc_notify_register<-_objc_init
_objc_init的调用是dyld 加载应用程序是执行的,所以在这个更早之前就编译阶段需要hook
3.2 通过 llvm 验证alloc的流程
-
在 llvm-project 项目中搜索
objc_alloc方法通过注释可得出,当前函数返回为true是,某些选择器调用相应的入口点:
alloc => objc_alloc
allocWithZone:nil => objc_allocWithZone -
搜索关键词
OMF_alloc找到文件后可得出,满足条件后,[Foo alloc] -> objc_alloc(Foo) -
为啥能够走到
EmitObjCAlloc?在编译阶段对alloc做了hook,初次创建满足条件,就会执行tryGenerateSpecializedMessageSend发送消息机制函数,回到了上一步的OMF_alloc条件,执行[Foo alloc] -> objc_alloc(Foo)操作,进行标记,执行objc_alloc后进入到callAlloc,通过调用objc_msgSend往LGPersion发送消息,底层llvm收到消息后不会在执行tryGenerateSpecializedMessageSend发送,而执行GenerateMessageSend普通的消息发送,执行正常的流程[LGPerson alloc] -> alloc -
流程图分析
三、对象内存影响的因素
结合案列分析,有个LGPerson类,其中有2个属性,分别是(NSString *)name和(NSString *)nickName ,通过class_getInstanceSize(LGPerson.class)获取实际的内存大小。
class_getInstanceSize:对象实际的内存大小,内存大小是由类的成员变量的大小决定。
24的原因呢?
LGPerson继承NSObject,有个8字节的isa也需要占用空间。所以至少需要的内存空间是8 + 8 + 8 = 24,正好是8字节对齐。
继续添加字段
(int age、double height、char c1、char c2),打印如下:
打印结果:40 原因 ?
(isa)8 + (NSString *name)8 + (NSString *nickName)8 + (int age)4 + (double height)8 + (char c1)1 + (char c2)1 = 38; 8字节对齐,所以输出结果为40
对象中添加一个成员变量,继续打印
@interface LGPerson : NSObject {
NSString *nickName
}
@property (nonatomic, copy) NSString *name;
@end
打印结果:24; 8 + 8 + 8 = 24
对象中继续添加一个方法,继续打印
@interface LGPerson : NSObject {
NSString *nickName
}
@property (nonatomic, copy) NSString *name;
+ (void)sayNB;
@end
@implementation LGPerson
+ (void)sayNB{
}
@end
打印结果:24
结果分析
属性、成员变量都很会影响当前对象的内存大小;方法不会影响,方法不存在当前对象内存中。
- 内存结构分析:
总结:
age、c1、c2 系统进行了 内存优化,共占用 8 个字节
浮点数输出:p/f``e -f f -- 0x0xxxxxx
内存指针打印方式:
四、结构内存对齐
-
前面发现对象实际的内存大小是
8字节对齐,那么到底是怎么对齐的。引出重点内存对齐。c oc 32位 64位 bool BOOL(64位) 1 1 signed char ( _ _signed char)int8_t、BOOL(32位) 1 1 unsigned char Boolean 1 1 short int16_t 2 2 unsigned short unichar 2 2 int int32_t NSInteger(32位) 、boolean_t(32位) 4 4 unsinged int boolean_t(64位) 、NSUInteger(32位) 4 4 long NSInteger(64位) 4 8 unsigned long NSUInteger(64位) 4 8 long long int64_t 8 8 float CGFloat(32位) 4 4 double CGFloat(64位) 8 8
内存对齐原因:
内存是以字节为基本单位,
cpu在存取数据时,是以块为单位存取,并不是以字节为单位存取。频繁存取未对齐的数据,会极大降低cpu的性能。字节对齐后,会减低cpu的存取次数,这种以空间换时间的做法目的降低cpu的开销。
内存对齐原则
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的结果,.必须是其内部最大 成员的整数倍.不足的要补⻬。
案列分析
-
普通struct
-
普通struct属性交换顺序
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 LGStruct21 { int b; // 4 [0 1 2 3 4] double a; // 8 (5 6 7) [8 9 10 11 12 13 14 15] char c; // 1 [16] short d; // 2 (17) [18 19] 24 }struct21;打印结果:
sizeof是一个关键字,它是一个编译时运算符,用于判断变量或数据类型的字节大小。
sizeof运算符可用于获取类、结构、共用体和其他用户自定义数据类型的大小。结果发现,
struct1、struct2、struct21中包含的属性变量是一样的,只是顺序不一样,所引起的内存大小也不一致。Why? -> 结构体内存对齐.以下根据结构体内存对齐做出分析:
1、
struct1变量 占用字节 offset min 位置 double a 8 0 min(0, 8) 0 ~ 7 char b 1 8 min(8, 1) 8 int c 4 12 min(12, 4) 12 ~ 15 short d 2 16 min(16, 2) 16~17 分析结果发现,
struct1中的如果只是以字段算占用字节数为15,但根据数据成员对齐规则,第一个从0开始,以后每个子成员的起始位置需该子成员大小的整数倍开始,这一步得出,struct1的占用字节大小为17。但是struct1中最大变量a占8字节,struct1的实际内存大小必须是8的整数倍,17不会8的倍数,向上取整,得出struct1最终内存大小为242、
struct2变量 占用字节 offset min 位置 double a 8 0 min(0, 8) 0 ~ 7 int b 4 8 min(8, 4) 8 ~ 11 char c 1 12 min(12, 1) 12 short d 2 14 min(14, 2) 14~15 分析结果发现,
struct2中的如果只是以字段算占用字节数为15,但根据数据成员对齐规则,第一个从0开始,以后每个子成员的起始位置需该子成员大小的整数倍开始,这一步得出,struct2的占用字节大小为15。但是struct2中最大变量a占8字节,struct2的实际内存大小必须是8的整数倍,15不会8的倍数,向上取整,得出struct2最终内存大小为163、
LGStruct21变量 占用字节 offset min 位置 int a 4 0 min(0, 4) 0 ~ 3 double b 8 8 min(8, 8) 8 ~ 15 char c 1 16 min(16, 1) 16 short d 2 18 min(18, 2) 18~19 分析结果发现,
struct21中的如果只是以字段算占用字节数为15,但根据数据成员对齐规则,第一个从0开始,以后每个子成员的起始位置需该子成员大小的整数倍开始,这一步得出,struct21的占用字节大小为19。但是struct21中最大变量a占8字节,struct21的实际内存大小必须是8的整数倍,19不会8的倍数,向上取整,得出struct21最终内存大小为24。 -
结构嵌套案列分析
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 LGStruct3 { double a; // 8 [0 7] int b; // 4 [8 9 10 11] char c; // 1 [12] short d; // 2 (13) [14 15] int e; // 4 [16 17 18 19] struct LGStruct1 str; // 24 [24 47] }struct3; -
打印结果:
-
struct3分析结果如下变量 占用字节 offset min 位置 double a 8 8 min(0, 8) 0 ~ 7 int b 4 8 min(8, 4) 8 ~ 11 char c 1 12 min(12, 1) 12 short d 2 14 min(14, 2) 14~15 int e 4 16 min(16, 4) 16~19 str.a(double) 8 24 min(24, 8) 24~31 str.b(char) 1 32 min(32, 1) 32 str.c(int) 4 36 min(36, 4) 36~39 str.d(short) 2 40 min(40, 2) 40~41 -
struct LGStruct1 str, 结合结构体做完成员的规则,结构体成员要从 其内部最大元素大小的整数倍地址开始存储,
str里面最大的成员是double a, 那么str应该从8的整数倍开始,上个字段记录到19,往上取整,从24开始。
-
-
结合内部最大成员
8的整数倍, 不足的要补⻬,struct3的最终大小为48
-
五、malloc源码引入
-
案列分析
@interface LGPerson : NSObject @property (nonatomic, copy) NSString *name; @property (nonatomic, copy) NSString *nickName; @property (nonatomic, assign) int age; @property (nonatomic, assign) long height; @end-
sizeof是一个运算符,获取的是类型的大小(int、size_t、结构体、指针变量等),程序编译时获取。person是结构体指针,所以是8字节 -
class_getInstanceSize是一个函数,程序运行时才获取,创建的对象加所有实例变量实际占用的内存大小,内存对齐一般是以8字节对齐- LGPerson 成员大小为
28,8字节对齐,成员大小32,在加上isa的大小, 最终的大小为40
- LGPerson 成员大小为
-
malloc_size:在堆中开辟的大小,向系统申请的空间大小 在Mac、iOS中的malloc函数分配的内存大小总是16的倍数,也就是16进制对齐
-
六、malloc_size之libmalloc 源码分析
-
打开
libmalloc源码代码,执行以下部分代码void *p = calloc(1, 40); NSLog(@"%lu",malloc_size(p)); NSLog(@"Hello, World!");结果打印为`48` ,为什么呢? -
追踪代码进入到
_malloc_zone_calloc函数,根据返回值ptr查找关键部分代码,如下: -
断点调试在
ptr = zone->calloc(zone, num_items, size);这行代码,po打印zone->calloc函数(有赋值 就会有 存储值 打印),就会终端打印出下一个执行函数default_zone_calloc -
全局搜索
default_zone_calloc函数,继续断点调试,po打印zone->calloc,出现nano_calloc函数 -
继续搜索
nano_calloc函数,进入到nano_calloc函数,到当前的方法中,定位_nano_malloc_check_clear处代码。 -
进入到
_nano_malloc_check_clear函数,分析其中的代码执行流程.
所关心的大小设置,锁定于第621行处关键代,进入其中代码
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;
}
#define SHIFT_NANO_QUANTUM 4
#define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM) // 16
计算流程:
其中size=40,NANO_REGIME_QUANTA_SIZE=16,16进制对齐
k = (40 + 16 - 1) >> 4;先左移4位,得出k=56
之后再右移动4位, slot_bytes=48
刚好16进制对齐!