ios运行时那些事
前言
什么是运行时(runtime)?
首先我们要先知道编程语言有静态和动态之分。所谓静态语言,就是在程序运行前决定了所有的类型判断,类的所有成员、方法在编译阶段就确定好了内存地址。也就意味着所有类对象只能访问属于自己的成员变量和方法,否则编译器直接报错。比较常见的静态的语言如:java,c++,c等等。
而动态语言,恰恰相反,类型的判断、类的成员变量、方法的内存地址都是在程序的运行阶段才最终确定,并且还能动态的添加成员变量和方法。也就意味着你调用一个不存在的方法时,编译也能通过,甚至一个对象它是什么类型并不是表面我们所看到的那样,只有运行之后才能决定其真正的类型。相比于静态语言,动态语言具有较高的灵活性和可订阅性。而oc,正是一门动态语言。
介绍到这里,我想可以解释一下运行时是什么了?所谓运行时,就是程序在运行时做的一些事。苹果提供了一套纯c语言的api,即runtime。在iOS开发中runtime的特性使得oc这门语言具有独特的魅力,我们可以利用运行时处理一些特殊的事情,甚至你可以轻松的玩出一些逼格很高的花样来。下面就开始一起进入运行时的世界吧。
在正式进入篇幅之前,首先声明一下,本编的主旨是简要阐述运行时的一些机制和原理,重点是讲述运行时的一些常用用法,不会去深入探究底层的C语言api。
要了解运行时,我们得先了解oc的消息机制
那么什么是消息机制?
在Objective-C中,任何方法的调用,本质是发送消息。比如我们下面方法:
[obj method];
编译器会自动转化为:
objc_msgSend(obj, @selector (method));
也就是说我们在oc中调用任何一个方法,其实质是转换为runtime中的一个函数objc_msgSend(),这个函数的作用是向obj对象(方法的调用者)发送了一条消息,告诉它你该去执行某个方法。
所以,我们其实也可以直接用运行时去调用你想要调用的任何一个可调用的方法:
如:
Dog *dog = [Dog alloc] init];
[dog run:100];
等价于:
Dog *dog = objc_msgSend(objc_getClass("Dog"), @selector(alloc));
dog = objc_msgSend(dog, sel_registerName("init"));
objc_msgSend(dog, sel_registerName("run:"),100); //调用带参数的方法
注:使用objc_msgSend()函数,须要先import <objc/message.h>
讲到这里,我们就可以说一说什么是oc消息机制,也就是一个方法的调用流程。
1、编译器会先将代码[obj method]转化为objc_msgSend(obj, @selector (method))函数去执行。
2、在objc_msgSend()函数中,首先通过obj的isa指针找到(对象)obj对应的(类)class。
3、在class中会先去cache中 通过SEL查找对应函数method(cache中method列表是以SEL为key通过hash表来存储的,这样能提高函数查找速度),若 cache中未找到。再去class中的消息列表methodList中查找,若methodlist中未找到,则取superClass中查找。若能找到,则将method加 入到cache中,以方便下次查找,并通过method中的函数指针跳转到对应的函数中去执行。
补充:
>在oc中,每一个对象都有一个isa指针变量,这个指针指向的是对象的类,我们可以通过isa指针访问一个对象的类
>方法都保存在类的消息列表中,这个列表其实是一个字典,key是selector,value是IMP(imp是一个指针类型,指向方法的实现),并且selector和IMP之间的关系是在运行时才决定的,而不是编译时。如此们就可以做出一些特别事情来。
我们可以用运行时做什么
1、互换方法的实现
上面说到selector和IMP之间的关系是在运行时才决定的,那我们是不是可以改变selector和IMP的对应关系呢?runtime就给我们提供了这么一个函数:
void method_exchangeImplementations(Method m1, Method m2)
我们可以通过此函数交换两个方法的实现,在开发中,可能我们会经常遇到一种场景,想为系统的某个方法增加一些特定的功能,又不想改变原有的东西,想要做到无缝衔接,用runtime方法互换无疑是最完美的。下面以交换系统的dealloc方法的实现为例:
首先建一个NSObject类目NSObject+ExchangeMethod,在类目中为NSObject类扩展一个my_dealloc方法用于替换系统的dealloc方法,其.m文件实现如下:
这样,当一个类的dealloc方法被调用时,会执行my_dealloc方法里的实现,完全无需再对原有的代码做任何改动
2、动态添加方法
前面有说到,动态语言调用一个没有的方法时,编译阶段也不不会报错。比如:
Dog *dog = [Dog alloc] init];
[dog performSelector:@selector(eat)]];
注:dog类中没有声明也没实现eat方法
上面代码,编译阶段肯定会通过,但程序一运行时便直接抛出异常闪退,抛出异常的打印闭着眼睛也知道是 :-[Dog eat]: unrecognized selector sent to instance 0x7fac91d0eba0'。这也印证了动态语言的方法需要在运行阶段才最终确定。
从而,我们可以动态的为某个类添加方法,而苹果performSelector:这个方法也很好的为我们逃过编译报错提供了支持。示例代码如下:
届时,我们在如上面调用[dog performSelector:@selector(eat)]]时,就会去执行test函数了。
3、动态添加属性
这也是runtime的一个重量级功能了,我们经常会想为系统的类或者一些不便修改的第三方框架的类增加一些自定义的属性以满足开发的需求。这个时候我们还是首先会想到类目,但是问题来了,类目只能为一个类添加方法,不能添加属性。
怎么做呢,还是用到运行时,为类动态添加属性。示例代码:为NSobject类添加一个字符串类型的属性: NSString *name
首先我们还是为NSobject建一个类目,其.h文件如下:
这里我们用property,类目中用 property会自动生成set/get的声明,但是没有实现,也无法生成下划线的成员变量,我们需要手动实现set、get方法。其.m文件如下:
4、获取类中所有的成员变量和属性
在开发中,你可能会遇到想要改变系统自带的类的某一个值,却找不与之对应的api,然后你就在那找瞎了眼,找呀找,始终找不到。这个时候我们可以确定一点,苹果系统自带的类有很多私有的属性或成员变量没有公开出来,也就意味着苹果它不想让我们访问。我靠,那还搞毛,有时需求来了,我还非要访问不可,那怎么办?
用运行时获取类的所有成员变量,即便私有的也能获取的到,用的函数如下:
Ivar *class_copyIvarList(Class cls, unsigned int *outCount)//获取类中所有的成员变量
objc_property_t class_getProperty(Class cls, const char *name)//获取类中所有的属性
代码示例如下:
使用运行时获取类中所有成员变量,还是相当有用的,比如现在一些字典转模型框架,它需要获取到模型的所有属性名,以这个属性名为key,取到字典中对应的value,然后通过kvc给这个属性赋设置值。再比如,你要设置系统UITextField控件placeholder的颜色,你会发现你翻遍api也找不到一个属性和方法来设置,这时你用运行时获取UITextField类所有成员变量,你会发现有一个_placeholderLabel成员变量,我们只需:
[self.textField setValue:[UIColor blueColor] forKeyPath:@"_placeholderLabel.textColor"];
总结
以上便是运行时的一些常用用法,本文仅抛砖引玉,在ios开发的道路上,想要深入了解oc这门语言,runtime是一餐不容错过的盛宴。