【iOS】Block总结

文章目录


前言

之前的Block写的没啥重点,这一次总结一下重点,比如Block如何捕获外界变量,__block的使用

一、Block如何捕获外界变量

1.捕获自动变量

首先先上代码,我们在Block中添加了localA局部变量

bash 复制代码
int main() {

    int localA = 7;
    
    void(^block)(void) = ^{
        NSLog(@"block - %d - %d", localA, globalB);
    };
    
    block();
    
    localA += 10;
    
    block();
    
    return 0;
}

发现在外部修改localA并没有影响到Block内部

编译成源码

bash 复制代码
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int localA;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _localA, int flags=0) : localA(_localA) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

可以看到Block实现结构体中新增了localA变量

我们具体看一下函数实现代码

bash 复制代码
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int localA = __cself->localA; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_tm_rlw5m0t17cg39hpr19j64k140000gn_T_main_e839a5_mi_0, localA);
        }

我们分析一下这段代码,在我们int localA = __cself->localA; // bound by copy ,localA 通过拷贝与 struct __main_block_impl_0 绑定,意思是 localA 是拷贝过来 的。那么得出结论:在外部改变 localA 不会影响到 block 内部。

Block 捕获基本数据类型(如 int、float 等)或结构体时,它会通过值捕获的方式复制变量的当前值。这意味着在 Block

内部使用的是变量捕获时刻的快照。由于是值复制,所以之后即使原始变量的值发生改变,Block 内部的值也不会改变。

2.捕获静态局部变量

一样的来看一个例子

bash 复制代码
#import <Foundation/Foundation.h>

//全局变量
int global_a = 10;
//静态全局变量
static int staic_global_a = 20;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
     
        //基本数据类型的局部变量
        int a = 5;
        
        //对象类型的局部变量
        
        //局部静态变量
        static int staic_a = 6;
        
        void(^block)(void) = ^{
            
            NSLog(@"局部变量.基本数据类型 %d",a);
            NSLog(@"局部静态变量 %d",staic_a);
            NSLog(@"全局变量 %d",global_a);
            NSLog(@"静态全局变量 %d",staic_global_a);
        };
        
        block();
        
    }
    return 0;
}

编译源码

bash 复制代码
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  int *staic_a;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int *_staic_a, int flags=0) : a(_a), staic_a(_staic_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

可以看到只有自动变量a与静态局部变量staic_a被加到了Block实现结构体中,同时静态局部变量与Block建立关联的是指针(int *),也就是说Block捕获的静态局部变量捕获的是变量的指针,因此当我们对静态局部变量进行修改时,Block内部的静态局部变量的值也会随之改变

bash 复制代码
        //局部静态变量
        static int staic_a = 6;
        
        void(^block)(void) = ^{
            
            NSLog(@"局部变量.基本数据类型 %d",a);
            
//            NSLog(@"局部变量.__unsafe_unretained.对象类型 %@",unsafe_objc);
//            NSLog(@"局部变量.__strong.对象类型 %@",strong_objc);
            
            NSLog(@"局部静态变量 %d",staic_a);
            
            NSLog(@"全局变量 %d",global_a);
            NSLog(@"静态全局变量 %d",staic_global_a);
            
        };
        a++;
        staic_a++;
        global_a++;
        
        block();

3.全局、全局静态变量

我们可以看到全局、全局静态变量并没有出现在我们的Block实现结构体中,说明二者无法被捕获

二、__block修饰符

我们先来引出一个问题,如果我们想在Block中修改我们捕获的自动变量该如何实现

如果我们直接进行修改会出现这样的错误

错误是:变量无法被赋值

我们一样给出例子:

bash 复制代码
int main() {
    
    int localA = 7;
    __block int local__blockB = 8;
    
    void(^block)(void) = ^{
        NSLog(@"block - %d - %d", localA, local__blockB);
    };
    
    block();
    
    localA += 10;
    local__blockB += 10;
    
    block();
    
    return 0;
}

自由变量 localA 并没有改变(还是 7),但是被 __block 修饰的 local__blockB 改变了(8 += 10 >>> 18)。

查看源码

bash 复制代码
struct __Block_byref_local__blockB_0 {
  void *__isa;
__Block_byref_local__blockB_0 *__forwarding;
 int __flags;
 int __size;
 int local__blockB;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int localA;
  __Block_byref_local__blockB_0 *local__blockB; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _localA, __Block_byref_local__blockB_0 *_local__blockB, int flags=0) : localA(_localA), local__blockB(_local__blockB->__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_local__blockB_0 *local__blockB = __cself->local__blockB; // bound by ref
  int localA = __cself->localA; // bound by copy

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_jv_7x7mpj5d6d14k8m53xr3m4240000gn_T_main___block_71facf_mi_0, localA, (local__blockB->__forwarding->local__blockB));
    }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->local__blockB, (void*)src->local__blockB, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->local__blockB, 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 localA = 7;
    __attribute__((__blocks__(byref))) __Block_byref_local__blockB_0 local__blockB = {(void*)0,(__Block_byref_local__blockB_0 *)&local__blockB, 0, sizeof(__Block_byref_local__blockB_0), 8};

    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, localA, (__Block_byref_local__blockB_0 *)&local__blockB, 570425344));

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

    localA += 10;
    (local__blockB.__forwarding->local__blockB) += 10;

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

    return 0;
}

__block 修饰的自由变量 local__blockB 竟然变成了一个结构体 struct __Block_byref_local__blockB_0

bash 复制代码
// struct __Block_byref_local__blockB_0 实例化 local__blockB 
{
    __isa = (void *)0,
    __forwarding = (____Block_byref_local__blockB_0 *)&local__blockB,
    __flags = 0,
    __size = sizeof(__Block_byref_local__blockB_0),
    local__blockB = 8   // 这就是我们赋值给 local__blockB 的值 8
}

在这个结构体中主要封装了两个东西------__forwarding指针原本的local__blockB变量

结构体中的local_blockB是被 __block 修饰的实际变量,这里存储的是变量的值 。在你的例子中,local__blockB 被初始化或赋值为 8。这个字段代表了原始变量在被

__block 修饰后的存储位置。

我们来看block函数实现中,发现读取__block变量时拐了一个大弯

bash 复制代码
local__blockB->__forwarding->local__blockB

在修改值的时候也是

bash 复制代码
(local__blockB.__forwarding->local__blockB) += 10

我们暂且按下不表,这与下面将要讲的Blockcopy操作有关

不过在这里还是总结一下block对变量的捕获情况

变量类型 是否捕获到block内部 访问方式
自由变量 值拷贝
静态变量 指针拷贝
全局变量 无法捕获 直接使用

三、Block的类型

为了研究 block 的 copy 操作,我们先要搞清楚 block 到底存储在栈上还是堆上???

我们先前讲过了iOS的内存分区,拿一张图回忆一下

【iOS】内存分区

常用级别的 Block 分为三类:

_NSConcreteGlobalBlock : 全局 block,存储在全局内存中,相当于单例;
_NSConcreteMallocBlock : 堆 block,存储在堆内存中,是一个带有引用计数的对象,需要自行管理器内存;
_NSConcreteStackBlock : 栈 block,存储在栈内存中,超出作用域立马销毁。

四、判断block存储在哪里

说完了block的类型,我们来分情况讨论不同情况下block的存储位置

  • 情况一 不创建 Block 变量,不访问变量

结果:NSGlobalBlock

  • 情况二 创建 Block 变量,不访问变量

结果:NSGlobalBlock

  • 情况三 不创建 Block 变量,访问自由变量

    结果:NSStackBlock
  • 情况四 创建 Block 变量,访问自由变量

结果:NSMallocBlock

  • 情况五 不创建 Block 变量,访问全局变量

结果:NSGlobalBlock

  • 情况六 创建 Block 变量,访问全局变量

结果:NSGlobalBlock

总结一下:

结论:

1、访问全局变量与没有访问变量是相同的,因此都没有捕获操作,全局变量直接引用

2、由于不创建Block就没有copy操作,因此创建Block变量并且捕获自由变量时Block会被拷贝到堆上,如果没有copy操作使用完就直接释放了

五、Block的copy操作

我们这里讲一下为什么我们的Block访问自由变量时会自动进行copy操作存储到堆上

在 Objective-C 中,Block 最初是在栈上创建的。栈上的 Block(NSStackBlock )生命周期与其定义的作用域相关联,一旦该作用域结束,栈上的 Block 将不再有效。这意味着如果你需要在 Block 的定义作用域外使用它,比如将它作为回调传递或保存为后续使用,你需要将它复制到堆上(成为 NSMallocBlock)。

如何理解作用域结束后Block不再有效,如果想要继续使用就要拷贝到堆上?

给出一个例子

bash 复制代码
// 假设这个方法从网络获取数据
- (void)fetchDataWithCompletion:(void (^)(NSData *data, NSError *error))completion {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 模拟网络请求
        NSData *data = [@"Test data" dataUsingEncoding:NSUTF8StringEncoding];
        NSError *error = nil;
        // 模拟一个成功的获取数据的操作
        sleep(2); // 模拟耗时操作

        // 回到主线程来执行回调
        dispatch_async(dispatch_get_main_queue(), ^{
            if (completion) {
                completion(data, error); // 调用 Block 传递数据或错误信息
            }
        });
    });
}

当在子线程执行完操作需要回到主线程,我们就用Block进行回调,如果Block在栈上,那么超出作用域就会被销毁,无法回到主线程被调用,因此需要拷贝到堆上

在ARC环境下编译器会自动完成拷贝到堆上的操作,在MRC下需要我们手动拷贝与释放

我们通过[block copy]操作进行拷贝,不同类型的Block进行copy效果也不同

六、源码分析Block_copy()

我们通过源码研究一下copy操作

bash 复制代码
void *_Block_copy(const void *arg) {
    return _Block_copy_internal(arg, WANTS_ONE);
}

简化一下里面的操作

bash 复制代码
/* 拷贝 Block,或者增加 Block 的引用计数。若需要拷贝,调用拷贝协助方法(如果存在) */
static void *_Block_copy_internal(const void *arg, const int flags) {
    struct Block_layout *aBlock;
    const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE;

    ///Junes 1、若不存在源 Block ,则返回 NULL
    if (!arg) return NULL;
    
    ///Junes 2、将源 Block 指针转换为 (struct Block_layout *)
    aBlock = (struct Block_layout *)arg;
    
    ///Junes 3、若源 Block 的 flags 包含 BLOCK_IS_GC,则其为堆块。 \
    /// 此时增加其引用计数,并返回这个源 Block
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    
     ///Junes 4、源 Block 是全局块,直接返回源 Block(全局 Block 就是一个单例)
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }

    ///Junes 5、源 Block 是一个栈 Block,执行拷贝操作。首先申请相同大小的内存
    struct Block_layout *result = malloc(aBlock->descriptor->size);
    if (!result) return (void *)0;
    
    ///Junes 6、使用 memmove 方法将栈区里的源 Block 逐位复制到刚申请的堆区 Block 内存中。这样做是为了保证完全复制所有元数据。
    memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
    
    ///Junes 7、更新 result 的 flags。
    result->flags &= ~(BLOCK_REFCOUNT_MASK);    // XXX not needed    ///Junes 确保引用计数为 0。注释表示没这个必要,可能因为此时引用计数早已为 0。但是为了防止 bug 被保留下来。
    result->flags |= BLOCK_NEEDS_FREE | 1;  ///Junes 为 result 的 flags 添加 BLOCK_NEEDS_FREE,并设置其引用计数为 1。表明这是一个堆 Block(一旦引用计数降为 0,则其内存将被回收)
    
    ///Junes 8、将 result 的 isa 指向 _NSConcreteMallocBlock。这意味着 result 是一个堆 Block。
    result->isa = _NSConcreteMallocBlock;
    
    ///Junes 9、如果 result 存在拷贝协助方法,调用它。
    /// 如果 block 捕获对象,编译器将会生成这个协助方法。
    /// 这个协助方法将会 retain 被捕获的对象。
    if (result->flags & BLOCK_HAS_COPY_DISPOSE) {
        (*aBlock->descriptor->copy)(result, aBlock); 
    }
    
    return result;
}

源码中对copy操作进行了分类

1、如果源Block不存在则返回NULL

2、如果源 Block 是 _NSConcreteMallocBlock,增加其引用计数,然后返回源 Block;

3、如果源 Block 是 _NSConcreteGlobalBlock,直接返回源 Block,因为_NSConcreteGlobalBlock是一个单例;

4、如果源 Block 是 _NSConcreteStackBlock,那么操作就比较复杂

  1. 申请一块相同大小的内存
  2. 拷贝栈上的block的所有元数据到新申请的内存空间上,也就是将数据拷贝到堆上,堆上的block我们叫做result
  3. 更新 result 的 flags,确保其引用计数为 0;
  4. 更新 result 的 flags,添加 BLOCK_NEEDS_FREE,并设置其引用计数为 1;
  5. 将 result 的 isa 指向 _NSConcreteMallocBlock。标明 result 是一个堆 Block;
  6. 如果 result 捕获了对象,调用编译器生成的拷贝协助方法 retain 被捕获的对象。

既然我们的Block被拷贝了,那么Block中的捕获的变量也会一起被拷贝到堆区,这里我们直接看一下总结

这里需要注意被捕获的__block结构体,我们分析一下源码

bash 复制代码
static int _Byref_flag_initial_value = BLOCK_NEEDS_FREE | 2;

/*
 * 当拷贝目标为 __block 修饰变量而生成的结构体时,则执行
*/
static void _Block_byref_assign_copy(void *dest, const void *arg, const int flags) {
    struct Block_byref **destp = (struct Block_byref **)dest;
    struct Block_byref *src = (struct Block_byref *)arg;
        
    ///Junes __block 变量结构体还在栈区,拷贝它
    if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
        ///Junes 判断这是否为一个弱引用
        bool isWeak = ((flags & (BLOCK_FIELD_IS_BYREF|BLOCK_FIELD_IS_WEAK)) == (BLOCK_FIELD_IS_BYREF|BLOCK_FIELD_IS_WEAK));
        ///Junes 申请相同大小的空间
        struct Block_byref *copy = (struct Block_byref *)_Block_allocator(src->size, false, isWeak);
        ///Junes 将新结构体标记为堆,并将引用计数置为 2。一份给调用者,一份给栈。
        copy->flags = src->flags | _Byref_flag_initial_value;
        ///Juens 将栈区结构体与新结构体的 __forwarding 都之上堆区中的新结构体
        copy->forwarding = copy; 
        src->forwarding = copy; 
        ///Junes 赋值 size
        copy->size = src->size;
        
        ///Junes 如果是弱引用,isa 指向 _NSConcreteWeakBlockVariable。标记为 Block 的弱引用
        if (isWeak) {
            copy->isa = &_NSConcreteWeakBlockVariable;  
        }
        
        ///Junes 如果存在 copy_dispose 内存管理方法,执行
        if (src->flags & BLOCK_HAS_COPY_DISPOSE) {
            ///Junes 将新结构体的内存管理方法指针指向全区源结构体的相应方法
            copy->byref_keep = src->byref_keep;
            copy->byref_destroy = src->byref_destroy;
            ///Junes 调用源结构体的 byref_keep 方法(也就是 _Block_object_assign),管理被捕获的对象内存。不过会加上 BLOCK_BYREF_CALLER 标记
            (*src->byref_keep)(copy, src);
        }
        else {
            ///Junes 仅适用于普通变量(非对象),全字节拷贝 byref_keep
            _Block_memmove(
                (void *)&copy->byref_keep,
                (void *)&src->byref_keep,
                src->size - sizeof(struct Block_byref_header));
        }
    }
    ///Junes 这个结构体已经在堆区,引用计数 +1
    else if ((src->forwarding->flags & BLOCK_NEEDS_FREE) == BLOCK_NEEDS_FREE) {
        latching_incr_int(&src->forwarding->flags);
    }
    // assign byref data block pointer into new Block
    ///Junes 将源结构体指针也指向堆区的这个新结构体
    *destp = src->forwarding;  // _Block_assign(src->forwarding, (void **)destp);
}

这里解释了拷贝后forwarding指针的变化

bash 复制代码
        copy->forwarding = copy; 
        src->forwarding = copy; 

当拷贝到堆上后,栈上的forwarding指向了堆上的新结构体,堆上的结构体仍然指向自身,这里就引出我们在前面一直遗留的问题,forwarding指针的作用

七、__block 与 __forwarding

我们在前面知道了,如果一个block捕获了自由变量,编译器会自动将栈上的block拷贝到堆区,从一个 _NSConcreteStackBlock 变为一个 _NSConcreteMallocBlock

被 __block 标记的自由变量 local__blockB,Block 并不是简单的值拷贝,而是拷贝了 local__blockB 这个结构体(自动被重写成 sturct __Block_byref_local__blockB_0 的一个实例,而 __Block_byref_local__blockB_0.local__blockB 承载之前的自由变量 a)的指针。

当变量被拷贝到堆区后,我们会发现一个问题,现在有两个变量,一个在栈上一个在堆上,此时访问 local__blockB 到底是要访问栈上的 local__blockB 还是堆上的 local__blockB 呢?于是, __forwading 登场了。

我们来看一张经典的图

当一个变量被__block修饰符声明时,编译器会将这个变量包装在一个结构体 中,这个结构体除了存储变量值外,还包含一个名为forwarding的指针。这个forwarding指针的主要功能是指向包含实际变量值的最新版本的地址

初始化时,__block变量仍然存储在栈上,forwarding指向自身

被拷贝后栈上的forwarding指向堆上的被拷贝的结构体,因为后续栈上的变量可能被释放

这也使无论变量在栈上还是在堆上,我们都能访问到正确的同一个变量

所以才会存在绕一大圈的访问方法:

bash 复制代码
(local__blockB.__forwarding->local__blockB) += 10;

八、block发生copy的时机

  1. 手动copy
bash 复制代码
void (^stackBlock)(void) = ^{
    NSLog(@"This is a block on the stack.");
};
void (^heapBlock)(void) = [stackBlock copy]; // 明确复制到堆
  1. block作为函数返回值
bash 复制代码
typedef void (^CompletionBlock)(void);

CompletionBlock myFunction() {
    return [^{ NSLog(@"Block returned from a function."); } copy]; // 返回时复制到堆
}

CompletionBlock block = myFunction();
block();
  1. Block 被赋值给 __block 修饰的变量时

如果你将一个栈上的 Block 赋值给一个 __block 修饰的变量,编译器会自动将这个 Block 复制到堆上。__block 变量用于存储指向堆上 Block 的指针。

bash 复制代码
__block int (^blockVar)(void);

int value = 42;
blockVar = ^{
    return value; // 编译器会自动将该 Block 复制到堆上
};

NSLog(@"%d", blockVar()); // 输出 42
  1. Block 访问了 __block 修饰的变量时

如果 Block 内部访问了 __block 修饰的变量,编译器会自动将该 Block 复制到堆上,以确保变量在 Block 执行时是有效的。

bash 复制代码
__block int value = 42;

int (^blockObj)(void) = ^{
    value = 100; // 访问了 __block 变量,编译器会自动将该 Block 复制到堆上
    return value;
};
NSLog(@"%d", blockObj()); // 输出 100
  1. Block 被 GCD API 持有时

如果你将一个 Block 传递给 Grand Central Dispatch (GCD) API (如 dispatch_async),GCD 会自动将该 Block 复制到堆上,以确保在异步执行期间 Block 是有效的。

bash 复制代码
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
int value = 42;
dispatch_async(queue, ^{
    NSLog(@"%d", value); // GCD 会自动将该 Block 复制到堆上
});

总结

Block的重点其实就在于捕获自动变量与使用__block修饰时forwarding指针的变化,还有其发生拷贝的时机:使用__block变量,使用dispatch API, 手动copy与作为函数返回值

另外重要的还有block的循环引用,后面会一起总结循环引用

相关推荐
SoraLuna3 小时前
「Mac畅玩鸿蒙与硬件28」UI互动应用篇5 - 滑动选择器实现
macos·ui·harmonyos
追风林3 小时前
mac 本地docker-mysql主从复制部署
mysql·macos·docker
yqcoder3 小时前
mac 安装 nodemon
macos
一ge科研小菜鸡3 小时前
macOS开发环境配置与应用开发(详细讲解)
macos
hairenjing11233 小时前
使用 Mac 数据恢复从 iPhoto 图库中恢复照片
windows·stm32·嵌入式硬件·macos·word
2401_865854886 小时前
iOS应用想要下载到手机上只能苹果签名吗?
后端·ios·iphone
zorchp10 小时前
在 MacOS 上跑 kaldi
macos·kaldi
德育处主任10 小时前
Mac和安卓手机互传文件(ADB)
android·macos
土小帽软件测试11 小时前
jmeter基础01-2_环境准备-Mac系统安装jdk
java·测试工具·jmeter·macos·软件测试学习
小沈同学呀13 小时前
Mac M1 Docker创建Rocketmq集群并接入Springboot项目
macos·docker·java-rocketmq·springboot