美文网首页
iOS探索:Block解析浅谈

iOS探索:Block解析浅谈

作者: 熊猫超人biubiubiu | 来源:发表于2018-12-19 18:07 被阅读0次

    什么是Block

    • Block是将函数及其执行上下文封装起来的对象

    接下来让我们通过源码来看一看Block的本质

    WX20181219-105950@2x.png
    • 我们在一个方法中写了三行代码,第一行是定义了一个局部变量,第二行是一个Block,第三行是这个Block的调用

    这里我们通过一个clang的编译命令clang -rewrite-objc xxx.m来看一下源码的实现

    WX20181219-111003@2x.png
    • 我们的那段代码通过编译器编写后,首先第一行I代表的是一个实例方法后面的是对象和方法名,传了两个参数一个是self,一个是选择器因子

    • 然后我们方法中的第一行代码在编译后没有发生改变,我们着重看一下Block方法编译后的改变

    • 首先我们可以看到__BlockOneObj__testMethod_block_impl_0这样一个结构体,在这个结构体中传递了几个参数,第一个参数(void*)__BlockOneObj__testMethod_block_func_0我们通过名字可以知道这是一个无类型的函数指针,第二个参数&__BlockOneObj__testMethod_block_desc_0_DATA是一个Block相关描述的结构体然后取地址符,第三个参数muIntNum就是我们定义的局部变量。最后取这个结构体地址强制转换赋值给我们定义的这个Block

    然后我们来看看__BlockOneObj__testMethod_block_impl_0这个结构体中有什么具体操作,如下图

    WX20181219-140037@2x.png

    其中第一个结构体里面又是什么数据结构呢,请看下图

    WX20181219-140536@2x.png

    在我们上面介绍的结构体下面还有一个函数,具体解释请看下图

    WX20181219-141327@2x.png

    那么什么是Block的调用呢

    WX20181219-143015@2x.png

    Block的调用其实就是函数的调用,从源码中我们可以看出来

    • 首先先对这个Block进行一个强制类型转换(__block_impl *)Block

    • 之后又取出它之中的成员变量FuncPtr(函数指针),找到我们上面解析的结构体和函数,在其中拿到对应的函数调用,然后把其中的参数传递进去,一个参数是我们这个Block本身,一个是我们传递的2,然后就回去调用__BlockOneObj__testMethod_block_func_0函数,最终进行调用

    Block截获变量

    首先我们先来看一段代码

    - (void)testMethod {
        
        int muIntNum = 6;
        int(^Block)(int) = ^int(int num){
            return num *muIntNum;
        };
        
        muIntNum = 4;
        Block(2);
    }
    
    

    这段代码执行完Block(2)返回的值是多少呢?-------答案是12
    接下来我们看一下为什么是12以及Block截获变量的本质是什么

    • 对于基本数据类型的局部变量截获其值

    • 对于对象类型的局部变量连同其所有权修饰符一起截获

    • 对于局部静态变量是以指针形式去截获

    • 对于全局变量和静态全局变量不截获

    下面直接上代码

    #import "BlockTwoObj.h"
    
    //全局变量
    int global_var = 4;
    //全局静态变量
    static int static_global_var = 5;
    
    @implementation BlockTwoObj
    
    - (void)testMethodTwo {
        
        //基本数据类型的局部变量
        int var = 1;
        //对象类型的的局部变量
        __unsafe_unretained id unsafe_obj = nil;
        __strong id strong_obj = nil;
        
        //局部静态变量
        static int static_var = 3;
        
        void(^Block)(void) = ^{
            
            NSLog(@"基本数据类型局部变量:%d", var);
            NSLog(@"对象类型局部变量(__unsafe_unretained修饰):%@", unsafe_obj);
            NSLog(@"对象类型局部变量(__strong修饰):%@", strong_obj);
            
            NSLog(@"局部静态变量:%d", static_var);
            
            NSLog(@"全局变量:%d", global_var);
            NSLog(@"全局静态变量:%d", static_global_var);
        };
        
        Block();
    }
    
    @end
    

    接下来我们通过clang命令clang -rewrite-objc -fobjc-arc xxx.m来看一下源码

    WX20181219-152500@2x.png
    • 在这张图中可以很清晰的看到Block中的变量截获,其中需要注意的是对于局部的静态变量截获的是指针,也就是说如果后面这个局部静态变量发生了修改,那么Block中使用的是最新的值

    __block修饰符

    我们在什么情况下使用__block修饰符呢?
    一般情况下,对被截获变量进行赋值操作需要添加__block修饰符,这里需要注意的是赋值不等于是使用,切记!!!

    例如在下面的代码中是否需要__block修饰符来修饰

    NSMutableArray *muArr = [[NSMutableArray alloc] init];
        
        void(^Block)(void) = ^{
            //这里只是做了添加操作,并非赋值,所以不需要用__block进行修饰
            [muArr addObject:@"111"];
        };
        
        Block();
    

    那么在下面的代码段当中呢?

    __block NSMutableArray *muArrOther = nil;
         void(^BlockOther)(void) = ^{
            //这里做了赋值操作,所以需要用__block进行修饰,否则会出现编译报错
            muArrOther = [NSMutableArray array];
        };
        
        BlockOther();
    

    对变量进行赋值时

    • 需要__block修饰符修饰的是局部变量(包括基本数据类型和对象类型)

    • 不需要__block修饰符修饰的是静态局部变量、全局变量和静态全局变量,因为对于全局变量和静态全局变量不涉及到变量的截获,而对于静态局部变量呢,是通过使用指针来操作对应的变量的,所以也不需要修饰

    下面请看一段代码,还是我们上面的那个例子

    - (void)testMethod {
        
        __block int muIntNum = 6;
        int(^Block)(int) = ^int(int num){
            return num *muIntNum;
        };
        
        muIntNum = 4;
        Block(2);
    }
    

    此时Block返回的是8,这里是为什么呢,我们只是用了__block来修饰

    • 因为在这里会发生一个非常奇妙的变化,__block修饰的变量变成了对象

    请看下面的流程图


    WX20181219-162224@2x.png
    • 首先__block int muIntNum会被转化成第一个这样一个结构体,其中具有isa指针,我们也可以理解成一个对象

    • 从这个角度来看muIntNum经过编译后就会变成一个对象,通过__forwarding指针去找到对应的对象,然后进行赋值

    • 刚才我们看到的代码段是在栈上,在__block变量中有一个__forwarding指针,而这个指针指向的是自己,这里要注意的是前提是在栈上,如果在堆上,这个__forwarding指针指向的就不是自己了,在下面会讲到

    • 所以在栈上我们修改这个变量的值,就会通过__forwarding指针找到自己本省去修改这个变量的值

    那么这里有一个问题就是我们在栈上这个__forwarding指向的是自己到底有什么用呢?我们完全可以通过访问成员变量来修改,为什么还需要这个指针呢,请继续往下看

    Block的内存管理

    Block有三种类型

    • _NSConcreteGlobalBlock 全局Block

    • _NSConcreteStackBlock 栈Block

    • _NSConcreteMallocBlock 堆Block

    Block的Copy操作

    WX20181219-172635@2x.png
    • 当我们栈上的Block通过copy在堆上产生一个一样的Block,有相同的Block和__block变量,当变量作用于结束后,栈上的Block对象就会被销毁,而堆上的block依旧存在,所有如果栈上Block不用copy拷贝到堆上,在作用于销毁后会因为找不到Block对象而崩溃

    • 当然我们在这里有一个问题,假如说在MRC环境下,如果在栈上进行了copy操作,会不会产生内存泄漏,答案是肯定的,相当于一个对象alloc出来,但是并没有对应的relese操作一样

    WX20181219-173758@2x.png
    • 当我们栈上的Block经过copy操作后,在堆上会产生一个一样的Block,在栈中的Block中的__forwarding指针指向的事堆上Block的__block变量,并且在堆上Block的__forwarding指针也是指向的它自己的__block变量

    参考书籍

    Objective - C 高级编程:iOS与OS X多线程和内存管理

    Github

    Demo

    相关文章

      网友评论

          本文标题:iOS探索:Block解析浅谈

          本文链接:https://www.haomeiwen.com/subject/onpdkqtx.html