一道有意思的iOS面試題

前言

最近在群里看到有人發(fā)的一道面試題,題目如下:

  1. @interface Spark : NSObject

  2. @property(nonatomic,copy) NSString *name;

  3. @end

  4. @implementation Spark

  5. - (void)speak {

  6. NSLog(@"My name is:%@",self.name);

  7. }

  8. @end

  9. @implementation ViewController

  10. - (void)viewDidLoad {

  11. [super viewDidLoad];

  12. id cls = [Spark class];

  13. void *obj = &cls;

  14. [(__bridge id)obj speak];

  15. }

</pre>

問題:上述代碼運(yùn)行起來會: Complieerror?|Runtimecrash?|NSLog?

最終問題就是這段代碼的運(yùn)行結(jié)果奸披。

過程

第一眼看這個問題污茵,我直接就想說,這個東西啊钧忽,肯定是編譯報錯了毯炮、要不就是崩潰啊

所以我就跟著寫了些代碼,結(jié)果發(fā)現(xiàn):

WTF? 怎么能運(yùn)行耸黑,而且結(jié)果竟然還是

image

相信當(dāng)你看到這個結(jié)果的時候會和我一樣吃驚桃煎,不和邏輯啊,怎么竟然能執(zhí)行成功并且還打印出來當(dāng)前controller了大刊,不符合常理啊为迈。

解析

對于計算機(jī)而言,不存在什么魔法缺菌,如果一段代碼能運(yùn)行必然存在它的原理葫辐。

我們需要做的就是分析為什么能成功。

  1. 為什么調(diào)用不崩潰 我們需要了解伴郁, cls的意思耿战。

cls在C語言里,就是一個指針焊傅,這個指針的內(nèi)容指向Spark類

當(dāng)我們通過 void*obj=&cls;這個語句執(zhí)行后剂陡,獲取的就是一個指向這個指針 cls的指針

事實(shí)上在這一步操作實(shí)現(xiàn)后,obj 這個指針就已經(jīng)具有Object-c對象的功能了狐胎,為什么呢鸭栖?接下來我們可以看看runtime實(shí)現(xiàn)原理了,這里我只說一點(diǎn)

  1. //對象

  2. struct objc_object {

  3. Class isa OBJC_ISA_AVAILABILITY;

  4. };

  5. //類

  6. struct objc_class {

  7. Class isa OBJC_ISA_AVAILABILITY;

  8. #if !__OBJC2__

  9. Class super_class OBJC2_UNAVAILABLE;

  10. const char *name OBJC2_UNAVAILABLE;

  11. long version OBJC2_UNAVAILABLE;

  12. long info OBJC2_UNAVAILABLE;

  13. long instance_size OBJC2_UNAVAILABLE;

  14. struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;

  15. struct objc_method_list **methodLists OBJC2_UNAVAILABLE;

  16. struct objc_cache *cache OBJC2_UNAVAILABLE;

  17. struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;

  18. #endif

  19. } OBJC2_UNAVAILABLE;

  20. //方法列表

  21. struct objc_method_list {

  22. struct objc_method_list *obsolete OBJC2_UNAVAILABLE;

  23. int method_count OBJC2_UNAVAILABLE;

  24. #ifdef __LP64__

  25. int space OBJC2_UNAVAILABLE;

  26. #endif

  27. /* variable length structure */

  28. struct objc_method method_list[1] OBJC2_UNAVAILABLE;

  29. } OBJC2_UNAVAILABLE;

  30. //方法

  31. struct objc_method {

  32. SEL method_name OBJC2_UNAVAILABLE;

  33. char *method_types OBJC2_UNAVAILABLE;

  34. IMP method_imp OBJC2_UNAVAILABLE;

  35. }

</pre>

引自: iOS Runtime詳解-簡書

可以看到 objc_object這個對象的首字段是isa 指向一個Class

也就是說握巢,我們?nèi)绻幸粋€指向Class的地址的指針晕鹊,相當(dāng)于這個對象就已經(jīng)可以使用了,只是像他的成員變量等等的一系列值都還沒有被初始化暴浦。

所以接下來用 (__bridge id)obj溅话,調(diào)用是不會產(chǎn)生問題的

  1. 為什么能打印出ViewController對象?

這個問題就是由兩個小部分組成的

  1. 1. name 這個屬性是什么時候賦的值?

  2. 2. ViewController 這個對象是什么時候被傳入的?

</pre>

首先我們需要先了解一下歌焦,一個類對象的數(shù)據(jù)是如何存儲的公荧。

這里我就按照上文一樣引用很多的論證了,我們自己來探究

該上代碼了:

  1. @interface Cls : NSObject

  2. @property(nonatomic,strong) NSString *test;

  3. @property(nonatomic,strong) NSString *test1;

  4. @end

  5. @implementation Cls

  6. - (void)printPrinter {

  7. NSLog(@"self:%p",self);

  8. NSLog(@"self.test:%p",&_test);

  9. NSLog(@"self.test1:%p",&_test1);

  10. }

  11. @end

</pre>

接下來調(diào)用 printPrinter,打印一下對象指針地址:

image

可以發(fā)現(xiàn)同规,指針偏移量成員變量和指針首地址差8個字節(jié)循狰,每個成員變量與上一個成員變量偏移量也是8個字節(jié)窟社。

完成到這一步,我們?nèi)匀粵]有發(fā)現(xiàn)上述兩個問題是應(yīng)該怎么解釋绪钥。但是我們知道了灿里,一個Object-C 對象的指針,和它的成員變量的指針肯定是連續(xù)的程腹。這就為接下來我們的分析提供了一些思路匣吊。

下一步,我在原本的題目中增加一行代碼:

  1. [super viewDidLoad];

  2. NSString *str = @"11111";

  3. id cls = [Spark class];

</pre>

為啥要增加這行代碼呢寸潦,這步是經(jīng)過深(瞎)思(J)熟(B)慮(試)色鸳,主要是考慮到函數(shù)內(nèi)部的參數(shù)生成必然會需要地方存儲,但這部分存儲地址见转,我們是不知曉的命雀,它的實(shí)現(xiàn)是被系統(tǒng)隱藏的。而我們的代碼又沒有明顯的設(shè)置相關(guān)代碼斩箫,那么必然是由這些條件實(shí)現(xiàn)的吏砂。所以當(dāng)我們增加了這一行代碼后,不出意外的乘客,打印結(jié)果變了

2018-11-29 20:49:39.254021+0800 test[1961:92498] My name is:11111

變成了 我們 上述的值狐血,這一切都和猜想的差不多

于是一個基本設(shè)想就出來了:

因?yàn)闂I系牡刂方Y(jié)構(gòu)和原本類的需求地址結(jié)構(gòu)高度重合了,同時所有地址都能訪問到對應(yīng)的值易核。我們通過棧的默認(rèn)行為生成了一個Spark對象!

為了驗(yàn)證匈织,我們打印一下 clsstr的指針堆棧地址

  1. NSLog(@"cls address:%p str address:%p",&cls,&str);

</pre>

2018-11-29 21:03:30.490989+0800 test[2129:122769] cls address:0x7ffeebf4fa00 str address:0x7ffeebf4fa08

我們可以看到他們之間相差也正好是8,而且正好和對象結(jié)構(gòu)體定義的一模一樣牡直。所以這也正好能說明我們上述的打印結(jié)果 Mynameis:11111為什么會發(fā)生报亩。

注:這個存在的原因是因?yàn)楹瘮?shù)內(nèi)部變量采用的小端模式,也就是將參數(shù)地址由棧區(qū)從高地址依次向低地址分配井氢,所以我們打印 cls地址會比 str要小。

由此岳链,第一個小問題就解決了花竞,答案是因?yàn)槲覀冊谏啥褩?shù)的時候,拼湊出了Spark對象的地址數(shù)據(jù)結(jié)構(gòu)格式掸哑,和真正的對象地址數(shù)據(jù)結(jié)構(gòu)一樣约急,所以 self.name就是在生成 cls的那一刻起內(nèi)存地址就已經(jīng)被賦值了。

接下來到下一個問題了ViewController 是什么時候傳入的?

在這一步里我們只能把目光向 cls對象生成前執(zhí)行的操作來看苗分, [superviewDidLoad];我們只執(zhí)行了這一步操作厌蔽,那必然是這個操作產(chǎn)生的結(jié)果。為了驗(yàn)證摔癣,我們可以更改一下調(diào)用順序

  1. id cls = [Cls class];

  2. [super viewDidLoad];

</pre>

當(dāng)我們進(jìn)行這部操作后奴饮,會發(fā)現(xiàn)纬向,執(zhí)行speak方法時崩潰了,錯誤是 EXC_BAC_ACCESS戴卜,說明是我們引用野指針了逾条。

由此也可以證實(shí), [superviewDidLoad];肯定做了一些騷操作投剥,將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,當(dāng)我們調(diào)用 [superviewDidLoad]時啄育,執(zhí)行的方法中傳入了參數(shù) self酌心,由此將 self做為一個值壓入了棧中,但是 _cmd這個參數(shù)并未被使用灸撰,因此谒府,沒有被壓入棧中。

至此浮毯,這個問題已經(jīng)被解釋出來了完疫。

答案

所有NSObject對象的首地址都是指向這個對象的所屬類。這個條件是充要條件债蓝。反過來說壳鹤,如果一個地址指向某個類,我們就可以把這個地址當(dāng)成對象去用饰迹。所以編譯是會通過的芳誓,也不會報 unrecognized selector的錯誤。

打印結(jié)果會是ViewController對象的原因是因?yàn)?cls在棧上的數(shù)據(jù)結(jié)構(gòu)符合了它作為真實(shí)的類時候的數(shù)據(jù)結(jié)構(gòu)啊鸭, cls.name原本地址正好是棧上ViewController對象地址锹淌,因此NSLog能打印出 <ViewController>

思索

這類問題,考察的東西很深赠制,并且結(jié)合了很多知識點(diǎn)赂摆。但是當(dāng)我們拿到面試題并且能進(jìn)行思索的時候一定要好好的考慮,我對這道題的想法钟些,也是在不斷的試驗(yàn)中逐漸的完善烟号,并且嘗試了很多。其實(shí)找面試題為什么是這個答案的過程和政恍,找代碼找bug的流程都是類似的汪拥,都是排除變量,逐步探索篙耗,最終將探索過程和概念結(jié)合迫筑。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末宪赶,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子铣焊,更是在濱河造成了極大的恐慌逊朽,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,681評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件曲伊,死亡現(xiàn)場離奇詭異叽讳,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)坟募,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,205評論 3 399
  • 文/潘曉璐 我一進(jìn)店門岛蚤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人懈糯,你說我怎么就攤上這事涤妒。” “怎么了赚哗?”我有些...
    開封第一講書人閱讀 169,421評論 0 362
  • 文/不壞的土叔 我叫張陵她紫,是天一觀的道長。 經(jīng)常有香客問我屿储,道長贿讹,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,114評論 1 300
  • 正文 為了忘掉前任够掠,我火速辦了婚禮民褂,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘疯潭。我一直安慰自己赊堪,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,116評論 6 398
  • 文/花漫 我一把揭開白布竖哩。 她就那樣靜靜地躺著哭廉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪相叁。 梳的紋絲不亂的頭發(fā)上遵绰,一...
    開封第一講書人閱讀 52,713評論 1 312
  • 那天,我揣著相機(jī)與錄音钝荡,去河邊找鬼。 笑死舶衬,一個胖子當(dāng)著我的面吹牛埠通,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播逛犹,決...
    沈念sama閱讀 41,170評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼端辱,長吁一口氣:“原來是場噩夢啊……” “哼梁剔!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起舞蔽,我...
    開封第一講書人閱讀 40,116評論 0 277
  • 序言:老撾萬榮一對情侶失蹤荣病,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后渗柿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體个盆,經(jīng)...
    沈念sama閱讀 46,651評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,714評論 3 342
  • 正文 我和宋清朗相戀三年朵栖,在試婚紗的時候發(fā)現(xiàn)自己被綠了颊亮。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,865評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡陨溅,死狀恐怖终惑,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情门扇,我是刑警寧澤雹有,帶...
    沈念sama閱讀 36,527評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站臼寄,受9級特大地震影響霸奕,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜脯厨,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,211評論 3 336
  • 文/蒙蒙 一铅祸、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧合武,春花似錦临梗、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,699評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至汤善,卻和暖如春什猖,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背红淡。 一陣腳步聲響...
    開封第一講書人閱讀 33,814評論 1 274
  • 我被黑心中介騙來泰國打工不狮, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人在旱。 一個月前我還...
    沈念sama閱讀 49,299評論 3 379
  • 正文 我出身青樓摇零,卻偏偏與公主長得像,于是被迫代替她去往敵國和親桶蝎。 傳聞我的和親對象是個殘疾皇子驻仅,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,870評論 2 361