Runtime是什么?
Apple关于Runtime的详细文档链接:Runtime Guide
其实大家对Runtime算是既熟悉又陌生的,因为在学习Objective-C的时候就知道这门语言的强大之处在于其动态性,那么什么是动态性呢,这个时候就会接触到Runtime的概念了,顾名思义,Runtime是在一种进行时的特性,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用,就是说在工程编译阶段才会确定所有函数的执行路径等,这个就是进行时的特色了。
那么知道了这个特点,对于我们来说与什么实际价值呢?
这边文章介绍了几个常用的场景可以让你快速的领悟Runtime的精神并且可以拿去分(zhuang)享(B)。。。
Runtime实现原理R简介
Runtime是一套比较底层的纯C语言API, 属于一个C语言库, 包含了很多底层的C语言API。
在我们平时编写的OC代码中, 程序运行过程时, 其实最终都是转成了Runtime的C语言代码。
Runtime算是Objective-C的幕后工作者!
例如,下面一个创建Dog对象的方法中,
在OC中 :
[[Dog alloc] init]
在Runtime中就变成 :
objc_msgSend(objc_msgSend(“Dog” , “alloc”), “init”)
Runtime用来做什么?
1、在程序运行过程中, 动态创建一个类(比如KVO的底层实现)
2、在程序运行过程中, 动态地为某个类添加属性\方法, 修改属性值\方法
3、遍历一个类的所有成员变量(属性)\所有方法
例如:我们需要对一个类的属性进行归档解档的时候属性特别的多,这时候,我们就会写很多对应的代码,但是如果使用了runtime就可以动态设置!
4、就是今天要着重讲的最常用到的一些使用:可以利用Runtime,避免UIButton 重复点击, 可变数组和可变字典为nil,或者数组越界导致的Crash问题。
利用Runtime解决数组字典的崩溃问题
适用场景:当我们从后台请求到的数据,需要把其中一个插入到数组的时候,需要先判断该对象是否为空值,非空才能插入,否则会引起崩溃。Runtime可以从根本上解决,即使我插入的是空值,也不会引起崩溃。
Method Swizzling
在Objective-C中调用一个方法,其实是向一个对象发送消息,而查找消息的唯一依据是selector的名字。所以,我们可以实现在运行时交换selector对应的方法实现以达到效果。
每个类都有一个方法列表,存放着SEL(selector)的名字和方法实现的映射关系。IMP(Implementation Method Path)有点类似函数指针,指向具体的Method实现。
关于SEL与IMP请参考文章:Class、IMP、SEL是什么?
在+load方法中进行
Swizzling应该在+load方法中实现,因为+load方法可以保证在类最开始加载时会调用。因为method swizzling的影响范围是全局的,所以应该放在最保险的地方来处理是非常重要的。+load能够保证在类初始化的时候一定会被加载,这可以保证统一性。试想一下,若是在实际时需要的时候才去交换,那么无法达到全局处理的效果,而且若是临时使用的,在使用后没有及时地使用swizzling将系统方法与我们自定义的方法实现交换回来,那么后续的调用系统API就可能出问题。
类文件在工程中,一定会加载,因此可以保证+load会被调用。
使用dispatch_once保证只交换一次,确保性能
方法交换应该要线程安全,而且保证只交换一次,除非只是临时交换使用,在使用完成后又交换回来。
最常用的用法是在+load方法中使用dispatch_once来保证交换是安全的。因为swizzling会改变全局,我们需要在运行时采取相应的防范措施。保证原子操作就是一个措施,确保代码即使在多线程环境下也只会被执行一次。而diapatch_once就提供这些保障,因此我们应该将其加入到swizzling的使用标准规范中。
注意使用+load方法和dispatch_once确保实现!
创建一个交换IMP的通用扩展很必要
@interface NSObject (Swizzling)
+ (void)swizzleSelector:(SEL)originalSelector withSwizzledSelector:(SEL)swizzledSelector;
@end
#import "NSObject+Swizzling.h"
#import <objc/runtime.h>
// 实现代码如下
@implementation NSObject (Swizzling)
+ (void)swizzleSelector:(SEL)originalSelector withSwizzledSelector:(SEL)swizzledSelector
{
Class class = [self class];
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);
}
}
@end
因为方法可能不是在这个类里,可能是在其父类中才有实现,因此先尝试添加方法的实现,若添加成功了,则直接替换一下实现即可。若添加失败了,说明已经存在这个方法实现了,则只需要交换这两个方法的实现就可以了。
尽量使用method_exchangeImplementations函数来交换,因为它是原子操作的,线程安全。尽量不要自己手动写这样的代码:
IMP imp1 = method_getImplementation(m1);
IMP imp2 = method_getImplementation(m2);
method_setImplementation(m1, imp2);
method_setImplementation(m2, imp1);
NSMutableArray中
还记得那些调用数组的addObject:方法加入一个nil值是的崩溃情景吗?还记得[__NSPlaceholderArray initWithObjects:count:]因为有nil值而崩溃的提示吗?还记得调用objectAtIndex:时出现崩溃提示empty数组问题吗?那么通过swizzling特性,我们可以做到不让它崩溃,而只是打印一些有用的日志信息。
我们先来看看NSMutableArray的扩展实现:
#import "NSMutableArray+Swizzling.h"
#import <objc/runtime.h>
#import "NSObject+Swizzling.h"
@implementation NSMutableArray (Swizzling)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzleSelector:@selector(removeObject:)withSwizzledSelector:@selector(safeRemoveObject:)];
[objc_getClass("__NSArrayM") swizzleSelector:@selector(addObject:) withSwizzledSelector:@selector(safeAddObject:)];
[objc_getClass("__NSArrayM") swizzleSelector:@selector(removeObjectAtIndex:) withSwizzledSelector:@selector(safeRemoveObjectAtIndex:)];
[objc_getClass("__NSArrayM") swizzleSelector:@selector(insertObject:atIndex:) withSwizzledSelector:@selector(safeInsertObject:atIndex:)];
[objc_getClass("__NSPlaceholderArray") swizzleSelector:@selector(initWithObjects:count:) withSwizzledSelector:@selector(safeInitWithObjects:count:)];
[objc_getClass("__NSArrayM") swizzleSelector:@selector(objectAtIndex:) withSwizzledSelector:@selector(safeObjectAtIndex:)];
});
}
- (instancetype)safeInitWithObjects:(const id _Nonnull __unsafe_unretained *)objects count:(NSUInteger)cnt
{
BOOL hasNilObject = NO;
for (NSUInteger i = 0; i < cnt; i++) {
if ([objects[i] isKindOfClass:[NSArray class]]) {
NSLog(@"%@", objects[i]);
}
if (objects[i] == nil) {
hasNilObject = YES;
NSLog(@"%s object at index %lu is nil, it will be filtered", __FUNCTION__, i);
//#if DEBUG
// // 如果可以对数组中为nil的元素信息打印出来,增加更容 易读懂的日志信息,这对于我们改bug就好定位多了
// NSString *errorMsg = [NSString stringWithFormat:@"数组元素不能为nil,其index为: %lu", i];
// NSAssert(objects[i] != nil, errorMsg);
//#endif
}
}
// 因为有值为nil的元素,那么我们可以过滤掉值为nil的元素
if (hasNilObject) {
id __unsafe_unretained newObjects[cnt];
NSUInteger index = 0;
for (NSUInteger i = 0; i < cnt; ++i) {
if (objects[i] != nil) {
newObjects[index++] = objects[i];
}
}
return [self safeInitWithObjects:newObjects count:index];
}
return [self safeInitWithObjects:objects count:cnt];
}
- (void)safeAddObject:(id)obj {
if (obj == nil) {
NSLog(@"%s can add nil object into NSMutableArray", __FUNCTION__);
} else {
[self safeAddObject:obj];
}
}
- (void)safeRemoveObject:(id)obj {
if (obj == nil) {
NSLog(@"%s call -removeObject:, but argument obj is nil", __FUNCTION__);
return;
}
[self safeRemoveObject:obj];
}
- (void)safeInsertObject:(id)anObject atIndex:(NSUInteger)index {
if (anObject == nil) {
NSLog(@"%s can't insert nil into NSMutableArray", __FUNCTION__);
} else if (index > self.count) {
NSLog(@"%s index is invalid", __FUNCTION__);
} else {
[self safeInsertObject:anObject atIndex:index];
}
}
- (id)safeObjectAtIndex:(NSUInteger)index {
if (self.count == 0) {
NSLog(@"%s can't get any object from an empty array", __FUNCTION__);
return nil;
}
if (index > self.count) {
NSLog(@"%s index out of bounds in array", __FUNCTION__);
return nil;
}
return [self safeObjectAtIndex:index];
}
- (void)safeRemoveObjectAtIndex:(NSUInteger)index {
if (self.count <= 0) {
NSLog(@"%s can't get any object from an empty array", __FUNCTION__);
return;
}
if (index >= self.count) {
NSLog(@"%s index out of bound", __FUNCTION__);
return;
}
[self safeRemoveObjectAtIndex:index];
}
@end
然后,我们测试nil值的情况,是否还会崩溃呢?
NSMutableArray *array = [@[@"value", @"value1"] mutableCopy];
[array lastObject];
[array removeObject:@"value"];
[array removeObject:nil];
[array addObject:@"12"];
[array addObject:nil];
[array insertObject:nil atIndex:0];
[array insertObject:@"sdf" atIndex:10];
[array objectAtIndex:100];
[array removeObjectAtIndex:10];
NSMutableArray *anotherArray = [[NSMutableArray alloc] init];
[anotherArray objectAtIndex:0];
NSString *nilStr = nil;
NSArray *array1 = @[@"ara", @"sdf", @"dsfdsf", nilStr];
NSLog(@"array1.count = %lu", array1.count);
// 测试数组中有数组
NSArray *array2 = @[@[@"12323", @"nsdf", nilStr], @[@"sdf", @"nilsdf", nilStr, @"sdhfodf"]];
都不崩溃了,而且还打印出崩溃原因。是不是很神奇?如果充分利用这种特性,是不是可以给我们带来很多便利之处?
上面只是swizzling的一种应用场景而已。其实利用swizzling特性还可以做很多事情的,比如处理按钮重复点击问题等。
NSMutableDictionary中
#import <Foundation/Foundation.h>
@interface NSMutableDictionary (Swizzling)
@end
#import "NSMutableDictionary+Swizzling.h"
#import <objc/runtime.h>
#import "NSObject+Swizzling.h"
@implementation NSMutableDictionary (Swizzling)
+(void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[objc_getClass("__NSDictionaryM") swizzleSelector:@selector(setValue:forKey:) withSwizzledSelector:@selector(safeSetValue:forKey:)];
[objc_getClass("__NSDictionaryM") swizzleSelector:@selector(setObject:forKey:) withSwizzledSelector:@selector(safeSetObject:forKey:)];
[objc_getClass("__NSDictionaryM") swizzleSelector:@selector(removeObjectForKey:) withSwizzledSelector:@selector(safeRemoveObjectForKey:)];
});
}
- (void)safeSetValue:(id)value forKey:(NSString *)key
{
if (key == nil || value == nil || [key isEqual:[NSNull null]] || [value isEqual:[NSNull null]]) {
#if DEBUG
NSLog(@"%s call -safeSetValue:forKey:, key或vale为nil或null", __FUNCTION__);
#endif
return;
}
[self safeSetValue:value forKey:key];
}
- (void)safeSetObject:(id)anObject forKey:(id<NSCopying>)aKey
{
if (aKey == nil || anObject == nil || [anObject isEqual:[NSNull null]]) {
#if DEBUG
NSLog(@"%s call -safeSetObject:forKey:, key或vale为nil或null", __FUNCTION__);
#endif
return;
}
[self safeSetObject:anObject forKey:aKey];
}
- (void)safeRemoveObjectForKey:(id)aKey
{
if (aKey == nil || [aKey isEqual:[NSNull null]] ) {
#if DEBUG
NSLog(@"%s call -safeRemoveObjectForKey:, aKey为nil或null", __FUNCTION__);
#endif
return;
}
[self safeRemoveObjectForKey:aKey];
}
@end
UIButton避免重复恶意点击
#import <UIKit/UIKit.h>
#define defaultInterval 0.5 //默认时间间隔
@interface UIButton (Swizzling)
@property (nonatomic, assign) NSTimeInterval timeInterval;
@end
#import "UIButton+Swizzling.h"
#import <objc/runtime.h>
#import "NSObject+Swizzling.h"
@interface UIButton()
/**bool 类型 YES 不允许点击 NO 允许点击 设置是否执行点UI方法*/
@property (nonatomic, assign) BOOL isIgnoreEvent;
@end
@implementation UIButton (Swizzling)
+(void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[objc_getClass("UIButton") swizzleSelector:@selector(sendAction:to:forEvent:) withSwizzledSelector:@selector(customSendAction:to:forEvent:)];
});
}
- (void)customSendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
{
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 customSendAction:action to:target forEvent:event];
}
- (NSTimeInterval)timeInterval
{
return [objc_getAssociatedObject(self, _cmd) doubleValue];
}
- (void)setTimeInterval:(NSTimeInterval)timeInterval
{
objc_setAssociatedObject(self, @selector(timeInterval), @(timeInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
//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)resetState{
[self setIsIgnoreEvent:NO];
}
@end