博客地址:张飞的技术博客
在对导航栏进行深度学习的时候,在网上发现@我就叫Sunny怎么了
开源了一个导航栏返回手势的库FDFullscreenPopGesture,我看了看源代码,作者使用Runtime的一些知识实现的,今天我就借这个库的源代码进行Runtime的用法进行学习。
如果看过我前面几篇关于Runtime的文章,应该知道Runtime的消息发送机制的原理是对象根据方法编号SEL去映射表查找对应的方法实现。因此在运行时阶段我们利用Runtime的一些方法可以帮助我们实现用正常方法很难办到的事情。
1.给分类动态添加属性
在FDFullscreenPopGesture
中给UIViewController的分类里有这么一个属性:
@property (nonatomic, copy) _FDViewControllerWillAppearInjectBlock fd_willAppearInjectBlock;
这是一个block的属性,block定义如下:
typedef void (^_FDViewControllerWillAppearInjectBlock)(UIViewController *viewController, BOOL animated);
看到这里也许你会提问,OC中不是不能给分类添加属性么?正常情况下,OC是不允许给OC添加属性的。但是利用Runtime的特性,这是可以办到的。实现方法如下:
- (_FDViewControllerWillAppearInjectBlock)fd_willAppearInjectBlock
{
return objc_getAssociatedObject(self, _cmd);// 根据关联的key,获取关联的值。这里的key等于_cmd,_cmd等于fd_willAppearInjectBlock
}
- (void)setFd_willAppearInjectBlock:(_FDViewControllerWillAppearInjectBlock)block
{
// 第一个参数:给哪个对象添加关联
// 第二个参数:关联的key,通过这个key获取
// 第三个参数:关联的value
// 第四个参数:关联的策略
objc_setAssociatedObject(self, @selector(fd_willAppearInjectBlock), block, OBJC_ASSOCIATION_COPY_NONATOMIC);//关联对象
}
动态给分类添加属性的方法是:
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
获取这个属性的方法是:
objc_getAssociatedObject(id object, const void *key)
还有一个方法是移除属性:
objc_removeAssociatedObjects(id object)
是的,这样就动态的给UIViewController
的分类添加了fd_willAppearInjectBlock
这么一个属性。
NOTE:在使用Runtime的这些方法的时候不要忘了导入
objc/runtime.h
这个头文件哦!
2.动态添加方法
要想动态添加方法我们必须了解方法是如何执行的,通常我们调用方法是通过[object message]
这种方法,除了这种方法还有一种是比较少用的,就是[object performSelector:@selector(message)]
这种方式。通过下面这张图我们可以了解一下他们对消息的处理的不同之处。
通过上图,我们可以得知,要想动态添加方法必须是通过[object performSelector:@selector(message)]
这种方式调用方法才能在运行时阶段通过Runtime的一些方法达到动态的添加方法。如果现在有一个Person
类,在其它地方通过performSelector
的方式调用Person
的run
方法。但是Person
类中并没有实现这个方法。
Person p = [Person alloc] init];
// 这个时候即使Person类没有实现run方法编译器也不会报错
[p performSelector:@selector(run)];
这时候只需要在Person
中实现resolveInstanceMethod:
方法就可以达到动态添加方法的目的。
//首先我们要在Person类里面实现我们要动态添加的方法
// 要注意,默认方法都有两个隐式参数
void run(id self,SEL sel){
NSLog(@"%@ %@",self,NSStringFromSelector(sel));
}
// 当一个对象调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来.
// 刚好可以用来判断未实现的方法是不是我们想要动态添加的方法
+ (BOOL)resolveInstanceMethod:(SEL)sel{
//先判断一下传过来的是不是run方法
if (sel == @selector(run)){
//如果是run方法就动态添加run方法
class_addMethod(self.class, @selector(run),(IMP)run, "v@:");
// 第一个参数:给哪个类添加方法
// 第二个参数:添加方法的方法编号
// 第三个参数:添加方法的函数实现(函数地址),如果是OC方法
//可以用+(IMP)instanceMethodForSelector:(SEL)aSelector;获得方法的实现。
// 第四个参数:方法的签名,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
}
}
这样就达到了给一个类动态添加方法的效果了,如果想把方法转发给其他的类实现,需要处理消息转发的第二或第三个函数了。
3.替换系统自带的方法
当一些时候,系统自带效果满足不了我们的时候,要么我们自定义,要么直接替换系统的方法。在公有的API是没有方法办到的。我们来看一段FDFullscreenPopGesture
的代码(注释是我加的):
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
//获取系统方法的SEL
SEL originalSelector = @selector(viewWillAppear:);
//获取替换方法的SEL
SEL swizzledSelector = @selector(fd_viewWillAppear:);
//为了获取IMP指针,获得方法的Method
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
//为了安全起见,先判断是否已经存在要交换的方法
BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (success) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
- (void)fd_viewWillAppear:(BOOL)animated
{
//不要认为这句代码有错,其实很好理解,在调用这句的时候方法已经交换了
// Forward to primary implementation.
[self fd_viewWillAppear:animated];
if (self.fd_willAppearInjectBlock) {
self.fd_willAppearInjectBlock(self, animated);
}
}
通过上面的代码我们可以看出来,替换系统自带的方式实现需要用到的重要方法是method_exchangeImplementations()
方法,并且要注意替换方法里面对自己的调用。这个方法也就是人们常说的Method Swizzling
黑魔法,用的时候要注意,这是一把双刃剑!
结尾
Runtime在项目中很少用,但是要理解它,理解了之后用起来也不危险。如果你喜欢我的文章,不妨扫一扫下面的二维码请我喝杯茶。祝大家在iOS开发的道路上玩得愉快!