首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

Objective-C 方法替換完全指南:Method Swizzling、Category 重寫與直接 Override 深度解析

     編輯:關於C語言

背景:為什麼需要替換無法控制實例化的類的方法?

  • 第三方庫/系統框架類,無法子類化
  • 單例、工廠方法回傳具體類實例
  • 熱修復、AOP 埋點、行為擴展

方案演進:從 Overriding 到 Swizzling

1. 子類重寫——最標準但受限

@interface MyView : NSView @end
@implementation MyView
- (void)drawRect:(NSRect)r { /* 自定義繪製 */ }
@end
/* 必須控制實例化:[[MyView alloc] init] */

❌ 無法影響現有實例或框架內部建立的實例

2. Posing(已棄用)

子類偽裝成父類,Runtime 將所有父類訊息轉發給子類。
❌ 64-bit / iOS 不支援,已廢棄。

3. Category 重寫——簡單但危險

@implementation NSView (MyOverride)
- (void)drawRect:(NSRect)r {
    [[NSColor blueColor] set];
    NSRectFill(r);
    // ❌ 無法呼叫原始實現
    // ❌ 多個 Category 重寫同一方法 → 行為未定義
}
@end

4. Method Swizzling(移魂大法)——標準解法

核心思想:用新選擇器實現替換原選擇器,並保留原實現供呼叫。

步驟 1:定義新方法(命名慣例 prefix_original)

@implementation NSView (Swizzle)
- (void)my_drawRect:(NSRect)r {
    /* 呼叫原實現:此時 my_drawRect 已與 drawRect 交換,
       所以呼叫 my_drawRect 實際執行原始 drawRect */
    [self my_drawRect:r];
    
    [[NSColor blueColor] set];
    NSRectFill(r);
}
@end

步驟 2:Runtime 交換實現

#import <objc/runtime.h>

void MethodSwizzle(Class c, SEL orig, SEL override) {
    Method origMethod = class_getInstanceMethod(c, orig);
    Method overrideMethod = class_getInstanceMethod(c, override);
    
    /* 情況 A:原方法未在類中實現(繼承自父類) */
    if (class_addMethod(c, orig,
                        method_getImplementation(overrideMethod),
                        method_getTypeEncoding(overrideMethod))) {
        /* 成功添加 → 類中現有 orig 指向新實現
           再將 override 指向原父類實現 */
        class_replaceMethod(c, override,
                            method_getImplementation(origMethod),
                            method_getTypeEncoding(origMethod));
    } 
    /* 情況 B:原方法已在類中實現 */
    else {
        method_exchangeImplementations(origMethod, overrideMethod);
    }
}

/* 在 +load 中自動執行 */
+ (void)load {
    MethodSwizzle(self, @selector(drawRect:), @selector(my_drawRect:));
}

關鍵點解析

API用途
class_getInstanceMethod獲取 Method 結構體(含 IMP、type encoding)
class_addMethod添加新方法;若已存在返回 NO(用於檢測情況 A/B)
class_replaceMethod替換現有方法實現
method_exchangeImplementations交換兩個 Method 的 IMP
method_getImplementation取得函數指標 (IMP)
method_getTypeEncoding取得型別編碼字串(如 "v@:{CGRect={CGPoint=dd}{CGSize=dd}}")

5. 直接 Override(更簡潔替代方案)

不依賴選擇器交換,改用全域函數指標保存原 IMP。

/* 全域儲存原始 IMP */
static void (*gOrigDrawRect)(id, SEL, NSRect);

+ (void)load {
    Method m = class_getInstanceMethod(self, @selector(drawRect:));
    gOrigDrawRect = (void*)method_getImplementation(m);
    
    /* 直接替換:class_replaceMethod 內部處理 add/replace */
    class_replaceMethod(self, @selector(drawRect:),
                        (IMP)OverrideDrawRect,
                        method_getTypeEncoding(m));
}

/* C 風格函數作為新實現 */
static void OverrideDrawRect(NSView *self, SEL _cmd, NSRect r) {
    gOrigDrawRect(self, _cmd, r);  // 呼叫原實現
    [[NSColor blueColor] set];
    NSRectFill(r);
}

Swizzling vs 直接 Override 對比

維度Swizzling直接 Override
概念複雜度高(選擇器交換、遞迴呼叫假象)低(函數指標直觀)
多重 Swizzling 安全性需注意順序、鏈式呼叫各自保存原 IMP,互不干擾
除錯體驗堆疊顯示交換後選擇器堆疊顯示 C 函數名
適用場景框架級、需與其他 Swizzling 共存應用層、簡單替換、團隊熟悉 C

⚠️ 核心警示:Swizzling 危險邊界

  1. 不要 Swizzling 系統私有方法:底層變動導致崩潰
  2. 不要破壞契約:如 `init` 必須回傳 `self`,`dealloc` 必須呼叫 `[super dealloc]`
  3. 執行順序不確定:多個 Category 的 `+load` 順序未定義
  4. 測試困難:單元測試需隔離 Swizzling 影響
  5. 優先嘗試委託/通知/Block 回調:Swizzling 是最後手段

最佳實踐檢查清單

  • ✅ 僅在 `+load` 中執行(保證類載入完成、執行緒安全)
  • ✅ 使用唯一前綴命名新選擇器(如 `xyz_originalSelector`)
  • ✅ 保存原 IMP 並正確呼叫(參數完全透傳)
  • ✅ 處理「父類實現」與「類自有實現」兩種情況
  • ✅ 針對目標類、目標方法編寫專用測試案例
  • ✅ 文檔記錄:為什麼 Swizzling、替代方案評估、風險緩解
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved