个人认为,method是runtime中最重要的一部分了吧,方法的定义很简单:返回值+方法名+入参+方法体。当我们调用一个方法的时候,到底是经过哪些步骤呢,当遇到同名函数的时候,调用的究竟是哪个方法?runtime的Swizzling黑魔法是怎么做到替换系统方法的呢?
ok,带着这些疑问,开始我们的旅程吧!
1. 先来看下runtime里面关于method的定义吧
struct objc_method {
SEL method_name //方法id OBJC2_UNAVAILABLE;
char *method_types //各参数和返回值类型的typeEncode OBJC2_UNAVAILABLE;
IMP method_imp //方法实现 OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
}
1.1 先看一下objc_method_list
看源码:objc_method_list
顾名思义,就是对象的方法列表了,看下结构体里面的内容:
- objc_method_list *obsolete:不明白,先不管
- method_count :方法数量
- space:继续跳过先
- objc_method method_list[1]:这个我感觉应该是指向方法列表的第一个方法,看这样式,方法列表应该是个链式存储结构,搜一下有没有next方法什么的:
class_nextMethodList(Class _Nullable, void * _Nullable * _Nullable)
果然搜到了,那应该是链式存储无疑了哈(要是我错了一定要告诉我)
1.2 继续看objc_method结构体
objc_method里面有三个参数,我们来看下
- method_name
method_name:一看就明白啦,方法名,但是SEL是什么,看起来很面熟对不对,想到一个方法:performSelector:(SEL)aSelector withObject...
,我们这样调用[self performSelector:@selector(report:) withObject:nil afterDelay:2];
,所以很明显SEL=@selector
SEL 类成员方法的指针,可以理解 @selector()就是取类方法的编号,他的行为基本可以等同C语言中的函数指针,只不过C语言中,可以把函数名直接赋给一个函数指针,而Object-C的类不能直接应用函数指针,这样只能做一个@selector语法来取。它的结果是一个SEL类型。这个类型本质是类方法的编号
精简一下:SEL是类成员方法的指针,本质是类方法的编号。
- method_types
各参数和返回值类型的typeEncode - IMP method_imp
方法实现
题外话:imp忽然想到了《权力的游戏》里面的小恶魔,超喜欢他!
言归正传,这个IMP也老重要了,基本面试必问的有没有!
IMP是Implement缩写,IMP 是一个函数指针,这个被指向的函数包含一个接收消息的对象id(self 指针), 调用方法的选标 SEL (方法名),以及不定个数的方法参数,并返回一个id。也就是说 IMP 是消息最终调用的执行代码,是方法真正的实现代码 。我们可以像在C语言里面一样使用这个函数指针。
总之IMP是一个函数指针,保存了方法的地址,是方法的真正实现
下面来写个例子看一下方法列表吧
show the code
/// 定义一个Person类
@interface Person : NSObject
{
NSString * _address;
NSString * _idNo;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *sex;
@property (atomic, assign) int age;
- (instancetype)initWithID:(NSString *)idNo address:(NSString *)address;
- (void)instanceDescription;
+ (void)staticDescription;
@end
/// 定义一个关于方法操作的方法
- (void)methodOperation:(id)obj {
NSLog(@"%s的方法操作:",object_getClassName(obj));
Class cls = object_getClass(obj);
unsigned int outCount = 0;
/// 获取方法列表
Method *methods = class_copyMethodList(cls, &outCount);
for (int i = 0; i < outCount; i++) {
Method method = methods[i];
SEL sel = method_getName(method);
NSLog(@"%@",NSStringFromSelector(sel));
}
free(methods);
}
///调用
Person *person = [[Person alloc] initWithID:@"3715251993098767567" address:@"山东聊城冠县"];
person.name = @"Elaine";
person.sex = @"F";
person.age = 25;
[self methodOperation:person];
/// 输出结果
2018-06-03 04:33:31.876527+0800 RuntimeDemo[2124:294878] Person的方法操作:
2018-06-03 04:33:31.876875+0800 RuntimeDemo[2124:294878] initWithID:address:
2018-06-03 04:33:31.877025+0800 RuntimeDemo[2124:294878] setSex:
2018-06-03 04:33:31.877136+0800 RuntimeDemo[2124:294878] instanceDescription
2018-06-03 04:33:31.877317+0800 RuntimeDemo[2124:294878] sex
2018-06-03 04:33:31.877472+0800 RuntimeDemo[2124:294878] .cxx_destruct
2018-06-03 04:33:31.877615+0800 RuntimeDemo[2124:294878] name
2018-06-03 04:33:31.878932+0800 RuntimeDemo[2124:294878] setName:
2018-06-03 04:33:31.879089+0800 RuntimeDemo[2124:294878] setAge:
2018-06-03 04:33:31.879254+0800 RuntimeDemo[2124:294878] age
SEL 和 IMP的关系
每一个继承于NSObject的类都能自动获得runtime的支持。在这样的一个类中,有一个isa指针,指向该类定义的数据结构体,这个结构体是由编译器编译时为类(需继承于NSObject)创建的.在这个结构体中有包括了指向其父类类定义的指针以及 Dispatch table. Dispatch table是一张SEL和IMP的对应表。
2. runtime的黑魔法
我们用一个例子来说明一下吧,我们经常使用UITableView,UITableView会显示空的,比如网不好或者本来就没有数据什么的,这种情况我们一般都会显示一张空界面,当然可以在调用reload之后加个判断,但是UITableView使用超级频繁,这样加起来太麻烦了,有童鞋说我们可以自己写个新的newReload方法。确实,这比加判断简单多了,不过我们一般都是多人开发,你加的这个方法你知道如何调用,其他人呢,或者新来的员工呢?所以这时候就用到我们的黑魔法了!!!
代码实现:
@implementation UITableView (Runtime)
+(void)load{
Class class = [self class];
SEL originalSelector = @selector(reloadData);
SEL swizzledSelector = @selector(swizzle_reloadData);
/// 获取我们自定义的swizzle_reloadData方法
Method swizzledMethod = class_getInstanceMethod (class, swizzledSelector);
/// 获取系统的reloadData方法
Method originalMethod = class_getInstanceMethod (class, originalSelector);
/// 交换两个方法的实现
method_exchangeImplementations(originalMethod, originalMethod);
}
-(void)swizzle_reloadData {
//执行系统的reloadData方法
[self swizzle_reloadData];
if (self.visibleCells.count == 0) {
NSLog(@"处理空table");
}
}
@end
这样当我们调用系统的reloadData方法时,实际调用的是swizzle_reloadData的实现,当然在swizzle_reloadData里面调用swizzle_reloadData实际上是调用的reloadData的实现,运行一下,没有问题,perfect! really?
再来看另外一种情况,这次不在Category里面实现了,定义一个UITableView的子类MyTableView,在MyTableView里面写入上面的两个方法,调用一下试试,发现并没有执行我们的swizzle_reloadData方法,这是因为MyTableView并没有重写reloadData方法,实际上是调用父类的reloadData,ok,来看下最完整的写法吧
+ (void)load {
static dispatch_once_t once_Token;
/// dispatch_once这里不是“单例”,是保证方法替换只执行一次.
dispatch_once(&once_Token, ^ {
Class class = [self class];
SEL originalSelector = @selector(reloadData);
SEL swizzledSelector = @selector(swizzle_reloadData);
/// 获取我们自定义的swizzle_reloadData方法
Method swizzledMethod = class_getInstanceMethod (class, swizzledSelector);
/// 获取系统的reloadData方法
Method originalMethod = class_getInstanceMethod (class, originalSelector);
/// 交换两个方法的实现
method_exchangeImplementations(originalMethod, originalMethod);
/*
当 class_addMethod 返回 NO 时,说明主类本身就实现了需要被替换的方法,
* 这种情况比较简单,我们直接交换两个方法的实现就可以了。
* 当 class_addMethod 返回 YES 时,说明主类本身没有实现需要被替换的方法,而是继承了父类的实现。
* 这时 class_getInstanceMethod 函数获取到的 originalSelector 指向的就是父类的方法。
* 然后我们再通过 class_replaceMethod 把父类的实现替换到我们自定义的 swizzle_reloadData 中,
* 这样就达到了在 swizzle_reloadData 方法中调用父类实现的目的。
*/
BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
/// 交换两个方法的实现
method_exchangeImplementations(originalMethod, originalMethod);
}
});
}
-(void)swizzle_reloadData {
//执行系统的reloadData方法
[self swizzle_reloadData];
if (self.visibleCells.count == 0) {
NSLog(@"处理空table");
}
}
/// 运行一下
2018-06-03 10:42:16.801534+0800 RuntimeDemo[1813:185046] 处理空table
这次没问题了
为什么runtime可以在运行时更改方法的实现?
其实这块我是不怎么明白的,我的理解是在编译期把方法编译成了runtime中的obj_send()方法,绑定了方法编号SEL,在运行期,通过方法编号在对应的table里面查找方法实现IMP,所以我们能通过修改table里面的对应关系来实现动态更改方法实现,水平有限,自己确实不太明白,希望各位能够跟我讲明白一点,不胜感激!!
附上method相关操作方法:
//判断类中是否包含某个方法的实现
BOOL class_respondsToSelector(Class cls, SEL sel)
//获取类中的方法列表
Method *class_copyMethodList(Class cls, unsigned int *outCount)
//为类添加新的方法,如果方法该方法已存在则返回NO
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
//替换类中已有方法的实现,如果该方法不存在添加该方法
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
//获取类中的某个实例方法(减号方法)
Method class_getInstanceMethod(Class cls, SEL name)
//获取类中的某个类方法(加号方法)
Method class_getClassMethod(Class cls, SEL name)
//获取类中的方法实现
IMP class_getMethodImplementation(Class cls, SEL name)
//获取类中的方法的实现,该方法的返回值类型为struct
IMP class_getMethodImplementation_stret(Class cls, SEL name)
//获取Method中的SEL
SEL method_getName(Method m)
//获取Method中的IMP
IMP method_getImplementation(Method m)
//获取方法的Type字符串(包含参数类型和返回值类型)
const char *method_getTypeEncoding(Method m)
//获取参数个数
unsigned int method_getNumberOfArguments(Method m)
//获取返回值类型字符串
char *method_copyReturnType(Method m)
//获取方法中第n个参数的Type
char *method_copyArgumentType(Method m, unsigned int index)
//获取Method的描述
struct objc_method_description *method_getDescription(Method m)
//设置Method的IMP
IMP method_setImplementation(Method m, IMP imp)
//替换Method
void method_exchangeImplementations(Method m1, Method m2)
//获取SEL的名称
const char *sel_getName(SEL sel)
//注册一个SEL
SEL sel_registerName(const char *str)
//判断两个SEL对象是否相同
BOOL sel_isEqual(SEL lhs, SEL rhs)
//通过块创建函数指针,block的形式为^ReturnType(id self,参数,...)
IMP imp_implementationWithBlock(id block)
//获取IMP中的block
id imp_getBlock(IMP anImp)
//移出IMP中的block
BOOL imp_removeBlock(IMP anImp)
//调用target对象的sel方法
id objc_msgSend(id target, SEL sel, 参数列表...)
参考链接:(排名不分先后)
https://www.jianshu.com/p/23836159aa49
https://www.jianshu.com/p/eac6ed137e06
https://blog.csdn.net/qq_30513483/article/details/52326035
https://www.jianshu.com/p/eac6ed137e06
https://www.jianshu.com/p/4a09d5ebdc2c