iOS底層-28:block底層原理

block在我們的代碼中經(jīng)常使用,通過block我們實現(xiàn)了高內(nèi)聚、低耦合,極大的方便了我們的編程,今天我們探究一下block的底層原理。

什么是block?

block是將函數(shù)及其執(zhí)行上下文封裝起來的對象。


通過clang編譯查看底層代碼


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

  • 搜索__ViewController__viewDidLoad_block_impl_0

  • 搜索__ViewController__viewDidLoad_block_func_0

  • 搜索__block_impl


    block內(nèi)部有isa指針,所以其本質(zhì)也是OC對象,他有4個屬性

  • isa

  • Flags

  • Reserved

  • FuncPtr 保存了方法實現(xiàn)的地址

所以說block是將函數(shù)及其執(zhí)行上下文封裝起來的對象。
既然block內(nèi)部封裝了函數(shù),那么它同樣也有參數(shù)和返回值。

block的類型

block有3種類型,全局block(__NSGlobalBlock__)、棧block(__NSStackBlock__)和堆block(__NSMallocBlock__)

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

打印結(jié)果:


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

打印結(jié)果:


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

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

打印結(jié)果:


訪問了外部變量,并且強引用的是堆block

block的變量捕獲

  • clang編譯

    結(jié)構(gòu)體的構(gòu)造方法,多了一個參數(shù)a


結(jié)構(gòu)體中的變量也增加了一個a,而且是在編譯時就自動生成了。


函數(shù)方法中,用一個新的變量賦值,是值拷貝。

前面的a并不是我們寫入block中的a,這也就解釋了局部變量可以被捕獲,但是不能被修改的原因。要想修改a的值,需要用__block修飾。

  • __block


    再次用clang編譯

    _I_ViewController_viewDidLoad方法中,多了一行。多了一個__Block_byref_a_0結(jié)構(gòu)體。

  • 查看__Block_byref_a_0


    結(jié)構(gòu)體中有一個a,__forwarding保存的是a的地址

  • 查看__ViewController__viewDidLoad_block_impl_0


    __ViewController__viewDidLoad_block_impl_0中多了一個__Block_byref_a_0類型的a

  • 再看__ViewController__viewDidLoad_block_func_0


    函數(shù)傳入的是_a->__forwarding,所以這里是指針拷貝,可以直接修改a的值。

block的循環(huán)引用

block的循環(huán)引用是經(jīng)常出現(xiàn)的問題,請看下一段代碼,dealloc方法在ViewController回退時是否會被調(diào)用?

@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退出的時候,打印結(jié)果如下:


dealloc方法沒有調(diào)用,發(fā)生了循環(huán)引用。

self持有block,block又捕獲self.name,selfblock相互持有,發(fā)生循環(huán)引用。

解決辦法

方案一:自動釋放

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


此時持有情況變?yōu)榱?br> self -> block -> weakSelf -\-> self -> name,而weakSelf在一張弱引用表里,不會對self的引用計數(shù)產(chǎn)生影響。

然而這樣是有問題的


我們添加一段代碼,延遲2秒打印,進去后直接出來。

dealloc方法先走,才開始打印,這個時候VC已經(jīng)被釋放了,所以打印為null,self的生命周期并沒有得到保全。

  • 添加一段代碼,以上情況得到解決

    打印結(jié)果

    self會在打印結(jié)束之后才釋放。引用關(guān)系鏈就變成了下面這樣:
    self -> block -> strongSelf -> weakSelf -\-> self -> name

方案二:手動釋放
使用__block修飾符,在block中手動釋放VC


因為auto類型的局部變量可以被block捕獲,但不能修改,所以這里我們要借助__block就可以在block中修改了。

方案三:VC作為參數(shù)傳入
循環(huán)引用的原因是self ->block -> self,所以我們只要不直接持有VC就可以解決問題。 把self通過傳參的方式傳進block,就能解決問題。

block的結(jié)構(gòu)與簽名

這里我們通過開啟匯編,看一下block的底層執(zhí)行流程。

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

新建一個工程是因為Xcode會對block的流程進行優(yōu)化緩存,暴露出來的信息會減少,不方便研究。

  • 打上斷點,開啟Always Show Disassembly
1. 沒有引用外界變量


查看匯編

可以看到調(diào)用了一個objc_retainBlock方法

此時讀寄存器rax,block是一個NSGlobalBlock

  • 添加objc_retainBlock符號斷點


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

  • 添加_Block_copy符號斷點


    可以看到,調(diào)用了libsystem庫里的方法。

  • 下載libclosure-74源碼

  • 搜索_Block_copy

  • 首先看到的是一個Block_layout的結(jié)構(gòu)體,點擊查看

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

這個結(jié)構(gòu)與我們在clang中看到的基本一致。

  • 搜索查看flags

// Values for Block_layout->flags to describe block objects
enum {
    BLOCK_DEALLOCATING =      (0x0001),  // runtime 是否在析構(gòu)
    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;
};

我們查詢的時候還找到了相應(yīng)的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方法中,首先根據(jù)標識符判斷,沒有就會直接返回NULL,有這兩個結(jié)構(gòu)體時,才會根據(jù)Block_descriptor_1的地址進行內(nèi)存平移得到。

  • 回到匯編,執(zhí)行完objc_retainBlock,通過LLDB調(diào)試


    詳細的驗證結(jié)果如下

block的簽名


v:表示無返回值
8:參數(shù)大小為8
@?:是對象類型,是block類型
0:從0號位置開始

2. 捕獲外界變量
  • 查看block類型


    此時是NSStackBlock

  • 執(zhí)行完objc_retainBlock斷點


    block已經(jīng)變?yōu)榱?code>NSMallocBlock

執(zhí)行objc_retainBlock中的_Block_copy方法后,block變?yōu)榱?code>NSMallocBlock。

  • 繼續(xù)往下執(zhí)行到callq

    讀取SEL失敗,這里其實是一個_block_invoke方法,control+step into進入即可驗證
  • 進入_block_invoke

    從匯編可以看出,在這里執(zhí)行了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強轉(zhuǎn)為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);//在堆上開辟一塊內(nèi)存
        if (!result) return NULL;
        memmove(result, aBlock, aBlock->descriptor->size); // 把原來的數(shù)據(jù)拷貝進來
#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編譯,細心的同學會發(fā)現(xiàn)還有兩段這樣的代碼

block_copy_0block_dispose_0一起組裝成了block_desc_0_DATA,保存進Block_layout組,其實就是Block_descriptor_2中的兩個變量。

  • 搜索_Block_object_assign

    根據(jù)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,對對象內(nèi)存進行了持有,這也是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;

只是調(diào)用了_Block_byref_copy

  • 查看_Block_byref_copy
static struct Block_byref *_Block_byref_copy(const void *arg) {
    //Block_byref結(jié)構(gòu)體,進來的變量在底層就是這個類型
    struct Block_byref *src = (struct Block_byref *)arg;

    if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
        // 開辟內(nèi)存
        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;
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容