如何判斷block回調(diào)未被調(diào)用

在處理異步過程中,我們經(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

這里簡單的說明一下段匯編的邏輯。

  1. 取出block->called苦掘,并置為1(可能改為真正的計(jì)數(shù)會比較好)换帜。
  2. 取出原始block block->block,并放到第一個(gè)參數(shù)位置鹤啡。
  3. 調(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植锉。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末辫樱,一起剝皮案震驚了整個(gè)濱河市峭拘,隨后出現(xiàn)的幾起案子俊庇,更是在濱河造成了極大的恐慌,老刑警劉巖鸡挠,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件辉饱,死亡現(xiàn)場離奇詭異,居然都是意外死亡拣展,警方通過查閱死者的電腦和手機(jī)彭沼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來备埃,“玉大人姓惑,你說我怎么就攤上這事“唇牛” “怎么了于毙?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長辅搬。 經(jīng)常有香客問我唯沮,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任介蛉,我火速辦了婚禮萌庆,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘币旧。我一直安慰自己践险,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布吹菱。 她就那樣靜靜地躺著捏境,像睡著了一般。 火紅的嫁衣襯著肌膚如雪毁葱。 梳的紋絲不亂的頭發(fā)上垫言,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天,我揣著相機(jī)與錄音倾剿,去河邊找鬼筷频。 笑死,一個(gè)胖子當(dāng)著我的面吹牛前痘,可吹牛的內(nèi)容都是我干的凛捏。 我是一名探鬼主播,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼芹缔,長吁一口氣:“原來是場噩夢啊……” “哼坯癣!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起最欠,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤示罗,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后芝硬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蚜点,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年拌阴,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了绍绘。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,137評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡迟赃,死狀恐怖陪拘,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情纤壁,我是刑警寧澤左刽,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站摄乒,受9級特大地震影響悠反,放射性物質(zhì)發(fā)生泄漏残黑。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一斋否、第九天 我趴在偏房一處隱蔽的房頂上張望梨水。 院中可真熱鬧,春花似錦茵臭、人聲如沸疫诽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽奇徒。三九已至,卻和暖如春缨硝,著一層夾襖步出監(jiān)牢的瞬間摩钙,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工查辩, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留胖笛,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓宜岛,卻偏偏與公主長得像长踊,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子萍倡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評論 2 355

推薦閱讀更多精彩內(nèi)容