最近在羣裏看到有人發的一道面試題,題目以下:c++
@interface Spark : NSObject
@property(nonatomic,copy) NSString *name;
@end
@implementation Spark
- (void)speak {
NSLog(@"My name is:%@",self.name);
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [Spark class];
void *obj = &cls;
[(__bridge id)obj speak];
}
複製代碼
問題:上述代碼運行起來會:Complie error?|Runtime crash?|NSLog ?
git
最終問題就是這段代碼的運行結果。github
第一眼看這個問題,我直接就想說,這個東西啊,確定是編譯報錯了、要不就是崩潰啊面試
因此我就跟着寫了些代碼,結果發現:objective-c
WTF? 怎麼能運行,並且結果居然仍是bash
相信當你看到這個結果的時候會和我同樣吃驚,不和邏輯啊,怎麼居然能執行成功而且還打印出來當前controller了,不符合常理啊。數據結構
對於計算機而言,不存在什麼魔法,若是一段代碼能運行必然存在它的原理。app
咱們須要作的就是分析爲何能成功。iphone
cls
的意思。cls
在C語言裏,就是一個指針,這個指針的內容指向Spark類函數
當咱們經過void *obj = &cls;
這個語句執行後,獲取的就是一個指向這個指針cls
的指針
事實上在這一步操做實現後,obj 這個指針就已經具備Object-c對象的功能了,爲何呢?接下來咱們能夠看看runtime實現原理了,這裏我只說一點
//對象
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
//類
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
//方法列表
struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
//方法
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
複製代碼
引自: iOS Runtime詳解-簡書
上述簡介中部分是錯誤的,由於這個只是在<objc/runtime.h>中的顯示,可是以爲直接刪除又顯現不出更改。於是在此專門寫出,我會在下面給出正確的解釋與數據來源
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits;
}
複製代碼
數據來源: 蘋果obj4開源代碼 第1012行 用以替換 上述簡述引用中的 objc_class
能夠看到objc_object
這個對象的首字段是isa 指向一個Class
也就是說,咱們若是有一個指向Class的地址的指針,至關於這個對象就已經可使用了,只是像他的成員變量等等的一系列值都尚未被初始化。
因此接下來用(__bridge id)obj
,調用是不會產生問題的
這個問題就是由兩個小部分組成的
1. name 這個屬性是何時賦的值?
2. ViewController 這個對象是何時被傳入的?
複製代碼
首先咱們須要先了解一下,一個類對象的數據是如何存儲的。
這裏我就按照上文同樣引用不少的論證了,咱們本身來探究
該上代碼了:
@interface Cls : NSObject
@property(nonatomic,strong) NSString *test;
@property(nonatomic,strong) NSString *test1;
@end
@implementation Cls
- (void)printPrinter {
NSLog(@"self:%p",self);
NSLog(@"self.test:%p",&_test);
NSLog(@"self.test1:%p",&_test1);
}
@end
複製代碼
接下來調用printPrinter
,打印一下對象指針地址:
能夠發現,指針偏移量成員變量和指針首地址差8個字節,每一個成員變量與上一個成員變量偏移量也是8個字節。
完成到這一步,咱們仍然沒有發現上述兩個問題是應該怎麼解釋。可是咱們知道了,一個Object-C 對象的指針,和它的成員變量的指針確定是連續的。這就爲接下來咱們的分析提供了一些思路。
下一步,我在本來的題目中增長一行代碼:
[super viewDidLoad];
NSString *str = @"11111";
id cls = [Spark class];
複製代碼
爲啥要增長這行代碼呢,這步是通過深(瞎)思(J)熟(B)慮(試),主要是考慮到函數內部的參數生成必然會須要地方存儲,但這部分存儲地址,咱們是不知曉的,它的實現是被系統隱藏的。而咱們的代碼又沒有明顯的設置相關代碼,那麼必然是由這些條件實現的。因此當咱們增長了這一行代碼後,不出意外的,打印結果變了
2018-11-29 20:49:39.254021+0800 test[1961:92498] My name is:11111
變成了 咱們 上述的值,這一切都和猜測的差很少
因而一個基本設想就出來了:
由於棧上的地址結構和本來類的需求地址結構高度重合了,同時全部地址都能訪問到對應的值。咱們經過棧的默認行爲生成了一個Spark對象!
爲了驗證,咱們打印一下cls
和str
的指針堆棧地址
NSLog(@"cls address:%p str address:%p",&cls,&str);
複製代碼
2018-11-29 21:03:30.490989+0800 test[2129:122769] cls address:0x7ffeebf4fa00 str address:0x7ffeebf4fa08
咱們能夠看到他們之間相差也正好是8,並且正好和對象結構體定義的如出一轍。因此這也正好能說明咱們上述的打印結果My name is:11111
爲何會發生。
注:這個存在的緣由是由於函數內部變量採用的小端模式,也就是將參數地址由棧區從高地址依次向低地址分配,因此咱們打印cls
地址會比str
要小。
由此,第一個小問題就解決了,答案是由於咱們在生成堆棧參數的時候,拼湊出了Spark對象的地址數據結構格式,和真正的對象地址數據結構同樣,因此self.name
就是在生成cls
的那一刻起內存地址就已經被賦值了。
接下來到下一個問題了ViewController 是何時傳入的?
在這一步裏咱們只能把目光向cls
對象生成前執行的操做來看,[super viewDidLoad];
咱們只執行了這一步操做,那必然是這個操做產生的結果。爲了驗證,咱們能夠更改一下調用順序
id cls = [Cls class];
[super viewDidLoad];
複製代碼
當咱們進行這部操做後,會發現,執行speak方法時崩潰了,錯誤是EXC_BAC_ACCESS
,說明是咱們引用野指針了。
由此也能夠證明,[super viewDidLoad];
確定作了一些騷操做,將ViewController的self
壓入了棧區。
接下來咱們就須要探究究竟作了什麼操做,咱們能夠用以下的命令行代碼將ViewController.m重寫成c++代碼,而後觀看發生了什麼。
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ViewController.m -o ViewController.cpp
複製代碼
static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
複製代碼
咱們能夠發現本來這個方法裏面會傳入兩個參數一個是self
,一個是_cmd
,當咱們調用[super viewDidLoad]
時,執行的方法中傳入了參數self
,由此將self
作爲一個值壓入了棧中,可是_cmd
這個參數並未被使用,所以,沒有被壓入棧中。
至此,這個問題已經被解釋出來了。
全部NSObject對象的首地址都是指向這個對象的所屬類。這個條件是充要條件。反過來講,若是一個地址指向某個類,咱們就能夠把這個地址當成對象去用。因此編譯是會經過的,也不會報unrecognized selector
的錯誤。
打印結果會是ViewController對象的緣由是由於cls
在棧上的數據結構符合了它做爲真實的類時候的數據結構,cls.name
本來地址正好是棧上ViewController對象地址,所以NSLog能打印出<ViewController >
這類問題,考察的東西很深,而且結合了不少知識點。可是當咱們拿到面試題而且能進行思索的時候必定要好好的考慮,我對這道題的想法,也是在不斷的試驗中逐漸的完善,而且嘗試了不少。其實找面試題爲何是這個答案的過程和,找代碼找bug的流程都是相似的,都是排除變量,逐步探索,最終將探索過程和概念結合。
也許答案不是很專業,但願你們若是有更專業的答案,能夠告訴我。順便同步推廣本身的博客:www.wdtechnology.club/
謝謝您的閱讀
本文同步發行於本人博客 未經受權不得隨意使用