over 1 year ago

Objective-C 屬於動態程式語言,可以在程式執行的同時改變程式結構,
這次要介紹的就是 iOS在 run time 時改變 method 實作。

首先說明 iOS 內 method 的結構,在 class 內宣告的 method 都是 struct。

typedef struct objc_method *Method;
typedef struct objc_ method {

    SEL method_name;

    char *method_types;

    IMP method_imp;

};

SEL 表示 method 的名稱,
types 表示 method 的參數,
IMP 是一個 function pointer 用來指出 method 真正實作的 address。

在 compile 時會將每個 class 的 method 列成一份表格,
objc 是一個動態程式語言,意指這份表格在 compile 完成且進入 runTime 時是可以隨時修改的,
若要在動態時修改表格,首先要先抓到要修改哪個 class 的哪個 method,
並且先攔截下這個 method,再進行修改。

先定義要改變實作的目標。

@interface TestClass : NSObject
@end

@implementation TestClass
- (void)test
{
    NSLog(@"test");
}
@end

首先建立一個 NSObject,主要用來模擬 method 的結構,
並將想要置換的 method 原封不動的 copy 到這個結構中。

@interface TestMethod : NSObject
@property (assign, nonatomic) SEL selector;
@property (assign, nonatomic) IMP implementation;
@property (assign, nonatomic) Method method;

再來就是攔截目標 method,
在 compile 時期對於 class 的每份 method 都會註冊一個簽名,
這個簽名記錄了這個 method 的所有資訊。

因此先透過 class 以及 SEL 取得 NSMethodSignature,

TestClass *c = [[TestClass alloc] init];
Class inClass = [c Class];
SEL inSelector = @selector(old);
NSMethodSignature *methodSignature = [inClass instanceMethodSignatureForSelector:inSelector];

透過這份簽名,將原本 method 的一些資訊 copy 至模擬的 method object。

TestMethod *method = [[TestMethod alloc] init];
method.selector = inSelector;
method.hasReturnValue = [methodSignature methodReturnLength] > 0;
method.methodSignature = methodSignature;
method.returnValueLength = [methodSignature methodReturnLength];

透過 class 以及 SEL 去抓 IMP。

method.method = class_getInstanceMethod(inClass, inSelector);
method.implementation = class_getMethodImplementation(inClass, inSelector);

這時候將原本的 method 實作替換成攔截訊號。

method_setImplementation(method.method, (IMP)_objc_msgForward);

這時候在原本的 class 內動態新增一個新定義的 method。

SEL newSelector = NSSelectorFromString(@"New");
class_addMethod(inClass, newSelector, method.implementation, method.typeEncoding);

最後來呼叫原本的 function。

[c old];

因為剛剛已經將原本的實作替換成攔截訊號,因此原本的 class 會收到一連串的 delegate 來處理。

第一個收到的 delegate 是要確認這個 method 主要負責的對象是誰。

- (id)forwardingTargetForSelector:(SEL)aSelector

第二個收到的 delegate 會要要求一份 method 的簽名。
在這個時間點,原本的 method 已經無法再使用了,
但剛剛有事先將實作保存在模擬的 method object 內,可以回傳這個 method 的簽名。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

第三個收到的 delegate 會給予一份實作的 invocation。
如果剛剛有回傳事先保留的 method,這裡的 invocation 便會是 method 的資訊,
雖然原本的 method 已經無法使用,但剛剛在原本的 class 有定義一個新的 method,
所以可以這邊將 invocation 的 selector 換成新的名稱來執行。

- (void)forwardInvocation:(NSInvocation *)anInvocation
← iOS Swift : Access Control iOS ReactiveCocoa Basic Method →