探祕Runtime - Runtime介紹

該文章屬於<簡書 — 劉小壯>原創,轉載請註明:

<簡書 — 劉小壯> https://www.jianshu.com/p/ce97c66027cdhtml


RuntimeiOS系統中重要的組成部分,面試也是必問的問題,因此Runtime是一個iOS工程師必須掌握的知識點。git

如今市面上有不少關於Runtime的學習資料,也有很多高質量的,可是大多數質量都不是很高,並且都只介紹某個點,並不全面。github

這段時間正好公司內部組織技術分享,我分享的主題就是Runtime,我把分享的資料發到博客,你們一塊兒學習交流。面試

文章都是個人一些筆記,和平時的技術積累。我的水平有限,文章有什麼問題還請各位大神指導,謝謝!性能優化


博客配圖

描述

**OC語言是一門動態語言,會將程序的一些決定工做從編譯期推遲到運行期。**因爲OC語言運行時的特性,因此其不僅須要依賴編譯器,還須要依賴運行時環境。數據結構

OC語言在編譯期都會被編譯爲C語言的Runtime代碼,二進制執行過程當中執行的都是C語言代碼。而OC的類本質上都是結構體,在編譯時都會以結構體的形式被編譯到二進制中。Runtime是一套由C、C++、彙編實現的API,全部的方法調用都叫作發送消息。併發

根據Apple官方文檔的描述,目前OC運行時分爲兩個版本,ModernLegacy。兩者的區別在於Legacy在實例變量發生改變後,須要從新編譯其子類。Modern在實例變量發生改變後,不須要從新編譯其子類。app

Runtime不僅是一些C語言的API,其由ClassMeta ClassInstance、Class Instance組成,是一套完整的面向對象的數據結構。因此研究Runtime總體的對象模型,比研究API是怎麼實現的更有意義。ide

使用Runtime

Runtime是一個共享動態庫,其目錄位於/usr/include/objc,由一系列的C函數和結構體構成。和Runtime系統發生交互的方式有三種,通常都是用前兩種:函數

  1. 使用OC源碼 直接使用上層OC源碼,底層會經過Runtime爲其提供運行支持,上層不須要關心Runtime運行。
  2. NSObject 在OC代碼中絕大多數的類都是繼承自NSObject的,NSProxy類例外。RuntimeNSObject中定義了一些基礎操做,NSObject的子類也具有這些特性。
  3. Runtime動態庫 上層的OC源碼都是經過Runtime實現的,咱們通常不直接使用Runtime,直接和OC代碼打交道就能夠。

使用Runtime須要引入下面兩個頭文件,一些基礎方法都定義在這兩個文件中。

#import <objc/runtime.h>
#import <objc/message.h>

對象模型

下面圖中表示了對象間isa的關係,以及類的繼承關係。

對象模型

Runtime源碼能夠看出,每一個對象都是一個objc_object的結構體,在結構體中有一個isa指針,該指針指向本身所屬的類,由Runtime負責建立對象。

類被定義爲objc_class結構體,objc_class結構體繼承自objc_object,因此類也是對象。在應用程序中,類對象只會被建立一份。在objc_class結構體中定義了對象的method listprotocolivar list等,表示對象的行爲。

既然類是對象,那類對象也是其餘類的實例。因此Runtime中設計出了meta class,經過meta class來建立類對象,因此類對象的isa指向對應的meta class。而meta class也是一個對象,全部元類的isa都指向其根元類,根原類的isa指針指向本身。經過這種設計,isa的總體結構造成了一個閉環。

// 精簡版定義
typedef struct objc_class *Class;

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
}

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

在對象的繼承體系中,類和元類都有各自的繼承體系,但它們都有共同的根父類NSObject,而NSObject的父類指向nil。須要注意的是,上圖中Root Class(Class)NSObject類對象,而Root Class(Meta)NSObject的元類對象。

基礎定義

objc-private.h文件中,有一些項目中經常使用的基礎定義,這是最新的objc-723中的定義,能夠來看一下。

typedef struct objc_class *Class;
typedef struct objc_object *id;

typedef struct method_t *Method;
typedef struct ivar_t *Ivar;
typedef struct category_t *Category;
typedef struct property_t *objc_property_t;

IMP

RuntimeIMP本質上就是一個函數指針,其定義以下。在IMP中有兩個默認的參數idSELid也就是方法中的self,這和objc_msgSend()函數傳遞的參數同樣。

typedef void (*IMP)(void /* id, SEL, ... */ );

Runtime中提供了不少對於IMP操做的API,下面就是不分IMP相關的函數定義。咱們比較常見的是method_exchangeImplementations函數,Method Swizzling就是經過這個API實現的。

OBJC_EXPORT void
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

OBJC_EXPORT IMP _Nonnull
method_setImplementation(Method _Nonnull m, IMP _Nonnull imp) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

OBJC_EXPORT IMP _Nonnull
method_getImplementation(Method _Nonnull m) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

OBJC_EXPORT IMP _Nullable
class_getMethodImplementation(Class _Nullable cls, SEL _Nonnull name) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
// ....

獲取IMP

經過定義在NSObject中的下面兩個方法,能夠根據傳入的SEL獲取到對應的IMPmethodForSelector:方法不僅實例對象能夠調用,類對象也能夠調用。

- (IMP)methodForSelector:(SEL)aSelector;
+ (IMP)instanceMethodForSelector:(SEL)aSelector;

例以下面建立C函數指針用來接收IMP,獲取到IMP後能夠手動調用IMP,在定義的C函數中須要加上兩個隱藏參數。

void (*function) (id self, SEL _cmd, NSObject object);

function = (id self, SEL _cmd, NSObject object)[self methodForSelector:@selector(object:)];

function(instance, @selector(object:), [NSObject new]);

性能優化

經過這些API能夠進行一些優化操做。若是遇到大量的方法執行,能夠經過Runtime獲取到IMP,直接調用IMP實現優化。

TestObject *object = [[TestObject alloc] init];
void(*function)(id, SEL) = (void(*)(id, SEL))class_getMethodImplementation([TestObject class], @selector(testMethod));
function(object, @selector(testMethod));

在獲取和調用IMP的時候須要注意,每一個方法默認都有兩個隱藏參數,因此在函數聲明的時候須要加上這兩個隱藏參數,調用的時候也須要把相應的對象和SEL傳進去,不然可能會致使Crash

IMP for block

Runtime還支持block方式的回調,咱們能夠經過RuntimeAPI,將原來的方法回調改成block的回調。

// 類定義
@interface TestObject : NSObject
- (void)testMethod:(NSString *)text;
@end

// 類實現
@implementation TestObject
- (void)testMethod:(NSString *)text {
    NSLog(@"testMethod : %@", text);
}
@end

// runtime
IMP function = imp_implementationWithBlock(^(id self, NSString *text) {
    NSLog(@"callback block : %@", text);
});
const char *types = sel_getName(@selector(testMethod:));
class_replaceMethod([TestObject class], @selector(testMethod:), function, types);
    
TestObject *object = [[TestObject alloc] init];
[object testMethod:@"lxz"];

// 輸出
callback block : lxz

Method

Method用來表示方法,其包含SELIMP,下面能夠看一下Method結構體的定義。

typedef struct method_t *Method;

struct method_t {
    SEL name;
    const char *types;
    IMP imp;
};

在運行過程當中是這樣。

Method

Xcode進行編譯的時候,只會將XcodeCompile Sources.m聲明的方法編譯到Method List,而.h文件中聲明的方法對Method List沒有影響。

Property

Runtime中定義了屬性的結構體,用來表示對象中定義的屬性。@property修飾符用來修飾屬性,修飾後的屬性爲objc_property_t類型,其本質是property_t結構體。其結構體定義以下。

typedef struct property_t *objc_property_t;

struct property_t {
    const char *name;
    const char *attributes;
};

能夠經過下面兩個函數,分別獲取實例對象的屬性列表,和協議的屬性列表。

objc_property_t * class_copyPropertyList(Class cls,unsigned int * outCount)
objc_property_t * protocol_copyPropertyList(Protocol * proto,unsigned int * outCount)

能夠經過下面兩個方法,傳入指定的ClasspropertyName,獲取對應的objc_property_t屬性結構體。

objc_property_t class_getProperty(Class cls,const char * name)
objc_property_t protocol_getProperty(Protocol * proto,const char * name,BOOL isRequiredProperty,BOOL isInstanceProperty)

分析實例變量

對象間關係

在OC中絕大多數類都是繼承自NSObject的(NSProxy例外),類與類之間都會存在繼承關係。經過子類建立對象時,繼承鏈中全部成員變量都會存在對象中。

例以下圖中,父類是UIViewController,具備一個view屬性。子類UserCenterViewController繼承自UIViewController,並定義了兩個新屬性。這時若是經過子類建立對象,就會同時包含着三個實例變量。

對象間關係

可是類的結構在編譯時都是固定的,若是想要修改類的結構須要從新編譯。若是上線後用戶安裝到設備上,新版本的iOS系統中更新了父類的結構,也就是UIViewController的結構,爲其加入了新的實例變量,這時用戶更新新的iOS系統後就會致使問題。

對象間關係

原來UIViewController的結構中增長了childViewControllers屬性,這時候和子類的內存偏移就發生衝突了。只不過,Runtime有檢測內存衝突的機制,在類生成實例變量時,會判斷實例變量是否有地址衝突,若是發生衝突則調整對象的地址偏移,這樣就在運行時解決了地址衝突的問題。

內存佈局

類的本質是結構體,在結構體中包含一些成員變量,例如method listivar list等,這些都是結構體的一部分。method、protocolproperty的實現這些均可以放到類中,全部對象調用同一份便可,但對象的成員變量不能夠放在一塊兒,由於每一個對象的成員變量值都是不一樣的。

**建立實例對象時,會根據其對應的Class分配內存,內存構成是ivars+isa_t。**而且實例變量不僅包含當前Classivars,也會包含其繼承鏈中的ivarsivars的內存佈局在編譯時就已經決定,運行時須要根據ivars內存佈局建立對象,因此Runtime不能動態修改ivars,會破壞已有內存佈局。

內存佈局

(上圖中,「x」表示地址對其後的空位)

以上圖爲例,建立的對象中包含所屬類及其繼承者鏈中,全部的成員變量。由於對象是結構體,因此須要進行地址對其,通常OC對象的大小都是8的倍數。

**也不是全部對象都不能動態修改ivars,若是是經過runtime動態建立的類,是能夠修改ivars的。**這個在後面會有講到。

ivar讀寫

實例變量的isa_t指針會指向其所屬的類,對象中並不會包含methodprotocolpropertyivar等信息,這些信息在編譯時都保存在只讀結構體class_ro_t中。在class_ro_tivarsconst只讀的,在image loadcopyclass_rw_t中時,是不會copy ivars的,而且class_rw_t中並無定義ivars的字段。

在訪問某個成員變量時,直接經過isa_t找到對應的objc_class,並經過其class_ro_tivar list作地址偏移,查找對應的對象內存。正是因爲這種方式,因此對象的內存地址是固定不可改變的。

方法傳參

當調用實例變量的方法時,會經過objc_msgSend()發起調用,調用時會傳入selfSEL。函數內部經過isa在類的內部查找方法列表對應的IMP,傳入對應的參數併發起調用。若是調用的方法時涉及到當前對象的成員變量的訪問,這時候就是在objc_msgSend()內部,經過類的ivar list判斷地址偏移,取出ivar並傳入調用的IMP中的。

調用super的方式時則調用objc_msgSendSuper()函數實現,調用時將實例變量的父類傳進去。可是須要注意的是,調用objc_msgSendSuper函數時傳入的對象,也是當前實例變量,因此是在向本身發送父類的消息。具體能夠看一下[self class][super class]的結果,結果應該都是同樣的。

在項目中常常會經過[super xxx]的方式調用父類方法,這是由於須要先完成父類的操做,固然也能夠不調用,視狀況而定。以常常見到的自定義init方法中,常常會出現if (self = [super init])的調用,這是在完成本身的初始化以前先對父類進行初始化,不然只初始化自身可能會存在問題。在調用[super init]時若是返回nil,則表示父類初始化失敗,這時候初始化子類確定會出現問題,因此須要作判斷。

參考資料

Apple Runtime Program Guild 維基百科-Objective-C 維基百科-Clang 維基百科-GCC(GNU)

蘋果開源代碼不建議去Github,上面的版本通常更新不及時,建議去蘋果的開源官網。 Apple Opensource

簡書因爲排版的問題,閱讀體驗並很差,佈局、圖片顯示、代碼等不少問題。因此建議到我Github上,下載Runtime PDF合集。把全部Runtime文章總計九篇,都寫在這個PDF中,並且左側有目錄,方便閱讀。

Runtime PDF

下載地址:Runtime PDF 麻煩各位大佬點個贊,謝謝!

相關文章
相關標籤/搜索