开篇详谈:最近在做埋点需求,研究了下Runtime,本着研究学习的心态 忙里偷闲总结下,有不足之处,欢迎大佬指正 共同探讨。另外注明:原创,转载请注明,谢谢。
一、Runtime介绍
Runtime简称运行时。OC是运行时机制,也就是在运行时才做一些处理。
例如:C语言在编译的时候就知道要调用哪个方法函数,而OC在编译的时候并不知道要调用哪个方法函数,只有在运行的时候才知道调用的方法函数名称,来找到对应的方法函数进行调用。
OC中一切都被设计成了对象,我们都知道一个类被初始化成一个实例,这个实例是一个对象。实际上一个类本质上也是一个对象,在runtime中用结构体表示。
- 类在runtime中的表示
struct objc_class {
Class isa;//指针,顾名思义,表示是一个什么,
//实例的isa指向类对象,类对象的isa指向元类
#if !__OBJC2__
Class super_class; //指向父类
const char *name; //类名
long version;
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
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
- 方法调用在runtime中的流程(图解:业余画图,请轻喷!)
如果用实例对象调用实例方法,会到实例的isa指针指向的对象(也就是类对象)中操作。
如果调用的是类方法,就会到类对象的isa指针指向的对象(也就是元类对象)中操作。
![](https://img.haomeiwen.com/i4275143/fd044a36683c1186.png)
以上的过程给我带来的启发:
- 重写父类的方法,并没有覆盖掉父类的方法,只是在当前类对象中找到了这个方法后就不会再去父类中找了。
- 如果想调用已经重写过的方法的父类的实现,只需使用super这个编译器标识,它会在运行时跳过在当前的类对象中寻找方法的过程。
二、获取列表
开发过程中,可以通过runtime的一系列方法获取类的一些信息(包括属性列表,方法列表,成员变量列表,和遵循的协议列表)
unsigned int count;
//获取属性列表
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
for (unsigned int i = 0; i < count; i ++) {
const char *propertyName = property_getName(propertyList[I]);
NSLog(@"property---->%@", [NSString stringWithUTF8String:propertyName]);
}
//获取方法列表
Method *methodList = class_copyMethodList([self class], &count);
for (unsigned int i = 0; i < count; i ++) {
Method method = methodList[I];
NSLog(@"method---->%@", NSStringFromSelector(method_getName(method)));
}
//获取成员变量列表
Ivar *ivarList = class_copyIvarList([self class], &count);
for (unsigned int i = 0; i < count; i ++) {
Ivar myIvar = ivarList[I];
const char *ivarName = ivar_getName(myIvar);
NSLog(@"Ivar---->%@", [NSString stringWithUTF8String:ivarName]);
}
//获取协议列表
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
for (unsigned int i = 0; i < count; i ++) {
Protocol *myProtocal = protocolList[I];
const char *protocolName = protocol_getName(myProtocal);
NSLog(@"protocol---->%@", [NSString stringWithUTF8String:protocolName]);
}
三、拦截调用
在方法调用中说到了,如果没有找到方法就会转向拦截调用。
顾名思义,拦截调用就是,在找不到调用的方法程序崩溃之前,你有机会通过重写NSObject的四个方法来处理。
+ (BOOL)resolveClassMethod:(SEL)sel;
+ (BOOL)resolveInstanceMethod:(SEL)sel;
//后两个方法需要转发到其他的类处理
- (id)forwardingTargetForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
+ (BOOL)resolveClassMethod:(SEL)sel;
方法是当你调用一个不存在的类方法的时候,会调用这个方法,返回值查阅相关资料说默认返回NO,你可以加上自己的处理然后返回YES(小编尝试了YES和NO没有看出区别,有待进一步研究)
+ (BOOL)resolveInstanceMethod:(SEL)sel;
方法和+ (BOOL)resolveClassMethod:(SEL)sel;
方法相似,只不过处理的是实例方法。
- (id)forwardingTargetForSelector:(SEL)aSelector;
方法是将你调用的不存在的方法重定向到一个其他声明了这个方法的类,只需要你返回一个有这个方法的target。
- (void)forwardInvocation:(NSInvocation *)anInvocation;
方法是将你调用的不存在的方法打包成NSInvocation传给你。做完你自己的处理后,调用invokeWithTarget:方法让某个target触发这个方法。
四、动态添加方法
方法调用中没找到方法转向了拦截调用,然后在拦截调用要怎么处理才能使程序不crash呢?那就是根据传进来的SEL类型的selector动态添加一个方法,实现并执行。
首先从外部隐式调用一个不存在的方法:
//隐式调用方法
[target performSelector:@selector(resolveAdd:) withObject:@"test"];
然后,在target对象内部重写拦截调用的方法,动态添加方法
- (void)OCMethod:(NSStrng *)string
{
NSLog(@"添加OC方法成功:%@",string);
}
void runAddMethod(id self, SEL _cmd, NSString *string){
NSLog(@"添加C方法成功:%@", string);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
//给本类动态添加一个方法
if ([NSStringFromSelector(sel) isEqualToString:@"resolveAdd:"]) {
//OC的IMP方法
//class_addMethod(self, sel, [[self class] instanceMethodForSelector:@selector(OCMethod:)], "");
//C的IMP方法
class_addMethod(self, sel, (IMP)runAddMethod, "");
}
return YES;
}
其中class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) ;
的四个参数分别是:
Class _Nullable cls
给哪个类添加方法,本例中是self
SEL _Nonnull name
添加的方法,本例中是重写的拦截调用传进来的selector。
IMP _Nonnull imp
imp方法的实现,C方法的方法实现可以直接获得。如果是OC方法,可以用+ (IMP)instanceMethodForSelector:(SEL)aSelector;
获得方法的实现。(注:IMP指针是指向实现函数的指针,通过SEL取得IMP,objc_msgSend来执行实现方法)
const char * _Nullable types
方法的签名,代表有一个参数的方法。
五、关联对象
现在准备用一个系统的类,但是系统的类并不能满足你的需求,需要额外添加一个属性。这种情况的一般解决办法就是继承。
但是只增加一个属性,就去继承一个类,总是觉得太麻烦类。这个时候,runtime的关联属性就发挥它的作用了。
#import <Foundation/Foundation.h>
@interface NSObject (QFAddtions)
@property(nonatomic,copy)NSString *hotelName;
@end
#import "NSObject+ QFAddtions.h"
#import <objc/runtime.h>
static void* QFObjProKey = @"QFObjProKey";
@implementation NSObject(QFAddtions)
-(void)setHotelName:(NSString *)hotelName
{
//关联对象
objc_setAssociatedObject(self, &QFObjProKey, hotelName, OBJC_ASSOCIATION_COPY);
}
-(NSString *)hotelName
{
//获取关联对象
return objc_getAssociatedObject(self, &QFObjProKey);
}
@end
- 其中
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy)
四个参数分别是:
id _Nonnull object
给谁设置关联对象。
const void * _Nonnull key
关联对象唯一的key,获取时会用到。
id _Nullable value
关联对象。
objc_AssociationPolicy policy
关联策略,有以下几种策略:
OBJC_ASSOCIATION_ASSIGN; //assign策略
OBJC_ASSOCIATION_COPY_NONATOMIC; //copy,nonatomic策略
OBJC_ASSOCIATION_RETAIN_NONATOMIC; // retain,nonatomic策略
OBJC_ASSOCIATION_RETAIN //retain策略
OBJC_ASSOCIATION_COPY //copy策略
- 其中
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
两个参数分别是:
(id _Nonnull object
获取谁的关联对象。
const void * _Nonnull key
根据这个唯一的key获取关联对象。
六、方法交换
方法交换,顾名思义,就是将两个方法的实现交换,属于面向切面编程(Aspect-Oriented Programming)的一种实现
例如:将A方法和B方法交换,调用A方法的时候,就会执行B方法中的代码,反之亦然。
- 首先将方法交换的逻辑实现封装在
CQFRuntimeTool
工具类中方便使用
CQFRuntimeTool.h
#import <Foundation/Foundation.h>
@interface CQFRuntimeTool : NSObject
/**
方法交换:应用场景-->埋点
@param cls 类
@param originalSelector 原始方法
@param swizzledSelector 需要交换的方法
*/
+ (void)swizzlingInClass:(Class)cls originalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector;
@end
CQFRuntimeTool.m
#import "CQFRuntimeTool.h"
#import <objc/runtime.h>
@implementation CQFRuntimeTool
#pragma mark 交换方法
+ (void)swizzlingInClass:(Class)cls originalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector
{
Class class = cls;
Method originalMethod = class_getInstanceMethod(class, originalSelector);//原始方法
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);//交换方法
BOOL didAddMethod =
class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
@end
- 然后在UIViewController中调用
@implementation UIViewController ()
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originalSelector = @selector(viewWillAppear:);//原始方法
SEL swizzledSelector = @selector(swiz_viewWillAppear:);//交换方法
[CQFRuntimeTool swizzlingInClass:[self class] originalSelector:originalSelector swizzledSelector:swizzledSelector];
});
}
#pragma mark - Method Swizzling
- (void)swiz_viewWillAppear:(BOOL)animated
{
//插入需要执行的代码
NSLog(@"我在viewWillAppear执行前偷偷插入了一段代码");
//不能干扰原来的代码流程,插入代码结束后要让本来该执行的代码继续执行
[self swiz_viewWillAppear:animated];
}
@end
流程讲解:此处是将- (void)viewWillAppear:(BOOL)animated
与- (void)swiz_viewWillAppear:(BOOL)animated
方法进行交换
1.在- (void)viewWillAppear:(BOOL)animated
方法执行之前的方法中+ (void)load
方法中进行方法交换处理
2.当程序执行到- (void)viewWillAppear:(BOOL)animated
方法时,实际执行的是- (void)swiz_viewWillAppear:(BOOL)animated
方法,此时可以在该方法中插入自己的代码
3.然后在- (void)swiz_viewWillAppear:(BOOL)animated
方法中调用自己,这样根据交换方法的原理,程序会执行- (void)viewWillAppear:(BOOL)animated
方法,从未不影响整个程序的运行。
七、结语
以上就是小编对Runtime的一个大概了解。有理解不到位或者错误的地方,请大佬们指正哈。
- 喜欢本文可以点一下喜欢并关注我,或者留个言示个爱(抛媚眼中)
- 不喜欢可以留言提建议,我必虚心接受
- 欢迎转载
网友评论
https://www.jianshu.com/p/fbfbcb75dc4b