[toc]
![](https://img.kancloud.cn/6b/32/6b32cffc4212471fcbf4943c2347bdfc_2783x1147.png)
## 一、简介
### 1.1 什么是Runtime
>Runtime是一套底层纯`C语言API`,我们编写的OC代码最终都会被编译器转化为`运行时代码`,通过`消息机制`决定函数调用方式,这也是OC作为`动态语言`使用的基础。
### 1.2 消息机制的基本原理
在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.3 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.4 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.5 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<count; i++) {
Ivar ivar = ivarList[i];
const char *ivarName = ivar_getName(ivar);
NSLog(@"Ivar(%d): %@", i, [NSString stringWithUTF8String:ivarName]);
}
free(ivarList);
```
#### 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 动态方法解析与消息转发
#### 3.1.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)
```
#### 3.1.2 解决方法无响应崩溃问题
执行OC方法其实就是一个发送消息的过程,若方法未实现,可以利用`方法动态解析`与`消息转发`来避免程序崩溃,这主要涉及下面一个处理未实现消息的过程:
在这个过程中,可能还会使用到的方法有:
![](https://img.kancloud.cn/96/a6/96a634389fccb2ae5e93123fa08dc13a_785x425.png)
**例子:**
```
#import "ViewController.h"
#import <objc/runtime.h>
@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 <objc/runtime.h>
@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 <objc/runtime.h>
@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)
```
#### 4.1.1 动态方法交换
```
#import "RuntimeKit.h"
#import <objc/runtime.h>
@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方法
```
#### 4.1.2拦截并替换系统方法
```
#import "UIViewController+xk.h"
#import <objc/runtime.h>
@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 <objc/runtime.h>
@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 <objc/runtime.h>
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 <NSObject>
@optional
//占位文字
- (NSString*)placeholderForEmptyDataSet:(UIScrollView*)scrollView;
@end
//空页面设置
@interface UIView (EmptyDataSet)
@property (nonatomic,weak) id<XKEmptyDataSetDelegate>emptyDataDelegate;
@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<XKEmptyDataSetDelegate>)emptyDataDelegate{
objc_setAssociatedObject(self, &xk_EmptyDataSetDelegateKey, [[XKEmptyDataWeakObjectContainer alloc] initWithWeakObject:emptyDataDelegate], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id<XKEmptyDataSetDelegate>)emptyDataDelegate{
XKEmptyDataWeakObjectContainer * container = objc_getAssociatedObject(self, &xk_EmptyDataSetDelegateKey);
return container.weakObject;
}
@end
```
### 4.3 获取类详细属性
#### 4.3.1获取属性列表
获取类属性列表用到runtime的 `class_copyPropertyList`方法,该方法接收一个类对象及返回属性数量的地址引用
```
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(@"PropertyName(%d): %@",i,[NSString stringWithUTF8String:propertyName]);
}
free(propertyList);
```
#### 4.3.2获取所有成员变量
获取类中所有的成员变量,使用的是runtime的`class_copyIvarList`方法。
```
Ivar *ivarList = class_copyIvarList([self class], &count);
for (int i= 0; i<count; i++) {
Ivar ivar = ivarList[i];
const char *ivarName = ivar_getName(ivar);
NSLog(@"Ivar(%d): %@", i, [NSString stringWithUTF8String:ivarName]);
}
free(ivarList);
```
#### 4.3.3.获取所有方法
获取类中所有的方法列表,使用runtime的`class_copyMethodList`方法。
```
Method *methodList = class_copyMethodList([self class], &count);
for (unsigned int i = 0; i<count; i++) {
Method method = methodList[i];
SEL mthodName = method_getName(method);
NSLog(@"MethodName(%d): %@",i,NSStringFromSelector(mthodName));
}
free(methodList);
```
#### 4.3.4获取当前遵循的所有协议
获取当前遵循的所有协议,使用`class_copyProtocolList`方法。
```
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
for (int i=0; i<count; i++) {
Protocol *protocal = protocolList[i];
const char *protocolName = protocol_getName(protocal);
NSLog(@"protocol(%d): %@",i, [NSString stringWithUTF8String:protocolName]);
}
free(propertyList); //C语言中使用Copy操作的方法,要注意释放指针,防止内存泄漏
```
上面几组获取类的属性列表,成员列表,方法列表及遵循的协议列表的方法最后都调用了`free`函数。 这是因为`C语言`中使用`Copy`操作的方法,要注意`释放指针`,`防止内存泄漏`
### 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 动态操作属性
#### 4.5.1修改私有属性
**场景:**
我们使用第三方框架里的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<count; i++) {
//第二步:获取每个属性名
Ivar ivar = ivarList[i];
const char *ivarName = ivar_getName(ivar);
NSString *propertyName = [NSString stringWithUTF8String:ivarName];
if ([propertyName isEqualToString:@"_nickName"]) {
//第三步:匹配到对应的属性,然后修改;注意属性带有下划线
object_setIvar(ps, ivar, @"allenlas");
}
}
NSLog(@"nickName: %@",[ps valueForKey:@"nickName"]); //allenlas
```
#### 4.5.2改进iOS归档和解档
`归档`是一种常用的`轻量型`文件存储方式,但是它有个弊端:
>在归档过程中,若一个`Model`有多个属性,我们不得不对每个属性进行处理,非常繁琐
归档操作主要涉及两个方法: `encodeObject` 和 `decodeObjectForKey` ,对于这两个方法,可以利用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
```
#### 4.5.3实现字典与模型的转换
在日常项目开发中,经常会使用`YYModel `或 `MJExtension`等对接口返回的数据对象实现转模型操作。对于此,可以利用`KVC`和`Runtime `来进行类似的功能实现,在这个过程中需要解决的问题有:
![](https://img.kancloud.cn/e6/03/e603492fc5539473162411d9698094ad_696x214.png)
利用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<NSObject>
@optional
+ (nullable NSDictionary<NSString *, id> *)modelContainerPropertyGenericClass;
@end;
@interface NSObject (model)
+ (instancetype)xk_modelWithDictionary:(NSDictionary *)dictionary;
@end
NS_ASSUME_NONNULL_END
```
```
#import "NSObject+model.h"
#import <objc/runtime.h>
@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<count; i++) {
//2.1获取成员属性
Ivar ivar = ivarList[i];
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)] ;
//2.2截取成员变量名:去掉成员变量前面的"_"号
NSString *propertyName = [ivarName substringFromIndex:1];
//2.3以属性名为key,在字典中查找value
id value = dictionary[propertyName];
//3.获取成员变量类型, 因为ivar_getTypeEncoding获取的类型是"@\"NSString\""的形式
//所以我们要做以下的替换
NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];// 替换:
//3.1去除转义字符:@\"name\" -> @"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
```
**2.分别新建`UserModel、AddressModel、TasksModel`对json处理进行处理:**
UserModel类
```
#import "NSObject+model.h"
#import "AddressModel.h"
#import "TasksModel.h"
NS_ASSUME_NONNULL_BEGIN
@interface UserModel : NSObject<AAModel>
//普通属性
@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<NSString *,id> *)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
```
**3.代码测试**
```
- (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);
}
```
其执行结果,数据结构如下:
![](https://img.kancloud.cn/42/57/425752265b260a5fb58fa4adb4edb978_1692x428.png)
- 前言
- WebRTC知识集
- iOS 集成WebRTC各知识点小集
- iOS WebRTC集成时遇到的问题总结
- WebRTC多人音视频聊天架构及实战
- iOS端 使用WebRTC实现1对1音视频实时通话
- iOS 基于WebRTC的点对点音视频通信 总结篇
- WebRTC Native 源码导读 - iOS 相机采集实现分析
- OC 底层原理
- OC runtime 运行时详解
- GCD dispatch_queue_create 创建队列
- iOS底层 Runtime深入理解
- iOS底层 RunLoop深入理解
- iOS底层 Block的本质与使用
- iOS内存泄漏
- iOS中isKindOfClass和isMemberOfClass
- 从预编译的角度理解Swift与Objective-C及混编机制
- 移动支付集成
- iOS 微信支付集成及二次封装
- iOS 支付宝支付 Alipay集成及二次封装
- iOS Paypal 贝宝支付集成及二次封装
- iOS 微信、支付宝、银联、Paypal 支付组件封装
- iOS 微信、支付宝、银联支付组件的进一步设计
- iOS 组件化
- iOS 组件化实施过程
- iOS 组件化的二进制化
- 使用pod package打包framework 实现组件的二进制化
- iOS 自制Framework 获取指定bundle并读取里面的资源
- .podSpec文件相关知识整理
- 开发并上传静态库到CocoaPods
- pod引用第三方库的几种方式
- 如何在.podspec 文件中添加对本地库的依赖
- lipo 命令合并真机与模拟器生成的framework
- iOS多线程
- NSOperation相关知识点
- 自定义NSOperation
- ios多个网络请求之间的并行与串行场景的处理
- iOS动画
- ios animation 动画学习总结
- CABasicAnimation使用总结
- UITableView cell呈现的动效整理
- CoreAnimation动画使用详解
- iOS音视频开发
- iOS 音视频开发之AVCaptureMetadataOutput
- iOS操作本地视频 - 获取,压缩,取第一帧
- 使用 GPUImage 实现一个简单相机
- 直播App架构及思维导图
- 如何快速的开发一个完整的iOS直播app
- iOS视频拖动预览及裁剪
- iOS 直播流程概述
- iOS直播:评论框与粒子系统点赞动画
- iOS音视频开发 - 采集
- 基于AVFoundation实现视频录制的两种方式
- Swift知识集
- Swift 的枚举、结构体和类详解
- Swift 泛型详解
- Swift属性的包装器@PropertyWrapper
- SwiftHub项目 之网络层封装的一点见解
- Moya+RxSwift+HandyJson 实现网络请求及模型转换
- Swift开发小记(含面试题)
- RxSwift 入坑手册 - 基础概念
- 理解 Swift 中的元类型:.Type 与 .self
- Swift HandyJSON库中的类型相互转换的实现
- Swift 中使用嵌套结构体定义一组相关的常量
- Swift Type-Erased(类型擦除)
- Swift中的weak和unowned关键字
- Swift 中的错误处理
- Swift中的Result 类型的简单介绍
- Swift Combine 入门导读
- Swift CustomStringConvertible 协议的使用
- 跨平台
- Cordova跨平台方案 iOS工程创建的步骤
- 使用Cordova 打包WebApp为原生应用详解 (加壳封装)
- RAC响应式编程
- 快速上手ReactiveCocoa之基础篇
- RAC ReactiveCocoa 使用小集
- 优雅的 RACCommand
- 三方库集成及使用
- 融云IM iOS sdk 集成 一篇就够了
- iOS YYTextView使用笔记
- iOS YYLabel使用笔记
- iOS 苹果集成登录及苹果图标的制作要求
- iOS 面向切面编程 Aspects 库的使用
- VKMsgSend库对oc runtime的封装
- OC Protocol协议分发器
- iOS 高德地图实现大头针展示,分级大头针,自定制大头针,在地图上画线,线和点共存,路线规划(驾车路线规划),路线导航,等一些常见的使用场景
- 工作总结
- 自定义UINavigationBar 适配iOS11, iOS15的问题
- SFSafariViewController 加载的网页与原生oc之间的交互
- UICollectionView 设置header的二种方法
- UIPanGestureRecognizer进行视图滑动并处理手势冲突
- OC与Swift混编 注意事项
- UICollectionView 设置水平滑动后,调整每个Item项的排列方式
- oc 下定义字符串枚举
- 高性能iOS应用开发中文版读书笔记
- iOS 图集滑动到最后时添加“显示更多”效果的view组件 实现
- CocoaPods 重装
- WKWebview使用二三事
- IOS电商首页如何布局
- iOS中的投屏方案
- CGAffineTransform 介绍
- 用Block实现链式编程
- iOS 本地化简明指南
- iOS 检查及获取相机、麦克风、相册、位置等权限
- iOS 手势UIGestureRecognizer详解
- ios 编译时报 Could not build module xxx 的解决方法尝试
- iOS 常见编译报错及解决方案汇总(持续更新)
- AVMakeRectWithAspectRatioInsideRect 的使用
- graphhopper-ios 编译过程详解
- 算法
- iOS实现LRU缓存
- 架构
- IOS项目架构
- 其他杂项
- 推荐一个好用的Mac精品软件下载站
- 如何能成为一位合格的职业经理人
- 零基础怎么学习视频剪辑?这篇初剪辑学者指南你一定不要错过
- 免费SSL证书的制作
- 《一部手机拍全景》汇总课
- Linux下JAVA常用命令大全
- 即时通讯
- 通讯协议与即时通讯杂谈
- 简述移动端IM开发的那些坑:架构设计、通信协议和客户端
- 基于实践:一套百万消息量小规模IM系统技术要点总结
- PaddleOCR 文字识别深度学习
- PaddleOCR mac 安装指南
- PaddleOCR 标注工具PPOCRLabel的使用
- PaddleOCR 更换模型
- PaddleOCR 自制模型训练