【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的循环引用,后面会一起总结循环引用

相关推荐
开心就好20254 小时前
iOS App 安全加固流程记录,代码、资源与安装包保护
后端·ios
开心就好20254 小时前
iOS App 性能测试工具怎么选?使用克魔助手(Keymob)结合 Instruments 完成
后端·ios
zhongjiahao1 天前
面试常问的 RunLoop,到底在Loop什么?
ios
wvy2 天前
iOS 26手势返回到根页面时TabBar的动效问题
ios
RickeyBoy2 天前
iOS 图片取色完全指南:从像素格式到工程实践
ios
aiopencode3 天前
使用 Ipa Guard 命令行版本将 IPA 混淆接入自动化流程
后端·ios
二流小码农3 天前
鸿蒙开发:路由组件升级,支持页面一键创建
android·ios·harmonyos
vi_h3 天前
在 macOS 上通过 Docker 安装并运行 Ollama(详细可执行教程)
macos·docker·ollama
iceiceiceice4 天前
iOS PDF阅读器段评实现:如何从 PDFSelection 精准还原一个自然段
前端·人工智能·ios
ssshooter5 天前
Tauri 踩坑 appLink 修改后闪退
前端·ios·rust