一道值得深入思考的iOS面試題詳解
前言
最近在群里看到有人發(fā)的一道面試題,題目如下:
@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 ?
最終問題就是這段代碼的運行結(jié)果。
過程
第一眼看這個問題,我直接就想說,這個東西啊,肯定是編譯報錯了、要不就是崩潰啊
所以我就跟著寫了些代碼,結(jié)果發(fā)現(xiàn):
WTF? 怎么能運行,而且結(jié)果竟然還是
相信當你看到這個結(jié)果的時候會和我一樣吃驚,不和邏輯啊,怎么竟然能執(zhí)行成功并且還打印出來當前controller了,不符合常理啊。
解析
對于計算機而言,不存在什么魔法,如果一段代碼能運行必然存在它的原理。
我們需要做的就是分析為什么能成功。
為什么調(diào)用不崩潰
我們需要了解,cls的意思。
cls在C語言里,就是一個指針,這個指針的內(nèi)容指向Spark類
當我們通過void *obj = &cls;
這個語句執(zhí)行后,獲取的就是一個指向這個指針cls的指針
事實上在這一步操作實現(xiàn)后,obj 這個指針就已經(jīng)具有Object-c對象的功能了,為什么呢?接下來我們可以看看runtime實現(xiàn)原理了,這里我只說一點
//對象 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>中的顯示,但是覺得直接刪除又顯現(xiàn)不出更改。因而在此專門寫出,我會在下面給出正確的解釋與數(shù)據(jù)來源
struct objc_class : objc_object { // Class ISA; Class superclass; cache_t cache; // formerly cache pointer and vtable class_data_bits_t bits; }
數(shù)據(jù)來源: 蘋果obj4開源代碼 第1012行 用以替換 上述簡述引用中的 objc_class
可以看到objc_object這個對象的首字段是isa 指向一個Class
也就是說,我們?nèi)绻幸粋€指向Class的地址的指針,相當于這個對象就已經(jīng)可以使用了,只是像他的成員變量等等的一系列值都還沒有被初始化。
所以接下來用(__bridge id)obj
,調(diào)用是不會產(chǎn)生問題的
為什么能打印出ViewController對象?
這個問題就是由兩個小部分組成的
1. name 這個屬性是什么時候賦的值?
2. ViewController 這個對象是什么時候被傳入的?
首先我們需要先了解一下,一個類對象的數(shù)據(jù)是如何存儲的。
這里我就按照上文一樣引用很多的論證了,我們自己來探究
該上代碼了:
@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
接下來調(diào)用printPrinter,打印一下對象指針地址:
可以發(fā)現(xiàn),指針偏移量成員變量和指針首地址差8個字節(jié),每個成員變量與上一個成員變量偏移量也是8個字節(jié)。
完成到這一步,我們?nèi)匀粵]有發(fā)現(xiàn)上述兩個問題是應(yīng)該怎么解釋。但是我們知道了,一個Object-C 對象的指針,和它的成員變量的指針肯定是連續(xù)的。這就為接下來我們的分析提供了一些思路。
下一步,我在原本的題目中增加一行代碼:
[super viewDidLoad]; NSString *str = @"11111"; id cls = [Spark class];
為啥要增加這行代碼呢,這步是經(jīng)過深(瞎)思(J)熟(B)慮(試),主要是考慮到函數(shù)內(nèi)部的參數(shù)生成必然會需要地方存儲,但這部分存儲地址,我們是不知曉的,它的實現(xiàn)是被系統(tǒng)隱藏的。而我們的代碼又沒有明顯的設(shè)置相關(guān)代碼,那么必然是由這些條件實現(xiàn)的。所以當我們增加了這一行代碼后,不出意外的,打印結(jié)果變了
2018-11-29 20:49:39.254021+0800 test[1961:92498] My name is:11111
變成了 我們 上述的值,這一切都和猜想的差不多
于是一個基本設(shè)想就出來了:
因為棧上的地址結(jié)構(gòu)和原本類的需求地址結(jié)構(gòu)高度重合了,同時所有地址都能訪問到對應(yīng)的值。我們通過棧的默認行為生成了一個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,而且正好和對象結(jié)構(gòu)體定義的一模一樣。所以這也正好能說明我們上述的打印結(jié)果My name is:11111為什么會發(fā)生。
注:這個存在的原因是因為函數(shù)內(nèi)部變量采用的小端模式,也就是將參數(shù)地址由棧區(qū)從高地址依次向低地址分配,所以我們打印cls地址會比str要小。
由此,第一個小問題就解決了,答案是因為我們在生成堆棧參數(shù)的時候,拼湊出了Spark對象的地址數(shù)據(jù)結(jié)構(gòu)格式,和真正的對象地址數(shù)據(jù)結(jié)構(gòu)一樣,所以self.name就是在生成cls的那一刻起內(nèi)存地址就已經(jīng)被賦值了。
接下來到下一個問題了ViewController 是什么時候傳入的?
在這一步里我們只能把目光向cls對象生成前執(zhí)行的操作來看,[super viewDidLoad];我們只執(zhí)行了這一步操作,那必然是這個操作產(chǎn)生的結(jié)果。為了驗證,我們可以更改一下調(diào)用順序
id cls = [Cls class]; [super viewDidLoad];
當我們進行這部操作后,會發(fā)現(xiàn),執(zhí)行speak方法時崩潰了,錯誤是EXC_BAC_ACCESS,說明是我們引用野指針了。
由此也可以證實,[super viewDidLoad];肯定做了一些騷操作,將ViewController的self壓入了棧區(qū)。
接下來我們就需要探究究竟做了什么操作,我們可以用如下的命令行代碼將ViewController.m重寫成c++代碼,然后觀看發(fā)生了什么。
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"));
我們可以發(fā)現(xiàn)原本這個方法里面會傳入兩個參數(shù)一個是self,一個是_cmd,當我們調(diào)用[super viewDidLoad]時,執(zhí)行的方法中傳入了參數(shù)self,由此將self做為一個值壓入了棧中,但是_cmd這個參數(shù)并未被使用,因此,沒有被壓入棧中。
至此,這個問題已經(jīng)被解釋出來了。
答案
所有NSObject對象的首地址都是指向這個對象的所屬類。這個條件是充要條件。反過來說,如果一個地址指向某個類,我們就可以把這個地址當成對象去用。所以編譯是會通過的,也不會報unrecognized selector的錯誤。
打印結(jié)果會是ViewController對象的原因是因為cls在棧上的數(shù)據(jù)結(jié)構(gòu)符合了它作為真實的類時候的數(shù)據(jù)結(jié)構(gòu),cls.name原本地址正好是棧上ViewController對象地址,因此NSLog能打印出<ViewController >
思索
這類問題,考察的東西很深,并且結(jié)合了很多知識點。但是當我們拿到面試題并且能進行思索的時候一定要好好的考慮,我對這道題的想法,也是在不斷的試驗中逐漸的完善,并且嘗試了很多。其實找面試題為什么是這個答案的過程和,找代碼找bug的流程都是類似的,都是排除變量,逐步探索,最終將探索過程和概念結(jié)合。
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
相關(guān)文章
iOS自定義字體設(shè)置和系統(tǒng)自帶的字體詳解
這篇文章主要給大家介紹了關(guān)于iOS自定義字體設(shè)置和系統(tǒng)自帶的字體的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-01-01iOS開發(fā)UICollectionView實現(xiàn)拖拽效果
這篇文章主要為大家詳細介紹了iOS開發(fā)UICollectionView實現(xiàn)拖拽效果,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-01-01iOS開發(fā)之Objective-c的Runtime理解指南
這篇文章主要介紹了iOS開發(fā)之Objective-c的Runtime理解指南的相關(guān)資料,需要的朋友可以參考下2022-08-08iOS動態(tài)調(diào)整UILabel高度的幾種方法
在iOS編程中UILabel是一個常用的控件,下面這篇文章主要給大家介紹了關(guān)于iOS動態(tài)調(diào)整UILabel高度的幾種方法,文中通過示例代碼介紹的非常詳細,需要的朋友可以參考借鑒,下面隨著小編來一起學習學習吧。2017-12-12