[置顶] runtime学习之 - 黑魔法 Swizzling,改变系统方法!

上篇文章讲述了 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);


其中 originalMethod 是源方法,也就是我们要交换的 reloadData。swizzledMethod 是目标方法,是我们自己写的一个方法,比如就叫做 swizzled_reloadData。(我们先简单地这样说,一会再具体地说)

然后,实现我们自己写的这个方法,搞定!

- (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_addMethod(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 的方法,这个方法的实现是 swizzled_reloadData 的实现。如果添加成功了,我们就再往当前类中添加一个叫做 swizzled_reloadData 的方法,这个方法的实现是 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 中的第一个方法哦:

[置顶] runtime学习之 - 黑魔法 Swizzling,改变系统方法!_第1张图片

对于一个类而言,如果没有实现 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 哦





你可能感兴趣的:(ios,Objective-C,Runtime,运行时,Swizzle)