一 、对象初始化
init方法
直接源码分析:
LGPerson *p = [[LGPerson alloc]init] ;
Step into --> - (id)init
- (id)init {
return _objc_rootInit(self);
}
Step into --> id _objc_rootInit(id obj)
id _objc_rootInit(id obj)
{
// In practice, it will be hard to rely on this function.
// Many classes do not properly chain -init calls.
return obj;
}
从上面可以看出来,init 只是一个构造函数,中间不进行任何操作。
new方法
LGPerson *p = [LGPerson new] ;
Step into --> +(id)new
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
从这里可以看出来new 是对alloc 和 init的一个包装。
二、内存对齐
什么是内存对齐?
通俗的理解:针对不同的数据类型,在内存中占用特定的空间。
为什么要内存对齐?
1、安全性
市面上存在有多种不同架构的CPU,有的CPU读取特点类型的数据时,需要从特定的地址开始读取,如果不进行内存对齐,在CPU进行读取数据的时候就有可能出错。
2、效率
内存是以字节为基本单位,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 LGStruct1 {
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; //str的大小为24 (20 .. [24 ..47] 48
}struct3;
结构体的成员变量在结构体中的顺序不一样,也会影响结构体占用的内存大小。
三、系统内存优化
LLDB调试命令:x/nuf <addr>
探索对象的成员变量内存占用情况:
LGPerson *person = [LGPerson alloc];
person.height = 168.0;
person.name = @"fuck";
person.age = 100;
person.c1 = 'a';
person.c2 = 'b';
NSLog(@"----%lu",class_getInstanceSize([person class]));
打印内存占用结果:
//找到person实例的内存地址
(lldb) po person
<LGPerson: 0x600003a1c060>
//显示person对象的内存明细
(lldb) x/6gx 0x600003a1c060
0x600003a1c060: 0x0000000108c46890 0x0000006400006261
0x600003a1c070: 0x0000000108c41018 0x0000000000000000
0x600003a1c080: 0x0000000000000000 0x4065000000000000
//打印第一个地址内的值,也就是isa的值
(lldb) po 0x0000000108c46890
LGPerson
//第二个地址内存储了多个变量的值0x0000006400006261
(lldb) po 0x61
97 //'a'
(lldb) po 0x62
98 //'b'
(lldb) po 0x64
100 //'age'
//第三个地址的值
(lldb) po 0x0000000108c41018
fuck //'name'
//打印浮点型的值,需特殊处理 e/f
(lldb) e/f 0x4065000000000000
(long) $6 = 168 //'height'
以上可以看到,a 和 b 共同占用了8个字节的内存。这就是系统内部做了优化。
LLVM优化alloc
callalloc 为什么走两次的原因??
下面的内容未探究清楚,暂时不看,待补充
全局搜索代码:
搜索该方法所在的文件:read_images
```
// Fix up @selector references
static size_t UnfixedSelectors;
{
mutex_locker_t lock(selLock);
for (EACH_HEADER) {
if (hi->hasPreoptimizedSelectors()) continue;
bool isBundle = hi->isBundle();
SEL *sels = _getObjc2SelectorRefs(hi, &count);
UnfixedSelectors += count;
for (i = 0; i < count; i++) {
const char *name = sel_cname(sels[i]);
SEL sel = sel_registerNameNoLock(name, isBundle);
if (sels[i] != sel) {
sels[i] = sel;
}
}
}
}
```
```
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 的映射关系在read_image之前,`UnfixedSelectors`这里是修复,意味着在更前面才是绑定,更前面是llvm的编译阶段。
探索llvm的过程暂时省略:仅保留以下几个节点
```
llvm-project
vscode 看代码比较快
定位object_alloc
快速定位OMF_alloc
搜索:generateposs
```
分析:
alloc llvm 底层做了拦截
alloc -> objc_alloc -标记receiver (判断进不去)
objc_alloc -> objc_msgSend
alloc{} ->
下载最新版的 llvm-project
四、对象内存的影响因素
对象包括成员变量、属性、方法,我们依次来探索:
首先创建一个类
@interface LGPerson : NSObject
@end
@implementation LGPerson
@end
查看当前内存占用情况
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *p = [LGPerson alloc] ;
NSLog(@"%ld",class_getInstanceSize(LGPerson.class));
}
return 0;
}
打印内容:
2021-07-19 17:20:20.862992+0800 ObjcBuild[5434:229825] 8
添加属性
@property (nonatomic, copy) NSString *name;
@property (nonatomic) int age;
LGPerson *p = [LGPerson alloc] ;
p.name = @"大神";
p.age = 32;
NSLog(@"%ld",class_getInstanceSize(LGPerson.class));
查看打印结果:
2021-07-19 17:25:06.377168+0800 ObjcBuild[5469:232786] 24
添加成员变量 height
@interface LGPerson : NSObject
{
CGFloat _height;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic) int age;
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *p = [LGPerson alloc] ;
p.name = @"大神";
p.age = 10;
NSLog(@"%ld",class_getInstanceSize(LGPerson.class));
}
return 0;
}
打印:
2021-07-19 17:33:24.604171+0800 ObjcBuild[5582:238911] 32
添加方法
- (void)saySomething;
- (void)saySomething{
NSLog(@"%s",__func__);
}
打印:
2021-07-19 17:36:21.257952+0800 KCObjcBuild[5613:240839] 32
结论
属性和成员变量可以影响对象内存大小,方法不影响,而
属性 = 成员变量 + setter + getter方法,因此真正影响对象内存大小的因素是 成员变量。
五、malloc源码引入
实例测试
先来了解以下三个方法:
sizeof():是一个运算符,获取的是类型的大小(int、size_t、结构体、指针变量等),程序编译时获取,单位是字节;
size_t class_getInstanceSize(Class cls) :是一个函数,程序运行时才获取,创建的对象加所有实例变量实际占用的内存大小,内存对齐一般[8]字节对齐,#import <objc/runtime.h>;
malloc_size():在堆中开辟的大小,向系统申请的空间大小,在Mac、iOS中的malloc函数分配的内存大小总是[16]的倍数#import <malloc/malloc.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *p = [LGPerson alloc] ;
p.name = @"大神";
p.age = 10;
NSLog(@"p:%@",p);
NSLog(@"sizeof:%lu",sizeof(p));//p是对象的地址,结构体指针占8个字节
NSLog(@"class_getInstanceSize:%ld",class_getInstanceSize(LGPerson.class));//(isa)8+(NSString)8+(int)4 = 20 ,按8字节对齐 24 字节。
NSLog(@"malloc_size:%zu", malloc_size((__bridge const void *)(p)));//24字节向系统申请,遵循16字节对齐,所以分配32字节。
NSLog(@"%@",p);
}
return 0;
}
打印:
2021-07-19 18:17:05.485905+0800 ObjcBuild[5805:260501] p:<LGPerson: 0x1012c08d0>
2021-07-19 18:17:05.486388+0800 ObjcBuild[5805:260501] sizeof:8
2021-07-19 18:17:05.486491+0800 ObjcBuild[5805:260501] class_getInstanceSize:24
2021-07-19 18:17:05.486562+0800 ObjcBuild[5805:260501] malloc_size:32
位置: usr/include/malloc
malloc分析探索思路
准备:malloc 源码
点击malloc_size(),进入定义,发现是extern需要到源码中查看:
上一节中对alloc进行了深入分析,其中分配空间的核心方法是calloc,计算内存的方法是instacnceSize,那么下面来做个测试:
#import <Foundation/Foundation.h>
#import <malloc/malloc.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
void *p = calloc(1, 40);
NSLog(@"%lu",malloc_size(p));
}
return 0;
}
打印:
2021-07-20 10:52:36.288801+0800 MallocBuild[1557:52383] 48
我们向系统申请40个字节的空间,而malloc分配空间是按【16】字节对齐的,因此是最后分配的是48字节,结果跟预想的一样。
当然上面只是猜想和测试结果,那么它内部究竟是如何实现的呢?
断点跟踪进入 :
Step into ->calloc
Step into -> _malloc_zone_calloc
Step into -> zone->calloc
Step into -> nano_calloc
Step into -> 跟size相关的内容 _nano_malloc_check_clear
进入 segregated_size_to_fit,这里有 16字节对齐算法,也就是 系统实际开辟内存空间的大小 。其中NANO_REGIME_QUANTA_SIZE = 16,SHIFT_NANO_QUANTUM = 4,所以k = (24 + 16 - 15) >> 4;之后再左移4位,16字节对齐
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;
}
对象内存对齐原理和总结
使用 alloc 创建对象,分为三步
第一步:通过instanceSize方法计算出 需要申请的内存空间(默认已经16字节对齐);
第二步(本节内容重点):通过calloc对上一步的结果进行16字节对齐处理,然后系统分配内存空间,并返回指向该内存地址的指针;
第三步: 初始化ISA并关联cls: obj->initInstanceIsa
参考链接: