iOS開發小記Runtime篇

NO IMAGE

現在看起來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, "[email protected]:@");
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方法。

相關文章

angular髒檢查原理及偽代碼實現

區塊鏈不談技術的都是韭菜——區塊鏈技術組成及架構

iOS開發小記網絡篇(持續更新)

iOS開發小記RunLoop篇