iOS Block底层探索

苹果官方资源opensource
objc4-838可编译联调源码
libclosure源码

本章节探究:
1.Block的类型
2.Block的实质
3.内容捕获
4.__block的底层原理
5.Block的循环引用
6.面试题

一、Block的类型

1.Global Block - 全局
__NSGlobalBlock__
2.Malloc Block - 堆
__NSMallocBlock__
3.Stack Block - 栈
__NSStackBlock__

__weak修饰block会在出方法栈的时候立马被销毁。

Block的类型总结:
全局Block:没有捕获外部的局部变量;只使用了全局变量/静态变量。
堆Block:捕获了局部变量;赋值给了强引用
栈Block:捕获了局部变量;赋值给了弱引用

为什么Block要⽤copy关键字修饰?
Block在创建的时候,它的内存是分配在方法栈上的,⽽不是在堆上。
栈区的特点是:对象随时有可能被销毁,⼀旦被销毁,在调⽤的时候,就会造成系统的崩溃。所以我们要使⽤copy把它拷⻉到堆上。
ARC下,对于Block使⽤copystrong其实都⼀样,因为blockretain就是⽤copy来实现的。所以ARCblock使⽤ copystrong 都可以。

二、Block的本质

把上面代码编译成.cpp文件

$ clang -rewrite-objc main.m

打开main.cpp,找到main函数

main.cpp

被编译之后Block就变成了__main_block_impl_0的数据结构了

__main_block_impl_0

1.NSObject *objc就是捕获的外部局部变量;
2.impl.isa = &_NSConcreteStackBlock;这行代码可以看出Block是一个对象
3.Block在创建的时候它是栈Block

栈Block具备捕获外部局部变量的能力。
Block不是在调用的时候才捕获外部局部变量,而是在声明的时候就已经捕获了。

1.汇编分析Block的底层调用逻辑
  • 打开汇编模式Always Show Disassembly

objc_retainBlock打上符号断点,看看它底层怎么调用的

_Block_copy的实现是放在libclosure源码里的,参数是block

打开objc4源码找到objc_retainBlock

objc_retainBlock源码声明
2.源码分析Block的底层调用逻辑

打开libclosure源码找到_Block_copy

// 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;
    
    // 1.如果现在已经在堆上
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        // 就只将引用计数加 1
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    // 2.如果 block 在全局区,不用加引用计数,也不用拷贝,直接返回 block 本身
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
    else {
        // Its a stack block.  Make a copy.
        // 3.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;
    }
}

参数argblock,它会先把block强转成Block_layout这个结构体类型。
Block的本质就是Block_layout结构体。(后面会分析)

  • 1.aBlock已经在堆上,只将引用计数+1;
  • 2.aBlock在全局区,不做操作返回出去;
  • 3.aBlock在栈上,现在需要将其拷贝到堆上,在堆上重新开辟一块和aBlock相同大小的内存。(原本的栈上的在出方法栈的时候就释放掉了)
  • Block_layout结构体

(注意:Block_layout拥有Block_descriptor_2里的copydispose函数是在捕获了堆区外部局部变量的时候才会有。用于这个变量的引用计数管理)

  • _Block_call_copy_helper函数的作用是拷贝aBlock的成员变量的工作。
_Block_call_copy_helper
3.为什么Block需要调用的时候才会执行
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
    }
    int a = 10;
    void (^block)(void) = ^ {
        NSLog(@"%d",a);
    };
    block();
    return NSApplicationMain(argc, argv);
}

main.m编译成main.cpp

$ $ clang -rewrite-objc main.m

打开main.cpp找到mian函数

来看看__main_block_impl_0结构体构造方法和__main_block_func_0函数实现部分:

这里传递的a是指针(不是地址)。

struct __block_impl impl;指定了isaflagsFuncPtr

__block_impl
__main_block_desc_0

__main_block_impl_0结构体与源码里的Block_layout是一样的。这就证实了源码分析部分没有错。

block声明就是把方法实现保存到了__block_impl这个结构体中去;等到调用的时候从这里取出函数实现地址直接调用。

三、内容捕获

  • 捕获栈区的外部局部变量
-(void)test1 {
    int a = 10;
    NSLog(@"a--%p",&a);
    
    void (^block)(void) = ^ {
        NSLog(@"a--%p",&a);
    };
    block();
}

/**
打印结果:
a--0x7ff7bf79c29c
a--0x60000137c2f0
*/

很明显两个变量a同一个东西,block外部的a是栈区地址,block外部的a是堆区地址。
因为在block在栈区的时候就已经捕获了a,当blockcopy到堆区时,原本捕获的成员变量也需要在堆区开辟内存。

如果需要改变原本a的值需要使用__block来修饰a

-(void)test1 {
    __block int a = 10;
    NSLog(@"a--%p",&a);
    
    void (^block)(void) = ^ {
        NSLog(@"a--%p",&a);
        a++;
    };
    block();
    NSLog(@"a: %d", a);
}
/**
打印结果:
a--0x7ff7bf79c29c
a--0x60000137c2f0
a: 11
*/
  • 捕获堆区的外部局部变量
-(void)test2 {
    NSObject *objc = [NSObject new];
    NSLog(@"%@",objc);
    NSLog(@"%ld",(long)CFGetRetainCount((__bridge CFTypeRef)(objc)));
    
    NSLog(@"-------------------");
    
    void (^block)(void) = ^ {
        NSLog(@"%@",objc);
        NSLog(@"%ld",(long)CFGetRetainCount((__bridge CFTypeRef)(objc)));
    };
    
    block();
}

/**
打印结果:
<NSObject: 0x600003a00310>
1
-------------------
<NSObject: 0x600003a00310>
3
*/

可以看到objc的打印是同一个东西。这是因为objc所指向的内存分配本身就在堆区,block在捕获到堆区的外部局部变量时,不需要另外开辟内存空间,只需要堆区block的成员变量指针指向同一块内存空间即可。
因为栈区block和堆区block的成员变量都指向objc实际的堆区内存区域,所以objc的引用计数+2。

  • 是否能捕获 全局变量、静态变量
int b = 20; // 全局变量
static int c = 30; // 静态变量
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
    }
    __block int a = 10; // 局部变量
    NSLog(@"a--%p",&a);
    NSLog(@"b--%p",&b);
    NSLog(@"c--%p",&c);
    void (^block)(void) = ^ {
        NSLog(@"捕获a--%p",&a);
        NSLog(@"捕获b--%p",&b);
        NSLog(@"捕获c--%p",&c);
        a++; // 必须使用__block修饰才能修改局部变量a的值
    };
    block();
    NSLog(@"a: %d", a);
    return NSApplicationMain(argc, argv);
}
2022-06-13 20:33:01.485055+0800 Block的本质[44760:11096843] a--0x7ff7b3368248
2022-06-13 20:33:01.485157+0800 Block的本质[44760:11096843] b--0x10cb9fd58
2022-06-13 20:33:01.485189+0800 Block的本质[44760:11096843] c--0x10cb9fd5c
2022-06-13 20:33:01.485265+0800 Block的本质[44760:11096843] 捕获a--0x6000004ab938
2022-06-13 20:33:01.485300+0800 Block的本质[44760:11096843] 捕获b--0x10cb9fd58
2022-06-13 20:33:01.485327+0800 Block的本质[44760:11096843] 捕获c--0x10cb9fd5c
2022-06-13 20:33:01.485347+0800 Block的本质[44760:11096843] a: 11

可以看到捕获前后变量a的地址明显不一样,那说明他俩不是一个东西。

再把mian.m编译成main.cpp

(请注意这里的a.__forwarding->a,与__block有关后面会讲)

找到__main_block_impl_0结构体声明的地方

__main_block_impl_0

Block只捕获了外部局部变量a;没有捕获全局b和静态c

Block不会捕获全局变量和静态变量。
Block捕获的变量是在Block内部生成新的指针指向捕获变量的内存(浅拷贝)。

四、__block修饰局部变量的底层原理

block内部可以修改全局变量和静态变量的值,但是不允许修改局部变量的值。要箱子block内部修改局部变量的值需要用__block修饰

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
    }
    __block NSObject *obc = [NSObject new];
    void (^block)(void) = ^ {
        NSLog(@"%@",obc);
    };
    block();
    return NSApplicationMain(argc, argv);
}

mian.m编译成main.cpp,找到main函数

__main_block_impl_0是构造函数,它传递一个捕获外部局部变量(__Block_byref_obc_0 *)&obc

__main_block_impl_0

同时在构造__Block_byref_obc_0的时候传递了__Block_byref_id_object_copy_131__Block_byref_id_object_dispose_131这两个函数地址

__Block_byref_obc_0

这里的__forwarding是指向__Block_byref_obc_0本身。

  • _Block_object_assign的分析

__Block_byref_id_object_copy_131的调用时机是在Block从栈拷贝到堆的时候,也就是Block源码分析环节里的_Block_call_copy_helper函数

_Block_call_copy_helper

__Block_byref_id_object_copy_131的底层其实就是调用了_Block_object_assign函数
在源码中找到_Block_object_assign的声明

//
// 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修饰的局部变量,它就会走case BLOCK_FIELD_IS_BYREF代码段,去调用_Block_byref_copy(object);

_Block_byref_copy的源码声明:

// 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;
}
  1. 如果 byref 原来在堆上,就将其拷贝到堆上,拷贝的包括 Block_byrefBlock_byref_2Block_byref_3,被 __weak 修饰的 byref 会被修改 isa_NSConcreteWeakBlockVariable,原来 byrefforwarding 也会指向堆上的 byref;
  2. 如果 byref 已经在堆上,就只增加一个引用计数。

理解:
我们代码的Block在声明的时候一定是在栈上的,当把Block拷贝到堆上时连同捕获的局部变量一起拷贝(因为都存储在结构体__block_impl),如果此时使用__block修饰局部变量了,原本栈上Block__forwording指向栈Block自己,拷贝到堆上Block__forwording指向堆Block自己,而此时栈上的Block__forwording会改变指向堆上Block

__block修饰的局部变量原本是在栈上的,需要拷贝到堆;__block修饰的局部变量原本是在堆上的,将其引用计数+1。

  • _Block_object_dispose的分析
// When Blocks or Block_byrefs hold objects their destroy helper routines call this entry point
// to help dispose of the contents
// 当 block 和 byref 要 dispose 对象时,它们的 dispose helper 会调用这个函数
void _Block_object_dispose(const void *object, const int flags) {
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      // 如果是 byref
      case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
      case BLOCK_FIELD_IS_BYREF:
        // get rid of the __block data structure held in a Block
        // 对 byref 对象做 release 操作
        _Block_byref_release(object);
        break;
      // 如果是 block
      case BLOCK_FIELD_IS_BLOCK:
        // 对 block 做 release 操作
        _Block_release(object);
        break;
      // 如果是对象
      case BLOCK_FIELD_IS_OBJECT:
        // 默认啥也不干,但在 _Block_use_RR() 中可能会被 Objc runtime 或者 CoreFoundation 设置一个 release 函数,里面可能会涉及到 runtime 的引用计数
        _Block_release_object(object);
        break;
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK  | BLOCK_FIELD_IS_WEAK:
        break;
      default:
        break;
    }
}

如果是__block修饰的外部局部变量,会走case BLOCK_FIELD_IS_BYREF: 调用_Block_byref_release函数(对 byref 对象做 release 操作)

_Block_byref_release的源码声明:

// 对 byref 对象做 release 操作,
// 堆上的 byref 需要 release,栈上的不需要 release,
// release 就是引用计数减 1,如果引用计数减到了 0,就将 byref 对象销毁
static void _Block_byref_release(const void *arg) {
    struct Block_byref *byref = (struct Block_byref *)arg;

    // dereference the forwarding pointer since the compiler isn't doing this anymore (ever?)
    // 取得真正指向的 byref,如果 byref 已经被堆拷贝,则取得是堆上的 byref,否则是栈上的,栈上的不需要 release,也没有引用计数
    byref = byref->forwarding;
    
    // byref 被拷贝到堆上,需要 release
    if (byref->flags & BLOCK_BYREF_NEEDS_FREE) {
        // 取得引用计数
        int32_t refcount = byref->flags & BLOCK_REFCOUNT_MASK;
        os_assert(refcount);
        // 引用计数减 1,如果引用计数减到了 0,会返回 true,表示 byref 需要被销毁
        if (latching_decr_int_should_deallocate(&byref->flags)) {
            // 如果 byref 有 dispose helper,就先调用它的 dispose helper
            if (byref->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
                struct Block_byref_2 *byref2 = (struct Block_byref_2 *)(byref+1);
                // dispose helper 藏在 Block_byref_2 里
                (*byref2->byref_destroy)(byref);
            }
            free(byref);
        }
    }
}

byref对象release 操作,堆上的 byref 需要 release,栈上的不需要 releaserelease 就是引用计数减 1,如果引用计数减到了 0,就将 byref 对象销毁。

_Block_object_assign_Block_object_dispose是在Block捕获外部局部变量时成对出现,对捕获的外部变量进行内存管理(引用计数增减)。

总结__block的底层原理:

__block修饰的变量在编译过后会变成 __Block_byref__XXX 类型的结构体,在结构体内部有一 个 __forwarding 的结构体指针,指向结构体本身。 block创建的时候是在栈上的,在将栈block拷⻉到堆上的时候,同时也会将block中捕获的对象拷⻉到堆上,然后就会将栈上的__block修饰对象的__forwarding指针指向堆上的拷⻉之后的对象。 这样我们在block内部修改的时候虽然是修改堆上的对象的值,但是因为栈上的对象的 __forwarding指针将堆和栈的对象链接起来。因此就可以达到修改的目的。

__block不可以用于修饰静态变量和全局变量。

五、循环引用

  • 案例一:
- (void)test {
    self.block = ^{
        self.name = @"AnAn";
    };
}

原因:self -> block -> self

block在声明实现的时候就会捕获外部局部变量,所以案例一不需要调用block也会产生循环引用。
即便把self.name改成_name,在捕获的时候也会把self对象捕获。

block要释放必须self要释放,可使用__weak去修饰self就解决循环引用:

- (void)test {
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        weakSelf.name = @"AnAn";
    };
}

但是依然会有问题,比如block里面有一个dispatch_after

-(void)test {
    self.name = @"lg";
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",weakSelf.name);
        });
    };
    self.block();
}

如果controller还没等到执行dispatch_afterpop/dismiss,等到3s后就会运行dispatch_after,此时打印weakSelf.name的结果是null
造成这样的结果是controllerpop/dismiss后就释放了。

解决方式一:强弱共舞(weak strong dance)

-(void)test {
    self.name = @"lg";
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        NSLog(@"%p",&strongSelf);
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",strongSelf.name);
        });
    };
    self.block();
}

解决方式二:临时变量

-(void)test1 {
    self.name = @"lg";
    __block MyViewController *vc = self;
    self.block = ^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",vc.name);
            vc = nil;
        });
    };
    self.block();
}

解决方式三:block参数

-(void)test2 {
    self.name = @"lg";
    self.block = ^(MyViewController *vc){
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",vc.name);
        });
    };
    self.block(self);
}
  • 案例二:
static MyViewController *_staticSelf;
-(void)test2 {
    __weak typeof(self) weakSelf = self; // self是MyViewController的实例对象
    _staticSelf = weakSelf;
}
// 产生循环引用

__weak typeof(self) weakSelf = self;的意思是用一个弱引用指针指向self所指向的堆内存,不会对堆内存引用计数不会+1。
_staticSelf是不会自动释放的,所以self也是一直不被释放。需要手动把_staticSelf = nil

  • 案例三:
- (void)test3 {
    __weak typeof(self) weakSelf = self;
    self.block1 = ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        NSLog(@"%p", &strongSelf);
        weakSelf.block2 = ^{
            NSLog(@"%@", strongSelf);
        };
        weakSelf.block2();
    };
    
    self.block1();
}
// 产生循环引用

weakSelf -> block2 -> StrongSelf
若把NSLog(@"%@", strongSelf); 换成 NSLog(@"%@", weakSelf);就不存在循环引用了,因为block2捕获的weakSelf是弱引用。

六、面试题

  • 第1道面试题
-(void)test {
    NSObject *objc = [NSObject new];
    NSLog(@"%ld", CFGetRetainCount((__bridge  CFTypeRef)objc));
    
    // 栈+1 堆+1
    void(^block1)(void) = ^{
        NSLog(@"%ld", CFGetRetainCount((__bridge  CFTypeRef)objc));
    };
    block1();
    
    // 栈+1
    void(^__weak block2)(void) = ^{
        NSLog(@"%ld", CFGetRetainCount((__bridge  CFTypeRef)objc));
    };
    block2();
    
     // 堆+1
    void(^block3)(void) = [block2 copy];
    block3();

    void(^block4)(void) = [block1 copy];
    block4();
}

// 13455

上面的分析已经知道Block的本质就是Block_layout,于是我仿照源码写了一个_MyBlock结构体:

// NSObject+Block.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

typedef void(*MyBlockCopyFunction)(void *, const void *);
typedef void(*MyBlockDisposeFunction)(const void *);
typedef void(*MyBlockInvokeFunction)(void *, ...);

enum {
    BLOCK_HAS_COPY_DISPOSE =  (1 << 25),
    BLOCK_HAS_CTOR =          (1 << 26), // helpers have C++ code
    BLOCK_IS_GLOBAL =         (1 << 28),
    BLOCK_HAS_STRET =         (1 << 29), // IFF BLOCK_HAS_SIGNATURE
    BLOCK_HAS_SIGNATURE =     (1 << 30),
};

struct _MyBlockDescriptor1 {
    uintptr_t reserved;
    uintptr_t size;
};

struct _MyBlockDescriptor2 {
    // requires BLOCK_HAS_COPY_DISPOSE
    MyBlockCopyFunction copy;
    MyBlockDisposeFunction dispose;
};

struct _MyBlockDescriptor3 {
    // requires BLOCK_HAS_SIGNATURE
    const char *signature;
    const char *layout;     // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};
// 底层
struct _MyBlock {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved;
    // 函数指针
    MyBlockInvokeFunction invoke;
    struct _MyBlockDescriptor1 *descriptor;

};

static struct _MyBlockDescriptor3 * _My_Block_descriptor_3(struct _MyBlock *aBlock) {
    if (! (aBlock->flags & BLOCK_HAS_SIGNATURE)) return nil;
    uint8_t *desc = (uint8_t *)aBlock->descriptor;
    desc += sizeof(struct _MyBlockDescriptor1);
    if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE) {
        desc += sizeof(struct _MyBlockDescriptor2);
    }
    return (struct _MyBlockDescriptor3 *)desc;
}

static const char *MyBlockTypeEncodeString(id blockObj) {
    struct _MyBlock *block = (__bridge void *)blockObj;
    return _My_Block_descriptor_3(block)->signature;
}
@interface NSObject (Block)

// 打印Block 签名
- (NSMethodSignature *)getBlcokSignature;

// 打印Block 签名
- (NSString *)getBlcokSignatureString;

// 调用block
- (void)invokeBlock;

@end

NS_ASSUME_NONNULL_END
// NSObject+Block.m
#import "NSObject+Block.h"
@implementation NSObject (Block)

- (NSString *)getBlcokSignatureString {
    
    NSMethodSignature *signature = self.getBlcokSignature;
    if (signature) {
        NSMutableString *blockSignature = [NSMutableString stringWithFormat:@"BlcokSignature: return type: %s, ", [signature methodReturnType]];
        for (int i = 0; i < signature.numberOfArguments; i++) {
            [blockSignature appendFormat:@"argument number: %d, argument type: %s ", i+1, [signature getArgumentTypeAtIndex:i]];
        }
        return blockSignature;
    }
    return nil;
}

- (NSMethodSignature *)getBlcokSignature {
    if ([self isKindOfClass:NSClassFromString(@"__NSMallocBlock__")] || [self isKindOfClass:NSClassFromString(@"__NSStackBlock__")] || [self isKindOfClass:NSClassFromString(@"__NSGlobalBlock__")]) {
        NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:MyBlockTypeEncodeString(self)];
        return signature;
    }
    return nil;
}

- (void)invokeBlock {
    NSMethodSignature *signature = self.getBlcokSignature;
    if (signature) {
        // 动态的消息转发
        NSInvocation *blockInvocation = [NSInvocation invocationWithMethodSignature:signature];
        [blockInvocation invokeWithTarget:self];
    }
}
// block OC 对象
- (NSString *)description {
    if ([self isKindOfClass:NSClassFromString(@"__NSMallocBlock__")] || [self isKindOfClass:NSClassFromString(@"__NSStackBlock__")] || [self isKindOfClass:NSClassFromString(@"__NSGlobalBlock__")]) {
        //签名
        return [NSString stringWithFormat:@"<%@:%p>--%@", self.class, self, [self getBlcokSignatureString]];
    }
    return [NSString stringWithFormat:@"<%@:%p>", self.class, self];
}
@end

于是我就可以对系统的Block进行强转成_MyBlock类型去操作内存:

  • 第2.1道面试题:
// ViewController.m
#import "ViewController.h"
#import "NSObject+Block.h"

@implementation ViewController

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-unsafe-retained-assign"
- (void)viewDidLoad {
    [super viewDidLoad];
    [self blockDemo1];
}

- (void)blockDemo1{
    int a = 1;
    // 栈block
    void(^ __weak weakBlock)(void) = ^{
        NSLog(@"-----%d", a);
    };
    // 强转成自定义的_MyBlock结构体类型
    struct _MyBlock *blc = (__bridge struct _MyBlock *)weakBlock;
    void(^strongBlock)(void) = weakBlock;
    blc->invoke = nil;
    strongBlock();
}
@end

此时strongBlockweakBlock指向的是同一块栈block内存;
block的函数实现置为nil,会导致崩溃。

怎么修改不会导致崩溃?

void(^strongBlock)(void) = [weakBlock copy]; 

将栈block拷贝到堆,此时strongBlock是指向堆内存,weakBlock是指向栈内存。blc->invoke = nil;是修改栈block的内容,并不影响strongBlock的调用。

  • 第2.2道面试题:
#import "ViewController.h"
#import "NSObject+Block.h"
@interface ViewController ()

@end

@implementation ViewController

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-unsafe-retained-assign"
- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self blockDemo2];
}

- (void)blockDemo2 {
    int a = 10;
    void(^__weak block1)(void) = nil;
    // {}里的代码块会立马执行
    {
        // 栈block2
        void(^__weak block2)(void) = ^{
            NSLog(@"%d",a);
        };
        block1 = block2;

        int b = 5;
        NSLog(@"%p", &c);
    }
    // 此时出了{...}的作用域b会被释放吗?
    block1();  // 10
}
@end

此时的block1();会打印10。

此时出了{...}的作用域b会被释放吗? 不会。
因为{}的一整块代码需要出了blockDemo2方法作用域才会被释放。

  • 第2.3道面试题:

此时我把blockDemo2再修改一下

- (void)blockDemo3{
    int a = 10;
    void(^__weak block1)(void) = nil;
    {
        // 栈block
        void(^__weak block2)(void) = ^{
            NSLog(@"%d",a);
        };
        block1 = [block2 copy]; 
    }
    block1();
}

此时block1();会崩溃!为什么?
因为[block2 copy];是返回的结果是堆内存的block,同时block1去弱引用这块堆内存并不会发生引用计数+1的情况(堆的内存管理是通过引用计数的),所以block1在出{...}作用域的时候,ARC会自动地[block1 release],导致block1被释放。

要解决上面的问题,需要一个强引用的方式去引用[block2 copy];

 void(^block1)(void) = nil;
  • 第2.4道面试题:
    此时我再把blockDemo2再修改一下
- (void)blockDemo4 {
    NSObject *objc = [NSObject new];
    void(^__weak block1)(void) = nil;
    {
        void(^__weak block2)(void) = ^{
            NSLog(@"%@", objc);
        };
        block1 = block2;
        block1();
    }
    block1();
}

{...}里的block1()会打印objc的地址block1会打印null

<NSObject:0x600000060590>
(null)

因为在{...}里栈block2捕获的objc是在堆区,并且objc的引用计数不会改变,在出{...}objcARC自动释放[objc release]

blockDemo4方法进行如下修改

- (void)blockDemo4{
    NSObject *objc = [NSObject new];
    void(^block1)(void) = nil;
    {
        void(^__weak block2)(void) = ^{
            NSLog(@"%@", objc);
        };
        block1 = [block2 copy];
        block1();
    }
    block1();
}
Block与内存布局相关面试题

面试题:Block中可以修改全局变量全局静态变量局部静态变量局部变量 吗?

  • 可以修改全局变量,全局静态变量。因为全局变量 和 静态全局变量是全局的,作用域很广。

  • 可以修改局部静态变量,不可以修改局部斌量
    1.局部静态变量(static修饰的) 和 局部变量,被block从外面捕获,成为 __main_block_impl_0这个结构体的成员变量;
    2.局部静态变量是以指针形式被block捕获的,由于捕获的是指针,所以可以修改局部静态变量的值;
    3.局部变量是以值方式传递到block的构造函数,只会捕获block中会用到的变量,由于只捕获了变量的值,并非内存地址,所以在block内部不能改变局部变量的值。

  • ARC环境下,一旦使用__block修饰并在block中修改,就会触发copy,block就会从栈区copy到堆区,此时的block是堆区block。

  • ARC模式下
    1.block中引用id类型的数据,无论有没有__block修饰,都会retain
    2.block中引用基础数据类型的数据,没有__block就无法修改变量值;如果有__block修饰,也是在底层修改__Block_byref_a_0结构体,将其内部的forwarding指针指向copy后的地址,来达到值的修改。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 207,113评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,644评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,340评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,449评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,445评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,166评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,442评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,105评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,601评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,066评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,161评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,792评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,351评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,352评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,584评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,618评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,916评论 2 344

推荐阅读更多精彩内容