OC的本质
- 我们平时写的OC代码的底层都是c/c++代码实现的
- OC的面向对象都是基于c/c++的数据结构实现的
- OC的对象、类主要是基于c/c++的结构体来实现的
将oc对象转换成c++代码
第一种
1、首先我们cd到需要转换成c++代码的文件所在目录
2、然后执行命令:clang -rewrite-objc 需要转换成c++代码的文件名和后缀 -o 转换后输出的文件名 例如:clang -rewrite-objc main.m -o main.cpp
- clang 是编译器前端的一种
- rewrite-objc 表示重写objc代码
- main.m表示重写main.m文件
- -o表示输出
- main表示输出的文件名
- .cpp表示输出c++代码
上面的这条指令会根据不同平台生成不同的代码,因为编译器针对不同平台生成的代码是不一样的,所以平时不用这条指令生成c/c++代码,而是通过指定生成某种平台上的c/c++代码。
第二种
1、 cd到需要转换成c++代码的文件所在目录
2、 执行命令 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 输出的c++文件 例如: xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
.
如果用到运行时的需要这样xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
- xcrun: xc表示xcode的缩写,xcrun是xcode一种工具
- -sdk: 表示指定那种sdk
- iphoneos: 表示具体的sdk,这里表示运行在iphone上的
- clang: 编译器
- -arch: 表示指定那种架构
- arm64: 表示arm64架构,另外还有armv7(32位系统)、i386(模拟器)
OC对象在内存中的布局
OC对象在内存中需要占用多少字节
通过OC的API可以看到NSObject
的定义是@interface NSObject { Class isa; }@end
通过转化成c++代码可以看到NSObject
的实现是struct NSObject_IMPL { Class isa; };
这里可以得到oc对象中存放的就是一个isa
指针。通过查看Class的定义typedef struct objc_class *Class;
可以知道Class就是一个objc_class
类型的结构体指针。在32位机上指针是占4个字节,在64位机上指针是占8个字节。由于结构体中只有一个Class
指针,所以NSObject_IMPL
结构体在64位机上占用8个字节,也就是一个NSObject
对象在内存中至少需要占用8个字节。
系统给一个OC对象分配多少内存
- 通过C 语言的sizeof(参数)运算符获取类型的大小.
- 通过runtime中的class_getInstanceSize()函数可以得到一个类实例对象的成员变量所占用的大小。
import
NSLog(@"%zd",class_getInstanceSize([NSObject class]));// 8 - 通过malloc库中的malloc_size()函数可以获得实例对象指针所指向内存的大小。
#import
NSLog(@"%zd",malloc_size((__bridge const void *)[[NSObject alloc] init])); // 16
通过上面我们可以得出结论:系统为一个OC对象分配16个字节的内存,但是真正利用的只有8个字节,用来存放成员变量isa指针。为什么是16个字节,通过CoreFo源码可以看出当一个实例对象需要的内存小于16字节时(if(size < 16) return 16;
),系统直接分配16字节。
- 可以通过XCode的工具侧面验证上面的结论:通过断点获取一个objc对象的地址指针,复制该指针,显示Xcode工具栏上的Debug->Debug Workflow->View Memory界面,在界面的Address输入框中输入刚才复制的地址值回车,查看内存里面的内容验证上面的结论。需要注意的是ios都是小端模式,小端模式读地址是从高地址开始读取的
OC的一些底层源码已经开放了,可以在opensource.apple.com/tarballs中查看objc4文件夹下查看runtime的源码
- 内存对齐:结构体的大小必须是最大成员大小的倍数
- 系统给oc对象分配的内存都是16的倍数(可以侧面通过 GNU 开源的 malloc 源码得知),系统这样分配是为了访问速度.
举例
@interface Person: NSObject
{
int _number;
}
@end
@interface Student: Person
{
int _age;
}
@end
@interface Teacher: Person
{
int _jobId;
int _level;
}
@end
Person *p = [[Person alloc] init];
// 输出16, Person对象底层结构体中包含两个成员变量:一个isa指针占8个字节,
// 一个int类型的number占4个字节,加起来一共是12个字节,根据内存对齐原则所以是16。
NSLog(@"p - %zd",class_getInstanceSize([Person class]));
// 输出16,成员变量内存加起来是12字节,
// 根据系统为OC对象分配内存是16的倍数,所以这里是16。
NSLog(@"p - %zd",malloc_size((__bridge const void *)p));
Student *stu = [[Student alloc] init];
// 输出16, Student对象底层结构体中包含两个成员变量:
// 一个是person类型的结构体占16字节,一个是int类型的age占4个字节。
// 这里因为person类型的结构体实际只占了12个字节,
// 编译器会把剩余的4个字节给age使用,所以是16个字节
NSLog(@"stu - %zd",class_getInstanceSize([Student class])); //16
// 输出16, person 结构体实际占用的 12 字节,和 int 类型的 age 占用 4 字节
NSLog(@"stu - %zd",malloc_size((__bridge const void *)stu)); //16
Teacher *t = [[Teacher alloc] init];
// 输出24, person 结构体实际占用的 12 字节 和
// 两个 int 类型的成员变量各占 4 字节,总共是 20 字节,
// 根据内存对齐原则,所以最少是 24
NSLog(@"stu - %zd",class_getInstanceSize([Teacher class]));
// 输出 32, 实际需要20字节,系统给 oc 对象分配的实际内存大小
// 都是 16 的整数倍, 所以这里是 32
NSLog(@"stu - %zd",malloc_size((__bridge const void *)t));
gnu 开源组织
OC 对象的分类
- Objective-C中的对象主要分为以下三类:
instance对象(实例对象)
- 实例对象就是通过类 alloc 出来的对象,每次调用 alloc 都会产生新的实例对象
- 实例对象在内存中存储着该实例对象的所有成员变量(isa 指针,和其他成员变量)
- 实例对象中为什么不存放实例方法? 因为方法只需要存一份就够了,而成员变量的值对每个对象来说可能都不一样,所有每个实例对象都都会存放成员变量
objc_class struct 内部结构:
- objc_calss 中 cashe 是方法缓存列表,用来存放经常调用的方法,当调用方法时,先去 cache 中去找,找不到再去方法列表里面去找
- objc_calss 中 bits 需要 & 上 FAST_DATA_MASK 才能取出类的具体信息 class_rw_t.
*class_rw_t 中 ro 是 readOnly 的意思,这里存放的是类的初始信息 - class_rw_t 中 methods 存放的是方法列表,它是一个二维列表,列表中的元素是类的原始方法列表,每个分类的方法类表
- class_rw_t 中 propertires 存放的是属性列表,它也是二维列表
- class_rw_t 中 protocols 存放的是协议方法列表,它也是二维列表
- class_rw_t中的methods、propertires、protocols、都是可读可写的,包含初始类的信息、分类的内容
- class_ro_t 中 baseMethodList 存放的是原始方法列表,存放的是 method_t 类型
- class_ro_t 中 ivars 存放的是成员变量列表
- class_ro_t 中 basePropertiew 中存放的是属性列表
class_ro_t 中 baseProtocols 中存放的是协议方法列表 - class_ro_t 里面的baseMethodList、basePropertiew、baseProtocols、ivars、是一维数组,class_ro_t里面的内容是只读的,包含了类的初始内容
method_t
struct method_t{
SEL name; // 函数名
const char * types; // 包含了函数返回值、参数编码的字符串
IMP imp; // 指向函数的指针(函数地址)
}
- IMP 代表函数的具体实现
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
- SEL 代表方法/函数名,一般叫选择器,底层结构跟 char * 类似; 可以通过@selector() 和 sel_registerName()获得,可以通过 sel_getName() 和 NSStringFromSelector() 转成字符串,不同类中相同名字的方法,所对应的方法选择器是相同的,
- types ios 中提供了一个叫做@encode 的指令,可以将具体的类型表示成字符串编码
Code | Meaning |
---|---|
c | char |
i | int |
v | void |
: | SEL |
@ | id |
... | ... |
对应的一个test 方法经过 Type Encoding 之后就变成了 v16@0:8
: v 表示返回参数是 void, @ 表示第一个参数是 id,:表示第二个参数是 SEL, 16表示参数所占的总字节数,0 表示第一个参数的位置,也就是 self 参数,它暂用 8 个字节,8 表示_CMP 的位置,它也占用 8 个字节,这里要注意ios 中方法有两个默认参数 self 和 _CMP
方法缓存 cache_t
- 我们知道 ios 调用方法是通过 isa 指针找到类或元类对象,然后在方法列表里面查找方法,如果找不到就通过 superClass 查找父类的方法列表,这样一层一层往上找,一直找到基类,而方法列表是一个二维数组,查找起来就要遍历,如果多次调用就要多次查找,这样就非常麻烦. Class 内部结构中的 cache_t 就是用来解决这个问题的,cache_t 采用散列表来缓存曾经调用过的方法,可以提高方法的查找速度.
struct cache_t{
struct bucket_t *_buckets; //散列表, _buckets是一个数组,里面的元素是bucket_t类型的
mask_t _mask; // 散列表的长度-1
mask_t _occupied; // 已经缓存的方法数量
}
struct bucket_t{
cache_key_t _key; // SEL 作为 key
IMP _imp; // 函数内存地址
}
- 子类对象调用父类的方法,其方法缓存会缓存到子类的 cache 中去
class 对象(类对象)
NSObject *obj1 = [[NSObject alloc] init];
Class class1 = [obj1 class];
Class class2 = [NSObject class];
Class class3 = object_getClass(obj1);
- class1、class2、class3都是 NSObject 的 class 对象(类对象)
- 它们都是同一个对象,每个类在内存中有且只有一个 class 对象
-
类对象在内存中存储的信息主要包括:
1、isa 指针
2、superclass 指针
3、类的属性信息(@property)、类的对象方法(包括分类中的对象方法)信息(instance method)
4、类的协议信息(protocol)、类的成员变量信息(ivar)
meta-class对象(元类对象)
- 获取元类对象:
Class metaClass = object_getClass([NSObject class]); // 将类对象当做参数传入,获得元类对象
; 注意通过 class 方法只能得到类对象,调用多少次 class 方法返回的都是类对象.得不到元类对象的Class class = [[[NSObject class] class] class]; //返回的是类对象
- 每个类在内存中有且只有一个 meta-class 对象
- 元类对象和类对象的内存结构是一样的,但是用途不一样
- 元类对象在内存中存储的信息包括:
1、isa 指针
2、superclass 指针
3、类的类方法(包括分类中的方法)信息(class method) - 判断 class 是否是 meta-class
#import
BOOL result = class_isMetaClass([NSObject class]);