[iOS开发]block再学习

Posted Billy Miracle

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[iOS开发]block再学习相关的知识,希望对你有一定的参考价值。

一、认识block

(一)block定义

带有自动变量(局部变量)的匿名函数叫做block

(二)block分类

  • 全局block——__NSGlobalBlock__
  • 堆block——__NSMallocBlock__
  • 栈block——__NSStackBlock__

总结:

不使用外界变量的block是__NSGlobalBlock__类型
使用外界变量的block是__NSMallocBlock__类型
在堆block拷贝前的block是__NSStackBlock__类型

除此之外,还有三种系统级别的block类型

  • _NSConcreteAutoBlock
  • _NSConcreteFinalizingBlock
  • _NSConcreteWeakBlockVariable

二、block循环引用

(一)循环引用的产生

self.name = @"Billy";
self.block = ^
    NSLog(@"%@", self.name);
;

同时,编译器给出警告:

⚠️Capturing 'self' strongly in this block is likely to lead to a retain cycle

循环引用的问题在于:

  1. self持有了block
  2. block持有了self(self.name)

这样就形成了self -> block -> self的循环引用。循环引用时:A、B互相引用,引用计数不能为0,dealloc不会被调用。

(二)解决循环引用

1. 强弱共舞

__weak typeof(self) weakSelf = self;
self.name = @"Billy";
self.block = ^
	NSLog(@"%@", weakSelf.name);
;

使用 中介者模式 __weak typeof(self) weakSelf = self将循环引用改为weakself -> self -> block -> weakself。表面看上去还是一个“引用圈”,但是weakself -> self这一层是弱引用——引用计数不处理,使用weak表管理。所以此时在页面析构时self就能正常的调用dealloc了。
但并不是最终的解决方案,此时仍有可能存在着问题,比如如下代码:

__weak typeof(self) weakSelf = self;
self.name = @"Billy";
self.block = ^
	dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^
		NSLog(@"%@", weakSelf.name);
	);
;

这种延时情况,如若调用block之后立马返回上一页进行页面释放,3秒后weakself指向的self已经为nil了,此时的打印就只能打印出null
于是就有了强弱共舞

__weak typeof(self) weakSelf = self;
self.name = @"Billy";
self.block = ^
    __strong typeof(weakSelf) strongSelf = weakSelf;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^
        NSLog(@"%@", strongSelf.name);
    );
;

再加一层临时的强持有,此时的引用就变成了strongself -> weakself -> self -> block -> strongself
看上去又是一个循环引用,但实际上strongSelf是个临时变量,当block作用域结束后就会释放,从而打破循环引用进行释放(让释放延后了3秒)。

2. 其他中间者模式

既然有自动置空,那么也可以手动置空。

__block UIViewController *viewController = self;
self.name = @"Billy";
self.block = ^
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^
        NSLog(@"%@", viewController.name);
        viewController = nil;
    );
;

上述代码也是使用 中介者模式 打破循环应用的——使用viewController作为中介者代替self从而打破循环引用
此时的引用情况为viewController -> self -> block -> viewController (viewController在用完之后手动置空),这里依然会存在问题:但是只要不调用block,仍然存在着循环应用
解决循环引用还有一种方式——不引用

self.name = @"Felix";
self.block = ^(UIViewController *viewController) 
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^
        NSLog(@"%@", viewController.name);
        viewController = nil;
    );

上述代码使用当前viewController作为参数传入block时拷贝一份,就不会出现持有的情况,同时还能使用self的内存空间,能够完美避免循环引用。

3. Q&A

Q:Masonry中是否存在循环引用?

A:Monsary使用的block是当做参数传递的,即便block内部持有self,设置布局的view持有block,但是block不持有view,当block执行完后就释放了,self的引用计数-1,所以block也不会持有self,所以不会导致循环引用

Q:[UIView animateWithDuration: animations:]中是否存在循环引用?

A:UIView动画是类方法,不被self持有(即self持有了view,但view没有实例化)所以不会循环引用

三、block底层

(一)block本质

1.

int main(int argc, const char * argv[]) 
//    int a = 10;
    void(^block)(void) = ^
        printf("Billy");
//        printf("Billy - %d",a);
    ;
    block();
    return 0;

转化成C++代码:

int main(int argc, const char * argv[]) 

    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    return 0;

main函数中可以看到block的赋值是__main_block_impl_0类型,它是C++中的构造函数:

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

block的本质是个 __main_block_impl_0 的结构体对象

fp传递了具体的block的实现__main_block_func_0,然后保存在block结构体的impl中,这就说明了block声明只是将block实现保存起来,具体的函数实现需要自行调用。

2. 当block为堆block时(外接传入变量)重新clang编译

int main(int argc, const char * argv[]) 
    int a = 10;
    void(^block)(void) = ^
//        printf("Billy");
        printf("Billy - %d",a);
    ;
    block();
    return 0;

此时的block构造函数中就会多出一个参数a,并且在block结构体中也会多出一个属性a

struct __main_block_impl_0 
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) 
    ...
  
;
static void __main_block_func_0(struct __main_block_impl_0 *__cself) 
  int a = __cself->a; // bound by copy
        printf("Billy - %d",a);
    

...

int main(int argc, const char * argv[]) 
    int a = 10;
    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    return 0;

接着看__main_block_func_0的实现

__cself__main_block_impl_0的指针,即block本身
int a = __cself->aint a = block->a
由于a只是个属性,所以是堆block只是值拷贝(值相同,内存地址不同)
这也是为什么捕获的外界变量不能直接进行操作的原因,如a++会报错

__block修饰外界变量

int main(int argc, const char * argv[]) 
    __block int a = 10;
    void(^block)(void) = ^
//        printf("Billy");
        printf("Billy - %d",a);
    ;
    block();
    return 0;


__block修饰的属性在底层会生成响应的结构体,保存原始变量的指针,并传递一个指针地址给block,因此是指针拷贝。

(二)block的copy

我们打断点调试:

可以看到objc_retainBlock,继续step into:

可以看到调用block的copy函数:_Block_copy

1.

struct Block_layout *aBlock;

if (!arg) return NULL;
// The following would be better done as a switch statement
aBlock = (struct Block_layout *)arg;
// 判断flags标识位
if (aBlock->flags & BLOCK_NEEDS_FREE) 
    // latches on high
    latching_incr_int(&aBlock->flags);
    return aBlock;

static int32_t latching_incr_int(volatile int32_t *where) 
    while (1) 
        int32_t old_value = *where;
        if ((old_value & BLOCK_REFCOUNT_MASK) == BLOCK_REFCOUNT_MASK) 
            return BLOCK_REFCOUNT_MASK;
        
        if (OSAtomicCompareAndSwapInt(old_value, old_value+2, where)) 
            return old_value+2;
        
    

为什么引用计数是 +2 而不是 +1 ?因为flags的第一号位置已经存储着释放标记。

2.

else if (aBlock->flags & BLOCK_IS_GLOBAL) 
    return aBlock;

是否是全局block——是的话直接返回block

3.

else 
    // Its a stack block.  Make a copy.
    size_t size = Block_size(aBlock);
    struct Block_layout *result = (struct Block_layout *)malloc(size);
    // 开辟堆空间
    if (!result) return NULL;
    memmove(result, aBlock, size); // bitcopy first
#if __has_feature(ptrauth_calls)
    // Resign the invoke pointer as it uses address authentication.
    result->invoke = aBlock->invoke;

#if __has_feature(ptrauth_signed_block_descriptors)
    if (aBlock->flags & BLOCK_SMALL_DESCRIPTOR) 
        uintptr_t oldDesc = ptrauth_blend_discriminator(
                    &aBlock->descriptor,
                    _Block_descriptor_ptrauth_discriminator);
        uintptr_t newDesc = ptrauth_blend_discriminator(
                    &result->descriptor,
                    _Block_descriptor_ptrauth_discriminator);

        result->descriptor =
                    ptrauth_auth_and_resign(aBlock->descriptor,
                                            ptrauth_key_asda, oldDesc,
                                            ptrauth_key_asda, newDesc);
    
#endif
#endif
    // reset refcount
    result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
    result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
    _Block_call_copy_helper(result, aBlock);
    // Set isa last so memory analysis tools see a fully-initialized object.
    result->isa = _NSConcreteMallocBlock;
    return result;

  1. 先通过malloc在堆区开辟一片空间
  2. 再通过memmove将数据从栈区拷贝到堆区
  3. invokeflags同时进行修改
  4. block的isa标记成_NSConcreteMallocBlock

(三)__block的深入探究

1. 第一层拷贝(block)

block中的第一层拷贝其实就是上面的_Block_copy,将block从栈拷贝到堆。

2. 第二层拷贝(捕获变量的内存空间)

在函数声明时会传__main_block_desc_0_DATA结构体,在里面又会去调用__main_block_copy_0函数,__main_block_copy_0里面会调用_Block_object_assign——这就是第二层拷贝的调用入口。

//
// When Blocks or Block_byrefs hold objects then their copy routine helpers use this entry point
// to do the assignment.
//
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_retain_object(object);
        *dest = object;
        break;

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

        *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 = _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;
        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;
        break;

      default:
        break;
    

根据flags & BLOCK_ALL_COPY_DISPOSE_FLAGS进到不同分支来处理捕获到的变量

枚举值数值含义
BLOCK_FIELD_IS_OBJECT3对象
BLOCK_FIELD_IS_BLOCK7block变量
BLOCK_FIELD_IS_BYREF8__block修饰的结构体
BLOCK_FIELD_IS_WEAK16__weak修饰的变量
BLOCK_BYREF_CALLER128处理block_byref内部对象内存的时候会加的一个额外的标记,配合上面的枚举一起使用

此时捕获到的变量是被__block修饰的BLOCK_FIELD_IS_BYREF类型,就会调用*dest = _Block_byref_copy(object);

static struct Block_byref *_Block_byref_copy(const void *arg) 
    // 临时变量的保存
    struct Block_byref *src = (struct Block_byref *)arg;

    if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) 
        // src points to stack
        // 用原目标的大小在堆区生成一个Block_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
        copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
        
        // 原来的区域和新的区域都指向同一个对象,使得block具备了修改能力
        copy->forwarding = copy; // patch heap copy to point to itself
        src->forwarding = copy;  // patch stack to point to heap copy
        copy->size = src->size;

        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
            struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
            struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
            copy2->byref_keep = src2->byref_keep;
            copy2->byref_destroy = src2->byref_destroy;

            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);
                copy3->layout = src3->layout;
            
            // 第三层拷贝
            (*src2->byref_keep)(copy, src);
        
        else 
            // Bitwise copy.
            // This copy includes Block_byref_3, if any.
            memmove(copy+1, src+1, src->size - sizeof(*src));
        
    
    // already copied to heap
    else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) 
        latching_incr_int(&src->forwarding->flags);
    
    
    return src->forwarding;

  • 用原目标name的大小在堆区生成一个Block_byref
  • copy->forwarding = copy; & src->forwarding = copy;——原来的区域和新的区域都指向同一个对象,使得block具备了修改能力
  • (*src2->byref_keep)(copy, src)开始第三层拷贝

3. 第三层拷贝(拷贝对象)

(*src2->byref_keep)(copy, src)点进去会来到Block_byref结构来,而byref_keepBlock_byref的第5个属性

struct Block_byref 
    void * __ptrauth_objc_isa_pointer isa;
    struct Block_byref *forwarding;
    volatile int32_t flags; // contains ref count
    uint32_t size;
;

struct Block_byref_2 
    // requires BLOCK_BYREF_HAS_COPY_DISPOSE
    BlockByrefKeepFunction byref_keep;
    BlockByrefDestroyFunction byref_destroy;
;

struct Block_byref_3 
    // requires BLOCK_BYREF_LAYOUT_EXTENDED
    const char *layout;
;


第5位就等于byref_keep,所以在第二层拷贝时会调用__Block_byref_id_object_copy_131

static void __Block_byref_id_object_copy_131(void *dst,

以上是关于[iOS开发]block再学习的主要内容,如果未能解决你的问题,请参考以下文章

iOS block 为啥官方文档建议用 copy 修饰

iOS开发学习48 OC的lambda block

iOS开发学习48 OC的lambda block

iOS开发——Block使用小结

iOS开发:对Block使用的一次研究总结

iOS开发:对Block使用的一次研究总结