- Runtime 簡介
- Runtime 訊息機制和相關函式
- Runtime 三次轉發流程
- Runtime 應用
- Runtime 面試題
1. Runtime 簡介
Objective-C
是一個動態語言,這意味著它不僅需要一個編譯器,也需要一個執行時系統來動態得建立類和物件、進行訊息傳遞和轉發。Runtime
是 Objective-C
物件導向和動態機制的基石,可以從系統層面解決一些設計或技術問題。它基本是用 C
和彙編寫的,屬於1個 C
語言庫,包含了很多底層的 C
語言 API
,如跟類、成員變數、方法相關的API。它的核心是 - 訊息傳遞 ( Messaging
)。
- 動態繫結(在執行時確定要呼叫的方法)
動態繫結將呼叫方法的確定推遲到執行時。在編譯時,方法的呼叫並不和程式碼繫結在一起,只有在消實傳送出來之後,才確定被呼叫的程式碼。通過動態型別和動態繫結技術,您的程式碼每次執行都可以得到不同的結果。執行時因此負責確定訊息的接收者和被呼叫的方法。
執行時的訊息分發機制為動態繫結提供支援。當您向一個動態型別確定了的物件傳送訊息時,執行環境系統會通過接收者的isa
指標定位物件的類,並以此為起點確定被呼叫的方法,方法和訊息是動態繫結的。 - 與
Runtime
互動
Objective-C
從三種不同的層級上與Runtime
系統進行互動:Objective-C
原始碼Foundation
框架的NSObject
類定義的方法- 對
runtime
函式的直接呼叫
NSProxy
Cocoa
中大多數類都繼承於NSObject
類,也就自然繼承了它的方法。最特殊的例外是NSProxy
,它是個抽象超類,它實現了一些訊息轉發有關的方法,可以通過繼承它來實現一個其他類的替身類或是虛擬出一個不存在的類。
2. Runtime 訊息機制和相關函式
- Runtime 詳細訊息傳送步驟:
- 檢測這個
selector
是不是要忽略的。比如 Mac OS X 開發,有了垃圾回收就不理會retain
,release
這些函式了。 - 檢測這個
target
是不是nil
物件。Objective-C
的特性是允許對一個nil
物件執行任何一個方法不會Crash
,因為會被忽略掉。 - 如果上面兩個都過了,那就開始查詢這個類的
IMP
,先從cache
裡面找,完了找得到就跳到對應的函式去執行。 - 如果
cache
找不到就找一下方法分發表。 - 如果分發表找不到就到超類的分發表去找,一直找,直到找到
NSObject
類為止。 - 如果還找不到就要開始進入動態方法解析了。
- 如果還是找不到並且訊息轉發都失敗了就回執行
doesNotRecognizeSelector:
方法報unrecognized selector
錯。
- 檢測這個
- 舉例:
一個物件的方法像這樣[obj eat]
,編譯器轉成訊息傳送objc_msgSend(obj, eat)
,Runtime
時執行的流程是這樣的:- 通過
obj
的isa
指標找到它的class
- 在
class
的method list
找eat
- 如果
class
中沒找到eat
,繼續往它的superclass
中找,一旦找到eat
這個函式,就去執行它的實現IMP
- 通過
- 標頭檔案
<objc/runtime.h>
<objc/message.h>
- 訊息傳遞用到的一些概念:
例項objc_object
類物件objc_class
元類Meta Class
Methodobjc_method
SELobjc_selector
類快取objc_cache
Categoryobjc_category
IMP
objc_msg
id objc_msgSend ( id self, SEL op, ... );
複製程式碼
- id
objc_msgSend
第一個引數型別為id,是一個指向類例項的指標typedef struct objc_object *id; 複製程式碼
- SEL(
objc_selector
)
objc_msgSend
第二個引數型別為SEL,它是selector
在Objective-C
中的表示型別(Swift
中是Selector
類)。selector
是方法選擇器,可以理解為區分方法的ID
,而這個ID
的資料結構是SEL
。可以看到selector
是SEL
的一個例項typedef struct objc_selector *SEL; 複製程式碼
其實它就是個對映到方法的C字串,你可以用@property SEL selector; 複製程式碼
Objc
編譯器命令@selector()
或者Runtime
系統的sel_registerName
函式來獲得一個SEL
型別的方法選擇器。
注意:寫C
程式碼的時候,經常會用到函式過載,就是函式名相同,引數不同,但是這在Objc
中是行不通的,因為selector
只記了method
的name
,沒有引數,所以沒法區分不同的method
。 - 舉例
OC:[[Person alloc] init]
Runtime:objc_msgSend(objc_msgSend("Person" , "alloc"), "init")
例項(objc_object)
objc_msgSend
第一個引數型別為id
指向類例項的指標,即objc_object
objc_object
結構體包含一個 isa
指標,型別為 isa_t
聯合體。根據 isa
指向物件所屬的類。isa
這裡還涉及到 tagged pointer 等概念。因為 isa_t
使用 union
實現,所以可能表示多種形態,既可以當成是指標,也可以儲存標誌位置。
struct objc_object {
private:
isa_t isa;
public:
// ISA() assumes this is NOT a tagged pointer object
Class ISA();
// getIsa() allows this to be a tagged pointer object
Class getIsa();
... 此處省略其他方法宣告
}
複製程式碼
注意: isa
指標不總是指向例項物件所屬的類,不能依靠它來確定型別,而是應該用 class
方法來確定例項物件的類。因為 KVO
的實現機理就是將被觀察物件的 isa
指標指向一箇中間類而不是真實的類,這是一種叫做 isa-swizzling
的技術。
objc_class
Objective-C
類是由 Class
型別來表示的,它實際上是一個指向 objc_class
結構體的指標。
typedef struct objc_class *Class;
複製程式碼
objc/runtime.h
中 objc_class
結構體的定義如下:
struct objc_class {
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;
複製程式碼
結構體裡儲存了指向父類的指標、類的名字、版本、例項大小、例項變數列表、方法列表、快取、遵守的協議列表等。
物件在記憶體中的排布可以看成一個結構體,該結構體的大小並不能動態變化,所以無法在執行時動態給物件增加成員變數。相對的,物件的方法定義都儲存在類的可變區域中。如下圖所示為 Class
的描述資訊,其中 methodList
為可訪問類中定義的方法的指標的指標,通過修改該指標所指向的指標的值,我們可以實現為類動態增加方法實現。
objc_class
繼承於 objc_object
,也就是說一個 Objective-C
類本身同時也是一個物件,我們稱之為類物件,類物件就是一個結構體 struct objc_class
,這個結構體存放的資料稱為後設資料。為了處理類和物件的關係,runtime
庫建立了一種叫做元類 (Meta Class
) 的東西,類物件所屬型別就叫做元類,它用來表述類物件本身所具備的後設資料。類方法就定義於此處,因為這些方法可以理解成類物件的例項方法。每個類僅有一個類物件,而每個類物件僅有一個與之相關的元類。
當你發出一個類似 [NSObject alloc]
的訊息時,你事實上是把這個訊息發給了一個類物件 (Class Object
) ,這個類物件必須是一個元類的例項,而這個元類同時也是一個根元類 (root meta class
) 的例項。所有的元類最終都指向根元類為其超類。所有的元類的方法列表都有能夠響應訊息的類方法。所以當 [NSObject alloc]
這條訊息發給類物件的時候,objc_msgSend()
會去它的元類裡面去查詢能夠響應訊息的方法,如果找到了,然後對這個類物件執行方法呼叫。
元類(Meta Class)
元類(Meta Class
)是一個類物件的類。
在上面我們提到,所有的類自身也是一個物件,我們可以向這個物件傳送訊息(即呼叫類方法)。
為了呼叫類方法,這個類的 isa
指標必須指向一個包含這些類方法的一個 objc_class
結構體,這就引出了 meta-class
的概念。
類物件中的後設資料儲存的都是如何建立一個例項的相關資訊,那麼類物件和類方法應該從哪裡建立呢?
就是從 isa
指標指向的結構體建立,類物件的 isa
指標指向的我們稱之為元類(metaclass
),元類中儲存了建立類物件以及類方法所需的所有資訊。
- 每個
Class
都有一個isa
指標指向一個唯一的Meta Class
- 每一個
Meta Class
的isa
指標都指向最上層的Meta Class
(圖中的NSObject
的Meta Class
) - 最上層的
Meta Class
的isa
指標指向自己,形成一個迴路 - 每一個
Meta Class
的super_class
指標指向它原本Class
的super_class
的Meta Class
。但是最上層的Meta Class
的super_class
指向NSObject Class
本身 - 最上層的
NSObject Class
的super_class
為nil
,也就是它沒有超類
Method(objc_method)
objc/runtime.h
:
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
複製程式碼
- objc_method 結構體的內容:
SEL method_name
: 方法名,相同名字的方法即使在不同類中定義,它們的方法選擇器也相同
char *method_types
: 方法型別,是個char指標,其實儲存著方法的引數型別和返回值型別
IMP method_imp
: 方法實現,本質上是一個函式指標
在 iOS
的 Runtime
中,Method
通過 selector
和 IMP
兩個屬性,實現了快速查詢方法及實現,相對提高了效能,又保持了靈活性
類快取(objc_cache)
cache
為方法呼叫的效能進行優化。每個訊息都需要遍歷一次 isa
指向的類的方法列表(objc_method_list
),這樣效率太低了。Runtime
系統會把被呼叫的方法存到 cache
中( method_name
作為key
,method_imp
作為value
)。下次查詢的時候會優先在 cache
中查詢,效率更高。
objc_cache
是存在 objc_class
結構體中的。
cache_t
中 _buckets
、_mask
和 _occupied
:
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
... 省略其他方法
}
複製程式碼
bucket_t
中儲存了 指標
與 IMP
的鍵值對:
struct bucket_t {
private:
cache_key_t _key;
IMP _imp;
public:
inline cache_key_t key() const { return _key; }
inline IMP imp() const { return (IMP)_imp; }
inline void setKey(cache_key_t newKey) { _key = newKey; }
inline void setImp(IMP newImp) { _imp = newImp; }
void set(cache_key_t newKey, IMP newImp);
};
複製程式碼
Category(objc_category)
Category
為現有的類提供了擴充性,它是 category_t
一個指向分類的結構體的指標。
typedef struct category_t *Category;
複製程式碼
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
};
複製程式碼
name:是指 class_name 而不是 category_name。
cls:要擴充套件的類物件,編譯期間是不會定義的,而是在Runtime階段通過name對 應到對應的類物件。
instanceMethods:category中所有給類新增的例項方法的列表。
classMethods:category中所有新增的類方法的列表。
protocols:category實現的所有協議的列表。
instanceProperties:表示Category裡所有的properties,這就是我們可以通過objc_setAssociatedObject和objc_getAssociatedObject增加例項變數的原因,不過這個和一般的例項變數是不一樣的。
複製程式碼
從上邊category_t
的結構體中可以看出,分類中可以新增例項方法,類方法,甚至可以實現協議,新增屬性,不可以新增成員變數。
Ivar
Ivar
是一種代表類中例項變數的型別。
typedef struct ivar_t *Ivar;
複製程式碼
ivar_t
:
struct ivar_t {
int32_t *offset;
const char *name;
const char *type;
// alignment is sometimes -1; use alignment() instead
uint32_t alignment_raw;
uint32_t size;
uint32_t alignment() const {
if (alignment_raw == ~(uint32_t)0) return 1U << WORD_SHIFT;
return 1 << alignment_raw;
}
};
複製程式碼
class_copyIvarList
函式獲取的不僅有例項變數,還有屬性。但會在原本的屬性名前加上一個下劃線。
objc_property_t
@property
標記了類中的屬性,它是一個指向objc_property
結構體的指標:
typedef struct property_t *objc_property_t;
複製程式碼
可以通過 class_copyPropertyList
和 protocol_copyPropertyList
方法來獲取類和協議中的屬性:
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)
複製程式碼
返回型別為指向指標的指標,因為屬性列表是個陣列,每個元素內容都是一個 objc_property_t
指標,而這兩個函式返回的值是指向這個陣列的指標。
class_copyIvarList
和 class_copyPropertyList
對比:
- (void)runtimeGetPropertyList {
id RuntimeExploreInfo = objc_getClass("RuntimeExploreInfo");
unsigned int outCount, i;
objc_property_t *properties = class_copyPropertyList(RuntimeExploreInfo, &outCount);
for (i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
fprintf(stdout, "runtimeGetPropertyList---%s %s\n", property_getName(property), property_getAttributes(property));
}
}
- (void)runtimeGetIvarList {
id RuntimeExploreInfo = objc_getClass("RuntimeExploreInfo");
unsigned int numIvars = 0;
Ivar *ivars = class_copyIvarList(RuntimeExploreInfo, &numIvars);
for(int i = 0; i < numIvars; i++) {
Ivar thisIvar = ivars[i];
const char *type = ivar_getTypeEncoding(thisIvar);
NSString *stringType = [NSString stringWithCString:type encoding:NSUTF8StringEncoding];
if (![stringType hasPrefix:@"@"]) {
continue;
}
fprintf(stdout, "runtimeGetIvarList---%s\n", ivar_getName(thisIvar));
}
}
複製程式碼
列印結果:
IMP
就是指向最終實現程式的記憶體地址的指標。
typedef void (*IMP)(void /* id, SEL, ... */ );
複製程式碼
它就是一個函式指標,這是由編譯器生成的。當你發起一個 Objective-C
訊息之後,最終它會執行的那段程式碼,就是由這個函式指標指定的。而 IMP 這個函式指標就指向了這個方法的實現。
你會發現 IMP
指向的方法與 objc_msgSend
函式型別相同,引數都包含 id
和 SEL
型別。每個方法名都對應一個 SEL
型別的方法選擇器,而每個例項物件中的 SEL
對應的方法實現肯定是唯一的,通過一組 id
和 SEL
引數就能確定唯一的方法實現地址;反之亦然。
3. Runtime的三次轉發流程
進行一次傳送訊息會在相關的類物件中搜尋方法列表,如果找不到則會沿著繼承樹向上一直搜尋直到繼承樹根部(通常為 NSObject
),如果還是找不到並且訊息轉發都失敗了就回執行 doesNotRecognizeSelector:
方法報 unrecognized selector
錯。
- 動態方法解析:
+resolveInstanceMethod:
,+resolveClassMethod:
- 訊息轉發:
forwardingTargetForSelector
- 重定向:
methodSignatureForSelector:
,forwardInvocation:
動態方法解析
Objective-C
執行時會呼叫 +resolveInstanceMethod:
或者 +resolveClassMethod:
,讓你有機會提供一個函式實現。如果你新增了函式並返回YES, 那執行時系統就會重新啟動一次訊息傳送的過程。
```
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//執行foo函式
[self performSelector:@selector(foo:)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(foo:)) {//如果是執行foo函式,就動態解析,指定新的IMP
class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void fooMethod(id obj, SEL _cmd) {
NSLog(@"Doing foo");//新的foo函式
}
```
如果`resolve`方法返回 `NO` ,執行時就會移到下一步 `:forwardingTargetForSelector`
複製程式碼
訊息轉發
如果目標物件實現了 -forwardingTargetForSelector:
,Runtime
這時就會呼叫這個方法,給你把這個訊息轉發給其他物件的機會。
Controller
:
```
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[self performSelector:@selector(runtimeMessageTest)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return YES; // 返回YES,進入下一步轉發
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(runtimeMessageTest)) {
return [RuntimeExploreInfo new]; // 返回RuntimeExploreInfo物件,讓RuntimeExploreInfon物件接收這個訊息
}
return [super forwardingTargetForSelector:aSelector];
}
```
複製程式碼
RuntimeExploreInfo
:
```
#import "RuntimeExploreInfo.h"
@implementation RuntimeExploreInfo
- (void)runtimeMessageTest {
NSLog(@"runtimeMessageTest---");
}
@end
```
通過 `forwardingTargetForSelector` 把當前 `Controller` 的方法轉發給了 `RuntimeExploreInfo` 去執行。
複製程式碼
重定向
如果在上一步還不能處理未知訊息,則唯一能做的就是啟用完整的訊息轉發機制了。
首先它會傳送 -methodSignatureForSelector:
訊息獲得函式的引數和返回值型別。如果 -methodSignatureForSelector:
返回 nil
,Runtime
則會發出 -doesNotRecognizeSelector:
訊息,程式這時也就掛掉了。如果返回了一個函式簽名,Runtime
就會建立一個 NSInvocation
物件併傳送 -forwardInvocation:
訊息給目標物件。
```
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[self performSelector:@selector(runtimeMessageTest)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return YES; // 返回YES,進入下一步轉發
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
return nil; // 返回nil,進入下一步轉發
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if ([NSStringFromSelector(aSelector) isEqualToString:@"runtimeMessageTest"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"]; // 簽名,進入forwardInvocation
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = anInvocation.selector;
RuntimeExploreInfo *p = [RuntimeExploreInfo new];
if([p respondsToSelector:sel]) {
[anInvocation invokeWithTarget:p];
}else {
[self doesNotRecognizeSelector:sel];
}
}
```
複製程式碼
我們實現了完整的轉發。通過簽名,Runtime
生成了一個物件 anInvocation
,傳送給了 forwardInvocation
,我們在 forwardInvocation
方法裡面讓 RuntimeExploreInfo
物件去執行了 runtimeMessageTest
函式。
4. Runtime 應用
- 關聯物件(
Objective-C Associated Objects
)給分類增加屬性 - 方法魔法(
Method Swizzling
)方法新增和替換 KVO
實現- 實現
NSCoding
的自動歸檔和自動解檔 - 實現字典和模型的自動轉換(
MJExtension
、YYModel
) - 用於封裝框架(想怎麼改就怎麼改)
關聯物件( Objective-C Associated Objects
)給分類增加屬性
RuntimeExploreInfo+RuntimeAddProperty.h
新增了 phoneNum
屬性
```
#import "RuntimeExploreInfo+RuntimeAddProperty.h"
#import "objc/runtime.h"
@implementation RuntimeExploreInfo (RuntimeAddProperty)
static char kPhoneNumKey;
- (void)setPhoneNum:(NSString *)phoneNum {
objc_setAssociatedObject(self, &kPhoneNumKey, phoneNum, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)phoneNum {
return objc_getAssociatedObject(self, &kPhoneNumKey);
}
@end
```
```
- (void)runtimeAddProperty {
RuntimeExploreInfo *test = [RuntimeExploreInfo new];
test.phoneNum = @"12342424242";
NSLog(@"RuntimeAddProperty---%@", test.phoneNum);
}
```
複製程式碼
方法魔法( Method Swizzling
)方法新增和替換和 KVO
實現
- 新增方法
/** class_addMethod(Class _Nullable __unsafe_unretained cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) cls 被新增方法的類 name 新增的方法的名稱的SEL imp 方法的實現。該函式必須至少要有兩個引數,self,_cmd 型別編碼 */ class_addMethod([self class], sel, (IMP)fooMethod, "v@:"); 複製程式碼
- 替換方法
class_replaceMethod
替換類方法的定義
method_exchangeImplementations
交換兩個方法的實現
method_setImplementation
設定一個方法的實現
注意:class_replaceMethod
試圖替換一個不存在的方法時候,會呼叫class_addMethod
為該類增加一個新方法
swizzling應該只在+ (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class class = [self class]; SEL originalSelector = @selector(viewDidLoad); SEL swizzledSelector = @selector(runtimeReplaceViewDidLoad); Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); //judge the method named swizzledMethod is already existed. BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); // if swizzledMethod is already existed. if (didAddMethod) { class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); }else { method_exchangeImplementations(originalMethod, swizzledMethod); } }); } - (void)runtimeReplaceViewDidLoad { NSLog(@"替換的方法"); //[self runtimeReplaceViewDidLoad]; } 複製程式碼
+load
中執行一次(dispatch_once
)完成。在Objective-C
的執行時中,每個類有兩個方法都會自動呼叫。+load
是在一個類被初始裝載時呼叫,+initialize
是在應用第一次呼叫該類的類方法或例項方法前呼叫的。兩個方法都是可選的,並且只有在方法被實現的情況下才會被呼叫。
KVO實現
Apple 使用了 `isa-swizzling` 來實現 `KVO` 。當觀察物件A時,`KVO`機制動態建立一個新的名為:`NSKVONotifying_A`的新類,該類繼承自物件A的本類,且 `KVO` 為 `NSKVONotifying_A` 重寫觀察屬性的 `setter` 方法,`setter` 方法會負責在呼叫原 `setter` 方法之前和之後,通知所有觀察物件屬性值的更改情況。
`NSKVONotifying_A` 類剖析
```
NSLog(@"self->isa:%@",self->isa);
NSLog(@"self class:%@",[self class]);
```
在建立KVO監聽前,列印結果為:
```
self->isa:A
self class:A
```
在建立KVO監聽之後,列印結果為:
```
self->isa:NSKVONotifying_A
self class:A
```
子類setter方法剖析:
`KVO` 的鍵值觀察通知依賴於 `NSObject` 的兩個方法: `willChangeValueForKey:` 和 `didChangeValueForKey:` ,在存取數值的前後分別呼叫 2 個方法:
複製程式碼
被觀察屬性發生改變之前,willChangeValueForKey:
被呼叫,通知系統該 keyPath
的屬性值即將變更;當改變發生後, didChangeValueForKey:
被呼叫,通知系統該keyPath
的屬性值已經變更;之後, observeValueForKey:ofObject:change:context:
也會被呼叫。且重寫觀察屬性的 setter
方法這種繼承方式的注入是在執行時而不是編譯時實現的。
KVO 為子類的觀察者屬性重寫呼叫存取方法的工作原理在程式碼中相當於:
- (void)setName:(NSString *)newName { [self willChangeValueForKey:@"name"]; //KVO 在呼叫存取方法之前總呼叫 [super setValue:newName forKey:@"name"]; //呼叫父類的存取方法 [self didChangeValueForKey:@"name"]; //KVO 在呼叫存取方法之後總呼叫 }
實現NSCoding的自動歸檔和自動解檔
原理描述:用 runtime
提供的函式遍歷 Model
自身所有屬性,並對屬性進行 encode
和 decode
操作。
核心方法:在Model的基類中重寫方法:
```
- (id)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
unsigned int outCount;
Ivar * ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar ivar = ivars[i];
NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
[self setValue:[aDecoder decodeObjectForKey:key] forKey:key];
}
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
unsigned int outCount;
Ivar * ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar ivar = ivars[i];
NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
[aCoder encodeObject:[self valueForKey:key] forKey:key];
}
}
```
複製程式碼
實現字典和模型的自動轉換
原理描述:用runtime提供的函式遍歷Model自身所有屬性,如果屬性在json中有對應的值,則將其賦值。
核心方法:在NSObject的分類中新增方法
```
- (instancetype)initWithDict:(NSDictionary *)dict {
if (self = [self init]) {
//(1)獲取類的屬性及屬性對應的型別
NSMutableArray * keys = [NSMutableArray array];
NSMutableArray * attributes = [NSMutableArray array];
unsigned int outCount;
objc_property_t * properties = class_copyPropertyList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
objc_property_t property = properties[i];
//通過property_getName函式獲得屬性的名字
NSString * propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
[keys addObject:propertyName];
//通過property_getAttributes函式可以獲得屬性的名字和@encode編碼
NSString * propertyAttribute = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];
[attributes addObject:propertyAttribute];
}
//立即釋放properties指向的記憶體
free(properties);
//(2)根據型別給屬性賦值
for (NSString * key in keys) {
if ([dict valueForKey:key] == nil) continue;
[self setValue:[dict valueForKey:key] forKey:key];
}
}
return self;
}
```
複製程式碼
5. Runtime 面試題
-
Self & Super
@implementation Son : Father - (id)init { self = [super init]; if (self) { NSLog(@"%@", NSStringFromClass([self class])); NSLog(@"%@", NSStringFromClass([super class])); } return self; } @end 複製程式碼
答案:都輸出 Son
解惑:這個題目主要是考察關於objc
中對self
和super
的理解。self
是類的隱藏引數,指向當前呼叫方法的這個類的例項。而super
是一個Magic Keyword
, 它本質是一個編譯器標示符,和self
是指向的同一個訊息接受者。上面的例子不管呼叫[self class]
還是[super class]
,接受訊息的物件都是當前 Son *xxx 這個物件。而不同的是,super
是告訴編譯器,呼叫class
這個方法時,要去父類的方法,而不是本類裡的。當使用
self
呼叫方法時,會從當前類的方法列表中開始找,如果沒有,就從父類中再找;
而當使用super
時,則從父類的方法列表中開始找。然後呼叫父類的這個方法。當呼叫
[self class]
時,實際先呼叫的是objc_msgSend
函式,第一個引數是Son
當前的這個例項,然後在Son
這個類裡面去找- (Class)class
這個方法,沒有,去父類Father
裡找,也沒有,最後在NSObject
類中發現這個方法。而- (Class)class
的實現就是返回self
的類別,故上述輸出結果為Son
。當呼叫
[super class]
時,會轉換成objc_msgSendSuper
函式。第一步先構造objc_super
結構體,結構體第一個成員就是self
。第二個成員是(id)class_getSuperclass(objc_getClass(“Son”))
, 實際該函式輸出結果為Father
。第二步是去Father
這個類裡去找- (Class)class
,沒有,然後去NSObject
類去找,找到了。最後內部是使用objc_msgSend(objc_super->receiver
,@selector(class))
去呼叫,此時已經和[self class]
呼叫相同了,故上述輸出結果仍然返回Son
。 -
Object
&Class
&Meta Clas
@interface Sark : NSObject @end @implementation Sark @end int main(int argc, const char * argv[]) { @autoreleasepool { BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]]; BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]]; BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]]; BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]]; NSLog(@"%d %d %d %d", res1, res2, res3, res4); } return 0; } 複製程式碼
答案: 1 0 0 0
我們看到在Objective-C
的設計哲學中,一切都是物件。Class在設計中本身也是一個物件。而這個Class
物件的對應的類,我們叫它Meta Class
。即Class
結構體中的isa
指向的就是它的Meta Class
。
Meta Class
理解為 一個Class
物件的Class
。簡單的說:
當我們傳送一個訊息給一個NSObject
物件時,這條訊息會在物件的類的方法列表裡查詢;
當我們傳送一個訊息給一個類時,這條訊息會在類的Meta Class
的方法列表裡查詢 -
訊息 和 Category
@interface NSObject (Sark) + (void)foo; @end @implementation NSObject (Sark) - (void)foo { NSLog(@"IMP: -[NSObject(Sark) foo]"); } @end int main(int argc, const char * argv[]) { @autoreleasepool { [NSObject foo]; [[NSObject new] foo]; } return 0; } 複製程式碼
答案:
IMP: -[NSObject(Sark) foo]
IMP: -[NSObject(Sark) foo]
解釋:objc runtime
載入完後,NSObject
的Sark Category
被載入。而NSObject
的Sark Category
的標頭檔案+ (void)foo
並沒有實質參與到工作中,只是給編譯器進行靜態檢查,所有我們編譯上述程式碼會出現警告,提示我們沒有實現+ (void)foo
方法。而在程式碼編譯中,它已經被註釋掉了。- 實際被加入到
Class
的method list
的方法是- (void)foo
,它是一個例項方法,所以加入到當前類物件NSObject
的方法列表中,而不是NSObject
Meta class
的方法列表中。 - 當執行
[NSObject foo]
時,我們看下整個objc_msgSend
的過程:
objc_msgSend
第一個引數是(id)objc_getClass("NSObject")
,獲得NSObject Class
的物件。- 類方法在
Meta Class
的方法列表中找,我們在load Category
方法時加入的是- (void)foo
例項方法,所以並不在NSOBject
Meta Class
的方法列表中 - 繼續往
super class
中找,NSObject
Meta Class
的super class
是NSObject
本身。所以,這個時候我們能夠找到- (void)foo
這個方法。
所以正常輸出結果。
- 當執行
[[NSObject new] foo]
,我們看下整個objc_msgSend
的過程:
[NSObject new]
生成一個NSObject
物件。直接在該物件的類(NSObject
)的方法列表裡找。能夠找到,所以正常輸出結果。
-
成員變數與屬性
@interface Sark : NSObject @property (nonatomic, copy) NSString *name; @end @implementation Sark - (void)speak { NSLog(@"my name is %@", self.name); } @end @interface Test : NSObject @end @implementation Test - (instancetype)init { self = [super init]; if (self) { id cls = [Sark class]; void *obj = &cls; [(__bridge id)obj speak]; } return self; } @end int main(int argc, const char * argv[]) { @autoreleasepool { [[Test alloc] init]; } return 0; } 複製程式碼
答案: my name is
更多實用詳見 Demo Runtime資料夾下