iOS底层原理 02:OC对象原理探索(中)

这是我参与更文挑战的第2天,活动详情查看: 更文挑战

在上一篇文章OC对象原理探索(上)中我们大致介绍了alloc的调用流程,今天我们来对alloc的具体流程进行分析

LLVM优化alloc

在上一篇文章中我们按照源码分析了一下alloc的调用流程,大致如下:

[Person alloc]->[NSObject alloc]->_objc_rootAlloc->callAlloc

现在我们在objc源码中,再做一次分析

alloc具体流程分析

1、断点调试[Person alloc]

image.png

2、查看汇编调用

Debug->Debug Workflow->Always Show disassembly

image.png

我们发现[Person alloc]调用了符号objc_alloc而不是调用alloc

3、执行objc_alloc

我们添加objc_alloc符号断点,然后继续执行,进入objc_alloc的调用

image.png

此时关闭汇编窗口,查看代码执行发现,调用了objc_alloc方法,这个我们之前按照源码查看调用顺序时的alloc->_objc_rootAlloc是不一样的

4、执行alloc

我们分别在objc_rootAlloc方法和alloc方法中打上断点

image.png
image.png

继续,发现相继执行了alloc_objc_rootAlloc两个方法

image.png

image.png

本该直接调用的alloc方法,在调用之前插入了objc_alloc的执行逻辑,为什么?发生了什么事
这事LLVM优化的结果

LLVM优化

LLVM源码 速度太慢,没下完,待续…

影响对象内存的因素

1、我们创建一个类Person

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation Person

@end
复制代码

2、查看内存占用结果:

注意需要引入#import <objc/runtime.h>

image.png

class_getInstanceSize用于获取类的实例对象所占用的内存大小,并返回具体的字节数,其本质是获取实例对象中成员变量的内存大小

3、添加属性

我们给Person添加nickName属性,然后赋值,再次查看打印结果

image.png

继续给Person添加成员变量height

@interface Person : NSObject {
    int height;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@end
复制代码

查看打印结果

image.png

4、添加方法

Person添加- (void)work+ (void)say方法并实现

@interface Person : NSObject {
    int height;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
- (void)work;
+ (void)say;
@end

@implementation Person
- (void)work {
}

+ (void)say {   
}
@end
复制代码

查看打印结果:

image.png

5、结论

以上步骤可知,属性成员变量可以影响对象内存方法不影响对象内存

最终结论:影响对象内存的因素为成员变量

结构体内存对齐

类型占用字节数表格

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
unsigned 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

内存对齐的三大原则

1、数据成员对齐规则

结构struct(或联合(union))的数据成员,第一个数据成员放在offset0的地方,以后每个数据成员存储的起始位置要从给成员大小或者成员的子成员大小(只要该成员有子成员,比如说数组,结构体等)的整数倍数开始(比如int4个字节,则要从4的整数倍地址开始存储)

2、结构体作为成员

如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储,(struct a中存有struct bb里有charintdouble等元素,那么b应该从8的整数倍开始存储),因为double占用8个字节

3、结构体总大小

结构体总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的要补齐

请看一下例子:

struct Struct1 {
    double a;       // 占8字节 存放在[0 7]
    char b;         // 占1字节 下一个索引8是1的整数倍,存放在[8]
    int c;          // 占4字节 下一个索引9不是4的整数倍,所以空出9,10,11,存放在 [12 13 14 15]
    short d;        // 占2字节 下一个索引16是2的整数倍,存放在[16 17]
}struct1;           // 总区间为[0...17],大小为18,取最大元素double8字节的整倍数,所以总大小为24

struct Struct2 {
    double a;       // 占8字节 存放在[0 7]
    int b;          // 占4字节 下一个索引8是4的整数倍,存放在[8 9 10 11]
    char c;         // 占1字节 下一个索引12是1的整倍数,存放在[12]
    short d;        // 占2字节 下一个索引13不是2的整倍数,所以空出13 存放在[14 15]
}struct2;           // 总区间为[0...15],大小为16,取最大元素double8字节的整倍数,所以总大小为16

// 家庭作业 : 结构体内存对齐
struct Struct3 {
    double a;           // 占8字节 存放在[0 7]
    int b;              // 占4字节 下一个索引8是4的整数倍,存放在[8 9 10 11]
    char c;             // 占1字节 下一个索引12是1的整倍数,存放在[12]
    short d;            // 占2字节 下一个索引13不是2的整倍数,所以空出13 存放在[14 15]
    int e;              // 占4字节 下一个索引16是4的整倍数,存放在[16 17 18 19]
    struct Struct1 str; // 占24字节 下一个索引20不是str中double8字节整数倍,所以空出20 21 22 23,存放在[24.....46]
}struct3;               // 总区间为[0...46],大小为47,取最大元素double8字节的整倍数,所以总大小为48

NSLog(@"\n第一个结构体大小:%lu\n第二个结构体大小:%lu\n第三个结构体大小:%lu", sizeof(struct1), sizeof(struct2), sizeof(struct3));
复制代码

最终打印结果:

第一个结构体大小:24
第二个结构体大小:16
第三个结构体大小:48
复制代码

Struct3中添加多个子Struct

struct Struct1 {
    double a;       // 占8字节 存放在[0 7]
    char b;         // 占1字节 下一个索引8是1的整数倍,存放在[8]
    int c;          // 占4字节 下一个索引9不是4的整数倍,所以空出9,10,11,存放在 [12 13 14 15]
    short d;        // 占2字节 下一个索引16是2的整数倍,存放在[16 17]
}struct1;           // 总区间为[0...17],大小为18,取最大元素double8字节的整倍数,所以总大小为24

struct Struct2 {
    double a;       // 占8字节 存放在[0 7]
    int b;          // 占4字节 下一个索引8是4的整数倍,存放在[8 9 10 11]
    float c;        // 占4字节 下一个索引12是4的整数倍,存放在[12 13 14 15]
    short d;        // 占2字节 下一个索引16是2的整数倍,存放在[16 17]
    bool e;         // 占1字节 下一个索引18是1的整数倍,存放在[18]
}struct2;           // 总区间为[0...18],大小为19,取最大元素double8字节的整倍数,所以总大小为24

struct Struct3 {
    double a;               // 占8字节 存放在[0 7]
    int b;                  // 占4字节 下一个索引8是4的整数倍,存放在[8 9 10 11]
    char c;                 // 占1字节 下一个索引12是1的整数倍,存放在[12]
    short d;                // 占2字节 下一个索引13不是2的整数倍,所以空出13,存放在[14 15]
    int e;                  // 占4字节 下一个索引16是4的整数倍,存放在[16 17 18 19]
    struct Struct1 str1;    // 占24字节 下一个索引20不是str1中double8字节的整数倍,所以空出20 21 22 23,最后存放在[24.....47]
    struct Struct2 str2;    // 占24字节 下一个索引48是str2中double8字节整数倍,存放在[48.....71]
    bool f;                 // 占1字节 下一个索引72是1的整数倍,存放在[72]
}struct3;                   // 总区间为[0...72],大小为73,取最大元素double8字节的整倍数,所以总大小为80
复制代码

malloc源码引入

sizeof、class_getInstanceSize和malloc_size

1、我们将Person类修改如下:

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;
@end

@implementation Person

@end
复制代码

2、然后在main中编写代码如下:

需要引入#import <objc/runtime.h>#import <malloc/malloc.h>

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        Person *person = [Person alloc];
        person.name = @"张三";
        person.nickName = @"法外狂徒";

        NSLog(@"person==>%@", person);
        NSLog(@"sizeof==>%lu", sizeof(person));
        NSLog(@"class_getInstanceSize==%zu", class_getInstanceSize([Person class]));
        NSLog(@"malloc_size==%zu", malloc_size((__bridge const void *)(person)));

        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
复制代码

3、打印结果:

image.png

4、解释:

  • sizeof计算数据(数组,变量,类型,结构体等)所占内存空间,单位字节
    • person为对象,其结构体指针地址为8字节
  • class_getInstanceSize计算对象及成员变量占用的内存空间,需要注意父类属性isa8字节对齐
    • name(NSString8字节)+nickName(NSString8字节)+age(int4字节)+height(long8字节)+isa(来自NSObject8字节) = 36字节,按照8字节对齐规则,最终为40字节
  • malloc_size计算实际向系统申请开辟的内存空间,16字节对齐
    • 40字节向系统申请时,遵循16字节对齐原则,最终为48字节

那么malloc是如何向系统申请内存的呢,我们接下来结合malloc源码分析一下

malloc源码分析

1、定位malloc_size源码位置

我们在工程中点击malloc_size可以定位到其在源码中的位置如下:

image.png

那么我们可以下载malloc的源码来进行分析

malloc源码下载地址,我们以317.40.8版本源码为例进行分析

image.png

2、malloc源码分析

我们结合上文中实际占用40字节最终申请48字节的结论来分析源码

  • 申请开辟空间

我们在malloc源码的main方法中添加如下代码:

image.png
通过打印我们得知,最终却是开辟了48字节的空间,那么是如何实现的呢?,下边我们结合源码分析

  • 调用流程

通过分析源码我们发现calloc->_malloc_zone_calloc最终进入方法

image.png
我们分析可知,核心代码为ptr = zone->calloc(zone, num_items, size);,但是我们无法查看zone->calloc的具体实现了,这里我们有两种方法可以继续分析:

1.通过po打印

image.png

2.通过汇编汇编

打开汇编窗口,control+step into一直往下执行,最终进入

image.png

我们全局搜索default_zone_calloc,并添加断点,关闭汇编窗口继续执行,结果如下:

image.png

我们po打印发现,接下来调用了nano_malloc.cnano_calloc方法

image.png

定位到nano_malloc方法,打下断点,继续执行:

image.png

image.png

slot_LIFOLast in, First out标明这块内存开辟在

目前已知的16字节对齐的算法有两个:

  • alloc源码中的align16
  • malloc源码中的segregated_size_to_fit

我们在_nano_malloc_check_clear方法中,明显看到segregated_size_to_fit对齐算法的调用,点击进入segregated_size_to_fit方法内部:

image.png

典型的对齐算法,40经过16字节对齐之后为48字节,这就是最终会开辟48字节空间的核心所在

总结

  • 上的对象内存以16字节对齐
  • 对象的成员变量内存以8字节对齐
  • 对象对象之间内存以16字节对齐
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享