iOS開發小記-Runtime篇

如今看起來Runtime篇整理的少了,有時間再完善下,將就着看吧數組

什麼是Runtime?


Objective-C將不少靜態語言在編譯和連接時期作的工做放在了Runtime運行時處理,能夠說Runtime就是Objective-C的幕後工做者。bash

  1. Runtime(簡稱運行時),是一套由純C寫的API。
  2. 對於C語言,函數的調用會在編譯的時候決定調用哪一個函數。
  3. OC中的函數調用成爲 消息發送 ,屬於動態調用過程。在編譯的時候並不能真正決定調用那個函數,只有真正運行的時候纔會根據函數名稱找到對應的函數來調用。
  4. 事實證實:在編譯階段,OC能夠調用任意函數,即便這個函數並未實現,只有聲明過就不會報錯,只有運行時纔會報錯,這是由於OC是動態調用的。而C語言調用未實現的函數就會報錯。

消息機制


任何方法調用的本質,就是發送了一個消息(用Runtime發送消息,OC底層實現經過Runtime實現)。併發

  • 原理

對象根據方法編號SEL去隱射表查找對應的方法實現。函數

  • 方法調用流程
  1. OC在向一個對象發送消息時,Runtime會根據該對象的isa指針找到該對象對應的類或者父類。
  2. 根據編號SEL在Method_List中查找對應方法。
  3. 若是找到最終函數實現地址,根據地址去方法區調用對應函數。若是沒找到,會有三次拯救機會,不然拋出異常。
  4. Method resolution:objc運行時會調用+resolveInstanceMethod:或者 +resolveClassMethod:,讓你有機會提供一個函數實現。若是你添加了函數,那運行時系統就會從新啓動一次消息發送的過程,不然 ,運行時就會移到下一步,消息轉發(Message Forwarding)。
  5. Fast forwarding:若是目標對象實現了-forwardingTargetForSelector:,Runtime 這時就會調用這個方法,給你把這個消息轉發給其餘對象的機會。 只要這個方法返回的不是nil和self,整個消息發送的過程就會被重啓,固然發送的對象會變成你返回的那個對象。不然,就會繼續Normal Fowarding。 這裏叫Fast,只是爲了區別下一步的轉發機制。由於這一步不會建立任何新的對象,但下一步轉發會建立一個NSInvocation對象,因此相對更快點。
  6. Normal forwarding:這一步是Runtime最後一次給你挽救的機會。首先它會發送-methodSignatureForSelector:消息得到函數的參數和返回值類型。若是-methodSignatureForSelector:返回nil,Runtime則會發出-doesNotRecognizeSelector:消息,程序這時也就掛掉了。若是返回了一個函數簽名,Runtime就會建立一個NSInvocation對象併發送-forwardInvocation:消息給目標對象。 PS:對象方法保存在類對象的方法列表中,類方法保存在元類的方法列表中。

經常使用場景


  • 交換方法

有時候咱們須要對類的方法進行修改,可是又沒法拿到源碼,咱們即可以經過Runtime來交換方法實現。ui

+ (void)load {
    //獲取實例方法實現
    Method method1 = class_getInstanceMethod(self, @selector(show));
    Method method2 = class_getInstanceMethod(self, @selector(ln_show));
    //獲取類方法實現
//    Method method3 = class_getClassMethod(self, @selector(show));
//    Method method4 = class_getClassMethod(self, @selector(ln_show));

    //交換兩個方法的實現
    method_exchangeImplementations(method1, method2);
    //將method1的實現換成method2
//    method_setImplementation(method1, method_getImplementation(method2));
}

- (void)show {
    NSLog(@"show person");
}

- (void)ln_show {
    NSLog(@"show person exchange");
}
複製代碼
  • 添加屬性

實際上並無產生真正的成員變量,經過關聯對象來實現,具體參考分類。spa

  • 字典轉模型

除了可使用KVC實現外,還能夠經過Runtime實現,就是取出全部ivars遍歷賦值。但實際狀況通常比較複雜:指針

  1. 當字典的key和模型的屬性匹配不上。
  2. 模型中嵌套模型(模型屬性是另一個模型對象)。
  3. 數組中裝着模型(模型的屬性是一個數組,數組中是一個個模型對象)。 咱們這裏僅考慮最簡單的狀況
+ (instancetype)modelWithDic:(NSDictionary *)dic {
    /*
     1.初始化實例對象
     */
    id object = [[self alloc] init];
    
    /**
     2.獲取ivars
     class_copyIvarList: 獲取類中的全部成員變量
     Ivar:成員變量
     第一個參數:表示獲取哪一個類中的成員變量
     第二個參數:表示這個類有多少成員變量,傳入一個Int變量地址,會自動給這個變量賦值
     返回值Ivar *:指的是一個ivar數組,會把全部成員屬性放在一個數組中,經過返回的數組就能所有獲取到。
     count: 成員變量個數
     */
    unsigned int count = 0;
    Ivar *ivarList = class_copyIvarList(self, &count);
    
    /*
     3.遍歷賦值
     */
    for(int i = 0; i < count; i++) {
        //獲取ivar屬性
        Ivar ivar = ivarList[i];
        //獲取屬性名
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        //去掉成員變量的下劃線
        NSString *key = [ivarName substringFromIndex:1];
        //獲取dic中對應值
        id value = dic[ivarName];
        //若是值存在,則賦值
        if(value) {
            [object setValue:value forKey:ivarName];
        }
    }
    
    return object;
}
複製代碼
  • 動態添加方法

若是一個類方法很是多,加載類到內存的時候也比較耗費資源,須要給每一個方法生成映射表,可使用動態給某個類,添加方法解決。code

- (void)viewDidLoad {
    [super viewDidLoad];   
    Person *p = [[Person alloc] init];
    // 默認person,沒有實現run:方法,能夠經過performSelector調用,可是會報錯。
    // 動態添加方法就不會報錯
    [p performSelector:@selector(run:) withObject:@10];
}

@implementation Person
// 沒有返回值,1個參數
// void,(id,SEL)
void aaa(id self, SEL _cmd, NSNumber *meter) {
    NSLog(@"跑了%@米", meter);
}

// 任何方法默認都有兩個隱式參數,self,_cmd(當前方法的方法編號)
// 何時調用:只要一個對象調用了一個未實現的方法就會調用這個方法,進行處理
// 做用:動態添加方法,處理未實現
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    // [NSStringFromSelector(sel) isEqualToString:@"run"];
    if (sel == NSSelectorFromString(@"run:")) {
        // 動態添加run方法
        // class: 給哪一個類添加方法
        // SEL: 添加哪一個方法,即添加方法的方法編號
        // IMP: 方法實現 => 函數 => 函數入口 => 函數名(添加方法的函數實現(函數地址))
        // type: 方法類型,(返回值+參數類型) v:void @:對象->self :表示SEL->_cmd
        class_addMethod(self, sel, (IMP)aaa, "v@:@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
@end
複製代碼
  • NSCoding的自動歸檔和解檔

在實現encodeObjectdecodeObjectForKey方法中,咱們通常須要把每一個屬性都要寫一遍,這樣很麻煩,咱們能夠經過Runtime來自動化。orm

- (void)encodeWithCoder:(NSCoder *)aCoder {
    unsigned int count = 0;
    Ivar *ivarList = class_copyIvarList([self class], &count);
    
    for(int i = 0; i < count; i++) {
        Ivar ivar = ivarList[i];
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        id value = [self valueForKey:ivarName];
        [aCoder encodeObject:value forKey:ivarName];
    }
    free(ivarList);
}

- (id)initWithCoder:(NSCoder *)aDecoder {
    if(self == [super init]) {
        unsigned int count = 0;
        Ivar *ivarList = class_copyIvarList([self class], &count);
        
        for(int i = 0; i < count; i++) {
            Ivar ivar = ivarList[i];
            NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
            id value = [aDecoder decodeObjectForKey:ivarName];
            [self setValue:value forKey:ivarName];
        }
        free(ivarList);
    }
    return self;
}
複製代碼

還有更簡便的方法,抽象成宏,參考網上資料。對象

  • 經常使用API
unsigned int count = 0;
    //獲取屬性列表
    Ivar *propertyList = class_copyPropertyList([self class], &count);
    
    //獲取方法列表
    Method *methodList = class_copyMethodList([self class], &count);
    
    //獲取成員變量列表
    Ivar *ivarList = class_copyIvarList([self class], &count);
    
    //獲取協議列表
    __unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);

    //獲取類方法
    Method method1 = class_getClassMethod([self class], @selector(run));
    
    //獲取實例方法
    Method method2 = class_getInstanceMethod([self class], @selector(tempRun));
    
    //添加方法
    class_addMethod([self class], @selector(run), method_getImplementation(method2), method_getTypeEncoding(method2));
    
    //替換方法
    class_replaceMethod;
    
    //交換方法
    method_exchangeImplementations;
複製代碼

load與initialize


  • load

當類被引進項目的時候會執行load函數(在main函數開始以前),與這個類是會被用到無關,每一個類的load函數只會被調用一次。因爲load函數是自動加載的,不須要調用父類的load函數。

  1. 當父類和子類都實現了load函數時,先調用父類再調用子類。
  2. 當子類未實現load方法時,不會調用父類的load方法。
  3. 類中的load執行順序要優於分類。
  4. 多個類別都有load方法時,其執行順序與分類中其餘相同方法同樣,根據編譯順序決定。
  • initialize

這個方法會在類接收到第一次消息時調用。因爲是系統調用,也不須要調用父類方法。

  1. 父類的initialize方法比子類優先。
  2. 當子類未實現initialize方法,會調用父類initialize方法;子類實現initialize方法時,會重載initialize方法。
  3. 當多個分類都實現了initialize方法,會執行最後一個編譯的分類中的initialize方法。
相關文章
相關標籤/搜索