iOS底层原理 02:OC对象初始化之内存探索

1,537 阅读10分钟

物来则应,物去不留 ~

发现问题:

  1. objc 无法编译? 调试问题?
  2. alloc -> objc_alloc ?
  3. instanceSize 大小计算?8 + 8 +4 -> 32的大小, 为啥不是24 ?
  4. x/4gx ?
  5. [LGPerson alloc]源码编译,为啥先跑objc_alloc(Class cls){...}函数,而不直接是+ (id)alloc {...}呢???

一、Objc 编译问题

1. 编译设置选择KCObjcBuild

截屏2021-08-30 下午10.25.34.png

2. 编译失败,不能通过

①: Build Settings -> enable hardened runtime -> NO

截屏2021-08-30 下午10.34.57.png

②: Build phase -> denpendenice -> objc

截屏2021-08-30 下午10.36.08.png

3. M1电脑错误解决

错误位置

截屏2021-08-30 下午10.39.40.png

解决方式

  • 注释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 处标记符号断点

截屏2021-08-31 下午10.06.59.png

2. 查看汇编代码

截屏2021-08-31 下午10.13.27.png

通过汇编代码可查看下一步实际调用的是 objc_alloc 函数,而查看objc源码点击下一步调用的是alloc函数,为什么呢?

3. 分析 alloc 函数调用顺序

截屏2021-08-31 下午10.35.50.png

3.1 分析结果

通过源码快速查找,在objc-runtime-new.mm文件中,找到在fixupMessageRef函数里面修改了allocimp,往前推理,查看是在_read_images (类的加载映射当前文件) 里面调用的 fixupMessageRef函数,通过调用函数的注释说明,_read_images调用fixupMessageRef是用来修复消息发送信息

通过 _read_images继续往前推理,可得出如下调用顺序:

  • fixupMessageRef <- _read_images <- _objc_init_image
  • fixupMessageRef <- _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 方法

    截屏2021-08-31 下午11.48.55.png

    通过注释可得出,当前函数返回为true是,某些选择器调用相应的入口点:

    alloc => objc_alloc
    allocWithZone:nil => objc_allocWithZone

  • 搜索关键词 OMF_alloc找到文件后可得出,满足条件后,[Foo alloc] -> objc_alloc(Foo)

    截屏2021-09-01 上午12.12.54.png

  • 为啥能够走到 EmitObjCAlloc?在编译阶段对alloc做了hook,初次创建满足条件,就会执行 tryGenerateSpecializedMessageSend 发送消息机制函数,回到了上一步的 OMF_alloc条件,执行 [Foo alloc] -> objc_alloc(Foo) 操作,进行标记,执行 objc_alloc 后进入到callAlloc,通过调用objc_msgSendLGPersion发送消息,底层llvm收到消息后不会在执行tryGenerateSpecializedMessageSend 发送,而执行 GenerateMessageSend普通的消息发送,执行正常的流程 [LGPerson alloc] -> alloc

    截屏2021-09-01 上午12.25.50.png

  • 流程图分析

截屏2021-09-01 上午12.57.48.png

三、对象内存影响的因素

结合案列分析,有个LGPerson类,其中有2个属性,分别是(NSString *)name(NSString *)nickName ,通过class_getInstanceSize(LGPerson.class)获取实际的内存大小。

class_getInstanceSize :对象实际的内存大小,内存大小是由类的成员变量的大小决定。

截屏2021-09-19 下午5.42.15.png

24的原因呢?

LGPerson继承NSObject,有个8字节的isa也需要占用空间。所以至少需要的内存空间是8 + 8 + 8 = 24,正好是8字节对齐。

继续添加字段

(int age、double height、char c1、char c2),打印如下:

截屏2021-09-19 下午5.58.10.png

打印结果: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


结果分析

属性、成员变量都很会影响当前对象的内存大小;方法不会影响,方法不存在当前对象内存中。

  • 内存结构分析:

截屏2021-09-19 下午6.33.54.png

总结:

age、c1、c2 系统进行了 内存优化,共占用 8 个字节
浮点数输出:p/f``e -f f -- 0x0xxxxxx

内存指针打印方式:

LLDB文档.png

四、结构内存对齐

  • 前面发现对象实际的内存大小是8字节对齐,那么到底是怎么对齐的。引出重点内存对齐。

    coc32位64位
    boolBOOL(64位)11
    signed char( _ _signed char)int8_t、BOOL(32位)11
    unsigned charBoolean11
    shortint16_t22
    unsigned shortunichar22
    int int32_tNSInteger(32位) 、boolean_t(32位)44
    unsinged intboolean_t(64位) 、NSUInteger(32位)44
    longNSInteger(64位)48
    unsigned longNSUInteger(64位)48
    long longint64_t88
    floatCGFloat(32位)44
    doubleCGFloat(64位)88

内存对齐原因:

内存是以字节为基本单位,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;
    

    打印结果:

    截屏2021-09-20 下午4.02.53.png

    sizeof 是一个关键字,它是一个编译时运算符,用于判断变量或数据类型的字节大小。
    sizeof 运算符可用于获取类、结构、共用体和其他用户自定义数据类型的大小。

    结果发现,struct1struct2struct21 中包含的属性变量是一样的,只是顺序不一样,所引起的内存大小也不一致。Why? -> 结构体内存对齐.

    以下根据结构体内存对齐做出分析:

    1、struct1

    变量占用字节offsetmin位置
    double a80min(0, 8)0 ~ 7
    char b18min(8, 1)8
    int c412min(12, 4)12 ~ 15
    short d216min(16, 2)16~17

    分析结果发现,struct1中的如果只是以字段算占用字节数为15,但根据数据成员对齐规则,第一个从0开始,以后每个子成员的起始位置需该子成员大小的整数倍开始,这一步得出,struct1的占用字节大小为 17。但是struct1中最大变量a8字节,struct1的实际内存大小必须是8的整数倍,17不会8的倍数,向上取整,得出struct1最终内存大小为24

    截屏2021-09-20 下午3.39.36.png

    2、struct2

    变量占用字节offsetmin位置
    double a80min(0, 8)0 ~ 7
    int b48min(8, 4)8 ~ 11
    char c112min(12, 1)12
    short d214min(14, 2)14~15

    分析结果发现,struct2中的如果只是以字段算占用字节数为15,但根据数据成员对齐规则,第一个从0开始,以后每个子成员的起始位置需该子成员大小的整数倍开始,这一步得出,struct2的占用字节大小为 15。但是struct2中最大变量a8字节,struct2的实际内存大小必须是8的整数倍,15不会8的倍数,向上取整,得出struct2最终内存大小为16

    截屏2021-09-20 下午3.40.25.png

    3、LGStruct21

    变量占用字节offsetmin位置
    int a40min(0, 4)0 ~ 3
    double b88min(8, 8)8 ~ 15
    char c116min(16, 1)16
    short d218min(18, 2)18~19

    分析结果发现,struct21中的如果只是以字段算占用字节数为15,但根据数据成员对齐规则,第一个从0开始,以后每个子成员的起始位置需该子成员大小的整数倍开始,这一步得出,struct21的占用字节大小为 19。但是struct21中最大变量a8字节,struct21的实际内存大小必须是8的整数倍,19不会8的倍数,向上取整,得出struct21最终内存大小为24

    截屏2021-09-20 下午3.41.56.png

  • 结构嵌套案列分析

    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;
    
  • 打印结果:

    截屏2021-09-20 下午4.07.29.png

    • struct3分析结果如下

      变量占用字节offsetmin位置
      double a88min(0, 8)0 ~ 7
      int b48min(8, 4)8 ~ 11
      char c112min(12, 1)12
      short d214min(14, 2)14~15
      int e416min(16, 4)16~19
      str.a(double)824min(24, 8)24~31
      str.b(char)132min(32, 1)32
      str.c(int)436min(36, 4)36~39
      str.d(short)240min(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
    

    截屏2021-09-20 下午5.48.50.png

    1. sizeof 是一个运算符,获取的是类型的大小(int、size_t、结构体、指针变量等),程序编译时获取。person结构体指针,所以是8字节

    2. class_getInstanceSize 是一个函数,程序运行时才获取,创建的对象加所有实例变量实际占用的内存大小,内存对齐一般是以8字节对齐

      • LGPerson 成员大小为288字节对齐,成员大小32,在加上isa的大小, 最终的大小为40
    3. malloc_size :在堆中开辟的大小,向系统申请的空间大小 在Mac、iOS中的malloc函数分配的内存大小总是16的倍数,也就是16进制对齐

六、malloc_sizelibmalloc 源码分析

  1. 打开libmalloc源码代码,执行以下部分代码 void *p = calloc(1, 40); NSLog(@"%lu",malloc_size(p)); NSLog(@"Hello, World!");

    截屏2021-09-20 下午6.44.32.png

     结果打印为`48` ,为什么呢?
    
  2. 追踪代码进入到_malloc_zone_calloc函数,根据返回值ptr查找关键部分代码,如下:

    截屏2021-09-20 下午6.48.38.png

  3. 断点调试在ptr = zone->calloc(zone, num_items, size); 这行代码,po打印zone->calloc函数(有赋值 就会有 存储值 打印),就会终端打印出下一个执行函数default_zone_calloc

    截屏2021-09-20 下午8.17.10.png

  4. 全局搜索default_zone_calloc函数,继续断点调试,po打印zone->calloc,出现nano_calloc函数

    截屏2021-09-20 下午8.22.23.png

  5. 继续搜索nano_calloc函数,进入到 nano_calloc函数,到当前的方法中,定位 _nano_malloc_check_clear 处代码。

    截屏2021-09-20 下午8.27.04.png

  6. 进入到_nano_malloc_check_clear函数,分析其中的代码执行流程.

    截屏2021-09-20 下午8.37.50.png

所关心的大小设置,锁定于第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=40NANO_REGIME_QUANTA_SIZE=16,16进制对齐
k = (40 + 16 - 1) >> 4;先左移4位,得出k=56
之后再右移动4位, slot_bytes=48
刚好16进制对齐!