深入语言的设计原理,理解其特性的本质。
0.背景
关于Runtime的文章有很多,多数人都知道Runtime是做什么用的,但是要彻底弄清Runtime存在的意义,不仅仅需要知道它是如何工作的,更需要知道为什么需要它这么工作,弄清楚为什么需要Runtime这么工作就需要从理解OC面对对象的过程开始。
Objective-C(称之为OC)语言是在C语言基础上实现面对对象,从而实现开发者使用面对对象的方式进行应用开发。理解OC语言如何实现面对对象,是深入理解OC的基础。本篇文章和Runtime相关文章互为理解才能更加清晰的理解OC的面对对象的实现原理。读过这篇文章,你更能够理解Runtime的实现作用和存在意义。本篇文章一步步讲解从C语言进化到Objective-C的面对对象的过程。
1.面对过程和面对对象编程
- 编程
面对过程和面对对象都是编程的方式,我们首先要理解编程,才能更好的理解两种方式的应用。简单讲,编程是输入到系统处理然后得到想要的结果。输入可能是用户输入也可能数据,无论是什么,经过系统处理,得到结果。那么编程是:
1. 输入
2. 处理
3. 输出
- 面对过程
拿C语言来讲,C语言各种数据类型,用于存储数据,我们称之为变量;C语言函数,我们称之为操作。那么面对过程的编程的简单理解:把输入的数据存储到变量中,按照流程使用函数操作处理,输出结果。那么面对过程其实是最简单的编程思维,输入到变量,函数处理,输出结果:
int main() {
// 0.输入
// 1.变量存储
int a = 0;
int b = 1;
// 2.函数操作
func1(a);
func2(b);
// 3. 输出
return func3(a,b);
}
- 面对对象
用一个通俗的例子来讲,用面对对象的方式去造一台车,员工只需要关心这辆车需要哪些对象组成: 轮胎,车架,车门,发动机,油箱等等;至于具体的车门上需要使用螺丝,装什么玻璃,是门这个对象它自己需要管理的(方法函数和其他小的对象玻璃等)。那么组装工作就容易轻松很多,面对对象的开发类似于此。
OC语言除了包含C语言基本数据类型,同时也包含在C语言基础之上封装的类,比如类实质是C语言的结构体struct。类是对真实对象的抽象,类里面有变量和方法。面对对象编程方式,以对象为基本单位进行输入输出和处理的操作,从而得到输出。输入存储为对象,操作是对象协作,输出结果是对象。每个对象可以有自己的方法操作,同时可以有其他对象属性。
// 伪代码
int main() {
// 0.输入
// 1.变量存储,转换为各种对象, 对象中有自己专属类型的操作(方法函数),处理本类型的问题,其他类型对象不需要关心
NSObject *obj1;
NSObject *obj2;
// 2. 对象协作处理问题,只需要对象协作就行了,不在关心操作细节,对象自己会管理好自己的操作(方法函数)
obj1.doSometh();
NSObject *obj3 = obj2.doSometh(obj1);
// 3. 输出处理结果
return obj3;
}
2.OC面对对象
面对对象有三大特性:封装,继承,多态。对象是类的实例,类是对象的抽象。类本身是对同一类事物进行的抽象,然后封装属性和方法,所以封装这一特性显而易见;类是对象的抽象,抽象的模糊点的时候特点描述少,是父类,抽象的具体点的时候特点描述详细,方法多,比如动物类是模糊抽象,只抽象出来一个特点:会动,人类是具体抽象继承于动物类,所以抽象出来的类本身应该是继承的;多态,同样都是人,每个人都说话但声音不一样,这就是多态特性之一。
OC封装了很多类,有了这些类就可以使用面对对象的方式进行程序开发。比如,Foundation框架,UIKit框架下的类:
·NSObject
·NSArray
·NSSet
·UIView
...
OC的类是单继承,所有的类都间接或直接继承于NSObject。所以研究OC如何面对对象的,就需要研究OC是如何封装类的,研究类就需要从NSObject这个类开始。如下:
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
NSObject类只有一个变量,这个变量是它真正的类结构--struct,简化如下:
@interface NSObject <NSObject> {
objc_class * isa; // 类
}
struct objc_class {
objc_class * _Nonnull isa; // 元类,类的静态方法会有此类存储
objc_class * _Nullable super_class; // 父类, NSObject 父类nil
const char * _Nonnull name; // 类名称 NSObject
long version; // 类版本信息
long info;
long instance_size; // 创建实例对象时需要分配的内存大小
struct objc_ivar_list * _Nullable ivars; // 类的变量,链表里每一个链是一个结构体存储一个变量
struct objc_method_list * _Nullable * _Nullable methodLists; // 类的方法,每个struct存有一个方法相关信息
struct objc_cache * _Nonnull cache; // 缓存的方法
struct objc_protocol_list * _Nullable protocols;
};
小结:NSObject及其子类实例化一个对象,这个对象的isa是一个objc_class的对象的一个结构体,这个结构体内,记录了实例对象的变量信息和方法信息。由此可见,类实例化成对象,必须是在类对象(objc_class结构体)存在的前提下才能实现。
使用OC开发中,每一个类其背后必须有一个类对象(objc_class结构体)的实例进行维护,才能使用定义的类进行面对对象的方式进行开发。既然是这样,我们可以思考程序加载顺应该是这样的:所有类对象加载之后,才能加载我们开发的代码。因为我们使用OC的类进行面对对象的开发。类对象的实例存在,才能加载我们的类进行类的实例化对象,对象的方法和变量都有类对象(objc_class)的方法列表和变量列表维护,OC语言的动态性是也表现在这里。使用Runtime才能够操作对象的类对象,因为对象的方法调用是在类对象中找方法才能够调用,那么Runtime可以理解为一组操作类对象结构体的C的函数,这就是我们可以理解的Runtime。
3.OC面对对象动态性
动态性是OC面对对象的另一大特性。关于语言我们讨论静态语言和动态语言两种语言,避免混淆我们先看下两种类型语言,然后再讨论OC的动态性:
动态语言: 是一类在运行时可以改变其结构的语言
静态语言: 运行时结构不可变的语言就是静态语言
C语言是编译期就确定了命令执行的语言,不可以改变结构,是静态语言:
int main() {
printf("Hello world!");
return 0;
}
上述C代码在编译期,就已经确定了输出数据和数据类型,输出Hello world!
// 发消息
[obj sayHello]; // sayHello 同样是printf("Hello world!");
//编译后
((void (*)(id, SEL))(void *)objc_msgSend)((id)obj, sel_registerName("sayHello"));
上述OC, 编译后仅仅是调用函数objc_msgSend的两个参数,obj和SEL(sayHello函数名称),这是编译后的结果,只有在运行过程中才执行的函数调用,那么程序运行中才确认下一步真正的操作,这就是动态性!编译期只是确定了对象和对象的方法,这个方法的调用就可以在运行过程调用,但是调用可能在方法列表里找不到sayHello方法,在找不到方法的情况下,你可能在代码了做了找不到sayHello方法的处理,这就是Method Swizzling。这种编译期不确定函数调用的情况的,就足以说明这是动态性的,这种动态性不仅仅取决于Runtime的机制,同时取决于OC的面对对象的模式————由类方法运行时维护对象,在类对象中管理方法。实例对象是实例类对象维护的,类对象 是运行时才存在的,也说明这种模式的动态性。总结OC语言的动态性如下:
- 对象是类对象实例维护的,类对象实例在运行时首先被生成实例对象,才能给维护对象,体现在运行时;
- 对象可以动态的修改变量和方法;
- 对象方法是在运行时确定寻找调用哪个方法,而且方法不一定存在。
4.理解OC的消息机制
为什么OC不是直接调用,是消息机制呢?我们知道C语言的方法是直接调用的,方法提前声明,然后就可以方法调用。对于OC的对象为什么就不是直接进行方法调用的呢?如下我们一起思考,在注释中有思考过程:
// 1.C语言方法调用
printf("hello");
// 思考:
// printf()函数在编译过程就已经确定了函数地址并写在此行的是函数地址,运行时直接就是根据函数地址直接调用
// 2.OC方法调用
[obj hello];
// 思考:
// obj这个对象需要去类对象中找方法hello,那么“hello”一定是方法名称的字符串编码,通过字符串去类对象中找方法,本身就不是直接调用hello方法。
// 可以理解,这种OC对象的本质,就首先构成一定不能是直接调用方法,需要找到方法在进行调用。
// 3.OC编译后的C语言 发送消息
objc_msgSend(obj, sel_registerName("hello"));
// 思考:
// 编译后是objc_msgSend函数调用,不是直接调用hello方法。这个过程就构成了消息传递,objc_msgSend发送消息,给obj对象,让它去调用hello方法。
// 可以理解为————objc_msgSend发消息告诉obj说:"这里需要调用obj你的hello方法"
// 本质是通过objc_msgSend找obj类对象的方法列表里的方法hello,然后调用C语言方法————hello对应的方法。
通过上述的思考过程(注释中有思考过程),我们就可以得知,OC一定不能是直接调用的,是使用objc_msgSend寻找对象方法然后进行方法调用的,这个寻找方法调用过程,我们理解为发送消息,同时可以理解是OC对象的本质才导致这一结果。
5.理解Runtime
因为Runtime的函数是辅助OC的对象去类对象找方法和变量的函数,所以Runtime一定不能是OC写的,Runtime是C语言和汇编语言写的。一个OC对象聚合了一类事务,这一类是事物的属性、方法都都是有C语言的基本数据结构类型组成,他们是被objc_class这个结构体(类对象)聚合一起。Runtime是C语言的方法包,这些方法包是OC对象的消息传递者。当OC对象调用方法时,Runtime方法包中的objc_msgSend去这个对象的类对象objc_class中寻找对象要调用的方法并执行它,当OC对象访问其变量的时候,Runtime方法包中有C语言方法去对象的类对象objc_class中寻找对应的变量(变量的偏移地址),Runtime的方法包可以访问类对象objc_class的任何属性也可以修改。看起来Runtime就简简单单是OC对象协作的助手一样,但是看过以下我们才能更清楚的理解OC对象和Runtime的关系:
// 1. 首先,OC对象是运行时实例化的objc_class维护的
NSObject *obj = [[NSObject alloc] init];
// 2. 其次,objc_class等结构体实例是Runtime系统维护的
objc_class *class = objc_getClass("NSObject");
// Runtime方法objc_getClass可以获取objc_class类对象,
// 那么一定是Runtime的维护了所有的类对象,才能寻到类对象
可以看出Runtime才是维护OC对象的最底层的系统,同时OC对象的本质必须需要一套系统来维护其类对象,帮助它传递消息,并且帮它寻找方法和变量,这个系统就是Runtime了。相信到这里Runtime的功能作用应该可以理解了。
6.串联总结
1. OC对象本质是objc_class结构体实例运行时维护的
2. OC对象动态性是其对象本质的表现
3. OC对象的消息机制是因为对象本身并不能直接找到调用的方法造成的
4. OC对象需要一套系统来维护并提供协作帮助的能力,这个系统就是Runtime
5. Runtime的是OC面对对象的幕后支持者(类对象的维护者,对象的消息传递者)
6. 有了Runtime的出现Method Swizzling不足为奇
7. 有了Runtime使用类的分类给类添加方法也不足为奇
网友评论