Runtime面試題與棧區參數

1. 面試題

朋友發給我一到面試題,問:ios

  1. 下面代碼執行 ⌘+R 後會 Compile ErrorRuntime Crash 或者 NSLog 輸出?
  2. 若是 [(__bridge id)obj speak]; 能調用成功,輸出什麼?
@interface Speaker : NSObject
@property (nonatomic, copy) NSString *name;
- (void)speak;
@end

@implementation Speaker
- (void)speak {
    NSLog(@"Speaker's name: %@", self.name);
}
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    id cls = [Speaker class]; // 1
    void *obj = &cls; // 2
    [(__bridge id)obj speak]; // 3
}
@end
複製代碼

固然,本着 反正不是真面試 的態度,直接跑一下不就好了,嘿嘿。面試

//輸出
Speaker's name: <ViewController: 0x7fcc84e09e90>
複製代碼

能夠看到運行時成功的,但輸出的結果讓我有點懵逼???緣由有2點:shell

  1. 爲何 [(__bridge id)obj speak] 不會崩潰,並且感受看着像給 類對象發消息 ,這應該解析不了啊?
  2. 爲何 self.nameViewController對象?

下面咱們仔細分析一下。iphone

2. 分析

2.1 爲何能夠發消息?

  • 第一步函數

    id cls = [Speaker class]; // 1
    複製代碼

    這一步獲取到了Speaker的類對象,id表示將其轉換爲一個對象指針,實際類型爲struct objc_object *flex

    struct objc_object {
        Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    };
    
    typedef struct objc_object *id;
    複製代碼

    [Speaker class] 的返回類型爲Class,其實類型爲struct objc_class *ui

    typedef struct objc_class *Class;
    複製代碼

    雖然,咱們寫的類型爲struct objc_object *,但其本質仍是struct objc_class *atom

    id cls = [Speaker class];
    if (object_isClass(cls)) {
        NSLog(@"object_isClass");
    }
    // 輸出
    object_isClass
    複製代碼

    也就是說這一步拿到的 本質仍是類對象spa

    id cls = [Speaker class];
    [cls speak];
    // 直接發送消息,是會崩潰的
    +[Speaker speak]: unrecognized selector sent to class 0x106824f08
    複製代碼
  • 第二步、第三步指針

    void *obj = &cls; // 2
    複製代碼

    這一步纔是關鍵。

    struct objc_object {
        Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    };
    複製代碼

    能夠看到struct objc_object這個結構體的首字段是 isa 指向一個Class

    也就是說,咱們若是有一個指向Class的地址的指針,至關於這個對象就已經可使用了。

    @interface Speaker : NSObject
    @property (nonatomic, copy) NSString *name;
    - (void)speak;
    @end
    
    @implementation Speaker
    - (void)speak {
        NSLog(@"speak");
    }
    @end
    
    struct my_object {
        Class isa;
    };
    
    struct my_object *getObject() {
        // id cls = [Speaker class]; id類型的實質是一個指針,因此cls是一個指針
        // void *obj = &cls; 這裏取cls的地址,至關於[Speaker class]如今被一個 指針 的 指針 所指向
        // 下面 struct my_object * 是一個指針,isa 是一個也是一個指針
        // 因此也等效於[Speaker class]如今被一個 指針 的 指針 所指向
        struct my_object *obj = (struct my_object *)malloc(sizeof(struct my_object));
        obj->isa = [Speaker class];
        return obj;
    }
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        struct my_object *obj = getObject();
        id obj1 = (__bridge id)obj;
        [obj1 speak]; // 3
        free(obj);
    }
    複製代碼

    咱們能夠看到,經過id類型轉換obj1也被Xcode識別爲了Speaker實例對象,並且咱們調用 [obj1 speak] 也順利輸出了。

    至關於消息 objc_msgSend 執行過程當中經過 obj1 順利訪問到了 isa 對象,在Speaker類中找到了speak實例方法,併成功調用。

2.2 爲何輸出的name是ViewController實例對象?

2.2.1 等價代碼

#import <UIKit/UIKit.h>
#import <objc/runtime.h>

@interface Speaker : NSObject
@property (nonatomic, copy) NSString *name;
- (void)speak;
@end
@implementation Speaker
- (void)speak {
    NSLog(@"my name's %@", self.name);
}
@end

@interface ViewController : UIViewController
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    id cls = [Speaker class];
    void *obj = &cls;
    [(__bridge id)obj speak];
}
@end
複製代碼

咱們將這個 ViewController.m 文件編譯爲 ViewController.cpp 來看一下。

終端 中切換到 ViewController.m 所在目錄,並輸入如下命令:

xcrun -sdk iphoneos clang -arch arm64 -w -rewrite-objc -fobjc-arc -mios-version-min=8.0.0 -fobjc-runtime=ios-8.0.0 ViewController.m
複製代碼

執行完畢後咱們能夠在同一個目錄下找到 ViewController.cpp 文件。

打開 ViewController.cpp ,並搜索 ViewController_viewDidLoad 便可找到下面的方法:

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"));
    id cls = ((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Speaker"), sel_registerName("class"));
    void *obj = &cls;
    ((void (*)(id, SEL))(void *)objc_msgSend)((id)(__bridgeid)obj, sel_registerName("speak"));
}
複製代碼

看起來有點複雜,咱們把非必要的格式轉換去掉:

static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
    objc_msgSendSuper((__rw_objc_super){self, class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad")); // 1
    id cls = objc_msgSend(objc_getClass("Speaker"), sel_registerName("class")); // 2
    void *obj = &cls; // 3
    objc_msgSend(obj, sel_registerName("speak")); // 4
}
複製代碼

能夠看到:

  1. 對應 [super viewDidLoad]
  2. 對應 id cls = [Speaker class];
  3. 對應 void *obj = &cls;
  4. 對應 [(__bridge id)obj speak];

objc_msgSend 會傳入兩個隱式參數self_cmd,想必你們已經很熟悉了。

objc_msgSend(void /* id self, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
複製代碼

objc_msgSendSuper 須要傳入另外一個結構體 struct objc_super *

/// Specifies the superclass of an instance. 
struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull 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 _Nonnull Class class;
#else
    __unsafe_unretained _Nonnull Class super_class;
#endif
    /* super_class is the first class to search */
};
複製代碼

{self, class_getSuperclass(objc_getClass("ViewController"))} 實際上就是在初始化一個struct objc_super結構體。

知道這些以後,再閱讀上面的代碼就沒有什麼難度了。

2.2.2 參數順序

void sum(NSNumber *a, NSNumber *b) {
    NSLog(@"a地址 = %p", &a);
    NSLog(@"b地址 = %p", &b);
    printf("%d", a.intValue + b.intValue);
}

- (void)viewDidLoad {
    [super viewDidLoad];
    sum(@(1), @(2));
    NSNumber *c = @(4);
    NSLog(@"c地址 = %p", &c);
}
複製代碼

咱們在給函數傳入參數時,參數會做爲自動變量入棧 :

並且咱們能夠看到入棧的順序是a先入棧,b後入棧,由於 棧從高地址到低地址分配內存

可是在初始化一個結構體的時候,這個順序是相反的:

咱們看到 two_number tn = {@(1), @(2)}; 先傳入的是1後傳入的2,但實際狀況是2先入棧,1後入棧。

按照上面2條規則,下面代碼第5步以前的變量入棧的順序應該是:

static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) { // 1
    objc_msgSendSuper((__rw_objc_super){self, class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad")); // 2
    id cls = objc_msgSend(objc_getClass("Speaker"), sel_registerName("class")); // 3
    void *obj = &cls; // 4
    objc_msgSend(obj, sel_registerName("speak")); // 5
}
複製代碼
  1. self_cmd爲函數的隱式參數,依次先入棧。

  2. objc_msgSendSuper 初始化了一個結構體,這個結構體的參數會入棧。

    又由於參數入棧是從右到左的順序入棧:

    • class_getSuperclass(objc_getClass("ViewController"))
    • self後入棧
  3. cls本地變量賦值爲Speaker類,最後入棧

那麼入棧的順序爲self_cmdclass_getSuperclass(objc_getClass("ViewController"))selfSpeaker類。下面咱們驗證一下:

@interface Speaker : NSObject
@property (nonatomic, copy) NSString *name;
- (void)speak;
@end

@implementation Speaker
- (void)speak {
    NSLog(@"Speaker self: %p, _name: %p", self, &_name);
    NSLog(@"Speaker's name: %@", self.name);
}
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    id cls = [Speaker class]; // 1
    void *obj = &cls; // 2
    NSLog(@"棧區變量");
    void *start = (void *)&self;
    void *end = (void *)&obj;
    long count = (start - end) / 0x8;
    for (long i = 0; i < count; i++) {
        void *address = start - 0x8 * i;
        if (i == 1) {
            NSLog(@"%p: %s", address, *(char **)(address));
        } else {
            NSLog(@"%p: %@", address, *(void **)address);
        }
    }
    NSLog(@"obj speak");
    [(__bridge id)obj speak]; // 3
}
@end
// 打印
Demo[32768:1105890] 棧區變量
Demo[32768:1105890] 0x7ffeec17c648: <ViewController: 0x7fb445607ee0>
Demo[32768:1105890] 0x7ffeec17c640: viewDidLoad
Demo[32768:1105890] 0x7ffeec17c638: ViewController //這裏比較怪
Demo[32768:1105890] 0x7ffeec17c630: <ViewController: 0x7fb445607ee0>
Demo[32768:1105890] 0x7ffeec17c628: Speaker
Demo[32768:1105890] obj speak
Demo[32768:1105890] Speaker self: 0x7ffeec17c628, _name: 0x7ffeec17c630
Demo[32768:1105890] Speaker's name: <ViewController: 0x7fb445607ee0>
複製代碼

從輸出能夠看到,棧區的打印順序和咱們的分析基本吻合。

下面咱們看一下爲何Speaker實例對象的 self.name 訪問到的是ViewController實例對象。

  • Speaker實例對象,若是咱們經過 [[Speaker alloc] init] 初始化的話,會在堆區分配內存。但如今,咱們是使用棧區指針指向了Speaker類對象地址,"假裝"成了一個Speaker實例對象,因此傳入的self值爲棧區的地址:0x7ffeec17c628

  • 從上面的輸出咱們能夠看到,name屬性的實例變量_nameSpeaker實例對象 self + 0x8 的地址,即 0x7ffeec17c630

  • 根據輸出_name實例變量訪問的地址 0x7ffeec17c630 ,找到棧區對應的數據 0x7ffeec17c630: <ViewController: 0x7fb445607ee0> ,因此輸出爲 Speaker's name: <ViewController: 0x7fb445607ee0>

3. 總結

經過這個面試題咱們得出了一下結論:

  • Objective-C中的對象是一個指向class_object地址的變量,即 id obj = &class_object

  • 對象的實例變量 void *ivar = &obj + offset(N)

但這裏還有一個疑問:

咱們看到直接調用 [super viewDidLoad]; ,棧區的第3個變量爲ViewController類。

但根據咱們用Clang重寫的代碼 [super viewDidLoad]; 實現作替換:

- (void)viewDidLoad {
    ((void (*)(struct objc_super *, SEL))(void *)objc_msgSendSuper)(&((struct objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}), sel_registerName("viewDidLoad"));
    
    id cls = [Speaker class]; // 1
    void *obj = &cls; // 2
    NSLog(@"棧區變量");
    void *start = (void *)&self;
    void *end = (void *)&obj;
    long count = (start - end) / 0x8;
    for (long i = 0; i < count; i++) {
        void *address = start - 0x8 * i;
        if (i == 1) {
            NSLog(@"%p: %s", address, *(char **)(address));
        } else {
            NSLog(@"%p: %@", address, *(void **)address);
        }
    }
    NSLog(@"obj speak");
    [(__bridge id)obj speak]; // 3
}
// 輸出
Demo[33008:1114325] 棧區變量
Demo[33008:1114325] 0x7ffee4983648: <ViewController: 0x7f9e0bf07fd0>
Demo[33008:1114325] 0x7ffee4983640: viewDidLoad
Demo[33008:1114325] 0x7ffee4983638: UIViewController // 這裏符合預期
Demo[33008:1114325] 0x7ffee4983630: <ViewController: 0x7f9e0bf07fd0>
Demo[33008:1114325] 0x7ffee4983628: Speaker
Demo[33008:1114325] obj speak
Demo[33008:1114325] Speaker self: 0x7ffee4983628, _name: 0x7ffee4983630
Demo[33008:1114325] Speaker's name: <ViewController: 0x7f9e0bf07fd0>
複製代碼

咱們看到棧區的第3個變量爲UIViewController類,這個輸出是符合預期的,由於class_getSuperclass(objc_getClass("ViewController"))咱們獲取的就是父類。

但爲何直接調用 [super viewDidLoad]; ,棧區的第3個變量爲ViewController類,這個問題難道是Xcode的Bug???


若是以爲本文對你有所幫助,給我點個贊吧~ 👍🏻

相關文章
相關標籤/搜索