美文网首页iOS开发(OC)
block引用外部变量时底层实现原理

block引用外部变量时底层实现原理

作者: 54番茄 | 来源:发表于2018-04-12 20:19 被阅读5次

前言:Block不允许修改外部变量的值,Apple这样设计,应该是考虑到了block的特殊性,block也属于“函数”的范畴,变量进入block,实际就是已经改变了作用域。在几个作用域之间进行切换时,如果不加上这样的限制,变量的可维护性将大大降低。

block 有三种类型:

首先要知道block的三种类型:

  • _NSConcreteGlobalBlock:全局的静态 block,类似函数。如果block里不获取任何外部变量。或者的变量是全局作用域时,如成员变量、属性等; 这个时候就是Global类型。

  • _NSConcreteStackBlock:保存在栈中的 block,栈都是由系统管理内存,当函数返回时会被销毁。__block类型的变量也同样被销毁。为了不被销毁,block会将block和__block变量从栈拷贝到堆。

  • _NSConcreteMallocBlock:保存在堆中的 block,堆内存可以由开发人员来控制。当引用计数为 0 时会被销毁。

具体根据代码分析中他们属于那种类型。

Block与变量

1、全局变量,block可以进行读取和修改。

@interface ViewController () {
    NSInteger num;
} 

@implementation ViewController 
- (void)viewDidLoad { 
    //1、block修改成员变量
    void (^block1)() = ^(){ 
        ++num;
        NSLog(@"调用成员变量: %ld", num);
    };
    block1();
}

2、局部变量,block只能读取,不能修改局部变量。这个时候是值传递,如果想修改局部变量,要用__block来修饰,这个时候是引用传递。下面会聊下block的实现原理:

看例子:

 //1、调用局部变量,不用__block
  NSInteger testNum = 10; 
  void ( ^ blockNum )() = ^() { 
        //testNum = 1000; 这样是编译不通过的
        NSLog(@"修改局部变量: %ld", testNum);    //打印:10
  };

   testNum = 20;
   blockNum();
   NSLog(@"testNum最后的值: %ld", 20);           //打印:30 

  //2、修改局部变量,要用__block
  __block NSInteger testNum2 = 10; 
  void (^blockNum2)() = ^() {
        NSLog(@"读取局部变量: %ld", testNum3);   //打印:20
        testNum2 = 1000;
        NSLog(@"修改局部变量: %ld", testNum3);   //打印:1000
   };
    testNum2 = 20;
    blockNum2();
    testNum2 = 30;
    NSLog(@"testNum最后的值: %ld", testNum2);  //打印:30

block代码分析

网上很多通过Clang(LLVM编译器)将OC的代码转换成C++源码,来进行分析的。但是这些转换的代码并不是block的源代码,只是用来理解用的过程代码。

1、block不包含任何变量

新建一个testBlock.m文件。文件中代码为:

图一

执行clang命令:

clang -rewrite-objc testBlock.m

生成.cpp的核心代码主要在.cpp文件的底部,大家可以看下图:

图二

详细的注释,具体的看图片备注。代码执行的时候,block的 isa 有上面三个类型。
从图二中可以发现,block的类型问题:

impl.isa = &_NSConcreteStackBlock; 

这里 impl.isa 的类型为_NSConcreteStackBlock,由于clang改写的具体实现方式和 LLVM 不太一样,所以这里 isa 指向的还是_NSConcreteStackBlock。但在 LLVM 的实现中,开启 ARC 时,block 应该是 _NSConcreteGlobalBlock类型,所以 block是什么类型 在 clang代码里是看不出来的。如果要查看block的类型还是要通过Xcode进行打印:

打印结果:
myBlock调用前======<__NSGlobalBlock__: 0x1000010b0>
myBlock 调用后=====<__NSGlobalBlock__: 0x1000010b0>

这个block没有获取任何外部变量,打印出来的是_NSConcreteGlobalBlock,该类型的block和函数一样,存放在代码段内存段,内存管理简单。

注意:
在ARC环境下,平时见到最多的是_NSConcreteMallocBlock,是因为我们会对Block有赋值操作,所以ARC下,block 类型通过 = 进行传递时,会导致调用objc_retainBlock->_Block_copy->_Block_copy_internal方法链。并导致 __NSStackBlock类型的 block 转换为 __NSMallocBlock类型。


打印的结果:
//打印block函数
myBlock : <__NSMallocBlock__: 0x100607780>

上面block代码,没有获取任何外部变量,但是通过 = 进行传递,所以是_NSConcreteMallocBlock类型的。

测试了几次都不能打印出来__NSStackBlock,看了个别人的写法,然后发现ARC下Block也是存在__NSStackBlock的:

 __block int temp = 10;
 NSLog(@"%@",^{NSLog(@"*******%d %p",temp ++,&temp);});

打印结果:
<__NSStackBlock__: 0x7ffeefbff528>
这种情况就是ARC环境下Block是__NSStackBlock的类型。

2、block调用外部变量

  • 如果block里面用到外部局部变量,会先把外部局部变量从栈区到堆区。因此可以访问外部局部变量的值,但是无法直接改变外部局部变量的值。


    image.png
打印结果:
 1==0x7ffee2642a9c
 3==0x7ffee2642a9c
 xBlock === <__NSMallocBlock__: 0x60c00004f330>
 2==0x60c00004f350
 bb === 0x7ffee2642a1c

从上面打印结果可以看出,在引用了外部局部变量后,block把变量aa从栈区拷贝到堆区。

  • 下面在新建一个例子,然后继续使用clang命令查看一下底层实现流程:
#import <Foundation/Foundation.h>

int global_variable = 1;

static int static_global_variable = 2;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        static int static_local_variable = 3;
        int val = 4;
        
        void (^myBlock)(void) = ^{
            global_variable ++;
            static_global_variable ++;
            static_local_variable ++;
            printf("Block中 :global_variable = %d",global_variable);
            printf("Block中 :static_global_variable = %d",static_global_variable);
            printf("Block中 :static_local_variable = %d",static_local_variable);
            printf("Block中 :val = %d",val);
        };
        
        global_variable ++;
        static_global_variable ++;
        static_local_variable ++;
        val ++;
        printf("Block中 :global_variable = %d",global_variable);
        printf("Block中 :static_global_variable = %d",static_global_variable);
        printf("Block中 :static_local_variable = %d",static_local_variable);
        printf("Block中 :val = %d",val);
        
        myBlock();
    }
    return 0;
}

clang编译后:


image.png

因为全局变量global_variable和静态全局变量static_global_variable,是全局的,作用域很广,它们可以被Block捕获进去,在Block捕获了它们进去之后,直接进行++操作,而且Block结束之后,它们的值依旧可以得以保存下来。

接下来仔细看看自动变量val和静态变量static_local_variable的问题:

__main_block_impl_0中,可以看到静态变量static_local_variable和自动变量val,被Block从外面捕获进来,成为__main_block_impl_0这个结构体的成员变量了。这个构造函数中,自动变量和静态变量被捕获为成员变量追加到了构造函数中。在执行Block语法的时候,Block语法表达式所使用的自动变量的值是被保存进了Block的结构体实例中,也就是Block自身中。
   这里值得说明的一点是,如果Block外面还有很多自动变量,静态变量等等,这些变量在Block里面并不会被使用到,那么这些变量就不会被Block捕获进来,用不到的值,它不会去捕获。

再研究一下源码,我们注意到__main_block_func_0这个函数的实现

 int *static_local_variable = __cself->static_local_variable; // bound by copy
 int val = __cself->val; // bound by copy

系统自动加上的注释,bound by copy自动变量val虽然被捕获进来了,但是是用 __cself -> val来访问的。Block仅仅捕获了val的值,并没有捕获val的内存地址。所以在__main_block_func_0这个函数中即使我们重写这个自动变量val的值,依旧没法去改变Block外面自动变量val的值。

OC基于这一点,在编译的层面就防止开发者可能犯的错误,因为自动变量没法在Block中改变外部变量的值,所以编译过程中就报编译错误。

小结一下:
自动变量是以值传递方式传递到Block的构造函数里面去的。Block只捕获Block中会用到的变量。由于只捕获了自动变量的值,并非内存地址,所以Block内部不能改变自动变量的值。Block捕获的外部变量时,可以改变值的是静态全局变量、全局变量。

3、在Block中改变变量值有2种方式,一是传递内存地址指针到Block中,二是改变存储区方式(__block)。

  • 先来实验一下第一种方式,传递内存地址到Block中,改变变量的值。
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        NSMutableArray * muArray = [NSMutableArray array];
        [muArray addObject:@1];
        NSLog(@"block前 muArray 指针地址 %x--- 指针指向的内存地址: %p",&muArray,muArray);

        void(^myBlock)(void) = ^{
            
            [muArray addObject:@23232];
            NSLog(@"block内 muArray 指针地址 %x--- 指针指向的内存地址: %p",&muArray, muArray);
        };
        myBlock();
        NSLog(@"block外 muArray 指针地址 %x--- 指针指向的内存地址: %p",&muArray, muArray);
        
    }
    return 0;
}

运行后打印结果:

block 前 muArray 指针地址 efbff568--- 指针指向的内存地址: 0x10050c280
block 内 muArray 指针地址 506480 --- 指针指向的内存地址: 0x10050c280
block 外 muArray 指针地址 efbff568--- 指针指向的内存地址: 0x10050c280

从上面结果可以看出:muArray在block内的指针发生了变化,但是指向的内存地址都是一样的。

在使用clang命令后:(我直接把编译后的全部源码贴出来了,有点儿长,你们可以写个工程编译后,对着看,这样更好理解)

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSMutableArray *muArray;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSMutableArray *_muArray, int flags=0) : muArray(_muArray) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  NSMutableArray *muArray = __cself->muArray; // bound by copy


            ((void (*)(id, SEL, ObjectType _Nonnull))(void *)objc_msgSend)((id)muArray, sel_registerName("addObject:"), (id _Nonnull)((NSNumber *(*)(Class, SEL, int))(void *)objc_msgSend)(objc_getClass("NSNumber"), sel_registerName("numberWithInt:"), 23232));
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_q8_8t5zqb1x3yg3dzb7h7trddj80000gn_T_main_dbee92_mi_1,&muArray, muArray);
        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->muArray, (void*)src->muArray, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->muArray, 3/*BLOCK_FIELD_IS_OBJECT*/);}

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; 



        NSMutableArray * muArray = ((NSMutableArray * _Nonnull (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("array"));
        ((void (*)(id, SEL, ObjectType _Nonnull))(void *)objc_msgSend)((id)muArray, sel_registerName("addObject:"), (id _Nonnull)((NSNumber *(*)(Class, SEL, int))(void *)objc_msgSend)(objc_getClass("NSNumber"), sel_registerName("numberWithInt:"), 1));
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_q8_8t5zqb1x3yg3dzb7h7trddj80000gn_T_main_dbee92_mi_0,&muArray,muArray);

        void(*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, muArray, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_q8_8t5zqb1x3yg3dzb7h7trddj80000gn_T_main_dbee92_mi_2,&muArray, muArray);

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

__main_block_func_0里面可以看到NSMutableArray *muArray = __cself->muArray传递的是指针,所以成功改变了变量的值。

因为在C语言的结构体中,编译器没法很好的进行初始化和销毁操作。这样对内存管理来说是很不方便的。所以就在__main_block_desc_0结构体中间增加成员变量:
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*)

void (*dispose)(struct __main_block_impl_0*)
利用OC的Runtime进行内存管理,相应的也就增加了2个方法,这里的_Block_object_assign和_Block_object_dispose就对应着retain和release方法。。

#对__Block_byref_num_0结构体进行内存管理。新加了copy和dispose函数。
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->muArray, (void*)src->muArray, 3/*BLOCK_FIELD_IS_OBJECT*/);}

# 当 block 从堆内存释放时,调用此函数:__main_block_dispose_0
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->muArray, 3/*BLOCK_FIELD_IS_OBJECT*/);}

#BLOCK_FIELD_IS_OBJECT 是Block截获对象时候的特殊标示,
#如果是截获的__block,那么是BLOCK_FIELD_IS_BYREF。

Block_copy实现了把Block从栈上拷贝到堆上,dispose函数是把堆上的函数在废弃的时候销毁掉。《Block_copy底层实现》

  • 改变外部变量值的第二种方式是加 __block
    __block修饰外部变量num
int main(int argc, const char * argv[]) {
    @autoreleasepool {        
        __block int num = 20;
        NSLog(@"block 前 num 指针地址 %x--- 指针指向的内存地址: %p",&num,num);
        void(^myBlock)(void) = ^{
            num = 100;
            NSLog(@"block 中 num 指针地址 %x--- 指针指向的内存地址: %p",&num,num);
        };
        myBlock();
        NSLog(@"block 后 num 指针地址 %x--- 指针指向的内存地址: %p",&num,num);
    }
    return 0;
}

从上面clang编译后我们能发现,带有 __block的变量num也被转化成了一个结构体__Block_byref_num_0这个结构体有5个成员变量。

struct __Block_byref_num_0 {
  void *__isa;                      //第一个是isa指针
__Block_byref_num_0 *__forwarding; //第二个是指向自身类型的__forwarding指针
 int __flags;                     //第三个是一个标记flag
 int __size;                    //第四个是它的大小
 int num;                      //第五个是变量值,名字和变量名同名
};

__Block_byref_num_0结构体中有一个指向自身类型的__forwarding指针,这个__forwarding到底是怎么指向自身的呢?运行上面的代码看看num的地址:

打印结果:
block 前 num 指针地址 efbff568  --- 指针指向的内存地址: 0x14
block 中 num 指针地址 31013b8  --- 指针指向的内存地址: 0x64
block 后 num 指针地址 31013b8  --- 指针指向的内存地址: 0x64

明显看出num地址不同了,这个__forwarding指针并没有指向之前的自己啊,那__forwarding指针现在指向到哪里了呢?
       在前文的分析中,已经说过如果block里面用到外部局部变量,Block通过copy到了堆上,堆上也会重新复制一份Block,并且该Block也会继续持有该 __block 。当Block释放的时候,__block没有被任何对象引用,也会被释放销毁。
       __forwarding指针这里的作用就是针对堆上的Block,这句话怎么来解释呢? 原来是把__forwarding指针指向自己的,换成了指向拷贝成_NSConcreteMallocBlock的__block自己。然后堆上的变量的__forwarding再指向原来的自己。这样不管__block怎么复制到堆上,还是在栈上,这样就可以通过 (num -> __forwarding -> num) = 100 来访问到变量值。如下图:

image.png

ARC环境下,一旦Block赋值就会触发copy,__block就会copy到堆上,Block也就变成__NSMallocBlock。ARC环境下也是存在__NSStackBlock的时候,这种情况下,__block就在栈上,如 NSLog(@"%@",^{NSLog(@"*******%d %p",temp ++,&temp);})

MRC环境下,只有copy,__block才会被复制到堆上,否则,__block一直都在栈上,block也只是__NSStackBlock,这个时候__forwarding指针就只指向自己了。__block根本不会对指针所指向的对象执行copy操作,而只是把指针进行的复制。而在ARC环境下,对于声明为__block的外部对象,在block内部会进行retain,以至于在block环境内能安全的引用外部对象。

对象的变量

对象的变量在OC中,默认声明自带__strong所有权修饰符的,当block被赋值给__strong类型的对象,或者block的成员变量时,编译器会自动调用block的copy方法。执行copy方法,block拷贝到堆上。

相关文章

网友评论

    本文标题:block引用外部变量时底层实现原理

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