探秘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地址