美文网首页
iOS高级编程 --Block

iOS高级编程 --Block

作者: shengchang | 来源:发表于2020-06-19 16:10 被阅读0次

    概要

    • 带有自动变量的匿名函数

    Blocks模式

    语法(表达式/函数区别于类型变量)

    ^返回值类型(可省)+参数列表(可省)+表达式

    ^int (int count){
        return count + 1;
    }
    

    Block类型变量

    //声明block类型变量
    int (^blk)(int)
    //函数指针
    int(*funcptr)(int) = &func
    
    • block类型变量与c语言类型变量一样可以作为自动变量、函数参数、全局变量、静态变量、静态全局变量
    • 通过block类型变量调用Block与c语言函数调用没有区别
    //使用typedef简化类型变量
    typedef int (^blk)(int)
    

    截获自动变量值

    在Block表达式中使用自动变量时,或截获保存其瞬间值,之后的改动不会影响截获值

    __block说明符

    在Block表达式中++重新赋值++自动变量(对象类型)时,会产生编译错误,需要声明__block类型

    截获的自动变量

    • 在Block表达式中使用截获的自动变量没问题,但是赋值会编译报错,例如NSMutableArray对象可以正常添加元素,但是重新赋值给这个指针就会编译报错
    使用c语言数组会编译报错
    const char text[] = "hello"
    void (^blk)(void) = ^{
        char text = text[2];
    }
    Blocks中没有对c语言数组支持,使用指针可以解决
    const char *text = "hello"
    void (^blk)(void) = ^{
        char text = text[2];
    }
    

    Blocks实现

    Block的实质

    将oc代码转为c++代码的clang指令

    clang -rewrite-objc 源文件名
    

    block的本质是__main_block_impl_0(mian所在的函数名,0函数调用顺序)结构体实例的调用,结构体为

    struct __main_block_impl_0{
        void *isa;//初始化为&_NSConcreteStackBlock,相当于对象的class_t的结构体
        int flags;
        int reserved;
        void *Funptr;//初始化指向调用函数__main_block_func_0
        struct __main_block_desc_0 *desc;
    }
    

    class_t结构体

    struct class_t{
        struct class_t *isa;
        struct class_t *superclass;
        Cache cache;//方法缓存
        IMP *vtable;//方式实现
        unitptr_data_NEVER_USE
    }
    

    截获自动变量值

    截获的自动变量会赋值到Block的成员变量当中,Block中为使用的自动变量不会截获

    struct __main_block_impl_0{
        void *isa;
        int flags;
        int reserved;
        void *Funptr;
        struct __main_block_decs_0 *decs;
        //截获的成员变量,构造函数初始化时进行赋值
        int val;
        const char *fmt;
    }
    

    Block中不能使用c语言数组类型的变量,这是因为c语言数组类型变量不能赋值给c语言数组类型变量,可以使用指针来解决

    void func(char a[10]){
        char b[10] = a;//c语言数组赋值给数组非法
        printf("%d\n",b[0])
    }
    int main(){
        char a[10] = {2};
        func(a);
    }
    
    

    __block说明符

    在block中保存值的第一种方法是使用c语言中的静态变量,全局变量,静态全局变量;第二种则是使用"__block储存域说明符"

    __block编译转换的代码:

    struct __Block_byref_val_0{
        void *isa;
        __Block_byref_val_0 *__forwarding;//指向自身
        int __flags;
        iny __size;
        int val;//使用值
    }
    
    struct __main_block_impl_0{
        struct __block_impl impl;
        struct __main_block_decs_0 *Decs;
        __Block_byref_val_0 *val;//block使用__block结构体
        ...
    }
    

    __block变量的赋值代码的转换

    ^{val = 1};
    

    转为:

    static void __main_block_func_0(struct __main_block_impl_0 *__cself){
        __Block_byref_val_0 *val = __cself->val;
        (val->__forwarding->val) = 1;//通过forwarding指针访问使用值(主要为了从栈拷贝到堆中访问同一个内存区域)
    }
    

    Block存储域

    Block类的存储区域

    设置对象的区域
    _NSConreteStackBlock
    _NSConcreteGlobalBlock 数据区域(.data区)
    _NSConcreteMallocBlock

    存储在程序的数据区域的情况:

    • 记述在全局变量的地方有Block语法
    • block语法中未使用应截获的对象
    impl.isa = &_NSConcreteGlobalBlock
    

    除此之外存储在栈区;

    impl.isa = &_NSConcreteStackBlock
    

    为了解决变量作用域结束时,栈上的block和__block变量被释放的问题,将栈上Block复制到堆上

    impl.isa = &_NSConcreteMallocBlock
    

    __block结构体变量中的__forwarding指针可以保证无论在栈上还是堆上,都能正确访问__block变量;

    ARC有效时,大多数情形编译器都能够恰当的进行判断,自动的将block从栈上复制到堆上;例如将block作为函数返回值就会自动生成objc_retainBlock(),即_Block_copy;

    栈上复制到堆上会消耗CUP资源,如果在栈上能够使用就不必复制,某些情况就需要手动复制;

    - (NSArray *)getBlockArray{
        int val = 10;
        //需要执行copy复制到堆上,否则变量作用域结束,会销毁,执行异常
        return [[NSArray alloc] initWithObjects:[^{NSLog(@"blk1--%d",val);} copy], [^{NSLog(@"blk2--%d",val);} copy], nil];
    }
    
        typedef void (^blk_t)(void);
        testObj *obj = [[testObj alloc] init];
        NSArray *arr = [obj getBlockArray];
        blk_t blk = [arr objectAtIndex:1];
        NSLog(@"blk class %@",[blk class]);
        blk();
    

    按不同储存区域copy之后的变化:

    设置对象的区域 复制效果
    _NSConreteStackBlock 从栈复制到堆上
    _NSConcreteGlobalBlock 数据区域(.data区) 什么都不做
    _NSConcreteMallocBlock 引用计数增加

    ARC有效时多次复制不会有问题

    __Block变量存储区域

    1.当Block从栈中复制到堆中时,所截获的__block也会从栈中赋值到堆中,并且被Block所持有;当多个Block截获__block变量时,__block引用计数的会增加,符合引用计数的内存管理思考方式.

    2.使用__block变量结构体中的__fowarding指针可以使得不管__block变量配置的栈上还是堆上都可以正确访问解释如下:

    __block int val = 0;
    void (^blk)(void) = [^{++ val;} copy];//堆上__block变量
    ++ val;//栈上__block变量
    blk()
    

    当__block从栈上复制到堆上时,栈上的__forwarding指针会指向堆上的__Block_byref_val_0结构体实例,确保访问同一个__block变量

    val->__forwarding->val;//栈上和堆上都是这样访问
    

    截获对象

    概要:Block中捕获对象的持有与废弃,对象自动从栈copy复制到堆的情况

        typedef void (^blk_t)(id obj);
        blk_t blk;
        {
            NSMutableArray *array1 = [[NSMutableArray alloc] init];
            blk = [^(id obj){
                [array1 addObject:obj];
                NSLog(@"array count : %ld",[array1 count]);
            } copy];
        }
        blk([[NSObject alloc] init]);
        blk([[NSObject alloc] init]);
        blk([[NSObject alloc] init]);
        NSLog(@"class:%@",[blk class]);
    

    clang编译之后:

    struct __main_block_impl_0{
        struct __block_impl impl;
        struct __main_block_desc_0 *Desc;
        id __strong array;//注意使用了__strong修饰符
    }
    

    c语言结构体中是不能使用__strong修饰符的对象类型,但是此处oc运行库可以准确把握Block从栈复制到堆以及从堆中废弃的时机;为此,需要在__main_block_desc_0结构体中增加copy和dispose成员变量,以及赋值的成员变量函数__main_block_copy_0和__main_block_dispose_0;两个函数定义如下:

    __Block_object_assign函数相当于retain实例方法,持有对象

    static void __main_block_copy_0(struct __main_block_impl_0 *dst,struct __main_block_impl_0 *src){
        __Block_object_assign(&dst->array,src->array,BLOCK_FIELD_IS_OBJECT);//BLOCK_FIELD_IS_OBJECT表示对象;__block类型则是BLOCK_FIELD_IS_BYREF
    }
    

    __Block_object_dispose函数相当于release的实例方法,谁都不持有Block时调用

    static void __main_block_dispose_0(struct __main_block_impl_0 *dst,){
        __Block_object_dispose(&dst->array,BLOCK_FIELD_IS_OBJECT);
    }
    

    这两个函数不会主动调用,从栈copy到堆(本质调用_Block_copy)的时机如下:

    • 调用Block函数的copy方法
    • Block作为函数的返回值
    • 将Block赋值给__strong修饰符的id类型或者赋值给Block类型的成员变量
    • 方法名中含有usingBlock的cocoa框架方法或者GCD中传递的Block

    __block也是同理,截获的__block从栈复制到堆上时调用copy函数,不再持有__block变量时调用dispose函数,所使用的参数是BLOCK_FIELD_IS_BYREF

    __block变量和对象

    __block修饰符可以指定任何类型的自动变量,例如指定对象类型

    __block id object = [[NSObject alloc] init];
    

    在Block中使用对象类型的的自动变量时,当Block从栈拷贝到堆中时,调用吧_Block_object_assign函数,持有Block捕获的对象;当堆上的Block废弃时,调用_Block_object_dispose函数,释放截获的对象.__block变量也会发生同样的过程,参数不一样BLOCK_FIELD_IS_OBJECT/BLOCK_FIELD_IS_BYREF

    使用__weak修饰的对象则不能超出变量作用域存在,因为Block不持有对象,会被释放

    Block循环引用

    有两种方式:1.使用weak临时变量;2使用__block变量类型,在Block函数中将捕获的变量值为nil

    一个循环引用的例子:

    typedef void (^blk_t)(void);
    @interface testObj : NSObject{
        blk_t blk_;//testObj类持有block
    }
    @end
    @implementation testObj
    - (instancetype)init{
        if (self = [super init]) {
            blk_ = ^{NSLog(@"self = %@",self);};//block赋值给成员变量,block从栈拷贝到堆中;self是__strong修饰的id类型,block持有self
        }
        return self;
    }
    
    
    1.第一种方法,使用__weak修饰符的方式
    id __weak weakSelf = self;
    blk_ = ^{NSLog(@"self = %@",weakSelf);};//Block结构体中捕获的self为__weak类型,不再持有self
    

    1.2.面向iOS4的程序可以使用__unsafe_unretained修饰符.由于在调用block函数时,self必定是存在的,不用担心调用时weakSelf为空,产生悬垂指针的情况

     id __unsafe_unretained weakSelf = self;
     blk_ = ^{NSLog(@"self = %@",weakSelf);};
    

    1.3.Block中使用了成员变量,捕获的也是self

    typedef void (^blk_t)(void);
    @interface testObj : NSObject{
        blk_t blk_;//testObj类持有block
        id obj_;
    }
    @end
    @implementation testObj
    - (instancetype)init{
        if (self = [super init]) {
            blk_ = ^{NSLog(@"obj_ = %@",obj_);};//obj_只是对象的成员变量,捕获的是self
        }
        return self;
    }
    

    使用个临时weak变量传到Block中(或者__unsafe_unretained)

    id __weak weakObj = obj_;
    blk_ = ^{NSLog(@"obj_ = %@",weakObj);}
    
    2.第二种方法,使用__block变量
    __block id tmp = obj_;
    blk_ = ^{
            NSLog(@"obj_ = %@",tmp);
            tmp = nil;//将对象置位nil,使得__block结构体类型__Block_byref_tmp_0不再持有tmp对象
            }
    

    如果不执行blk_()调用,则会引起内存泄露,Block->__Block变量->tmp对象->Block(成员函数)

    使用__block变量的优点:

    • 通过__block变量可以控制对象的持有期
    • 在ios4的环境中使用,不必担心悬空指针(unsafe_unretained)
    • 在执行Block时可以动态的决定是否将nil或者其他对象赋值到__block变量中

    缺点:

    • 为避免循环引用,必须执行block调用

    copy/release

    1.ARC无效时copy和release;2.ARC有效无效时__block解决循环引用的区别

    1.ARC无效时,一般需要手动从栈复制到堆中.
    blk_t blk_on_heap = [blk_on_stack copy];
    [blk_on_heap release];
    
    • 推荐使用copy来持有Block,使用retain的话,如果是[blk_on_stack retain]则不起作用;
    • 在c语言中可以使用Block_copy()/Block_release()来替换
    2.ARC有效无效时__block解决循环引用的区别

    ARC无效时,使用了__Block修饰的对象类型,该对象不会retain,即__block不持有对象;无__Block修饰则会自动retain.在解决循环引用上与ARC有效时有区别:

    __block id tmp = self;
    blk_ = ^{NSLog("self = %@",tmp)};//不需要tmp置nil,因为ARC无效时__block不持有对象
    

    相关文章

      网友评论

          本文标题:iOS高级编程 --Block

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