一、前言
- Block 和 Delegate 是對象間傳遞消息的常用機制,這兩個機制可以說是各有千秋。 Delegate 可以很方便把目標動作的執行過程劃分為多個方法,以展現不同時間節點下特定的操作;Block 則擅長處理一個回調多個落點的情況,并且它可以通過捕捉上下文信息,來達到減少創建額外變量,集中消息處理邏輯的目的。
- 結合以上兩種通信方式的特點,我們可以添加一些額外的橋接處理,讓 Delegate 機制也能享有 Block 機制所擁有的部分優點,橋接處理的核心就是用 Block 實現委托方法。
- 由于 Runtime 的存在,在消息轉發的最后一步,可以輕松地攔截對未定義方法的調用,并且針對當前消息做一些額外的處理,比如改變它的入參、設置另一個消息接受者等。借助于這一特性,我們可以創建一個統一的 Delegate 對象,并在這個對象的 -forwardInvocation: 方法中,用預先設置的 Block 替換對委托方法的調用,以達到用 Block 實現委托方法的目的。
二、NSInvocation 基本使用
- 蘋果官方對 NSInvocation 的用途給出的解釋如下:
NSInvocation objects are used to store and forward messages between objects and between applications
- 一個 NSInvocation 對象包含了 Objective-C 消息的所有要素:消息接收對象、 方法選擇器 (SEL) 、參數以及返回值,并且這些要素都可以由開發者直接設置。如下所示,簡單使用 NSInvocation:
NSString
*foo
= @"foo";
NSMethodSignature
*signature
= [foo methodSignatureForSelector
:@selector(stringByAppendingString
:)];
NSInvocation
*invocation
= [NSInvocation invocationWithMethodSignature
:signature
];
invocation
.selector
= @selector(stringByAppendingString
:);NSString
*bar
= @"bar";
[invocation setArgument
:&bar atIndex
:2];
[invocation invokeWithTarget
:foo
];void *result
= nil
;
[invocation getReturnValue
:&result
];NSString
*resultString
= (__bridge NSString
*)(result
);
NSLog(@"%@", resultString
);
foobar
- 可以看到,這個結果和執行 [foo stringByAppendingString:bar] 的結果是一致的。
- 關于 NSInvocation 的使用,需要注意以下兩點:
-
- 一般方法的自定義參數從索引 2 開始,前兩個分別是對象自身以及發送方法的 SEL;
-
- 從 -getArgument:atIndex: 和 -getReturnValue: 方法中獲取的對象是不會被 retain 的,所以如果使用 ARC ,那么以下代碼都是錯誤的:
NSString
*bar
= nil
;
[invocation getArgument
:&bar atIndex
:2];NSString
*result
= nil
;
[invocation getReturnValue
:&result
];
-
- ARC 編譯環境下局部對象默認具有 __strong 屬性,它會針對這個對象添加 release 代碼,所以可能會因為 release 已經釋放的對象而崩潰。正確代碼如下:
void *bar
= nil
;
[invocation getArgument
:&bar atIndex
:2];void *result
= nil
;
[invocation getReturnValue
:&result
];
-
- 如果是在兩個 NSInvocation 對象間傳遞參數/返回值,那么可以直接傳入指針獲取并設置目標地址,以返回值為例:
....
NSInvocation
*invocation
= [NSInvocation invocationWithMethodSignature
:signature
];
NSInvocation
*shadowInvocation
= [NSInvocation invocationWithMethodSignature
:signature
];
....
void *resultBuffer
= malloc(invocation
.methodSignature
.methodReturnLength
);
memset(resultBuffer
, 0, invocation
.methodSignature
.methodReturnLength
);[invocation getReturnValue
:resultBuffer
];
[shadowInvocation setReturnValue
:resultBuffer
];
....
free(resultBuffer
);
- 這時,如果返回值是一個 NSString 對象,那么 resultBuffer 實際上是指向 NSString 對象指針的指針,這時可以這樣讀取實際內容:
NSString
*result
= (__bridge NSString
*)(*(void **)resultBuffer
);
- 不過在已經知道返回值是一個對象時,一般會直接傳入對象指針的地址,以便直接讀取對象。
三、獲取方法簽名
① 從對象中獲取方法簽名
- NSMethodSignature 是創建一個有效 NSInvocation 對象的必要成分,它提供了方法調用所必須的參數和返回值信息。
- NSObject 類用以下兩個方法獲取實例方法的方法簽名:
- (NSMethodSignature
*)methodSignatureForSelector
:(SEL
)aSelector
OBJC_SWIFT_UNAVAILABLE("");
+ (NSMethodSignature
*)instanceMethodSignatureForSelector
:(SEL
)aSelector
OBJC_SWIFT_UNAVAILABLE("");
- 既然類也是對象,那么類方法的方法簽名也就可以通過 -methodSignatureForSelector: 方法獲取。
② 從協議中獲取方法簽名
- 由于協議定義了接口的參數和返回值信息,所以從協議中也可以獲取到特定方法的方法簽名。利用 protocol_getMethodDescription 函數,可以獲取到描述類型的 C 字符串,再通過這個字符串構造方法簽名。
- 針對協議中的接口有 required 和 optional 兩種,并且不允許重復這一特點,可以創建構造方法簽名的函數:
static NSMethodSignature
*ydw_getProtocolMethodSignature(Protocol
*protocol
, SEL selector
, BOOL isInstanceMethod
) {struct objc_method_description methodDescription
= protocol_getMethodDescription(protocol
, selector
, YES
, isInstanceMethod
);if (!methodDescription
.name
) {methodDescription
= protocol_getMethodDescription(protocol
, selector
, NO
, isInstanceMethod
);}return [NSMethodSignature signatureWithObjCTypes
:methodDescription
.types
];
}
③ 從 Block 中獲取方法簽名
- 蘋果并沒有提供一個開放的接口,供開發者獲取 Block 的方法簽名。不過根據 LLVM 對 Block 結構描述,可以通過操作指針獲取簽名字符串。
- Block 的結構如下:
typedef NS_OPTIONS(int, YDWBlockFlags
) {YDWBlockFlagsHasCopyDisposeHelpers
= (1 << 25),YDWBlockFlagsHasSignature
= (1 << 30)
};
typedef struct ydw_block
{__unused Class isa
;YDWBlockFlags flags
;__unused
int reserved
;void (__unused
*invoke
)(struct ydw_block
*block
, ...);struct {unsigned long int reserved
;unsigned long int size
;void (*copy
)(void *dst
, const void *src
);void (*dispose
)(const void *);const char *signature
;const char *layout
;} *descriptor
;
} *YDWBlockRef
;
- 可以看到,只要獲取 descriptor 指針,然后根據不同條件添加特定的偏移量,就可以獲取到 signature:
static NSMethodSignature
*ydw_signatureForBlock(id block
) {YDWBlockRef layout
= (__bridge YDWBlockRef
)(block
);if (!(layout
->flags
& YDWBlockFlagsHasSignature
)) {return nil
;}void *desc
= layout
->descriptor
;desc
+= 2 * sizeof(unsigned long int);if (layout
->flags
& YDWBlockFlagsHasCopyDisposeHelpers
) {desc
+= 2 * sizeof(void *);}char *objcTypes
= (*(char **)desc
);return [NSMethodSignature signatureWithObjCTypes
:objcTypes
];
}
四、方法調用 -> Block 調用
- 經過上文,已經可以獲取到 Block 和接口方法的簽名信息,現在根據這個簽名信息,結合方法對應的 NSInvocation 對象,創建和 Block 關聯的 NSInvocation 對象。
① 存儲 Block 信息
- 首先要做的是,存儲 Block 的簽名信息,并且和接口方法的簽名信息做匹配處理。因為在調用前,需要將接口方法得到的參數轉換成 Block 的入參,調用之后需要將 Block 的返回值重新傳給接口方法,所以必須確保兩者的簽名信息在一定程度上是兼容的。
- (instancetype
)initWithMethodSignature
:(NSMethodSignature
*)methodSignature block
:(id
)block
{return [self initWithMethodSignature
:methodSignature blockSignature
:ydw_signatureForBlock(block
) block
:block
];
}- (instancetype
)initWithMethodSignature
:(NSMethodSignature
*)methodSignature blockSignature
:(NSMethodSignature
*)blockSignature block
:(id
)block
{NSAssert(ydw_isCompatibleBlockSignature(blockSignature
, methodSignature
), @"Block signature %@ is not compatible with method signature %@", blockSignature
, methodSignature
);if (self = [super init
]) {_methodSignature
= methodSignature
;_blockSignature
= blockSignature
;_block
= block
;}return self;
}
② 簽名匹配
- Block 的簽名信息相較于方法的簽名信息,只在參數類型上少了 SEL。
- 方法的簽名信息如果要獲取自定義參數類型的話,需要從索引 2 開始,而 Block 的自定義參數類型信息則從索引 1 開始。
static BOOL
ydw_isCompatibleBlockSignature(NSMethodSignature
*blockSignature
, NSMethodSignature
*methodSignature
) {NSCParameterAssert(blockSignature
);NSCParameterAssert(methodSignature
);if ([blockSignature isEqual
:methodSignature
]) {return YES
;}if (blockSignature
.numberOfArguments
>= methodSignature
.numberOfArguments
||blockSignature
.methodReturnType
[0] != methodSignature
.methodReturnType
[0]) {return NO
;}BOOL compatibleSignature
= YES
;for (int idx
= 2; idx
< blockSignature
.numberOfArguments
; idx
++) {const char *methodArgument
= [methodSignature getArgumentTypeAtIndex
:idx
];const char *blockArgument
= [blockSignature getArgumentTypeAtIndex
:idx
- 1];if (!methodArgument
|| !blockArgument
|| methodArgument
[0] != blockArgument
[0]) {compatibleSignature
= NO
;break;}} return compatibleSignature
;
}
③ Invocation 調用
- 得到有效的 Block 簽名信息,就可以構造 NSInvocation 對象,不過還需要接口方法的實參信息,這可以通過讓外部傳入接口方法的 NSInvocation 對象實現。
- (void)invokeWithMethodInvocation
:(NSInvocation
*)methodInvocation
{NSParameterAssert(methodInvocation
);NSAssert([self.methodSignature isEqual
:methodInvocation
.methodSignature
], @"Method invocation's signature is not compatible with block signature");NSMethodSignature
*methodSignature
= methodInvocation
.methodSignature
;NSInvocation
*blockInvocation
= [NSInvocation invocationWithMethodSignature
:self.blockSignature
];void *argumentBuffer
= NULL;for (int idx
= 2; idx
< methodSignature
.numberOfArguments
; idx
++) {const char *type
= [methodSignature getArgumentTypeAtIndex
:idx
];NSUInteger size
= 0;NSGetSizeAndAlignment(type
, &size
, NULL);if (!(argumentBuffer
= reallocf(argumentBuffer
, size
))) {return;}[methodInvocation getArgument
:argumentBuffer atIndex
:idx
];[blockInvocation setArgument
:argumentBuffer atIndex
:idx
- 1];}[blockInvocation invokeWithTarget
:self.block
];if (methodSignature
.methodReturnLength
&&(argumentBuffer
= reallocf(argumentBuffer
, methodSignature
.methodReturnLength
))) {[blockInvocation getReturnValue
:argumentBuffer
];[methodInvocation setReturnValue
:argumentBuffer
];}free(argumentBuffer
);
}
- reallocf 函數是 realloc 函數的增強版,它可以在后者無法申請到堆空間時,釋放舊的堆空間:
void *reallocf(void *p
, size_t s
) {void *tmp
= realloc(p
, s
);if(tmp
) return tmp
;free(p
);return NULL;
}
- 這樣就可以直接用 argumentBuffer = reallocf(argumentBuffer, size) 形式的語句,否則如果使用 realloc,一旦返回的是 NULL,會造成舊的堆空間無法釋放的問題。
五、實現委托方法
- 現在已經可以構造 Block 的 NSInvocation 對像,就缺攜帶參數和返回值信息的接口方法 NSInvocation 對象,接下來就針對實例方法,簡單地實現動態委托類。
① 儲存 Block Invocation 信息
- 以接口方法選擇器對應的字符串為 Key,以 Block 對應的 Invocation 封裝類為 Value 儲存調用信息:
- (instancetype
)initWithProtocol
:(Protocol
*)protocol
{_protocol
= protocol
;_selectorInvocationMap
= [NSMutableDictionary dictionary
];return self;
}- (void)implementInstanceMethodOfSelector
:(SEL
)selector withBlock
:(id
)block
{NSMethodSignature
*methodSignature
= ydw_getProtocolMethodSignature(self.protocol
, selector
, YES
);YDWBlockInvocation
*invocation
= [[YDWBlockInvocation alloc
] initWithMethodSignature
:methodSignature block
:block
];self.selectorInvocationMap
[NSStringFromSelector(selector
)] = invocation
;
}
② 消息轉發
- 向動態委托類發送委托消息后,會觸發消息轉發機制。在消息轉發的最后一步,可以構造委托方法對應的 NSInvocation 對象:
- (void)forwardInvocation
:(NSInvocation
*)invocation
{YDWBlockInvocation
*blockInvocation
= self.selectorInvocationMap
[NSStringFromSelector(invocation
.selector
)];[blockInvocation invokeWithMethodInvocation
:invocation
];
}- (NSMethodSignature
*)methodSignatureForSelector
:(SEL
)sel
{return self.selectorInvocationMap
[NSStringFromSelector(sel
)].methodSignature
;
}- (BOOL
)respondsToSelector
:(SEL
)aSelector
{return !!self.selectorInvocationMap
[NSStringFromSelector(aSelector
)];
}
六、實例
@class Computer
;
@protocol ComputerDelegate
<NSObject
>
@required
- (void)computerWillStart
:(Computer
*)computer
;
- (BOOL
)computerShouldBeLocked
:(Computer
*)computer
;
@end@interface Computer
: NSObject
@property (weak
, nonatomic
) id
<ComputerDelegate
> delegate
;- (void)start
;
- (void)lock
;@end@implementation Computer
- (void)start
{[self.delegate computerWillStart
:self];
}- (void)lock
{__unused BOOL locked
= [self.delegate computerShouldBeLocked
:self];printf("computer should be locked: %d \n", locked
);
}
@end
YDWDynamicDelegate
<ComputerDelegate
> *dynamicDelegate
= (id
)[[YDWDynamicDelegate alloc
] initWithProtocol
:@protocol(ComputerDelegate
)];
[dynamicDelegate implementInstanceMethodOfSelector
:@selector(computerWillStart
:) withBlock
:^(Computer
*c
) {NSLog(@"%@ will start", c
);
}];
[dynamicDelegate implementInstanceMethodOfSelector
:@selector(computerShouldBeLocked
:) withBlock
:^BOOL(Computer
*c
) {NSLog(@"%@ should not be locked", c
);return NO
;
}];Computer
*computer
= [Computer new
];
computer
.delegate
= dynamicDelegate
;
[computer start
];
[computer lock
];
will start
should not be locked
computer should be locked
: 0
七、總結
- 用 Block 實現委托方法的開源方案在比較早的時候就已經出來了,本文的實現就是 BlocksKit 的 A2BlockInvocation 和 A2DynamicDelegate 類的簡易版本,不過省略了類方法以及一些邊界條件的處理,不過大體的思路基本是一致的,都是圍繞 NSInvocation 和消息轉發。
總結
以上是生活随笔為你收集整理的iOS之深入解析如何使用Block实现委托方法的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。