美文网首页
iOS-浅谈OC中Block的变量捕获和__block修饰符

iOS-浅谈OC中Block的变量捕获和__block修饰符

作者: 晴天ccc | 来源:发表于2019-07-08 13:49 被阅读0次

目录

  • 变量捕获
  • 变量
    ---- 变量的基本知识
    ---- 为什么要捕获变量?
    ---- 为什么局部变量需要捕获?
    ---- 为什么全局变量不用捕获?
    ---- self会被捕获吗?
    ---- 成员变量会被捕获吗?
  • 局部变量捕获和数值修改探究
    ---- 局部自动auto变量
  • 静态static变量捕获和数值修改探究
  • 全局变量捕获和数值修改探究
  • __block修饰符

变量捕获

如果Block的执行体使用了外界的局部变量,为了保证Block内部能够正常访问外部的变量,Block有一个捕获变量的机制。
相当于往Block结构体里增加一个成员变量,把值传递给这个成员变量,分为值传递指针传递
如果执行体使用了外界的全局变量,则不需要捕获,直接使用即可。

变量

  • 变量的基本知识

首先变量可以分为两种:局部变量和全局变量。
局部变量分为:局部自动auto变量和局部静态static变量。
全局变量分为:全局变量和全局静态static变量。
  • 为什么要捕获变量?

因为变量有作用域的限制,在Block里面使用Block外声明的局部变量,相当于跨函数使用这个局部变量。
如果不存一份到Block里面,是无法使用的,会造成访问无效内存,因为外面的局部变量有可能过了作用域就会自动被销毁。
  • 为什么局部变量需要捕获?

因为局部变量只能在方法内部访问,离开作用域(大括号)就会自动销毁。
  • 为什么全局变量不用捕获?

因为作用域是全局,无论方法内外都可以随时访问。
  • self会被捕获吗?

会,因为self也是局部变量,我们来回想一下,在OC里调用方法实际上会传递self指针的参数,而且捕获的是指针,所以属于引用传递。
objc_msgSend(id self, SEL _cmd, ...)
所以我们之所以能在每一个方法中使用self,就是因为默认传入self变量。
  • 成员变量会被捕获吗?

会,因为访问的成员变量也是局部变量。

局部变量捕获和数值修改探究

  • 局部变量只能在方法内部访问,离开作用域(大括号)就会自动销毁。
  • Block内访无法修改局部(自动auto)变量。
  • 局部自动auto变量

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        // 定义一个局部变量
        int global = 100;
        NSLog(@"global Memory address is :%p ",&global);
        // 定义一个Block
        void (^aBlock)(void);
        // 把Block指向一个代码块
        aBlock = ^{
            NSLog(@"in block global memory address is :%p ",&global);
            NSLog(@"global is :%d",global);
        };
        // 修改局部变量的值
        global = 101;
        //调用Block
        aBlock();
        
    }
    return 0;
}
 
global Memory address is :0x16fdff2bc
in block global memory address is :0x1011040a0
global is :100

从结果来看到在Block中不可以直接修改局部变量,且两个global内存首地址不同。

我们把OC代码编译成C/C++来看下底层实现,仅截取部分如下:

#pragma clang assume_nonnull end

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

// Block结构体
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int global;  // Block捕获变量后相当于往Block结构体里增加一个成员变量
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _global, int flags=0) : global(_global) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
// Block的具体代码区执行区
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int global = __cself->global; // bound by copy
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_j2_bxd08__54lzbn226msl27gww0000gn_T_main_2eb08c_mi_1,&global);
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_j2_bxd08__54lzbn226msl27gww0000gn_T_main_2eb08c_mi_2,global);
}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {
    
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
 
        int global = 100;
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_j2_bxd08__54lzbn226msl27gww0000gn_T_main_2eb08c_mi_0,&global);

        void (*aBlock)(void);
        aBlock = ((void (*)())&__main_block_impl_0(
                                                   (void *)__main_block_func_0,
                                                   &__main_block_desc_0_DATA,
                                                   global) );

        global = 101;

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

    }
    return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

  • 我们看到首先在main函数中声明并初始化局部变量int global = 100;
  • 初始化Block,给__main_block_impl_0这个结构体传入局部变量global也就是数值100,属于值传递
  • Block捕获变量后,往Block结构体中增加一个局部变量int global,来接收这个数值100
  • 因为是值传递所以在main函数中int global的变化不会影响Blockint global的值,不存在关联关系。
  • 具体细节可以看我上面的注释。

静态static变量捕获和数值修改探究

静态变量:用static修饰的变量,特点是在程序运行过程中,一直在内存中存在,且只能在方法内部访问。

Block内可以直接访问和修改静态变量。

    // 定义一个静态变量
    static int global = 100;
    global Memory address is :0x102c68b20
    in block global memory address is :0x102c68b20
    global is :101 

编译成C/C++代码进行查看:

#pragma clang assume_nonnull end

// Block结构体
struct __main_block_impl_0 {
 struct __block_impl impl;
 struct __main_block_desc_0* Desc;
 int *global;
 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_global, int flags=0) : global(_global) {
   impl.isa = &_NSConcreteStackBlock;
   impl.Flags = flags;
   impl.FuncPtr = fp;
   Desc = desc;
 }
};
// Block的具体代码区执行区
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
   int *global = __cself->global; // bound by copy
   NSLog((NSString *)&__NSConstantStringImpl__var_folders_j2_bxd08__54lzbn226msl27gww0000gn_T_main_cc8127_mi_1,&(*global));
   NSLog((NSString *)&__NSConstantStringImpl__var_folders_j2_bxd08__54lzbn226msl27gww0000gn_T_main_cc8127_mi_2,(*global));
}

static struct __main_block_desc_0 {
 size_t reserved;
 size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {
   
   /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;

       static int global = 100;
       NSLog((NSString *)&__NSConstantStringImpl__var_folders_j2_bxd08__54lzbn226msl27gww0000gn_T_main_cc8127_mi_0,&global);

       void (*aBlock)(void);
       aBlock = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &global));

       global = 101;
       ((void (*)(__block_impl *))((__block_impl *)aBlock)->FuncPtr)((__block_impl *)aBlock);

   }
   return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

  • 我们看到首先在main函数中声明并初始化静态局部变量static int global = 100;
  • 初始化Block__main_block_impl_0这个结构体传入变量global的指针,属于指针传递
  • Block捕获变量后相当于往Block结构体里增加一个局部变量int *global,来接收这个指针
  • 因为是指针传递,所以后续以后续在__main_block_func_0具体执行方法区中首先拿到global的指针地址。
  • 通过地址访问和修改内存上的内容
  • 具体细节可以看我上面的注释。

补充:static变量一直在内存中存在,所以Block无论什么时候执行,都可以访问到static的指针。

全局变量捕获和数值修改探究

全局变量:在程序运行过程中,一只在内存中存在,可以被所有方法访问。

Block内可以直接访问和修改全局变量。

#import <Foundation/Foundation.h>

// 定义一个全局变量
int global = 100;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
         
        NSLog(@"global Memory address is :%p ",&global);
        // 定义一个Block
        void (^aBlock)(void);
        // 把Block指向一个代码块
        aBlock = ^{
            NSLog(@"in block global memory address is :%p ",&global);
            NSLog(@"global is :%d",global);
        };
        // 修改全局变量的值
        global = 101;
        //调用Block
        aBlock();
        
    }
    return 0;
}
    global Memory address is :0x102c68b20
    in block global memory address is :0x102c68b20
    global is :101 

编译成C/C++代码进行查看:

#pragma clang assume_nonnull end

int global = 100;

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_j2_bxd08__54lzbn226msl27gww0000gn_T_main_527828_mi_1,&global);
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_j2_bxd08__54lzbn226msl27gww0000gn_T_main_527828_mi_2,global);
}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_j2_bxd08__54lzbn226msl27gww0000gn_T_main_527828_mi_0,&global);

        void (*aBlock)(void);

        aBlock = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

        global = 101;

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

    }
    return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

  • Block没有对全局变量进行捕获。
  • 全局变量可以被程序内所有方法直接访问,也就不用block捕获。
  • 剩下的这里不过多赘述了。

__block修饰符

  • __block可以用于解决Block内部无法修改auto变量值的问题。
  • __block不能修饰全局变量、静态static变量。

如果我们想在block中修改局部变量的值,那就需要在定义局部变量的时候增加修饰词__block

    // 定义一个局部变量
    __block int global = 100;
    NSLog(@"global内存首地址:%p ",&global);
    void(^dBlock)(void) = ^{
        NSLog(@"在Block内global内存首地址:%p ",&global);
        global  = 101;
        NSLog(@"在Block内global的值为:%d",global);
    };
    //调用Block,观察global的值是否会被修改
    dBlock();
    NSLog(@"在Block外global的值为:%d",global);
    global内存首地址:0x101006308
    在Block中global内存首地址:0x101064bc8
    在Block内global的值为:101
    在Block外global的值为:101

结果所示:在定义局部变量的时候增加修饰词__block,就可以在Block中修改局部变量的值。

这是为什么呢?我们接下来进行探究。把OC代码编译成C/C++来看下底层实现。部分代码如下:


#pragma clang assume_nonnull end

// __Block修饰之后变成了一个结构体
struct __Block_byref_global_0 {
  void *__isa;  // isa指针代表该结构体也是一个OC对象
__Block_byref_global_0 *__forwarding; // 指针变量,指向结构体自己的内存首地址。
 int __flags;
 int __size;
 int global; // 我们声明的int global最终存放的位置
};

// Block
struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    // 一个__Block_byref_global_0结构体的对象,可以通过指针变量找到
    __Block_byref_global_0 *global; // by ref
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_global_0 *_global, int flags=0) : global(_global->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};
// Block的代码区执行位置
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

    __Block_byref_global_0 *global = __cself->global; // bound by ref
    // global->__forwarding->global
    // 通过__forwarding存放的指针地址找到我们初始化的那个__Block_byref_global_0的结构体
    // 在找到__Block_byref_global_0结构体下面的global变量
    // 最后进行赋值
    (global->__forwarding->global) = 101;
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_j2_bxd08__54lzbn226msl27gww0000gn_T_main_7a782e_mi_2,(global->__forwarding->global));
          
}

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->global, (void*)src->global, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->global, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;

    // __block int global = 100 <==> 转化成了一个__Block_byref_global_0结构体对象
    // 并对该对象进行初始化,
    // 其中__forwarding指针指向自己(&global代表了该指针对象的内存首地址)
    __attribute__((__blocks__(byref))) __Block_byref_global_0 global = {
                                                                        (void*)0,
                                                                        (__Block_byref_global_0 *)&global,
                                                                        0,
                                                                        sizeof(__Block_byref_global_0),
                                                                        100};
        
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_j2_bxd08__54lzbn226msl27gww0000gn_T_main_7a782e_mi_0,&(global.__forwarding->global));
          
    // 初始化Block
    // 给__main_block_impl_0这个结构体传入 &global也就是global对象的指针地址
    void(*dBlock)(void) = ((void (*)())&__main_block_impl_0(
                                                            (void *)__main_block_func_0,
                                                            &__main_block_desc_0_DATA,
                                                            (__Block_byref_global_0 *)&global,
                                                            570425344));

        ((void (*)(__block_impl *))((__block_impl *)dBlock)->FuncPtr)((__block_impl *)dBlock);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_j2_bxd08__54lzbn226msl27gww0000gn_T_main_7a782e_mi_3,(global.__forwarding->global));

    }
    return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

我们可以发现:

  • 通过__block修饰后的局部变量global已经不再是一个局部变量了,而是一个__Block_byref_global_0的结构体对象。
  • 系统会把局部变量作为一个成员变量包装进它体内。
  • 会把__forwarding指针指向自己。

所以不能再把它当成一个局部变量来分析了。

  • Block是不可以直接捕获这个__block变量global的。
  • Block在初始化的时候会捕获__block变量global的的指针地址,属于指针传递
  • Block代码块中我们看到(global->__forwarding->global) = 101;代表着会通过内存直接访问的形式找到int global并修改值。
  • 具体细节可以看我上面的注释。
  • 这里就思考了:

为什么不直接在Block中存储global呢,就类似静态static变量那样,偏偏搞个结构体来存放global呢?

  • 我的想法:

结合Block三种类型的知识我们知道:

  • 当了一个Block访问了auto变量,那Block的类型就是是__NSStackBlock__类型,存放在栈中。内存由系统控制,如果超过变量作用域就会被系统自动销毁。
  • ARC环境下如果block访问了auto变量,编译器会根据情况自动执行copy,变成__NSMallocBlock__类型,然后将栈上的block复制到堆上。

Block__NSStackBlock__类型转换成__NSMallocBlock__类型的时候:
Block会从栈中copy到堆中,那block在栈中的变量也会跟着copy到堆中,让堆Block持有它。
并且让栈__block变量__forwarding指针指向堆上面的__block变量
这样,无论是在Block语法内外使用__block变量,还是__block变量配置在栈上或堆上,都可以顺利地访问同一个__block变量

相关文章

网友评论

      本文标题:iOS-浅谈OC中Block的变量捕获和__block修饰符

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