关于IOS对象的小事的探究

1,059 阅读9分钟

前言

在上一篇文章 一道有意思的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-cTest

@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 对象可以在运行时更换所属类么
......

好像就只额外想到一个。。。

本文首发于,本人博客与公众号(见下图),如果希望转载到公众号,请联系本人开通权限。

公众号

公众号刚刚起步,以后也会经常更新一些相关的技术性文章,大家也可留言一些遇到的问题,看到了一定秒回