OC|OC 对象的本质

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)); //16Teacher *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 内部结构:

OC|OC 对象的本质
文章图片
image.png 【OC|OC 对象的本质】
  • 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)

    OC|OC 对象的本质
    文章图片
    类对象.png
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]);

    推荐阅读