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:@""];
} #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 = @"";
NSLog(@"runtime动态添加属性name==%@",objc.name);
//结果如下: -- ::10.530 runtime[:] runtime动态添加属性--name ==
其实给属性赋值的本质,就是让属性与一个对象产生关联,所以要给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 = , /**< Specifies a weak reference to the associated object. */
OBJC_ASSOCIATION_RETAIN_NONATOMIC = , /**< Specifies a strong reference to the associated object.
* The association is not made atomically. */
OBJC_ASSOCIATION_COPY_NONATOMIC = , /**< Specifies that the associated object is copied.
* The association is not made atomically. */
OBJC_ASSOCIATION_RETAIN = , /**< Specifies a strong reference to the associated object.
* The association is made atomically. */
OBJC_ASSOCIATION_COPY = /**< 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
结果如下:
打印结果:
-- ::44.977732+ ocram[:] UIExtendedGrayColorSpace
从打印结果来看:我们成功在分类上添加一个属性,实现了它的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方法中?
(2)为什么方法要在dispatch_once中执行?
实例2.防止按钮重复暴力点击
#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 == ?defaultInterval:self.timeInterval;
if (self.isIgnoreEvent){
return;
}else if (self.timeInterval > ){
[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 = ; 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 = ; i < outCount; i ++) {
Ivar ivar = ivars[i];
NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
[aCoder encodeObject:[self valueForKey:key] forKey:key];
}
}
上面就是Runtime的知识点以及常用场景,博客会持续更改,欢迎指正。
最新文章
- Fabio 安装和简单使用
- Python3基础 casefold 将字符串中的所有字符变成小写
- perl q qq qr qw qx 区别与使用方法
- csharp:Nhibernate Procedure with CreateSQLQuery and GetNamedQuery
- 用bower命令创建项目
- Android酷炫实用的开源框架(UI框架) 转
- 批量下载QQ空间日志
- Android第三方授权(新浪微博篇)
- RELATED INTRODUCED
- Swift语法总结(精简版)
- 连接pgsql
- ROS_Kinetic_27 在ROS中使用Cartographer进行SLAM
- html/css的学习之路(2)
- 本地开发环境搭建(windows)
- cocos2d-x android工程接入第三方支付宝SDK
- 使用python requests模块搭建http load压测环境
- MYSQL 优化常用方法总结
- leetcode74:二维矩阵搜索问题
- Office 2010 激活 - Failed to inject memory!
- 用css3实现社交分享按钮