今天来总结运行时(Runtime)的相关知识。有的人总是说runtime我没用过啊,那为什么面试的时候经常要问。其实在项目里你肯定用到了,只是你没注意。看了好多博客文章,都是先说的原理,但是原理好多人一看一大堆直接就不想看了,今天我们先来看看runtime的有什么用。它对我们的开发又有哪些帮助呢。怎么用它
- 关联对象(Objective-C Associated Objects)给分类增加属性
- Method Swizzling方法添加和替换 KVO实现
- 消息转发(热更新)解决Bug
- 实现NSCoding的自动归档和自动解档
- 实现字典和模形的自动转换
一、Runtime的使用
关联对象添加属性
//关联对象
//objc 被关联的对象
//key 关联的key 要求唯一
//value关联的对象
//policy 内存管理的策略
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
//获取关联的对象
id objc_getAssociatedObject(id object, const void *key)
//移除关联的对象
void objc_removeAssociatedObjects(id object)
上面说了方法和参数,下面来使用
#import <UIKit/UIKit.h>
#import <objc/runtime.h>
NS_ASSUME_NONNULL_BEGIN
@interface UIView (DefaultColor)
@property (nonatomic, strong) UIColor *defaultColor;
@end
NS_ASSUME_NONNULL_END
@implementation UIView (DefaultColor)
//@dynamic defaultColor;
static char kDefaultColorKey;
-(void)setDefaultColor:(UIColor *)defaultColor{
objc_setAssociatedObject(self, &kDefaultColorKey, defaultColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(UIColor *)defaultColor{
return objc_getAssociatedObject(self, &kDefaultColorKey);
}
@end
在set方法中关联属性,get方法中获取关联的对象。为了验证是否添加成功,下来调用一下
UIView *firstView=[UIView new];
firstView.defaultColor=[UIColor redColor];
NSLog(@"默认颜色是:%@",firstView.defaultColor);
RuntimeDemo[33582:12690582] 默认颜色是:UIExtendedSRGBColorSpace 1 0 0 1
成功的在分类上添加了一个属性,通过关联对象实现的内存管理是由ARC管理的,所以只需要内定合适的内存策略,就不用担心对象的释放。
二 动态方法交换
//cls 获取方法的类
//name 方法的名称SEL
//获取类方法的Mthod
Method _Nullable class_getClassMethod(Class _Nullable cls, SEL _Nonnull name)
//获取实例对象方法的Mthod
Method _Nullable class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)
//交换两个方法的实现
void method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
1.动态方法交换
下面来看看具体的实现过程
[self testA];
[self testB];
}
-(void)testA{
NSLog(@"测试A");
}
-(void)testB{
NSLog(@"测试B");
}
-(IBAction)btnclick:(UIButton *)sender{
Method test1=class_getInstanceMethod([self class], @selector(testA));
Method test2=class_getInstanceMethod([self class], @selector(testB));
method_exchangeImplementations(test1, test2);
[self testA];
[self testB];
2021-03-15 15:01:31.348613+0800 RuntimeDemo[33614:12699375] 测试A
2021-03-15 15:01:31.348648+0800 RuntimeDemo[33614:12699375] 测试B
2021-03-15 15:01:46.393346+0800 RuntimeDemo[33614:12699375] 测试B
2021-03-15 15:01:46.393598+0800 RuntimeDemo[33614:12699375] 测试A
在最初调用的顺序来看,是测试A--测试B,之后点击按钮完成方法的交换,最后打印出测试B--测试A,交换成功。
上面是一个简单的方法交换,那么对于系统的方法又是怎么去替换的呢?
拦截并替换系统方法
#import "UIFont+Test.h"
#import <objc/runtime.h>
@implementation UIFont (Test)
+(UIFont *)test_systemFontOfSize:(CGFloat)fontSize{
//获取设备屏幕宽度,并计算出比例scale
CGFloat width = [[UIScreen mainScreen] bounds].size.width;
CGFloat scale = width/375.0;
//注意:由于方法交换,系统的方法名已变成了自定义的方法名,所以这里使用了
//自定义的方法名来获取UIFont
return [UIFont test_systemFontOfSize:fontSize * scale];
}
+(void)load{
Method method1=class_getClassMethod([UIFont class], @selector(systemFontOfSize:));
Method method2=class_getClassMethod([UIFont class], @selector(test_systemFontOfSize:));
method_exchangeImplementations(method1, method2);
}
创建了UIFont的一个分类,并且拦截替换了系统的systemFontOfSize方法。当在调用systemFontOfSize时,可以看到效果是test_systemFontOfSize。
当然也可以拦截替换viewDidLoad。
kvo的实现我们放到后面仔细的说一说原理
实现NSCoding的自动归档和解档
归档过程中model中有超级多的属性是时一个一个处理起来很麻烦,这个时候就可以使用Runtim来改进他们
//归档
-(void)encodeWithCoder:(NSCoder *)coder{
unsigned int count=0;
Ivar *varlist=class_copyIvarList([self class], &count);
for (NSInteger i=0; i<count; i++) {
Ivar ivar=varlist[i];
NSString *key=[NSString stringWithUTF8String:ivar_getName(ivar)];
id value=[self valueForKey:key];
[coder encodeObject:value forKey:key];
}
free(varlist);
}
-(instancetype)initWithCoder:(NSCoder *)coder{
self=[super init];
if (self) {
unsigned int count=0;
Ivar *varlist=class_copyIvarList([self class], &count);
for (NSInteger i=0; i<count; i++) {
Ivar ivar=varlist[i];
const char *name = ivar_getName(ivar);
NSString *key=[NSString stringWithUTF8String:name];
id value = [coder decodeObjectForKey:key];
[self setValue:value forKey:key];
}
free(varlist);
}
return self;
}
通过Runtime我们拿到类的属性列表,遍历列表来归档和解档。
StudetModel *stModel=[[StudetModel alloc]init];
stModel.name=@"小李子";
stModel.age=@"18";
stModel.number=@"9527";
stModel.score=@"598";
NSString *temp=NSTemporaryDirectory();
NSString *file=[temp stringByAppendingString:@"student.data"];
[NSKeyedArchiver archiveRootObject:stModel toFile:file];
StudetModel *model=[NSKeyedUnarchiver unarchiveObjectWithFile:file];
NSLog(@"person-name:%@,person-age:%@",model.name,model.age);
打印出:2021-03-15 16:43:38.446580+0800 RuntimeDemo[33700:12732235] person-name:小李子,person-age:18
实现字典和模型的自动转换(MJExtension)
- (instancetype)initWithDict:(NSDictionary *)dict {
if (self = [self init]) {
//(1)获取类的属性及属性对应的类型
NSMutableArray * keys = [NSMutableArray array];
NSMutableArray * attributes = [NSMutableArray array];
/*
* 例子
* name = value3 attribute = T@"NSString",C,N,V_value3
* name = value4 attribute = T^i,N,V_value4
*/
unsigned int outCount;
objc_property_t * properties = class_copyPropertyList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
objc_property_t property = properties[i];
//通过property_getName函数获得属性的名字
NSString * propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
[keys addObject:propertyName];
//通过property_getAttributes函数可以获得属性的名字和@encode编码
NSString * propertyAttribute = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];
[attributes addObject:propertyAttribute];
}
//立即释放properties指向的内存
free(properties);
//(2)根据类型给属性赋值
for (NSString * key in keys) {
if ([dict valueForKey:key] == nil) continue;
[self setValue:[dict valueForKey:key] forKey:key];
}
}
return self;
}
以上就是runtime的一些简单应用场景
Runtime消息传递原理
先来看看
PeopleOBJC *people=[[PeopleOBJC alloc]init];
[people testRuntime:@"888"];
上面这段代码它的实现原理是什么呢?
个人把它分为了3个阶段
定位方法
在这之前先要明白2个概念
1.Class
class被定义为指向objc_class的指针
struct objc_class {
Class _Nonnull isa //指向元类
#if !__OBJC2__
Class _Nullable super_class //父类
const char * _Nonnull name //类名
long version //类的版本信息
long info //类信息
long instance_size //该类的大小
struct objc_ivar_list * _Nullable ivars //类的成员变量链表
struct objc_method_list * _Nullable * _Nullable methodLists //方法定义链表
struct objc_cache * _Nonnull cache //方法缓存
struct objc_protocol_list * _Nullable protocols //协议链表
#endif
} OBJC2_UNAVAILABLE;
isa指向元类,后面在详细的说。可以看到,一个勒种保存了所有的成员变量(ivar)、所有的方法(method_list)、所有实现的协议(protocol_list).cache 我们后面在说。下面来看对象的定义
struct objc_object {
Class isa;
};
typedef struct objc_object *id;
这里id被定义指向了objc_object的指针,说明objc_object就是我们常用对象的定义。一个对象唯一保存的信息是它的class的地址。当我们调用一个对象的方法时,它会通过对象的isa找到对应的objc_class。
2.元类
在上面我们看了objc_class,我们把成员变量 方法列表统统去掉,发现没,是不是和objc_object。这说明objc_class不仅是个类,它同时也是一个对象
那么问题来了,它指向那个类了?
这时候就引出了Meta Class(元类)。
每一个类都有对应的元类,而在元类的methodLists 中,保存了类的方法列表。isa指针指向对应的元类。
那么方法的定位就可以变成
1.对象的isa找到对应的类
2.然后通过类的isa找到了对应的元类
3.在元类的methodLists 中,找到对应的方法。
元类也是objc_class,那它应该也对应一个对象的啊,按这么弄下去,无限循环没个头啊,所以元类的isa指向的是基类的元类。而基类的元类的 isa 指向自己。这样就形成了一个完美的闭环。
然后下面这个比较常见的图片也就能理解了
方法的实现
既然找到了方法,我们就来看看方法是怎么实现的。先来看看几个定义
typedef struct objc_method *Method;
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
}
Method定义了一个objc_method指针,而在objc_method中定义了SEL和imp。那么问题来了,这两又是什么玩意?
其实在我们平常的项目中绝对见过SEL
SEL sel = @selector(viewDidLoad);
NSLog(@"%s", sel); // 输出:viewDidLoad
打印出来的是viewDidLoad,这说明SEL它只是保存了一个方法名的字符串。
这是不是也就解释了为什么在同一个类中,不能取相同的名字。即使它们的参数类型不同,也不能行。
2.imp
// IMP
typedef id (*IMP)(id, SEL, ...);
可以看到它是一个函数指针,它就是函数的地址。imp中有两个参数,第一个参数id就是当前对象的地址。第二个参数SEL 就是方法名
这么一看,Method的机构就很明了了。它建立了SEL和IMP的关联,当对一个对象发送消息是,会通过给出的SEL去找到IMP,然后执行
上面调用方法的过程是不是也就清晰了。
1.当想一个对象发送消息时,通过对象isa找到它锁对应的类
2.类的isa指向的元类,在缓存列表中查找方法。
3.若在缓存中找到,调用方法,若没找到,则去MethodList列表中查找。
4.如果在当前类的方法列表中还没找到,则需要到他的父类去查找。
5找到方法以后,通过SEL找到IMP,然后调用方法。
拦截调用
既然是消息的传递肯定会有找不到方法的时候,那找不到方法时,又是怎么处理的呢。先来看看这张图
从上图看到,运行时会调用+resolveInstanceMethod:或者 +resolveClassMethod:,可以提供一个函数实现。如果添加了函数,并且返回yes,运行时系统就会重新启动一次消息发送过程
[self performSelector:@selector(testbtnclick:)];
}
+(BOOL)resolveInstanceMethod:(SEL)sel{
if (sel==@selector(testbtnclick:)) {
class_addMethod([self class], sel, (IMP)testbtnclickMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void testbtnclickMethod(id obj,SEL _cmd){
NSLog(@"测试动态解析");
}
输出: RuntimeDemo[35397:13336395] 测试动态解析
平常没有实现testbtnclick方法的时候,是不是直接就奔溃了,但是现在我们通过class_addMethod动态的添加testbtnclickMethod函数,并执行这个函数的IMP,打印出结果。
那我不想在这个方法里设置,还有没有其他的方式呢?当然是有的
如果resolve方法返回 YES ,运行时就会移到下一步:forwardingTargetForSelector。
如果目标对象实现了forwardingTargetForSelector:,runtime就会调用这个方法,就有把这个消息转发给其他对象的机会。上代码
#import "PeopleOBJC.h"
@implementation PeopleOBJC
-(void)testRuntime{
NSLog(@"测试runtime数据");
}
@end
[self performSelector:@selector(testRuntime)];
}
+(BOOL)resolveInstanceMethod:(SEL)sel{
return YES;
}
-(id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector==@selector(testRuntime)) {
return [PeopleOBJC new];
}
return [super forwardingTargetForSelector:aSelector];
}
打印出:RuntimeDemo[35441:13341243] 测试runtime数据
把当前的testRuntime方法转到了PeopleOBJC实现。
零零总总的几天,总结Runtime,方便自己系统化的理解。要是真的一行一行的去看runtime的源码,我肯定看不下来。只能看别人的博客,加上自己的理解,写出来。