在處理異步過程中,我們經(jīng)常會碰到這種情況论悴,需要異步處理并異步回調(diào)completionHandler掖棉,但是有些場景下,如果你在處理完異步邏輯膀估,而不回調(diào)completion的時(shí)候幔亥,會產(chǎn)生邏輯上的bug或者內(nèi)存泄露問題,那么我們就需要知道調(diào)用方是否調(diào)用了completion察纯。
這里舉幾個(gè)比較典型的例子紫谷,比如WKUIDelegate
中的回調(diào):
- (void)webView:(WKWebView *)webView
runJavaScriptAlertPanelWithMessage:(NSString *)message
initiatedByFrame:(WKFrameInfo *)frame
completionHandler:(void (^)(void))completionHandler;
如果不回調(diào)其completionHandler
,會導(dǎo)致其邏輯上的錯誤捐寥,那么這里我們來看看如何動態(tài)監(jiān)測completionHandler
是否被調(diào)用過笤昨。
這里說一下,WK是通過WTF
的C++模板來實(shí)現(xiàn)的握恳,我這里采用C語言來實(shí)現(xiàn)瞒窒,其思路是大致相同的。
Block
首先我們來看看Block是什么乡洼。雖然我們平時(shí)可以像OC對象那樣去使用它崇裁,但它嚴(yán)格意義上來說并不是一個(gè)OC對象,或者說它是一中極為特殊的OC對象束昵。
struct Block_layout {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
void (*invoke)(void *, ...);
Descriptor *descriptor;
// imported variables
};
struct Descriptor {
uintptr_t reserved;
uintptr_t size;
void (*copy)(void *dst, const void *src);
void (*dispose)(const void *);
};
上面就是Block的內(nèi)存布局拔稳,其中Block_layout
是一個(gè)不定長的結(jié)構(gòu)體,我們平時(shí)看到的捕獲變量都會存在結(jié)構(gòu)尾部锹雏。這里我們看到和OC對象一樣巴比,也有isa
指針,但是這里的指針永遠(yuǎn)只會指向幾個(gè)地方,這個(gè)之后會說轻绞。
其實(shí)我們在調(diào)用Block的時(shí)候采记,實(shí)際上調(diào)用的是block->invoke()
,第一個(gè)參數(shù)是Block本身政勃,然后是入?yún)错樞蚺畔氯ミ罅洌@一部分編譯器都會給我們做好,所以一個(gè)block調(diào)用實(shí)際是這樣的:
block->invoke(block, arg1, arg2, arg3);
可以看到和OC的objc_msgSend
方法相同的是第一個(gè)參數(shù)是對象本身奸远,但是不同的是第二個(gè)參數(shù)不再是SEL
既棺。
既然知道了Block的結(jié)構(gòu),那么我們就可以自定義block了懒叛。
Block類型
Block定義的類型有:
BLOCK_EXPORT void * _NSConcreteGlobalBlock[32]
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
BLOCK_EXPORT void * _NSConcreteStackBlock[32]
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
BLOCK_EXPORT void * _NSConcreteMallocBlock[32]
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
BLOCK_EXPORT void * _NSConcreteAutoBlock[32]
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
BLOCK_EXPORT void * _NSConcreteFinalizingBlock[32]
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
BLOCK_EXPORT void * _NSConcreteWeakBlockVariable[32]
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
其中只有前2中是公開的丸冕,而我們平時(shí)會碰到的基本都是前3種類型,其中Global是永遠(yuǎn)不會被釋放的芍瑞,Stack是在棧上晨仑,所以只要棧銷毀了就會被釋放褐墅,Malloc和普通OC對象一樣拆檬,采用引用計(jì)數(shù)來決定生命周期的。
那么我們回到最初的目的妥凳,如何判斷是否被調(diào)用了呢竟贯?因?yàn)檫@個(gè)調(diào)用有可能是異步的,所以不可能通過__block bool called
這樣的臨時(shí)對象來判斷逝钥,也不能通過其是否由Stack拷貝成Malloc來判斷屑那,因?yàn)閏opy了并不一定會被調(diào)用。
Block Wrap
這里要判斷Block是否被調(diào)用艘款,肯定是需要在原始Block基礎(chǔ)上包裹一層可以計(jì)數(shù)調(diào)用次數(shù)的Block持际。C++會方便的多,可以直接通過模板來構(gòu)造一個(gè)簽名一樣的Block哗咆。
這里我們利用了MallocBlock在未被任何人引用的時(shí)候會銷毀的特性蜘欲,在其被釋放之前,來監(jiān)測計(jì)數(shù)是否為0晌柬。如果是0則說明從來沒有被調(diào)用過姥份,不是0則說明被調(diào)用了。
那么接下來我們來看看如何動態(tài)構(gòu)建這樣一個(gè)Block年碘,以及如果去包裹其實(shí)現(xiàn)體澈歉。
動態(tài)構(gòu)建Block
struct Block_layout {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
void (*invoke)(void);
void *descriptor;
// imported variables
void *block;
int64_t called;
char *message;
};
首先我們將我們所需要的幾個(gè)參數(shù)定義在Block末尾,分別是原始的Block屿衅,調(diào)用計(jì)數(shù)埃难,以及錯誤信息(這個(gè)在報(bào)錯的時(shí)候使用,和該方案關(guān)系不大)。
然后凯砍,我們需要定義自己的descriptor箱硕。這里重寫了dispose方法,我們需要在這里判斷是否計(jì)數(shù)為0悟衩,同時(shí)也要在這里將對象釋放掉(由于在C環(huán)境中剧罩,所以block也需要手動將其釋放)。
void block_call_assert_wrap_dispose(const void * ptr) {
struct Block_layout *self = (struct Block_layout *)ptr;
if (!((struct Block_layout *)ptr)->called) {
if (exception_handler) {
if (self->message) {
char *buf = (char *)malloc((strlen(self->message) + 64) * sizeof(char));
sprintf(buf, "ERROR: Block must be called at %s!\n", self->message);
exception_handler(buf);
free(buf);
}
else {
exception_handler("ERROR: Block must be called at %s!\n");
}
}
}
Block_release(self->block);
if (self->message) free(self->message);
}
static const struct Descriptor descriptor = {
0,
sizeof(struct Block_layout),
NULL,
block_call_assert_wrap_dispose
};
接下來就是將我們的所有數(shù)據(jù)內(nèi)容填入Block_layout座泳,來合成一個(gè)Block對象惠昔。
void *block_call_assert_wrap_block(void *orig_blk, char *message) {
struct Block_layout *block = (struct Block_layout *)malloc(sizeof(struct Block_layout));
block->isa = _NSConcreteMallocBlock;
enum {
BLOCK_NEEDS_FREE = (1 << 24),
BLOCK_HAS_COPY_DISPOSE = (1 << 25),
};
const unsigned retainCount = 1;
block->flags = BLOCK_HAS_COPY_DISPOSE | BLOCK_NEEDS_FREE | (retainCount << 1);
block->reserved = 0;
block->invoke = (void (*)(void))block_call_assert_wrap_invoke;
block->descriptor = (void *)&descriptor;
block->block = (void *)Block_copy(orig_blk);
block->called = 0;
size_t len = strlen(message)*sizeof(char);
char *buf = (char *)malloc(len);
memcpy(buf, message, len);
block->message = buf;
return block;
}
其中invoke方法被我們的新方法block_call_assert_wrap_invoke
所替換,在這個(gè)方法里面挑势,會更新計(jì)數(shù)镇防,并且調(diào)用原始block的invoke方法。
block_call_assert_wrap_invoke的實(shí)現(xiàn)
block的方法是非常靈活的潮饱,參數(shù)個(gè)數(shù)以及返回值不一樣的時(shí)候来氧,經(jīng)過前幾篇內(nèi)容,我們知道不能簡單的通過方法調(diào)用來實(shí)現(xiàn)參數(shù)的傳遞香拉,而且在這里我們也無法知道參數(shù)的個(gè)數(shù)以及類型啦扬。那么我們要怎么做才能簡單而又實(shí)用呢?
這時(shí)候凫碌,我們想到objc_msgSend
方法扑毡,它就實(shí)現(xiàn)了非常技巧的實(shí)現(xiàn)了arguments forward
的功能(其功能特性可以參考C++模板的多參傳遞template <typename Args...>
)。
由于這里找不到i386的系統(tǒng)已經(jīng)arm32的系統(tǒng)了盛险,所以只給出x86_64和arm64的實(shí)現(xiàn)方案瞄摊。
#if __x86_64__
.align 4
.global _block_call_assert_wrap_invoke
_block_call_assert_wrap_invoke:
mov %rdi, %r10
movq $1, 0x28(%r10) // called
movq 0x20(%r10), %r11 // block
movq %r11, %rdi
movq 0x10(%r11), %r11 // block->block->invoke
jmp *%r11
#endif
#ifdef __arm64__
.align 4
.global _block_call_assert_wrap_invoke
_block_call_assert_wrap_invoke:
mov x9, x0
add x10, x9, #0x20 // &block
add x11, x9, #0x28 // called
mov x12, #1
str x12, [x11]
ldr x12, [x10] // block
add x12, x12, #0x10 // block->invoke
ldr x12, [x12]
mov x0, x11
br x12
ret
#endif
這里簡單的說明一下段匯編的邏輯。
- 取出
block->called
苦掘,并置為1
(可能改為真正的計(jì)數(shù)會比較好)换帜。 - 取出原始block
block->block
,并放到第一個(gè)參數(shù)位置鹤啡。 - 調(diào)用原始block的invoke
call block->block->invoke
惯驼。
這樣我們就非常簡單的包裹了原始invoke方法,并且插入了自己的邏輯揉忘。
使用
首先我們需要設(shè)置上述的exception_handler
跳座。
void exception_log(const char *str) {
NSLog(@"%s", str);
}
block_call_assert_set_exception_handler(exception_log);
這里我只是讓他打印出錯誤,更好的應(yīng)該是直接拋出異常[NSException raise:]
泣矛。
在此基礎(chǔ)上疲眷,定義一個(gè)宏以方便使用,以及可以加入#if DEBUG
您朽,來禁用線上環(huán)境的該功能狂丝,并且把當(dāng)前的位置傳遞給exception_message
:
#define BLOCK_CALL_ASSERT(x) ({ \
typeof ((x)) blk = x; \
char *message = (char *)malloc(512); \
memset(message, 0, 512); \
sprintf(message, "(%s:%d %s)", __FILE__, __LINE__, __FUNCTION__); \
typeof (blk) ret = (__bridge_transfer typeof(blk))block_call_assert_wrap_block((__bridge void *)blk, message); \
free(message); \
ret; \
})
bridge
换淆,恩我們是支持的ARC,所以在此為了防止類型轉(zhuǎn)換的warning和error几颜,在此使用宏來定義倍试。(好像Objc++會有警告)
那么在使用的時(shí)候就是這樣:
- (void)doAsyncWithCompletion:(block_t)completionBlock {
dispatch_async(..., ^{
completionBlock(...)
});
}
[self doAsyncWithCompletion:BLOCK_CALL_ASSERT(^{
do_after_completion();
do_clear();
})];
那么在此時(shí),如果被調(diào)用者沒有調(diào)用過completionBlock()
時(shí)蛋哭,就會觸發(fā)exception_handler
县习。這樣我們就可以檢測到是否出現(xiàn)可能的邏輯錯誤和內(nèi)存泄露了。
ERROR: Block must be called at (BlockCallAssert/BlockCallAssert/BlockCallAssert/ViewController.mm:41 -[ViewController test2])!
最后
一般來說谆趾,我們一旦設(shè)計(jì)了包含completionBlock
這樣的接口躁愿,基本是需要回調(diào)方100%
的回調(diào)的,如果可以不用回調(diào)沪蓬,那么我們?yōu)槭裁床桓淖冊O(shè)計(jì)方案呢彤钟。
當(dāng)我們的調(diào)用方是自己的時(shí)候,我們可以確保跷叉,而如果是SDK逸雹,我們就很難確保,文檔這個(gè)東西是不靠譜的云挟,那么我們就讓調(diào)用方在忽略了回調(diào)的時(shí)候給他一個(gè)重拳吧(exception)梆砸。
這個(gè)方案的實(shí)現(xiàn)我放在github,和cocoaPods BlockCallAssert
植锉。