Runtime详解(下)
Runtime应用
1.Runtime 交换方法
应用场景:当第三方框架或者系统原生方法功能不能满足我们的时候,我们可以在保持系统原有功能的基础上,添加额外的功能。
需求:加载一张图片直接用系统的[UIImage imageNamed:@""];是无法知道到底有没有加载成功。给系统的imageNamed添加额外功能,(是否加载图片成功,以及加载未完成的时候,用模糊的该照片代替)
方法一:继承系统的类,重写方法:(每次使用都需要导入)
方法二:使用runtime,交换方法
实现步骤:
(1)给系统的方法添加分类
(2)自己实现一个带有扩展功能的方法
(3)交换方法,只需要交换一次
下面是案例代码:
- (void)viewDidLoad { [super viewDidLoad]; // 方案一:先搞个分类,定义一个能加载图片并且能打印的方法+ (instancetype)imageWithName:(NSString *)name; // 方案二:交换 imageNamed 和 ln_imageNamed 的实现,就能调用 imageNamed,间接调用 ln_imageNamed 的实现。 UIImage *image = [UIImage imageNamed:@"123"]; } #import <objc/message.h> @implementation UIImage (Image) /** load方法: 把类加载进内存的时候调用,只会调用一次 方法应先交换,再去调用 */ + (void)load { // 1.获取 imageNamed方法地址 // class_getClassMethod(获取某个类的方法) Method imageNamedMethod = class_getClassMethod(self, @selector(imageNamed:)); // 2.获取 ln_imageNamed方法地址 Method ln_imageNamedMethod = class_getClassMethod(self, @selector(ln_imageNamed:)); // 3.交换方法地址,相当于交换实现方式;「method_exchangeImplementations 交换两个方法的实现」 method_exchangeImplementations(imageNamedMethod, ln_imageNamedMethod); } /** 看清楚下面是不会有死循环的 调用 imageNamed => ln_imageNamed 调用 ln_imageNamed => imageNamed */ // 加载图片 且 带判断是否加载成功 + (UIImage *)ln_imageNamed:(NSString *)name { UIImage *image = [UIImage ln_imageNamed:name]; if (image) { NSLog(@"runtime添加额外功能--加载成功"); } else { NSLog(@"runtime添加额外功能--加载失败"); } return image; } /** 不能在分类中重写系统方法imageNamed,因为会把系统的功能给覆盖掉,而且分类中不能调用super 所以第二步,我们要 自己实现一个带有扩展功能的方法. + (UIImage *)imageNamed:(NSString *)name { } */ @end
总结:我们所做的就是在方法调用流程第三步的时候,交换两个方法地址指向。而且我们改变指向要在系统的imageNamed:方法调用前,所以将代码写在了分类的load方法里,最后当运行的时候系统的方法就会去找我们实现的方法。
2.动态添加属性
给一个类声明属性,其实本质就是给这个类添加关联,并不是直接把这个值的内存空间添加到类存空间。
给系统的类添加属性的时候,可以使用runtime动态添加属性。
注解:系统NSObject添加一个分类,我们知道在分类中不能添加成员属性的,虽然我们用了@property,但是仅仅是自动生成get和set方法的声明,并没有带下滑线的属性和方法实现生成。我们可以通过runtime就可以做到给它方法的实现。
需求:给系统NSObject动态添加属性name字符串。
案例如下:
@interface NSObject (Property) // @property分类:只会生成get,set方法声明,不会生成实现,也不会生成下划线成员属性 @property NSString *name; @property NSString *height; @end @implementation NSObject (Property) - (void)setName:(NSString *)name { // objc_setAssociatedObject(将某个值跟某个对象关联起来,将某个值存储到某个对象中) // object:给哪个对象添加属性 // key:属性名称 // value:属性值 // policy:保存策略 objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (NSString *)name { return objc_getAssociatedObject(self, @"name"); } // 调用 NSObject *objc = [[NSObject alloc] init]; objc.name = @"123"; NSLog(@"runtime动态添加属性name==%@",objc.name);
//结果如下: 2016-02-17 19:37:10.530 runtime[12761:543574] runtime动态添加属性--name == 123
其实给属性赋值的本质,就是让属性与一个对象产生关联,所以要给NSObject的分类的name属性赋值就是让那个name和NSObject产生关联,而Runtime可以做到这一点。
下面再举个例子:
关联对象(objective-C Associated objects)给分类增加属性
关联对象Runtime提供了几个接口:
//关联对象 void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) //获取关联的对象 id objc_getAssociatedObject(id object, const void *key) //移除关联的对象 void objc_removeAssociatedObjects(id object)
参数注释:
id object:被关联的对象 const void *key:关联的key,要求唯一 id value:关联的对象 objc_AssociationPolicy policy:内存管理的策略
内存管理的策略
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) { OBJC_ASSOCIATION_ASSIGN = 0, /**< Specifies a weak reference to the associated object. */ OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. * The association is not made atomically. */ OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**< Specifies that the associated object is copied. * The association is not made atomically. */ OBJC_ASSOCIATION_RETAIN = 01401, /**< Specifies a strong reference to the associated object. * The association is made atomically. */ OBJC_ASSOCIATION_COPY = 01403 /**< Specifies that the associated object is copied. * The association is made atomically. */ };
下面实现一个UIView
的Category
添加自定义属性defaultColor
。
#import "ViewController.h" #import "objc/runtime.h" @interface UIView (DefaultColor) @property (nonatomic, strong) UIColor *defaultColor; @end @implementation UIView (DefaultColor) @dynamic defaultColor; static char kDefaultColorKey; - (void)setDefaultColor:(UIColor *)defaultColor { objc_setAssociatedObject(self, &kDefaultColorKey, defaultColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (id)defaultColor { return objc_getAssociatedObject(self, &kDefaultColorKey); } @end @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. UIView *test = [UIView new]; test.defaultColor = [UIColor blackColor]; NSLog(@"%@", test.defaultColor); } @end
结果如下:
打印结果: 2018-04-01 15:41:44.977732+0800 ocram[2053:63739] UIExtendedGrayColorSpace 0 1
从打印结果来看:我们成功在分类上添加一个属性,实现了它的setter和getter方法。
通过关联对象实现的属性的内存管理也是有ARC
管理的,所以我们只需要给定适当的内存策略就行了,不需要操心对象的释放。
3.方法魔法:(俗称黑魔法)-method swizzling
简单的说就是进行方法交换
在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector
的名字。利用Objective-C
的动态特性,可以实现在运行时偷换selector
对应的方法实现,达到给方法挂钩的目的。
每一个类都有一个方法列表,存放着方法的名字实现的映射关系,selector的本质就是方法名,IMP有点类似函数指针,指向具体的method实现,通过selector就可以找到对应的IMP。
交换方法的几种实现方式:
(1)利用method_exchangeImplementations 交换两个方法的实现
(2)利用class_replaceMethod替换方法的实现。
(3)利用method_setImplementation来直接设置某个方法的IMP。
目前已更新实例汇总:
.替换ViewController生命周期方法
.解决获取索引、添加、删除元素越界崩溃问题
.防止按钮重复暴力点击
.全局更换控件初始效果
.App热修复
.全局修改导航栏后退(返回)按钮
Method Swizzling通用方法封装
我们可以将Method Swizzling功能封装为类方法,作为NSObject的类别。
#import <Foundation/Foundation.h> #import <objc/runtime.h> @interface NSObject (Swizzling) + (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector bySwizzledSelector:(SEL)swizzledSelector; @end
#import "NSObject+Swizzling.h" @implementation NSObject (Swizzling) + (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector bySwizzledSelector:(SEL)swizzledSelector{ Class class = [self class]; //原有方法 Method originalMethod = class_getInstanceMethod(class, originalSelector); //替换原有方法的新方法 Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); //先尝试給源SEL添加IMP,这里是为了避免源SEL没有实现IMP的情况 BOOL didAddMethod = class_addMethod(class,originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) {//添加成功:说明源SEL没有实现IMP,将源SEL的IMP替换到交换SEL的IMP class_replaceMethod(class,swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else {//添加失败:说明源SEL已经有IMP,直接将两个SEL的IMP交换即可 method_exchangeImplementations(originalMethod, swizzledMethod); } } @end
解析:为什么要添加didAddMethod判断?
先尝试添加原SEL其实是为了做一层保护,因为如果这个类如果没有实现originalSelector,但其父类实现了,那class_getInstanceMethod会返回父类的方法。这样method_exchangeImplementations替换的是父类的那个方法。这样method_exchangeImplementations替换的是父类的那个方法,这当然不是我们想要。所以我们先尝试添加orginalSelector,如果已经存在,再用method_exchangeImplement把原方法的实现跟新的方法实现给交换掉。
大概的意思就是我们可以通过class_addMethod为一个类添加方法
class_addMethod(class,originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
同时再将原有的实现(IMP)替换到swizzledMethod方法上,
class_replaceMethod(class,swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
从而实现了方法的交换,并且未影响父类方法的实现。反之如果class_addMethod返回NO,说明子类中本身就具有方法originalSelector的实现,直接调用交换即可。
method_exchangeImplementations(originalMethod, swizzledMethod);
实例1:替换ViewController
当然可以依次在每个界面的viewWillDisappear方法中添加去除方法,但如果类似的界面过多,一味的复制粘贴也不是方法。这时候就能体现Method Swizzling的作用了,我们可以替换系统的viewWillDisappear方法,使得每当执行该方法时即自动去除加载栏。
#import "UIViewController+Swizzling.h" #import "NSObject+Swizzling.h" @implementation UIViewController (Swizzling) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self methodSwizzlingWithOriginalSelector:@selector(viewWillDisappear:) bySwizzledSelector:@selector(sure_viewWillDisappear:)]; }); } - (void)sure_viewWillDisappear:(BOOL)animated { [self sure_viewWillDisappear:animated]; [SVProgressHUD dismiss]; }
⚠️补充知识点
(1)为什么方法交换调用+load方法中?#import <UIKit/UIKit.h> //默认时间间隔 #define defaultInterval 1 @interface UIButton (Swizzling) //点击间隔 @property (nonatomic, assign) NSTimeInterval timeInterval; //用于设置单个按钮不需要被hook @property (nonatomic, assign) BOOL isIgnore; @end
#import "UIButton+Swizzling.h" #import "NSObject+Swizzling.h" @implementation UIButton (Swizzling) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self methodSwizzlingWithOriginalSelector:@selector(sendAction:to:forEvent:) bySwizzledSelector:@selector(sure_SendAction:to:forEvent:)]; }); } - (NSTimeInterval)timeInterval{ return [objc_getAssociatedObject(self, _cmd) doubleValue]; } - (void)setTimeInterval:(NSTimeInterval)timeInterval{ objc_setAssociatedObject(self, @selector(timeInterval), @(timeInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } //当按钮点击事件sendAction 时将会执行sure_SendAction - (void)sure_SendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event{ if (self.isIgnore) { //不需要被hook [self sure_SendAction:action to:target forEvent:event]; return; } if ([NSStringFromClass(self.class) isEqualToString:@"UIButton"]) { self.timeInterval =self.timeInterval == 0 ?defaultInterval:self.timeInterval; if (self.isIgnoreEvent){ return; }else if (self.timeInterval > 0){ [self performSelector:@selector(resetState) withObject:nil afterDelay:self.timeInterval]; } } //此处 methodA和methodB方法IMP互换了,实际上执行 sendAction;所以不会死循环 self.isIgnoreEvent = YES; [self sure_SendAction:action to:target forEvent:event]; } //runtime 动态绑定 属性 - (void)setIsIgnoreEvent:(BOOL)isIgnoreEvent{ // 注意BOOL类型 需要用OBJC_ASSOCIATION_RETAIN_NONATOMIC 不要用错,否则set方法会赋值出错 objc_setAssociatedObject(self, @selector(isIgnoreEvent), @(isIgnoreEvent), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (BOOL)isIgnoreEvent{ //_cmd == @select(isIgnore); 和set方法里一致 return [objc_getAssociatedObject(self, _cmd) boolValue]; } - (void)setIsIgnore:(BOOL)isIgnore{ // 注意BOOL类型 需要用OBJC_ASSOCIATION_RETAIN_NONATOMIC 不要用错,否则set方法会赋值出错 objc_setAssociatedObject(self, @selector(isIgnore), @(isIgnore), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (BOOL)isIgnore{ //_cmd == @select(isIgnore); 和set方法里一致 return [objc_getAssociatedObject(self, _cmd) boolValue]; } - (void)resetState{ [self setIsIgnoreEvent:NO]; } @end
实例3.全局修改导航栏(返回)按钮
iOS默认的返回按钮样式如下,默认为蓝色左箭头,文字为上一界面标题文字。
闲话少说,我们创建基于UINavigationItem
的类别,在其load
方法中替换方法backBarButtonItem
#import "UINavigationItem+Swizzling.h" #import "NSObject+Swizzling.h" static char *kCustomBackButtonKey; @implementation UINavigationItem (Swizzling) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self methodSwizzlingWithOriginalSelector:@selector(backBarButtonItem) bySwizzledSelector:@selector(sure_backBarButtonItem)]; }); } - (UIBarButtonItem*)sure_backBarButtonItem { UIBarButtonItem *backItem = [self sure_backBarButtonItem]; if (backItem) { return backItem; } backItem = objc_getAssociatedObject(self, &kCustomBackButtonKey); if (!backItem) { backItem = [[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:NULL]; objc_setAssociatedObject(self, &kCustomBackButtonKey, backItem, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } return backItem; } @end
这里进行将返回按钮的文字清空操作,其他需求样式大家也可随意替换,现在再次运行程序,就会发现所有的返回按钮均只剩左箭头,并右滑手势依然有效。如图所示
4.KVO实现
提供了一种当其它对象属性被修改的时候能通知当前对象的机制。
KVO
的实现依赖于 Objective-C
强大的 Runtime
,当观察某对象 A
时,KVO
机制动态创建一个对象A
当前类的子类,并为这个新的子类重写了被观察属性 keyPath
的 setter
方法。setter
方法随后负责通知观察对象属性的改变状况。
Apple
使用了 isa-swizzling
来实现 KVO
。当观察对象A
时,KVO
机制动态创建一个新的名为:NSKVONotifying_A
的新类,该类继承自对象A的本类,且 KVO
为 NSKVONotifying_A
重写观察属性的 setter
方法,setter
方法会负责在调用原 setter
方法之前和之后,通知所有观察对象属性值的更改情况。KVO
的键值观察通知依赖于 NSObject 的两个方法:willChangeValueForKey:
和 didChangeValueForKey:
,在存取数值的前后分别调用 2 个方法:被观察属性发生改变之前,
willChangeValueForKey:
被调用,通知系统该 keyPath
的属性值即将变更;当改变发生后,
didChangeValueForKey:
被调用,通知系统该keyPath
的属性值已经变更;之后, observeValueForKey:ofObject:change:context:
也会被调用。且重写观察属性的setter
方法这种继承方式的注入是在运行时而不是编译时实现的。KVO
为子类的观察者属性重写调用存取方法的工作原理在代码中相当于:- (void)setName:(NSString *)newName { [self willChangeValueForKey:@"name"]; //KVO 在调用存取方法之前总调用 [super setValue:newName forKey:@"name"]; //调用父类的存取方法 [self didChangeValueForKey:@"name"]; //KVO 在调用存取方法之后总调用 }
5.消息转发(热更新)解决Bug(JSPatch)
JSPatch 是一个 iOS 动态更新框架,只需在项目中引入极小的引擎,就可以使用 JavaScript 调用任何 Objective-C 原生接口,获得脚本语言的优势:为项目动态添加模块,或替换项目原生代码动态修复 bug。
6.实现NSCoding的自动归档和自动解档
用runtime
提供的函数遍历Model
自身所有属性,并对属性进行encode
和decode
操作。
核心方法:在Model
的基类中重写方法:
- (id)initWithCoder:(NSCoder *)aDecoder { if (self = [super init]) { unsigned int outCount; Ivar * ivars = class_copyIvarList([self class], &outCount); for (int i = 0; i < outCount; i ++) { Ivar ivar = ivars[i]; NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)]; [self setValue:[aDecoder decodeObjectForKey:key] forKey:key]; } } return self; } - (void)encodeWithCoder:(NSCoder *)aCoder { unsigned int outCount; Ivar * ivars = class_copyIvarList([self class], &outCount); for (int i = 0; i < outCount; i ++) { Ivar ivar = ivars[i]; NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)]; [aCoder encodeObject:[self valueForKey:key] forKey:key]; } }
上面就是Runtime的知识点以及常用场景,博客会持续更改,欢迎指正。