现在一段时间在回顾小runtime 的一些知识点。搜了一些资料,自己学习后总结下,以便后面回顾。
Method Swizzling:
我们可以使用苹果的“黑魔法”Method Swizzling,Method Swizzling本质上就是对IMP和SEL进行交换。
Method Swizzling原理:
Method Swizzing是发生在运行时的,主要用于在运行时将两个Method进行交换,我们可以将Method Swizzling代码写到任何地方,但是只有在这段Method Swilzzling代码执行完毕之后互换才起作用。
上面图一中selector2原本对应着IMP2,但是为了更方便的实现特定业务需求,我们在图二中添加了selector3和IMP3,并且让selector2指向了IMP3,而selector3则指向了IMP2,这样就实现了“方法互换”。
在OC语言的runtime特性中,调用一个对象的方法就是给这个对象发送消息。是通过查找接收消息对象的方法列表,从方法列表中查找对应的SEL,这个SEL对应着一个IMP(一个IMP可以对应多个SEL),通过这个IMP找到对应的方法调用。
在每个类中都有一个Dispatch Table,这个Dispatch Table本质是将类中的SEL和IMP(可以理解为函数指针)进行对应。而我们的Method Swizzling就是对这个table进行了操作,让SEL对应另一个IMP。
应用场景:
当第三方框架 或者 原生方法不能满足我们的时候,我们可以在保持系统原有的方法功能基础上,添加额外的功能
需求:
加载一张图片直接调用系统的[UIImage imageNamed:@"image"];
是不能知道到底有没有加载成功。给系统的方法添加提示是否加载成功的功能
方案:
1、继承系统类,重写方法 (弊端:每次使用都需要导入)
2、使用runtime 交换方法
实现步骤:
1、给系统的方法添加分类 UIImage (Image)
2、自己实现一个带有扩展功能的方法 +(UIImage *)ym_imageNamed:(NSString *)name
3、交换方法
下面就摘录一种比较严谨的写法。
+(void)load{
[super load];
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = object_getClass((id)self);
Method imgNameMethod = class_getClassMethod(class, @selector(imageNamed:));
Method ym_imgNameMethod = class_getClassMethod(class, @selector(ym_imageNamed:))
;
//
BOOL didAddMethod = class_addMethod(class, @selector(imageNamed:), method_getImplementation(ym_imgNameMethod), method_getTypeEncoding(ym_imgNameMethod));
if (didAddMethod) {
class_replaceMethod(class, @selector(ym_imageNamed:), method_getImplementation(imgNameMethod), method_getTypeEncoding(imgNameMethod));
}else{
method_exchangeImplementations(imgNameMethod, ym_imgNameMethod);
}
});
}
+(UIImage *)ym_imageNamed:(NSString *)name{
UIImage *image = [UIImage ym_imageNamed:name];
if (image) {
NSLog(@"runtime添加额外功能---加载成功");
}else{
NSLog(@"runtime添加额外功能---加载失败");
}
return image;
}
解析:
dispatch_once
这里不是单例,这里是确保方法交换只被执行一次。因为load
作为一个+
方法是可以手动调用的,当load
调用的次数为奇数的时候交换方法能成功,为偶数就会失败
class_addMethod:
如果发现方法已经存在,会失败返回,也可以用来做检查用,我们这里是为了避免源方法没有实现的情况;如果方法没有存在,我们则先尝试添加被替换的方法的实现
1.如果返回成功:则说明被替换方法没有存在.也就是被替换的方法没有被实现,我们需要先把这个方法实现,然后再执行我们想要的效果,用我们自定义的方法去替换被替换的方法. 这里使用到的是class_replaceMethod
这个方法. class_replaceMethod
本身会尝试调用class_addMethod
和method_setImplementation
,所以直接调用class_replaceMethod
就可以了)
2.如果返回失败:则说明被替换方法已经存在.直接将两个方法的实现交换即
拓展:
Method Swizzling类簇
之前我也说到,在我们项目开发过程中,经常因为NSArray数组越界或者NSDictionary的key或者value值为nil等问题导致的崩溃,对于这些问题苹果并不会报一个警告,而是直接崩溃,感觉苹果这样确实有点“太狠了”。
由此,我们可以根据上面所学,对NSArray、NSMutableArray、NSDictionary、NSMutableDictionary等类进行Method Swizzling,实现方式还是按照上面的例子来做。但是....你发现Method Swizzling根本就不起作用,代码也没写错啊,到底是什么鬼?
这是因为Method Swizzling对NSArray这些的类簇是不起作用的。因为这些类簇类,其实是一种抽象工厂的设计模式。抽象工厂内部有很多其它继承自当前类的子类,抽象工厂类会根据不同情况,创建不同的抽象对象来进行使用。例如我们调用NSArray的objectAtIndex:方法,这个类会在方法内部判断,内部创建不同抽象类进行操作。
所以也就是我们对NSArray类进行操作其实只是对父类进行了操作,在NSArray内部会创建其他子类来执行操作,真正执行操作的并不是NSArray自身,所以我们应该对其“真身”进行操作。
下面我们实现了防止NSArray因为调用objectAtIndex:方法,取下标时数组越界导致的崩溃:
#import "NSArray+LXZArray.h"
#import "objc/runtime.h"
@implementation NSArray (LXZArray)
+ (void)load {
[super load];
Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(lxz_objectAtIndex:));
method_exchangeImplementations(fromMethod, toMethod);
}
- (id)lxz_objectAtIndex:(NSUInteger)index {
if (self.count-1 < index) {
// 这里做一下异常处理,不然都不知道出错了。
@try {
return [self lxz_objectAtIndex:index];
}
@catch (NSException *exception) {
// 在崩溃后会打印崩溃信息,方便我们调试。
NSLog(@"---------- %s Crash Because Method %s ----------\n", class_getName(self.class), __func__);
NSLog(@"%@", [exception callStackSymbols]);
return nil;
}
@finally {}
} else {
return [self lxz_objectAtIndex:index];
}
}
@end
你会发现,__NSArrayI才是NSArray真正的类,而NSMutableArray又不一样????。
我们可以通过runtime函数获取真正的类:
objc_getClass("__NSArrayI")
下面我们列举一些常用的类簇的“真身”:Method Swizzling封装
在项目中我们肯定会在很多地方用到Method Swizzling,而且在使用这个特性时有很多需要注意的地方。我们可以将Method Swizzling封装起来,也可以使用一些比较成熟的第三方。
在这里我推荐Github上星最多的一个第三方-jrswizzle
讨论
这里是一些 Method Swizzling的陷阱:
Method swizzling is not atomic
Changes behavior of un-owned code
Possible naming conflicts
Swizzling changes the method's arguments
The order of swizzles matters
Difficult to understand (looks recursive)
Difficult to debug
我将逐一分析这些点,增进对Method Swizzling的理解的同时,并搞懂如何应对。
Method swizzling is not atomic
我所见过的使用method swizzling实现的方法在并发使用时基本都是安全的。95%的情况里这都不会是个问题。通常你替换一个方法的实现,是希望它在整个程序的生命周期里有效的。也就是说,你会把 method swizzling 修改方法实现的操作放在一个加号方法 +(void)load里,并在应用程序的一开始就调用执行。你将不会碰到并发问题。假如你在 +(void)initialize初始化方法中进行swizzle,那么……rumtime可能死于一个诡异的状态。
Changes behavior of un-owned code
这是swizzling的一个问题。我们的目标是改变某些代码。swizzling方法是一件灰常灰常重要的事,当你不只是对一个NSButton类的实例进行了修改,而是程序中所有的NSButton实例。因此在swizzling时应该多加小心,但也不用总是去刻意避免。
想象一下,如果你重写了一个类的方法,而且没有调用父类的这个方法,这可能会引起问题。大多数情况下,父类方法期望会被调用(至少文档是这样说的)。如果你在swizzling实现中也这样做了,这会避免大部分问题。还是调用原始实现吧,如若不然,你会费很大力气去考虑代码的安全问题。
Possible naming conflicts
命名冲突贯穿整个Cocoa的问题. 我们常常在类名和类别方法名前加上前缀。不幸的是,命名冲突仍是个折磨。但是swizzling其实也不必过多考虑这个问题。我们只需要在原始方法命名前做小小的改动来命名就好,比如通常我们这样命名:
1. @interface NSView : NSObject
2. - (void)setFrame:(NSRect)frame;
3. @end
6. @implementation NSView (MyViewAdditions)
9. - (void)my_setFrame:(NSRect)frame {
10. // do custom work
11. [self my_setFrame:frame];
12. }
15. + (void)load {
16. [self swizzle:@selector(setFrame:) with:@selector(my_setFrame:)];
17. }
20. @end
这段代码运行正确,但是如果my_setFrame: 在别处被定义了会发生什么呢?
这个问题不仅仅存在于swizzling,这里有一个替代的变通方法:
@implementation NSView (MyViewAdditions)
static void MySetFrame(id self, SEL _cmd, NSRect frame);
static void (*SetFrameIMP)(id self, SEL _cmd, NSRect frame);
static void MySetFrame(id self, SEL _cmd, NSRect frame) {
// do custom work
SetFrameIMP(self, _cmd, frame);
}
+ (void)load {
[self swizzle:@selector(setFrame:) with:(IMP)MySetFrame store:(IMP *)&SetFrameIMP];
}
@end
看起来不那么Objectice-C了(用了函数指针),这样避免了selector的命名冲突。
最后给出一个较完美的swizzle方法的定义:
typedef IMP *IMPPointer;
BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) {
IMP imp = NULL;
Method method = class_getInstanceMethod(class, original);
if (method) {
const char *type = method_getTypeEncoding(method);
imp = class_replaceMethod(class, original, replacement, type);
if (!imp) {
imp = method_getImplementation(method);
}
}
if (imp && store) { *store = imp; }
return (imp != NULL);
}
@implementation NSObject (FRRuntimeAdditions)
+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {
return class_swizzleMethodAndStore(self, original, replacement, store);
}
@end
Swizzling changes the method's arguments
我认为这是最大的问题。想正常调用method swizzling 将会是个问题。
[self my_setFrame:frame];
直接调用my_setFrame: , runtime做的是
objc_msgSend(self, @selector(my_setFrame:), frame);
runtime去寻找my_setFrame:的方法实现, _cmd参数为 my_setFrame: ,但是事实上runtime找到的方法实现是原始的 setFrame: 的。
一个简单的解决办法:使用上面介绍的swizzling定义。
The order of swizzles matters
多个swizzle方法的执行顺序也需要注意。假设 setFrame: 只定义在NSView中,想像一下按照下面的顺序执行:
1. [NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
2. [NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
3. [NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
What happens when the method on NSButton is swizzled? Well most swizzling will ensure that it's not replacing the implementation of setFrame: for all views, so it will pull up the instance method. This will use the existing implementation to re-define setFrame: in the NSButton class so that exchanging implementations doesn't affect all views. The existing implementation is the one defined on NSView. The same thing will happen when swizzling on NSControl (again using the NSView implementation).
When you call setFrame: on a button, it will therefore call your swizzled method, and then jump straight to the setFrame: method originally defined on NSView. The NSControl and NSView swizzled implementations will not be called.
But what if the order were:
1. [NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
2. [NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
3. [NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
Since the view swizzling takes place first, the control swizzling will be able to pull up the right method. Likewise, since the control swizzling was before the button swizzling, the button will pull up the control's swizzled implementation of setFrame:. This is a bit confusing, but this is the correct order. How can we ensure this order of things?
Again, just use load to swizzle things. If you swizzle in load and you only make changes to the class being loaded, you'll be safe. The load method guarantees that the super class load method will be called before any subclasses. We'll get the exact right order!
这段贴了原文,硬翻译太拗口……总结一下就是:多个有继承关系的类的对象swizzle时,从子类对象开始 。 如果先swizzle父类对象,那么后面子类对象swizzle时就无法拿到真正的原始方法实现了。
多个有继承关系的类的对象swizzle时,先从父对象开始。 这样才能保证子类方法拿到父类中的被swizzle的实现。在+(void)load中swizzle不会出错,就是因为load类方法会默认从父类开始调用。
Difficult to understand (looks recursive)
(新方法的实现)看起来像递归,但是看看上面已经给出的 swizzling 封装方法, 使用起来就很易读懂.
这个问题是已完全解决的了!
Difficult to debug
debug时打出的backtrace,其中掺杂着被swizzle的方法名,一团糟啊!上面介绍的swizzle方案,使backtrace中打印出的方法名还是很清晰的。但仍然很难去debug,因为很难记住swizzling影响过什么。给你的代码写好文档(即使只有你一个人会看到)。养成一个好习惯,不会比调试多线程问题还难的。
文章内容来自:
iOS之运行时机制及方法混写method swizzling
Runtime基础使用场景-拦截替换方法
iOS Runtime详解
XXX
iOS 模式详解—「runtime&runloop 面试、工作」看我就 🐒 了 _.