iOS底层-28:block底层原理

block在我们的代码中经常使用,通过block我们实现了高内聚、低耦合,极大的方便了我们的编程,今天我们探究一下block的底层原理。

什么是block?

block是将函数及其执行上下文封装起来的对象。


通过clang编译查看底层代码


其中ViewController是文件名,viewDidLoad是方法名

  • 搜索__ViewController__viewDidLoad_block_impl_0

  • 搜索__ViewController__viewDidLoad_block_func_0

  • 搜索__block_impl


    block内部有isa指针,所以其本质也是OC对象,他有4个属性

  • isa

  • Flags

  • Reserved

  • FuncPtr 保存了方法实现的地址

所以说block是将函数及其执行上下文封装起来的对象。
既然block内部封装了函数,那么它同样也有参数和返回值。

block的类型

block有3种类型,全局block(__NSGlobalBlock__)、栈block(__NSStackBlock__)和堆block(__NSMallocBlock__)

  • 全局block
    不使用外部变量的block是全局block
 void (^block) (void) = ^ {
        NSLog(@"%d",3);
    };
    NSLog(@"%@",block);

打印结果:


  • block
    使用外部变量,并且未进行copy操作的是栈block
    int a = 10;
    NSLog(@"%@",[^ {
        NSLog(@"%d",a);
    } class]);

打印结果:


  • block
    使用外部变量,并且进行了copy操作的是堆block
    int a = 10;
    
    void (^block) (void) = ^ {
        NSLog(@"%d",a);
    };

    NSLog(@"%@",[block class]);

打印结果:


访问了外部变量,并且强引用的是堆block

block的变量捕获

  • clang编译

    结构体的构造方法,多了一个参数a


结构体中的变量也增加了一个a,而且是在编译时就自动生成了。


函数方法中,用一个新的变量赋值,是值拷贝。

前面的a并不是我们写入block中的a,这也就解释了局部变量可以被捕获,但是不能被修改的原因。要想修改a的值,需要用__block修饰。

  • __block


    再次用clang编译

    _I_ViewController_viewDidLoad方法中,多了一行。多了一个__Block_byref_a_0结构体。

  • 查看__Block_byref_a_0


    结构体中有一个a__forwarding保存的是a的地址

  • 查看__ViewController__viewDidLoad_block_impl_0


    __ViewController__viewDidLoad_block_impl_0中多了一个__Block_byref_a_0类型的a

  • 再看__ViewController__viewDidLoad_block_func_0


    函数传入的是_a->__forwarding,所以这里是指针拷贝,可以直接修改a的值。

block的循环引用

block的循环引用是经常出现的问题,请看下一段代码,dealloc方法在ViewController回退时是否会被调用?

@interface ViewController ()
@property (nonatomic,copy) NSString *name;
@property (nonatomic,copy) dispatch_block_t block;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    
    self.name = @"Tom";
    
    self.block = ^{
        NSLog(@"%@",self.name);
    };
    self.block();
}
- (void)dealloc {
    NSLog(@"dealloc 来了");
}
@end

当ViewController退出的时候,打印结果如下:


dealloc方法没有调用,发生了循环引用。

self持有blockblock又捕获self.nameselfblock相互持有,发生循环引用。

解决办法

方案一:自动释放

解决办法相信大家也都知道,只需要在block之前用__weak修饰self即可。


此时持有情况变为了
self -> block -> weakSelf -\-> self -> name,而weakSelf在一张弱引用表里,不会对self的引用计数产生影响。

然而这样是有问题的


我们添加一段代码,延迟2秒打印,进去后直接出来。

dealloc方法先走,才开始打印,这个时候VC已经被释放了,所以打印为nullself的生命周期并没有得到保全。

  • 添加一段代码,以上情况得到解决

    打印结果

    self会在打印结束之后才释放。引用关系链就变成了下面这样:
    self -> block -> strongSelf -> weakSelf -\-> self -> name

方案二:手动释放
使用__block修饰符,在block中手动释放VC


因为auto类型的局部变量可以被block捕获,但不能修改,所以这里我们要借助__block就可以在block中修改了。

方案三:VC作为参数传入
循环引用的原因是self ->block -> self,所以我们只要不直接持有VC就可以解决问题。 把self通过传参的方式传进block,就能解决问题。

block的结构与签名

这里我们通过开启汇编,看一下block的底层执行流程。

  • 新建一个工程,写上如下代码
- (void)viewDidLoad {
    [super viewDidLoad];
   
    void (^block)(void) = ^{
        NSLog(@"hello ");
    };
    block();
}

新建一个工程是因为Xcode会对block的流程进行优化缓存,暴露出来的信息会减少,不方便研究。

  • 打上断点,开启Always Show Disassembly
1. 没有引用外界变量


查看汇编

可以看到调用了一个objc_retainBlock方法

此时读寄存器raxblock是一个NSGlobalBlock

  • 添加objc_retainBlock符号断点


    可以看到objc_retainBlock里面有一个_Block_copy方法

  • 添加_Block_copy符号断点


    可以看到,调用了libsystem库里的方法。

  • 下载libclosure-74源码

  • 搜索_Block_copy

  • 首先看到的是一个Block_layout的结构体,点击查看

struct Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved;
    BlockInvokeFunction invoke;
    struct Block_descriptor_1 *descriptor;
    // imported variables
};

这个结构与我们在clang中看到的基本一致。

  • 搜索查看flags

// Values for Block_layout->flags to describe block objects
enum {
    BLOCK_DEALLOCATING =      (0x0001),  // runtime 是否在析构
    BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime
    BLOCK_NEEDS_FREE =        (1 << 24), // runtime
    BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler 是否有copy 和dispose
    BLOCK_HAS_CTOR =          (1 << 26), // compiler: helpers have C++ code
    BLOCK_IS_GC =             (1 << 27), // runtime
    BLOCK_IS_GLOBAL =         (1 << 28), // compiler
    BLOCK_USE_STRET =         (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
    BLOCK_HAS_SIGNATURE  =    (1 << 30), // compiler 是否有签名
    BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31)  // compiler
};

里面保存的是block的一些标识位。

  • 搜索查看Block_descriptor_1
#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size;
};

我们查询的时候还找到了相应的Block_descriptor_2Block_descriptor_3

#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
    // requires BLOCK_HAS_COPY_DISPOSE
    BlockCopyFunction copy;
    BlockDisposeFunction dispose;
};

#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
    // requires BLOCK_HAS_SIGNATURE
    const char *signature;
    const char *layout;     // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};

这个Block_descriptor_1是每个block都有的,Block_descriptor_2Block_descriptor_3不是每个block都有的,要看它是否有copydispose方法,一级signaturelayout。配合上面的标识符,判断会不会生成这两个方法。

  • 查看验证Block_descriptor_2Block_descriptor_3


    getter方法中,首先根据标识符判断,没有就会直接返回NULL,有这两个结构体时,才会根据Block_descriptor_1的地址进行内存平移得到。

  • 回到汇编,执行完objc_retainBlock,通过LLDB调试


    详细的验证结果如下

block的签名


v:表示无返回值
8:参数大小为8
@?:是对象类型,是block类型
0:从0号位置开始

2. 捕获外界变量
  • 查看block类型


    此时是NSStackBlock

  • 执行完objc_retainBlock断点


    block已经变为了NSMallocBlock

执行objc_retainBlock中的_Block_copy方法后,block变为了NSMallocBlock

  • 继续往下执行到callq

    读取SEL失败,这里其实是一个_block_invoke方法,control+step into进入即可验证
  • 进入_block_invoke

    从汇编可以看出,在这里执行了block里面的NSLog方法。

_Block_copy深入研究

  • 查看_Block_copy源码
// Copy, or bump refcount, of a block.  If really copying, call the copy helper if present.
void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock;

    if (!arg) return NULL;
    
    // 进来的block强转为Block_layout类型
    aBlock = (struct Block_layout *)arg;
    if (aBlock->flags & BLOCK_NEEDS_FREE) {//是否需要被释放
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {//为global_block时直接返回,不做处理
        return aBlock;
    }
    else {
        // 编译期不能直接生成堆block,这里只能是栈block
        //栈block在这里copy
        struct Block_layout *result =
            (struct Block_layout *)malloc(aBlock->descriptor->size);//在堆上开辟一块内存
        if (!result) return NULL;
        memmove(result, aBlock, aBlock->descriptor->size); // 把原来的数据拷贝进来
#if __has_feature(ptrauth_calls)
       //invoke也拷贝过来
        result->invoke = aBlock->invoke;
#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);
        // 把isa的指向改为_NSConcreteMallocBlock
        result->isa = _NSConcreteMallocBlock;
        return result;
    }
}

这个代码很简单。

block的三层拷贝


将以上代码通过clang编译,细心的同学会发现还有两段这样的代码

block_copy_0block_dispose_0一起组装成了block_desc_0_DATA,保存进Block_layout组,其实就是Block_descriptor_2中的两个变量。

  • 搜索_Block_object_assign

    根据case分为几种情况,这里我们主要研究两种:
  1. BLOCK_FIELD_IS_OBJECT,普通id对象
  2. BLOCK_FIELD_IS_BYREF,__block修饰的对象
enum {
    // see function implementation for a more complete description of these fields and combinations
    BLOCK_FIELD_IS_OBJECT   =  3,  // id, NSObject, __attribute__((NSObject)), block, ...
    BLOCK_FIELD_IS_BLOCK    =  7,  // a block variable
    BLOCK_FIELD_IS_BYREF    =  8,  // the on stack structure holding the __block variable
    BLOCK_FIELD_IS_WEAK     = 16,  // declared __weak, only used in byref copy helpers
    BLOCK_BYREF_CALLER      = 128, // called from __block (byref) copy/dispose support routines.
};
  • BLOCK_FIELD_IS_OBJECT
static void (*_Block_retain_object)(const void *ptr) = _Block_retain_object_default;
static void _Block_retain_object_default(const void *ptr __unused) { }

case BLOCK_FIELD_IS_OBJECT:
        /*******
        id object = ...;
        [^{ object; } copy];
        ********/

        _Block_retain_object(object);
        *dest = object;
        break;

_Block_retain_object什么都没有做,直接交给了ARC处理
dest指针指向了object,对对象内存进行了持有,这也是block强引用的原因。

  • BLOCK_FIELD_IS_BYREF
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;

只是调用了_Block_byref_copy

  • 查看_Block_byref_copy
static struct Block_byref *_Block_byref_copy(const void *arg) {
    //Block_byref结构体,进来的变量在底层就是这个类型
    struct Block_byref *src = (struct Block_byref *)arg;

    if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
        // 开辟内存
        struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
        copy->isa = NULL;//一些赋值操作
        copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
       
       copy->forwarding = copy; // forwarding 是外界变量的地址
        src->forwarding = copy;  // 这两句代码就是将外面变量a的指针,和里面copy都指向了新开辟的空间

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

推荐阅读更多精彩内容