iOS底层学习——OC对象初始化补充以及内存对齐探索

简介: OC对象初始化补充以及内存对齐探索

大拿之路,任重道远,永不言弃!!!

上一篇文章对象初始化探索中,学习了OC对象的初始化流程,以及对象内存按照16字节对齐方式进行开辟过程。但是还留下了一些问题没有搞明白,比如:

  • LGPerson * person = [[LGPerson alloc] init];-init()方法的作用是什么呢?
  • LGPerson * newlg = [LGPerson new];+new()方法的作用是什么呢?
  • 对象初始化过程中调用alloc为什么会走到objc_alloc呢?
  • 系统会按照16字节对齐方式开辟内存,那么影响内存大小的因素是什么呢?

一.OC对象初始化补充

先解决上一篇对象初始化中留下的问题:-init()+new()作用是什么?直接看源码吗?肯定不合适。要想分析他们的功能,先要知道他们运行流程是什么,我们采用汇编+下符号断点的方式进行探索!

1.-init()分析

LGPerson * person = [[LGPerson alloc] init];这行代码中添加断点,并设置:Debug -> Debug Workflow -> Always Show Disassembly。运行程序,结果如下图:

image.png

发现该过程实际调用了objc_alloc_init方法,添加符号断点objc_alloc_init,查看运行情况,见下图:

image.png

objc_alloc_init方法依然来自libobjc.A.dylib库,同时[[LGPerson alloc] init];过程是调用了callAlloc方法创建对象之后,向该对象发送init消息。这里再添加init符号断点,继续运行程序:

image.png

因为LGPerson没有实现-init()方法,所以通过方法查找,最终会找到NSObject的init方法,运行-[NSObject init];流程,最终会调用到_objc_rootInit方法

image.png

结合源码,设置断点进行调试跟踪,流程和我们在汇编+符号断点的流程是一致的,如下图所示!最终返回的内容是callAlloc创建的对象自身。

image.png

总结流程:说明init方法只是一个构造方法,并不涉及到对象的初始化创建。

image.png

2.+new()分析

探索方式和-init()一样,这里不再进行汇编+下符号断点的流程演示,直接上源码。LGPerson * newlg = [LGPerson new];并不会直接调用+new(),而是执行了objc_opt_new方法,见下图:

image.png

这里有类似callAlloc的判断流程,如果初次初始化,hasCustomCore()会返回true,见下图,这样会进入发送消息流程,即发送new消息

image.png

因为LGPerson没有实现new类方法,所以在进行方法查找过程中会找到NSObject中,最终执行+[NSObject new];方法。见下图所示:

image.png

继续执行程序,会走到_objc_rootAllocWithZone中进行对象初始化。

image.png

总结流程: +new()方法是一个类方法,内部是对[[cls alloc] init];流程的封装。

image.png

虽然+new()是对[[cls alloc] init];的封装,但是依然建议使用[[cls alloc] init];进行对象的初始化!因为init作为构造器,可以自定义提供自己所需要的初始化方法。

二.LLVM优化alloc

**
理解的还不够透彻,写下来也是疑点重重!
待补充……
**

三.对象内存的影响因素

上一篇文章我们已经知道,系统会以16字节对齐的形式开辟对象内存空间的,同时对象的实际占用内存的大小采用的是8字节对齐算法

1.内存对齐说明

内存对齐可以理解为结构体中的成员申请内存大小时,系统最小分配的内存为8字节,是按每次8字节来申请的,不够8字节的也会申请8字节,然后按结构体中成员变量顺序再次申请,直到所有成员变量都能放下为止。

当结构体成员内存小的在前面时,会因为内存对齐的原因比较浪费内存,为了解决这个问题苹果中采用空间换时间,将类中的属性进行重排,以此来达到内存优化的目的。

2.内存影响因素案例说明

那么对象内存的影响因素有哪些呢?结合案例进行分析!这里有一个GFPerson类,类中有两个属性(NSString *) - lgName、nikeName,通过class_getInstanceSize(GFPerson.class)获取创建的对象【至少】需要的内存大小。如下图所示:

image.png

  • 为什么是24呢?因为GFPerson继承NSObject,还有一个8字节的isa也需要占用空间。所以至少需要的内存空间是8 + 8 + 8 = 24,正好是8字节对齐
  • 调整一下再添加一个int - 属性age,此时内容共占用为32!因为8 + 8 + 8 + 4 = 28,8字节对齐算法,所以占用内存为32
  • 再次调整,添加两个方法,一个对象方法,一个类方法,运行发现此时已然是32。说明方法不会影响内存的占用
  • 添加一个成员变量double - height;,运行结果为40,8 + 8 + 8 + 4 + 8 = 36,8字节对齐,所以输出结果为40

总结一下:

从以上的案例中可以得出,影响对象大小的因素是属性和成员变量,方法不会影响!同时只要有这个属性就会占用这份空间,即使不初始化也会占用!

3.内存结构解析

结合上面的例子,GFPerson提供以下成员变量和属性:height(double)、lgName(NSString *)、nickName(NSString *)、age(int)、score(double)、ch1(char)、ch2(char)、ch3(char)。其中height未被初始化,运行程序,打印内存结构,以及对象占用空间大小。见下图所示:

image.png

总结说明:

  1. 因为对象占用48个字节,所以前6个8字节的数据时gf1的存储内容,从0x100b450e0地址开始就不是gf1的数据内容了!
  2. age、ch1、cha2、ch3系统进行了内存优化共占8个字节
  3. 浮点数输出是需要特殊处理一下采用p/f,或者e -f f --

四.结构体内存对齐

在对象内存分析的基础上做个引申,我们来探讨一下结构体的内存对齐方式。

1.结构体内存对齐的原则

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

2.案例分析结构体内存对齐

结合下面的案例进行分析,两个结构体Struct1和Struct2,属性类型都一样,只是charint的顺序不一样的,根据运算符sizeof获取的两个结构体的大小一样吗?

image.png

很意外,不一样,为什么呢?结合上面的结构体内存对齐的原则来分析

1. 结构体-Struct1

  • double a: 第一个成员,8字节,从0开始,占用位置:0-7
  • char b: 1个字节,从8开始,也是1的整数倍,占用位置:8
  • int c: 4个字节,从9开始?不对,从12开始,占用位置:12 13 14 15
  • short d: 2个字节,从16开始,也是2的整数倍,占用位置:16 17

结合8字节对齐原则,最终大小24。

2. 结构体-Struct2

  • double a: 第一个成员,8字节,从0开始,占用位置:0-7
  • int b: 4个字节,从8开始,也是4的整数倍,占用位置:8 9 10 11
  • char c: 1个字节,从12开始,占用位置:12
  • short d: 2个字节,从13开始?不对,从14开始,占用位置:14 15

结合8字节对齐原则,最终大小16。

那么我们在开发中是否需要注意类的参数的顺序呢,因为类也是结构体,类是不是也有这样情况呢?不需要!因为类是OC对结构体的一层封装,做了一些内存优化,比如我们上面提到的内存重排。而结构体有点傻^_^!

附一张C、Objc-C内存占用表:

image.png

3.结构体嵌套案例解析

下面两个结构体,Struct1、Struct3,其中Struct3中嵌套了一个Struct1,如果使用运算符sizeOf获取Struct3的大小,应该输出多少呢?

struct Struct1 {
    double a;
    char   b;
    int    c;
    short  d;
} struct1;

struct Struct3 {
    double a;
    int    b;
    char   c;
    short  d;
    int    e;
    struct Struct1 str;
} struct3;

结合上面的原则,先手动算一下:

  • double a:第一个成员,8字节,从0开始,占用位置:0-7
  • int b:4个字节,从8开始,占用位置:8 9 10 11
  • char c:1个字节,从12开始,占用位置:12
  • short d:2个字节,从13开始?不对,从14开始,占用位置:14 15
  • int e:4个字节,从16开始,占用位置:16 17 18 19

Struct1 str:Struct1中最大的是a,占8个字节,位置需要是8的整数倍,所以从24开始;

  • double a:占用位置:24-31
  • char b:1个字节,从32开始,占用位置:32
  • int c:4个字节,从36开始,占用位置:36 37 38 39
  • short d:2个字节,从40开始,占用位置:40 41

结合8字节对齐原则,最终大小48。

结果对不对呢?验证一下,完全正确:

image.png

4.为什么要内存对齐

cpu在存取数据时,并不是以字节为单位,而是以块为单位存取,当读取到下面这段特殊的组合内存区域时,8字节读取肯定是无法解析的,而如果以字节为单位进行读取会极大的影响性能,效率太低!

在读取这段特殊的组合内存区域时,根据已知的数据组合,里面最大的4个字节空间去读,需要读取两次,并且需要将两次的数据拼接后才能获取int。

image.png

如果采用内存对齐原则,就可以一次性完整的读取一个int数据。
提高了读取效率!

image.png

五.malloc分析探索

在进行malloc分析探索之前,先引入一个案例:

image.png

结果分析:

  1. sizeof:是一个运算符,获取的是类型的大小(int、size_t、结构体、指针变量等)——>lg1指针类型,所以返回8字节
  2. class_getInstanceSize:是一个函数(调用时需要开辟额外的内存空间),程序运行时才获取,计算的是类的大小(至少需要的大小)——>属性、成员变量

    • 创建的对象【至少】需要的内存大小
    • 其中class_getInstanceSize,源码核心(x + WORD_MASK) & ~WORD_MASK;8字节对齐。
    • 不考虑malloc函数的话,内存对齐一般是以【8】对齐
    • #import <objc/runtime.h>
  3. malloc_size:堆空间【实际】分配给对象的内存大小——>16字节对齐

    • 在Mac、iOS中的malloc函数分配的内存大小总是【16】的倍数
    • #import <malloc/malloc.h>

1.malloc分析探索思路

calloc方法,作为alloc流程中的核心步骤之一,主要功能是为对象分配内存空间,并返回指向该内存地址的指针。通过前面的学习,我们知道在instacnceSize方法里计算出了对象需要申请的内存大小,那系统为对象实际分配的内存大小需要申请的内存大小是一样吗?

准备一份libmalloc源码。运行下面的一段程序:

void *p = calloc(1, 24);
NSLog(@"%lu", malloc_size(p));

申请24,按照16字节对齐原则,实际开辟控件应该是32字节!也就是说malloc_size(p)应该等于32。

image.png

运行结果和猜想是一致的32,那么底层是不是这样处理的呢?探究一下!
跟踪代码运行到malloc_zone_calloc方法,根据返回值,判断关键代码是1441行。
image.png

继续运行程序,根据total_bytes<=NANO_MAX_SIZE的判断,正常流程是不会大于最大值的,所以886行是关键代码。
image.png

我们所关心的点是大小的设置,所以判断621行位关键流程。
image.png

在这个方法里找到了16字节对齐算法,也就是系统实际开辟的内存空间的大小。其中NANO_REGIME_QUANTA_SIZE = 16,SHIFT_NANO_QUANTUM = 4,所以k = (24 + 16 - 15) >> 4;之后再左移4位,16字节对齐!
image.png

2.内存申请和内存分配总结

alloc创建对象时,系统会先经过instanceSize方法计算需要申请多大的内存空间(已默认16字节对齐),再在calloc方法里对申请的内存大小进行16字节对齐处理,然后按处理后的结果来给对象分配内存空间,并返回内存地址。系统最终实际为对象分配的内存空间大小为16字节的整数倍,并且最少16字节,如果instanceSize方法里是按16字节对齐的,那实际分配的内存大小和申请的内存大小相同;如果是按8字节对齐,则不同。^_^

相关文章
|
2月前
|
存储 Java 程序员
Java中对象几种类型的内存分配(JVM对象储存机制)
Java中对象几种类型的内存分配(JVM对象储存机制)
65 5
Java中对象几种类型的内存分配(JVM对象储存机制)
|
2月前
|
存储 程序员 Python
Python类的定义_类和对象的关系_对象的内存模型
通过类的定义来创建对象,我们可以应用面向对象编程(OOP)的原则,例如封装、继承和多态,这些原则帮助程序员构建可复用的代码和模块化的系统。Python语言支持这样的OOP特性,使其成为强大而灵活的编程语言,适用于各种软件开发项目。
18 1
|
2月前
|
Swift iOS开发
iOS开发-属性的内存管理
【8月更文挑战第12天】在iOS开发中,属性的内存管理至关重要,直接影响应用性能与稳定性。主要策略包括:`strong`(强引用),不维持对象生命期,可用于解除循环引用;`assign`(赋值),适用于基本数据类型及非指针对象属性;`copy`,复制对象而非引用,确保对象不变性。iOS采用引用计数管理内存,ARC(自动引用计数)自动处理引用增减,简化开发。为避免循环引用,可利用弱引用或Swift中的`[weak self]`。最佳实践包括:选择恰当的内存管理策略、减少不必要的强引用、及时释放不再使用的对象、注意block内存管理,并使用Xcode工具进行内存分析。
|
3月前
|
存储 缓存 算法
(五)JVM成神路之对象内存布局、分配过程、从生至死历程、强弱软虚引用全面剖析
在上篇文章中曾详细谈到了JVM的内存区域,其中也曾提及了:Java程序运行过程中,绝大部分创建的对象都会被分配在堆空间内。而本篇文章则会站在对象实例的角度,阐述一个Java对象从生到死的历程、Java对象在内存中的布局以及对象引用类型。
|
3月前
|
NoSQL Redis C++
c++开发redis module问题之在复杂的Redis模块中,特别是使用第三方库或C++开发时,接管内存统计有哪些困难
c++开发redis module问题之在复杂的Redis模块中,特别是使用第三方库或C++开发时,接管内存统计有哪些困难
|
3月前
|
Java 运维
开发与运维内存问题之在堆内存中新创建的对象通常首先分配如何解决
开发与运维内存问题之在堆内存中新创建的对象通常首先分配如何解决
19 1
|
2月前
|
搜索推荐 Java API
Electron V8排查问题之分析 node-memwatch 提供的堆内存差异信息来定位内存泄漏对象如何解决
Electron V8排查问题之分析 node-memwatch 提供的堆内存差异信息来定位内存泄漏对象如何解决
44 0
|
3月前
|
安全 Java 运维
开发与运维内存问题之动态对象年龄判定的意义如何解决
开发与运维内存问题之动态对象年龄判定的意义如何解决
21 0
|
3月前
|
Java 运维
开发与运维内存问题之长期存活的对象最终会进入哪个内存区域如何解决
开发与运维内存问题之长期存活的对象最终会进入哪个内存区域如何解决
26 0
|
3月前
|
算法 Java Serverless
Java演进问题之Java程序占用的内存经常比实际应用运行产生的对象占用要多如何解决
Java演进问题之Java程序占用的内存经常比实际应用运行产生的对象占用要多如何解决

热门文章

最新文章

下一篇
无影云桌面