Objective-C是一门动态语言,不同于许多静态语言,例如C语言,只能在编译和链接阶段把程序运行的上下文做好,在运行期间,无法修改,缺少动态性。Objective-C的动态性,给开发者提供了一种在运行期,修改程序执行流程的机会,这要归功于其强大的Runtime机制。
这篇文章主要介绍,目前,Runtime机制在我们项目中的应用场景。
- 前言
ObjC语言中,runtime运行机制主要依赖于两个头文件
#include <objc/runtime.h>
#include <objc/message.h>
其中runtime.h声明了一些类,实例变量操作相关的东西,message.h声明了一些消息发送相关的东西。
下面列举一下,runtime.h中常用的方法,以及它们的用法。
object_getClass: 获取一个对象所属的类
object_getIvar: 读取一个对象中某个变量的值
objc_getClass: 通过字符串获取到类Class
class_getSuperclass:获取一个类的父类
class_getInstanceMethod:获取实例方法,返回Method
class_getMethodImplementation:获取实例方法的实现
class_getProperty: 获取某个类的属性
class_addMethod: 增加方法。
...等等
上面大部分列举了get相关的方法。runtime.h提供了获取(get),设置(set)(类的属性,实例变量,实例方法,类方法)的操作。
下面列举一下message.h的相关方法。
objc_msgSend: 给对象发送消息。
objc_msgSendSuper: 向父类的发送消息
message.h类,主要提供了这两个关键的方法。
- 下面总结一下Runtime有哪些使用场景,并通过具体的代码,来说明各种使用场景如何运用Runtime。
总结了如下四种应用场景:
1.动态拼接URL
2.交换两个方法的实现(用自己的方法,替换系统的方法)
3.给系统已有的方法添加新的功能(不影响系统方法原来的功能)
4.消息转发中Runtime的应用
下面结合项目中使用到Runtime的地方进行说明,并列举了一些代码示例。
第一个使用场景:动态拼接URL
在做app开发的时候,肯定会遇到http请求,URL拼接的问题。初期大部分采取的方案是,采用NSString的格式化方法,自己手动去拼接URL. 例如:
NSMutableString *stringUrl = [NSMutableString stringWithFormat:@"http:mytest/host/code=%@",code];
[stringUrl appendString:@"&uin=45"];
[stringUrl appendString:@"&my=5"];
[stringUrl appendString:@"&your=6"];
这样写有几个问题:
1. 手动书写,很容易写错。
2. 如果很多类似的请求,都需要uin或者其他通用的参数,那么就会每个请求都需要写一遍。不满足OOP的特性。
3. 参数所代表的含义不够清晰,需要开发去猜测。
解决方法:
把url拼接,抽象成一个类。
上面的参数code, uin,my,your可以当成类的属性。通用的属性有uin,可以抽象出一个基类。实现方法如下:
CURLParamBase类
@interface CURLParamBase : NSObject
@property (nonatomic, strong) NSString *uin;
@end
需要构造的请求类XXX,如下定义:
@interface CURLParamXXX : CURLParamBase
@property (nonatomic, strong) NSString* my;
@property (nonatomic, strong) NSString* your;
@end
使用:定义CURLParamXXX的类的实例,使用Runtime来获取属性名和属性的value来拼接参数,如下:
unsigned int propertyCount;
//获取所有属性
objc_property_t *properties =
class_copyPropertyList(class, &propertyCount);
for (unsigned int i = 0; i < propertyCount; i++)
{
objc_property_t property = properties[i];
const char *propertyName = property_getName(property);
//获取实例变量,某个属性的值
object_getInstanceVariable(self,propertyName,&value);
}
-第二个使用场景:交换两个方法的实现
使用newSEL方法名,新的newIMP实现,替换原有的origSEL方法及其实现。
实现代码如下,代码中添加了相关的注释。
/**
**新方法的实现替换旧方法
**成功:返回YES,
**失败:返回NO.
*/
BOOL replaceMethodNewImpl(Class c, SEL origSEL, SEL newSEL, IMP newIMP)
{
//新方法已经存在
if ([c instancesRespondToSelector:newSEL])
{
return YES;
}
//旧方法
Method origMethod = class_getInstanceMethod(c, origSEL);
//先把新方法加入到class的方法列表中
if (!class_addMethod(c, newSEL, newIMP, method_getTypeEncoding(origMethod)))
{
//如果加入失败,直接返回
NSLog(@"Failed to add method:%@ on %@",NSStringFromSelector(newSEL),c);
return NO;
}
else
{
//如果加入成功
Method newMethod = class_getInstanceMethod(c, newSEL);
//给original添加新的实现
//有可能失败失败原因:(for example, the class already contains a method implementation with that name).
if (class_addMethod(c, origSEL, method_getImplementation(newMethod), method_getTypeEncoding(origMethod)))
{
//新方法的实现替换成旧方法的实现。
class_replaceMethod(c, newSEL, method_getImplementation(origMethod), method_getTypeEncoding(newMethod));
}
else
{
//交换实现
method_exchangeImplementations(origMethod, newMethod);
}
}
return YES;
}
- 第三个使用场景:给系统已有的方法添加功能
例如给系统的UIView下的方法drawRect,添加NSLog的功能,这个可以参考上面说明的第三种使用场景。变化的只是,新方法的实现要去调用老的方法的实现。在override_drawRect方法中,调用drawRect方法。代码如下:
- (void)override_drawRect:(CGRect)r
{
// 调用旧的实现。因为它们已经被替换了
[self override_drawRect: r];
NSLog(@"rect = %@",NSStringFromCGRect(r));
}
- 第四使用场景:消息转发
开发过程中,经常遇到unrecognized selector sent to instance 0x87 Terminating app due to uncaught exception
NSInvalidArgumentException’, 这个问题。
这是什么原因?直观上看,是系统没有处理某个消息。
情况分两种,第一,接受消息的对象错了。第二,对象没错,发送的消息不对。
消息转发的整个流程如下图所示:
总体来说,就是给某个实例,发送某个消息。
首先如果没找到响应方法,系统会给你转到其他方法的机会,只要实现了resolveInstanceMethod方法即可。
其次,如果没有实现,你还可以修改接受消息的对象,让其他对象去响应消息,覆盖方法forwardingTargetSelector即可。
如果这两者都没做,你还可以在forwardInvocation,做自己的逻辑处理,是否继续处理消息。
这篇文章不具体讨论这个转发流程的细节,只是为了说明runtime在整个过程中的运用。
Runtime在其中的运用。
在resolveInstanceMethod方法中,可以通过覆盖resolveInstanceMethod方法,如下:
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSString *selectorString = NSStringFromSelector(sel);
if ([selectorString hasPrefix:@"set"]) {
class_addMethod(self, sel, (IMP)mySetMethod, "v@:@");
} else {
class_addMethod(self, sel, (IMP)myGetMethod, "@@:");
}
return YES;
}
代码中,本类实现了动态的添加选择子,把选择子关联到自己定义的方法上,这样在访问某个属性的时候,会动态的访问我们自己定义的方法。在消息转发的流程中,实现了动态添加方法实现的能力。
综上所述,Runtime提供了Objective-C强大的动态性,可谓是方便灵活,运用起来能做很多事情,也欢迎大家补充说明在你们的项目中,Runtime都为你们做了什么。
(完)