下所有內(nèi)容均為個人觀點(diǎn),轉(zhuǎn)載請注明出處<簡書--小蝸牛吱呀之悠悠 >,謝謝!
最近工作中出現(xiàn)較多的數(shù)組越界、插入空對象、字典插入空鍵值對方面的問題,由于涉及范圍廣,如果一處一處尋找解決,非常耗費(fèi)人力,于是使用runtime并結(jié)合category,通過方法替換來規(guī)避解決。使用過程中遇到不少問題,也許讀者您也存在同樣的問題,為方便查閱,故寫下這篇博客,若有不恰當(dāng)之處敬請留言指導(dǎo),不勝感激!本文主要介紹runtime的用法,將會從下面三方面詳細(xì)介紹:動態(tài)綁定屬性、動態(tài)獲取屬性列表、動態(tài)交換方法
注意:此文涉及category及runtime用法,如果對這兩模塊未接觸過,請自行百度有所熟悉后再閱讀此文,以便您能更好的理解用法。
一、category、運(yùn)行時簡述
1、眾所周知,category可以為各種類擴(kuò)充能力集;有時,簡單的靜態(tài)運(yùn)行方法和變量無法滿足實(shí)際的需要,于是,你將會使用到runtime,同時結(jié)合category的特性,解決一些繁瑣的、耗費(fèi)人力的工作。
2、runtime(運(yùn)行時)是OC語言的三大特性之一,指變量、方法、對象等的類型需要在運(yùn)行時才會確定,由此衍生出很多面試題(感興趣的同學(xué)請自行翻閱)。
二、runtime+category之動態(tài)綁定屬性
1、大家應(yīng)該都了解,category是不允許使用聲明式屬性的,原因也比較簡單。聲明式屬性通常是由系統(tǒng)替開發(fā)者完成setter/getter方法的生成,同時綁定實(shí)例變量。表面上是比較好的解釋了為什么category不能添加聲明式屬性。請注意此處用詞“聲明式屬性”。然后真實(shí)原因是什么呢?真實(shí)原因是,類的屬性是在編譯期就已經(jīng)確定了,編譯器會在編譯時,將屬性對應(yīng)的實(shí)力變量添加到屬性列表中。category是運(yùn)行時的一種體現(xiàn),否則也法與runtime結(jié)合使用,因此,category需要在運(yùn)行狀態(tài)才會確定變量。然而,在運(yùn)行狀態(tài),類的屬性列表處于封閉狀態(tài),無法改變內(nèi)部格局,所以,category是無法添加屬性的。
2、既然category無法添加屬性,那么為何又說動態(tài)綁定呢?其實(shí)所謂的動態(tài)綁定屬性,是一種假屬性,是你自己完成setter/getter方法,同時完成實(shí)力變量的綁定操作。具體代碼參考如下:
//1、和普通屬性添加一樣
@property (nonatomic, assign) BOOL subEnabled;
//2、動態(tài)關(guān)聯(lián)屬性
static const void *subEnableKey = &subEnableKey;
//手動生成setter/getter方法
- (void)setSubEnabled:(BOOL)subEnable {
objc_setAssociatedObject(self, subEnableKey, @(subEnable), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)subEnabled {
return [objc_getAssociatedObject(self, subEnableKey) boolValue];
}
動態(tài)綁定屬性的操作比較簡單,教程也比較多,如果有不理解的,可以再翻閱更加詳細(xì)的文章,基本格式都如文中所述,可以說是固定格式。如果僅僅是添加一個屬性,實(shí)際使用意義并不是很大,但是,如果這個屬性是系統(tǒng)類所帶的,并且這個屬性不完全滿足你的要求,而且這個屬性被項目中大規(guī)模的使用,牽一發(fā)動全身的情況下,該怎么辦呢?答案是:動態(tài)綁定屬性+動態(tài)方法替換。詳細(xì)的請看下文。
三、runtime+category之動態(tài)獲取屬性列表
為什么要獲取屬性列表呢,類具體有哪些屬性我們不是可以清楚的看到嗎?在了解動態(tài)獲取屬性列表之前,請初步了解一下KVC鍵值對模式。其實(shí),本質(zhì)上,我們對屬性賦值或者是取出屬性的值,都是通過setter/getter方法實(shí)現(xiàn)的,這一點(diǎn)大家都清楚;但深究下去,setter/getter方法卻又是通過KVC的形式去操作實(shí)例變量的。那么了解到這里你會發(fā)現(xiàn),很多時候我們需要用到屬性對應(yīng)的key值,比如:數(shù)據(jù)解析、數(shù)據(jù)持久化等。而這些方式卻都是通過KVC的形式實(shí)現(xiàn)的,所以,即便我們清楚的知道了每一個屬性是什么,但如果一個一個的通過KVC的形式添加,工作量之大可以想象,這時候就需要用runtime來為我們提高效率了。
言歸正傳,直接上代碼:
//獲取屬性列表
/*
參數(shù)1:要獲取的屬性列表所在的class類
參數(shù)2:屬性的數(shù)量
*/
unsigned int outCount = 0;
Ivar *varList = class_copyIvarList(self.class, &outCount);
//通過for循環(huán),遍歷每個屬性
for (int i = 0; i<outCount; i++) {
//Ivar是結(jié)構(gòu)體,包含屬性名等
Ivar tmpIvar = varList[i];
//從Ivar結(jié)構(gòu)體中獲取屬性名字符串
const char *name = ivar_getName(tmpIvar);
//獲取屬性的名稱的字符串 c字符串->oc字符串
NSString *propertyName = [NSString stringWithUTF8String:name];
//通過KVC模式,獲取對應(yīng)的值
id obj = [self valueForKey:propertyName];
#warning 用完以后必須釋放
free(varList);
}
代碼注釋應(yīng)該比較清楚,這里不多做贅述。下面請看一下歸檔解檔的具體應(yīng)用:
//歸檔操作
- (void)encodeWithCoder:(NSCoder*)aCoder{
//獲取屬性列表
unsigned int outCount = 0;
Ivar *varList = class_copyIvarList(self.class, &outCount);
//通過for循環(huán),對每個屬性進(jìn)行設(shè)置
for (int i = 0; i<outCount; i++) {
//Ivar是結(jié)構(gòu)體,包含屬性名等
Ivar tmpIvar = varList[i];
//從Ivar結(jié)構(gòu)體中獲取屬性名字符串
const char *name = ivar_getName(tmpIvar);
//獲取屬性的名稱的字符串 c字符串->oc字符串
NSString *propertyName = [NSString stringWithUTF8String:name];
//通過KVC模式,獲取對應(yīng)的值
id obj = [self valueForKey:propertyName];
[aCoder encodeObject:obj forKey:propertyName];
}
free(varList);
}
//解檔操作
- (instancetype)initWithCoder:(NSCoder*)aDecoder{
if (self = [super init]) {
unsigned int outCount = 0;
//1.拿屬性列表
Ivar *varList = class_copyIvarList(self.class, &outCount);
//2.for循環(huán)每個屬性,獲取名字
for (int i = 0; i<outCount; i++) {
Ivar tmpIvar = varList[i];
const char *name = ivar_getName(tmpIvar);
NSString *propertyName = [NSString stringWithUTF8String:name];
//3.通過名字用decodeobj獲取對應(yīng)值
id obj = [aDecoder decodeObjectForKey:propertyName];
//4.通過setValue forKey進(jìn)行賦值操作
[self setValue:obj forKey:propertyName];
}
free(varList);
}
return self;
}
歸解檔操作往往在數(shù)據(jù)本地持久化(保存硬盤、NSUserDefault中的自定義對象保存等)應(yīng)用較為廣泛,如果不適用runtime,則工作量巨大,并且代碼非常不優(yōu)雅,會有大量的垃圾代碼。
四、runtime+category之動態(tài)交換方法
runtime結(jié)合category可以實(shí)現(xiàn)方法體的動態(tài)交換,以滿足一些特殊需要,例如上文中提到動態(tài)綁定屬性+動態(tài)方法替換
1、某一種應(yīng)用場景是這樣的:一個已經(jīng)上線的項目,收集到的崩潰日志中,大量體現(xiàn)了數(shù)組越界、插入空對象、字典插入空對象等數(shù)據(jù)不合法的操作導(dǎo)致的崩潰。此時,就需要對數(shù)組、字典的操作增加越界保護(hù)、非空保護(hù)等措施來避免崩潰。然而,項目中使用到數(shù)組字典的地方非常的多,并且邏輯應(yīng)用也比較復(fù)雜,完全依靠人工修改,是不可能完成的,并且很容易出錯。那么該怎么辦呢?
2、要解決上述問題,我們可以采取重寫系統(tǒng)方法來完成,但是這種做法風(fēng)險巨大,一不小心就不給上架。那么就還有另一個更好的措施,既能解決問題,又不影響上架,而且代碼很優(yōu)雅------動態(tài)替換系統(tǒng)方法
3、在學(xué)習(xí)動態(tài)替換系統(tǒng)方法之前,我們先梳理一下思路:我們想要對數(shù)據(jù)做保護(hù),僅僅是希望在操作系統(tǒng)的方法之前,增加一些我們認(rèn)為合理的判斷,只需要保證在執(zhí)行系統(tǒng)方法之前我們的判斷被執(zhí)行即可。那么,我們就沿著這個思路往下走。
4、要使得所有的數(shù)組字典都生效,唯一的快速高效的辦法就是借助category。category中需要用到的兩個方法我們先一起來學(xué)習(xí)一下:
+ (void)load;
+ (void)initialize;
這兩個方法有什么區(qū)別呢?
- (void)initialize 當(dāng)類第一次被調(diào)用的時候就會調(diào)用該方法
- (void)load 當(dāng)程序啟動的時候就會調(diào)用該方法,換句話說,只要程序一啟動就會調(diào)用load方法,整個程序運(yùn)行中只會調(diào)用一次
這兩個方法的選擇非常的關(guān)鍵,選擇的不合適將會導(dǎo)致程序運(yùn)行不起來。我舉兩個實(shí)際的例子,如下:
例一:
+ (void)initialize {
static dispatch_once_t once;
//替換方法,校驗url中是否含有http
dispatch_once(&once, ^{
[self swizzleMethods:[UIImageView class] originalSelector:@selector(sd_setImageWithURL:placeholderImage:options:progress:completed:) swizzledSelector:@selector(dd_setImageWithURL:placeholderImage:options:progress:completed:)];
});
}
sd_setImageWithURL:placeholderImage:options:progress:completed:方法是SDWebImage框架中設(shè)置圖片的方法。為了降低帶寬消耗和安全性考慮,從某個版本開始,部分URL的前綴地址放在APP測拼接,上述方法的作用就是校驗是否已經(jīng)拼接完成。
因為SDWebImge的方法也是一個category中的方法,所以此處使用的是initialize,只要該方法被調(diào)用了,就會先去校驗。
例二:
+ (void)load {
static dispatch_once_t once;
//非空保護(hù)
dispatch_once(&once, ^{
[self swizzleMethods:NSClassFromString(@"__NSPlaceholderDictionary") originalSelector:@selector(initWithObjects:forKeys:count:) swizzledSelector:@selector(__NSPlaceholderDictionary_initWithObject:forKeys:count:)];
Class targetClass = NSClassFromString(@"__NSDictionaryI");
[self swizzleClassMethods:targetClass originalSelector:@selector(dictionaryWithObject:forKey:) swizzledSelector:@selector(__NSDictionaryI_dictionaryWithObject:forKey:)];
[self swizzleClassMethods:targetClass originalSelector:@selector(dictionaryWithObjects:forKeys:) swizzledSelector:@selector(__NSDictionaryI_dictionaryWithObjects:forKeys:)];
});
}
由于NSDictionary是系統(tǒng)類,在difinishLaunch方法執(zhí)行之前就會有部分系統(tǒng)操作會調(diào)用到,所以此處使用load方法最為合適。
5、了解完前提內(nèi)容后,我們來一起看一下如何使用runtime去動態(tài)交換方法。
@interface NSObject (Utils)
/*
* 實(shí)例方法交換
* 帶"-"號的方法應(yīng)該使用這個
* originalSelector 原始方法選擇器
* swizzledSelector 新方法選擇器
*/
+ (void)swizzleMethods:(Class)class originalSelector:(SEL)origSel swizzledSelector:(SEL)swizSel;
/*
* 類方法交換
* 帶"+"號的方法應(yīng)該使用這個
* originalSelector 原始方法選擇器
* swizzledSelector 新方法選擇器
*/
+ (void)swizzleClassMethods:(Class)class originalSelector:(SEL)origSel swizzledSelector:(SEL)swizSel;
@end
@implementation NSObject (Utils)
+ (void)swizzleMethods:(Class)class originalSelector:(SEL)origSel swizzledSelector:(SEL)swizSel {
Method origMethod = class_getInstanceMethod(class, origSel);
Method swizMethod = class_getInstanceMethod(class, swizSel);
BOOL didAddMethod = class_addMethod(class, origSel, method_getImplementation(swizMethod), method_getTypeEncoding(swizMethod));
if (didAddMethod) {
class_replaceMethod(class, swizSel, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
} else {
method_exchangeImplementations(origMethod, swizMethod);
}
}
+ (void)swizzleClassMethods:(Class)class originalSelector:(SEL)origSel swizzledSelector:(SEL)swizSel {
@autoreleasepool {
Method origMethod = class_getClassMethod(class, origSel);
Method swizMethod = class_getClassMethod(class, swizSel);
method_exchangeImplementations(origMethod, swizMethod);
}
}
@end
上述方法中,第一個方法用于交換實(shí)例方法(帶“-”號的方法),第二個方法用于交換類方法(帶“+”號的方法),至于這兩個方法的區(qū)別,這里不做贅述,有疑問的同學(xué)請百度一下。
- class_getInstanceMethod 獲取實(shí)例方法的方法體
- class_getClassMethod 獲取類方法的方法體
- class_addMethod 添加方法,如果方法為實(shí)現(xiàn),則添加成功,返回YES
- class_replaceMethod 方法替換,覆蓋原有方法
- method_exchangeImplementations 方法體交換
上述方法的大致意思:獲取替換前后方法的方法體,然后互相交換
注意:互相交換以后,函數(shù)名稱不變,函數(shù)的具體實(shí)現(xiàn)互相交換了
6、替換后的方法體實(shí)現(xiàn)
/*__NSPlaceholderDictionary*/
- (instancetype)__NSPlaceholderDictionary_initWithObject:(id *)objects forKeys:(id<NSCopying> *)keys count:(NSUInteger)count {
for (int i = 0; i < count; i++) {
if (!objects[i] || !keys[i]) {
NSArray *syms = [NSThread callStackSymbols];
NSAssert(NO, @"字典的key和對象不能為nil,調(diào)用者:%@",NSStringFromSelector(_cmd),[syms objectAtIndex:1]);
return nil;
}
}
return [self __NSPlaceholderDictionary_initWithObject:objects forKeys:keys count:count];
}
此處需要特殊說明一下,替換方法的時候,應(yīng)該使用NSDictionary的族類,不是直接使用NSDictionary
NSArray:
- [NSArray alloc] 的族類為__NSPlaceholderArray
- [[NSArray alloc] init]和@[]的族類為__NSArray0
- 數(shù)組中有一個對象時的族類為__NSSingleObjectArrayI
- 數(shù)組中有多個對象時的族類為__NSArrayI
NSMutableNSArray:
- 所有的族類為__NSArrayM
NSDictionary:
- [NSDictionary alloc] 的族類為__NSPlaceholderDictionary
- 其余情況的族類為__NSDictionaryI
NSMutableNSDictionary:
- 所有的族類為__NSDictionaryM
特別需要注意:
- 由于NSMutableNSArray和NSMutableNSDictionary分別是NSArray和NSDictionary的子類,所以一部分方法是直接繼承父類的,在替換方法時一定要注意區(qū)分,避免造成崩潰
- 每一種派生類對應(yīng)的替換方法都需要唯一,并且重新實(shí)現(xiàn)一遍,否則會導(dǎo)致方法交換錯亂如下
[self swizzleMethods:NSClassFromString(@"__NSArray0") originalSelector:@selector(objectAtIndex:) swizzledSelector:@selector(__NSArray0_objectAtIndex:)];
[self swizzleMethods:NSClassFromString(@"__NSArrayI") originalSelector:@selector(objectAtIndex:) swizzledSelector:@selector(__NSArrayI_objectAtIndex:)];
[self swizzleMethods:NSClassFromString(@"__NSSingleObjectArrayI") originalSelector:@selector(objectAtIndex:) swizzledSelector:@selector(__NSSingleObjectArrayI_objectAtIndex:)];
7、替換后的方法實(shí)現(xiàn)距離如下:
/*__NSArray0*/
- (id)__NSArray0_objectAtIndex:(NSUInteger)index {
if (index >= self.count) {
NSArray *syms = [NSThread callStackSymbols];
NSAssert(NO, @"數(shù)組越界了,調(diào)用者:%@",NSStringFromSelector(_cmd),[syms objectAtIndex:1]);
return nil;
}
return [self __NSArray0_objectAtIndex:index];
}
想必有不少同學(xué)對這兩句不理解:
- [self __NSArray0_objectAtIndex:index]
- [syms objectAtIndex:1]
會有疑問,同樣是一個方法體中,這里為何用法完全相反?[self __NSArray0_objectAtIndex:index]這樣使用難道不會造成死循環(huán)嗎?答案是否定的。
要理解這里,我們需要從方法被調(diào)用時來看。假設(shè),數(shù)組內(nèi)容如下:
/*__NSArray0*/
NSArray *array = @[@"這是一個數(shù)組"];
[array objectAtIndex:0];
當(dāng)objectAtIndex被調(diào)用時,它的方法體其實(shí)已經(jīng)被替換成了我們增加過越界保護(hù)的內(nèi)容了,而__NSArray0_objectAtIndex的方法體才是系統(tǒng)自帶的方法體內(nèi)容,此處比較繞,請大家仔細(xì)想明白。當(dāng)執(zhí)行到[syms objectAtIndex:1]時,其實(shí)也是需要數(shù)組越界保護(hù)的,所以此處應(yīng)該調(diào)用替換后的方法體,對應(yīng)的方法名則是原始方法名。執(zhí)行至[self __NSArray0_objectAtIndex:index]處時,它的方法體已經(jīng)是系統(tǒng)提供的真正的objectAtIndex的方法體,所以此處并不會造成死循環(huán)。
動態(tài)綁定屬性+動態(tài)方法替換
其實(shí)只要理解了上述內(nèi)容,此種用法就很簡單了,原理就是將getter或者setter方法的方法體替換為我們自己定義的方法體,這樣就實(shí)現(xiàn)了對屬性的功能擴(kuò)充