内容简介:不知道什么时候开始,只要使用了
不知道什么时候开始,只要使用了 swizzling 都能被解读成是 AOP 开发,开发者张口嘴就是 runtime ,将其高高捧起,称之为 黑魔法 ;以项目中各种 method_swizzling 为荣,却不知道这种做法破坏了代码的整体性,使关键逻辑支离破碎。本文基于iOS界的毒瘤一文,从另外的角度谈谈为什么我们应当 警惕
调用顺序性
调用顺序性 是链接文章讲述的的核心问题,它会破坏方法的原有执行顺序,导致意料之外的错误。先从一段简单的代码聊起:
@interface SLTestObject: NSObject
@end
@implementation SLTestObject
- (instancetype)init {
self = [super init];
return self;
}
@end
void testIsSelectorSame() {
Method allocate1 = class_getClassMethod([NSObject class], @selector(alloc));
Method allocate2 = class_getClassMethod([SLTestObject class], @selector(alloc));
Method initialize1 = class_getInstanceMethod([NSObject class], @selector(init));
Method initialize2 = class_getInstanceMethod([SLTestObject class], @selector(init));
assert(allocate1 == allocate2 && initialize1 != initialize2);
}
复制代码
这段代码的目的是证明一个定论:
如果子类没有重写父类声明的方法,在子类对象调用该方法时,执行的是父类实现的代码
基于这一定论,假定一个场景:现在通过无埋点方案统计用户进入和离开 Controller 次数:
@implementation UIViewController (SLCount)
+ (void)load {
sl_swizzle([self class], @selector(viewWillAppear:), @selector(sl_viewWillAppearI:));
sl_swizzle([self class], @selector(viewDidDisappear:), @selector(sl_viewDidDisappearI:));
}
- (void)sl_viewWillAppearI: (BOOL)animated {
[SLControllerCounter countControllerEnter: [self class]];
[self sl_viewWillAppearI: animated];
}
- (void)sl_viewDidDisappearI: (BOOL)animated {
[SLControllerCounter countControllerLeave: [self class]];
[self sl_viewDidDisappearI: animated];
}
@end
复制代码
由于 UIViewController 是所有控制器的父类,所以理论上只要 swizzle 这个类就能统计到所有控制器的信息。同时项目中存在一个定制的基础控制器 SLBaseViewController 存在这么一段代码:
@implementation SLBaseViewController (SLCount)
+ (void)load {
sl_swizzle([self class], @selector(viewWillAppear:), @selector(sl_viewWillAppearII:));
sl_swizzle([self class], @selector(viewDidDisappear:), @selector(sl_viewDidDisappearII:));
}
- (void)sl_viewWillAppearII: (BOOL)animated {
[self prepareRequest];
[self sl_viewWillAppearII: animated];
}
- (void)sl_viewDidDisappearII: (BOOL)animated {
[self sl_viewDidDisappearII: animated];
[self cancelAllRequests];
}
@end
复制代码
但是这两段代码却在特定的场景下发生 crash ,发生异常的原因在于子类在没有重写方法的情况下,子类先于父类进行了 swizzle 的操作。 iOS 使用中方法名称 SEL 和方法实现 IMP 是分开存放的,使用结构体 Method 将两者关联到一起:
typedef struct Method {
SEL name;
IMP imp;
} Method;
复制代码
交换方法会将两个 method 中的 imp 进行交换。而在理想情况下,父类先于子类完成了 swizzle ,原有方法保存了 swizzle 之后的 imp ,这时候子类再进行 swizzle 就能正确调用。下图标识了 SEL 和 IMP 的关联,箭头表示 IMP 的调用次序:
但是如果子类的 swizzle 发生的更早,这时候 viewWillAppear 对应的 imp 已经被修改,父类再进行 swizzle 的时候,调用次序已经出错:
解决方式也并不复杂,包括:
- 在
swizzle之前先addMethod,保证子类不沿用父类的默认实现 - 每次调用通过
sel去获取imp执行
具体的实现代码可以参考iOS界的毒瘤的解决方案
行为冲突
在 OOP 的设计中,将描述对象抽象成类,将对象行为抽象成接口。从工程师的角度来说,职责单一的接口更利于迭代维护。类一旦设计好,应当不改动或者少改动接口。对于设计良好的接口来说, swizzle 很可能直接破坏了整个接口的行为:
举个例子, crash防护 是当下被追捧的工具,但其中 KVO 的防护或许是一种很烂的手段。从实现来说,为了避免 KVO 导致的循环引用,需要在引用关系的中间插入一个 weakProxy 来做防护,因此监听代码实际上可以转换成:
// 表面代码 [observedObj addObserver: self forKeyPath: keyPath options: NSKeyValueObservingOptionNew context: nil]; // 实际效果 WeakProxy *proxy = [WeakProxy new]; proxy.client = self; [observedObj addObserver: proxy forKeyPath: keyPath options: NSKeyValueObservingOptionNew context: nil]; 复制代码
为什么说这种设计很烂的?一旦客户端出现这样的代码:
- (void)dealloc {
......
[observedObj removeObserver: self forKeyPath: keyPath];
}
复制代码
通常情况下,以现在的多数 防护工具 的实现,都会发生崩溃。对于 swizzle 代码外的使用者来说,或许根本不清楚 observer 早已发生了转移,导致了原有的正确调用出错。解决方案之一是对 remove 接口同样进行 swizzle ,使得两次调用的监听对象配套:
- (void)sl_removeObserver: (id)observer forKeyPath: (NSString *)keyPath {
[self sl_removeObserver: observer.proxy forKeyPath: keyPath];
}
复制代码
然而这样做之后,首先 KVO 的行为已经被修改,接口被破坏可能导致潜在的隐患。其次,如果存在多个防护工具,如果按照 weakProxy 的实现,那么一旦有 2 个或者更多的防护时, KVO 功能将失效:
OneWeakProxy *proxy = [OneWeakProxy new]; proxy.client = self; [observedObj addObserver: proxy forKeyPath: keyPath options: NSKeyValueObservingOptionNew context: nil]; TwoWeakProxy *proxy = [TwoWeakProxy new]; proxy.client = self; /// self is OneWeakProxy [observedObj addObserver: proxy forKeyPath: keyPath options: NSKeyValueObservingOptionNew context: nil]; 复制代码
在第二次生成 WeakProxy 后并调用方法后, OneWeakProxy 创建的对象被释放。如果要避免多个防护 工具 对流程造成干扰,还需要做更多额外的工作。况且一旦有其中一个没有完美实现,整套 防护机制 可能就直接崩溃失效了,因此 KVO防护 不见得是一种好手段
代码整体性
以上面例子来说, KVO 是 NSObject 这个基类提供的能力,由于 子类默认沿用父类的方法实现 这一原则,这种方法的 swizzle 实际上影响了全部的对象,例如下面的代码实际上效果是完全一样的:
/// swizzle 1
void swizzleTableView() {
Method ori = class_getClassMethod([UITableView class], @selector(addObserver:forKeyPath:options:context:));
Method cus = class_getClassMethod([UITableView class], @selector(sl_addObserver:forKeyPath:options:context:));
method_exchange(ori, cus);
}
/// swizzle 2
void swizzleObj() {
Method ori = class_getClassMethod([NSObject class], @selector(addObserver:forKeyPath:options:context:));
Method cus = class_getClassMethod([NSObject class], @selector(sl_addObserver:forKeyPath:options:context:));
method_exchange(ori, cus);
}
复制代码
而第一个方法由于默认实现是 NSObject 的,因此一旦发生了 swizzle 所有的对象都会生效,这存在两个问题:
- 非
UITableView对象依旧受到了KVO的拦截影响 - 没有
sl_addObserver:forKeyPath:options:context:的对象会发生崩溃
另一方面,类的接口设计总是偏向于 装扮模式 的思维,不同层级的类对象在自己的方法被调用起时会执行自身特有的工作,这种设计让继承有足够的灵活性,从 viewDidLoad 的实现代码可见一斑:
- (void)viewDidLoad {
[super viewDidLoad];
/// setup work
}
复制代码
换句话说,以这种 装扮模式 思维来构建的代码,如果中间的一个方法被影响甚至破坏了,在中间的这个类开始往下将呈现塌式破坏,可以想象如果 UIView 一旦出错,应用几乎丧失展示控件的能力。但假如确实需要 swizzle 的中间环节,必须保证 swizzle 不对或者尽量少地对子类对象造成影响
以上所述就是小编给大家介绍的《警惕swizzle》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Beginning Google Maps API 3
Gabriel Svennerberg / Apress / 2010-07-27 / $39.99
This book is about the next generation of the Google Maps API. It will provide the reader with the skills and knowledge necessary to incorporate Google Maps v3 on web pages in both desktop and mobile ......一起来看看 《Beginning Google Maps API 3》 这本书的介绍吧!