該文章屬于劉小壯原創,轉載請注明:劉小壯
Runtime
是iOS
系統中重要的組成部分,面試也是必問的問題,所以Runtime
是一個iOS
工程師必須掌握的知識點。
現在市面上有很多關于Runtime
的學習資料,也有不少高質量的,但是大多數質量都不是很高,而且都只介紹某個點,并不全面。
這段時間正好公司內部組織技術分享,我分享的主題就是Runtime
,我把分享的資料發到博客,大家一起學習交流。
文章都是我的一些筆記,和平時的技術積累。個人水平有限,文章有什么問題還請各位大神指導,謝謝!??
描述
OC
語言是一門動態語言,會將程序的一些決定工作從編譯期推遲到運行期。由于OC
語言運行時的特性,所以其不只需要依賴編譯器,還需要依賴運行時環境。
OC
語言在編譯期都會被編譯為C
語言的Runtime
代碼,二進制執行過程中執行的都是C
語言代碼。而OC
的類本質上都是結構體,在編譯時都會以結構體的形式被編譯到二進制中。Runtime
是一套由C
、C++
、匯編實現的API
,所有的方法調用都叫做發送消息。
根據Apple
官方文檔的描述,目前OC運行時分為兩個版本,Modern
和Legacy
。二者的區別在于Legacy
在實例變量發生改變后,需要重新編譯其子類。Modern
在實例變量發生改變后,不需要重新編譯其子類。
Runtime
不只是一些C語言的API,其由Class
、Meta Class
、Instance、Class Instance
組成,是一套完整的面向對象的數據結構。所以研究Runtime整體的對象模型,比研究API是怎么實現的更有意義。
使用Runtime
Runtime
是一個共享動態庫,其目錄位于/usr/include/objc
,由一系列的C函數和結構體構成。和Runtime
系統發生交互的方式有三種,一般都是用前兩種:
- 使用
OC
源碼
直接使用上層OC
源碼,底層會通過Runtime
為其提供運行支持,上層不需要關心Runtime
運行。 -
NSObject
在OC代碼中絕大多數的類都是繼承自NSObject
的,NSProxy
類例外。Runtime
在NSObject
中定義了一些基礎操作,NSObject
的子類也具備這些特性。 -
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 list
、protocol
、ivar 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
在Runtime
中IMP
本質上就是一個函數指針,其定義如下。在IMP
中有兩個默認的參數id
和SEL
,id
也就是方法中的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
獲取到對應的IMP
。methodForSelector:
方法不只實例對象可以調用,類對象也可以調用。
- (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
方式的回調,我們可以通過Runtime
的API
,將原來的方法回調改為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
用來表示方法,其包含SEL
和IMP
,下面可以看一下Method
結構體的定義。
typedef struct method_t *Method;
struct method_t {
SEL name;
const char *types;
IMP imp;
};
在運行過程中是這樣。
在Xcode
進行編譯的時候,只會將Xcode
的Compile 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)
可以通過下面兩個方法,傳入指定的Class
和propertyName
,獲取對應的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 list
、ivar list
等,這些都是結構體的一部分。method、protocol
、property
的實現這些都可以放到類中,所有對象調用同一份即可,但對象的成員變量不可以放在一起,因為每個對象的成員變量值都是不同的。
創建實例對象時,會根據其對應的Class
分配內存,內存構成是ivars
+isa_t
。并且實例變量不只包含當前Class
的ivars
,也會包含其繼承鏈中的ivars
。ivars
的內存布局在編譯時就已經決定,運行時需要根據ivars
內存布局創建對象,所以Runtime
不能動態修改ivars
,會破壞已有內存布局。
(上圖中,x
表示地址對其后的空位)
以上圖為例,創建的對象中包含所屬類及其繼承者鏈中,所有的成員變量。因為對象是結構體,所以需要進行地址對其,一般OC
對象的大小都是8的倍數。
也不是所有對象都不能動態修改ivars
,如果是通過runtime
動態創建的類,是可以修改ivars
的。這個在后面會有講到。
ivar讀寫
實例變量的isa_t
指針會指向其所屬的類,對象中并不會包含method
、protocol
、property
、ivar
等信息,這些信息在編譯時都保存在只讀結構體class_ro_t
中。在class_ro_t
中ivars
是const
只讀的,在image load
時copy
到class_rw_t
中時,是不會copy ivars
的,并且class_rw_t
中并沒有定義ivars
的字段。
在訪問某個成員變量時,直接通過isa_t
找到對應的objc_class
,并通過其class_ro_t
的ivar list
做地址偏移,查找對應的對象內存。正是由于這種方式,所以對象的內存地址是固定不可改變的。
方法傳參
當調用實例變量的方法時,會通過objc_msgSend()
發起調用,調用時會傳入self
和SEL
。函數內部通過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
麻煩各位大佬點個贊,謝謝!??