Runtime 系列文章
深入淺出 Runtime(一):初識
深入淺出 Runtime(二):資料結構
深入淺出 Runtime(三):訊息機制
深入淺出 Runtime(四):super 的本質
深入淺出 Runtime(五):具體應用
深入淺出 Runtime(六):相關面試題
Q:你瞭解 isa 指標嗎?
isa
指標用來維護物件和類之間的關係,並確保物件和類能夠通過isa
指標找到對應的方法、例項變數、屬性、協議等;- 在 arm64 架構之前,
isa
就是一個普通的指標,直接指向objc_class
,儲存著Class
、Meta-Class
物件的記憶體地址。instance
物件的isa
指向class
物件,class
物件的isa
指向meta-class
物件; - 從 arm64 架構開始,對
isa
進行了優化,變成了一個共用體(union
)結構,還使用位域來儲存更多的資訊。將 64 位的記憶體資料分開來儲存著很多的東西,其中的 33 位才是拿來儲存class
、meta-class
物件的記憶體地址資訊。要通過位運算將isa
的值& ISA_MASK
掩碼,才能得到class
、meta-class
物件的記憶體地址; isa
指標儲存的資訊;isa
指標的指向。
傳送門:深入淺出 Runtime(二):資料結構
Q:類物件與元類物件的區別和聯絡。
class
、meta-class
底層結構都是objc_class
結構體,objc_class
繼承自objc_object
,所以它也有isa
指標,它也是物件;class
中儲存著例項方法、成員變數、屬性、協議等資訊,meta-class
中儲存著類方法等資訊;isa
指標和superclass
指標的指向;- 基類的
meta-class
的superclass
指向基類的class
,決定了一個性質:
當我們呼叫一個類方法,會通過class
的isa
指標找到meta-class
,在meta-class
中查詢有無該類方法,如果沒有,再通過meta-class
的superclass
指標逐級查詢父meta-class
,一直找到基類的meta-class
如果還沒找到該類方法的話,就會去找基類的class
中同名的例項方法的實現。
Q:為什麼要設計 meta-class ?
目的是將例項和類的相關方法列表以及構建資訊區分開來,方便各司其職,符合單一職責設計原則。
Q:Runtime 的訊息機制,objc_msgSend 方法呼叫流程。
OC
中的方法呼叫,其實都是轉換為objc_msgSend()
函式的呼叫(不包括[super message]
)。objc_msgSend()
的執行流程可以分為 3 大階段:訊息傳送、動態方法解析、訊息轉發。
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
class
和superclass
方法的實現在 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
:為屬性生成下劃線成員變數,並且自動生成setter
和getter
方法的實現。以前 Xcode 還沒這麼智慧的時候就要這麼做。而現在預設我們寫的屬性,會自動進行@synthesize
。
有時候我們不希望它自動生成,而是在程式執行過程中再去決定該方法的實現,就可以使用@dynamic
。@dynamic
:是告訴編譯器不用自動生成setter
和getter
的實現,不用自動生成成員變數,等到執行時再新增方法實現,但是它不會影響setter
和getter
方法的宣告。- 動態執行時語言與編譯時語言的區別:動態執行時語言將函式決議推遲到執行時,編譯時語言在編譯器進行函式決議。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
以下是isMemberOfClass
和isKindOfClass
方法以及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-class
的superclass
指向它的class
物件,所以它滿足isKindOfClass
方法的判斷條件。