Runtime介紹
Objective-C 是一個動態語言,這意味著它不僅需要一個編譯器,也需要一個執行時系統來動態得建立類和物件、進行訊息傳遞和轉發。 Runtime就是這個執行時系統。
Runtime 基本是用C和彙編寫的,OC並不能直接編譯為組合語言,而是要先轉寫為純C語言再進行編譯和彙編的操作,從OC到C語言的過渡就是由runtime來實現的。
你可以在 這裡下到蘋果維護的開原始碼。蘋果和GNU各自維護一個開源的 runtime 版本,這兩個版本之間都在努力的保持一致。
平時的業務中主要是使用官方Api,解決我們框架性的需求。
Runtime訊息傳遞
呼叫一個物件的方法: [obj foo]
編譯器轉成訊息傳送:objc_msgSend(obj, foo)
一個簡單的demo,在main.m檔案中
Person * person = [[Person alloc]init];
[person run];
複製程式碼
clang命令編譯 clang -rewrite-objc main.m
開啟編譯後的main.cpp檔案,一直拉到最後可以看見我們剛剛寫的兩行程式碼的編譯結果
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
Person * person = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("run"));
}
return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
複製程式碼
可以清晰的看到[person run] 被編譯成了objc_msgSend(person,run),我們常說在OC中呼叫一個物件,就是像一個物件傳送一個方法指令。
要了解objc_msgSend訊息傳遞的原理,先來了解幾個概念:
1、例項(objc_object)
在objc.h中
typedef struct objc_object *id; // 指向 objc_object 結構體的指標
複製程式碼
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY; // isa指標 - 指向類物件:類物件中儲存了建立一個例項的資訊
};
複製程式碼
2、類物件(objc_class)
objc.h 中 calss 的定義
typedef struct objc_class *Class; // 類物件是一個指向 objc_class 結構體的指標
複製程式碼
runtime.h 中 objc_class 結構體的定義
struct objc_class {
// isa指標 - 指向元類:元類儲存了建立類物件以及類方法的所有資訊
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
// 父類指標
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
// 變數列表
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
// 方法列表
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
// 快取
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
// 協議列表
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
複製程式碼
3、元類(Meta Class)
所有的類自身也是一個物件,我們可以向這個物件傳送訊息(即呼叫類方法)。 為了呼叫類方法,這個類的isa指標必須指向一個包含這些類方法的一個objc_class結構體。這就引出了meta-class的概念。
例項中的isa指標指向類物件,類中儲存了建立一個例項物件及例項方法所需的所有資訊,類物件的isa指標指向元類,元類中儲存了建立類物件以及類方法所需的所有資訊。
基類的meta-class的isa指標是指向它自己
通過上圖我們可以看出整個體系構成了一個自閉環
4、Method方法(objc_method)
runtime.h 檔案中
typedef struct objc_method *Method; // 指向 objc_method 結構體的指標
複製程式碼
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE; // 方法名
char * _Nullable method_types OBJC2_UNAVAILABLE; // 方法型別
IMP _Nonnull method_imp OBJC2_UNAVAILABLE; // 方法實習
}
複製程式碼
5、SEL方法(objc_selector)
objc.h 中
typedef struct objc_selector *SEL;
複製程式碼
@property SEL selector;
SEL 是 selector 的表示型別,
selector是方法選擇器,是區分方法的ID,這個ID的資料結構是 objc_selector 結構體
複製程式碼
- 原始碼中沒有objc_selector結構體的具體定義
- 其實就是個對映到方法的C字串,命名規則是 className+methodName
- 導致不能像C語言一樣寫過載函式,就是函式名相同,引數不同。因為select只記了方法名沒有引數,所以沒有辦法區分不同引數的方法。
6、 IMP 指標 - 指向最終實現程式的記憶體地址
objc.h 中
/// A pointer to the function of a method implementation.
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
#endif
複製程式碼
Method通過 selector 和 IMP 兩個屬性,實現了快速查詢方法及實現
7、 類快取(objc_cache)
基於理論:如果你在類上呼叫一個訊息,你可能以後會再次呼叫該訊息。
為了加快訊息分發,系統會對方法和對應的地址進行快取,放在objc_cache中,大部分常用的方法都是會被快取起來的,Runtime系統實際上非常快,接近直接執行記憶體地址的程式速度。
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method _Nullable buckets[1] OBJC2_UNAVAILABLE;
};
複製程式碼
mask 可以理解為當前能達到的最大的index
occupied 被佔用的槽位
buckets 用陣列表示的Hash表
runtime會根據這三項找到快取的位置、經過一些計算在 bukets陣列中找到buket、每一個bucket包含一個selector和一個IMP,通過對比selector來判斷是否有快取
在回過頭來看objc_msgSend 的執行流程:
首先,判斷接收物件 person 是否為nil;
根據person物件的isa指標找到它的class類;
從類的快取中找run,找到則分發
如果快取中沒有,在 class 類的 method list 找 run ;
如果 class 中沒到 run,繼續往它的 superClass 中找 ;直到基類;
都沒有找到,報錯,丟擲異常;
逐行剖析objc_msgSend彙編原始碼文章對objc_msgSend的彙編指令進行分析,快取詳細的分析了是怎麼在方法中找到快取。
訊息轉發
如果在一個物件的類和父類基類中都沒有找到要執行的方法,程式會crash;控制檯會顯示類似錯誤資訊:unrecognized selector,訊息被髮送給了不能處理它的物件。
OC是一門動態語言,我們可以在執行期做一些事來讓crash不發生,訊息轉發機制就是用來解決這個問題的,在執行期通過3分 【接盤俠】方法,給物件和訊息更多的機會來完成成功的呼叫,而不是直接 crash。
在一個函式找不到時,OC提供了三種方式去補救:
一號接盤俠:
動態解析階段:執行期新增方法
+(BOOL)resolveInstanceMethod:(SEL)sel (例項方法呼叫)
+(BOOL)resolveClassMethod:(SEL)sel (類方法呼叫)
複製程式碼
通過class_addMethod動態新增一個方法
二號接盤俠:
備援接受者:轉發給另1個物件、改變方法時
-(id)forwardingTargetForSelector:(SEL)aSelector
複製程式碼
詢問是否把訊息轉發給其他接受者處理
三號接盤俠:
完整訊息轉發:需要轉發給多個物件時
-(void)forwardInvocation:(NSInvocation *)anInvocation
複製程式碼
如果都不中,呼叫doesNotRecognizeSelector丟擲異常。
Runtime的應用
1. 方法交換
使用 method_exchangeImplementations
Method m1 = class_getClassMethod([M1 class], @selector(method1name));
Method m2 = class_getClassMethod([M2 class], @selector(method2name));
method_exchangeImplementations(m1, m2);
複製程式碼
runtime的原始碼,在runtime.h中方法的宣告
OBJC_EXPORT void
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
複製程式碼
objc-runtime-new.mm 檔案中方法的實現,可以看到核心的程式碼實現,是交換了方法 m1 和 m2 的imp指標,所以當我們呼叫方法 m1 時,實際呼叫的是 m2 的imp,也就實現了方法的交換。這也能體現 OC 執行時語言的特點。
void method_exchangeImplementations(Method m1, Method m2)
{
if (!m1 || !m2) return;
rwlock_writer_t lock(runtimeLock);
IMP m1_imp = m1->imp;
m1->imp = m2->imp;
m2->imp = m1_imp;
// RR/AWZ updates are slow because class is unknown
// Cache updates are slow because class is unknown
// fixme build list of classes whose Methods are known externally?
flushCaches(nil);
updateCustomRR_AWZ(nil, m1);
updateCustomRR_AWZ(nil, m2);
}
複製程式碼
2. 為category新增屬性
我們都知道category中不能新增屬性,準確的說是隻能宣告屬性,而不能為我們增加屬性的實現。category的實現原理,以及為什麼不能新增屬性,請移步這裡有詳細的介紹。
3. 其他
1、動態的新增一個類
KVO的實現就是利用runtime動態的新增類,系統是在程式執行的時候根據你要監聽的類,動態新增一個新類繼承自該類,然後重寫原類的setter方法並在裡面通知observer的;
2、通過 Runtime 獲取一個類的所有屬性
YYModel 等資料解析的框架都有用到,獲取類的多有屬性,屬性名稱,屬性型別,利用遞迴的方式和 json 資料一一賦值;
3、動態變數控制,動態增加方法
4、自動歸檔和解檔
5、外掛開發
XCode官方不支援外掛開發,通過標頭檔案方法名猜測方法的作用,swizzle 這些方法,插入自己的程式碼實現外掛邏輯。
6、JSPatch 熱更新,其根本原理都是利用OC的動態語言特性去動態修改類的方法實現。
小結
runtime的應用還有很多,沒一個點深入研究都是一個 topc,大家有興趣和時間的時候可以逐一去研究其中的原理和實現。總之,runtime 的應用,就是利用 OC 動態語言的特性,在執行時做一些 ‘手腳’,去完成一些功能。