iOS 底层原理(2)--OC对象原理中

315 阅读8分钟

一 、对象初始化

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 是对allocinit的一个包装。

二、内存对齐

什么是内存对齐?

通俗的理解:针对不同的数据类型,在内存中占用特定的空间。

为什么要内存对齐?

1、安全性

市面上存在有多种不同架构的CPU,有的CPU读取特点类型的数据时,需要从特定的地址开始读取,如果不进行内存对齐,在CPU进行读取数据的时候就有可能出错。

2、效率

内存是以字节为基本单位,cpu在存取数据时,是以为单位存取,并不是以字节为单位存取。频繁存取未对齐的数据,会极大降低cpu的性能。字节对齐后,会减低cpu的存取次数。是典型的以空间换时间的做法。

内存对齐的规则

基本数据类型在不同的架构下,所占用的内存大小如下图所示:

image.png

结构体数据类型的内存对齐规则如下:

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的结果,.必须是其内部最⼤成员的整数倍.不⾜的要补⻬。

结构体内存对齐的探索

image.png

练习一下:

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>

LLDB文档.png

探索对象的成员变量内存占用情况:

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'

以上可以看到,ab 共同占用了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():是一个运算符,获取的是类型的大小(intsize_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需要到源码中查看:

image.png

上一节中对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字节,结果跟预想的一样。

当然上面只是猜想和测试结果,那么它内部究竟是如何实现的呢?

断点跟踪进入 :

image.png

Step into ->calloc

image.png

Step into -> _malloc_zone_calloc

Screen Shot 2021-07-20 at 11.57.41 AM.png

Step into -> zone->calloc

image.png

Step into -> nano_calloc

image.png

Step into -> 跟size相关的内容 _nano_malloc_check_clear

image.png

进入 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

参考链接:

LLVM: www.jianshu.com/p/037fb5002…