上篇文章讲述了 runtime 中的关联 association(传送门),今天我们继续来学习 runtime,揭开它神秘的黑魔法-swizzling!
也许很多人都和我一样,不知道什么时候该用 runtime(有一次某 bat 公司的面试,就问我什么时候用到了 runtime)。那我们就先来举个用 runtime 的栗子。
asociation 的栗子我们上篇文章已经举过了,就是在分类中添加属性的时候,用于 setter 方法和 getter 方法中。那么 swizzling 呢?
假设你现在开发了一个 app,它的首页是一个 tableView,当没有数据的时候你的首页会显示什么呢?
(有的同学可能会问:“为什么会没有数据呢?” 也许是网挂了,或者数据丢了,反正就是有这种可能啦。。。)
什么也不显示肯定不太好吧?那样用户岂不是会很困惑?用户不知道是什么情况导致了没有数据,也许用户会觉得你这个 app 本来就什么都没有呢!
所以我们应该在没有数据的时候做一些处理,比如我们公司的项目,它的首页就是 tableView,在没有数据的时候,我们的做法是显示一张背景图片,并且用一个 label 显示 “网络不给力,请点击屏幕重试”,并且给 tableView 加入了一个点击事件,点击 tableView 就会重新请求数据。
每次用 tableView 都要判断是否为空并且做这些事岂不是很笨?所以我们直接写一个 tableView 的分类 UITableView+EmptyDataSet。
接下来我们来思考一个问题:应该什么时候去判断 tableView 是否为空呢?
答案是,在 tableView reloadData 之后。
(有的同学可能会说:“这不是废话吗!在 reloadData 之前判断有啥用,reloadData 之后也许就变了啊!“)
嗯的确是句废话,这句话就好比你正准备要吃饭,但还没开始吃的时候我就问你吃饱了吗,显然我应该等你吃完再问你。
重点其实是,怎么在 reloadData 之后判断?请仔细思考这个问题。
有的同学可能又会说了:“这还不简单,像下面这样就行了啊”
- (void)reloadData {
[self reloadData];
// 做你想做的事
}
如果你也是这么想的,那么,请再一次仔细思考这个问题。
很显然,这句代码是个死循环。也许有的同学会说:“重写 reloadData 方法”。
呵呵,你可真敢说!系统的方法你重写一个给我看看?Talk is cheap,show me the code!
那怎么办呢?究竟怎么才能在 reloadData 之后做我们想做的事呢?
=================== 我 == 是 == 分 == 割 == 线 ===================
ok,背景交代完毕,我们的主角 swizzling 终于要登场了,它就可以办到这件事情。
首先,把 reloadData 方法和我们写的某个方法交换。
method_exchangeImplementations(originalMethod, swizzledMethod);
然后,实现我们自己写的这个方法,搞定!
- (void)swizzled_reloadData {
[self swizzled_reloadData];
// 做你想做的事
}
有的同学可能会说:“咦?这不还是死循环吗?!”
并不是。
- (void)swizzled_reloadData; 的确是我们自己写的方法,但是 [self swizzled_reloadData]; 调用的却不是这个方法,而是调用了系统中的 reloadData 方法,这是因为我们把 reloadData 的实现和 swizzled_reloadData 的实现交换了。请仔细思考这句话和下面这句话。
现在你在任意一个地方调用 [self.tableView reloadData]; 其实调用的都不是系统中的 reloadData 方法,而是我们在 tableView 的分类中写的 swizzled_reloadData 方法,在 swizzled_reloadData 方法中调用的那句才是系统中的 reloadData 方法。
如果这几句你理解了,那我们就可以具体来说说了。交换两个方法的实现当然不是简单一句就搞定的啦。事实上是这样的:
Class class = [self class];
SEL originalSelector = @selector(reloadData);
SEL swizzledSelector = @selector(swizzled_reloadData);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
这些其实也比较好理解,就是获取源方法和目标方法的实现。因为我们要交换的是两个方法的实现。
其实在交换之前,我们应该先这样:
Class class = [self class];
SEL originalSelector = @selector(reloadData);
SEL swizzledSelector = @selector(swizzled_reloadData);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
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, swizzledMethod);
}
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
这个方法的意思是,往 cls 这个类中添加一个叫做 name 的方法,这个方法的具体实现是 imp。types 是编码类型,这里我们讨论的重点不是它,先不用太在意它。如果添加成功则返回 YES,否则返回 NO。
这句话的意思就是,我们先尝试往当前类(也就是 UITableView)中添加一个叫做 reloadData 的方法。为什么呢?因为当前类(主类)不一定实现了原方法(此处为reloadData方法),而有可能是继承了父类的方法。
当 class_addMethod 返回 NO 时,说明主类本身就实现了需要被替换的方法,这种情况比较简单,我们直接交换两个方法的实现就可以了。当 class_addMethod 返回 YES 时,说明主类本身没有实现需要被替换的方法,而是继承了父类的实现。这时 class_getInstanceMethod 函数获取到的 originalSelector 指向的就是父类的方法。然后我们再通过 class_replaceMethod 把父类的实现替换到我们自定义的 swizzled_reloadData 中,这样就达到了在 swizzled_reloadData 方法中调用父类实现的目的。
如果交换了两次岂不是又换回来了?所以我们应该用 GCD 中的 dispatch_once 来保证交换只会执行一次。
static dispatch_once_t once_Token;
dispatch_once(&once_Token, ^ {
Class class = [self class];
SEL originalSelector = @selector(reloadData);
SEL swizzledSelector = @selector(swizzled_reloadData);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_addMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
说了这么多,这些东西写在哪里啊?写在 load 里。
也许很多人都不知道这个方法吧?我们平时开发确实不太常用到它。事实上它可是 NSObject 中的第一个方法哦:
对于一个类而言,如果没有实现 load 方法,就不会调用它,如果实现了的话,该类就会自动调用它。load 的调用时机很早。
关于 load 和 initialize,这里有详细说明:传送门
所以这段代码应该是这样的:
+ (void)load {
static dispatch_once_t once_Token;
dispatch_once(&once_Token, ^ {
Class class = [self class];
SEL originalSelector = @selector(reloadData);
SEL swizzledSelector = @selector(swizzled_reloadData);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_addMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
总结一下,swizzling 可以用于我们想改变系统方法的时候。既然是黑魔法,当然也有一定的风险,保证它只执行一次、加上恰当的前缀等做法可以降低它的风险。swizzling 应该在 +load 方法中实现,load 方法会在一个类最开始加载时调用。
关于本文中提到的判断 tableView 是否为空,并在为空时做出相应处理的完整源码,请见我的github:https://github.com/963239327/LZNEmptyDataSet 别忘了点击右上角的 star 哦