Runtime

Runtime的特性是消息/方法的传递,如果消息/方法在对象中找不到,就进行转发。是对获取Objective-C运行时和Objective-C根类底层的访问。

都知道Objective-C是C语言的一门衍生语言,而Runtime的核心就是C。C语言在函数调用的时候就会决定用哪个函数,如果调用了未实现的函数就会报错。然而Objective-C是一门动态语言,即在动态调用过程(运行时)编译器才会决定调用哪个函数。当调用的某对象为被实现时,可以通过”消息转发”进行解决,即编译截断。

下面讲讲Runtime的作用:

动态交换(类方法)

Method Swizzling(方法交换):本质上就是对IMP和SEL进行交换。是发生在运行时的,主要用于将两个Method进行交换。需要注意的是只有在这段Method Swizzling代码执行完毕之后互换才起作用。

IMP:指向方法实现开始的指针。

1
2
> id (*IMP)(id, SEL, ...)
>

此数据类型是指向实现该方法的函数的开头指针。此函数使用为当前CPU体系结构实现的标准C调用约定。第一个参数是指向self的指针(即此类的特定实例的内存或者对应的方法,指向元类的指针)。第二个参数是方法选择器。


SEL:类成员方法的指针,但不同于C语言的函数指针。函数指针直接保存了方法第地址,但SEL只是方法编码。

1
2
> typedef struct objc_selector *SEL;
>

方法选择器用于表示运行时方法的名称。是已使用Objective-C运行时注册的C字符串。加载类时编译器生成选择器将由运行时自动映射。

借用网上的两张图了表达Method Swizzling的原理再好不过了。(有侵权请联系博主谢谢!)


从图中不难看出SEL和IMP都是在Dispatch Table这个表中的,也就是说SEL最后还是要通过Dispatch Table来寻找对应的IMP后在执行。

具体实现直接看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
+ (void)load {
[super load];
// 通过class_getInstanceMethod()函数从当前对象中的method list获取method结构体,如果是类方法就使用class_getClassMethod()函数获取。
Method fromMethod = class_getInstanceMethod([self class], @selector(viewDidLoad));
Method toMethod = class_getInstanceMethod([self class], @selector(swizzlingViewDidLoad));
/**
* 我们在这里使用class_addMethod()函数对Method Swizzling做了一层验证,如果self没有实现被交换的方法,会导致失败。
* 而且self没有交换的方法实现,但是父类有这个方法,这样就会调用父类的方法,结果就不是我们想要的结果了。
* 所以我们在这里通过class_addMethod()的验证,如果self实现了这个方法,class_addMethod()函数将会返回NO,我们就可以对其进行交换了。
*/
if (!class_addMethod([self class], @selector(viewDidLoad), method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) {
method_exchangeImplementations(fromMethod, toMethod);
}
}

// 我们自己实现的方法,也就是和self的viewDidLoad方法进行交换的方法。
- (void)swizzlingViewDidLoad {
NSString *str = [NSString stringWithFormat:@"%@", self.class];
// 我们在这里加一个判断,将系统的UIViewController的对象剔除掉
if(![str containsString:@"UI"]){
NSLog(@"统计打点 : %@", self.class);
}
[self swizzlingViewDidLoad];
}
  • 注意的是在自己定义的方法里[self swizzlingViewDidLoad]的作用其实是在调用原方。因为自己定义的方法是在交换代码结束后才开始调用的,也就是在调用自定义的方法时[self swizzlingViewDidLoad]中的swizzlingViewDidLoad方法其实是相当于原方法了。

为分类添加属性

该功能请转至Category & Extension

在此补充下
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,id _Nullable value, objc_AssociationPolicy policy)
objc_AssociationPolicy policy

1
2
3
4
5
6
policy:关联策略。有五种关联策略。
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)。

获取类中所有的变量和方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)getProperties {

unsigned int count = 0;

// 拷贝出UITextField所有的成员变量列表
Ivar *ivars = class_copyIvarList([UITextField class], &count);

for (int i = 0; i<count; i++) {
// 取出成员变量
Ivar ivar = *(ivars + i);
// 打印成员变量名字
NSLog(@"变量名字 = %s", ivar_getName(ivar));
// 打印成员变量的数据类型
NSLog(@"数据类型 = %s", ivar_getTypeEncoding(ivar));
}
}

实现NSCoding的自动归档和自动解档

首页得了解什么是归档解档

归档(序列化 持久化):就是把需要存储的对象的数据存储到沙盒的Documents目录下的文件中,即存储到磁盘上,实现数据的持久化存储和备份。

解档(接档 反序列化):从磁盘上读取归档下的文件数据,用来完成用户的需求。

首先用代码简单讲解下归档和解档的实现

1.创建一个Model类并添加NSSecureCoding协议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// .h 文件
@interface FourLines : NSObject <NSSecureCoding>

@property (nonatomic,strong) NSString* name;//姓名
@property (nonatomic,assign) NSString* age;//年龄
@property (nonatomic,strong) NSString* work;//职业

@end

// .m文件
@implementation FourLines

- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:self.name forKey:@"name"];
[aCoder encodeObject:self.age forKey:@"age"];
[aCoder encodeObject:self.work forKey:@"work"];
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
self.name = [aDecoder decodeObjectForKey:@"name"];
self.age = [aDecoder decodeObjectForKey:@"age"];
self.work = [aDecoder decodeObjectForKey:@"work"];
}
return self;
}

+ (BOOL)supportsSecureCoding{
return YES;
}
@end

2.ViewController.m文件下

归档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 注释掉的代码为iOS 12之前的实现方式  iOS 12之后以弃用了改方法
- (void)saveClick:(UIButton *)sender {
FourLines *line = [[FourLines alloc] init];
line.name = @"例子";
line.age = @"12";
line.work = @"ios";

NSError *error;
self.archivedData = [NSKeyedArchiver archivedDataWithRootObject:line requiringSecureCoding:YES error:&error];

if (self.archivedData == nil || error) {
NSLog(@"归档失败:%@", error);
return;
}

// FourLines *newPerson = (FourLines *)[NSKeyedUnarchiver unarchivedObjectOfClass:FourLines.class fromData:data error:&error];
// NSLog(@"newPerson====:%@",newPerson.name);

// NSArray *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
// NSString *documentPath = [path firstObject];
// NSLog(@"path====:%@",documentPath);
// NSString *filePath = [documentPath stringByAppendingPathComponent:@"lines"];

// [NSKeyedArchiver archiveRootObject:line toFile:filePath];

}

解档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 注释掉的代码为iOS 12之前的实现方式  iOS 12之后以弃用了改方法
- (void)takeClick:(UIButton *)sender {
NSError *error = nil;
FourLines *per = [NSKeyedUnarchiver unarchivedObjectOfClass:[FourLines class] fromData:self.archivedData error:&error];
if (per == nil || error) {
NSLog(@"解档失败:%@", error);
return;
}
NSLog(@"name = %@ age = %@",per.name,per.age);

// NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
// NSString *documentPath = [paths objectAtIndex:0];
// NSString *filePath = [documentPath stringByAppendingPathComponent:@"lines"];

// FourLines *lines = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
// NSLog(@"名字==%@,性别==%@,年龄==%@",lines.name,lines.age,lines.work);
}

以上是归解档的简单实现方式,接下来了解下Runtime的自动归档和自动解档.

如果还是拿上面的例子来修改的话其实只要改变Model类.m文件下的代码即可(记得添加#import <objc/runtime.h>)

归档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)encodeWithCoder:(NSCoder *)aCoder {
Class currentClass = self.class;

while (currentClass && currentClass != [NSObject class]) {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList(currentClass, &count);
for (int i = 0; i < count; i++) {
Ivar ivar = ivars[i];
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
id value = [self valueForKey:key];
[aCoder encodeObject:value forKey:key];
}
currentClass = [currentClass superclass];
free(ivars);
}
}

解档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
Class currentClass = self.class;
while (currentClass && currentClass != [NSObject class]) {
unsigned int count = 0;
Ivar *ivar = class_copyIvarList(currentClass, &count);
for (int i = 0; i < count; i++) {
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar[i])];

id value = [aDecoder decodeObjectForKey:key];

[self setValue:value forKey:key];
}

currentClass = [currentClass superclass];
free(ivar);
}
}
return self;
}

其实从代码中就可以看出其实就是通过RuntimeIvar来获取成员变量的列表从而使用for循环把成员变量自动添加到归档文件当中,而不用繁琐的一个个添加。


动态添加方法

用意:当硬件内存过小的时候,如果我们将每个方法都直接加载到内存当中,但是却极少的使用这就造成了内存的浪费。像懒加载就很好的避开了这个问题,只有当被用到时才会被加载。

都知道调用一个未实现的方法必然会报错奔溃 如

1
2
3
4
5
6
// FourLines类中未实现`draw:`方法
FourLines *line = [[FourLines alloc] init];
[line performSelector:@selector(draw:) withObject:@"画一条线"];

// 警告:Undeclared selector 'draw:'
// 运行后就会崩溃

当利用- (id)performSelector:(SEL)aSelector withObject:(id)object;
1.调用一个未实现的对象方法时,就会调用+(BOOL)resolveInstanceMethod:(SEL)sel方法

2.调用一个未实现的类方法时,就会调用+(BOOL)resolveClassMethod:(SEL)sel方法

因为我们需要FourLines类中添加如下代码

首选添加#import <objc/message.h>

1
2
3
4
5
6
7
8
9
10
11
// 因为调用的是一个对象方法 所以添加 + (BOOL)resolveInstanceMethod:(SEL)sel 方法

+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(draw:)) {
BOOL isSuccess = class_addMethod(self, sel, (IMP)draw, "v@:@");

return isSuccess;
}

return [super resolveInstanceMethod:sel];
}

class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,const char * _Nullable types)

cls : 表示给哪个类添加方法

SEL _Nonnull name : SEL即方法编号 name 指的应该是方法名称 因为有判断是sel方法 所以对应输入 sel

IMP _Nonnull imp : 表示方法的实现 具体应该是值 SEL 所指定的 IMP 方法的实现。

const char * _Nullable types : 方法类型。如果有参数的方法需要对方法的实现和class_addMethod方法内的方法类型参数做一些修改。


实现字典和模型的自动转换

说到字典和模型的转换很贴切的例子就是在数据请求后的JSON转Model了,因为这个过其实就是字典转模型的过程。直接代码讲解:

首选来实现一个简单的GET请求

1
2
3
4
5
6
7
8
9
10
11
在ViewController中添加一个NSMutableData类型的实例变量,并添加<NSURLSessionDataDelegate>协议

@property (nonatomic, strong) NSMutableData *responseData;

// 设置getter方法
- (NSMutableData *)responseData {
if (_responseData == nil) {
_responseData = [NSMutableData data];
}
return _responseData;
}

实现相关协议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 请求的是一个天气预报的API
- (void)networkRequest {
NSURL *url = [NSURL URLWithString:@"http://www.weather.com.cn/data/sk/101110101.html"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];

NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];

NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request];

[dataTask resume];
}

// 接收到服务器响应的时候调用该方法
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {

completionHandler(NSURLSessionResponseAllow);

}

// 接收到服务器返回数据的时候会调用该方法,如果数据较大那么该方法可能会调用多次
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
[self.responseData appendData:data];
}

// 当请求完成(成功|失败)的时候会调用该方法,如果请求失败,则error有值
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
if (error == nil) {

NSError *error;
NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:self.responseData options:kNilOptions error:&error];

FourLines *lines = [FourLines modelWithDict2:dic];
NSLog(@"lines = %@",lines.weatherinfo);

} else {
NSLog(@"请求失败 = %@",error);
}
}

接下来设置Model类

1
2
// 添加两个类方法 来对NSDictionary进行类型转换
+ (instancetype)modelWithDict:(NSDictionary *)dict;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 实现
+ (instancetype)modelWithDict:(NSDictionary *)dict {
id obj = [[self alloc] init];

unsigned int outCount = 0;

Ivar *ivarList = class_copyIvarList([self class], &outCount);

for (int i = 0; i < outCount; i++) {
Ivar ivar = ivarList[i];

const char *name = ivar_getName(ivar);

NSString *ivarName = [NSString stringWithUTF8String:name];

NSString *key = [ivarName substringFromIndex:1];

id value = dict[key];

if (value) {
[obj setValue:value forKey:key];
}
}

free(ivarList);

return obj;
}

重写- (NSString *)description;方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
- (NSString *)description {
unsigned int count;
const char *clasName = object_getClassName(self);
NSMutableString *string = [NSMutableString stringWithFormat:@"<%s: %p>:[ \n",clasName, self];
Class clas = NSClassFromString([NSString stringWithCString:clasName encoding:NSUTF8StringEncoding]);
Ivar *ivars = class_copyIvarList(clas, &count);
for (int i = 0; i < count; i++) {
@autoreleasepool {
Ivar ivar = ivars[i];
const char *name = ivar_getName(ivar);
//得到类型
NSString *type = [NSString stringWithCString:ivar_getTypeEncoding(ivar) encoding:NSUTF8StringEncoding];
NSString *key = [NSString stringWithCString:name encoding:NSUTF8StringEncoding];
id value = [self valueForKey:key];
NSLog(@"type = %@",type);
//确保BOOL 值输出的是YES 或 NO,这里的B是我打印属性类型得到的……
if ([type isEqualToString:@"B"]) {
value = (value == 0 ? @"NO" : @"YES");
}
[string appendFormat:@"\t%@ = %@\n",[self delLine:key], value];
}
}
[string appendFormat:@"]"];
return string;
}

- (NSString *)delLine:(NSString *)string {
if ([string hasPrefix:@"_"]) {
return [string substringFromIndex:1];
}
return string;
}

打印输出NSLog(@”lines = %@”,lines.weatherinfo);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2019-05-12 19:34:27.705523+0800 Test[2666:406683] lines = {
AP = "962.7hPa";
Radar = "JC_RADAR_AZ9290_JB";
SD = "52%";
WD = "\U897f\U5357\U98ce";
WS = "\U5c0f\U4e8e3\U7ea7";
WSE = "<3";
city = "\U897f\U5b89";
cityid = 101110101;
isRadar = 1;
njd = "\U6682\U65e0\U5b9e\U51b5";
sm = "1.2";
temp = "23.3";
time = "18:00";
}

返回的JSON格式还有模型套模型,模型中的数组中装着模型。在此就讲解最简单的一种,后期在讲解数据请求时在来细致讨论。

其实Runtime实现字典和模型的转换,其实跟自动归解档原理上都是很像是的。都是利用RuntimeIvar来获取成员变量列表,在利用for寻来来自动设置value和key,从而实现自动的转换。