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 <objc/runtime.h> NSLog(@"%zd",class_getInstanceSize([NSObject class]));// 8
  • 通过malloc库中的malloc_size()函数可以获得实例对象指针所指向内存的大小。#import <malloc/malloc.h> 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 内部结构:

image.png

  • 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)


    类对象.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 <objc/runtime.h>
BOOL result = class_isMetaClass([NSObject class]);
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,692评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,482评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,995评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,223评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,245评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,208评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,091评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,929评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,346评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,570评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,739评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,437评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,037评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,677评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,833评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,760评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,647评论 2 354

推荐阅读更多精彩内容