iOS--RunTime的那些事儿(一)

引言:笔者在接触runtime之前,认为runtime是一个可用可不用的,因为在我们的实际开发中毕竟用的少。但是通过对runtime的相关学习和整体之后,就觉得在代码中用的确实不是很多,但是runtime能在关键的时候或者用OC(Swift)解决问题棘手的时候,帮助我们化险为夷。之前笔者也是在了解和学习runtime之前,不停的在心里问:runtime究竟能在哪里用到?就举个简单的例子:我们在设置button的点击事件的时候,一般是这样设置的:

button的点击事件

但是这样,我们的Button声明和方法实现已经分开了,那我们可不可以设置成这样呢:

Button点击事件

要是想实现以上的方法,就需要用到runtime了,现在就把自己学习runtime的心得分享给大家,如果有错误的地方,欢迎大家指出。

一、runtime(运行时)的简介

众所周知,Objective-C是一门动态语言,它的函数调用(消息调用)在运行时才会执行。简单的来说,runtime是属于C语言的API,Objective-C的幕后工作者。点击这里可以看到runtime的开源代码以及点击这里有对Objective-C Runtime的介绍文档。

二、runtime的消息机制

其实Objective-C中的方法调用,实质上就是发送消息。例如我们可以将一个.m文件转化成.cpp文件。

Objective-C中的普通方法调用

上述是我们在开发中调用的普通方法,但是我们把.m文件转化成.cpp文件会看到什么呢?(转化方法是使用clang重写命令:clang -rewrite-objc Students.m.假如在重写命令的过程中,出现“./ViewController.h:9:9: fatal error: 'UIKit/UIKit.h' file not found”类似这样的错误,可以参考该大神的文章Objective-C编译成C++代码报错)。

runtime发送消息

红箭头指向的就是消息发送函数,在Xcode中看到的objc_msgSend是这样:

objc_msgSend

下面就先介绍下objc_msgSend中出现的id和SEL吧:

id:(还记得我们一开始生成的.cpp文件吗,对,点击打开文件就可以看到这些信息了)

所谓的id原来是C中的结构体

我们在结构体中看到了objc_object,那么它又是什么呢,我们可以点击这里,看到objc_object的真身,也是结构体!

太长了,截取了一部分。

我们可以看到objc_object这个结构体中,包含了一个private的isa指针,它的类型为isa_t。根据isa可以找到对象所对应的类,另外Objective-C的引用技术原理也跟这个isa_t相关,以后再研究。

SEL在Objective-C中的selector在objc中是以SEL表示的。SEL是一个方法选择器,个人理解它就是实现哪个方法的标识,通过Objective-C中的@selector方法或者runtime中的sel_registerName(const char *str)都可以获得一个SEL的方法选择器,当然更直接一点,SEL sel = NSSelectorFromString(@"1111");也可以。

同样的,SEL也是一个结构体。

三、runtime发送消息的流程

Class:另外,我们在objc_object的结构体中,看到了Class ISA();,那么Class又是什么呢?

我们可以看到,Class是指向结构体objc_class的指针

在Xcode中,我们可以看到结构体objc_class的真身:

在runtime发送消息的整个流程中,我们需要用到objc_class结构体中的super_class,methodLists,cache

我在网上看大神们的介绍,objc_class是继承于objc_object,但是在源码中我只看到截图所示的介绍。从objc_class结构体中,我们可以看到它包含了isa(指向自身类的指针),super_class指向父类的指针,name(类名),ivars(成员变量),methodLists(方法列表),cache(方法缓存),protocols(协议)。

Cache:主要是为了方法的缓存。一般来说,当方法调用的时候(消息发送),不会直接在isa所指向的类中查找能够响应的消息的方法,而是到cache中去查找。假如在cache中没有找到能够能够响应的消息的方法,那么它才会去methodLists(方法列表)中查找,并且runtime会把已经调用过得方法缓存到cache中,方便下次调用,也是为了优化性能。更多的关于缓存的实现细节,可以戳这里看去,反正我已经看的眼花缭乱了。

runtime发送消息的流程:就以[self  StudetsRun];这个方法为例。首先,Xcode会将[self  StudetsRun];方法转化为runtime中的objc_msgSend,并将方法调用者self和方法名字StudetsRun作为参数,以消息的方式传输出去。objc_class中的isa指针,会找到对象所对应的类。在类中,首先去cache中,通过SEL来查找相对应的Method,如果有的话,消息就发送成功了。假如没有,就去isa指向的类中的methodLists(方法列表)中查找,并且runtime会把已经调用过得方法缓存到cache中。如果isa指向的类中没有该方法,就去该类的父类中去查找,一直找到根类NSObject中。根类中也没有的话,要么就报类似这样的crash信息:unrecognized selector sent to instance 0x7fd0a141afd0。说明没有找到该方法,或者我们采用消息的转发,可以避免crash。

四、runtime动态添加属性

我们知道,分类是没有办法添加成员变量的,但是可以用runtime动态添加属性,还可以达到偷天换日的效果,也为我们解决问题提供了新的思路和更好的办法。

runtime动态添加属性所用到的函数

1.objc_setAssociateObject:主要是为属性动态添加setter方法,第一个参数是id类型的,即你想要在哪个属性上添加set方法,就需要传入哪个属性。第二个参数key,属性上面的值和键是要一一对应,键的类型不限(int,char都可以),但是一般会用char,因为它节省内存。第三个参数就是要添加在属性上的值了,注意这个属性的值必须是id类型的(那int,BOOL类型的怎么办呢,稍后再说),最后一个policy是描述值的类型,系统给我们枚举了一下几种:

policy(类型)

2.objc_getAssociateObject:该方法主要是为了动态生成getter方法,第一个属性是你要从哪个属性上面取值,第二个属性是传入一个键。

3.objc_removeAssociateObjects:从哪个属性中移除

4.举个例子:就为NSObject添加分类,并添加一个属性name。

在.h文件中声明一个name属性,方便点语法的调用。

.h文件

在.m文件中我们需要实现getter,setter方法:

实现setter,getter方法

5.动态添加基础数据类型属性(int,BOOL等):

在.h中的属性声明:

BOOL类型的属性声明

.m文件实现getter,setter方法:

实现getter,setter方法

6.动态添加属性的应用:

关于runtime动态添加的属性的应用,我举的例子是为button动态添加一个block,让button的点击事件在block中运行,关于Demo,请点击我的Demo下载吧!

五、Category为什么不能添加成员变量

我们都知道Category(分类)是不能添加成员变量的,那其中的原因又是什么呢?

在runtime中category是指向结构体objc_category的指针,其中的原因,我们还需要在这个objc_category结构体中下手。

Category
objc_category结构体

而我们的类在创建的时候要用到objc_class属性,我们就拿结构体objc_class和objc_category做下对比:

objc_class结构体

通过两个结构体的对比我们就会发现:相比于objc_class,objc_category缺少isa、super_class、ivars(成员列表)、cache等属性。在分类中,我们可以用@property来声明属性,同时也声明了setter和getter方法。但是因为objc_category中缺少ivars(成员列表属性),所以我们无法生成成员变量(想深入了解@property的小伙伴请点击iOS @property探究(二): 深入理解)。但是objc_category中有instace_method和class_method两个属性,所以分类是可以添加方法的。

六、block的实现原理

block在我们的开发中也是比较常用的,那么它的实现原理是什么呢?

首先看下我们平时写的普通block代码:

block

当我们用clang重写命令重写该部分的时候,发现会转化成如下形式:

block

我们的block的底层实现其实也是runtime,在.cpp文件中,我们可以看到__block_impl,那么它又是什么呢?

struct __block_impl {

void *isa;

int Flags;

int Reserved;

void *FuncPtr;

};

block的实现就暂时给大家提到这里,更多细节请参考大神的文章

七、runtime拦截系统方法:

runtime拦截系统的方法要用到三个方法:

获取实例方法和类方法
交换两个方法

例如,苹果出的iPhoneX,有些全屏图片(例如App的引导页)需要UI切两套图,那么我们可以用拦截系统的[UIImage imageNamed]方法,实现两套图片的互切。

图片适配

点击这里,可以下载Demo

八、runtime动态添加方法

Objective-C是一门动态语言,基本上我们用到的控件、 数组、数据等都是以懒加载的形式加载的。在程序运行的时候才回去添加方法,动态添加的方法的作用就是去处理未实现的实例方法或者是类方法。只要我们调用了一个不存在的方法时,它就会动态添加方法。

class_addMethod

动态添加属性需要用到class_addMethod函数,在该函数中需要我们传入cls(类名)、name(方法)、imp(implementation,实现新方法的函数,它至少要传入两个参数:self和_cmd)、types(类型)。不懂得请进官方文档传送门

两个方法分别是检查类方法、实例方法是否实现

动态添加方法一般应用于:当我们想用一个类拥有一种新的方法,但是不知道类的内部实现的时候,我们可以添加分类,动态添加方法的功能实现。例如我给一个Game类动态添加方法。

动态添加方法

我们在函数调用:

这样就可以实现动态添加方法了

但是我们在应用的过程中会发现报了一个警告:PerformSelector may cause a leak because its selector is unknown。

消除警告的办法,将performSelector方法换成这种写法:((void (*)(id, SEL, NSString *))[game methodForSelector:sel])(game, sel, @"11212");

关于这种问题地详细原因以及解决办法,请参照大神博客。我的动态添加方法的Demo地址,请看点击这里下载吧

runtime这部分就先分享到这里,有关后续的归档解档,字典转模型,Method Swizzling,消息的转发,KVO的底层,Objective-C的引用计数实现等等后续再更新,如果笔者有理解或者书写错误的地方,欢迎指出,蟹蟹!!

非常感谢网上各位大神的文章:

Runtime全方位装逼指南

杨萧玉的Objective-C Runtime

推荐阅读更多精彩内容

  • 本文转载自:http://yulingtianxia.com/blog/2014/11/05/objective-...
    ant_flex阅读 518评论 0 1
  • 我们常常会听说 Objective-C 是一门动态语言,那么这个「动态」表现在哪呢?我想最主要的表现就是 Obje...
    Ethan_Struggle阅读 1,801评论 0 7
  • 本文详细整理了 Cocoa 的 Runtime 系统的知识,它使得 Objective-C 如虎添翼,具备了灵活的...
    lylaut阅读 654评论 0 4
  • 文中的实验代码我放在了这个项目中。 以下内容是我通过整理[这篇博客] (http://yulingtianxia....
    茗涙阅读 588评论 0 6
  • runtime 和 runloop 作为一个程序员进阶是必须的,也是非常重要的, 在面试过程中是经常会被问到的, ...
    SOI阅读 20,024评论 3 62
  • 铃声打响那一刻, 放下笔,走出那个考场—— 三年青春终结的地方, 走出考场的那天下午, 回想着在学校经历的那些时光...
    個人xi阅读 99评论 0 0
  • button.transform=CGAffineTransformRotate(button.transfor...
    开着保时捷堵你家门口阅读 901评论 0 0
  • 时光辗转千年; 榕树见了他的流连; 土地是树叶的归所; 我不曾别离; 大理石上的斑迹; 残留多少风雨; 我轻推门庭...
    墨上梢头阅读 59评论 0 1
  • 《素问·六节藏象论篇》曰:“肝者,罢极之本,魂之居也……通于春气。 中医素来有“春宜养肝”之说。明代医学家张景岳说...
    孙文竹阅读 2,461评论 0 18