iOS Runtime理解与运用
ping怎么这么高?
哈哈,进入正题!
什么是Runtime?
这还要说?run( 运行)、time(时),runtime(运行时),没毛病!好了,我们都知道Objective-C是基于C衍生出来的动态语言,加入了面向对象特征和消息机制,这都归功于Runtime,它将静态语言在编译和链接时期做的事放到了运行时来处理。在我们Objective-C中,runtime是一个运行时库,是一套纯C的API。
-
面向对象,在OC中一切都被设计成对象,它们的基础数据结构在Runtime库中用C语言的结构体表示。
当一个类被初始化成一个实例,这个实例就是一个对象,在runtime中用objc_object
结构体表示,可以到objc/objc.h
查看定义
typedef struct objc_class *Class;
/// Represents an instance of a class.
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY; //结构体指针,指向类对象,这样,当我们向对象发送消息时,runtime库会根据这个isa指针找到对象所属的类,然后从类的方法列表及父类方法列表中查找消息对应的selector指向的函数实现,然后执行。
};
/// A pointer to an instance of a class.
typedef struct objc_object *id; //该类型对象可以转换成任意对象
当然类也是对象,由Class
类型表示,它实际上是一个指向 objc_class
结构体的指针,可以到objc/runtime.h
中查看定义
typedef struct objc_class *Class;
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY; //结构体的指针,每个对象都有一个isa指针,实例的isa指向类对象,类对象的isa指向元类。
#if !__OBJC2__
Class super_class //父类
const char *name //类名
long version //类的版本信息,默认为0
long info //类信息,提供一些标识
long instance_size //实例变量大小
struct objc_ivar_list *ivars //成员变量列表
struct objc_method_list **methodLists //方法列表
struct objc_cache *cache //方法缓存
struct objc_protocol_list *protocols //协议列表
#endif
}
上面引出一个元类(Meta Class):类对象的类,它储存着一个类所有的类方法,每个类都有一个单独的meta-class,因为每个类的类方法基本不可能完全相同,那么细想,元类也是有isa
指针的,它指向谁呢?为了不让这种结构无限延伸下去,isa
指向基类的meta-class,而基类的meta-class的isa
指针指向它自己。
-
消息机制,在OC中任何的方法调用,其本质都是消息发送,
id objc_msgSend(id self, SEL op, ...)
,属于动态调用的过程,比如[receiver doSomething]
,在运行时就会转成objc_msgSend(receiver,@selector(doSomething))
,receiver作为一个消息接收对象,@selector(doSomething)
是一个消息体,函数内部执行顺序:
- 检查消息对象是否为nil,如果是,则什么都不做。
- 通过receiver 的
isa
指针找到receiver对应的类,从类的方法缓存中通过SEL
查找IMP
,有,调用;没有,往下。(类的方法很多,如果每次都去方法列表中查找就会影响到效率,所以每一个类都会有一个方法缓存)。 - 从方法列表中查找,有,调用;没有,往下。
- 查找父类的方法缓存,有,直接调用;没有,往下。
- 查找父类的方法列表,有,直接调用;没有,往下,一直找到基类,以上就是一个正常的消息发送过程。
- 如果在基类也没有找到,则会调用NSObject的决议方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
或+ (BOOL)resolveClassMethod:(SEL)sel
,返回YES则重启一次消息的发送过程,返回NO则会进入消息转发。 - 调用
- (id)forwardingTargetForSelector:(SEL)aSelector
,如果实现了这个方法,并返回一个非nil的对象,则这个对象会作为消息的新接收者,且消息会被分发到这个对象,当然这个对象不能是self自身,否则就是出现无限循环;如果返回的是nil,往下继续。 - 调用
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
,生成一个方法签名,接着会创建一个NSInvocation(消息调用对象,包含target,selector,以及方法签名)
,并传给- (void)forwardInvocation:(NSInvocation *)anInvocation
,进行转发调用。
消息异常处理
当消息异常的时候,会执行方法决议以及消息转发,在上面的消息发送过程中也具体介绍了,这里借用一张图片来更好的理解
-
在这个过程中,我们可以在方法决议中添加方法实现并返回YES,来阻止crash
@implementation NSObject (ZMSafe) +(void)load{ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self zm_swizzleInstanceMethodWithSrcClass:[self class] srcSel:@selector(forwardInvocation:) swizzledSel:@selector(zm_forwardInvocation:)]; [self zm_swizzleInstanceMethodWithSrcClass:[self class] srcSel:@selector(methodSignatureForSelector:) swizzledSel:@selector(zm_methodSignatureForSelector:)]; [self zm_swizzleInstanceMethodWithSrcClass:[self class] srcSel:@selector(forwardingTargetForSelector:) swizzledSel:@selector(zm_forwardingTargetForSelector:)]; [self zm_swizzleClassMethodWithSrcClass:[self class] srcSel:@selector(resolveInstanceMethod:) swizzledSel:@selector(zm_resolveInstanceMethod:)]; }); } +(BOOL)zm_resolveInstanceMethod:(SEL)sel{ if(sel == NSSelectorFromString(@"push")){ NSLog(@"unrecognized selector -[%@ %@]\n%s",NSStringFromClass(self),NSStringFromSelector(sel),__FUNCTION__); /* //这是method的数据结构,在method其实就相当于在SEL跟IMP之间作了一个映射,有了SEL,我们便可以找到对应的IMP struct objc_method { SEL method_name //方法名 char *method_types //方法类型 IMP method_imp //实现地址 } */ Method method = class_getClassMethod([self class], @selector(empty)); // 获取函数类型,有没有返回参数,传入参数 const char *type = method_getTypeEncoding(method); // 添加方法,将未实现的方法编号sel跟自定义的方法实现imp关联 class_addMethod([self class], sel, method_getImplementation(method), type); // 返回YES,重启一次消息的发送过程,现在已经添加了方法实现empty,所以会直接调用它 return YES; } return [[self class]zm_resolveInstanceMethod:sel]; } - (void)empty{ NSLog(@"empty"); }
看调用结果,执行了决议方法和自定义的方法实现empty,并没有crash。
其实在这里还可以做很多的事情,比如版本的适配,在低版本中调用了高版本的方法,在这里就可以把方法名提取出来,再指向我们自定义的方法实现,等等。
-
也可以在
- (id)forwardingTargetForSelector:(SEL)aSelector
替换消息接收对象- (id)zm_forwardingTargetForSelector:(SEL)aSelector{ if(aSelector == NSSelectorFromString(@"push")){ NSLog(@"unrecognized selector -[%@ %@]\n%s",NSStringFromClass([self class]),NSStringFromSelector(aSelector),__FUNCTION__); // 我这里就直接动态创建一个类 Class ZMClass = objc_allocateClassPair([NSObject class], "ZMClass", 0); // 注册类 objc_registerClassPair(ZMClass); // 获取自定义empty方法 Method method = class_getClassMethod([self class], @selector(empty)); // 获取函数类型,有没有返回参数,传入参数 const char *type = method_getTypeEncoding(method); // 添加方法,将未实现的方法编号sel跟自定义的方法实现imp关联 class_addMethod(ZMClass, aSelector, method_getImplementation(method), type); // 返回该对象来接收消息 return [[ZMClass alloc]init]; } return [self zm_forwardingTargetForSelector:aSelector]; }
再看调用结果,效果是一样的,只是不同的处理方式而已,从打印上可以看出,这是在- (id)zm_forwardingTargetForSelector:(SEL)aSelector
中进行处理的,也是替换的- (id)forwardingTargetForSelector:(SEL)aSelector
方法,找到返回的备用对象去执行调用的方法。
-
或者在最后一步也就是消息真正转发的方法中做处理,重写
- (void)forwardInvocation:(NSInvocation *)anInvocation
,同时一定要重写- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
,因为anInvocation
对象是通过返回方法签名来创建的。/** 消息转发方法 @param anInvocation 消息转发对象 */ - (void)zm_forwardInvocation:(NSInvocation *)anInvocation{ NSLog(@"unrecognized selector -[%@ %@]\n%s",anInvocation.target,NSStringFromSelector([anInvocation selector]),__FUNCTION__); //如果自定义实现方法中什么都没做,只是为了能在运行时找到该实现方法,不至于crash,那么这里可以不进行消息发送,可以注释掉 if (![self respondsToSelector:anInvocation.selector]) { // 拿到方法对象 Method method = class_getClassMethod([self class], @selector(empty)); // 获取函数类型,有没有返回参数,传入参数 const char *type = method_getTypeEncoding(method); // 添加方法 class_addMethod([self class], anInvocation.selector, method_getImplementation(method), type); // 转发给自己,没毛病 [anInvocation invokeWithTarget:self]; } } /** 构造一个方法签名,提供给- (void)forwardInvocation:(NSInvocation *)anInvocation方法,如果aSelector没有对应的IMP,则会生成一个空的方法签名,最终导致程序报错崩溃,所以必须重写。 @param aSelector 方法编号 @return 方法签名 */ - (NSMethodSignature *)zm_methodSignatureForSelector:(SEL)aSelector { if ([self respondsToSelector:aSelector]) { // 如果能够响应则返回原始方法签名 return [self zm_methodSignatureForSelector:aSelector]; }else{ // 构造自定义方法的签名,不实现则返回nil,导致crash return [[self class] instanceMethodSignatureForSelector: @selector(empty)]; } }
调用结果也是一样的,这也是异常消息处理最后的机会,错过了就没机会了。
小结
这里主要是通过一个异常的消息来演示消息发送以及转发的过程,并在消息转发过程中对异常消息的捕捉及处理,我把这些写到NSObject的类目中主要为了防止开发中调用了不存在的方法导致的crash,当然如果在子类中重写了这些方法,可以调用super,也是一样的。
基础用法
-
对象关联,
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
,这也是我在实际开发中使用<objc/runtime.h>
的第一个API,方法的意思就是将两个不相关的对象通过一个特定的key关联起来,这样我们拿到对象object可以通过key找到对象Value,最具有代表性的运用就是给类目添加属性了。@interface NSObject (Property) @property (nonatomic,copy)NSString *text; @end @implementation NSObject (Property) // 手动构造Set方法,让text对象通过SEL指针跟self关联起来 - (void)setText:(NSString *)text{ objc_setAssociatedObject(self, @selector(setText:), text, OBJC_ASSOCIATION_COPY_NONATOMIC); } // 手动构造Get方法,通过SEL指针获取text对象 - (NSString *)text{ return objc_getAssociatedObject(self, @selector(setText:)); } // 移除该对象下所有关联的对象 - (void)removeProperty{ objc_removeAssociatedObjects(self); }
-
获取属性列表,
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
,cls表示获取该类的属性列表,outCount表示属性的总个数。
举栗:模型转字典
- (NSDictionary *)dictionary{NSMutableDictionary *dic = [NSMutableDictionary dictionary]; unsigned int count; objc_property_t *propertyList = class_copyPropertyList([self class], &count); for (int i = 0; i < count; i ++) { //生成key NSString *key = [NSString stringWithUTF8String:property_getName(propertyList[i])]; //获取value id value = [self valueForKey:key]; if (!value) break; [dic setObject:value forKey:key]; } free(propertyList); return dic; }
-
获取成员变量列表,
Ivar *class_copyIvarList(Class cls, unsigned int *outCount)
,跟获取属性列表一个意思,不同的是这里会获取该类所有的成员变量,当然其中也包括所有的属性。
举栗:NSCoding协议,我们想要把模型直接写成本地文件,是要实现编解码协议的,而且要一个一个的写,这里通过拿到属性列表来对所有属性来编解码,一劳永逸。
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self) {
unsigned int count;
Ivar *ivarList = class_copyIvarList([self class], &count);for (int i = 0; i < count; i ++) { //拿到成员变量 Ivar ivar = ivarList[i]; //生成key NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)]; //获取value id value = [aDecoder decodeObjectForKey:key]; [self setValue:value forKey:key]; } free(ivarList); } return self; } - (void)encodeWithCoder:(NSCoder *)aCoder{ unsigned int count; Ivar *ivarList = class_copyIvarList([self class], &count); for (int i = 0; i < count; i ++) { //拿到成员变量 Ivar ivar = ivarList[i]; //获取key NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)]; //获取value id value = [self valueForKey:key]; [aCoder encodeObject:value forKey:key]; } free(ivarList); }
-
获取方法列表,
Method *class_copyMethodList(Class cls, unsigned int *outCount)
,可以获取cls类的方法列表,包括私有方法,这样我们就可以调用对象的私有方法。
@interface Student : NSObject
@end
@implementation Student
- (void)study{NSLog(@"学习"); } - (void)goHome{ NSLog(@"回家"); } @end @interface ViewController () @end @implementation ViewController - (void)findStudentMethods{ Student *student = [[Student alloc]init]; unsigned int count; Method *methodList = class_copyMethodList([Student class], &count); for (int i = 0; i < count; i ++) { //获取方法对象 Method method = methodList[i]; //获取方法名 SEL sel = method_getName(method); NSLog(@"方法名:%@",NSStringFromSelector(sel)); if (sel == NSSelectorFromString(@"study")) { //通过NSInvocation来转发消息 NSMethodSignature *methodSign = [[Student class] instanceMethodSignatureForSelector:sel]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSign]; invocation.selector = sel; [invocation invokeWithTarget:student]; } } }
打印结果如我们所料,能够拿到所有的方法,也能调用私有方法。
- 动态添加方法,动态创建类,细心的会发现,我在上面第一二段代码就已经描述过了,这里也不在啰嗦了。
-
Method Swizzling,这个我也不在这里多说了,之前写过一篇关于
Method Swizzling
的介绍,iOS Method Swizzling理解与运用
总结
这里主要是写了自己对Runtime的理解,以及在平时开发中的运用。Runtime里面的API有很多,目前对它的理解以及运用程度有限,所以借此来抛砖引玉,同时有什么错误的地方,希望朋友们能够指出改正,谢谢。