Block学习笔记

作者: DeepChafferer | 来源:发表于2018-03-06 21:31 被阅读61次

    什么是Block

    Block是对C语言的扩展,可以简单总结为“带有局部变量的匿名函数”,它类似于js中的闭包,是一种不需要定义名字、可以在使用的时候临时定义、并且能够访问不是在Block内部定义的全局/局部/静态变量的"函数"。目前Block已经广泛应用于iOS开发中,常用于GCD、动画及各类回调。

    Block的声明、赋值与调用

    Block的声明

    // Block声明的一般格式为:返回值类型(^Block名)(参数列表);
    // eg: 声明一个没有返回值类型,有一个int类型参数叫做block_1的block
    
    void (^block_1)(int a);
    
    // 其中形参名字可以省略
    void (^block_1)(int);
    
    

    Block的赋值

    // Block赋值的一般格式为 xxBlock = ^(参数列表){ Block体};
    
    // eg: 给一个没有返回值类型,有一个int类型参数叫做block_1的block赋值
    
    block_1 = ^(int a) {
      NSLog(@"%d", a);
    };
    
    // 这里一般会将返回值类型省略,编译器可以从block体中确定返回值类型
    
    // 我们可以在声明一个block的时候同时给它赋值
    
    // eg: 声明一个没有返回值类型,有一个int类型参数叫做block_1的block并给它赋值
    
    void (^block_1) (int a) = ^(int a) {
      NSLog(@"%d", a);
    };
    
    

    Block的调用

    // Block调用的一般格式为 xxBlock(参数列表);
    
    // eg: 调用一个没有返回值类型,有一个int类型参数叫做block_1的block
    
    block_1(20);
    
    

    使用typedef定义Block类型

    如果想要声明多个具有相同返回值类型、相同参数列表的block,按照上面的声明方式来做的话就要写很多繁琐的代码,这时我们可以使用typdef来定义block类型。

    typedef void(^commonBlock)(int a);
    
    // 通过commonBlock这个别名来声明一系列相似的block
    
    commonBlock block_2;
    commonBlock block_3;
    
    // 相当于
    void(^block_2)(int a);
    void(^block_3)(int a);
    

    Block作为函数的参数

    Block作为C函数的参数

    // 1.定义一个形参为Block的C函数
    void useBlockForC(int(^aBlock)(int, int))
    {
        NSLog(@"result = %d", aBlock(10,10));
    }
    
    // 2.声明并赋值定义一个Block变量
    int(^addBlock)(int, int) = ^(int x, int y){
        return x + y;
    };
    
    // 3.以Block作为函数参数,把Block像对象一样传递
    useBlockForC(addBlock);
    
    // 集合一下2、3两点
    useBlockForC(^(int x, int y) {
        return x + y;
    });
    
    // 最终结果 打印输出 result = 20
    

    Block作为OC函数的参数

    // 1.定义一个形参为Block的OC函数
    - (void)useBlockForOC:(int(^aBlock)(int, int))aBlock
    {
        NSLog(@"result = %d", aBlock(10,10));
    }
    
    // 2.声明并赋值定义一个Block变量
    int(^addBlock)(int, int) = ^(int x, int y){
        return x + y;
    }; 
    
    // 3.以Block作为函数参数,把Block像对象一样传递
    [self useBlockForOC:addBlock];
    
    // 集合一下2、3两点
    [self useBlockForOC:^(int x, int y){
        return x + y;
    }];
    
    // 最终结果 打印输出 result = 20
    

    Block内访问外部变量

    Block内访问局部变量

    int tempValue = 10;
    void (^block_1) (void) = ^{
      NSLog(@"in block tempValue is %d", tempValue);
    };
    block_1();
    
    // 打印输出 in block tempValue is 10
            
    

    原理解析

    我们进入到main.m所在文件的目录,用Clang命令clang -rewrite-objc main.m可以将.m文件重新转成.cpp文件,转换后的main.cpp文件大概有近10W行样子,将光标移到最后往上找可以看到一个main函数

    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            int tempValue = 10;
            void (*block_1) (void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, tempValue));
    
            ((void (*)(__block_impl *))((__block_impl *)block_1)->FuncPtr)((__block_impl *)block_1);
    
        }
            return 0;
    }
    

    对照原来OC代码

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            int tempValue = 10;
            void (^block_1) (void) = ^{
                NSLog(@"in block tempValue is %d", tempValue);
            };
            
            block_1();
    
        }
            return 0;
    }
    
    

    我们可以看到block的定义转换成C++代码后变成了

    void (*block_1) (void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, tempValue));
    

    可以看到Block实际上就是一个指向结构体__main_block_impl_0的指针,其中第三个元素是局部变量tempValue的值,在main.cpp文件中全局搜索__main_block_impl_0可以看到__main_block_impl_0结构如下

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int tempValue;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _tempValue, int flags=0) : tempValue(_tempValue) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    

    调用block_1的代码如下

    ((void (*)(__block_impl *))((__block_impl *)block_1)->FuncPtr)((__block_impl *)block_1);
    

    可以看到block的调用实际上是指向结构体的指针block_1访问其FuncPtr元素,在定义block时为FuncPtr元素传进去的是__main_block_func_0方法,我们在搜索这个方法

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      int tempValue = __cself->tempValue; // bound by copy
      NSLog((NSString *)&__NSConstantStringImpl__var_folders_f0_sl9z3g2x12l7pns1wx_0yyx00000gn_T_main_191180_mi_0, tempValue);
    }
    

    可以看到在block定义的时候就将局部变量tempValue的值传了进去,所以在当tempValue在调用block之前改变并不会影响到block内部的值,并且在block内部是无法对其进行修改的。

    总结

    在Block定义时便将局部变量的值传给Block所指向的结构体,因此在调用Block之前对局部变量进行修改并不会影响Block内部的值,同时内部的值也是不可修改的

    Block内访问__block修饰的局部变量

    • 在局部变量前使用__block修饰,在声明Block之后、调用Block之前对局部变量进行修改,此时在调用Block局部变量的值是修改后新的值
    __block int tempValue = 10;
    void (^block_1) (void) = ^{
      NSLog(@"in block tempValue is %d", tempValue);
    };
    block_1();
    
    // 打印输出 in block tempValue is 20
            
    

    原理解析

    同样我们再次用Clang命令将main.m文件转成main.cpp文件,找到main函数

    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            __attribute__((__blocks__(byref))) __Block_byref_tempValue_0 tempValue = {(void*)0,(__Block_byref_tempValue_0 *)&tempValue, 0, sizeof(__Block_byref_tempValue_0), 10};
            void (*block_1) (void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_tempValue_0 *)&tempValue, 570425344));
    
            (tempValue.__forwarding->tempValue) = 20;
            ((void (*)(__block_impl *))((__block_impl *)block_1)->FuncPtr)((__block_impl *)block_1);
    
        }
            return 0;
    }
    

    我们发现用__block修饰的局部变量的定义变成了__attribute__((__blocks__(byref))) __Block_byref_tempValue_0 tempValue = {(void*)0,(__Block_byref_tempValue_0 *)&tempValue, 0, sizeof(__Block_byref_tempValue_0), 10};这一串代码,全局搜索__Block_byref_tempValue_0可以看到是一个如下的结构体

    struct __Block_byref_tempValue_0 {
      void *__isa;
    __Block_byref_tempValue_0 *__forwarding;
     int __flags;
     int __size;
     int tempValue;
    };
    

    再来看block的定义发现,在block定义的时候我们传入的不再是tempValue这个局部变量的值,而是一个指向tempValue的一个指针,所以在block内部是可以修改这个局部变量的值并且当block外部tempValue的值改变block内部也会跟着改变。

    总结

    使用__block修饰局部变量,在Block定义时便将局部变量的指针传给Block所指向的结构体,因此在调用Block之前对局部变量进行修改会影响Block内部的值,同时Block内部的值也是可以修改的

    Block内访问全局变量

    • 在block中可以访问全局变量
    int tempValue = 10;
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            void (^block_1) (void) = ^{
                NSLog(@"in block tempValue is %d", tempValue);
            };
            
            tempValue = 20;
            block_1();
    
        }
            return 0;
    }
    
    // 打印输出 in block tempValue is 20
    
    

    原理解析

    同样我们再次用Clang命令将main.m文件转成main.cpp文件,找到main函数

    int tempValue = 10;
    
    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            void (*block_1) (void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    
            tempValue = 20;
            ((void (*)(__block_impl *))((__block_impl *)block_1)->FuncPtr)((__block_impl *)block_1);
    
        }
            return 0;
    }
    

    观察上述代码我们可以发现在block定义的时候并没有将全局变量的值或则会指针传入进去我们再来观察下__main_block_func_0方法

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    
                NSLog((NSString *)&__NSConstantStringImpl__var_folders_f0_sl9z3g2x12l7pns1wx_0yyx00000gn_T_main_08df78_mi_0, tempValue);
            }
    

    可以看到输出打印的是全局变量tempValue的值。

    总结

    全局变量所占用的内存只有一份,供所有函数共同调用,在Block定义时并未将全局变量的值或者指针传给Block所指向的结构体,因此在调用Block之前对局部变量进行修改会影响Block内部的值,同时内部的值也是可以修改的

    Block内访静态变量

    • 在Block中可以访问静态变量
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            static int tempValue = 10;
            void (^block_1) (void) = ^{
                NSLog(@"in block tempValue is %d", tempValue);
            };
            
            tempValue = 20;
            block_1();
    
        }
            return 0;
    }
    
    // 打印输出 in block tempValue is 20
    

    原理解析

    同样我们再次用Clang命令将main.m文件转成main.cpp文件,找到main函数

    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            static int tempValue = 10;
            void (*block_1) (void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &tempValue));
    
            tempValue = 20;
            ((void (*)(__block_impl *))((__block_impl *)block_1)->FuncPtr)((__block_impl *)block_1);
    
        }
            return 0;
    }
    

    我们可以看到在block定义的时候传入的是tempValue的地址,调用block实际上是指向结构体的指针block_1访问其FuncPtr元素,我们接着看__main_block_func_0

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      int *tempValue = __cself->tempValue; // bound by copy
    
                NSLog((NSString *)&__NSConstantStringImpl__var_folders_f0_sl9z3g2x12l7pns1wx_0yyx00000gn_T_main_6fa3db_mi_0, (*tempValue));
            }
    

    可以看到NSLog的tempValue正是定义Block时为结构体传进去的静态变量tempValue的指针。

    总结

    在Block定义时便将静态变量的指针传给Block所指向的结构体,因此在调用Block之前对静态变量进行修改会影响Block内部的值,同时内部的值也是可以修改的

    Block的内存管理

    • 由于现在工程主要都是在ARC环境下,所以主要讨论ARC环境下Block的内存管理
    • 在ARC默认情况下,Block的内存存储在堆中,ARC会自动进行内存管理,程序员只需要避免循环引用即可
    • 当Block变量出了作用域,Block的内存会被自动释放
    void(^block_1)() = ^{
        NSLog(@"block called");
    };
    block_1();
    
    // 当block_1执行完成后系统会自动释放其内存
    
    • 在Block的内存存储在堆中时,如果在Block中引用了外面的对象,会对所引用的对象进行强引用,但是在Block被释放时会自动去掉对该对象的强引用,所以不会造成内存泄漏(循环引用)
    Person *p = [[Person alloc] init];
            
    void(^ block_1)() = ^{
        NSLog(@"%@", p);
    };
    block_1();
            
    // Person对象在这里可以正常被释放
    
    • 如果对象内部有一个Block属性,而在Block内部又访问了该对象,那么会造成循环引用
    @interface Person: NSObject
    
    @property (nonatomic, copy) void(^block_1)();
    
    @end
    
    @implementation Person
    
    - (void)dealloc
    {
        NSLog(@"Person dealloc");
    }
    
    @end
    
    
    Person *p = [[Person alloc] init];
            
    p.block_1 = ^{
        NSLog(@"%@", p);
    };
    p.block_1();
            
    // 因为block_1作为Person的属性,采用copy修饰符修饰(保证Block在堆里面,以免Block在栈中被系统释放),所以Block会对Person对象进行一次强引用,导致循环引用无法释放
    
    

    循环引用解决方案

    Person *person = [[Person alloc] init];
    
    // 这里还可以使用__weak typeof(Person) *weakPerson = person;
    __weak Person *weakPerson = person;
    
    person.block_1 =  ^{
        NSLog(@"%@", weakPerson);
    };
    
    person.block_1();
    
    

    这样Person对象就可以正常被释放

    相关文章

      网友评论

        本文标题:Block学习笔记

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