关于IOS对象的小事的探究

栏目: Objective-C · 发布时间: 5年前

内容简介:在上一篇文章我们知道,Objective-C是一门动态语言。Objective-C对象的所有方法操作都是通过这个函数是Objective-C的灵魂(我个人认为的)。

前言

在上一篇文章 一道有意思的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] 我是由栈区分配的对象,我的地址很大:

通过打印发现,我们这个打印的就是一个没有重写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 对象可以在运行时更换所属类么
......

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

作者:chouheiwa

链接:https://juejin.im/post/5c064eb86fb9a049a81f1649


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Wireshark网络分析实战

Wireshark网络分析实战

[以色列 Yoram Orzach / 古宏霞、孙余强 / 人民邮电出版社 / 2015-1 / 79.00元

本书采用步骤式为读者讲解了一些使用Wireshark来解决网络实际问题的技巧。 本书共分为14章,其内容涵盖了Wireshark的基础知识,抓包过滤器的用法,显示过滤器的用法,基本/高级信息统计工具的用法,Expert Info工具的用法,Wiresahrk在Ethernet、LAN及无线LAN中的用法,ARP和IP故障分析,TCP/UDP故障分析,HTTP和DNS故障分析,企业网应用程序行......一起来看看 《Wireshark网络分析实战》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

随机密码生成器
随机密码生成器

多种字符组合密码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器