前言
在上一篇文章 一道有意思的 iOS 面试题 中写到, Objective-C 对象也是一种特殊的结构体. 那一部分写的可能不是很清楚, 也不是很易于理解. 但是在原文中改动, 并增加相关内容又觉得篇幅过于长. 所以新开一篇文章来写, 专门写 Object-C 对象相关的事.
正文
我们知道, Objective-C 是一门动态语言. Objective-C 对象的所有方法操作都是通过 objc_msgSend 这个函数传递的.
OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)
这个函数是 Objective-C 的灵魂(我个人认为的).
接下来我们需要清楚, 究竟什么是 iOS 对象, 在上一篇文章里, 我是这样讲述对象的
所有 NSObject 对象的首地址都是指向这个对象的所属类. 这个条件是充要条件. 反过来说, 如果一个地址指向某个类, 我们就可以把这个地址当成对象去用. 所以编译是会通过的, 也不会报 unrecognized selector 的错误.
其实这个总结的并不严谨, 但是也不算是错误. 这篇文章会对这个解释进行更为严谨的解释并且会有更深入的代码示范.
接下来我们就需要从头开始解释了, 首先 objc_object 在 iOS 中的定义:
- // 对象
- struct objc_object {
- Class isa OBJC_ISA_AVAILABILITY;
- };
从这个定义中可以看出来, 事实上所有的对象都是 *** 结构体 ***. 接下来需要知道 Class 的定义, 这个与 objc_object 的定义位于同一头文件 objc.h 下
typedef struct objc_class *Class;
也就是说, Class 事实上也是一个指针, 指针指向的位置是 objc_class 这个结构体, 到这里我们就不继续向下看过去了, 因为这步已经到了看到我们这次将要讲的终点了. 这个结构体是某个 Objective-C 的对象的类信息, 它就相当于是我们定义在. h 和. m 中间的 @interface 类的包含信息对象(以后篇幅会详细讲解这个结构体, 这里就是大致说一说, 因为这个结构体不是本篇文章的重点)
我们接下来可以简短地讲 c 语言中的结构体了.
这段还是直接放百度百科的定义吧(他的解释会比我的解释准确的多)
结构体作用
结构体和其他类型基础数据类型一样, 例如 int 类型, char 类型 只不过结构体可以做成你想要的数据类型. 以方便日后的使用.
在实际项目中, 结构体是大量存在的. 研发人员常使用结构体来封装一些属性来组成新的类型. 由于 C 语言内部程序比较简单, 研发人员通常使用结构体创造新的 "属性", 其目的是简化运算.
结构体在函数中的作用不是简便, 其最主要的作用就是封装. 封装的好处就是可以再次利用. 让使用者不必关心这个是什么, 只要根据定义使用就可以了.
结构体的大小与内存对齐 结构体的大小不是结构体元素单纯相加就行的, 因为我们主流的计算机使用的都是 32bit 字长的 CPU, 对这类型的 CPU 取 4 个字节的数要比取一个字节要高效, 也更方便. 所以在结构体中每个成员的首地址都是 4 的整数倍的话, 取数据元素时就会相对更高效, 这就是内存对齐的由来. 每个特定平台上的编译器都有自己的默认 "对齐系数"(也叫对齐模数). 程序员可以通过预编译命令 #pragma pack(n),n=1,2,4,8,16 来改变这一系数, 其中的 n 就是你要指定的 "对齐系数".
规则:
1, 数据成员对齐规则: 结构 (struct)(或联合(union)) 的数据成员, 第一个数据成员放在 offset 为 0 的地方, 以后每个数据成员的对齐按照 #pragma pack 指定的数值和这个数据成员自身长度中, 比较小的那个进行.
2, 结构 (或联合) 的整体对齐规则: 在数据成员完成各自对齐之后, 结构 (或联合) 本身也要进行对齐, 对齐将按照 #pragma pack 指定的数值和结构 (或联合) 最大数据成员长度中, 比较小的那个进行.
我们可以把 objc_object 的结构体简化下, 毕竟 Class 这个我们看着不是很顺眼, 顺便也把用不到的 OBJC_ISA_AVAILABILITY 去掉
- struct objc_object {
- struct objc_class *isa;
- };
这个简化结果就好了很多, 同时结合结构体的定义, 我们就可以说:
一个 Objective-c 对象, 实际上就是一个连续的内存片段, 这个内存片段的偏移量为 0 长度为某一固定值 (在 64 位系统上, 一个指针占用 8 个字节) 的地址内容是指向这个对象所属类的一个结构体的指针
同时我们将结论反推回来也是成立的, 说法是:
如果一个连续的内存片段, 偏移量为 0 长度为某一固定值的地址内容是指向某个对象所属类, 那么这段内存地址就会系统认为是这个类的一个实例对象.
有了结论, 我们接下来就可以做有意思的事情了, 当然就是去验证这个结论了
我会一步一步的把这个结论演示出来:
首先先定义一个 Objective-c 类 Test
- @interface Test : NSObject
- @end
- @implementation Test
- @end
接下来我们新建一个 Mac 的命令行工具来试验(就不新建 iOS 项目了, 因为太费时间):
首先我们先构建一个结构体:
- struct TestCase {
- void *isa;
- };
这个结构体是为了模拟对象的, 结构体类型, 只有一个泛型指针.
在 main 函数里我们按照如下过程写:
- // 由栈区初始化结构体内存
- struct TestCase testCase;
- // 将结构体中的 isa 指针指向 Test 的类 需要用__bridge 是因为 Objective-c 指针无法 直接强转成 c 的指针
- testCase.isa = (__bridge void *)[Test class];
- // 我们把这个结构体取地址, 后直接使用 __bridge 强转成 id 对象, 最后用 Test 类型的指针去接收
- Test *obj = (__bridge id)&testCase;
- // 打印对象
- NSLog(@"我是由栈区分配的对象, 我的地址很大:%@",(__bridge id)&testCase);
然后接下来我们运行这段代码, 终端会返回:
2018-12-04 12:37:09.621478+0800 TestCase[41835:1359221] 我是由栈区分配的对象, 我的地址很大:<Test: 0x7ffeefbff5a8>
通过打印发现, 我们这个打印的就是一个没有重写 description 方法的对象的标准返回, 返回中包含两个内容: 这个对象的 *** 类 *** 和 内存地址
此时已经说明了这个结构体已经被识别成对象了, 理论上这个结构体应该已经能执行这个类的所有方法了, 我们可以在 Test 这个类里面增加一个对象方法
- - (void)test {
- NSLog(@"执行了 Test Object 的 - test 方法");
- }
然后我们在这个上面的 main 方法中增加一个调用:
- // 调用对象方法
- [obj test];
运行代码, 控制台会多返回一条
2018-12-04 12:57:32.848874+0800 TestCase[42088:1396362] 执行了 Test Object 的 - test 方法
在这里就已经可以知道了, 我们的这个结构体就是彻底的一个对象了.
到这里, 本文的正文部分就相当于结束了, 我们相对细致的讲解了一下 Objective-c 对象.
彩蛋
接下来我们可以做一个很骚的操作, 这个操作我个人把它叫做偷天换日, 解释一下就是把一个实例类的对象的所属类更换, 通过这个方法, 例如我们可以把原本是 NSObject 对象的实例替换成我们自己定义的类的实例.
接下来我们把原本 main 函数的方法复制出来, 创建一个函数 testCase1, 然后清空 main 函数
首先, 我们再新建一个 Test1 的类, 里面有一个对象方法 - test
- @interface Test1 : NSObject
- @end
- @implementation Test1
- - (void)test {
- NSLog(@"执行了 Test1 Object 的 - test 方法");
- }
- @end
接下来就是骚操作的表演开始, 这里我们直接就把这段代码生成在一个测试函数中
- void testCase2() {
- // 创建一个 Test 类的实例对象
- Test *objc = [[Test alloc] init];
- // 调用 test 类的对象方法 -[ test]
- [objc test];
- // 用我们上文创建的 TestCase 结构体
- // 声明一个结构体指针, 指针指向刚才创建的对象
- struct TestCase *testCase = (__bridge void *)objc;
- // 骚操作开始, 我们把结构体的 isa 替换成 Test1 对象所属类
- // 然后接下来就是可以放弃这个结构体指针了, 我们的目标继续回归原 objc 对象
- testCase->isa = (__bridge void *)[Test1 class];
- // 调用 test 查看返回值吧
- [objc test];
- }
直接运行程序, 可以发现如下打印:
2018-12-04 16:44:33.922381+0800 TestCase[44225:1606663] 执行了 Test Object 的 - test 方法
2018-12-04 16:44:33.922641+0800 TestCase[44225:1606663] 执行了 Test1 Object 的 - test 方法
对象的所属类已经替换了
总结
我们都知道面向对象有三大特征: 封装, 继承, 多态
我们可以从这个示例中看出来 Objective-c 是如何实现的多态, 因为所有的类都是一样的数据结构, 所以多态由此形成. 我们还可以从更底层的去看为什么对象间的强转可以生效, 因为所有数据都不是预先定好的, 都和运行时候的内存内容相关.
由此看出, Objective-c 真的是一门神奇的语言
拓展
接下来, 我们可以通过这个想到一些其他的面试题.
接下来就是我自己的随意思考了.
1. Objective-C 对象可以在运行时更换所属类么
......
来源: https://juejin.im/post/5c064eb86fb9a049a81f1649