原做於:2018-10-08 GitHub Repo:BoyangBloggit
這裏將經過幾道面試題來擴展知識。 這幾道題有幾個取自sunnyxx。github
#import <UIKit/UIKit.h> #import "AppDelegate.h" int d = 1000; // 全局變量 static int e = 10000; // 靜態全局變量 int main(int argc, char * argv[]) { NSString * appDelegateClassName; @autoreleasepool { // Setup code that might create autoreleased objects goes here. appDelegateClassName = NSStringFromClass([AppDelegate class]); int a = 10; // 局部變量 static int b = 100; // 靜態局部變量 __block int c = 1000; void (^block)(void) = ^{ NSLog(@"Block中--\n a = %d \n b = %d\n c = %d \n d = %d \n e = %d",a,b,c,d,e); }; a = 20; b = 200; c = 2000; d = 20000; e = 200000; NSLog(@"Block上--\n a = %d \n b = %d\n c = %d \n d = %d \n e = %d",a,b,c,d,e); block(); NSLog(@"Block下--\n a = %d \n b = %d\n c = %d \n d = %d \n e = %d",a,b,c,d,e); } return UIApplicationMain(argc, argv, nil, appDelegateClassName); } 複製代碼
答案是面試
2019-04-04 04:50:58.508341+0800 Block_Test[19213:1138920] Block上-- a = 20 b = 200 c = 2000 d = 20000 e = 200000 2019-04-04 04:50:58.509229+0800 Block_Test[19213:1138920] Block中-- a = 10 b = 200 c = 2000 d = 20000 e = 200000 2019-04-04 04:50:58.509395+0800 Block_Test[19213:1138920] Block下-- a = 20 b = 200 c = 2000 d = 20000 e = 200000 複製代碼
解答:express
- (void)test{ __block Foo *foo = [[Foo alloc] init]; foo.fooNum = 20; __weak Foo *weakFoo = foo; self.block = ^{ NSLog(@"block中-上 fooNum = %d",weakFoo.fooNum); [NSThread sleepForTimeInterval:1.0f]; NSLog(@"block中-下 fooNum = %d",weakFoo.fooNum); }; dispatch_async(dispatch_get_global_queue(0, 0), ^{ self.block(); }); [NSThread sleepForTimeInterval:0.2f]; NSLog(@"end"); } 複製代碼
結果是swift
block中-上 fooNum = 20 end block中-下 fooNum = 0 複製代碼
weakFoo是一個弱指針,因此self.block對person是弱引用。 而後在併發隊列中經過異步函數添加一個任務來執行self.block();,因此是開啓了一個子線程來執行這個任務,此時打印fooNum值是20,而後子線程開始睡眠1秒鐘;與此同時主線程也睡眠0.2秒。 而因爲foo是一個局部變量,並且self.block對它也是弱引用,因此在test函數執行完後foo對象就被釋放了。再過0.8秒鐘,子線程結束睡眠,此時weakFoo所指向的對象已經變成了nil,因此打印的fooNum是0。markdown
[NSThread sleepForTimeInterval:0.2f];
改成[NSThread sleepForTimeInterval:2.0f];
呢?結果是併發
block中-上 fooNum = 20 end block中-下 fooNum = 20 複製代碼
由於子線程睡眠結束時主線程還在睡眠睡眠,也就是test方法還沒執行完,那person對象就還存在,因此子線程睡眠先後打印的fooNum都是20。app
__strong Foo *strongFoo = weakFoo;
,並改成打印strong.fooNum呢?結果仍是:框架
block中-上 fooNum = 20 end block中-下 fooNum = 20 複製代碼
__strong的做用就是保證在block中的代碼塊在執行的過程當中,它所修飾的對象不會被釋放,即使block外面已經沒有任何強指針指向這個對象了,這個對象也不會立馬釋放,而是等到block執行結束後再釋放。因此在實際開發過程當中__weak和__strong最好是一塊兒使用,避免出現block運行過程當中其弱引用的對象被釋放。異步
- (void)test{ self.age = 20; self.block = ^{ NSLog(@"%d",self.age); }; self.block(); } 複製代碼
答:會發生循環引用。 由於self經過一個強指針指向了block,而block內部又捕獲了self並且用強指針指向self,因此self和block互相強引用對方而形成循環引用。 若是要解決的話很簡單,加一個__weak typeof(self) weakSelf = self;
就好。
self.block();
呢?答: 同樣會引用,同樣會發生循環引用。
NSLog(@"%d",self.age);
改成NSLog(@"%d",_age);
呢?答:仍是會發生循環引用。由於_age,實際上就是self->age。
[UIView animateWithDuration:1.0f animations:^{ NSLog(@"%d",self.age); }]; dispatch_sync(dispatch_get_global_queue(0, 0), ^{ NSLog(@"%d",self.age); }); 複製代碼
答:不會。這裏的block其實是這個函數的一部分,是參數。雖然block強引用了self,可是self並無強引用block,因此沒事。
- (void)blockProblem { __block int a = 0; void (^block)(void) = ^{ self.string = @"retain"; NSLog(@"biboyang"); NSLog(@"biboyang%d",a); }; // block();//禁止 } 複製代碼
咱們能夠經過如下幾種方式來實現
- (void)blockProblemAnswer0:(void(^)(void))block { //動畫方法 [UIView animateWithDuration:0 animations:block]; //主線程 dispatch_async(dispatch_get_main_queue(), block); } 複製代碼
這裏兩個都是直接調用了原裝block的方法。
- (void)blockProblemAnswer1:(void(^)(void))block { [[NSBlockOperation blockOperationWithBlock:block]start]; } 複製代碼
直接使用NSOperation的方法去調用。注意,這個方法是在主線程上執行的。
- (void)blockProblemAnswer2:(void(^)(void))block { NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@?"]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; [invocation invokeWithTarget:block]; } 複製代碼
NSMethodSignature是方法簽名,封裝了一個方法的返回類型和參數類型,只有返回類型和參數類型。
- @? 表明了這個是一個block。
NSInvocation對象包含Objective-C消息的全部元素:目標、選擇器、參數和返回值。這些元素均可以直接設置,當NSncOcObjt對象被調度時,返回值自動設置。
NSInvocation對象能夠重複地分配到不一樣的目標;它的參數能夠在分派之間進行修改,以得到不一樣的結果;甚至它的選擇器也能夠改變爲具備相同方法簽名(參數和返回類型)的另外一個。這種靈活性使得NSInvocation對於使用許多參數和變體重複消息很是有用;您沒必要爲每一個消息從新鍵入稍微不一樣的表達式,而是每次在將NSInvocation對象分派到新目標以前根據須要修改NSInvocation對象。
- (void)blockProblemAnswer3:(void(^)(void))block { [block invoke]; } 複製代碼
咱們經過打印,能夠獲取到block的繼承線。
-> __NSMallocBlock__ -> __NSMallocBlock -> NSBlock -> NSObject
複製代碼
而後咱們查找 NSBlock的方法
(lldb) po [NSBlock instanceMethods] <__NSArrayI 0x600003265b00>( - (id)copy, - (id)copyWithZone:({_NSZone=} *)arg0 , - (void)invoke, - (void)performAfterDelay:(double)arg0 ) 複製代碼
咱們發現了一個invoke方法,這個方法實際上也是來自 NSInvocation。該方法是將接收方的消息(帶參數)發送到目標並設置返回值。
注意:這個方法是NSInvocation的方法,不是Block結構體中的invoke方法。
void *pBlock = (__bridge void*)block; void (*invoke)(void *,...) = *((void **)pBlock + 2); invoke(pBlock); 複製代碼
開始 (__bridge void*)block
將block轉成指向block結構體第一位的指針。而後去計算偏移量。
而後觀察block的內存佈局
struct Block_layout { void *isa; int flags; int reserved; void (*invoke)(void *, ...); struct Block_descriptor *descriptor; /* Imported variables. */ }; 複製代碼
在64位下,一個void指針佔了8byte。而int佔據4位,則flag和reserved一共佔據了8位,加一塊是16位。
咱們知道,一個 void*
佔據了8位, (void **)pBlock
表明了自己的8位地址長度。+2表示添加了兩倍的8位長度,也就是16位。到達了 void (*invoke)
方法。
而後咱們再調用 void (*invoke)(void *,...)
,這裏是block的函數指針,直接去調用就好。
static void blockCleanUp(__strong void(^*block)(void)){
(*block)();
}
- (void)blockProblemAnswer5:(void(^)(void))block {
__strong void(^cleaner)(void) __attribute ((cleanup(blockCleanUp),unused)) = block;
}
複製代碼
這裏能夠查看黑魔法__attribute__((cleanup))
- (void)blockProblemAnswer6:(void(^)(void))block { asm("movq -0x18(%rbp), %rdi"); asm("callq *0x10(%rax)"); } 複製代碼
咱們給一個block打斷點,並在lldb中輸入dis查看彙編代碼。
-> 0x1088c8d1e <+62>: movq -0x18(%rbp), %rax 0x1088c8d22 <+66>: movq %rax, %rsi 0x1088c8d25 <+69>: movq %rsi, %rdi 0x1088c8d28 <+72>: callq *0x10(%rax) 複製代碼
注意,必定要寫第一行。
不寫第一行的話,若是沒有攔截外部變量的話仍是沒問題的,可是一旦攔截到了外部變量,就會沒法肯定偏移位置而崩潰。
我最開始的思路是這樣的,將block的結構替換實現出來,做爲中間體用來暫存方法指針。而後一樣實現替換block的結構體,用來裝載。
//中間體 typedef struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }__block_impl; //接受體 typedef struct __block_impl_replace { void *isa_replace; int Flags_replace; int Reserved_replace; void *FuncPtr_replace; }__block_impl_replace; //替換方法 void hookBlockMethod() { NSLog(@"黃河入海流"); } void HookBlockToPrintHelloWorld(id block) { __block_impl_replace *ptr = (__bridge __block_impl *)block; ptr->FuncPtr_replace = &hookBlockMethod; } 複製代碼
注意,結構體裏的方法名不比和系統block中的方法名相同,這裏這麼寫只不過是爲了標明。 這裏事實上是會觸發一個警告 Incompatible pointer types initializing '__block_impl_replace *' (aka 'struct __block_impl_replace *') with an expression of type '__block_impl *' (aka 'struct __block_impl *')
警告咱們這兩個方法並不兼容。實際上,這兩個結構體裏的方法名並不相同,甚至個數不一樣均可以,可是必定要保證前四個成員的類型是對應了;前四個成員是存儲block內部數據的關鍵。 在四個成員下邊接着又其餘成員也是無所謂的。
typedef struct __block_impl_replace { void *isa_replace; int Flags_replace; int Reserved_replace; void *FuncPtr_replace; void *aaa; void *bbb; void *ccc; }__block_impl_replace; 複製代碼
好比這種方式,實際上方法依然成立。
固然,這種方式也是能夠優化的。好比說咱們就能夠吧中間結構體和替換block結合。
好比下面的這個就是優化以後的結果。
typedef struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }__block_impl; void OriginalBlock (id Or_Block) { void(^block)(void) = Or_Block; block(); } void HookBlockToPrintHelloWorld(id block) { __block_impl *ptr = (__bridge __block_impl *)block; ptr->FuncPtr = &hookBlockMethod; } ------------------ ------------------ void (^block)(void) = ^void() { NSLog(@"白日依山盡 "); }; HookBlockToPrintHelloWorld(block); block(); 複製代碼
這裏咱們就能夠打印出來 黃河入海流
了。
可是,咱們若是想要本來的方法也也打印出來該怎麼處理呢?
方法很簡單
void OriginalBlock (id Or_Block) { void(^block)(void) = Or_Block; block(); } void HookBlockToPrintHelloWorld(id block) { __block_impl *ptr = (__bridge __block_impl *)block; OriginalBlock(block); ptr->FuncPtr = &hookBlockMethod; } 複製代碼
保留原有block,並在該方法中執行原有的block方法。
咱們就能夠實現以下了
2018-11-19 17:12:16.599362+0800 BlockBlogTest[64408:32771276] 白日依山盡 2018-11-19 17:12:16.599603+0800 BlockBlogTest[64408:32771276] 黃河入海流 複製代碼
這裏我參考了網上的一些討論,並結合原有的思路,回答以下
static void (*orig_func)(void *v ,int i, NSString *str); void hookFunc_2(void *v ,int i, NSString *str) { NSLog(@"%d,%@", i, str); orig_func(v,i,str); } void HookBlockToPrintArguments(id block) { __block_impl *ptr = (__bridge __block_impl *)block; orig_func = ptr->FuncPtr; ptr->FuncPtr = &hookFunc_2; } ---------------- ---------------- void (^hookBlock)(int i,NSString *str) = ^void(int i,NSString *str){ NSLog(@"bby"); }; HookBlockToPrintArguments(hookBlock); hookBlock(1,@"biboyang"); 複製代碼
這樣就能夠打印出來
2018-11-19 17:12:16.599730+0800 BlockBlogTest[64408:32771276] 1,biboyang 2018-11-19 17:12:16.599841+0800 BlockBlogTest[64408:32771276] bby 複製代碼
第三題說實話我尚未實現出來,可是在北京參加swift大會的時候,和冬瓜討論過這個問題。 我當時的思路是在把block提出一個父類,而後在去統一修改。可是後來冬瓜介紹了fishhook框架,個人思路就變了。 在ARC中咱們使用的都是堆block,可是建立的時候是棧block,它會通過一個copy的過程,將棧block轉換成堆block,中間會有objc_retainBlock->_Block_copy->_Block_copy_internal方法鏈。咱們能夠hook這幾個方法,去修改。