Runtime —— 从应用场景说起
根据平时遇到的情况,通过查资料和自己的理解,对Runtime黑科技进行一次个人的学习总结😄😄😄。
什么是Runtime
Runtime又叫做运行时,是底层C语言的API,是iOS内部核心之一。它是一门动态语言,它会将一些工作放在代码运行时才处理而并非编译时。这就说明我们不止需要编译器,还需要一个运行时系统来处理这些工作。
iOS本身又一个Runtime库,里面有很多函数可以直接调用,但是在使用时一定要记得导入头文件 #import<objc/runtime.h>
发送消息
OC中调用方法的本质是,发送消息。
对象发送消息,即调用objc_msgSend
方法,其原理是:对象根据方法编号SEL去映射查找对应的方法实现。
[p message];// 无参数
// 底层运行时会被编译器转化为:
objc_msgSend(p, selector)
[p message:(id)arg...];// 有参数
// 底层运行时会被编译器转化为:
objc_msgSend(p, selector, arg1, arg2, ...)
获取属性/方法/成员变量/协议等列表
就我所遇到过的需求有两种情况,是要拿到当前类中的每个属性的。其一就是字典转模型,所谓字典转模型,就是进行网络请求时,拿到服务器返回给你的数据(json/xml等格式),此时你要使用该数据需先将其转成集合类型,然后将集合转成模型集合(即为字典转模型),这个有很多写的很好的第三方库,如JSONModel/MJExtension等,可以去GitHub上下载看源码原理。其二就是对当前类的所有属性统一设置,我所在项目的封装了三层网络请求,其中网络请求参数是根据一个LXProject
类来设置的,所以提供project
单例方法,下面根据代码来说明需求和实现方法。
LXProject.m
// 单例方法
+ (instancetype)project
{
static LXProject *_instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [[LXProject alloc] init];
});
return _instance;
}
如下面网络请求,请求参数根据LXProject
的属性来设置
// 网络请求参数根据LXProject的属性来设置
LXProject *project = [LXProject project];
project.key = self.productName;
project.productStatus = self.productStatus;
project.page = @(self.page);
project.size = @10;
[LXHttpClientList request_ProductListProject:project andBlock:^(id data, NSError *error) {
if (data) {// 成功码为success返回data,否则上一层封装做了处理
// 拿到网络请求的数据,此处进行字典转模型
}
// error有值表示网络请求失败返回错误码,上一层封装做了处理
}];
这时候存在一个问题就是,当该请求未释放的时候,那么再进行一次新的请求时,上次请求设置的key/productStatus/page/size
是有值的。所以每次请求创建LXProject
对象,设置参数前,都需要清空LXProject
对象的属性。
改进的LXProject.m
+ (instancetype)project
{
static LXProject *_instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [[LXProject alloc] init];
});
// 清空project对象的属性
NSArray *names = [LXProject getProperties];// #import "NSObject+Extension.h" 使用分类
for (NSString *name in names) {
[_instance setValue:nil forKey:name];
}
return _instance;
}
NSObject+Extension.m
//返回当前类的所有属性
+ (NSArray *)getProperties
{
unsigned int count;
objc_property_t *properties = class_copyPropertyList(self, &count);
NSMutableArray *mArray = [NSMutableArray array];
for (int i = 0; i < count; i++) {
objc_property_t property = properties[i];
const char *cName = property_getName(property);
NSString *name = [NSString stringWithCString:cName encoding:NSUTF8StringEncoding];
[mArray addObject:name];
}
return mArray.copy;
}
附上所有Runtime获取的列表:
- (void)userRuntimeGetSomeList
{
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; i<count; i++) {
Method method = methodList[i];
NSLog(@"method---->%@", NSStringFromSelector(method_getName(method)));
}
//获取成员变量列表
Ivar *ivarList = class_copyIvarList([self class], &count);
for (unsigned int i; 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; i<count; i++) {
Protocol *myProtocal = protocolList[i];
const char *protocolName = protocol_getName(myProtocal);
NSLog(@"protocol---->%@", [NSString stringWithUTF8String:protocolName]);
}
}
关联对象
在项目种我们时常要使用某个系统类,然而系统类过于局限性,我们要添加某个属性。这时候想到的解决方法有:一、直接继承系统类;二、创建分类,Runtime关联对象动态添加属性。
OC中的Category无法向既有的类添加属性, 但是可以使用Runtime的关联对象(associated objects)来实现,所以我们都称呼Runtime为黑科技😄,因为它能实现常理不能实现的东西。
/**
设置关联对象
@param object 给谁设置关联对象
@param key 关联对象唯一的key,获取时会用到
@param value 关联对象
@param policy 关联策略,有以下几种策略
*/
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
/**
获取关联对象
@param object 谁的关联对象
@param key 根据这个唯一的key获取关联对象
*/
objc_getAssociatedObject(id object, const void *key)
//添加关联对象
//把getAssociatedObject方法的地址作为唯一的key
- (void)addAssociatedObject:(id)object{
objc_setAssociatedObject(self, @selector(getAssociatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
//获取关联对象
- (id)getAssociatedObject{
// _cmd代表当前调用方法的地址
return objc_getAssociatedObject(self, _cmd);
}
使用objc_removeAssociatedObjects
可断开所有关联, 把对象恢复至原始状态
动态添加方法
使用场景一:如果一个类的方法非常多,加载类到内存的时候也比较耗费资源,需要给每个方法生成映射表,可以使用Runtime动态添加方法解决。
使用场景二:比如会员机制,一部分对象能够调用某方法,这个方法可以用Runtime动态添加。
/**
动态添加方法
@param cls 给哪个类添加方法
@param name 添加的方法
@param imp 方法的实现,C方法的方法实现可以直接获得。如果是OC方法,可以用+ (IMP)instanceMethodForSelector:(SEL)aSelector;获得方法的实现
@param types 表示返回值和参数
*/
class_addMethod(__unsafe_unretained Class cls, SEL name, IMP imp, const char *types)
上面最后一个参数表示返回值和参数,详情参见苹果API符号表:Type Encodings
@implementation Person
/**
void(*)()
默认方法都有两个隐式参数,
默认一个方法都有两个参数,self,_cmd,隐式参数
self:方法调用者
_cmd:调用方法的编号
*/
void eat(id self,SEL sel)
{
NSLog(@"%@ %@",self,NSStringFromSelector(sel));
}
// 当一个对象调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来.
// 刚好可以用来判断,未实现的方法是不是我们想要动态添加的方法
<!--动态添加方法,首先实现这个resolveInstanceMethod-->
<!-- 调用时间:当调用了没有实现的方法没有实现就会调用-->
<!-- 作用:知道哪些方法没有实现,从而动态添加方法-->
<!-- sel:没有实现方法-->
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(eat)) {
// 动态添加eat方法
class_addMethod(self, @selector(eat), eat, "v@:");
}
return [super resolveInstanceMethod:sel];
}
@end
上面代码的resolveInstanceMethod
是拦截实例方法的调用,还有类似的系统方法resolveClassMethod
拦截类方法的调用。
method swizzling 交换方法
系统自带的方法功能不够,给系统自带的方法扩展一些功能,并且保持原有的功能。此时能想到的方法有:一、继承系统类,重写方法;二、使用Runtime交换方法
使用场景:
- 在
viewWillAppear:
和viewDidAppear:
等生命周期上添加POA点(对 App 的用户行为进行追踪和分析),Blog。 - 比如调用
imageNamed
时可以知道图片是否加载 - 在AFNetworking中也有应用,AFN中利用runtime将访问网络的方法做了替换,替换后可以监听网络连接状态
- 还有一些别的类似作用,比如我在网上看到的博文——使用runtime解决3D Touch导致UIImagePicker崩溃的问题
给viewWillAppear:
方法添加POA点
#import "UIViewController+swizzling.h"
#import <objc/runtime.h>
@implementation UIViewController (swizzling)
//load方法会在类第一次加载的时候被调用
+ (void)load{
//方法交换应该被保证,在程序中只会执行一次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//获得viewController的生命周期方法的selector
SEL systemSel = @selector(viewWillAppear:);
//自己实现的将要被交换的方法的selector
SEL swizzSel = @selector(swiz_viewWillAppear:);
//两个方法的Method
Method systemMethod = class_getInstanceMethod([self class], systemSel);
Method swizzMethod = class_getInstanceMethod([self class], swizzSel);
//首先动态添加方法,实现是被交换的方法,返回值表示添加成功还是失败
BOOL isAdd = class_addMethod(self, systemSel, method_getImplementation(swizzMethod), method_getTypeEncoding(swizzMethod));
if (isAdd) {
//如果成功,说明类中不存在这个方法的实现
//将被交换方法的实现替换到这个并不存在的实现
class_replaceMethod(self, swizzSel, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod));
}else{
//否则,交换两个方法的实现
method_exchangeImplementations(systemMethod, swizzMethod);
}
});
}
- (void)swiz_viewWillAppear:(BOOL)animated{
//这时候调用自己,看起来像是死循环
//但是其实自己的实现已经被替换了
[self swiz_viewWillAppear:animated];
// POA...
NSLog(@"method swizzle");
}
@end
此时在任何类中调用viewWillAppear:
都会先打印 "method swizzle"
补充:Runtime里面的load与initialize方法
在苹果API中的介绍如下:
+initialize
Initializes the class before it receives its first message.
// 初始化类之前收到第一个消息,即收到第一个消息的时候调用
+load
Invoked whenever a class or category is added to the Objective-C runtime; implement this method to perform class-specific behavior upon loading.
// 当一个类或类调用添加到Objective-C运行;实现这个方法来加载后执行特定类的行为
// 即在类第一次加载的时候被调用