iOS 底层 day01 OC对象的本质
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>
@interface Person : NSObject
{
@public
int _age;
}
@end
@implementation Person
@end
@interface Student : Person
{
@public
int _no1;
int _no2;
}
@end
@implementation Student
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
Student *stu = [[Student alloc] init];
Person *person = [[Person alloc] init];
}
return 0;
}
代码简述:构建了一个
Person
类,带有一个_age
成员变量;构建了一个Student
类,继承自Person
类,带有_no1
和_no2
成员变量;
请问obj
,stu
,person
这三个实例对象,运行时分别会分配多少内存空间? 如果清楚的知道,那么请关闭自此文章。
1. 什么是 Objective-C ?
- Objective-C 是一门通用的、高级的、面向对象的编程语言,它扩展了标准的 ANSI C 语言。
- 还将
SmallTalk式
的消息传递机制
加入到其中。 - 目前主要支持的编译器有
Clang
,采用LLVM
作为后端。
2. 既然 Objective-C 是对标准 C 的扩展,请思考,那么 NSObject
底层是用 C 语言的什么来实现的呢?
- 结构体
- 结构体的
第一个成员变量
的地址
,就是这个结构体的地址
3. 如何证明是使用 C 语言的结构体来实现的呢?
-
将开篇的代码,使用
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
指令进行编译,可以获得main.cpp
文件,从中我们可以找到如下代码: -
cpp 是
C plus plus
, 也是 C++ 文件的后缀名 -
通过
xcrun
指令,可以编译单个 .m 文件,对我们观察 Objective-C 代码本质很有帮助 -
如下所示,就是一个
NSObject_IMPL
结构体
// IMPL 就是 implement 的缩写
struct NSObject_IMPL {
Class isa;
};
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
};
struct Student_IMPL {
struct Person_IMPL Person_IVARS;
int _no1;
int _no2;
};
4. 那么从上面结构体来看,一个 NSObject_IMPL
结构体对象,理论上会分配多少内存空间呢?
- 分析上面代码可得知
NSObject_IMPL
只有一个Class
成员,那么Class
是什么呢? - 我们可以在 Xcode 中点击
Class
定义获得typedef struct objc_class *Class;
,得知Class
其实就是一个类型为struct objc_class *
的指针变量
。 - 那么问题就变简单的,在 64 位的操作系统上,指针占 8 个字节,所以一个 NSobject 理论上占 8 个字节。
5. 实际上我们项目运行中,NSObject 会占据多少内存呢?我们借助两个函数分析。
NSObject *obj = [[NSObject alloc] init];
NSLog(@"%zu",class_getInstanceSize([NSObject class]));
NSLog(@"%zu",malloc_size((__bridge const void *)(obj)));
// 输出日志
// 2020-08-20 15:47:58.227486+0800 Demo3[7428:431600] 8
// 2020-08-20 15:47:58.228025+0800 Demo3[7428:431600] 16
-
class_getInstanceSize
函数,字面意思是获取实例的大小,好像符合我们的意愿,结果打印的是 8 个字节,和我们上面分析结果一致。 -
malloc_size
是 C 语言函数,获取实例在内存中的大小,结果打印的是 16 字节。
6. 分析 class_getInstanceSize
方法, 我们只能下载 objc4
源码进行分析了
-
下载地址 https://opensource.apple.com/tarballs/objc4/objc4-723.tar.gz
-
解压源码,用 Xcode 打开,我们可以找到如下代码
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}
-
class_getInstanceSize
调用了alignedInstanceSize
方法 -
alignedInstanceSize
方法有一句清晰的注释说明此方法用于返回类的成员变量大小
,ivar
就是成员变量的意思。 - 由此我们
class_getInstanceSize
返回 8 个字节,就得到圆满解释了。 - 另外我们发现,苹果爸爸源码中的
大括号
,也存在两种写法。
7. 分析 malloc_size
方法
- 分析
malloc_size
方法,需要从allocWithZone
入手
// Replaced by ObjectAlloc
+ (id)allocWithZone:(struct _NSZone *)zone {
return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone)
{
id obj;
obj = class_createInstance(cls, 0);
return obj;
}
class_createInstance(Class cls, size_t extraBytes)
{
return _class_createInstanceFromZone(cls, extraBytes, nil);
}
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
id obj;
size_t size = cls->instanceSize(extraBytes);
obj = (id)calloc(1, size);
}
size_t instanceSize(size_t extraBytes) {
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
-
从上面代码一层层跟进,我们发现最终苹果爸爸会做一个判断
if (size < 16) size = 16;
并且还有一个清晰的解释// CF requires all objects be at least 16 bytes.
-
这就解答了我们的疑惑,虽然
NSObject
,实际上只需要占用 8 个字节,可是苹果爸爸认为 CF 中所有的objects
,不得小于 16 字节,所以,最终给NSObject
的实例对象分配了 16 字节。
8. 那么 person 占据多少字节呢?
Person *person = [[Person alloc] init];
NSLog(@"person:class_getInstanceSize: %zu",class_getInstanceSize([Person class]));
NSLog(@"person:malloc_size: %zu",malloc_size((__bridge const void *)(person)));
// 打印结果
2020-08-20 16:16:59.162324+0800 Demo3[7806:473714] person:class_getInstanceSize: 16
2020-08-20 16:16:59.162382+0800 Demo3[7806:473714] person:malloc_size: 16
- 是否和你猜想的一样的?
- 如果按照我们上面的理论,
person
对象调用class_getInstanceSize
方法,应该是8 + 4 = 12
,他的成员变量只需要 12 个字节,为什么打印出来是 16 个字节呢? - 这关系到计算机基础知识
内存对齐
,结构体的大小会是占据空间最大成员变量
的倍数 。所以最终 12 字节会变成 8 的倍数,也就是 16 字节。
9. 那么 stu 占据多少字节呢?
Student *stu = [[Student alloc] init];
NSLog(@"stu:class_getInstanceSize: %zu",class_getInstanceSize([Student class]));
NSLog(@"stu:malloc_size: %zu",malloc_size((__bridge const void *)(stu)));
// 打印结果
2020-08-20 16:16:59.162441+0800 Demo3[7806:473714] stu:class_getInstanceSize: 24
2020-08-20 16:16:59.162473+0800 Demo3[7806:473714] stu:malloc_size: 32
-
通过前面的知识,
class_getInstanceSize
计算内存8 + 4 + 4 + 4 = 20
,然后进行内存对齐
,最终是 24 字节,没啥问题。 -
为什么
malloc_size
会得到 32 字节呢? 这主要是苹果对内存的管理策略
,对象最终获得的内存还会加一个约束最终的内存会以 16 的倍数出现
。所以 24 字节,又变成了 32 字节。 -
对于这一点,读者可以自己往
Student
上继续添加成员变量,观察占据内存的变化,来证实。