isa-swizzling應用

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];///組件內部業務邏輯調用

運行結果如下:


output-0x20-0

可以看出, 當外部組件試圖調用被隱藏的內部方法時, 就會拋出錯誤。 但是外部組件把這個對象返回來之后, 內部還是可以恢復成內部實現, 正常調用內部邏輯。

那么, 如果外部組件回傳的并非我們之前傳給它的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]; ///模擬調用內部邏輯

運行結果如下:

output-0x20-1

可以看出來, 這個絲毫不受影響。
其實如果明白了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) {}

運行結果:

output-0x30-0

可以看出, 如果不小心強制轉換兩個不兼容的類型, 肯定會報錯的。編譯器比較傻, 很容易騙過, 但是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);
output-0x30-1

是不是感覺runtime也是可以被欺騙的呢?(不過經過對其他類型的測試, 不一定100%都可以。 這個跟兩個類的ivars結構是否接近有關系, ivars結構越接近, 那么越容易成功。)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容