上篇文章:Runtime在工作中的運用
1.objc在向一個物件傳送訊息時,發生了什麼?
objc在向一個物件傳送訊息時,runtime會根據物件的isa指標找到該物件實際所屬的類,然後在該類中的方法列表以及其父類方法列表中尋找方法執行,如果一直到根類還沒找到,轉向攔截呼叫,走訊息轉發機制,一旦找到 ,就去執行它的實現
IMP
。
詳解:請看Runtime在工作中的運用 第二章Runtime訊息機制;
2.objc中向一個nil物件傳送訊息將會發生什麼?
如果向一個nil物件傳送訊息,首先在尋找物件的isa指標時就是0地址返回了,所以不會出現任何錯誤。也不會崩潰。
詳解: 如果一個方法返回值是一個物件,那麼傳送給nil的訊息將返回0(nil);
如果方法返回值為指標型別,其指標大小為小於或者等於sizeof(void*) ,float,double,long double 或者long long的整型標量,傳送給nil的訊息將返回0;
如果方法返回值為結構體,傳送給nil的訊息將返回0。結構體中各個欄位的值將都是0;
如果方法的返回值不是上述提到的幾種情況,那麼傳送給nil的訊息的返回值將是未定義的。
3.objc中向一個物件傳送訊息[obj foo]和objc_msgSend()
函式之間有什麼關係?
在objc編譯時,[obj foo] 會被轉意為:
objc_msgSend(obj, @selector(foo));
。
詳解:請看Runtime在工作中的運用 第二章Runtime訊息機制;
4.什麼時候會報unrecognized selector的異常?
objc在向一個物件傳送訊息時,runtime庫會根據物件的isa指標找到該物件實際所屬的類,然後在該類中的方法列表以及其父類方法列表中尋找方法執行,如果,在最頂層的父類中依然找不到相應的方法時,會進入訊息轉發階段,如果訊息三次轉發流程仍未實現,則程式在執行時會掛掉並丟擲異常unrecognized selector sent to XXX 。
詳解:請看Runtime在工作中的運用 第三章Runtime方法呼叫流程;
5.能否向編譯後得到的類中增加例項變數?能否向執行時建立的類中新增例項變數?為什麼?
不能向編譯後得到的類中增加例項變數;
能向執行時建立的類中新增例項變數;
1.因為編譯後的類已經註冊在 runtime 中,類結構體中的 objc_ivar_list 例項變數的連結串列和 instance_size 例項變數的記憶體大小已經確定,同時runtime會呼叫 class_setvarlayout 或 class_setWeaklvarLayout 來處理strong weak 引用.所以不能向存在的類中新增例項變數。
2.執行時建立的類是可以新增例項變數,呼叫class_addIvar函式. 但是的在呼叫 objc_allocateClassPair 之後,objc_registerClassPair 之前,原因同上.
6.給類新增一個屬性後,在類結構體裡哪些元素會發生變化?
instance_size :例項的記憶體大小;objc_ivar_list *ivars:屬性列表
7.一個objc物件的isa的指標指向什麼?有什麼作用?
指向他的類物件,從而可以找到物件上的方法
詳解:下圖很好的描述了物件,類,元類之間的關係:
圖中實線是 super_class指標,虛線是isa指標。
- Root class (class)其實就是NSObject,NSObject是沒有超類的,所以Root class(class)的superclass指向nil。
- 每個Class都有一個isa指標指向唯一的Meta class
- Root class(meta)的superclass指向Root class(class),也就是NSObject,形成一個迴路。
- 每個Meta class的isa指標都指向Root class (meta)。
8.[self class] 與 [super class]
下面的程式碼輸出什麼?
@implementation Son : Father
- (id)init
{
self = [super init];
if (self) {
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end
複製程式碼
NSStringFromClass([self class]) = Son NSStringFromClass([super class]) = Son
詳解:這個題目主要是考察關於 Objective-C 中對 self 和 super 的理解。
self 是類的隱藏引數,指向當前呼叫方法的這個類的例項;
super 本質是一個編譯器標示符,和 self 是指向的同一個訊息接受者。不同點在於:super 會告訴編譯器,當呼叫方法時,去呼叫父類的方法,而不是本類中的方法。
當使用 self 呼叫方法時,會從當前類的方法列表中開始找,如果沒有,就從父類中再找;而當使用 super 時,則從父類的方法列表中開始找。然後呼叫父類的這個方法。
在呼叫[super class]
的時候,runtime會去呼叫objc_msgSendSuper
方法,而不是objc_msgSend
;
OBJC_EXPORT void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
/// Specifies the superclass of an instance.
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained id receiver;
/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained Class class;
#else
__unsafe_unretained Class super_class;
#endif
/* super_class is the first class to search */
};
複製程式碼
在objc_msgSendSuper方法中,第一個引數是一個objc_super的結構體,這個結構體裡面有兩個變數,一個是接收訊息的receiver,一個是當前類的父類super_class。
objc_msgSendSuper的工作原理應該是這樣的: 從objc_super結構體指向的superClass父類的方法列表開始查詢selector,找到後以objc->receiver去呼叫父類的這個selector。注意,最後的呼叫者是objc->receiver,而不是super_class!
那麼objc_msgSendSuper最後就轉變成:
// 注意這裡是從父類開始msgSend,而不是從本類開始
objc_msgSend(objc_super->receiver, @selector(class))
/// Specifies an instance of a class. 這是類的一個例項
__unsafe_unretained id receiver;
// 由於是例項呼叫,所以是減號方法
- (Class)class {
return object_getClass(self);
}
複製程式碼
由於找到了父類NSObject裡面的class方法的IMP,又因為傳入的入參objc_super->receiver = self。self就是son,呼叫class,所以父類的方法class執行IMP之後,輸出還是son,最後輸出兩個都一樣,都是輸出son。
9.runtime如何通過selector找到對應的IMP地址?
每一個類物件中都一個方法列表,方法列表中記錄著方法的名稱,方法實現,以及引數型別,其實selector本質就是方法名稱,通過這個方法名稱就可以在方法列表中找到對應的方法實現.
10._objc_msgForward函式是做什麼的,直接呼叫它將會發生什麼?
_objc_msgForward
是 IMP 型別,用於訊息轉發的:當向一個物件傳送一條訊息,但它並沒有實現的時候,_objc_msgForward
會嘗試做訊息轉發。
詳解:_objc_msgForward
在進行訊息轉發的過程中會涉及以下這幾個方法:
resolveInstanceMethod:
方法 (或resolveClassMethod:
)。forwardingTargetForSelector:
方法methodSignatureForSelector:
方法forwardInvocation:
方法doesNotRecognizeSelector:
方法
具體請見:請看Runtime在工作中的運用 第三章Runtime方法呼叫流程;
11. runtime如何實現weak變數的自動置nil?知道SideTable嗎?
runtime 對註冊的類會進行佈局,對於 weak 修飾的物件會放入一個 hash 表中。 用 weak 指向的物件記憶體地址作為 key,當此物件的引用計數為0的時候會 dealloc,假如 weak 指向的物件記憶體地址是a,那麼就會以a為鍵, 在這個 weak 表中搜尋,找到所有以a為鍵的 weak 物件,從而設定為 nil。
更細一點的回答:
1.初始化時:runtime會呼叫objc_initWeak函式,初始化一個新的weak指標指向物件的地址。
2.新增引用時:objc_initWeak函式會呼叫objc_storeWeak() 函式, objc_storeWeak() 的作用是更新指標指向,建立對應的弱引用表。
3.釋放時,呼叫clearDeallocating函式。clearDeallocating函式首先根據物件地址獲取所有weak指標地址的陣列,然後遍歷這個陣列把其中的資料設為nil,最後把這個entry從weak表中刪除,最後清理物件的記錄。
SideTable結構體是負責管理類的引用計數表和weak表,
詳解:參考自《Objective-C高階程式設計》一書 1.初始化時:runtime會呼叫objc_initWeak函式,初始化一個新的weak指標指向物件的地址。
{
NSObject *obj = [[NSObject alloc] init];
id __weak obj1 = obj;
}
複製程式碼
當我們初始化一個weak變數時,runtime會呼叫 NSObject.mm 中的objc_initWeak函式。
// 編譯器的模擬程式碼
id obj1;
objc_initWeak(&obj1, obj);
/*obj引用計數變為0,變數作用域結束*/
objc_destroyWeak(&obj1);
複製程式碼
通過objc_initWeak
函式初始化“附有weak修飾符的變數(obj1)”,在變數作用域結束時通過objc_destoryWeak
函式釋放該變數(obj1)。
2.新增引用時:objc_initWeak函式會呼叫objc_storeWeak() 函式, objc_storeWeak() 的作用是更新指標指向,建立對應的弱引用表。
objc_initWeak
函式將“附有weak修飾符的變數(obj1)”初始化為0(nil)後,會將“賦值物件”(obj)作為引數,呼叫objc_storeWeak
函式。
obj1 = 0;
obj_storeWeak(&obj1, obj);
複製程式碼
也就是說:
weak 修飾的指標預設值是 nil (在Objective-C中向nil傳送訊息是安全的)
然後obj_destroyWeak
函式將0(nil)作為引數,呼叫objc_storeWeak
函式。
objc_storeWeak(&obj1, 0);
複製程式碼
前面的原始碼與下列原始碼相同。
// 編譯器的模擬程式碼
id obj1;
obj1 = 0;
objc_storeWeak(&obj1, obj);
/* ... obj的引用計數變為0,被置nil ... */
objc_storeWeak(&obj1, 0);
複製程式碼
objc_storeWeak
函式把第二個引數的賦值物件(obj)的記憶體地址作為鍵值,將第一個引數__weak修飾的屬性變數(obj1)的記憶體地址註冊到 weak 表中。如果第二個引數(obj)為0(nil),那麼把變數(obj1)的地址從weak表中刪除。
由於一個物件可同時賦值給多個附有__weak修飾符的變數中,所以對於一個鍵值,可註冊多個變數的地址。
可以把objc_storeWeak(&a, b)
理解為:objc_storeWeak(value, key)
,並且當key變nil,將value置nil。在b非nil時,a和b指向同一個記憶體地址,在b變nil時,a變nil。此時向a傳送訊息不會崩潰:在Objective-C中向nil傳送訊息是安全的。
3.釋放時,呼叫clearDeallocating函式。clearDeallocating函式首先根據物件地址獲取所有weak指標地址的陣列,然後遍歷這個陣列把其中的資料設為nil,最後把這個entry從weak表中刪除,最後清理物件的記錄。
當weak引用指向的物件被釋放時,又是如何去處理weak指標的呢?當釋放物件時,其基本流程如下:
1.呼叫objc_release
2.因為物件的引用計數為0,所以執行dealloc
3.在dealloc中,呼叫了_objc_rootDealloc函式
4.在_objc_rootDealloc中,呼叫了object_dispose函式
5.呼叫objc_destructInstance
6.最後呼叫objc_clear_deallocating
物件被釋放時呼叫的objc_clear_deallocating函式:
1.從weak表中獲取廢棄物件的地址為鍵值的記錄
2.將包含在記錄中的所有附有 weak修飾符變數的地址,賦值為nil
3.將weak表中該記錄刪除
4.從引用計數表中刪除廢棄物件的地址為鍵值的記錄
總結:
其實Weak表是一個hash(雜湊)表,Key是weak所指物件的地址,Value是weak指標的地址(這個地址的值是所指物件指標的地址)陣列。
12.isKindOfClass 與 isMemberOfClass
下面程式碼輸出什麼?
@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;
}
複製程式碼
1000
詳解:
在isKindOfClass
中有一個迴圈,先判斷class
是否等於meta class
,不等就繼續迴圈判斷是否等於meta class
的super class
,不等再繼續取super class
,如此迴圈下去。
[NSObject class]
執行完之後呼叫isKindOfClass
,第一次判斷先判斷NSObject
和 NSObject
的meta class
是否相等,之前講到meta class
的時候放了一張很詳細的圖,從圖上我們也可以看出,NSObject
的meta class
與本身不等。接著第二次迴圈判斷NSObject
與meta class
的superclass
是否相等。還是從那張圖上面我們可以看到:Root class(meta)
的superclass
就是 Root class(class)
,也就是NSObject本身。所以第二次迴圈相等,於是第一行res1輸出應該為YES。
同理,[Sark class]
執行完之後呼叫isKindOfClass
,第一次for迴圈,Sark的Meta Class
與[Sark class]
不等,第二次for迴圈,Sark Meta Class
的super class
指向的是 NSObject Meta Class
, 和Sark Class
不相等。第三次for迴圈,NSObject Meta Class
的super class
指向的是NSObject Class
,和 Sark Class
不相等。第四次迴圈,NSObject Class
的super class
指向 nil, 和 Sark Class
不相等。第四次迴圈之後,退出迴圈,所以第三行的res3輸出為NO。
isMemberOfClass
的原始碼實現是拿到自己的isa指標和自己比較,是否相等。
第二行isa 指向 NSObject
的 Meta Class
,所以和 NSObject Class
不相等。第四行,isa指向Sark的Meta Class
,和Sark Class
也不等,所以第二行res2和第四行res4都輸出NO。
13.使用runtime Associate方法關聯的物件,需要在主物件dealloc的時候釋放麼?
無論在MRC下還是ARC下均不需要,被關聯的物件在生命週期內要比物件本身釋放的晚很多,它們會在被 NSObject -dealloc 呼叫的object_dispose()方法中釋放。
詳解:
1、呼叫 -release :引用計數變為零
物件正在被銷燬,生命週期即將結束.
不能再有新的 __weak 弱引用,否則將指向 nil.
呼叫 [self dealloc]
2、 父類呼叫 -dealloc
繼承關係中最直接繼承的父類再呼叫 -dealloc
如果是 MRC 程式碼 則會手動釋放例項變數們(iVars)
繼承關係中每一層的父類 都再呼叫 -dealloc
>3、NSObject 調 -dealloc
只做一件事:呼叫 Objective-C runtime 中object_dispose() 方法
>4. 呼叫 object_dispose()
為 C++ 的例項變數們(iVars)呼叫 destructors
為 ARC 狀態下的 例項變數們(iVars) 呼叫 -release
解除所有使用 runtime Associate方法關聯的物件
解除所有 __weak 引用
呼叫 free()
複製程式碼
14. 什麼是method swizzling(俗稱黑魔法)
簡單說就是進行方法交換
詳解:請看Runtime在工作中的運用 第五章Runtime方法交換;
在Objective-C中呼叫一個方法,其實是向一個物件傳送訊息,查詢訊息的唯一依據是selector的名字。利用Objective-C的動態特性,可以實現在執行時偷換selector對應的方法實現,達到給方法掛鉤的目的。
每個類都有一個方法列表,存放著方法的名字和方法實現的對映關係,selector的本質其實就是方法名,IMP有點類似函式指標,指向具體的Method實現,通過selector就可以找到對應的IMP。
換方法的幾種實現方式
- 利用 method_exchangeImplementations 交換兩個方法的實現
- 利用 class_replaceMethod 替換方法的實現
- 利用 method_setImplementation 來直接設定某個方法的IMP
15.Compile Error / Runtime Crash / NSLog…?
下面的程式碼會?Compile Error
/ Runtime Crash
/ NSLog…
?
@interface NSObject (Sark)
+ (void)foo;
- (void)foo;
@end
@implementation NSObject (Sark)
- (void)foo {
NSLog(@"IMP: -[NSObject (Sark) foo]");
}
@end
// 測試程式碼
[NSObject foo];
[[NSObject new] performSelector:@selector(foo)];
複製程式碼
IMP: -[NSObject(Sark) foo] ,全都正常輸出,編譯和執行都沒有問題。
詳解:
這道題和上一道題很相似,第二個呼叫肯定沒有問題,第一個呼叫後會從元類中查詢方法,然而方法並不在元類中,所以找元類的superclass
。方法定義在是NSObject
的Category
,由於NSObject
的物件模型比較特殊,元類的superclass
是類物件,所以從類物件中找到了方法並呼叫。
感謝:
霜神、iOS程式犭袁、sunnyxx