1. 程式人生 > IOS開發 >iOS開發小記-Runtime篇

iOS開發小記-Runtime篇

現在看起來Runtime篇整理的少了,有時間再完善下,將就著看吧

什麼是Runtime?


Objective-C將很多靜態語言在編譯和連結時期做的工作放在了Runtime執行時處理,可以說Runtime就是Objective-C的幕後工作者。

  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來交換方法實現。

+ (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");
}
複製程式碼
  • 新增屬性

實際上並沒有產生真正的成員變數,通過關聯物件來實現,具體參考分類。

  • 字典轉模型

除了可以使用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;
}
複製程式碼
  • 動態新增方法

如果一個類方法非常多,載入類到記憶體的時候也比較耗費資源,需要給每個方法生成對映表,可以使用動態給某個類,新增方法解決。

- (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來自動化。


- (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方法。