iOS-Block

Blocks的学习

Block的分类

Block根据其类型可以分为三类:

  • 全局Block(NSGlobalBlock)
  • 栈Block(NSMallocBlock)
  • 堆Block(NSStackBlock)

而其区分的规则为:

如果没有引用局部变量,或者只引用了静态变量和全局变量,则为全局Block,如果内部有使用局部变量,如果有被强指针引用过,就是堆Block,如果没有则为栈Block。

objectivec 复制代码
- (void)func2 {
    /**
     --- 全局block,没有使用局部变量,或者只使用了静态变量或者只使用了全局变量
     */
    // 没有使用局部变量
    NSLog(@"block0 - %@",^{});
    // 使用了静态变量
    void(^block1)(void) = ^{
        blockInt = 3;
    };
    NSLog(@"block1 - %@",block1);
    
    /**
     - 堆Block 使用局部变量 并且用强指针引用过
     */
    // 即使用局部变量又使用,静态变量
    NSInteger i = 1;
    void(^block2)(void) = ^{
        NSLog(@"block %ld", i);
        NSLog(@"block static %d", blockInt);
    };
    NSLog(@"block2 - %@",block2);
    // 只使用局部变量
    void(^block3)(void) = ^{
        NSLog(@"block %ld", i);
    };
    NSLog(@"block3 - %@",block3);
    // 使用强指针引用过,再使用若指针引用
    void(^ __weak block4)(void) = block3;
    NSLog(@"block4 - %@",block4);
    /**
     - 栈Block 没有被强引用过的
     */
    // 没有使用强指针引用过
    void (^__weak block5)(void) = ^{
        NSLog(@"block %ld", i);
    };
    NSLog(@"block5 - %@",block5);
}

分析:

block0 : 没有使用任何变量,属于全局block。

block1 : 只使用了静态变量blockInt,属于全局block。

block2 : 使用了局部变量和静态变量,并且有被strong引用过,属于堆block

block3 : 使用了局部变量i,并且有被strong引用过,属于堆block

block4 : 虽然被weak指针引用的,但其已经被strong引用过,属于堆block

block5 : 没有被strong指针引用过。即使使用了局部变量,属于栈block

Block本质

可以将block代码通过clang 重写为C++代码,看其底层实现

objectivec 复制代码
int object_c_origin_block() {
    NSObject *obj = [[NSObject alloc] init];
    void(^ block)(void) = ^ {
        NSLog(@"%@",obj);
    };
    block();
    return 0;
}

重写命令

objectivec 复制代码
xcrun -sdk iphoneos clang -rewrite-objc <OriginFile> -o <CppFile>

得到Block结构的定义:

objectivec 复制代码
struct __object_c_origin_block_block_impl_0 {
  struct __block_impl impl;
  struct __object_c_origin_block_block_desc_0* Desc;
  NSObject *obj;
  __object_c_origin_block_block_impl_0(void *fp, struct __object_c_origin_block_block_desc_0 *desc, NSObject *_obj, int flags=0) : obj(_obj) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

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

从重写的结果,我们可以得到Block的以下特点:

  • block出生就是在栈上(isa指针指向_NSConcreteStackBlock)
  • block有捕获变量的能力(__object_c_origin_block_block_impl_0内部有obj变量)
  • block也是个对象(存在isa指针)

Block循环引用

在使用Block的时候,最容易出现的问题就是循环引用,尤其是在mvvm架构中,Controller引用ViewModel,ViewModel引用Block,有的Block的复制在Controller里面完成,有可能会捕获到Controller,从而造成循环引用。

objectivec 复制代码
- (void)bindViewModel {
    self.viewModel.refreshViewCallBack = ^(void) {
        [self.tableView reloadData];
    };
}

而解决循环引用有以下几种办法:

__weak __strong协作

objectivec 复制代码
- (void)bindViewModel01 {
    __weak typeof(self) weakSelf = self;
    self.viewModel.refreshViewCallBack = ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        [strongSelf.tableView reloadData];
    };
}

block对象,并没有引用self,在执行block的时候strongSelf的生命周期只有在block内部,在block内部,self的引用计数+1,当执行完block,引用计数-1。既没有引起循环引用,又适当延长了self的生命周期,一举双得。

__block

objectivec 复制代码
- (void)bindViewModel02 {
    __block Controller *blockSelf = self;
    self.viewModel.refreshViewCallBack = ^{
        [blockSelf.tableView reloadData];
        blockSelf = nil;
    };
}

使用这种方式,同样也可以解决循环引用,但是要注意,block执行完一次,下一次执行之前记得要给blockSelf重新复制,不然会出问题,显然维护这个是非常麻烦的,所以不推荐。

参数传递

objectivec 复制代码
- (void)bindViewModel03 {
    self.viewModel.refreshViewBlcok = ^(OSMVVMViewController * _Nonnull vc) {
        [vc.tableView reloadData];
    };
    
    self.viewModel.refreshViewBlcok(self);
}

通过block的参数进行传递,同样可以解决循环引用,但是这样做的意义不大,因为block在执行的地方,一定是需要获取到self的,如果已经获取到self了,就可以直接对self操作了,再使用block有点多余。应用并不多,只做了解。

Block中对象的引用计数

来道经典的面试题:

objectivec 复制代码
- (void)func3 {
    NSObject *o = [[NSObject alloc] init];
    NSLog(@"CFGetRetainCount print start");
    NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef)o));
    //1
    void(^strongBlock)(void) = ^ {
        NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef)o));
    };
    strongBlock();
    //3  在栈区引用一次,在堆区又引用一次
    
    void(^ __weak weakBlock)(void) = ^{
        NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef)o));
    };
    weakBlock();
    // 4栈区引用一次
    
    void(^copyBlock)(void) = [strongBlock copy];
    copyBlock();
    // 4 本来就在栈上不用+1。
    
    void(^copyBlock1)(void) = [weakBlock copy];
    copyBlock1();
    // 5
    NSLog(@"CFGetRetainCount print end");
}

Block Copy

在Block源码中,有关于BlockCopy的源码:

objectivec 复制代码
// Copy, or bump refcount, of a block.  If really copying, call the copy helper if present.
// 拷贝 block,
// 如果原来就在堆上,就将引用计数加 1;
// 如果原来在栈上,会拷贝到堆上,引用计数初始化为 1,并且会调用 copy helper 方法(如果存在的话);
// 如果 block 在全局区,不用加引用计数,也不用拷贝,直接返回 block 本身
// 参数 arg 就是 Block_layout 对象,
// 返回值是拷贝后的 block 的地址
// 运行?stack -》malloc
void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock;

    // 如果 arg 为 NULL,直接返回 NULL

    if (!arg) return NULL;
    
    // The following would be better done as a switch statement
    // 强转为 Block_layout 类型
    aBlock = (struct Block_layout *)arg;
    const char *signature = _Block_descriptor_3(aBlock)->signature;
    
    // 如果现在已经在堆上
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        // 就只将引用计数加 1
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    // 如果 block 在全局区,不用加引用计数,也不用拷贝,直接返回 block 本身
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
    else {
        // Its a stack block.  Make a copy.
        // block 现在在栈上,现在需要将其拷贝到堆上
        // 在堆上重新开辟一块和 aBlock 相同大小的内存
        struct Block_layout *result =
            (struct Block_layout *)malloc(aBlock->descriptor->size);
        // 开辟失败,返回 NULL
        if (!result) return NULL;
        // 将 aBlock 内存上的数据全部复制新开辟的 result 上
        memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
#if __has_feature(ptrauth_calls)
        // Resign the invoke pointer as it uses address authentication.
        result->invoke = aBlock->invoke;
#endif
        // reset refcount
        // 将 flags 中的 BLOCK_REFCOUNT_MASK 和 BLOCK_DEALLOCATING 部分的位全部清为 0
        result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
        // 将 result 标记位在堆上,需要手动释放;并且引用计数初始化为 1
        result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
        // copy 方法中会调用做拷贝成员变量的工作
        _Block_call_copy_helper(result, aBlock);
        // Set isa last so memory analysis tools see a fully-initialized object.
        // isa 指向 _NSConcreteMallocBlock
        result->isa = _NSConcreteMallocBlock;
        return result;
    }
}
  • copy的调用时机,有强指针第一次指向的时候,会调用一次copy。而这次copy,会把栈区的Block拷贝到堆区。在平时使用[block copy]的时候,也会调用。
  • copy的时候根据Block的类型采取了不同的操作,如果是堆block,只进行引用计数+1,相当于浅拷贝,如果是全局block,直接返回,相当于不拷贝,如果是栈block,是重新开辟新的内存并创建,并且isa指向_NSConcreteMallocBlock,设置类型为堆block,然后返回。

在_Block_copy源码中,从栈区copy到堆区的过程中,_Block_call_copy_helper(result, aBlock)的调用时为了复制栈区的Block里面的成员变量,给堆区的Block。

objectivec 复制代码
// 调用 block 的 copy helper 方法,即 Block_descriptor_2 中的 copy 方法
static void _Block_call_copy_helper(void *result, struct Block_layout *aBlock)
{
    // 取得 block 中的 Block_descriptor_2
    struct Block_descriptor_2 *desc = _Block_descriptor_2(aBlock);
    // 如果没有 Block_descriptor_2,就直接返回
    if (!desc) return;

    // 调用 desc 中的 copy 方法,copy 方法中会调用 _Block_object_assign 函数
    (*desc->copy)(result, aBlock); // do fixup
}

其中最最要的代码时找到了一个copy方法然后调用。而这个copy方法是在desc中。需要理解这些代码,需要借助clang重写的c++代码。

在重写的代码中可以看到__object_c_origin_block_hello_block_impl_0的构造函数的调用:

objectivec 复制代码
__object_c_origin_block_hello_block_desc_0_DATA = {
        0,
        sizeof(struct __object_c_origin_block_hello_block_impl_0),
        __object_c_origin_block_hello_block_copy_0,
        __object_c_origin_block_hello_block_dispose_0
};

其实这里根据变量名称,就能猜个大概,__object_c_origin_block_hello_block_desc_0_DATA就是源码中的desc,而__object_c_origin_block_hello_block_copy_0即为(*desc->copy)方法。所以我们要看其中拷贝成员变量的过程,需要关注__object_c_origin_block_hello_block_copy_0的实现。

objectivec 复制代码
static void __object_c_origin_block_hello_block_copy_0(
     struct __object_c_origin_block_hello_block_impl_0*dst,
     struct __object_c_origin_block_hello_block_impl_0*src)
{
        _Block_object_assign((void*)&dst->obj, (void*)src->obj, 8/*BLOCK_FIELD_IS_BYREF*/);
}

这里是因为重写前的objcet-c代码里面的block捕获了1个局部变量,只用copy一个成员变量所以只会有一句,如果捕获多个局部变量,就会有多句。例如:

objectivec 复制代码
static void __object_c_origin_block_hello_block_copy_0(
    struct __object_c_origin_block_hello_block_impl_0*dst, 
    struct __object_c_origin_block_hello_block_impl_0*src)
{
    _Block_object_assign((void*)&dst->obj, (void*)src->obj, 8/*BLOCK_FIELD_IS_BYREF*/);
    _Block_object_assign((void*)&dst->obj1, (void*)src->obj1, 3/*BLOCK_FIELD_IS_OBJECT*/);
 }

其内部其实是调用了_Block_object_assign方法,在源码中也可以找到_Block_object_assign的实现:

objectivec 复制代码
/*******************************************************
 block 可以引用 4 种不同的类型的对象,当 block 被拷贝到堆上时,需要 help,即帮助拷贝一些东西。
 1)基于 C++ 栈的对象
 2)Objective-C 对象
 3)其他 Block
 4)被 __block 修饰的变量
 
 block 的 helper 函数是编译器合成的(比如编译器写的 __main_block_copy_1() 函数),它们被用在 _Block_copy() 函数和 _Block_release() 函数中。copy helper 对基于 C++ 栈的对象调用调用 C++ 常拷贝构造函数,对其他三种对象调用 _Block_object_assign 函数。 dispose helper 对基于 C++ 栈的对象调用析构函数,对其他的三种调用 _Block_object_dispose 函数。
 
 _Block_object_assign 和 _Block_object_dispose 函数的第三个参数 flags 有可能是:
 1)BLOCK_FIELD_IS_OBJECT(3) 表示是一个对象
 2)BLOCK_FIELD_IS_BLOCK(7) 表示是一个 block
 3)BLOCK_FIELD_IS_BYREF(8) 表示是一个 byref,一个被 __block 修饰的变量;如果 __block 变量还被 __weak 修饰,则还会加上 BLOCK_FIELD_IS_WEAK(16)
 
 所以 block 的 copy/dispose helper 只会传入四种值:3,7,8,24
 
 上述的4种类型的对象都会由编译器合成 copy/dispose helper 函数,和 block 的 helper 函数类似,byref 的 copy helper 将会调用 C++ 的拷贝构造函数(不是常拷贝构造),dispose helper 则会调用析构函数。还一样的是,helpers 将会一样调用进两个支持函数中,对于对象和 block,参数值是一样的,都另外附带上 BLOCK_BYREF_CALLER (128) bit 的信息。#疑问:调用的这两个函数是啥?BLOCK_BYREF_CALLER 里究竟存的是什么??
 
 所以 __block copy/dispose helper 函数生成 flag 的值为:对象是 3,block 是 7,带 __weak 的是 16,并且一直有 128,有下面这么几种组合:
    __block id                   128+3       (0x83)
    __block (^Block)             128+7       (0x87)
    __weak __block id            128+3+16    (0x93)
    __weak __block (^Block)      128+7+16    (0x97)
 
********************************************************/

//
// When Blocks or Block_byrefs hold objects then their copy routine helpers use this entry point
// to do the assignment.
// 当 block 和 byref 要持有对象时,它们的 copy helper 函数会调用这个函数来完成 assignment,
// 参数 destAddr 其实是一个二级指针,指向真正的目标指针
void _Block_object_assign(void *destArg, const void *object, const int flags) {
    const void **dest = (const void **)destArg;
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      case BLOCK_FIELD_IS_OBJECT:
        /*******
        id object = ...;
        [^{ object; } copy];
        ********/
        // 默认什么都不干,但在 _Block_use_RR() 中会被 Objc runtime 或者 CoreFoundation 设置 retain 函数,
        // 其中,可能会与 runtime 建立联系,操作对象的引用计数什么的
        _Block_retain_object(object);
        // 使 dest 指向的目标指针指向 object
        *dest = object;
        break;

      case BLOCK_FIELD_IS_BLOCK:
        /*******
        void (^object)(void) = ...;
        [^{ object; } copy];
        ********/

       // 使 dest 指向的拷贝到堆上object
        *dest = _Block_copy(object);
        break;
    
      case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
      case BLOCK_FIELD_IS_BYREF:
        /*******
         // copy the onstack __block container to the heap
         // Note this __weak is old GC-weak/MRC-unretained.
         // ARC-style __weak is handled by the copy helper directly.
         __block ... x;
         __weak __block ... x;
         [^{ x; } copy];
         ********/
         // 使 dest 指向的拷贝到堆上的byref
        *dest = _Block_byref_copy(object);
        break;
        
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
        /*******
         // copy the actual field held in the __block container
         // Note this is MRC unretained __block only. 
         // ARC retained __block is handled by the copy helper directly.
         __block id object;
         __block void (^object)(void);
         [^{ object; } copy];
         ********/

        // 使 dest 指向的目标指针指向 object
        *dest = object;
        break;

      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK  | BLOCK_FIELD_IS_WEAK:
        /*******
         // copy the actual field held in the __block container
         // Note this __weak is old GC-weak/MRC-unretained.
         // ARC-style __weak is handled by the copy helper directly.
         __weak __block id object;
         __weak __block void (^object)(void);
         [^{ object; } copy];
         ********/

        // 使 dest 指向的目标指针指向 object
        *dest = object;
        break;

      default:
        break;
    }
}

这里是对成员变量的类型进行了分类,如果是对象类型的,直接将对象的增加对象的引用计数,如果是Block类型,会对该Block也进行一次_Block_copy操作,如果是__block修饰的,会调用_Block_byref_copy。_Block_byref_copy的解析在下面👇。

__block

对于__block修饰的对象,底层会将其多封装一层。

objectivec 复制代码
struct __Block_byref_obj_0 {
  void *__isa;
__Block_byref_obj_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 NSObject *obj;
};

在对block内部操作,其根本是操作他的__forwarding->obj操作:

objectivec 复制代码
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_qz_4pv2xnmd3g137rwtb0fpj2rr0000gn_T_OSBlockOriginFile_ce67cf_mi_1,(obj->__forwarding->obj));
    (obj->__forwarding->obj) = __null;

在Block Copy的时候,__block修饰的对象或类型在拷贝的过程中会调用_Block_byref_copy进行拷贝。

objectivec 复制代码
// 1. 如果 byref 原来在堆上,就将其拷贝到堆上,拷贝的包括 Block_byref、Block_byref_2、Block_byref_3,
//    被 __weak 修饰的 byref 会被修改 isa 为 _NSConcreteWeakBlockVariable,
//    原来 byref 的 forwarding 也会指向堆上的 byref;
// 2. 如果 byref 已经在堆上,就只增加一个引用计数。
// 参数 dest是一个二级指针,指向了目标指针,最终,目标指针会指向堆上的 byref
static struct Block_byref *_Block_byref_copy(const void *arg) {
    // arg 强转为 Block_byref * 类型
    struct Block_byref *src = (struct Block_byref *)arg;

    // 引用计数等于 0
    if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
        // src points to stack
        // 为新的 byref 在堆中分配内存
        struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
        copy->isa = NULL;
        // byref value 4 is logical refcount of 2: one for caller, one for stack
        // 新 byref 的 flags 中标记了它是在堆上,且引用计数为 2。
        // 为什么是 2 呢?注释说的是 non-GC one for caller, one for stack
        // one for caller 很好理解,那 one for stack 是为什么呢?
        // 看下面的代码中有一行 src->forwarding = copy。src 的 forwarding 也指向了 copy,相当于引用了 copy
        copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
        // 堆上 byref 的 forwarding 指向自己
        copy->forwarding = copy; // patch heap copy to point to itself
        // 原来栈上的 byref 的 forwarding 现在也指向堆上的 byref
        src->forwarding = copy;  // patch stack to point to heap copy
        // 拷贝 size
        copy->size = src->size;

        // 如果 src 有 copy/dispose helper
        if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
            // Trust copy helper to copy everything of interest
            // If more than one field shows up in a byref block this is wrong XXX
            // 取得 src 和 copy 的 Block_byref_2
            struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
            struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
            // copy 的 copy/dispose helper 也与 src 保持一致
            // 因为是函数指针,估计也不是在栈上,所以不用担心被销毁
            copy2->byref_keep = src2->byref_keep;
            copy2->byref_destroy = src2->byref_destroy;

            // 如果 src 有扩展布局,也拷贝扩展布局
            if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) {
                struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1);
                struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1);
                // 没有将 layout 字符串拷贝到堆上,是因为它是 const 常量,不在栈上
                copy3->layout = src3->layout;
            }
            // 调用 copy helper,因为 src 和 copy 的 copy helper 是一样的,所以用谁的都行,调用的都是同一个函数
            (*src2->byref_keep)(copy, src);
        }
        else {
            // Bitwise copy.
            // This copy includes Block_byref_3, if any.
            // 如果 src 没有 copy/dispose helper
            // 将 Block_byref 后面的数据都拷贝到 copy 中,一定包括 Block_byref_3
            memmove(copy+1, src+1, src->size - sizeof(*src));
        }
    }
    // already copied to heap
    // src 已经在堆上,就只将引用计数加 1
    else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
        latching_incr_int(&src->forwarding->flags);
    }
    
    return src->forwarding;
}

__block和Block类似,如果在栈区,会重新malloc一份,进行深拷贝操作,但这两个forwarding都会指向堆区的,如果已经在堆区,只会将其引用计数+1。

参考博客:

iOS八股文(十六)关于Block,你在第几层?

相关推荐
黑化旺仔18 小时前
【OC】Runtime:类元数据、加载初始化与消息派发
开发语言·macos·ios·objective-c
songgeb19 小时前
iOS App支持设置RTL语言的实践
ios·swift·ui kit
软泡芙19 小时前
【iOS】swift启动屏显示黑屏
ios
用户693717500138421 小时前
当你向领导申请 AI 编程工具预算时,如果领导追问“能提效多少,有量化数据吗”,你该怎么回答?
android·后端·ios
Layer21 小时前
一些脱离 Mac 依赖的 Agent 操作 iPhone 实现
ios·ai编程·测试
用户69371750013841 天前
当我说 Claude 搭配 GLM-5.3-Flash 太好用了,同事却提醒我:你可能正在失去对代码的掌控
android·后端·ios
茶底世界之下1 天前
一键裁方会丢掉什么?用 Awaking 给园林照片做两种画幅
ios
Marco_Marco1351 天前
定位SDK接入实战:Android/iOS/鸿蒙三端怎么选、怎么接
android·ios·harmonyos
老李IT笔记2 天前
关掉设备上的 AI 扩展:两个键各管一层,配错了等于没配
人工智能·ios·智能手机
2501_915921432 天前
iOS应用耗电测试:使用Energy Log、Keymob和Instruments进行性能监控与能源消耗分析
android·ios·小程序·https·uni-app·iphone·webview