从一下方面来深入研究:
理解面向对象的类到面向过程的结构体
深入理解OC消息转发机制
理解OC的属性property
实践Category添加属性与黑魔法method swizzling
深入weak实现机理
runtime就是运行时,runtime很强大,是OC最重要的一部分也是OC最大的特色,可以不夸张的说runtime成就了OC,尽管runtime是OC的一个模块而已。
我们都知道高级编程语言想要成为可执行文件需要先编译为汇编语言再汇编为机器语言,机器语言也是计算机能够识别的唯一语言,但是OC并不能直接编译为汇编语言,而是要先转写为纯C语言再进行编译和汇编的操作,从OC到C语言的过渡就是由runtime来实现的。然而我们使用OC进行面向对象开发,而C语言更多的是面向过程开发,这就需要将面向对象的类转变为面向过程的结构体,本文正是通过runtime源码分析来讲解runtime是如何将面向对象的类转变为面向过程的结构体。
深入代码理解instance、class object、metaclass
面向对象编程中,最重要的概念就是类,下面我们就从代码入手,看看OC是如何实现类的。
前文一直在说runtime将面向对象的类转变为面向过程的结构体,那这个结构体到底是什么样子的?打开#import文件,可以发现以下几行代码
通过注释和代码不难发现,我们创建的一个对象或实例其实就是一个struct objc_object结构体,而我们常用的id也就是这个结构体的指针。有如下代码:
通过上述代码可以看出,我们创建的NSString类的实例str其实就是一个struct objc_object结构体指针,所以不管是Foundation框架中的类或是自定义的类,我们创建的类的实例最终获取的都是一个结构体指针,这个结构体只有一个成员变量就是Class类型的isa指针,Class是结构体指针,指向struct objc_class,那这个结构体又是什么呢?这里先透露一句话str is a NSString,再加上Class这个指针的名字,我们不难猜测,Class就是代表NSString这个类。
接下来会详细讲解这个结构体,现在再看另一个例子,有时我们也会通过下述方法来创建一个实例:
可能你已经发现了,通过实例对象调用的class方法,我们能够获取到一个Class类型的变量,我们可以通过这个Class来创建相应的实例对象。
实际上,OC中的类也是一个对象,称为类对象,上述方法中通过[str class]方法获取到的就是NSString类的类对象,接着我们就可以通过这个类对象来创建实例对象,那这个类对象又是什么东西呢?打开#import文件,我们可以找到结构体struct objc_class的定义,该结构体定义如下:
struct objc_class结构体定义了很多变量,通过命名不难发现,结构体里保存了指向父类的指针、类的名字、版本、实例大小、实例变量列表、方法列表、缓存、遵守的协议列表等,一个类包含的信息也不就正是这些吗?没错,类对象就是一个结构体struct objc_class,这个结构体存放的数据称为元数据(metadata),该结构体的第一个成员变量也是isa指针,这就说明了Class本身其实也是一个对象,因此我们称之为类对象,类对象在编译期产生用于创建实例对象,是单例。
类对象中的元数据存储的都是如何创建一个实例的相关信息,那么类对象和类方法应该从哪里创建呢?就是从isa指针指向的结构体创建,类对象的isa指针指向的我们称之为元类(metaclass),元类中保存了创建类对象以及类方法所需的所有信息,因此整个结构应该如下图所示:
实例对象、类对象与元类简图
通过上图我们可以清晰的看出来一个实例对象也就是struct objc_object结构体它的isa指针指向类对象,类对象的isa指针指向了元类,super_class指针指向了父类的类对象,而元类的super_class指针指向了父类的元类,那元类的isa指针又指向了什么?为了更清晰的表达直接使用一个大神画的图。
实例对象、类对象与元类的自闭环
通过上图我们可以看出整个体系构成了一个自闭环,如果是从NSObject中继承而来的上图中的Root class就是NSObject。至此,整个实例、类对象、元类的概念也就讲清了,接下来我们在代码中看看这些概念该怎么应用。
c1是通过一个实例对象获取的Class,实例对象可以获取到其类对象,类名作为消息的接受者时代表的是类对象,因此类对象获取Class得到的是其本身,同时也印证了类对象是一个单例的想法。
那么如果我们想获取isa指针的指向对象呢?
介绍两个函数
class_isMetaClass用于判断Class对象是否为元类,object_getClass用于获取对象的isa指针指向的对象。
再看如下代码:
通过代码可以看出,一个实例对象通过class方法获取的Class就是它的isa指针指向的类对象,而类对象不是元类,类对象的isa指针指向的对象是元类。
你不知道的msg_send
我们知道在OC中的实例对象调用一个方法称作消息传递,比如有如下代码:
上述代码中的第二句str称为消息的接受者,appendString:称作选择子也就是我们常用的selector,selector和参数共同构成了消息,所以第二句话可以理解为将消息:"增加一个字符串: is a good guy"发送给消息的接受者str。
OC中里的消息传递采用动态绑定机制来决定具体调用哪个方法,OC的实例方法在转写为C语言后实际就是一个函数,但是OC并不是在编译期决定调用哪个函数,而是在运行期决定,因为编译期根本不能确定最终会调用哪个函数,这是由于运行期可以修改方法的实现,在后文会有讲解。举个栗子,有如下代码:
上述代码在编译期没有任何问题,因为id类型可以指向任何类型的实例对象,NSString有一个方法appendString:,在编译期不确定这个num到底具体指代什么类型的实例对象,并且在运行期还可以给NSNumber类型添加新的方法,因此编译期发现有appendString:的函数声明就不会报错,但在运行时找不到在NSNumber类中找不到appendString:方法,就会报错。这也就是消息传递的强大之处和弊端,编译期无法检查到未定义的方法,运行期可以添加新的方法。
讲了这么多OC究竟是怎么将实例方法转换为C语言的函数,又是如何调用这些函数的呢?这些都依靠强大的runtime。
在深入代码之前介绍一个clang编译器的命令:
通过上述clang命令可以转写代码,然后找到如下定义:
可以发现转写后的C语言代码将实例方法转写为了一个静态函数。接下来一行一行的分析上述代码,第一行代码可以简要表示为如下代码:
这一行代码做了三件事情,第一获取Person类,第二注册alloc方法,第三发送消息,将消息alloc发送给类对象,可以简单的将注册方法理解为,通过方法名获取到转写后C语言函数的函数指针。
第二行代码就可以简写为如下代码:
这一行代码与上一行类似,注册了init方法,然后通过objc_msgSend函数将消息init发送给消息的接受者p。
第三行是一个对setter的调用,同样的也可以简写为如下代码:
这一行代码同样是先注册方法setName:然后通过objc_msgSend函数将消息setName:发送给消息的接收者,只是多了一个参数的传递。
同理,最后一行代码也可以简写为如下:
解释与上述相同,不再赘述。
到这里,我们应该就可以看出OC的runtime通过objc_msgSend函数将一个面向对象的消息传递转为了面向过程的函数调用。
objc_msgSend函数根据消息的接受者和selector选择适当的方法来调用,那它又是如何选择的呢?这再来回顾一下几个主要的结构体:
注意结构体struct objc_class中包含一个成员变量struct objc_method_list **methodLists,通过名称我们分析出这个成员变量保存了实例方法列表,继续查找结构体struct objc_method_list的定义如下:
我们发现struct objc_method_list中还包含了一个未知的结构体struct _objc_method同时也找到它的定义,为了方便查看将两者写在一起。
结构体struct objc_method_list里面包含以下几个成员变量:结构体struct _objc_method的大小、方法个数以及最重要的方法列表,方法列表存储的是方法描述结构体struct _objc_method,该结构体里保存了选择子、方法类型以及方法的具体实现。可以看出方法的具体实现就是一个函数指针,也就是我们自定义的实例方法,选择子也就是selector可以理解为是一个字符串类型的名称,用于查找对应的函数实现(由于苹果没有开源selector的相关代码,但是可以查到GNU OC中关于selector的定义,也是一个结构体但是结构体里存储的就是一个字符串类型的名称)。
这样就能解释objc_msgSend的工作原理的,为了匹配消息的接收者和选择子,需要在消息的接收者所在的类中去搜索这个struct objc_method_list方法列表,如果能找到就可以直接跳转到相关的具体实现中去调用,如果找不到,那就会通过super_class指针沿着继承树向上去搜索,如果找到就跳转,如果到了继承树的根部(通常为NSObject)还没有找到,那就会调用NSObjec的一个方法doesNotRecognizeSelector:,这个方法就会报unrecognized selector错误(其实在调用这个方法之前还会进行消息转发,还有三次机会来处理,消息转发在后文会有介绍)。
这样一看,要发送消息真的好复杂,需要经过这么多步骤,难道不会影响性能吗?当然了,这样一次次搜索和静态绑定那样直接跳转到函数指针指向的位置去执行来比肯定是耗时很多的,因此,类对象也就是结构体struct objc_class中有一个成员变量struct objc_cache,这个缓存里缓存的正是搜索方法的匹配结果,这样在第二次及以后再访问时就可以采用映射的方式找到相关实现的具体位置。
到这里我们就已经弄清楚了整个发送消息的过程,但是当对象无法接收相关消息时又会发生什么?以及前文说的三次机会又是什么?下文将会介绍消息转发。
消息转发: unrecognized selector的最后三次机会
前文介绍了进行一次发送消息会在相关的类对象中搜索方法列表,如果找不到则会沿着继承树向上一直搜索知道继承树根部(通常为NSObject),如果还是找不到并且消息转发都失败了就回执行doesNotRecognizeSelector:方法报unrecognized selector错。那么消息转发到底是什么呢?接下来将会逐一介绍最后的三次机会。
第一次机会: 所属类动态方法解析
首先,如果沿继承树没有搜索到相关方法则会向接收者所属的类进行一次请求,看是否能够动态的添加一个方法,注意这是一个类方法,因为是向接收者所属的类进行请求。
第二次机会: 备援接收者
第三次机会: 消息重定向
理解OC的属性property,主要从runtime出发讲解属性property相关的底层实现和相关方法,
本文将会讲解一些runtime操作属性的相关方法。
首先回顾一下相关代码以及与property底层实现相关的两个结构体:
通过上述代码其实我们可以看出,一个@property属性在底层就是一个结构体描述,那么我们如何获取这个结构体呢?可以通过如下代码获取:
首先看一下objc_property_t是什么,在objc/runtime.h中可以找到相关定义:
typedefstructobjc_property*objc_property_t;
它是一个指向结构体struct objc_property的指针,这里的结构体struct objc_property其实就是前文中.cpp文件中的struct _prop_t结构体,通过class_copyPropertyList方法就可以获取到相关类的所有属性,具体函数声明如下:
注释可以看出,第一个参数是相关类的类对象(如有疑问可以查阅本系列文章的前两篇文章),第二个参数是一个指向unsigned int的指针,用于指明property的数量,通过该方法就能够获取到所有的属性,接下来可以通过property_getName和property_getAttributes方法获取该属性描述的name和attributes值,输出的结果如下:
name很好理解,后面的attributes通过对比不难发现其规律,感兴趣的读者也可以多设置几个不同类型、不同修饰符的property看一下输出。
除此之外哈有一下几个方法用于根据属性名获取一个属性描述结构体、添加属性、替换属性等方法。
举个简单的栗子:
通过上述方法就能添加一个属性,由于本人水平有限实际开发中没有用过上述方法,具体实际例子也举不出来所以不再过多赘述。
关联对象 Associated Object
如果我们想为系统的类添加一个方法可以采用类别的方式进行扩展,相对来说比较简单,但如果要添加一个属性或称为成员变量,通常采用的方法就是继承,这样就比较繁琐了,如果不想去继承那就可以通过runtime来进行关联对象操作。
使用runtime的关联对象添加属性与我们自定义类时定义的属性其实是两个不同的概念,通过关联对象添加属性本质上是使用类别进行扩展,通过添加setter和getter方法从而在访问时可以使用点语法进行方法,在使用上与自定义类定义的属性没有区别。
具体需要使用的C函数如下:
通过注释和函数名不难发现上诉三个方法分别是设置关联对象、获取关联对象和删除关联对象。
需要说明一下objc_AssociationPolicy,具体的定义如下:
这些关键词很眼熟,没错,就是property使用的修饰符,具体含义也与property修饰符相同,
说了这么多,接下来举个具体的栗子,为一个已有类添加一个关联对象。
这个栗子设置的关联对象其实没有任何实际意义,通过代码可以看出,使用runtime为一个已有类添加属性就是通过类别扩展getter和setter方法。
实例方法
在本系列文章的第二篇iOS runtime探究(二): 从runtime开始深入理解OC消息转发机制,我们详细介绍了runtime对方法的底层处理,以及发送消息和消息转发机制,这里就不再赘述了,如有需要可以查看相关文章,本文会介绍OC层面对方法的相关操作,同时会介绍method swizzling的方法。
先来回顾一下实例方法相关的结构体和底层实现,有如下代码:
weak
weak不论是用作property修饰符还是用来修饰一个变量的声明其作用是一样的,就是不增加新对象的引用计数,被释放时也不会减少新对象的引用计数,同时在新对象被销毁时,weak修饰的属性或变量均会被设置为nil,这样可以防止野指针错误,本文要讲解的也正是这个特性,runtime如何将weak修饰的变量的对象在销毁时自动置为nil。
那么runtime是如何实现在weak修饰的变量的对象在被销毁时自动置为nil的呢?一个普遍的解释是:runtime对注册的类会进行布局,对于weak修饰的对象会放入一个hash表中。用weak指向的对象内存地址作为key,当此对象的引用计数为0的时候会dealloc,假如weak指向的对象内存地址是a,那么就会以a为键在这个weak表中搜索,找到所有以a为键的weak对象,从而设置为nil。
了解了以上知识后就可以深入runtiem代码来看看具体实现细节,有兴趣的读者可以继续阅读。
深入runtime理解weak
这部分内容参考《Objective-C高级编程:iOS与OS X多线程和内存管理》,可以看出具体的实现方式就是使用了一个HashTable。
NSString*name = [[NSStringalloc] initWithString:@"Jiaming Chen"];
__weakNSString*weakStr = name;
当为weakStr这一weak类型的对象赋值时,编译器会根据name的地址为key去查找weak哈希表,该表项的值为一个数组,将weakStr对象的地址加入到数组中,当name变量超出变量作用域或引用计数为0时,会执行dealloc函数,在执行该函数时,编译器会以name变量的地址去查找weak哈希表的值,并将数组里所有weak对象全部赋值为nil。
本文出自
链接:https://www.jianshu.com/p/4a32fb8648a3