iOS中Block完全详解

欢迎大家关注我的个人博客:https://darkknightkazuma.github.io

一、Block基础介绍

1、概念介绍

Block又称为块或块对象,它是苹果在OSX10.6和iOS4.0中新加入的功能,是C语言层面的特性及功能实现,类似其它语言的闭包(closure)功能.当时苹果正使用LLVM的clang作为C语言的编译器来改进C/OC/C++/OC++等的编译处理,Block也是当时的重要成果.
##2、块的定义及语法
Block是带有自动变量(局部变量)的匿名函数.因为底层结构体实现有isa指针,也被看作是块对象.Block的出现其实是为了代替指针函数的一种语法结构,之前传递状态需要使用不透明的void指针来传递状态,而Block把C语言特性所编写的代码封装成简明且易用的接口.

下面是Block的实现语法结构:
block的语法是脱字符^加花括号,花括号里是块的实现.块可以当成是变量的值使用.如下:

^{
    //Block implementation
}

Block类型的声明语法结构:

return_type (^block_name)(parameters)
//中间的block_name可以看作是变量名

Block的调用:

block_name(parameters)

二、Block的关于捕获变量的底层实现

在块声明的范围里,所有变量都可以为其所捕获.捕获的自动变量不可修改,修改要加__block前缀.
块总能修改实例变、静态变量、全局变量、全局静态变量,无需加__block.

我们经常会看到block相关的文章或面试题中有这些内容.那么Block底层实现是怎么样的呢?
#####1、我们首先来看一下Block的内存布局,如下:

/* Revised new layout. */
struct Block_descriptor {
    unsigned long int reserved; //预留内存大小
    unsigned long int size; //块大小
    void (*copy)(void *dst, void *src); //指向拷贝函数的函数指针
    void (*dispose)(void *); //指向释放函数的函数指针
};


struct Block_layout {
    void *isa; //指向Class对象
    int flags; //状态标志位
    int reserved; //预留内存大小
    void (*invoke)(void *, ...); //指向块实现的函数指针
    struct Block_descriptor *descriptor;
    /* Imported variables. */
};

其中最重要的就是invoke函数指针和descriptor块的描述.invoke函数指针它指向了块的实现,它的void*参数传入的是块的结构体. descriptor的结构体中包含了块大小以及两个重要的辅助函数指针等.我们注意到块的布局中,最下面一部分是捕获到的变量,前面提到的void*参数传入块的结构体也是为了拿到捕获到的变量.那么下面让我们来看一下块的实际源码是什么样的:

cd到目录下,在终端通过 clang -rewrite-objc 文件名 的方法将文件转换为C/C++代码.如果发现不能转换需要下载插件,指令为xcode-select --install.

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

转换后点开目录下的.cpp文件,会看到上万行的代码,屏蔽掉无用的代码,直接找到主要代码如下:

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

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_y4_0279cbtj42s5p4r1j0yxvfmc0000gn_T_main_459ed6_mi_2,ivar);
        }

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 ivar = 1;
        void (*tmpBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, ivar));
        ((void (*)(__block_impl *))((__block_impl *)tmpBlock)->FuncPtr)((__block_impl *)tmpBlock);
    }
    return 0;
}

我们来挨个看一下:

__block_impl 结构体是包含我们刚刚在Block布局中提到的前四个成员变量.

__main_block_impl_0结构体是包含了__block_impl结构体变量和__main_block_desc_0结构体指针以及捕获到的变量.最后是初始化构造函数中对impl成员和desc进行赋值.

__main_block_func_0这个函数他传递了__main_block_impl_0*型的参数,并从中读取块结构体中捕获的参数进行实际操作.看到这里你有个明白了这个就是对应上面那个invoke函数指针所指向的函数.

__main_block_desc_0这个结构体就是上面所说的块的描述结构体.可能你会发现他少了两个辅助函数的函数指针,原因后面会说.

最后是main函数中的具体初始化和函数调用.刚好对应块变量的声明实现部分和块的调用.

#####2、关于捕获自动变量的分析

捕获自动变量这部分首先我们要明确有哪几种变量,如下:

  • auto自动变量:默认方法的作用域中不加前缀就是自动变量,而且ARC下默认还会加__strong.
  • static静态变量:存放在内存的可读写区,初始化只在第一次执行时起作用,运行过程中语句块变量将保持上一次执行的值.
  • static全局静态变量:也放在可读写区,作用域为当前文件.
  • 全局变量:也在可读写区,整个程序可用.
  • 另外在OC中它们又分为对象类型和非对象类型.

下面让我们看一下他们在Block中的表现.
首先是前面那个是用自动变量ivar的例子,如果我们修改它的值编译器会报警告:

Variable is not assignable (missing __block type specifier)

提示我们不能修改变量,这是为什么呢?我们来打印一下block内外的指针变量的地址:

    int ivar = 1;
        NSLog(@"块外%p",&ivar);
        void (^tmpBlock)(void) = ^{
            NSLog(@"块内%p",&ivar);
        };
        tmpBlock();

2019-12-05 18:20:40.063273+0800 LearningDemo[69358:5789849] 块外0x7ffeefbff4dc
2019-12-05 18:20:40.063840+0800 LearningDemo[69358:5789849] 块内0x103400ad0

你会发现它们不是同一个地址,是的,block的__main_block_impl_0结构体拷贝了一份自动变量进去作为结构体的成员变量,你修改的是结构体内部的ivar的值,而不是外部ivar的值,他们并不是同一块内存上的东西.苹果让编译器在这种情况下报警告,提示开发者是改不了的.
接下来我们给它加一个static前缀,如下:

        static int ivar = 1;
        NSLog(@"块外%p",&ivar);
        void (^tmpBlock)(void) = ^{
            ivar = 2;
            NSLog(@"块内%p",&ivar);
        };
        tmpBlock();

2019-12-05 18:41:36.083804+0800 LearningDemo[69801:5806136] 块外0x100003320
2019-12-05 18:41:36.084770+0800 LearningDemo[69801:5806136] 块内0x100003320

有意思的事情发生了,可以修改变量了,而且地址居然一样了~那我们看一下转换后的源码:

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

            (*ivar) = 2;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_y4_0279cbtj42s5p4r1j0yxvfmc0000gn_T_main_532557_mi_3,&(*ivar));
        }

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 ivar = 1;
        NSLog((NSString *)&am
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值