iOS Runtime ——實際應(yīng)用篇

我們都知道OC是一種面向?qū)ο蟮膭討B(tài)語言,它將很多在編譯和鏈接時期的事放到了runtime運行時來處理,runtime無疑是OC的靈魂。
那么對于我們開發(fā)來說,runtime有什么實際的應(yīng)用嗎?答案是有的。今天我們就具體應(yīng)用這方面來聊聊。

常見應(yīng)用方式:
1.runtime交換方法

場景:當(dāng)?shù)谌娇蚣芑蛘呦到y(tǒng)原生的方法不滿足需求的時候,可以在不改動原有方法的基礎(chǔ)上,添加額外的功能。
需求:加載一張圖片直接用[UIImage imageNamed:@"image"];是無法知道到底有沒有加載成功。給系統(tǒng)的imageNamed添加額外功能(是否加載圖片成功)。
方案一:繼承系統(tǒng)的類,重寫方法.
方案二:使用 runtime,交換方法.
步驟:1.給系統(tǒng)的方法添加分類 2.自己實現(xiàn)一個帶有擴展功能的方法 3.使用runtime,交換方法。
給UIImage添加分類代碼

// 加載圖片 且 帶判斷是否加載成功
+ (UIImage *)ln_imageNamed:(NSString *)name {
    
    UIImage *image = [UIImage ln_imageNamed:name];
    if (image) {
        NSLog(@"runtime交互方法 -> 圖片加載成功");
    } else {
        NSLog(@"runtime交互方法 -> 圖片加載失敗");
    }
    return image;
}
/
 作用:把類加載進內(nèi)存的時候調(diào)用,只會調(diào)用一次
 調(diào)用:方法應(yīng)先交換,再去調(diào)用
 */
+ (void)load {
    
    // 1.獲取 imageNamed方法地址
    Method imageNamedMethod = class_getClassMethod(self, @selector(imageNamed:));
    // 2.獲取 ln_imageNamed方法地址
    Method ln_imageNamedMethod = class_getClassMethod(self, @selector(ln_imageNamed:));
    
    // 3.交換方法地址,相當(dāng)于交換實現(xiàn)方式;「method_exchangeImplementations 交換兩個方法的實現(xiàn)」
    method_exchangeImplementations(imageNamedMethod, ln_imageNamedMethod);
}

調(diào)用的時候:

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    self.imageView.image = [UIImage imageNamed:@"CoerLN"];
}

- - -
// 打印輸出   --------- runtime交互方法 -> 圖片加載成功

總結(jié):我們所做的就是在方法調(diào)用流程第三步的時候,交換兩個方法地址指向。而且我們改變指向要在系統(tǒng)的imageNamed:方法調(diào)用前,所以將代碼寫在了分類的load方法里。最后當(dāng)運行的時候系統(tǒng)的方法就會去找我們的方法的實現(xiàn)。

2.給系統(tǒng)分類動態(tài)添加屬性

場景:給系統(tǒng)的類添加額外屬性的時候,可以使用runtime動態(tài)添加屬性方法。
原理:給一個類聲明屬性,其實本質(zhì)就是給這個類添加關(guān)聯(lián),并不是直接把這個值的內(nèi)存空間添加到類存空間。
注解:給系統(tǒng) NSObject 添加一個分類,我們知道在分類中是不能夠添加成員屬性的,雖然我們用了@property,但是僅僅會自動生成get和set方法的聲明,并沒有帶下劃線的屬性和方法實現(xiàn)生成。但是我們可以通過runtime就可以做到給它方法的實現(xiàn)。
需求:給系統(tǒng) NSObject 類動態(tài)添加屬性 name 字符串。
給UIImage添加分類代碼

- (NSString *)name
{
    // 利用參數(shù)key 將對象object中存儲的對應(yīng)值取出來
    return objc_getAssociatedObject(self, @"name");
}

- (void)setName:(NSString *)name
{
    /**
     將某個值跟某個對象關(guān)聯(lián)起來,將某個值存儲到某個對象中
     objc_setAssociatedObject(<#id  _Nonnull object#>:給哪個對象添加屬性, <#const void * _Nonnull key#>:屬性名稱, <#id  _Nullable value#>:屬性值, <#objc_AssociationPolicy policy#>:保存策略)
     */
    objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    NSLog(@"name---->%p",name);
}

調(diào)用代碼

NSObject *objc = [[NSObject alloc] init];
objc.name = @"Cary";
NSLog(@"runtime動態(tài)添加屬性name==%@",objc.name);
 +++++++++++++++
// 打印輸出----------------- runtime動態(tài)添加屬性name == Cary

總結(jié):屬性賦值的本質(zhì),就是讓屬性與一個對象產(chǎn)生關(guān)聯(lián),所以要給NSObject的分類的name屬性賦值就是讓name和NSObject產(chǎn)生關(guān)聯(lián),而runtime可以做到這一點。

3.字典轉(zhuǎn)模型

思路:利用運行時,遍歷模型中所有屬性,根據(jù)模型的屬性名,去字典中查找key,取出對應(yīng)的值,給模型的屬性賦值(從提醒:字典中取值,不一定要全部取出來);提供一個NSObject分類,專門字典轉(zhuǎn)模型,以后所有模型都可以通過這個分類實現(xiàn)字典轉(zhuǎn)模型。

考慮情況:
1.當(dāng)字典的key和模型的屬性匹配不上。
2.模型中嵌套模型(模型屬性是另外一個模型對象)。
3.數(shù)組中裝著模型(模型的屬性是一個數(shù)組,數(shù)組中是一個個模型對象)。

注解:
根據(jù)上面的三種特殊情況,先是字典的key和模型的屬性不對應(yīng)的情況。不對應(yīng)有兩種,一種是字典的鍵值大于模型屬性數(shù)量,這時候我們不需要任何處理,因為runtime是先遍歷模型所有屬性,再去字典中根據(jù)屬性名找對應(yīng)值進行賦值,多余的鍵值對也當(dāng)然不會去看了;另外一種是模型屬性數(shù)量大于字典的鍵值對,這時候由于屬性沒有對應(yīng)值會被賦值為nil,就會導(dǎo)致crash,我們只需加一個判斷即可??紤]三種情況下面一一注解

1、runtime 字典轉(zhuǎn)模型-->字典的 key 和模型的屬性不匹配「模型屬性數(shù)量大于字典鍵值對數(shù)」,這種情況處理如下:

#import "NSObject+Model.h"
#import <objc/message.h>

@implementation NSObject (Model)

// 思路:利用runtime 遍歷模型中所有屬性,根據(jù)模型中屬性,去字典中取出對應(yīng)的value給模型屬性賦值
+ (instancetype)modelWithDict:(NSDictionary *)dict
{
    // 1.創(chuàng)建對應(yīng)的對象
    id objc = [[self alloc] init];
    
    // 2.利用runtime給對象中的屬性賦值
    /**
      獲取類中的所有成員變量
        class_copyIvarList(Class _Nullable cls:表示獲取哪個類中的成員變量, unsigned int * _Nullable outCount:表示這個類有多少成員變量,傳入一個Int變量地址,會自動給這個變量賦值)
      返回值Ivar * =
        指的是一個ivar數(shù)組,會把所有成員屬性放在一個數(shù)組中,通過返回的數(shù)組就能全部獲取到
     */
    // 成員變量個數(shù)
    unsigned int count = 0;
    // 獲取類中的所有成員變量
    Ivar *ivarList = class_copyIvarList(self, &count);

    // 遍歷所有成員變量
    for (int i = 0; i < count; i++) {
        // 根據(jù)角標(biāo),從數(shù)組取出對應(yīng)的成員變量(Ivar:成員變量,以下劃線開頭)
        Ivar ivar = ivarList[i];
        
        // 獲取成員變量名字
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        
        // 處理成員變量名,字典中的key(去掉 _ ,從第一個角標(biāo)開始截取)
        NSString *key = [ivarName substringFromIndex:1];
        
        // 根據(jù)成員屬性名去字典中查找對應(yīng)的value
        id value = dict[key];
        
        //【如果模型屬性數(shù)量大于字典鍵值對數(shù)理,模型屬性會被賦值為nil】
        // 而報錯 (could not set nil as the value for the key age.)
        if (value) {
            // 給模型中屬性賦值
            [objc setValue:value forKey:key];
        }
    }
    return objc;
}

注解: 這里在獲取模型類中的所有屬性名,是采取 class_copyIvarList 先獲取成員變量(以下劃線開頭) ,然后再處理成員變量名,字典中的key(去掉 _ ,從第一個角標(biāo)開始截取) 得到屬性名。

2、runtime 字典轉(zhuǎn)模型-->模型中嵌套模型「模型屬性是另外一個模型對象」,這種情況處理如下:

// 思路:利用runtime 遍歷模型中所有屬性,根據(jù)模型中屬性,去字典中取出對應(yīng)的value給模型屬性賦值
+ (instancetype)modelWithDict2:(NSDictionary *)dict
{
    // 1.創(chuàng)建對應(yīng)的對象
    id objc = [[self alloc] init];
    
    // 2.利用runtime給對象中的屬性賦值
    // 成員變量個數(shù)
    unsigned int count = 0;
    // 獲取類中的所有成員變量
    Ivar *ivarList = class_copyIvarList(self, &count);
    
    // 遍歷所有成員變量
    for (int i = 0; i < count; i++) {
        // 根據(jù)角標(biāo),從數(shù)組取出對應(yīng)的成員變量(Ivar:成員變量,以下劃線開頭)
        Ivar ivar = ivarList[i];
        
        // 獲取成員變量名字
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        // 獲取成員變量類型
        NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
        
        // 替換: @\"User\" -> User
        ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
        ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
        
        // 處理成員變量名->字典中的key(去掉 _ ,從第一個角標(biāo)開始截取)
        NSString *key = [ivarName substringFromIndex:1];
        
        // 根據(jù)成員屬性名去字典中查找對應(yīng)的value
        id value = dict[key];
        
        // 二級轉(zhuǎn)換:如果字典中還有字典,也需要把對應(yīng)的字典轉(zhuǎn)換成模型
        // 判斷下value是否是字典,并且是自定義對象才需要轉(zhuǎn)換
        if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]) {
            
            // 字典轉(zhuǎn)換成模型 userDict => User模型, 轉(zhuǎn)換成哪個模型
            // 根據(jù)字符串類名生成類對象
            Class modelClass = NSClassFromString(ivarType);
            
            if (modelClass) { // 有對應(yīng)的模型才需要轉(zhuǎn)
                // 把字典轉(zhuǎn)模型
                value = [modelClass modelWithDict2:value];
            }
        }
        
        // 給模型中屬性賦值
        if (value) {
            [objc setValue:value forKey:key];
        }
    }
    return objc;
}

3、runtime 字典轉(zhuǎn)模型-->數(shù)組中裝著模型「模型的屬性是一個數(shù)組,數(shù)組中是字典模型對象」,這種情況處理如下:

// 思路:利用runtime 遍歷模型中所有屬性,根據(jù)模型中屬性,去字典中取出對應(yīng)的value給模型屬性賦值
+ (instancetype)modelWithDict3:(NSDictionary *)dict
{
    // 1.創(chuàng)建對應(yīng)的對象
    id objc = [[self alloc] init];
    
    // 2.利用runtime給對象中的屬性賦值
    // 成員變量個數(shù)
    unsigned int count = 0;
    // 獲取類中的所有成員變量
    Ivar *ivarList = class_copyIvarList(self, &count);
    
    // 遍歷所有成員變量
    for (int i = 0; i < count; i++) {
        // 根據(jù)角標(biāo),從數(shù)組取出對應(yīng)的成員變量(Ivar:成員變量,以下劃線開頭)
        Ivar ivar = ivarList[i];
        
        // 獲取成員變量名字
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        
        // 處理成員屬性名->字典中的key(去掉 _ ,從第一個角標(biāo)開始截取)
        NSString *key = [ivarName substringFromIndex:1];
        
        // 根據(jù)成員屬性名去字典中查找對應(yīng)的value
        id value = dict[key];
        
        
        //--------------------------- <#我是分割線#> ------------------------------//
        //
        
        // 三級轉(zhuǎn)換:NSArray中也是字典,把數(shù)組中的字典轉(zhuǎn)換成模型.
        // 判斷值是否是數(shù)組
        if ([value isKindOfClass:[NSArray class]]) {
            // 判斷對應(yīng)類有沒有實現(xiàn)字典數(shù)組轉(zhuǎn)模型數(shù)組的協(xié)議
            // arrayContainModelClass 提供一個協(xié)議,只要遵守這個協(xié)議的類,都能把數(shù)組中的字典轉(zhuǎn)模型
            if ([self respondsToSelector:@selector(arrayContainModelClass)]) {
                
                // 轉(zhuǎn)換成id類型,就能調(diào)用任何對象的方法
                id idSelf = self;
                
                // 獲取數(shù)組中字典對應(yīng)的模型
                NSString *type =  [idSelf arrayContainModelClass][key];
                
                // 生成模型
                Class classModel = NSClassFromString(type);
                NSMutableArray *arrM = [NSMutableArray array];
                // 遍歷字典數(shù)組,生成模型數(shù)組
                for (NSDictionary *dict in value) {
                    // 字典轉(zhuǎn)模型
                    id model =  [classModel modelWithDict3:dict];
                    [arrM addObject:model];
                }
                
                // 把模型數(shù)組賦值給value
                value = arrM;     
            }
        }
        
        // 如果模型屬性數(shù)量大于字典鍵值對數(shù)理,模型屬性會被賦值為nil,而報錯
        if (value) {
            // 給模型中屬性賦值
            [objc setValue:value forKey:key];
        }
    }
    return objc;
}
4.動態(tài)添加方法

場景:如果一個類方法非常多,加載類到內(nèi)存的時候也比較耗費資源,需要給每個方法生成映射表,可以使用動態(tài)給某個類,添加方法解決。

注解:OC 中我們很習(xí)慣的會用懶加載,當(dāng)用到的時候才去加載它,但是實際上只要一個類實現(xiàn)了某個方法,就會被加載進內(nèi)存。當(dāng)我們不想加載這么多方法的時候,就會使用到 runtime 動態(tài)的添加方法。

需求:runtime 動態(tài)添加方法處理調(diào)用一個未實現(xiàn)的方法 和 去除報錯。

場景代碼:方法+調(diào)用+打印輸出

#import "Person.h"
#import <objc/message.h>

@implementation Person

/**
 調(diào)用:只要一個對象調(diào)用了一個未實現(xiàn)的方法就會調(diào)用這個方法,進行處理
 作用:動態(tài)添加方法,處理未實現(xiàn)
 注解:任何方法默認都有兩個隱式參數(shù),self,_cmd(當(dāng)前方法的方法編號)
 */
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    
    if (sel == NSSelectorFromString(@"roll:")) {
        /**
         class_addMethod(<#Class  _Nullable __unsafe_unretained cls#>:給哪個類添加方法, <#SEL  _Nonnull name#>:添加哪個方法,即添加方法的方法編號, <#IMP  _Nonnull imp#>:方法實現(xiàn) => 函數(shù) => 函數(shù)入口 => 函數(shù)名(添加方法的函數(shù)實現(xiàn)(函數(shù)地址)), <#const char * _Nullable types#>:方法類型,(返回值+參數(shù)類型) v:void @:對象->self :表示SEL->_cmd)
         */
        // 給類添加roll:滾了多遠方法
        class_addMethod(self, sel, (IMP)LNRoll, "v@:@");
        
        return YES;
    }
    
    if ([NSStringFromSelector(sel) isEqualToString:@"go:"]) {
        // 給類添加go:走了多遠方法
        class_addMethod(self, sel, (IMP)LNGO, "v@:@");
        return YES;
    }
    
    return [super resolveInstanceMethod:sel];
}

// 調(diào)用
Person *p = [[Person alloc] init];
// 執(zhí)行某個方法
[p performSelector:@selector(roll:) withObject:@"11"];
[p performSelector:@selector(go:) withObject:@10];

// 打印輸出
2016-03-17 19:05:03.917 runtime[12761:543574] 我滾了 11 米遠
2016-03-17 19:05:04.617 runtime[12761:543574] 我走了 10 公里才到的家 
5.實現(xiàn)NSCoding的歸檔接檔

原理描述:用runtime提供的函數(shù)遍歷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 = 0; i < outCount; i ++) {
            Ivar ivar = ivars[i];
            NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            [self setValue:[aDecoder decodeObjectForKey:key] forKey:key];
        }
        free(ivars);
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    unsigned int outCount;
    Ivar * ivars = class_copyIvarList([self class], &outCount);
    for (int i = 0; i < outCount; i ++) {
        Ivar ivar = ivars[i];
        NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
        [aCoder encodeObject:[self valueForKey:key] forKey:key];
    }
    free(ivars);
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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