Runtime : 运行时详解

image.png

一、简介

1.1 什么是Runtime
Runtime是一套底层纯C语言API,OC代码最终都会被编译器转化为运行时代码,通过消息机制决定函数调用方式,这也是OC作为动态语言使用的基础。
复制代码
2.1 消息机制的基本原理

在Object-C的语言中,对象方法调用都是类似[receiver selector] 的形式,其本质:就是让对象在运行时发送消息的过程。

而方法调用[receiver selector] 分为两个过程:

  • 编译阶段

[receiver selector] 方法被编译器转化,分为两种情况:

1.不带参数的方法被编译为:objc_msgSend(receiver,selector)
2.带参数的方法被编译为:objc_msgSend(recevier,selector,org1,org2,…)
复制代码
  • 运行时阶段

消息接收者recever寻找对应的selector,也分为两种情况:

1.接收者能找到对应的selector,直接执行接收receiver对象的selector方法。
2.接收者找不到对应的selector,消息被转发或者临时向接收者添加这个selector对应的实现内容,否则崩溃
复制代码

总而言之:

OC调用方法[receiver selector],编译阶段确定了要向哪个接收者发送message消息,但是接收者如何响应决定于运行时的判断
复制代码
1.3 Runtime中的概念解析

1.3.1 objc_msgSend

所有 Objective-C 方法调用在编译时都会转化为对 C 函数 objc_msgSend 的调用。objc_msgSend(receiver,selector); 是 [receiver selector]; 对应的 C 函数
复制代码

1.3.2 Object(对象)

objc/runtime.h 中Object(对象) 被定义为指向 objc_object 结构体 的指针,objc_object结构体 的数据结构如下:

//runtime对objc_object结构体的定义
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

//id是一个指向objc_object结构体的指针,即在Runtime中:
typedef struct objc_object *id;

//OC中的对象虽然没有明显的使用指针,但是在OC代码被编译转化为C之后,每个OC对象其实都是拥有一个isa的指针的
复制代码

1.3.2 Class(类)

objc/runtime.h 中Class(类) 被定义为指向 objc_class 结构体 的指针,objc_class结构体 的数据结构如下:

//runtime对objc_class结构体的定义
struct objc_class {
    Class _Nonnull isa;                                          // objc_class 结构体的实例指针

#if !__OBJC2__
    Class _Nullable super_class;                                 // 指向父类的指针
    const char * _Nonnull name;                                  // 类的名字
    long version;                                                // 类的版本信息,默认为 0
    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

};

//class是一个指向objc_class结构体的指针,即在Runtime中:
typedef struct objc_class *Class; 
复制代码

1.3.3 SEL (方法选择器)

objc/runtime.h 中SEL (方法选择器) 被定义为指向 objc_selector 结构体 的指针:

typedef struct objc_selector *SEL;

//Objective-C在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int类型的地址),这个标识就是SEL
复制代码

注意:

1.不同类中相同名字的方法对应的方法选择器是相同的。
2.即使是同一个类中,方法名相同而变量类型不同也会导致它们具有相同的方法选择器。
复制代码

通常获取SEL有三种方法:

1.OC中,使用@selector(“方法名字符串”)
2.OC中,使用NSSelectorFromString(“方法名字符串”)
3.Runtime方法,使用sel_registerName(“方法名字符串”)
复制代码

1.3.4 Ivar

objc/runtime.h 中Ivar 被定义为指向 objc_ivar 结构体 的指针,objc_ivar结构体 的数据结构如下:

struct objc_ivar {
    char * Nullable ivar_name                               OBJC2UNAVAILABLE;
    char * Nullable ivar_type                               OBJC2UNAVAILABLE;
    int ivar_offset                                          OBJC2_UNAVAILABLE;
#ifdef LP64
    int space                                                OBJC2_UNAVAILABLE;
#endif
} 

//Ivar代表类中实例变量的类型,是一个指向ojbcet_ivar的结构体的指针
typedef struct objc_ivar *Ivar;
复制代码

objc_class中看到的ivars成员列表,其中的元素就是Ivar,可以通过实例查找其在类中的名字,这个过程被称为反射,下面的class_copyIvarList获取的不仅有实例变量还有属性:

   Ivar *ivarList = class_copyIvarList([self class], &count);
    for (int i= 0; i

1.3.5 Method(方法)

objc/runtime.h 中Method(方法) 被定义为指向 objc_method 结构体 的指针,在objct_class定义中看到methodLists,其中的元素就是Method,objc_method结构体 的数据结构如下:

struct objc_method {
    SEL _Nonnull method_name;                    // 方法名
    char * _Nullable method_types;               // 方法类型
    IMP _Nonnull method_imp;                     // 方法实现
};

//Method表示某个方法的类型
typedef struct objc_method *Method;

复制代码

二、和Runtime交互的三种方式

2.1 OC源代码
OC代码会在编译阶段被编译器转化。OC中的类、方法和协议等在Runtime中都由一些数据结构来定义。
所以在日常的项目开发过程中,使用OC语言进行编码时,这已经是在和Runtime进行交互了,只是这个过程对于开发者而言是无感的
复制代码
2.2 NSObject方法
Runtime的最大特征就是实现了OC语言的动态特性。
复制代码

作为大部分Objective-C类继承体系的根类的NSObject,其本身就具有了一些非常具有运行时动态特性的方法, 比如:

1\. -respondsToSelector:方法可以检查在代码运行阶段当前对象是否能响应指定的消息

2\. -description:返回当前类的描述信息 

3\. -isKindOfClass: 和 -isMemberOfClass:  检查对象是否存在于指定的类的继承体系中

4\. -conformsToProtocol:    检查对象是否实现了指定协议类的方法;

5\. -methodForSelector:     返回指定方法实现的地址。
复制代码
2.3 使用Runtime函数
Runtime系统是一个由一系列函数和数据结构组成,具有公共接口的动态共享库。头文件存放于/usr/include/objc目录下。
复制代码

在项目工程代码里引用Runtime的头文件,同样能够实现类似OC代码的效果:

//相当于:Class class = [UIView class];
Class viewClass = objc_getClass("UIView");

//相当于:UIView *view = [UIView alloc];
UIView *view = ((id (*)(id, SEL))(void *)objc_msgSend)((id)viewClass, sel_registerName("alloc"));

//相当于:UIView *view = [view init];
((id (*)(id, SEL))(void *)objc_msgSend)((id)view, sel_registerName("init"));
复制代码

三、Runtime消息转发

3.1 动态方法解析与消息转发
  • 动态方法解析:动态添加方法

Runtime足够强大,能够在运行时动态添加一个未实现的方法,这个功能主要有两个应用场景:

1\. 动态添加未实现方法,解决代码中因为方法未找到而报错的问题;
2\. 利用懒加载思路,若一个类有很多个方法,同时加载到内存中会耗费资源,可以使用动态解析添加方法
复制代码

方法动态解析主要用到的方法如下:

//OC方法:
//类方法未找到时调起,可于此添加类方法实现
+ (BOOL)resolveClassMethod:(SEL)sel

//实例方法未找到时调起,可于此添加实例方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel

//Runtime方法:
/**
 运行时方法:向指定类中添加特定方法实现的操作
 @param cls 被添加方法的类
 @param name selector方法名
 @param imp 指向实现方法的函数指针
 @param types imp函数实现的返回值与参数类型
 @return 添加方法是否成功
 */
BOOL class_addMethod(Class _Nullable cls,
                     SEL _Nonnull name,
                     IMP _Nonnull imp,
                     const char * _Nullable types)
复制代码
  • 解决方法无响应崩溃问题

执行OC方法其实就是一个发送消息的过程,若方法未实现,可以利用方法动态解析与消息转发来避免程序崩溃,这主要涉及下面一个处理未实现消息的过程:

在这个过程中,可能还会使用到的方法有:

例子:
#import "ViewController.h"
#import 

@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 执行 fun 函数
    [self performSelector:@selector(fun)];
}

// 重写 resolveInstanceMethod: 添加对象方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(fun)) { // 如果是执行 fun 函数,就动态解析,指定新的 IMP
        class_addMethod([self class], sel, (IMP)funMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void funMethod(id obj, SEL _cmd) {
    NSLog(@"funMethod"); //新的 fun 函数
}
@end

//日志输出:

2019-09-01 23:24:34.911774+0800 XKRuntimeKit[3064:521123] funMethod
复制代码

从执行任务的输出日志中,可以看到:

虽然没有实现 fun 方法,但是通过重写 resolveInstanceMethod: ,利用 class_addMethod 方法添加对象方法实现 funMethod 方法,并执行。从打印结果来看,成功调起了funMethod 方法。
复制代码
3.2 消息接收者重定向:

如果上一步中 +resolveInstanceMethod:或者 +resolveClassMethod: 没有添加其他函数实现,运行时就会进行下一步:消息接受者重定向。

如果当前对象实现了 -forwardingTargetForSelector:Runtime 就会调用这个方法,允许将消息的接受者转发给其他对象,其主要方法如下:

//重定向类方法的消息接收者,返回一个类
- (id)forwardingTargetForSelector:(SEL)aSelector

//重定向实例方法的消息接受者,返回一个实例对象
- (id)forwardingTargetForSelector:(SEL)aSelector
复制代码
例子:
#import "ViewController.h"
#import 

@interface Person : NSObject
- (void)fun;
@end

@implementation Person

- (void)fun {
    NSLog(@"fun");
}
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 执行 fun 方法
    [self performSelector:@selector(fun)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return YES; // 为了进行下一步 消息接受者重定向
}

// 消息接受者重定向
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(fun)) {
        return [[Person alloc] init];
        // 返回 Person 对象,让 Person 对象接收这个消息
    }

    return [super forwardingTargetForSelector:aSelector];
}

//日志输出:

2019-09-01 23:24:34.911774+0800 XKRuntimeKit[3064:521123] fun
复制代码

从执行任务的输出日志中,可以看到:

虽然当前 ViewController 没有实现 fun 方法,+resolveInstanceMethod: 也没有添加其他函数实现。
但是我们通过 forwardingTargetForSelector 把当前 ViewController 的方法转发给了 Person 对象去执行了
复制代码

通过forwardingTargetForSelector 可以修改消息的接收者,该方法返回参数是一个对象,如果这个对象是不是 nil,也不是 self,系统会将运行的消息转发给这个对象执行。否则,继续进行下一步:消息重定向流程

3.3 消息重定向:

如果经过消息动态解析、消息接受者重定向,Runtime 系统还是找不到相应的方法实现而无法响应消息,Runtime 系统会利用 -methodSignatureForSelector: 方法获取函数的参数和返回值类型。

其过程:

1\. 如果 -methodSignatureForSelector: 返回了一个 NSMethodSignature 对象(函数签名),Runtime 系统就会创建一个 NSInvocation 对象,
   并通过 -forwardInvocation: 消息通知当前对象,给予此次消息发送最后一次寻找 IMP 的机会。
2\. 如果 -methodSignatureForSelector: 返回 nil。则 Runtime 系统会发出 -doesNotRecognizeSelector: 消息,程序也就崩溃了
复制代码

所以可以在-forwardInvocation:方法中对消息进行转发。

其主要方法:

// 消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation;

// 获取函数的参数和返回值类型,返回签名
- (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector;
复制代码
例子:
#import "ViewController.h"
#import 

@interface Person : NSObject
- (void)fun;
@end

@implementation Person
- (void)fun {
    NSLog(@"fun");
}
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 执行 fun 函数
    [self performSelector:@selector(fun)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return YES; // 为了进行下一步 消息接受者重定向
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    return nil; // 为了进行下一步 消息重定向
}

// 获取函数的参数和返回值类型,返回签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString:@"fun"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }

    return [super methodSignatureForSelector:aSelector];
}

// 消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = anInvocation.selector;   // 从 anInvocation 中获取消息

    Person *p = [[Person alloc] init];

    if([p respondsToSelector:sel]) {   // 判断 Person 对象方法是否可以响应 sel
        [anInvocation invokeWithTarget:p];  // 若可以响应,则将消息转发给其他对象处理
    } else {
        [self doesNotRecognizeSelector:sel];  // 若仍然无法响应,则报错:找不到响应方法
    }
}
@end

//日志输出:
2019-09-01 23:24:34.911774+0800 XKRuntimeKit[30032:8724248] fun
复制代码

从执行任务的输出日志中,可以看到:

在 -forwardInvocation: 方法里面让 Person 对象去执行了 fun 函数
复制代码

既然 -forwardingTargetForSelector:-forwardInvocation: 都可以将消息转发给其他对象处理,那么两者的区别在哪?

区别就在于 -forwardingTargetForSelector: 只能将消息转发给一个对象。而 -forwardInvocation: 可以将消息转发给多个对象。
复制代码

四、Runtime的应用

4.1 动态方法交换

实现动态方法交换(Method Swizzling )是Runtime中最具盛名的应用场景,其原理是:

通过Runtime获取到方法实现的地址,进而动态交换两个方法的功能
复制代码
关键方法:
//获取类方法的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)
复制代码
  • 动态方法交换
#import "RuntimeKit.h"
#import 

@implementation RuntimeKit

- (instancetype)init
{
    self = [super init];
    if (self) {

        //交换方法的实现,并测试打印
        Method methodA = class_getInstanceMethod([self class], @selector(testA));
        Method methodB = class_getInstanceMethod([self class], @selector(testB));
        method_exchangeImplementations(methodA, methodB);

        [self testA];
        [self testB];
    }
    return self;
}

- (void)testA{
    NSLog(@"我是A方法");
}

- (void)testB{
    NSLog(@"我是B方法");
}
@end

日志输出:
2019-09-01 21:25:32.858860+0800 XKRuntimeKit[1662:280727] 我是B方法
2019-09-01 21:25:32.859059+0800 XKRuntimeKit[1662:280727] 我是A方法
复制代码
  • 拦截并替换系统方法
#import "UIViewController+xk.h"
#import 

@implementation UIViewController (xk)

+ (void)load{

    //获取系统方法地址
    Method sytemMethod = class_getInstanceMethod([self class], @selector(viewWillAppear:));

    //获取自定义方法地址
    Method customMethod = class_getInstanceMethod([self class], @selector(run_viewWillAppear:));

    //判断存在与否
    if (!class_addMethod([self class], @selector(viewWillAppear:), method_getImplementation(customMethod), method_getTypeEncoding(customMethod))) {
        method_exchangeImplementations(sytemMethod, customMethod);
    }
    else{
        class_replaceMethod([self class], @selector(run_viewWillAppear:), method_getImplementation(sytemMethod), method_getTypeEncoding(sytemMethod));
    }
}

- (void)run_viewWillAppear:(BOOL)animated{
    [self run_viewWillAppear:animated];
    NSLog(@"我是运行时替换的方法-viewWillAppear");
}

- (void)run_viewWillDisappear:(BOOL)animated{
    [self run_viewWillDisappear:animated];
    NSLog(@"我是运行时替换的方法-viewWillDisappear");
}
@end

日志输出:

2019-09-01 21:36:55.610385+0800 XKRuntimeKit[1921:310118] 我是运行时替换的方法-viewWillAppear
复制代码

将该分类引入,从执行结果可以看到,但系统的控制器执行viewWillAppear时,则会进入已经替换的方法run_viewWillAppear之中。

4.2 类目添加新的属性

在日常开发过程中,常常会使用类目Category为一些已有的类扩展功能。虽然继承也能够为已有类增加新的方法,而且相比类目更是具有增加属性的优势,但是继承毕竟是一个重量级的操作,添加不必要的继承关系无疑增加了代码的复杂度。

遗憾的是,OC的类目并不支持直接添加属性
复制代码

为了实现给分类添加属性,还需借助 Runtime的关联对象(Associated Objects)特性,它能够帮助我们在运行阶段将任意的属性关联到一个对象上:

/**
 1.给对象设置关联属性
 @param object 需要设置关联属性的对象,即给哪个对象关联属性
 @param key 关联属性对应的key,可通过key获取这个属性,
 @param value 给关联属性设置的值
 @param policy 关联属性的存储策略(对应Property属性中的assign,copy,retain等)
 OBJC_ASSOCIATION_ASSIGN             @property(assign)。
 OBJC_ASSOCIATION_RETAIN_NONATOMIC   @property(strong, nonatomic)。
 OBJC_ASSOCIATION_COPY_NONATOMIC     @property(copy, nonatomic)。
 OBJC_ASSOCIATION_RETAIN             @property(strong,atomic)。
 OBJC_ASSOCIATION_COPY               @property(copy, atomic)。
 */
void objc_setAssociatedObject(id _Nonnull object,
                              const void * _Nonnull key,
                              id _Nullable value,
                              objc_AssociationPolicy policy)

/**
 2.通过key获取关联的属性
 @param object 从哪个对象中获取关联属性
 @param key 关联属性对应的key
 @return 返回关联属性的值
 */
id _Nullable objc_getAssociatedObject(id _Nonnull object,
                                      const void * _Nonnull key)

/**
 3.移除对象所关联的属性
 @param object 移除某个对象的所有关联属性
 */
void objc_removeAssociatedObjects(id _Nonnull object)
复制代码

注意:

key与关联属性一一对应,我们必须确保其全局唯一性,常用我们使用@selector(methodName)作为key
复制代码
例子:

UIViewController+xk.h中新增一个name属性:

@interface UIViewController (xk)

//新增属性:名称
@property(nonatomic,copy)NSString * name;

- (void)clearAssociatedObjcet;
@end
复制代码

UIViewController+xk.m中补充对应的实现:

#import "UIViewController+xk.h"
#import 

@implementation UIViewController (xk)

//set方法
- (void)setName:(NSString *)name{
    objc_setAssociatedObject(self,
                             @selector(name),
                             name,
                             OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

//get方法
- (NSString *)name{
    return objc_getAssociatedObject(self,
                                    @selector(name));
}

//添加一个自定义方法,用于清除所有关联属性
- (void)clearAssociatedObjcet{
    objc_removeAssociatedObjects(self);
}
@end

复制代码

执行任务:


ViewController * vc = [ViewController new];
vc.name = @"我是根控制器";
NSLog(@"获取关联属性name:%@",vc.name);

[vc clearAssociatedObjcet];
NSLog(@"获取关联属性name:%@",vc.name);

日志输出:
2019-09-01 21:50:05.162915+0800 XKRuntimeKit[2066:335327] 获取关联属性name:我是根控制器
2019-09-01 21:50:05.163080+0800 XKRuntimeKit[2066:335327] 获取关联属性name:(null)
复制代码

同样的,使用运行时还可以为类目新增一些自身没有的方法,比如给UIView新增点击事件:

#import 

static char onTapGestureKey;
static char onTapGestureBlockKey;

@implementation UIView (Gesture)

//添加轻拍手势
- (void)addTapGestureActionWithBlock:(onGestureActionBlock)block{
    UITapGestureRecognizer *gesture = objc_getAssociatedObject(self, &onTapGestureKey);
    self.userInteractionEnabled = YES;
    if (!gesture){
        gesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(xk_handleActionForTapGesture:)];
        [self addGestureRecognizer:gesture];
        objc_setAssociatedObject(self, &onTapGestureKey, gesture, OBJC_ASSOCIATION_RETAIN);
    }

    //添加点击手势响应代码块属性
    objc_setAssociatedObject(self, &onTapGestureBlockKey, block, OBJC_ASSOCIATION_COPY);
}

//点击回调
- (void)xk_handleActionForTapGesture:(UITapGestureRecognizer*)sender{
    onGestureActionBlock block = objc_getAssociatedObject(self, &onTapGestureBlockKey);
    if (block) block(sender);
}
@end
复制代码

但是使用运行时给类目新增代理属性时,需要注意循环应用问题,由于运行时执行添加的属性都是retain操作,所以往往在执行过程会导致对应的 delegate 得不到释放,因而会导致崩溃,对此,可以进行以下修改操作:

场景: 给UIView新增emptyDataDelegate空页面代理,以处理一些异常情况的显示

UIView+EmptyDataSet.h中新增一个emptyDataDelegate属性,:

//页面无数据代理
@protocol XKEmptyDataSetDelegate 
@optional
//占位文字
- (NSString*)placeholderForEmptyDataSet:(UIScrollView*)scrollView;
@end

//空页面设置
@interface UIView (EmptyDataSet)
@property (nonatomic,weak) idemptyDataDelegate;
@end

复制代码

UIView+EmptyDataSet.m中借助XKEmptyDataWeakObjectContainer实现其方法:

//弱引用代理
@interface XKEmptyDataWeakObjectContainer : NSObject
@property (nonatomic,weak,readonly)id weakObject;
- (instancetype)initWithWeakObject:(id)object;
@end

@implementation XKEmptyDataWeakObjectContainer
- (instancetype)initWithWeakObject:(id)object{
    self = [super init];
    if (self) {
        _weakObject = object;
    }
    return self;
}
@end

static char xk_EmptyDataSetDelegateKey;

//空视图设置
@implementation UIView (EmptyDataSet)
- (void)setEmptyDataDelegate:(id)emptyDataDelegate{
     objc_setAssociatedObject(self, &xk_EmptyDataSetDelegateKey, [[XKEmptyDataWeakObjectContainer alloc] initWithWeakObject:emptyDataDelegate], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (id)emptyDataDelegate{
    XKEmptyDataWeakObjectContainer * container = objc_getAssociatedObject(self, &xk_EmptyDataSetDelegateKey);
    return container.weakObject;
}
@end
复制代码
4.3 获取类详细属性
  • 获取属性列表
unsigned int count;
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
for (unsigned int i = 0; i
  • 获取所有成员变量
Ivar *ivarList = class_copyIvarList([self class], &count);
for (int i= 0; i
  • 获取所有方法
Method *methodList = class_copyMethodList([self class], &count);
for (unsigned int i = 0; i
  • 获取当前遵循的所有协议
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
for (int i=0; i
4.4 解决同一方法高频率调用的效率问题

Runtime源码中的IMP作为函数指针,指向方法的实现。通过它,可以绕开发送消息的过程来提高函数调用的效率。当需要持续大量重复调用某个方法的时候,会十分有用,如下:

void (*setter)(id, SEL, BOOL);
int i;

setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ )
    setter(targetList[i], @selector(setFilled:), YES);
复制代码
4.5 动态操作属性
  • 修改私有属性
场景:
我们使用第三方框架里的Person类,在特殊需求下想要更改其私有属性nickName,这样的操作我们就可以使用Runtime可以动态修改对象属性。
复制代码
Person *ps = [[Person alloc] init];
NSLog(@"nickName: %@",[ps valueForKey:@"nickName"]); //null

//第一步:遍历对象的所有属性
unsigned int count;
Ivar *ivarList = class_copyIvarList([ps class], &count);
for (int i= 0; i
  • 改进iOS归档和解档

归档是一种常用的轻量型文件存储方式,但是它有个弊端:

在归档过程中,若一个Model有多个属性,我们不得不对每个属性进行处理,非常繁琐
复制代码

归档操作主要涉及两个方法: encodeObjectdecodeObjectForKey ,对于这两个方法,可以利用Runtime 来进行改进:

//原理:使用Runtime动态获取所有属性
//解档操作
- (instancetype)initWithCoder:(NSCoder *)aDecoder{
    self = [super init];
    if (self) {
        unsigned int count = 0;

        Ivar *ivarList = class_copyIvarList([self class], &count);
        for (int i = 0; i < count; i++) {
            Ivar ivar = ivarList[i];
            const char *ivarName = ivar_getName(ivar);
            NSString *key = [NSString stringWithUTF8String:ivarName];
            id value = [aDecoder decodeObjectForKey:key];
            [self setValue:value forKey:key];
        }
        free(ivarList); //释放指针
    }
    return self;
}

//归档操作
- (void)encodeWithCoder:(NSCoder *)aCoder{
    unsigned int count = 0;

    Ivar *ivarList = class_copyIvarList([self class], &count);
    for (NSInteger i = 0; i < count; i++) {
        Ivar ivar = ivarList[i];
        NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];

        id value = [self valueForKey:key];
        [aCoder encodeObject:value forKey:key];
    }
    free(ivarList); //释放指针
}
复制代码
测试:
//--测试归档
Person *ps = [[Person alloc] init];
ps.name = @"allenlas";
ps.age  = 20;
NSString *temp = NSTemporaryDirectory();
NSString *fileTemp = [temp stringByAppendingString:@"person.archive"];
[NSKeyedArchiver archiveRootObject:ps toFile:fileTemp];

//--测试解档
NSString *temp = NSTemporaryDirectory();
NSString *fileTemp = [temp stringByAppendingString:@"person.henry"];
Person *person = [NSKeyedUnarchiver unarchiveObjectWithFile:fileTemp];
NSLog(@"person-name:%@,person-age:%ld",person.name,person.age); 
//person-name:allenlas,person-age:20
复制代码
  • 实现字典与模型的转换

在日常项目开发中,经常会使用YYModelMJExtension等对接口返回的数据对象实现转模型操作。对于此,可以利用KVCRuntime 来进行类似的功能实现,在这个过程中需要解决的问题有:

利用Runtime实现的思路大体如下:

借助Runtime可以动态获取成员列表的特性,遍历模型中所有属性,然后以获取到的属性名为key,在JSON字典中寻找对应的值value;再将每一个对应Value赋值给模型,就完成了字典转模型的目的。
复制代码
json数据:
{
    "id":"10089",
    "name": "Allen",
    "age":"20",
    "position":"iOS开发工程师",
    "address":{
            "country":"中国",
            "province": "广州"
            },
    "tasks":[{
               "name":"Home",
               "desc":"app首页开发"
    },{
               "name":"Train",
               "desc":"app培训模块开发"
    },{
               "name":"Me",
               "desc":"完成个人页面"
    }
    ]
}
复制代码
  1. 创建NSObject的类目 NSObject+model,用于实现字典转模型
//在NSObject+model.h中

NS_ASSUME_NONNULL_BEGIN

//AAModel协议,协议方法可以返回一个字典,表明特殊字段的处理规则
@protocol AAModel
@optional
+ (nullable NSDictionary *)modelContainerPropertyGenericClass;
@end;

@interface NSObject (model)
+ (instancetype)xk_modelWithDictionary:(NSDictionary *)dictionary;
@end

NS_ASSUME_NONNULL_END

复制代码
#import "NSObject+model.h"
#import 

@implementation NSObject (model)
+ (instancetype)xk_modelWithDictionary:(NSDictionary *)dictionary{

    //创建当前模型对象
    id object = [[self alloc] init];
    //1.获取当前对象的成员变量列表
    unsigned int count = 0;
    Ivar *ivarList = class_copyIvarList([self class], &count);

    //2.遍历ivarList中所有成员变量,以其属性名为key,在字典中查找Value
    for (int i= 0; i @"name"
        ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
        //3.2去除@符号
        ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];

        //4.对特殊成员变量进行处理:
        //判断当前类是否实现了协议方法,获取协议方法中规定的特殊变量的处理方式
        NSDictionary *perpertyTypeDic;
        if([self respondsToSelector:@selector(modelContainerPropertyGenericClass)]){
            perpertyTypeDic = [self performSelector:@selector(modelContainerPropertyGenericClass) withObject:nil];
        }

        //4.1处理:字典的key与模型属性不匹配的问题,如id->uid
        id anotherName = perpertyTypeDic[propertyName];
        if(anotherName && [anotherName isKindOfClass:[NSString class]]){
            value =  dictionary[anotherName];
        }

        //4.2.处理:模型嵌套模型
        if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]) {
            Class modelClass = NSClassFromString(ivarType);
            if (modelClass != nil) {
                //将被嵌套字典数据也转化成Model
                value = [modelClass xk_modelWithDictionary:value];
            }
        }

        //4.3处理:模型嵌套模型数组
        //判断当前Vaue是一个数组,而且存在协议方法返回了perpertyTypeDic
        if ([value isKindOfClass:[NSArray class]] && perpertyTypeDic) {
            Class itemModelClass = perpertyTypeDic[propertyName];
            //封装数组:将每一个子数据转化为Model
            NSMutableArray *itemArray = @[].mutableCopy;
            for (NSDictionary *itemDic  in value) {
                id model = [itemModelClass xk_modelWithDictionary:itemDic];
                [itemArray addObject:model];
            }
            value = itemArray;
        }

        //5.使用KVC方法将Vlue更新到object中
        if (value != nil) {
            [object setValue:value forKey:propertyName];
        }
    }
    free(ivarList); //释放C指针
    return object;
}
@end
复制代码
  1. 分别新建 UserModelAddressModelTasksModel对json处理进行处理:
UserModel类
#import "NSObject+model.h"
#import "AddressModel.h"
#import "TasksModel.h"

NS_ASSUME_NONNULL_BEGIN

@interface UserModel : NSObject
//普通属性
@property (nonatomic, copy) NSString * uid;
@property (nonatomic, copy) NSString * name;
@property (nonatomic, copy) NSString * position;
@property (nonatomic, assign) NSInteger age;

//嵌套模型
@property (nonatomic, strong) AddressModel *address;

//嵌套模型数组
@property (nonatomic, strong) NSArray *tasks;
@end

NS_ASSUME_NONNULL_END

@implementation UserModel
+ (NSDictionary *)modelContainerPropertyGenericClass{
    //需要特别处理的属性
    return @{@"tasks" : [TasksModel class],@"uid":@"id"};
}
@end
复制代码
AddressModel类
#import "NSObject+model.h"

NS_ASSUME_NONNULL_BEGIN

@interface AddressModel : NSObject
@property (nonatomic, copy) NSString * country;
@property (nonatomic, copy) NSString * province;
@end

NS_ASSUME_NONNULL_END

@implementation AddressModel
@end
复制代码
TasksModel类
#import "NSObject+model.h"

NS_ASSUME_NONNULL_BEGIN

@interface TasksModel : NSObject
@property (nonatomic, copy) NSString * name;
@property (nonatomic, copy) NSString * desc;
@end
NS_ASSUME_NONNULL_END

@implementation TasksModel
@end
复制代码
  1. 代码测试
- (void)viewDidLoad {
    [super viewDidLoad];
    //读取JSON数据
    NSDictionary * jsonData = @{
                                @"id":@"10089",
                                @"name": @"Allen",
                                @"age":@"20",
                                @"position":@"iOS开发工程师",
                                @"address":@{
                                        @"country":@"中国",
                                        @"province":@"广州"
                                        },
                                @"tasks":@[@{
                                               @"name":@"Home",
                                               @"desc":@"app首页开发"
                                               },@{
                                               @"name":@"Train",
                                               @"desc":@"app培训模块开发"
                                               },@{
                                               @"name":@"Me",
                                               @"desc":@"完成个人页面"
                                               }
                                           ]
                                };

    //字典转模型
    UserModel * user = [UserModel xk_modelWithDictionary:jsonData];
    TasksModel * task = user.tasks[0];

    NSLog(@"%@",task.name);
}
复制代码

其执行结果,数据结构如下:

作者:ALLen、LAS
链接:https://juejin.im/post/5d6bea9651882563e82aff2b

你可能感兴趣的:(Runtime : 运行时详解)