1、方法调用的一个流程
- 如上面的例子,是如何调用eat方法的
- 对象方法:存储在类对象的方法列表中
- 类方法:存储在元类的方法列表中
- 寻找过程
- 通过isa指针去对应的类中查找这个 eat:方法
- 首先去类中的方法编号区去查找是否有eat:方法对应的编号(每当定义的方法的时候,都会在类的“方法编号区”注册一个编号)
- 如果有这个编号,然后根据eat:的编号去类的“方法列表”中查找(方法列表中存储的只是最终函数实现的地址)
- 根据这个函数地址去 “方法区” 调用对应的函数
p调用方法过程.png
2、方法交换的应用场景
-
需求:当是[UIImage imageNamed:@"图片名"]设置图片的时候,要知道这个图片是否有内容,并且要知道是哪个“图片名”不存在而造成的
- 策略一:当在使用[UIImage imageNamed:@"图片名"]的时候去判断并且输出造成失败的“图片名”。缺点:只要用到[UIImage imageNamed:@"图片名"]的地方就需要判断,代码会很臃肿。而且如果是旧项目这样的策略就更不合适了
- 策略二:自定义一个UIImage,在内部实现这些功能。缺点:每次加载一个图片的时候都需要使用自定义的方法,这样是可以实现的,但是当这是一个旧项目的时候,这个方法也就不是一个好的策略。
- 策略三: 给UIImage添加分类,重写imageNamed:方法,但是这样会覆盖系统的方法(系统优先调用分类中的方法),这个策略也不够好
- 策略四:给UIImage添加分类,在分类中实现一个有扩展功能的方法,当调用imageNamed:方法的时候使用runtime来交换这个由扩展功能的方法
使用runtime实现方法交换需要注意:防止“死循环”
//
// UIImage+image.m
// 002-runtime(方法交换)
//
// Created by 紫荆秋雪 on 2017/2/24.
// Copyright © 2017年 Revan. All rights reserved.
//
#import "UIImage+image.h"
#import <objc/runtime.h>
@implementation UIImage (image)
// 把类加载进内存的时候调用,只会调用一次
+ (void)load {
// 获取imageNamed方法
// 获取哪个类的方法
Method imageNamedMethod = class_getClassMethod(self, @selector(imageNamed:));
// 获取需要交换的方法
Method revan_imageNamedMethod = class_getClassMethod(self, @selector(revan_imageNamed:));
// 使用runtime来交换方法
method_exchangeImplementations(imageNamedMethod, revan_imageNamedMethod);
}
//会被多次调用
//+ (void)initialize {
//
//}
+(UIImage *)revan_imageNamed:(NSString *)name {
UIImage *image = [UIImage imageNamed:name];
/// 扩展功能
if (image) {
NSLog(@"图片加载成功");
} else {
NSLog(@"图片加载失败-%@", name);
}
return image;
}
@end
- 调用方法
//
// ViewController.m
// 002-runtime(方法交换)
//
// Created by 紫荆秋雪 on 2017/2/24.
// Copyright © 2017年 Revan. All rights reserved.
//
#import "ViewController.h"
#import "UIImage+image.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIImage *img = [UIImage imageNamed:@"1.png"];
NSLog(@"%@", img);
}
@end
- 为什么会出现“死循环”?
- 当没有进行“方法交换”的时候,方法调用的过程
方法调用过程.png
- 当方法交换之后,方法调用的过程
runtime交换方法后调用方法过程.png
runtime方法交换造成“死循环”原因:
当调用imageNamed:方法的时候,会在会在方法编号区寻找,如果有就会找到存储方法实现的入口,此时由于已经方法交换,所以此地址是指向方法区revan_imageNamed方法实现,所以就会进入分类中定义的有扩展功能的revan_imageNamed的方法中,会执行[UIImage imageNamed]方法,会再次进入UIImage的方法编号区查询是否存在,和上面的过程一样依然会进入revan_imageNamed方法的实现中,这样就造成了“死循环”
正确的runtime交换方法
//
// UIImage+image.m
// 002-runtime(方法交换)
//
// Created by 紫荆秋雪 on 2017/2/24.
// Copyright © 2017年 Revan. All rights reserved.
//
#import "UIImage+image.h"
#import <objc/runtime.h>
@implementation UIImage (image)
// 把类加载进内存的时候调用,只会调用一次
+ (void)load {
// 获取imageNamed方法
// 获取哪个类的方法
Method imageNamedMethod = class_getClassMethod(self, @selector(imageNamed:));
// 获取需要交换的方法
Method revan_imageNamedMethod = class_getClassMethod(self, @selector(revan_imageNamed:));
// 使用runtime来交换方法
method_exchangeImplementations(imageNamedMethod, revan_imageNamedMethod);
}
//会被多次调用
//+ (void)initialize {
//
//}
+(UIImage *)revan_imageNamed:(NSString *)name {
UIImage *image = [UIImage revan_imageNamed:name];
/// 扩展功能
if (image) {
NSLog(@"图片加载成功");
} else {
NSLog(@"图片加载失败-%@", name);
}
return image;
}
@end
- 调用方法
//
// ViewController.m
// 002-runtime(方法交换)
//
// Created by 紫荆秋雪 on 2017/2/24.
// Copyright © 2017年 Revan. All rights reserved.
//
#import "ViewController.h"
#import "UIImage+image.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIImage *img = [UIImage imageNamed:@"1.png"];
NSLog(@"%@", img);
}
@end
- 这次调用方法的过程
- 当在使用系统的UIImage *img = [UIImage imageNamed:@"1.png"];方法的时候,会在UIImage的方法编号区查询是否存在imageNamed:方法,如果存在就会通过"方法列表区"中的地址找到“方法区”中的函数实现,也就是revan_imageNamed:方法,在这个方法实现中有调用了UIImage *image = [UIImage revan_imageNamed:name];所以会在UIImage的方法编号区查找是否存储revan_imageNamed:的编号,如果有再通过“方法列表”中地址,找到“方法区”中的函数实现也就是系统方法 imageNamed方法。
runtime交换方法后调用方法过程.png
3、小结
- 当希望给系统提供的方法扩展功能的时候,可以考虑使用runtime交换方法来实现