Runtime是什么杨帽?
Apple關(guān)于Runtime的詳細(xì)文檔鏈接:Runtime Guide
其實(shí)大家對(duì)Runtime算是既熟悉又陌生的痪署,因?yàn)樵趯W(xué)習(xí)Objective-C的時(shí)候就知道這門語言的強(qiáng)大之處在于其動(dòng)態(tài)性霞势,那么什么是動(dòng)態(tài)性呢,這個(gè)時(shí)候就會(huì)接觸到Runtime的概念了,顧名思義,Runtime是在一種進(jìn)行時(shí)的特性抬伺,只有在真正運(yùn)行的時(shí)候才會(huì)根據(jù)函數(shù)的名稱找到對(duì)應(yīng)的函數(shù)來調(diào)用,就是說在工程編譯階段才會(huì)確定所有函數(shù)的執(zhí)行路徑等灾梦,這個(gè)就是進(jìn)行時(shí)的特色了峡钓。
那么知道了這個(gè)特點(diǎn),對(duì)于我們來說與什么實(shí)際價(jià)值呢若河?
這邊文章介紹了幾個(gè)常用的場景可以讓你快速的領(lǐng)悟Runtime的精神并且可以拿去分(zhuang)享(B)能岩。。萧福。
Runtime實(shí)現(xiàn)原理R簡介
Runtime是一套比較底層的純C語言API, 屬于一個(gè)C語言庫, 包含了很多底層的C語言API拉鹃。
在我們平時(shí)編寫的OC代碼中, 程序運(yùn)行過程時(shí), 其實(shí)最終都是轉(zhuǎn)成了Runtime的C語言代碼。
Runtime算是Objective-C的幕后工作者统锤!
例如毛俏,下面一個(gè)創(chuàng)建Dog對(duì)象的方法中,
在OC中 :
[[Dog alloc] init]
在Runtime中就變成 :
objc_msgSend(objc_msgSend(“Dog” , “alloc”), “init”)
Runtime用來做什么饲窿?
1煌寇、在程序運(yùn)行過程中, 動(dòng)態(tài)創(chuàng)建一個(gè)類(比如KVO的底層實(shí)現(xiàn))
2、在程序運(yùn)行過程中, 動(dòng)態(tài)地為某個(gè)類添加屬性\方法, 修改屬性值\方法
3逾雄、遍歷一個(gè)類的所有成員變量(屬性)\所有方法
例如:我們需要對(duì)一個(gè)類的屬性進(jìn)行歸檔解檔的時(shí)候?qū)傩蕴貏e的多阀溶,這時(shí)候,我們就會(huì)寫很多對(duì)應(yīng)的代碼鸦泳,但是如果使用了runtime就可以動(dòng)態(tài)設(shè)置银锻!
4、就是今天要著重講的最常用到的一些使用:可以利用Runtime,避免UIButton 重復(fù)點(diǎn)擊, 可變數(shù)組和可變字典為nil,或者數(shù)組越界導(dǎo)致的Crash問題做鹰。
利用Runtime解決數(shù)組字典的崩潰問題
適用場景:當(dāng)我們從后臺(tái)請(qǐng)求到的數(shù)據(jù)击纬,需要把其中一個(gè)插入到數(shù)組的時(shí)候,需要先判斷該對(duì)象是否為空值钾麸,非空才能插入更振,否則會(huì)引起崩潰。Runtime可以從根本上解決饭尝,即使我插入的是空值肯腕,也不會(huì)引起崩潰钥平。
Method Swizzling
在Objective-C中調(diào)用一個(gè)方法,其實(shí)是向一個(gè)對(duì)象發(fā)送消息,而查找消息的唯一依據(jù)是selector的名字捷兰。所以,我們可以實(shí)現(xiàn)在運(yùn)行時(shí)交換selector對(duì)應(yīng)的方法實(shí)現(xiàn)以達(dá)到效果肴甸。
每個(gè)類都有一個(gè)方法列表寂殉,存放著SEL(selector)的名字和方法實(shí)現(xiàn)的映射關(guān)系原在。IMP(Implementation Method Path)有點(diǎn)類似函數(shù)指針,指向具體的Method實(shí)現(xiàn)庶柿。
關(guān)于SEL與IMP請(qǐng)參考文章:Class村怪、IMP、SEL是什么浮庐?
在+load方法中進(jìn)行
Swizzling應(yīng)該在+load方法中實(shí)現(xiàn)审残,因?yàn)?load方法可以保證在類最開始加載時(shí)會(huì)調(diào)用。因?yàn)閙ethod swizzling的影響范圍是全局的搅轿,所以應(yīng)該放在最保險(xiǎn)的地方來處理是非常重要的璧坟。+load能夠保證在類初始化的時(shí)候一定會(huì)被加載既穆,這可以保證統(tǒng)一性幻工。試想一下黎茎,若是在實(shí)際時(shí)需要的時(shí)候才去交換,那么無法達(dá)到全局處理的效果迁酸,而且若是臨時(shí)使用的俭正,在使用后沒有及時(shí)地使用swizzling將系統(tǒng)方法與我們自定義的方法實(shí)現(xiàn)交換回來焙畔,那么后續(xù)的調(diào)用系統(tǒng)API就可能出問題。
類文件在工程中儿惫,一定會(huì)加載,因此可以保證+load會(huì)被調(diào)用肾请。
使用dispatch_once保證只交換一次留搔,確保性能
方法交換應(yīng)該要線程安全,而且保證只交換一次铛铁,除非只是臨時(shí)交換使用饵逐,在使用完成后又交換回來。
最常用的用法是在+load方法中使用dispatch_once來保證交換是安全的掷豺。因?yàn)閟wizzling會(huì)改變?nèi)直∩覀冃枰谶\(yùn)行時(shí)采取相應(yīng)的防范措施。保證原子操作就是一個(gè)措施德频,確保代碼即使在多線程環(huán)境下也只會(huì)被執(zhí)行一次廓奕。而diapatch_once就提供這些保障,因此我們應(yīng)該將其加入到swizzling的使用標(biāo)準(zhǔn)規(guī)范中蒸绩。
注意使用+load方法和dispatch_once確保實(shí)現(xiàn)铃肯!
創(chuàng)建一個(gè)交換IMP的通用擴(kuò)展很必要
@interface NSObject (Swizzling)
+ (void)swizzleSelector:(SEL)originalSelector withSwizzledSelector:(SEL)swizzledSelector;
@end
#import "NSObject+Swizzling.h"
#import <objc/runtime.h>
// 實(shí)現(xiàn)代碼如下
@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);
// 若已經(jīng)存在押逼,則添加會(huì)失敗
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
因?yàn)榉椒赡懿皇窃谶@個(gè)類里,可能是在其父類中才有實(shí)現(xiàn)雾消,因此先嘗試添加方法的實(shí)現(xiàn),若添加成功了狂窑,則直接替換一下實(shí)現(xiàn)即可桑腮。若添加失敗了,說明已經(jīng)存在這個(gè)方法實(shí)現(xiàn)了丛晦,則只需要交換這兩個(gè)方法的實(shí)現(xiàn)就可以了添忘。
盡量使用method_exchangeImplementations函數(shù)來交換,因?yàn)樗窃硬僮鞯母拢€程安全仲器。盡量不要自己手動(dòng)寫這樣的代碼:
IMP imp1 = method_getImplementation(m1);
IMP imp2 = method_getImplementation(m2);
method_setImplementation(m1, imp2);
method_setImplementation(m2, imp1);
NSMutableArray中
還記得那些調(diào)用數(shù)組的addObject:方法加入一個(gè)nil值是的崩潰情景嗎乏冀?還記得[__NSPlaceholderArray initWithObjects:count:]因?yàn)橛衝il值而崩潰的提示嗎?還記得調(diào)用objectAtIndex:時(shí)出現(xiàn)崩潰提示empty數(shù)組問題嗎昼捍?那么通過swizzling特性肢扯,我們可以做到不讓它崩潰蔚晨,而只是打印一些有用的日志信息。
我們先來看看NSMutableArray的擴(kuò)展實(shí)現(xiàn):
#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
// // 如果可以對(duì)數(shù)組中為nil的元素信息打印出來银择,增加更容 易讀懂的日志信息浩考,這對(duì)于我們改bug就好定位多了
// NSString *errorMsg = [NSString stringWithFormat:@"數(shù)組元素不能為nil被盈,其index為: %lu", i];
// NSAssert(objects[i] != nil, errorMsg);
//#endif
}
}
// 因?yàn)橛兄禐閚il的元素析蝴,那么我們可以過濾掉值為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值的情況,是否還會(huì)崩潰呢裁赠?
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);
// 測試數(shù)組中有數(shù)組
NSArray *array2 = @[@[@"12323", @"nsdf", nilStr], @[@"sdf", @"nilsdf", nilStr, @"sdhfodf"]];
都不崩潰了赴精,而且還打印出崩潰原因蕾哟。是不是很神奇?如果充分利用這種特性谭确,是不是可以給我們帶來很多便利之處逐哈?
上面只是swizzling的一種應(yīng)用場景而已。其實(shí)利用swizzling特性還可以做很多事情的禀梳,比如處理按鈕重復(fù)點(diǎn)擊問題等肠骆。
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避免重復(fù)惡意點(diǎn)擊
#import <UIKit/UIKit.h>
#define defaultInterval 0.5 //默認(rèn)時(shí)間間隔
@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 不允許點(diǎn)擊 NO 允許點(diǎn)擊 設(shè)置是否執(zhí)行點(diǎn)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互換了郊艘,實(shí)際上執(zhí)行 sendAction唯咬;所以不會(huì)死循環(huán)
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 動(dòng)態(tài)綁定 屬性
- (void)setIsIgnoreEvent:(BOOL)isIgnoreEvent{
// 注意BOOL類型 需要用OBJC_ASSOCIATION_RETAIN_NONATOMIC 不要用錯(cuò)胆胰,否則set方法會(huì)賦值出錯(cuò)
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