iOS探索 isa面試題分析

歡迎閱讀iOS探索系列(按序閱讀食用效果更加)c++

寫在前面

本文涉及的面試題以下:面試

  • isKindOfClassisMemberOfClass的區別
  • [self class][super class]的區別
  • isa綜合運用——內存偏移

1、isKindOfClass 和 isMemberOfClass

這是一道涉及isa走位圖的面試題,大膽猜想下結果bash

#import <Foundation/Foundation.h>
#import "FXPerson.h"
#import <objc/runtime.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];//1
        BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];// 0
        BOOL re3 = [(id)[FXPerson class] isKindOfClass:[FXPerson class]];//0
        BOOL re4 = [(id)[FXPerson class] isMemberOfClass:[FXPerson class]];// 0
        NSLog(@"\n re1 :%hhd\n re2 :%hhd\n re3 :%hhd\n re4 :%hhd\n",re1,re2,re3,re4);

        BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];//1
        BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];// 1
        BOOL re7 = [(id)[FXPerson alloc] isKindOfClass:[FXPerson class]];//1
        BOOL re8 = [(id)[FXPerson alloc] isMemberOfClass:[FXPerson class]];// 1
        NSLog(@"\n re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);
    }
    return 0;
}
複製代碼

這裏先不揭曉答案,先來探索一下isKindOfClassisMemberOfClass的實現函數

+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}
複製代碼
  • 這是一個相似於for (int i = 0; i < 3; i ++)的for循環
    • object_getClass獲得當前類對象的類——元類,初始化tcls
    • 只要tcls有值就能夠繼續循環,即當tclsnil時結束for循環
    • 取得tcls的父類,做爲它的新值,繼續下一次循環
  • 當for循環中有一次tcls == cls,返回YES
  • 結束for循環時還沒知足條件就返回NO

結論一:+isKindOfClass是元類及其父類 vs 類post

+ (BOOL)isMemberOfClass:(Class)cls {
    return object_getClass((id)self) == cls;
}
複製代碼
  • object_getClass獲得當前類對象的類——元類,和類自己cls進行比較
  • 相較於+isKindOfClass少了父類的比較,所以+isMemberOfClass爲YES時能夠獲得+isKindOfClass爲YES

結論二:+isMemberOfClass是元類 vs 類ui

結合 isa走位圖(實線爲父類走向)能夠得出前面四個打印結果:

  • NSObject元類NSObject類不相等,NSObject元類的父類(指向NSObject類)與NSObject類相等——YES
  • NSObject元類NSObject類不相等——NO
  • FXPerson元類FXPerson類不相等,FXPerson元類的父類FXPerson類不相等——NO
  • FXPerson元類FXPerson類不相等——NO

換成實例對象調用-isKindOfClass-isMemberOfClassatom

- (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}
複製代碼

同理可得:-isMemberOfClass是拿實例對象的類(即當前類)和cls做比較,-isKindOfClass多了一步for循環類對象的父類spa

結論三:-isKindOfClass是類自己及其父類 vs 類設計

結論四:-isMemberOfClass是類自己 vs 類3d

後面四個結果分析以下:

  • NSObject類NSObject類相等——YES
  • NSObject類NSObject類相等——YES
  • FXPerson類FXPerson類相等——YES
  • FXPerson類FXPerson類相等——YES

2、[self class] 和 [super class]

FXSon繼承於FXFather,主程序初始化FXSon,求問打印內容以及思路

#import "FXSon.h"

@implementation FXSon

- (instancetype)init {
    self = [super init];
    if (self) {
        NSLog(@"[self class] = %@", NSStringFromClass([self class]));
        NSLog(@"[super class] = %@", NSStringFromClass([super class]));
        NSLog(@"[self superclass] = %@", NSStringFromClass([self superclass]));
        NSLog(@"[super superclass] = %@", NSStringFromClass([super superclass]));
    }
    return self;
}

@end
複製代碼

打印結果以下

emmm...有點出乎意料, [self class]點進去來到 NSObject.mm文件查看源碼

  • class方法返回類
  • superclass方法返回類的父類
+ (Class)class {
    return self;
}

- (Class)class {
    return object_getClass(self);
}

+ (Class)superclass {
    return self->superclass;
}

- (Class)superclass {
    return [self class]->superclass;
}
複製代碼

從這段代碼能解釋[self class][self superclass],可是另外兩個又怎麼解釋呢?

終端clang編譯代碼獲得super.cpp,就能看到初始化的底層代碼了

clang -rewrite-objc FXSon.m -o super.cpp
複製代碼

已知調用方法就是發送消息 objc_msgSend,那 objc_msgSendSuper也是發送消息嗎?

查看源碼中對objc_msgSendSuper的定義,註釋中提示了objc_super

objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
    
/// Specifies the superclass of an instance. 
struct objc_msgSendSuper {
    /// 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 */
};
複製代碼

從以上源碼得出,使用objc_msgSendSuperobjc_super發送消息,而objc_superobjc2.0下有兩個元素——id類型的receiverClass類型的super_class

其實早在iOS探索 方法的本質和消息查找流程中就提過這個方法,而後筆者進行了仿寫

記得導入<objc/message.h>,報錯Too many arguments就去修改編譯期配置

從新運行就能獲得與 [super class]同樣的結果了

那蘋果爲何要這麼設計呢?把消息查找isa走位圖聯繫起來就明白了!

son實例對象實例方法存在 FXSon類

  • 調用[self class]就是son照着FXSon->FXFather->NSObject順序問老爸要-class方法
  • 調用[super class]就是son跳過FXSon,直接經過FXFather->NSObject查找
  • 還有比[super class]更快找到class方法的寫法
    • 結構體中[self class]改成[super class],直接找到NSObjct

補充: 當結構體中[self class]改成[FXFather class]時,由於類方法存在元類中,會按FXFather元類->NSObject元類->NSObject根元類+class方法,最後也是會輸出FXSon

結論:

  • [self class]就是發送消息objc_msgSend,消息接收者是self,方法編號是class
  • [super class]就是發送消息objc_msgSendSuper,消息接收者是self,方法編號是class,只不過objc_msgSendSuper會跳過self的查找

3、內存偏移

這是一道比較經典的「喪心病狂」的內存偏移面試題,若是你沒有研究過,大機率很難答上來

1.原始題

程序可否運行嗎?是否正常輸出?

#import "ViewController.h"

@interface FXPerson : NSObject

@property (nonatomic, copy) NSString *name;

@end

@implementation FXPerson

- (void)printMyProperty {
    NSLog(@"當前打印內容爲%s", __func__);
}

@end

//——————————————————————————————————————//

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    id cls = [FXPerson class];
    void *obj= &cls;
    [(__bridge id)obj printMyProperty];
    
    FXPerson *p = [FXPerson new];
    [p printMyProperty];
}

@end
複製代碼

運行結果與普通初始化對象如出一轍,可面試的時候不可能只說能或不能,還要說出個因此然來

正常初始化:指針p->實例對象isa->類對象

  • 對象的本質爲objc_object,第一個元素爲isa
  • 指針p存儲着FXPerson類實例出來的對象內存地址,因此指針p指向對象的首地址——p->實例對象isa
  • 實例對象的isa指向類對象——實例對象isa->類對象

騷操做:指針obj->指針cls->類對象

  • id cls = [LGPerson class]獲取到類對象指針
  • void *obj= &cls獲取到指向該類對象cls的對象obj

2.拓展一

修改打印方法printMyProperty——不但打印方法,同時打印屬性name

- (void)printMyProperty {
    NSLog(@"當前打印內容爲%s——————%@", __func__, self.name);
}
複製代碼

從新運行代碼,獲得結果以下

當前打印內容爲-[FXPerson printMyProperty]——————<ViewController: 0x7fc72cd09450>
當前打印內容爲-[FXPerson printMyProperty]——————(null)
複製代碼

爲何屬性name尚未賦值,卻打印出了ViewController的內存地址?

  • 因爲棧先入後出viewDidLoad入棧先拉伸棧空間,而後依次放入self、_cmd局部變量
  • 調用[super viewDidLoad],繼續放入super_class、self
  • 正常狀況下獲取name,本質是p的內存地址往下偏移8字節
  • 一樣的騷操做也是obj的內存地址往下偏移8字節獲得self

3.拓展二

修改viewDidLoad——在obj前面加個臨時字符串變量

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSString *temp = @"1";
    id cls = [FXPerson class];
    void *obj= &cls;
    [(__bridge id)obj printMyProperty];
    
    FXPerson *p = [FXPerson alloc];
    [p printMyProperty];
}
複製代碼

從新運行代碼,獲得結果以下

當前打印內容爲-[FXPerson printMyProperty]——————1
當前打印內容爲-[FXPerson printMyProperty]——————(null)
複製代碼

一樣道理,在obj入棧前已經有了temp變量,此時訪問self.name就會訪問到temp

4.拓展三

去掉臨時變量,FXPerson類新增字符串屬性hobby,打印方法改成打印hobby,運行

ViewController就是 obj偏移16字節拿到的 super_class([super viewDidLoad]壓棧進去的)

5.拓展四

①去掉[super viewDidLoad],運行

②臨時變量改爲NSInteger類型

這兩種狀況就是野指針——指針偏移的offset不正確,獲取不到對應變量的首地址

6.萬變不離其宗的理論

int a = 1;
int b = 2;
int c = 3;
int d = 4;
NSLog(@"\na = %p\nb = %p\nc = %p\nd = %p\n",&a,&b,&c,&d);
複製代碼

打印結果

a = 0x7ffee0ebd1bc
b = 0x7ffee0ebd1b8
c = 0x7ffee0ebd1b4
d = 0x7ffee0ebd1b0
複製代碼

局部變量的存放順序,是根據定義的前後順序,從函數棧底(高地址)開始,一個一個排列

關於這題還沒搞明白的能夠看下Runtime筆記(八)—— 面試題中的Runtime裏面的圖很形象

寫在後面

面試題是面試官用知識點變着法玩你的一種手段,同時也能表現出你掌握知識的熟練度。只有在平時多練習多研究,才能在面試的時候給面試官留下一個好的印象

相關文章
相關標籤/搜索