iOS开发之Runtime常用示例总结

Teri3992 7年前
   <p>经常有小伙伴私下在Q上问一些关于 Runtime 的东西,问我有没有Runtime的相关文章,之前还真没正儿八经的总结过。之前只是在解析第三方框架源码时,聊过一些用法,也就是这些第三方框架中用到的Runtime。比如属性关联,动态获取属性等等。本篇博客就针对Runtime这个主题来总结一些其常用的一些方法,当然“空谈误国”,今天文章中所聊的Runtime依然要依托于本篇博客所涉及的Demo。</p>    <p>本篇文章所聊的Runtime的内容大概有: 动态获取类名 、 动态获取类的成员变量 、 动态获取类的属性列表 、 动态获取类的方法列表 、 动态获取类所遵循的协议列表 、 动态添加新的方法 、 类的实例方法实现的交换 、 动态属性关联 、 消息发送与消息转发机制 等。当然,本篇文章总结的是运行时常用的功能,并不是所有Runtime的内容。</p>    <h2>一、构建Runtime测试用例</h2>    <p>本篇博客的内容是依托于实例的,所以我们在本篇博客中先构建我们的测试类, Runtime 将会对该类进行相关的操作。下方就是本篇博客所涉及 Demo 的目录,上面的 RuntimeKit 类是讲 Runtime 常用的功能进行了简单的封装,而下方的 TestClass 以及相关的类目就是我们 Runtime 要操作的对象了。下方会对 TestClass 以及类目中的内容进行详细介绍。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/1af5b192b957f2d64f3c1a0a98369e32.png"></p>    <p>下方这几个截图就是我们的测试类 TestClass 的主要部分,因为 TestClass 是专门用来测试的类,所以其涉及的内容要尽量的全面。 TestClass 遵循了 NSCoding , NSCopying 这两个协议,并且为其添加了 公有属性 、 私有属性 、 私有成员变量 、 公有实例方法、私有实例方法、类方法等。这些添加的内容,都将是我们Runtime的操作对象。下方那几个TestClass的类目稍后在使用Runtime时再进行介绍。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/5bde6ccc17fc83080475549894336e04.png"></p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/edcd1de4819a803280f16fc6729c6662.png"></p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/ac916b7b150c915bb8f1277e9cbb8e48.png"></p>    <h2>二、RuntimeKit的封装</h2>    <p>接下来我们就来看看 RuntimeKit 中的内容,其中对 Runtime 常用的方法进行了简单的封装。主要是动态的获取类的一些属性和方法的,以及动态方法添加和方法交换的。本部分的干货还是不少的。</p>    <h3>1、获取类名</h3>    <p>动态的获取类名是比较简单的,使用 class_getName(Class) 就可以在运行时来获取类的名称。 class_getName() 函数返回的是一个 char类型的指针 ,也就是C语言的字符串类型,所以我们要将其转换成NSString类型,然后再返回出去。下方的 +fetchClassName:方法 就是我们封装的获取类名的方法,如下所示:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/7b7c0a7841e4fdd10084f88ddceca13c.png"></p>    <h3>2、获取成员变量</h3>    <p>下方这个 +fetchIvarList: 这个方法就是我们封装的获取类的成员变量的方法。当然我们在获取成员变量时,可以用 ivar_getTypeEncoding() 来获取相应成员变量的类型。使用 ivar_getName() 来获取相应成员变量的名称。下方就是对获取成员变量的功能的封装。返回的是一个数组,数组的元素是一个字典,而字典中存储的就是相应成员变量的名称和类型。</p>    <p><img src="https://simg.open-open.com/show/1dedb2c573be2a28ba601c090b89c9fa.png"></p>    <p>下方就是调用上述方法获取的 TestClass 类的成员变量。当然在运行时就没有什么私有和公有之分了,只要是成员变量就可以获取到。在OC中的给类添加成员属性其实就是添加了一个成员变量和 getter 以及 setter 方法。所以获取的成员列表中肯定带有成员属性,不过成员属性的名称前方添加了下划线来与成员属性进行区分。我们也可以获取成员变量的类型,下方的 _var1 是 NSInteger 类型,动态获取到的是q字母,其实是 NSInteger 的符号。 而i就表示int类型 , c表示Bool类型 , d表示double类型 , f则就表示float类型 。当然这些基本类型都是由一个字母代替的,如果是引用类型的话,则直接就是一个字符串了,比如 NSArray类型就是"@NSArray" 。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/9669e9b62cce652d354abe7d65dea30c.png"></p>    <h3>3.获取成员属性</h3>    <p>上面获取的是类的成员变量,那么下方这个 +fetchPropertyList: 获取的就是成员属性。当然此刻获取的只包括成员属性,也就是那些有setter或者getter方法的成员变量。下方主要是使用了 class_copyPropertyList(Class,&count) 来获取的属性列表,然后通过for循环通过 property_getName() 来获取每个属性的名字。当然使用 property_getName() 获取到的名字依然是C语言的char类型的指针,所以我们还需要将其转换成NSString类型,然后放到数组中一并返回。如下所示:</p>    <p><img src="https://simg.open-open.com/show/bbf1e9b1d4948526e345218a0ec11acd.png"></p>    <p>下方这个截图就是调用上述方法获取的TestClass的所有的属性,当然 dynamicAddProperty 是我们使用 Runtime 动态给 TestClass 添加的,所以也是可以获取到的。当然我们获取到的属性的名称为了与其对应的成员变量进行区分,成员属性的名字前边是没有下划线的。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/cc9bc4b3ce2564e1ea90ba0f56aa0157.png"></p>    <h3>4、获取类的实例方法</h3>    <p>接下来我们就来封装一下获取类的实例方法列表的功能,下方这个 +fetchMethodList: 就是我们封装的获取类的实例方法列表的函数。在下方函数中,通过 class_copyMethodList() 方法获取类的实例方法列表,然后通过for循环使用 method_getName() 来获取每个方法的名称,然后将方法的名称转换成NSString类型,存储到数组中一并返回。具体代码如下所示:</p>    <p><img src="https://simg.open-open.com/show/68f514b4fdbcf32dcf0165636f3665a7.png"></p>    <p>下方这个截图就是上述方法在TestClass上运行的结果,其中打印了 TestClass 类的所有实例方法,当然其中也必须得包含成员属性的getter和setter方法。当然TestClass类目中的方法也是必须能获取到的。结果如下所示:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/111dafae8cf7f2d2ea93f9a9d9f62475.png"></p>    <h3>5、获取协议列表</h3>    <p>下方是获取我们类所遵循协议列表的方法,主要使用了 class_copyProtocolList() 来获取列表,然后通过for循序使用 protocol_getName() 来获取协议的名称,最后将其转换成NSString类型放入数组中返回即可。</p>    <p><img src="https://simg.open-open.com/show/dee221c5021a52bbf65a68eceac89424.png"></p>    <p>下方就是我们获取到的TestClass类所遵循的协议列表:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/b5e1230fcee097400dc60b087b072fc0.png"></p>    <h3>6、动态添加方法实现</h3>    <p>下方就是动态的往相应类上添加方法以及实现。下方的 +addMethod方法 有三个参数,第一个参数是要添加方法的类,第二个参数是方法的SEL,第三个参数则是提供方法实现的 SEL 。稍后在消息发送和消息转发时会用到下方的方法。下方主要是使用 class_getInstanceMethod() 和 method_getImplementation() 这两个方法相结合获取相应 SEL 的方法实现。下方的 IMP 其实就是 Implementation 的方法缩写,获取到相应的方法实现后,然后再调用 class_addMethod() 方法将 IMP与SEL进行绑定即可 。具体做法如下所示。</p>    <p><img src="https://simg.open-open.com/show/949874ae875d2b2e6f996701159a41cd.png"></p>    <h3>7、方法实现交换</h3>    <p>下方就是讲类的两个方法的实现进行交换。如果将 MethodA 与 MethodB 的方法实现进行交换的话,调用 MethodA 时就会执行 MethodB 的内容,反之亦然。</p>    <p><img src="https://simg.open-open.com/show/f309d14f0c5851a00c2cc0c29d1c4619.png"></p>    <p>下方这段代码就是对上述方法的测试。下方是TestClass的一个类目,在该类目中将类目中的方法与TestClass中的方法进行了替换。也就是将method1与method2进行了替换,替换后在method2中调用的method2其实就是调用的method1。在第三方库中,经常会使用该特性,已达到AOP编程的目的。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/dfd985c80beb32efb16a3815f6c6a183.png"></p>    <h2>三、属性关联</h2>    <p>属性关联说白了就是在类目中动态的为我们的类添加相应的属性,如果看过之前发布的对 Masonry 框架源码解析的博客的话,对下方的属性关联并不陌生。在 Masonry 框架中就利用 Runtime 的属性关联在 UIView 的类目中给 UIView 添加了一个约束数组,用来记录添加在当前 View 上的所有约束。下方就是在 TestClass 的类目中通过 objc_getAssociatedObject() 和 objc_setAssociatedObject() 两个方法为 TestClass 类添加了一个 dynamicAddProperty 属性。上面我们获取到的属性列表中就含有该动态添加的成员属性。</p>    <p>下方就是属性关联的具体代码,如下所示。</p>    <p><img src="https://simg.open-open.com/show/3b95eab3a09ef3c3ffff9d0a5b59e36d.png"></p>    <h2>四、消息处理与消息转发</h2>    <p>在Runtime中不得不提的就是OC的消息处理和消息转发机制。当然网上也有不少相关资料,本篇博客为了完整性,还是要聊一下消息处理与消息转发的。 当你调用一个类的方法时,先在本类中的方法缓存列表中进行查询,如果在缓存列表中找到了该方法的实现,就执行,如果找不到就在本类中的方列表中进行查找。在本类方列表中查找到相应的方法实现后就进行调用,如果没找到,就去父类中进行查找。如果在父类中的方法列表中找到了相应方法的实现,那么就执行,否则就执行下方的几步 。</p>    <p>当调用一个方法在缓存列表,本类中的方法列表以及父类的方法列表找不到相应的实现时,到程序崩溃阶段中间还会有几步让你来挽救。接下来就来看看这几步该怎么走。</p>    <h3>1.消息处理(Resolve Method)</h3>    <p>当在相应的类以及父类中找不到类方法实现时会执行 +resolveInstanceMethod: 这个类方法。该方法如果在类中不被重写的话,默认返回NO。如果返回NO就表明不做任何处理,走下一步。如果返回 YES 的话,就说明在该方法中对这个找不到实现的方法进行了处理。在该方法中,我们可以为找不到实现的 SEL 动态的添加一个方法实现,添加完毕后,就会执行我们添加的方法实现。这样,当一个类调用不存在的方法时,就不会崩溃了。具体做法如下所示:</p>    <p><img src="https://simg.open-open.com/show/ba96262c78c3f7d00ad0e06b1b1f8703.png"></p>    <h3>2、消息快速转发</h3>    <p>如果不对上述消息进行处理的话,也就是 +resolveInstanceMethod:返回NO 时,会走下一步消息转发,即 -forwardingTargetForSelector: 。该方法会返回一个类的对象,这个类的对象有SEL对应的实现,当调用这个找不到的方法时,就会被转发到SecondClass中去进行处理。这也就是所谓的消息转发。当该方法返回self或者nil, 说明不对相应的方法进行转发,那么就该走下一步了。</p>    <p><img src="https://simg.open-open.com/show/a1183232a9fd102ad4533ad475e6732e.png"></p>    <h3>3.消息常规转发</h3>    <p>如果不将消息转发给其他类的对象,那么就只能自己进行处理了。如果上述方法返回self的话,会执行 -methodSignatureForSelector: 方法来获取方法的参数以及返回数据类型,也就是说该方法获取的是方法的签名并返回。如果上述方法返回nil的话,那么消息转发就结束,程序崩溃,报出找不到相应的方法实现的崩溃信息。</p>    <p>在 +resolveInstanceMethod: 返回NO时就会执行下方的方法,下方也是讲该方法转发给 SecondClass ,如下所示:</p>    <p><img src="https://simg.open-open.com/show/52c4abf11681374a335cd100e7c0eca5.png"></p>    <p>今天的博客就先到这儿吧,当然还有其他一些 Runtime 的东西本篇博客并未涉及,如果以后解析那个开源库的源码时遇到了,我们在单独聊。依照惯例,本篇博客依附的Demo, 仍然会在 Github 上进行分享,下方是分享链接。</p>    <p> </p>    <p>来自:http://www.cnblogs.com/ludashi/p/6294112.html</p>    <p> </p>