深入淺出 Runtime(六):相關面試題

師大小海騰發表於2020-02-25

Runtime 系列文章

深入淺出 Runtime(一):初識
深入淺出 Runtime(二):資料結構
深入淺出 Runtime(三):訊息機制
深入淺出 Runtime(四):super 的本質
深入淺出 Runtime(五):具體應用
深入淺出 Runtime(六):相關面試題

深入淺出 Runtime(六):相關面試題

Q:你瞭解 isa 指標嗎?

  • isa指標用來維護物件和類之間的關係,並確保物件和類能夠通過isa指標找到對應的方法、例項變數、屬性、協議等;
  • 在 arm64 架構之前,isa就是一個普通的指標,直接指向objc_class,儲存著ClassMeta-Class物件的記憶體地址。instance物件的isa指向class物件,class物件的isa指向meta-class物件;
  • 從 arm64 架構開始,對isa進行了優化,變成了一個共用體(union)結構,還使用位域來儲存更多的資訊。將 64 位的記憶體資料分開來儲存著很多的東西,其中的 33 位才是拿來儲存classmeta-class物件的記憶體地址資訊。要通過位運算將isa的值& ISA_MASK掩碼,才能得到classmeta-class物件的記憶體地址;
  • isa指標儲存的資訊;
  • isa指標的指向。
    傳送門:深入淺出 Runtime(二):資料結構

Q:類物件與元類物件的區別和聯絡。

  • classmeta-class底層結構都是objc_class結構體,objc_class繼承自objc_object,所以它也有isa指標,它也是物件;
  • class中儲存著例項方法、成員變數、屬性、協議等資訊, meta-class中儲存著類方法等資訊;
  • isa指標和superclass指標的指向;
  • 基類的meta-classsuperclass指向基類的class,決定了一個性質:
    當我們呼叫一個類方法,會通過classisa指標找到meta-class,在meta-class中查詢有無該類方法,如果沒有,再通過meta-classsuperclass指標逐級查詢父meta-class,一直找到基類的meta-class如果還沒找到該類方法的話,就會去找基類的class中同名的例項方法的實現。
    isa 與 superclass 指標指向

Q:為什麼要設計 meta-class ?

目的是將例項和類的相關方法列表以及構建資訊區分開來,方便各司其職,符合單一職責設計原則。

Q:Runtime 的訊息機制,objc_msgSend 方法呼叫流程。

傳送門:深入淺出 Runtime(三):訊息機制

OC中的方法呼叫,其實都是轉換為objc_msgSend()函式的呼叫(不包括[super message])。objc_msgSend()的執行流程可以分為 3 大階段:訊息傳送、動態方法解析、訊息轉發。

深入淺出 Runtime(六):相關面試題

Q:呼叫以下 init 方法的列印結果是什麼?(super)

@interface HTPerson : NSObject
@end

@interface HTStudent : HTPerson
@end

@implementation HTStudent
- (instancetype)init
{
    if (self = [super init]) {
        
        NSLog(@"[self class] = %@",[self class]);
        NSLog(@"[super class] = %@",[super class]);
        NSLog(@"[self superclass] = %@",[self superclass]);
        NSLog(@"[super superclass] = %@",[super superclass]);
        
    }
    return self;
}
@end
複製程式碼

[self class] = HTStudent
[super class] = HTStudent
[self superclass] = HTPerson
[super superclass] = HTPerson

classsuperclass方法的實現在 NSObject 類中,可以看到它們的返回值取決於receiver

+ (Class)class {
    return self;
}
- (Class)class {
    return object_getClass(self);
}
+ (Class)superclass {
    return self->superclass;
}
- (Class)superclass {
    return [self class]->superclass;
}
複製程式碼

[self class]是從receiverClass開始查詢方法的實現,如果沒有重寫的情況,則會一直找到基類 NSObject,然後呼叫。
[super class]是從receiverClass->superclass開始查詢方法的實現,如果沒有重寫的情況,則會一直找到基類 NSObject,然後呼叫。
由於receiver相同,所以它們的返回值是一樣的。

Q:如何防止“呼叫無法識別的方法導致應用程式崩潰”?

實現doseNotRecognizeSelector方法。

Q:@synthesize 和 @dynamic

  • @synthesize :為屬性生成下劃線成員變數,並且自動生成settergetter方法的實現。以前 Xcode 還沒這麼智慧的時候就要這麼做。而現在預設我們寫的屬性,會自動進行@synthesize
    有時候我們不希望它自動生成,而是在程式執行過程中再去決定該方法的實現,就可以使用@dynamic
  • @dynamic :是告訴編譯器不用自動生成settergetter的實現,不用自動生成成員變數,等到執行時再新增方法實現,但是它不會影響settergetter方法的宣告。
  • 動態執行時語言與編譯時語言的區別:動態執行時語言將函式決議推遲到執行時,編譯時語言在編譯器進行函式決議。OC 是動態執行時語言。

Q:能否向編譯後的類增加例項變數?能否向執行時動態建立的類增加例項變數?

  • 不能向編譯後的類增加例項變數。類的記憶體佈局在編譯時就已經確定,類的例項變數列表儲存在class_ro_t結構體裡,編譯時就確定了記憶體大小無法修改,所以不能向編譯後的類增加例項變數。
  • 能向執行時動態建立的類增加例項變數。執行時動態建立的類只是通過alloc分配了類的記憶體空間,沒有對類進行記憶體佈局,記憶體佈局是在類初始化過程中完成的,所以能向執行時動態建立的類增加例項變數。
    需要注意的是,要在呼叫註冊類的方法之前去完成例項變數的新增,因為註冊類的時候,類的結構就生成了。說白了就是class_addIvar()函式不能給已經存在的類動態新增成員變數。
    // 動態建立一對類和元類(引數:父類,類名,額外的記憶體空間)
    Class newClass = objc_allocateClassPair([NSObject class], "Person", 0);
    // 動態新增成員變數
    class_addIvar(newClass, "_age", 4, 1, @encode(int));
    class_addIvar(newClass, "_name", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));
    // 註冊一對類和元類(要在類註冊之前新增成員變數)
    objc_registerClassPair(newClass);
    // 建立例項
    id person = [[newClass alloc] init];
    [person setValue:@"Lucy" forKey:@"name"];
    [person setValue:@"20" forKey:@"age"];  
    NSLog(@"name:%@, age:%@", [person valueForKey:@"name"], [person valueForKey:@"age"]);    
    // 當類和它的子類的例項存在時,不能呼叫 objc_disposeClassPair(),否則會 Crash:Attempt to use unknown class 0x1005af5c0.
    person = nil;    
    // 銷燬一對類和元類
    objc_disposeClassPair(newClass);

    // name:Lucy, age:20
複製程式碼

Q:你是否有使用過 performSelector: 方法?

使用場景:一個類在編譯時沒有這個方法,在執行的時候才產生了這個方法,這個時候要呼叫這個方法就要用到performSelector:方法。
關於動態新增方法的實現可以檢視:傳送門:深入淺出 Runtime(三):訊息機制

Q:以下列印結果是什麼?(isKindOfClass & isMemberOfClass)

@interface Person : NSObject
@end
......
    BOOL res1 = [[NSObject class] isKindOfClass:[NSObject class]];
    BOOL res2 = [[NSObject class] isMemberOfClass:[NSObject class]];
    BOOL res3 = [[Person class] isKindOfClass:[Person class]];
    BOOL res4 = [[Person class] isMemberOfClass:[Person class]];

    NSLog(@"%d,%d,%d,%d", res1, res2, res3, res4);
......
複製程式碼

列印結果:1,0,0,0
以下是isMemberOfClassisKindOfClass方法以及object_getClass()函式的實現。

+ (BOOL)isMemberOfClass:(Class)cls {
    return object_getClass((id)self) == cls;
}

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

+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

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

Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}
複製程式碼
  • isMemberOfClass方法是判斷當前instance/class物件的isa指向是不是class/meta-class物件型別;
  • isKindOfClass方法是判斷當前instance/class物件的isa指向是不是class/meta-class物件或者它的子類型別。

顯然isKindOfClass的範圍更大。如果方法呼叫著是instance物件,傳參就應該是class物件。如果方法呼叫著是class物件,傳參就應該是meta-class物件。所以res2-res4都為 0。那為什麼res1為 1呢?
因為 NSObject 的class的物件的isa指向它的meta-class物件,而它的meta-classsuperclass指向它的class物件,所以它滿足isKindOfClass方法的判斷條件。

相關文章