简介 前面几篇文章我们介绍了Runtime中NSObject的数据结构,下面我们来介绍下Runtime在平时开发过程中的常用方法以及实现原理。
方法交换 方法交换可以说是我们在开发中比较常用的方法,尤其是在一些需要埋点或者统计的位置,我们可以通过Hook系统的某些方法,通过在这些方法中添加自己代码的方式实现在不入侵业务的情况下实现功能。
首先我们来看下方法交换的实现,下面这段代码使我们在网上随便找了一份方法交换的代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 + (BOOL )swizzleMethod:(Class)class orgSel:(SEL)origSel swizzSel:(SEL)altSel { Method origMethod = class_getInstanceMethod(class , origSel); Method altMethod = class_getInstanceMethod(class , altSel); if (!origMethod || !altMethod) { return NO ; } BOOL didAddMethod = class_addMethod(class ,origSel, method_getImplementation(altMethod), method_getTypeEncoding(altMethod)); if (didAddMethod) { class_replaceMethod(class ,altSel, method_getImplementation(origMethod), method_getTypeEncoding(origMethod)); } else { method_exchangeImplementations(origMethod, altMethod); } return YES ; }
这个方法是交换class类中origSel
方法和swizzSel
方法。我们先来分析这个方法的实现步骤:
swizzleMethod
方法的三个参数分别为class交换方法的类 origSel
原方法 altSel
要替换的方法
class_getInstanceMethod
通过runtime
的这个方法获取方法Method结构体
给class添加origSel
方法 方法的实现是altSel
的实现
如果第三步添加成功 则替换class
中altSel
方法实现为origSel
如果第三步添加失败 则交换origMethod
和altMethod
方法的实现
下面我们先针对上面的实现提出几个自己的问题,然后带着这些问题去研究为什么需要这么实现,这么实现是否有其他的问题。
问题一:class_getInstanceMethod
到底是从哪里获取方法实现?如果是父类方法可以获取到吗? 问题二:交换方法之前为何要调用class_addMethod
方法先去添加方法? 问题三:为何根据添加方法class_addMethod
的返回值去判断下一步的操作? 问题四:method_exchangeImplementations
和class_replaceMethod
的区别是什么? 问题五:大多数方法都是交换一个类中的两个方法,是否可以交换两个不同类的方法呢?方法名相同可以交换吗?
下面我们带着这些问题来看方法交换的具体实现:
class_getInstanceMethod 我们先来看下方法的实现:
1 2 3 4 5 6 7 8 9 10 11 Method class_getInstanceMethod(Class cls, SEL sel) { if (!cls || !sel) return nil ; lookUpImpOrNil(cls, sel, nil , NO , NO , YES ); return _class_getMethod(cls, sel); }
从这个方法的实现中我们看到在正式调用_class_getMethod
方法获取实例方法前,还调用了lookUpImpOrNil方法,这个方法又是做什么的呢?关于这个问题,我们在后面消息转发模块会进一步介绍,这里暂时略过。
我们来看下递归查找方法的实现:
getMethod_nolock 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static method_t * getMethod_nolock(Class cls, SEL sel) { method_t *m = nil ; runtimeLock.assertLocked(); assert(cls->isRealized()); while (cls && ((m = getMethodNoSuper_nolock(cls, sel))) == nil ) { cls = cls->superclass; } return m; }
看到这个实现后,我们的第一个问题的答案就显而易见了。class_getInstanceMethod
方法在获取实例方法时会递归遍历所有的父类来查找sel
对应的方法,并通过getMethodNoSuper_nolock
方法获取方法的method_t
结构。
那么getMethodNoSuper_nolock
是如何在methodlist
中查找对应的方法的呢?
getMethodNoSuper_nolock 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 / 在类的方法列表中查找对应的方法实现static method_t * getMethodNoSuper_nolock(Class cls, SEL sel) { runtimeLock.assertLocked(); assert(cls->isRealized()); for (auto mlists = cls->data()->methods.beginLists(), end = cls->data()->methods.endLists(); mlists != end; ++mlists) { method_t *m = search_method_list(*mlists, sel); if (m) return m; } return nil ; }
我们先忽略for循环里的逻辑,我们先看下search_method_list
这个方法:
search_method_list 这个方法的实现 主要实现是findMethodInSortedMethodList
方法实现,我们直接看下这个方法
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 41 42 static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list) { assert(list); const method_t * const first = &list->first; const method_t *base = first; const method_t *probe; uintptr_t keyValue = (uintptr_t)key; uint32_t count; for (count = list->count; count != 0 ; count >>= 1 ) { probe = base + (count >> 1 ); uintptr_t probeValue = (uintptr_t)probe->name; if (keyValue == probeValue) { while (probe > first && keyValue == (uintptr_t)probe[-1 ].name) { probe--; } return (method_t *)probe; } if (keyValue > probeValue) { base = probe + 1 ; count--; } } return nil ; }
通过方法的注释我们可以清晰的看到这个方法的功能是:在方法列表中查找传入的方法,查找方法为二分查找,比较方法是否相等的条件为判断方法名是否相同。当然这里还存在一个如果方法列表中包含多个同名函数我们会向前查找,找到方法列表中第一个与要查找方法同名的方法。
class_addMethod 顾名思义,这个方法的作用是在类中添加一个方法,我们先来看下方法实现:
1 2 3 4 5 6 7 class_addMethod(Class cls, SEL name, IMP imp, const char *types) { if (!cls) return NO ; mutex_locker_t lock(runtimeLock); return ! addMethod(cls, name, imp, types ?: "" , NO ); }
class_addMethod
中主要是调用了addMethod
静态方法,该方法的返回值是要添加的方法的实现。
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 41 42 43 44 45 46 47 48 49 static IMP addMethod(Class cls, SEL name, IMP imp, const char *types, bool replace) { IMP result = nil ; runtimeLock.assertLocked(); checkIsKnownClass(cls); assert(types); assert(cls->isRealized()); method_t *m; if ((m = getMethodNoSuper_nolock(cls, name))) { if (!replace) { result = m->imp; } else { result = _method_setImplementation(cls, m, imp); } } else { method_list_t *newlist; newlist = (method_list_t *)calloc(sizeof (*newlist), 1 ); newlist->entsizeAndFlags = (uint32_t)sizeof (method_t) | fixed_up_method_list; newlist->count = 1 ; newlist->first.name = name; newlist->first.types = strdupIfMutable(types); newlist->first.imp = imp; prepareMethodLists(cls, &newlist, 1 , NO , NO ); cls->data()->methods.attachLists(&newlist, 1 ); flushCaches(cls); result = nil ; } return result; }
addMethod
方法主要的实现为:
先从cls中获取名为name的方法实现 如果可以找到方法,判断是否需要替换方法实现 如果需要则替换 如果不需要直接返回,如果找不到新创建一个method_list_t
结构体 并赋值,将新建的newlist
添加到已有的方法列表中,刷新缓存 并返回nil。
因此这个方法的返回值表示:
如果返回值不为空则表示这个方法本来就存在于方法列表中,如果返回值为nil则表示这个方法不存在与方法列表中。而class_addMethod
则是对addMethod
方法的返回值进行取反,因此class_addMethod
的返回值如果为true则表示方法不存在与方法列表中(然后添加到了方法列表中),返回值为false则表示方法之前就在方法列表中,不需要添加。
看完这部分问题二和问题三的答案也比较明显了:实际上class_addMethod
的调用实际上是为了确认
要替换的方法是否已经在方法列表中(方法是否已经实现)
如果不在方法列表中,那么需要先将方法添加到方法列表中
如果方法没有在方法列表中,那么我们要完成方法交换需要调用class_replaceMethod
方法,如果方法列表中已经有了该方法那么我们直接调用method_exchangeImplementations
来实现方法的交换。
class_replaceMethod 我们先来看下这个方法的实现:
1 2 3 4 5 6 7 8 9 IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types) { if (!cls) return nil ; mutex_locker_t lock(runtimeLock); return addMethod(cls, name, imp, types ?: "" , YES ); }
第一眼看到这个方法的实现时,我也是一脸懵逼,说好的替换方法呢 为何只是调用了addMethod
?
首先 我们要务必再次明确调用这个方法的前提是:这个类中并没有要交换的方法。虽然我们在前一步中添加了实现,但是如果我们要实现方法交换肯定是要存在两个方法的,因此带着这个疑问我们再次看下方法交换的代码实现:
1 2 3 4 5 6 7 8 9 BOOL didAddMethod = class_addMethod(class ,origSel, method_getImplementation(altMethod), method_getTypeEncoding(altMethod)); if (didAddMethod) { class_replaceMethod(class ,altSel, method_getImplementation(origMethod), method_getTypeEncoding(origMethod)); }
在调用class_addMethod我们传递的参数是:
方法
方法名
参数
class_addMethod
origSel 原始方法名
新自定义方法的方法实现和方法参数编码
class_replaceMethod
altSel 新定义的方法名
原始方法的方法实现和方法编码
method_exchangeImplementations 下面我们在来看下这个方法的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 void method_exchangeImplementations(Method m1, Method m2) { if (!m1 || !m2) return ; mutex_locker_t lock(runtimeLock); IMP m1_imp = m1->imp; m1->imp = m2->imp; m2->imp = m1_imp; flushCaches(nil ); updateCustomRR_AWZ(nil , m1); updateCustomRR_AWZ(nil , m2); }
交换方法的实现就更简单了:直接交换Method
结构体中的imp
指针,imp
指针又是指向了方法的实现。因此这里就完成了两个方法的交换。
上面我们就看完了方法交换的整个流程,我们仍有几个问题没想通
问题一: 大多数方法都是交换一个类中的两个方法,是否可以交换两个不同类的方法呢?方法名相同可以交换吗?
我封装了下面这个方法 用来交换两个类的两个方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + (BOOL )swizzleMethod:(Class)originClass swizzCls:(Class)swizzClass orgSel:(SEL)origSel swizzSel:(SEL)swizzSel { Method origMethod = class_getInstanceMethod(originClass, origSel); Method altMethod = class_getInstanceMethod(swizzClass, swizzSel); NSLog (@"swizzleMethod origMethod %@" ,NSStringFromSelector (method_getName(origMethod))); NSLog (@"swizzleMethod altMethod %@" ,NSStringFromSelector (method_getName(altMethod))); BOOL didAddMethod = class_addMethod(originClass,origSel, method_getImplementation(altMethod), method_getTypeEncoding(altMethod)); NSLog (@"swizzleMethod didAddMethod %@" ,@(didAddMethod)); if (didAddMethod) { class_replaceMethod(swizzClass,swizzSel, method_getImplementation(origMethod), method_getTypeEncoding(origMethod)); } else { method_exchangeImplementations(origMethod, altMethod); } return YES ; }
方法交换:
1 [self swizzleMethod:[Person class ] swizzCls:[Tiger class ] orgSel:@selector (speak) swizzSel:@selector (yall)];
这里我们交换了Person
类的speak
方法和Tiger
类的yall
方法,然后分别调用这两个方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Tiger.m - (void )yall { NSLog (@"Tiger -- yall" ); } ------- Person.m - (void )speak { NSLog (@"Person speak" ); } ViewController.m - (void )testExchangeTwoClsTwoMethod { Person *p = [[Person alloc] init]; [p speak]; NSLog (@"-----" ); Tiger *t = [[Tiger alloc] init]; [t yall]; }
打印结果为:
1 2 3 4 2020 -10 -25 20 :05 :24.400998 +0800 MethodSwizzlzeDemo[27125 :11429192 ] swizzleMethod didAddMethod 0 2020 -10 -25 20 :05 :24.455871 +0800 MethodSwizzlzeDemo[27125 :11429192 ] Tiger -- yall2020 -10 -25 20 :05 :24.456007 +0800 MethodSwizzlzeDemo[27125 :11429192 ] -----2020 -10 -25 20 :05 :24.456143 +0800 MethodSwizzlzeDemo[27125 :11429192 ] Person speak
从调用结果可以看出我们的方法交换是成功的。因此 这可以证明我们是可以交换两个类的两个方法的。
问题二: 我们是否可以交换一个不存在的方法?会有什么效果?
方法交换调用:
1 [self swizzleMethod:[Person class ] swizzCls:[Tiger class ] orgSel:@selector (speak) swizzSel:@selector (speak)];
我们的Tiger类并没有speak方法,我们看下控制台输出
1 2 3 4 5 6 2020 -10 -25 20 :08 :34.567600 +0800 MethodSwizzlzeDemo[27181 :11431963 ] swizzleMethod origMethod speak2020 -10 -25 20 :08 :34.568215 +0800 MethodSwizzlzeDemo[27181 :11431963 ] swizzleMethod altMethod (null)2020 -10 -25 20 :08 :34.568399 +0800 MethodSwizzlzeDemo[27181 :11431963 ] swizzleMethod didAddMethod 0 2020 -10 -25 20 :08 :34.614912 +0800 MethodSwizzlzeDemo[27181 :11431963 ] Person speak2020 -10 -25 20 :08 :34.615029 +0800 MethodSwizzlzeDemo[27181 :11431963 ] -----2020 -10 -25 20 :08 :34.615114 +0800 MethodSwizzlzeDemo[27181 :11431963 ] Tiger -- yall
由上面控制台输出结果我们看出,方法交换实际并未成功,且在方法交换是打印altMethod
结果为null。因此如果去交换一个并不存在的方法是不可能实现的。
但是如果我们换成另外一种写法:
1 2 [self swizzleMethod:[Tiger class ] swizzCls:[Person class ] orgSel:@selector (speak) swizzSel:@selector (speak)];
我们再看下输出结果:
1 2 3 2020 -10 -26 22 :29 :43.179796 +0800 MethodSwizzlzeDemo[1853 :33972 ] swizzleMethod origMethod (null)2020 -10 -26 22 :29 :43.180534 +0800 MethodSwizzlzeDemo[1853 :33972 ] swizzleMethod altMethod speak2020 -10 -26 22 :29 :43.180695 +0800 MethodSwizzlzeDemo[1853 :33972 ] swizzleMethod didAddMethod 1
从日志我们看到,方法添加成功!也就是说 我们可以交换一个不存在的方法,因为在方法交换时我们在利用运行时方法在类里添加了对应方法。那么我们看下是否可以成功调用呢?
1 2 3 4 5 6 7 - (void )testExchangeTwoClsTwoMethod { Person *p = [[Person alloc] init]; [p speak]; NSLog (@"-----" ); Tiger *t = [[Tiger alloc] init]; [t performSelector:@selector (speak)]; }
打印结果:
1 2 3 4 5 6 2020 -10 -26 22 :35 :40.918735 +0800 MethodSwizzlzeDemo[1957 :39109 ] swizzleMethod origMethod (null)2020 -10 -26 22 :35 :40.919307 +0800 MethodSwizzlzeDemo[1957 :39109 ] swizzleMethod altMethod speak2020 -10 -26 22 :35 :40.919464 +0800 MethodSwizzlzeDemo[1957 :39109 ] swizzleMethod didAddMethod 1 2020 -10 -26 22 :36 :15.806909 +0800 MethodSwizzlzeDemo[1957 :39109 ] Person speak2020 -10 -26 22 :36 :15.807017 +0800 MethodSwizzlzeDemo[1957 :39109 ] -----2020 -10 -26 22 :36 :15.807117 +0800 MethodSwizzlzeDemo[1957 :39109 ] Person speak
从结果我们看出,调用tiger
的speak
方法时打印出了Person speak
,这说明为Tiger动态增加了一个方法方法实现和Person
相同,然后在去替换Person
类的speak
时因为originMethod
为null
因此替换不成功,因此这个方法相当于给Tiger
增加了一个speak
方法。
问题三: 仔细看下下面这段代码:
1 2 [self swizzleMethod:[Tiger class ] swizzCls:[Person class ] orgSel:@selector (walk) swizzSel:@selector (speak)];
Tiger
继承自Animal
,walk
是Animal
对象中的一个实例方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @interface Animal : NSObject - (void )walk; - (void )eat;@end @interface Tiger : Animal - (void )yall;@end
但是当我断点调试时,却发现
从图中我们看到:altMethod
和origMethod
这两个方法是可以获取到的,但是didAddMethod
的值返回了YES。这说明我们在Tiger
的方法列表中并没有找到walk
方法。但是使用class_getInstanceMethod
方法获取到的方法是有值的,这是怎么回事呢?
当我们仔细查看class_addMethod
方法实现时我们发现了一处代码:
1 2 3 4 5 if ((m = getMethodNoSuper_nolock(cls, name))) { } else { }
我们在对比class_getInstanceMethod
方法获取方法的位置实现:
1 2 3 4 while (cls && ((m = getMethodNoSuper_nolock(cls, sel))) == nil ) { cls = cls->superclass; }
经过代码的对比我们很容易发现,在class_addMethod
方法中我们只是查找了当前类的方法列表,而在class_getInstanceMethod
方法中我们会递归查询当前类以及其父类的方法列表。
因此在Tiger类中找不到walk方法是正常的。
类方法的方法交换 跟交换对象方法相同,我们先来看下这个方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 + (BOOL )swizzleMethod:(Class)class orgSel:(SEL)origSel swizzSel:(SEL)altSel { Method origMethod = class_getClassMethod(class , origSel); Method altMethod = class_getClassMethod(class , altSel); BOOL didAddMethod = class_addMethod(class ,origSel, method_getImplementation(altMethod), method_getTypeEncoding(altMethod)); if (didAddMethod) { class_replaceMethod(class ,altSel, method_getImplementation(origMethod), method_getTypeEncoding(origMethod)); } else { method_exchangeImplementations(origMethod, altMethod); } return YES ; }
从上面的代码我们看到,交换类方法和交换对象方法相比我们只是修改了获取方法结构的方法为class_getClassMethod
。
下面我们看下这个方法的实现:
class_getClassMethod 1 2 3 4 5 6 7 8 Method class_getClassMethod(Class cls, SEL sel) { if (!cls || !sel) return nil ; return class_getInstanceMethod(cls->getMeta(), sel); }
其实内部实现很简单,获取类的元类对应的实例方法。
下面我们看下方法调用和最终结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 + (void )load { static dispatch_once_t onceToken; dispatch_once (&onceToken, ^{ [self swizzleClassMethod:[self class ] orgSel:@selector (classMethod1) swizzSel:@selector (classMethod2)]; }); } + (void )classMethod1 { NSLog (@"classMethod1" ); } + (void )classMethod2 { NSLog (@"classMethod2" ); } - (void )viewDidLoad { [super viewDidLoad]; [ViewController classMethod1]; NSLog (@"-------------------------" ); [ViewController classMethod2]; }
我们来看下打印结果:
1 2 3 4 2020 -10 -27 22 :42 :20.314694 +0800 MethodSwizzlzeDemo[4950 :153595 ] swizzleClassMethod didAddMethod 1 2020 -10 -27 22 :42 :20.360568 +0800 MethodSwizzlzeDemo[4950 :153595 ] classMethod12020 -10 -27 22 :42 :20.360679 +0800 MethodSwizzlzeDemo[4950 :153595 ] -------------------------2020 -10 -27 22 :42 :20.360790 +0800 MethodSwizzlzeDemo[4950 :153595 ] classMethod2
从结果我们看到,我们的交换方法并不成功。而且我们发现didAddMethod
标记为1,也就是说在class
中并未找到类方法origSel
。很明显问题出在了class_addMethod
方法里。上面我们已经分析了这个方法,他的实现实际上是从类中非递归的在方法列表中查找方法。
但是我们都知道类方法实际上是存放在元类中的,这一点在class_getClassMethod
的实现中我们也能得到验证
1 class_getInstanceMethod(cls->getMeta(), sel);
那么我们这里在方法添加时也应该从元类的方法列表中查找对应的类方法,那么如何获取这个类的元类呢,我们知道每个类对象中都有一个isa指针指向了他的元类。因此我们可以通过下面这个方法获取。
object_getClass 1 2 3 4 5 Class object_getClass(id obj) { if (obj) return obj->getIsa(); else return Nil; }
利用上面这个方法我们对方法交换函数进行修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 + (BOOL )swizzleMethod:(Class)class orgSel:(SEL)origSel swizzSel:(SEL)altSel { Method origMethod = class_getClassMethod(class , origSel); Method altMethod = class_getClassMethod(class , altSel); BOOL didAddMethod = class_addMethod(object_getClass(class ),origSel, method_getImplementation(altMethod), method_getTypeEncoding(altMethod)); if (didAddMethod) { class_replaceMethod(class ,altSel, method_getImplementation(origMethod), method_getTypeEncoding(origMethod)); } else { method_exchangeImplementations(origMethod, altMethod); } return YES ; }
我们再来看下输出结果:
1 2 3 4 2020 -10 -27 22 :56 :35.223362 +0800 MethodSwizzlzeDemo[5144 :163474 ] swizzleClassMethod didAddMethod 0 2020 -10 -27 22 :56 :35.267136 +0800 MethodSwizzlzeDemo[5144 :163474 ] classMethod22020 -10 -27 22 :56 :35.267236 +0800 MethodSwizzlzeDemo[5144 :163474 ] -------------------------2020 -10 -27 22 :56 :35.267320 +0800 MethodSwizzlzeDemo[5144 :163474 ] classMethod1
我们发现这里方法交换是成功的了!这样也就完成了类方法的交换
Demo demo代码地址