这是我参与更文挑战的第2天,活动详情查看: 更文挑战
在上一篇文章OC对象原理探索(上)中我们大致介绍了
alloc
的调用流程,今天我们来对alloc
的具体流程进行分析
LLVM优化alloc
在上一篇文章中我们按照源码分析了一下alloc
的调用流程,大致如下:
[Person alloc]
->[NSObject alloc]
->_objc_rootAlloc
->callAlloc
现在我们在objc源码
中,再做一次分析
alloc
具体流程分析
1、断点调试[Person alloc]
2、查看汇编调用
Debug
->Debug Workflow
->Always Show disassembly
我们发现[Person alloc]
调用了符号objc_alloc
而不是调用alloc
3、执行objc_alloc
我们添加objc_alloc
符号断点,然后继续执行,进入objc_alloc
的调用
此时关闭汇编窗口,查看代码执行发现,调用了objc_alloc
方法,这个我们之前按照源码查看调用顺序时的alloc
->_objc_rootAlloc
是不一样的
4、执行alloc
我们分别在objc_rootAlloc
方法和alloc
方法中打上断点
继续,发现相继执行了alloc
和_objc_rootAlloc
两个方法
本该直接调用的
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>
class_getInstanceSize
用于获取类的实例对象所占用的内存大小
,并返回具体的字节数,其本质是获取实例对象中成员变量的内存大小
3、添加属性
我们给Person
添加nickName
属性,然后赋值,再次查看打印结果
继续给Person
添加成员变量height
@interface Person : NSObject {
int height;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@end
复制代码
查看打印结果
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
复制代码
查看打印结果:
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
))的数据成员,第一个数据成员放在offset
为0
的地方,以后每个数据成员存储的起始位置要从给成员大小或者成员的子成员大小(只要该成员有子成员,比如说数组,结构体等)的整数倍数
开始(比如int
为4
个字节,则要从4
的整数倍地址开始存储)
2、结构体作为成员
如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素
大小的整数倍
地址开始存储,(struct
a
中存有struct
b
,b
里有char
,int
,double
等元素,那么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、打印结果:
4、解释:
sizeof
计算数据(数组,变量,类型,结构体等)所占内存空间,单位字节
person
为对象,其结构体指针地址为8
字节
class_getInstanceSize
计算对象及成员变量占用的内存空间,需要注意父类属性
和isa
,8
字节对齐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
可以定位到其在源码中的位置如下:
那么我们可以下载malloc
的源码来进行分析
malloc
源码下载地址,我们以317.40.8
版本源码为例进行分析
2、malloc
源码分析
我们结合上文中实际占用40
字节最终申请48
字节的结论来分析源码
- 申请开辟空间
我们在malloc
源码的main
方法中添加如下代码:
通过打印我们得知,最终却是开辟了48
字节的空间,那么是如何实现的呢?,下边我们结合源码分析
- 调用流程
通过分析源码我们发现calloc
->_malloc_zone_calloc
最终进入方法
我们分析可知,核心代码为ptr = zone->calloc(zone, num_items, size);
,但是我们无法查看zone->calloc
的具体实现了,这里我们有两种方法可以继续分析:
1.通过po
打印
2.通过汇编汇编
打开汇编窗口,control+step into
一直往下执行,最终进入
我们全局搜索default_zone_calloc
,并添加断点,关闭汇编窗口继续执行,结果如下:
我们po
打印发现,接下来调用了nano_malloc.c
的nano_calloc
方法
定位到nano_malloc
方法,打下断点,继续执行:
slot_LIFO
—Last in, First out
标明这块内存开辟在堆
上
目前已知的16
字节对齐的算法有两个:
alloc
源码中的align16
malloc
源码中的segregated_size_to_fit
我们在_nano_malloc_check_clear
方法中,明显看到segregated_size_to_fit
对齐算法的调用,点击进入segregated_size_to_fit
方法内部:
典型的对齐算法,40
经过16
字节对齐之后为48
字节,这就是最终会开辟48
字节空间的核心所在
总结
堆
上的对象
内存以16
字节对齐- 对象的
成员变量
内存以8
字节对齐 对象
与对象
之间内存以16
字节对齐