一道值得思考的iOS面試題

前言

最近在羣裏看到有人發的一道面試題,題目以下: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

  1. 爲何調用不崩潰 咱們須要瞭解,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. 爲何能打印出ViewController對象?

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

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對象!

爲了驗證,咱們打印一下clsstr的指針堆棧地址

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/

謝謝您的閱讀

本文同步發行於本人博客 未經受權不得隨意使用

相關文章
相關標籤/搜索