0x00
在OC中, isa是一個很重要的結構體(參考文章), OC runtime正是通過isa來確定object的類型。跟method-swizzling一樣, 在程序運行的過程中, 我們可以通過runtime方法來改變object的isa, 這種技術叫isa-swizzling。
0x01
isa-swizzling最著名的應用場景, 就是KVO。 當class A的某個實例的屬性被監聽, 系統會自動創建class A的子類NSKVONotifying_A
, 重寫對應屬性的setter方法,加入對外通知屬性變化的代碼。但是這個子類NSKVONotifying_A
對外界是不可見的, 我們通過運行時看到的也只是class A, 這是怎么實現的呢? 我們可以參考一下這篇文章. 摘錄主要部分如下:
///------ "addObserver:forKeyPath:options:context:"方法的實現 -----
/// 動態創建子類
NSString *newName = [@"PYKVONotifying_" stringByAppendingString:NSStringFromClass(object_getClass(self))];
NSString *setterName = [[@"set" stringByAppendingString:[[[keyPath uppercaseString] substringToIndex:1] stringByAppendingString:[keyPath substringFromIndex:1]]] stringByAppendingString:@":"];
Class subCls = objc_allocateClassPair(object_getClass(self), [newName UTF8String], 0);
class_addMethod(subCls, NSSelectorFromString(setterName), (IMP)DefaultSetterForKVO, "v@:@");
objc_registerClassPair(subCls);
/// 替換子類的isa
object_setClass(self, subCls);
更詳細的信息可以參考蘋果官方文檔.
0x02
一般的文章, 對于isa-swizzling的介紹, 也就僅僅限于KVO的實現原理, 貌似這種技術,也就是蘋果為了實現KVO而搞出來的一個補丁而已。 事實上, 在實際app開發中, isa-swizzling也可以有用武之地。
考慮這樣一種情況:
如果你開發了一個業務組件, 需要給外部提供服務, 但是你又希望盡可能的對外隱藏具體業務實現, 不希望外界可以通過runtime方式獲取內部實現, 那么isa-swizzling就是一個不錯的選擇方案。
舉例如下:
/// ========= 模擬業務組件內部邏輯 =========
/// 定義公開給外部組件的protocol
@protocol MyBusinessModel<NSObject>
@property(nonatomic, assign) NSInteger modelID;
@property(nonatomic, copy) NSString *modelName;
@end
/// 定義業務protocol實現類,
/// 該類并不會公開給外部組件, 但是外部組件通過runtime可以發現。
@interface MyBusinessModel : NSObject<MyBusinessModel>
@end
@implementation MyBusinessModel
@synthesize modelID, modelName;
@end
/// 定義業務protocol的內部實現類。
/// 該類并不公開給外部組件, 并且也不希望外部組件通過runtime獲取到。
/// 組件內部業務邏輯都隱藏在這個類中。
@interface MyBusinessModelInternal : MyBusinessModel
- (void)internalBusiness;
@end
@implementation MyBusinessModelInternal
/// 模擬需要隱藏的組件內部業務邏輯
- (void)internalBusiness {
NSLog(@"%@; id: %ld, name: %@", self, self.modelID, self.modelName);
}
@end
///========== 模擬外部組件 =========
void thirdpartyComponent(id<MyBusinessModel> param) {
///模擬外部組件正常業務邏輯
NSLog(@"param: %@", param);
///模擬外部組件試圖通過運行時調用組件內部私有邏輯
@try {
[param performSelector:@selector(internalBusiness)];
} @catch (NSException *exception) {}
}
/// ========== 測試代碼 =========
/// 組件內部業務邏輯
MyBusinessModelInternal *model = [[MyBusinessModelInternal alloc] init];
model.modelID = 1024;
model.modelName = @"foo model";
[model internalBusiness]; ///組件內部業務邏輯調用
/// 模擬組件和外部組件相互調用。
/// 需要給外部組件傳遞業務信息,但是希望能夠防范外部組件非正常調用組件內部業務邏輯。
/// 因此通過isa-swizzling隱藏內部類信息。
object_setClass(model, MyBusinessModel.class);
thirdpartyComponent(model); //向外部發起消息
/// 模擬外部組件處理處理完成之后, 返回相應業務信息,
/// 這個時候, 內部組件可以把傳遞回來的參數恢復成內部實現類。
object_setClass(model, MyBusinessModelInternal.class);
[model internalBusiness];///組件內部業務邏輯調用
運行結果如下:
可以看出, 當外部組件試圖調用被隱藏的內部方法時, 就會拋出錯誤。 但是外部組件把這個對象返回來之后, 內部還是可以恢復成內部實現, 正常調用內部邏輯。
那么, 如果外部組件回傳的并非我們之前傳給它的object, 而是外部組件自己創建的object呢? 收到這個object之后, 還能不能把它"恢復"成內部實現呢?
///模擬外部組件自己創建一個內部業務model
MyBusinessModel *model = [[MyBusinessModel alloc] init];
model.modelID = 1024;
model.modelName = @"foo model";
///模擬外部組件把自己創建的業務model回傳給內部組件。
///然后內部組件把它照常"恢復"成內部model
object_setClass(model, MyBusinessModelInternal.class);
[(MyBusinessModelInternal *)model internalBusiness]; ///模擬調用內部邏輯
運行結果如下:
可以看出來, 這個絲毫不受影響。
其實如果明白了isa的作用, 這里邊的原因就很好理解了。舉個不是特別準確的例子:runtime可以類比為警察, 每個object就是普通民眾, isa就是身份證。 警察判斷你的身份, 靠的就是你的身份證。 但是現在身份證可以被偽造, 而警察又沒有鑒定身份證真假的措施, 那么用偽造的身份證騙過警察也就不奇怪了。
0x03
延伸一下:
一般情況下, 我們如果需要把某個類型轉換成另外一個類型, 會使用強制轉換的方式:
NSArray *obj = [NSArray arrayWithObject:@"Hi, this is isa-swizzling test!"];
MyBusinessModelInternal *model = (MyBusinessModelInternal *)obj;
@try {
[model internalBusiness];
} @catch (NSException *exception) {}
運行結果:
可以看出, 如果不小心強制轉換兩個不兼容的類型, 肯定會報錯的。編譯器比較傻, 很容易騙過, 但是runtime可沒那么好騙。 那么如果我們動些手腳呢?
NSTextField *obj = [[NSTextField alloc] init];
MyBusinessModelInternal *model1 = (MyBusinessModelInternal *)obj;
object_setClass(model1, MyBusinessModelInternal.class);
NSLog(@"model1: %@", model1);
[model1 internalBusiness];
object_setClass(obj, NSTextField.class);
是不是感覺runtime也是可以被欺騙的呢?(不過經過對其他類型的測試, 不一定100%都可以。 這個跟兩個類的ivars結構是否接近有關系, ivars結構越接近, 那么越容易成功。)