runtime+category黑科技(動態(tài)添加屬性、動態(tài)獲取屬性列表、方法交換)

下所有內(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ò)充

本文至此已經(jīng)全部結(jié)束,希望能夠?qū)δ兴鶐椭?,后續(xù)將盡量補(bǔ)充數(shù)組、字典的category文件。感謝您的閱讀!
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容