一个苹果IOSiOS开发

block底层原理探究(一):捕获

2019-12-19  本文已影响0人  _小沫

iOS开发中block是比较常用也是比较好用的语法,平时开发中我们都用的很溜,但它的底层是如何实现的呢?__block原理是什么?__weak是如何解决循环引用问题的?

block的本质

这些问题,我们都可以通过clang命名分析代码得到答案;clang 命令可以将源码改写成C/C++的,通过C/C++ 源码可以很清楚的研究 block底层实现;
具体命令:
clang -rewrite-objc main.m
这个是最基本的命令,还可以增加参数生成具体平台,具体架构的代码,这样生成的代码量会少很多;具体的命令是:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
执行命令后,目录下会生成一个同名的main.cpp文件;

OC代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int i = 0;
        void (^block)(int) = ^(int a) {
            NSLog(@"HelloWorld-%d",i);
        };
    }
    return 0;
}

生成的main.cpp文件最后面,能找到与之对应的C/C++代码:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        int i = 0;
        void (*block)(int) = ((void (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, i));
    }
    return 0;
}

以此为基础可以分析出与block底层的结构为:
__main_block_impl_0结构体

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

__main_block_impl_0有4个部分,3个成员,1个构造函数

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

这个结构体,第一个成员就是我们很熟悉的isa指针,这就说明block本质就是OC对象;
最后一个成员FuncPtr是函数指针,这个存储的函数就是block里的代码实现:

static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a) {
  int i = __cself->i; // bound by copy
  NSLog((NSString *)&__NSConstantStringImpl__var_folders__p_gphdp0fd4yv2vlq4f0y1wm500000gn_T_main_a571b5_mi_0,i);
}
static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size; // bolck的内存大小
}

总的来说block本质就是:

block对变量的捕获

我们主要分析block对以下4种变量的捕获表现:

自动变量

我们平常声明的没有关键字修饰的局部变量默认就是自动变量,只是省略了auto关键字:
上面代码中int i = 0就是自动变量等价为auto int i = 0
从上面生成的代码可以看出,自动变量i被捕获到了__main_block_impl_0结构体中,且是捕获的是值;

静态变量

现在将main函数的i变量改为静态变量:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
       static int i = 0;
        void (^block)(int) = ^(int a) {
            NSLog(@"HelloWorld-%d",i);
        };
    }
    return 0;
}

重新生成C/C++代码

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *i; // 指针 不同于自动变量的int i 
....
}

静态变量i也被捕获了,只是这次捕获的是指针;

全局变量,静态全局变量

依次改为全局变量,静态全局变量,重新生成代码;最终会发现,block并未捕获该变量;这是因为全局变量,静态全局的作用域是全局的,任何地方都能访问该变量;block无需将该变量捕获到结构体找那个也能访问;

不同类型变量的捕获情况:

变量类型 是否捕获 访问方式
自动变量 值传递
静态变量 指针传递
全局变量(静态全局) 直接访问

由于捕获(访问)的方式不一样,外部变量修改后,block内部使用的变量结果不一样:

int global_i = 0;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int auto_i = 0;
        static int static_i = 0;
        void (^block)(void) = ^() {
            NSLog(@"%d,%d,%d",auto_i,static_i,global_i);
        };
        auto_i = 1;
        static_i = 1;
        global_i = 1;
        block();
    }
    return 0;
}

以上代码,输出结果 0,1,1;

void change(int i) {
    i = 1;
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int i = 0;
        change(i);
        NSLog(@"%d",i); // 0
    }
    return 0;
}
void change(int *i) {
    *i = 1;
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int i = 0;
        change(&i);
        NSLog(@"%d",i); // 1
    }
    return 0;
}

有一个疑问是,block对于自动变量为什么不和静态变量一样处理,将自动变量以指针传递的方式访问呢?归根结底还是因为自动变量作用域的问题,自动变量作用域是当前函数(方法)范围内;当出了作用域后,系统会自动释放;如果block对自动变量以指针方式捕获,block内部的变量指向的内容也释放了;所以对于自动变量,一定是要值传递;

block修改捕获的变量

以上是变量在外部进行了更改,如果在block内部进行更改又是什么情况呢?
block中分别对自动变量,静态变量,全局变量进行修改:

int global_i = 0;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int auto_i = 0;
        static int static_i = 0;
        void (^block)(void) = ^() {
            global_i = 1;
            static_i = 1;
            auto_i = 1;
        };
    }
    return 0;
}

编译后,auto_i = 1;这行报错Variable is not assignable (missing __block type specifier)
而静态变量,全局变量均可以修改成功,具体原因同上面分析的一样:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *i; // 指针
....
}

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *i = __cself->i; // bound by copy
  (*i) = 1;
}

为了更好的理解通过指针的方式传递修改变量,这里编写了类似的代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 外部变量
        int i = 1;
        
        // 模拟block结构体__main_block_impl_0
        struct __main_block_impl_0 {
            int *I;
        }blockImp;
        
        blockImp.i = &i; // 地址
        
        // 模拟block调用函数__main_block_func_0 (不同作用域)
        {
            int *i = blockImp.i;
            (*i) = 2; // 可以更改
        }
    }
    return 0;
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 外部变量
        int i = 1;
        
        // 模拟存储block结构体__main_block_impl_0
        struct __main_block_impl_0 {
            int I;
        }blockImp;
        
        blockImp.i = I;
        
        // 模拟block调用函数__main_block_func_0
        {
            int i = blockImp.i;
            i = 2; // (不会报错) 虽然可以更改 但这个i和外部那个i不是一个东西
        }
    }
    return 0;
}

一个很有意思的情况
如果外部的auto整型变量是一个指针,那block捕获到的也是指针,block内部能否修改呢?

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int *i = 0;
        void (^block)(void) = ^{
            *i = 1;
        };
        block();
    }
    return 0;
}

编译通过,但*i = 1这种赋值是错误的,运行会crash;需要通过地址的方式赋值(因为是auto变量,仍会报错提示加__block):

对象类型的auto变量
对于被捕获的对象类型的auto变量,容易让一些人误解或者说费解;
比如说以下代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSMutableArray *arr = [NSMutableArray array];
        void (^block)(void) = ^{
            [arr addObject:@(2)];
            NSLog(@"%@",arr); // 1,2
        };
        [arr addObject:@(1)];
        block();
    }
    return 0;
}

有些小伙伴就会认为block中的[arr addObject:@(2)];这句代码,修改了auto变量,编译会报错提示添加__block
但事实上是正常的,且外部修改了,blokc内部的变量值也改了;
这是因为对象类型的变量有两部分,1.指针(栈),2.指针指向的对象(堆);block捕获的是指针,而[arr addObject:@(2)]更改的指针指向的堆上的值,指针本身未改变;外部变量和block捕获的变量是两个不同的指针,但指向的是同一值;
如果我们直接修改指针,这时才会报错:

类似的,在block内部只是修改自动变量对象(self)的属性(成员变量),也是没问题的,不需要__block;

block的类型

block有三种类型,继承自NSBlock

block类型 环境 内存分配
NSStackBlock 捕获auto变量 栈区
NSMallocBlock NSStackBlock调用copy方法 堆区
NSGlobalBlock 没有捕获auto变量 数据区

需要注意的是,在ARC环境下,即使block没有捕获auto变量,block最终也会是NSMallocBlock类型;

    int i = 0;
    void (^block)(int) = ^(int a) {
        NSLog(@"%d",i);
    };
    NSLog(@"%@",[block class]); // MRC: __NSStackBlock__    ARC: __NSMallocBlock__

这是因为在ARC环境下,编译器会根据情况自动将栈区block copy到堆上;如以下情形:

因此ARC环境下,以下声明的block都会是NSMallocBlock类型:

@property (nonatomic, strong) void (^block)(void);
@property (nonatomic, copy) void (^block)(void);

ARC环境block为NSStackBlock的情形如下,只是这种场景极少,因为大部分block都会被赋值给变量;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int i = 0;
        NSLog(@"%@",^{NSLog(@"%d",i);});
    }
    return 0;
}

__block修改变量的原理

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block int i = 0;
        void (^block)(void) = ^{
            i = 1;
        };
        block();
    }
    return 0;
}

同样转换为C/C++代码,看下__block这个简单的声明到底做了什么事情;

struct __Block_byref_i_0 {
  void *__isa;
  __Block_byref_i_0 *__forwarding;
 int __flags;
 int __size;
 int I;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_i_0 *i; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    __Block_byref_i_0 *i = __cself->i; // bound by ref
   (i->__forwarding->i) = 1;
}

加了__block后不同点:

访问结构体成员值时也是通过这个__forwarding指针“迂回”的访问;
这个__forwarding指针看起来是多余的,因为上面访问值的方式完全可以直接通过结构体本身访问:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    __Block_byref_i_0 *i = __cself->i; // bound by ref
   i->i = 1; // 替换  (i->__forwarding->i) = 1;
}

__forwarding看起来“多此一举”,但实际上设计的很巧妙;

前面也提到过,block被copy到堆上,其捕获的变量也会copy一份到堆上;一段简单的代码可以验证:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int i = 0;
        NSLog(@"%p",&i);     // 0x7ffeefbff57c
        void (^block)(void) = ^{ // ARC环境,会copy
            NSLog(@"%p",&i); // 0x100410110
        };
        block();
    }
    return 0;
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int i = 0;
        NSLog(@"%p",&i);     // 0x7ffeefbff57c
        ^{ // ARC环境,没有被赋值,不会copy
            NSLog(@"%p",&i); // 0x7ffeefbff578
        }();
    }
    return 0;
}

可以看出,block被copy到堆的情况,捕获的i变量的地址变小了就是被copy到堆上了;以上代码如果改为__block变量,NSLog的结果是block内外变量地址是相同的;这也也验证了我们前面所述,这也是为什么__block变量能被修改的原因;

__forwarding指针这里的作用就是针对堆的block,原本栈区指向自己,之后指向被copy到堆的block。然后堆上的变量的__forwarding再指向自己。这样不管__block变量是copy到堆上,还是在栈上,都可以通过(i->__forwarding->i)来访问到变量值。

以上就是__block变量能被修改的原因,简单总结就是两点:

下篇:block底层原理探究(二):内存管理

上一篇 下一篇

猜你喜欢

热点阅读