探祕Block(四):修改Block的實現

原做於:2018-10-08 GitHub Repo:BoyangBloggit

這裏將經過幾道面試題來擴展知識。 這幾道題有幾個取自sunnyxxgithub

Question1 下面代碼運行結果是什麼??

#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

  • block在捕獲普通的局部變量時是捕獲的a的值,後面不管怎麼修改a的值都不會影響block以前捕獲到的值,因此a的值不變。
  • block在捕獲靜態局部變量時是捕獲的b的地址,block裏面是經過地址找到b並獲取它的值。因此b的值發生了改變。
  • __block是將外部變量包裝成了一個對象並將c存在這個對象中,實際上block外面的c的地址也是指向這個對象中存儲的c的,而block底層是有一個指針指向這個對象的,因此當外部更改c時,block裏面經過指針找到這個對象進而找到c,而後獲取到c的值,因此c發生了變化。
  • 全局變量在哪裏均可以訪問,block並不會捕獲全局變量,因此不管哪裏更改d和e,block裏面獲取到的都是最新的值。

Question2 下面代碼的運行結果是什麼?

- (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

  • 換個方式問:若是在block內部加上__strong Foo *strongFoo = weakFoo;,並改成打印strong.fooNum呢?

結果仍是:框架

block中-上 fooNum = 20
end
block中-下 fooNum = 20
複製代碼

__strong的做用就是保證在block中的代碼塊在執行的過程當中,它所修飾的對象不會被釋放,即使block外面已經沒有任何強指針指向這個對象了,這個對象也不會立馬釋放,而是等到block執行結束後再釋放。因此在實際開發過程當中__weak和__strong最好是一塊兒使用,避免出現block運行過程當中其弱引用的對象被釋放。異步

Questime3 下面的代碼會發生什麼?

- (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。

Question4 下面會發生循環引用嗎?

[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,因此沒事。

Question5 如何在禁止直接調用block的狀況下繼續使用block?

- (void)blockProblem {
    __block int a = 0;
    void (^block)(void) = ^{
        self.string = @"retain";
        NSLog(@"biboyang");
        NSLog(@"biboyang%d",a);
    };
// block();//禁止
}
複製代碼

咱們能夠經過如下幾種方式來實現

1.別的方法直接調用

- (void)blockProblemAnswer0:(void(^)(void))block {
    //動畫方法 
    [UIView animateWithDuration:0 animations:block];   
    //主線程
    dispatch_async(dispatch_get_main_queue(), block);
}
複製代碼

這裏兩個都是直接調用了原裝block的方法。

2.NSOperation

- (void)blockProblemAnswer1:(void(^)(void))block {
    [[NSBlockOperation blockOperationWithBlock:block]start];
}
複製代碼

直接使用NSOperation的方法去調用。注意,這個方法是在主線程上執行的。

3.NSInvocation

- (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對象。

4.invoke方法

- (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方法。

5.block的struct方法

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的函數指針,直接去調用就好。

6.attribute((cleanup))方法

static void blockCleanUp(__strong void(^*block)(void)){
    (*block)();
}
- (void)blockProblemAnswer5:(void(^)(void))block {
    __strong void(^cleaner)(void) __attribute ((cleanup(blockCleanUp),unused)) = block;
}
複製代碼

這裏能夠查看黑魔法__attribute__((cleanup))

7.彙編方法

- (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)
複製代碼

注意,必定要寫第一行。

不寫第一行的話,若是沒有攔截外部變量的話仍是沒問題的,可是一旦攔截到了外部變量,就會沒法肯定偏移位置而崩潰。

Question3 HookBlock

我才疏學淺,只對第一第二個有實現,第三個問題有思路可是確實沒寫出來(😌)。

第一題

我最開始的思路是這樣的,將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這幾個方法,去修改。

demo地址

相關文章
相關標籤/搜索