OC底層原理探索-block(下)

本篇文章我們來探索下block的底層原理實現(xiàn)搔啊,棧區(qū)block是如何拷貝的堆區(qū)的,block捕獲外部變量的本質(zhì)北戏,block的數(shù)據(jù)結構等內(nèi)容负芋。

block底層實現(xiàn)
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void(^block)(void) = ^{
            NSLog(@"block探索");
        };
        block();
    }
    return 0;
}

接下來我們clang一下

image.png

  • block是一個結構體,這個結構體定義為__main_block_impl_0嗜愈,該結構體繼承自__block_impl
struct __block_impl {
  void *isa;  //isa指向
  int Flags;
  int Reserved;
  void *FuncPtr;//函數(shù)保存
};
  • 在結構體__main_block_impl_0中有個構造函數(shù)旧蛾,該構造函數(shù)對block結構體中相關屬性進行設置
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
//構造函數(shù)
  __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;
  }
};
  • 我們看到構造函數(shù)的第一個參數(shù)*fp,在構造的時候傳入的是__main_block_func_0蠕嫁,其實是函數(shù)的實現(xiàn)地址锨天,在定義block時,剃毒,將block的任務函數(shù)封裝到FuncPtr屬性中
    image.png
  • 在執(zhí)行block時病袄,其實是調(diào)用block->FuncPtr,然后將block自身作為參數(shù)傳入
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

【總結】通過上述分析赘阀,block其實是個結構體益缠,也可以認為是一個函數(shù)。block將任務保存到結構體的FuncPtr屬性中基公,block->FuncPtr(block)幅慌,block作為隱藏參數(shù),函數(shù)執(zhí)行過程中轰豆,會持有block中的全部數(shù)據(jù)欠痴。

捕獲變量
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        NSObject *obj = [[NSObject alloc] init];
        void(^block)(void) = ^{
            NSLog(@"block探索:%@",obj);
        };
        block();
    }
    return 0;
}

重新clang

WX20210826-163032@2x.png
  • 首先在捕獲到外部變量時迄靠,__main_block_impl_0構造時傳入obj
  • 然后在構造函數(shù)中通過值拷貝的方式,對成員變量_obj賦值
  • 在執(zhí)行任務時,從結構體中取出相應的成員變量進行處理NSObject *obj = __cself->obj;

接下來驗證下是否是值copy

image.png

通過打印內(nèi)外部obj發(fā)現(xiàn)喇辽,兩個obj都指向同一片內(nèi)存,但是指針地址是不同的,這個時候就可以斷定掌挚,這里是值拷貝

這里不允許變更obj是因為,block內(nèi)部有個obj,外部也有菩咨,這就會造成歧義吠式,不知道該改變哪個obj

通過__block捕獲變量
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block NSObject *obj = [NSObject alloc];
        void(^block)(void) = ^{
            
            NSLog(@"block探索:%@",obj);
        };
        block(); 
    }
    return 0;
}

重新clang

WX20210826-164731@2x.png

  • 這里用__block修飾時,會初始化__Block_byref_a_0的結構體
  • block結構體中多出一個__Block_byref_a_0來聲明的屬性obj的取地址
  • obj的地址會賦值到__Block_byref_a_0結構體中的__forwarding屬性
  • 在調(diào)用函數(shù)時,取得也是forwarding中的obj,obj->__forwarding->obj

來看下__Block_byref_obj_0結構體

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;
};

這里我們再來驗證下是值拷貝還是指針拷貝

image.png

這里發(fā)現(xiàn)內(nèi)部和外部obj指針地址是一樣的,由此可以看出當使用__block修飾時,是進行的指針拷貝

block底層真正類型

我們在代碼blcok中打一個斷點抽米,并打開Always show Disassembly

image.png

接下來給objc_retainBlock下一個符號斷點特占,來到了_Block_copy
image.png

_Block_copy符號斷點,發(fā)現(xiàn)是在libsystem_blocks.dylib源碼執(zhí)行
image.png

但是libsystem_blocks.dylib并不開源云茸,我們可以查看libclosure
源碼

Block_layout
  struct Block_layout {
        void * __ptrauth_objc_isa_pointer isa;
        volatile int32_t flags; // contains ref count
        int32_t reserved;
        BlockInvokeFunction invoke;
        struct Block_descriptor_1 *descriptor;
        // imported variables
    };
    
    #define BLOCK_DESCRIPTOR_1 1
    struct Block_descriptor_1 {
        uintptr_t reserved;
        uintptr_t size;
    };

    #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_layout包括

  • isa指向的block類型
  • flags標識碼
  • reserved保留字段
  • invoke函數(shù)是目,即:FuncPtr
  • descriptor附加信息

先來看下flag定義

enum {
    //釋放標記,一般常用于BLOCK_BYREF_NEEDS_FREE做位與運算标捺,一同傳入flags懊纳,告知該block可釋放
    BLOCK_DEALLOCATING =      (0x0001),  // runtime
    //存儲引用引用計數(shù)的 值,是一個可選用參數(shù)
    BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime
    //低16位是否有效的標志亡容,程序根據(jù)它來決定是否增加或者減少引用計數(shù)位的值
    BLOCK_NEEDS_FREE =        (1 << 24), // runtime
    //是否擁有拷貝輔助函數(shù)嗤疯,(a copy helper function)決定block_description_2
    BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler
    //是否擁有block C++析構函數(shù)
    BLOCK_HAS_CTOR =          (1 << 26), // compiler: helpers have C++ code
    //標志是否有垃圾回收,OSX
    BLOCK_IS_GC =             (1 << 27), // runtime
    //標志是否是全局block
    BLOCK_IS_GLOBAL =         (1 << 28), // compiler
    //與BLOCK_HAS_SIGNATURE相對闺兢,判斷是否當前block擁有一個簽名茂缚,用于runtime時動態(tài)調(diào)用
    BLOCK_USE_STRET =         (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
    //是否有簽名
    BLOCK_HAS_SIGNATURE  =    (1 << 30), // compiler
    //使用有拓展,決定block_description_3
    BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31)  // compiler
};
  • Block_descriptor_1block是必然存在的reserved是保留字段,sizeblock大小
  • Block_descriptor_2是可選參數(shù)屋谭,通過Block_layout中的flag來判斷脚囊,將Block_descriptor_1內(nèi)存平移的方式來獲取對應的值
  • Block_descriptor_3也是可選參數(shù),先平移Block_descriptor_1大小,再判斷Block_descriptor_2是否存在桐磁,如果存在再平移Block_descriptor_2,不存在則不需要平移Block_descriptor_2
    image.png
block內(nèi)存變化

引入下面的案例凑术,在block內(nèi)部訪問外部變量

image.png

在經(jīng)過objc_retainBlock方法事,我們打印寄存器發(fā)現(xiàn)為棧block
image.png

在經(jīng)過_Block_copy方法后所意,同樣打印x0寄存器發(fā)現(xiàn)變成了堆block
image.png

【結論】由此得出結論淮逊,捕獲外部變量在編譯時為棧block,運行時通過_Block_copy方法會copy到堆區(qū),變成堆block扶踊。

_Block_copy分析
void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock;

    if (!arg) return NULL;
    
    // The following would be better done as a switch statement
    aBlock = (struct Block_layout *)arg;
    //是否需要釋放
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    //是否是全局block
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
    //這里只能是棧block泄鹏,因為編譯期是不可能申請堆的,這是是將棧copy到堆
    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;
        //進行block copy
        memmove(result, aBlock, size); // bitcopy first
#if __has_feature(ptrauth_calls)
        // Resign the invoke pointer as it uses address authentication.
        // invoke 賦值
        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.
        //修改isa指向堆
        result->isa = _NSConcreteMallocBlock;
        return result;
    }
}
  • 這里現(xiàn)將block強轉成Block_layout類型
  • 判斷是否正在釋放,如果是則不處理
  • 判斷是否是全局block秧耗,如果是則不處理
  • 接下來else判斷备籽,在編譯期不可能是堆block,所以這里一定是棧block
  • 獲取block大小
  • 開辟內(nèi)存空間,通過memmove進行數(shù)據(jù)拷貝,并將invoke 和 flag賦值
  • 最后將isa指向堆
Block_byref結構體拷貝

回到main.cpp文件中,查看__main_block_desc_0結構體定義

image.png

其中size和reservedBlock_descriptor_1的兩個屬性。void (*copy)和void (*dispose)對應的是Block_descriptor_2兩方法。在copy方法中會調(diào)用_Block_object_assign來查看下該方法
image.png

先看下注釋

  • BLOCK_FIELD_IS_OBJECT:捕獲OC對象
  • BLOCK_FIELD_IS_BLOCK :捕獲另一個block
  • BLOCK_FIELD_IS_BYREF : 捕獲用__block聲明的變量
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:
        _Block_retain_object(object);
        *dest = object;
        break;

      case BLOCK_FIELD_IS_BLOCK:
        *dest = _Block_copy(object);
        break;

      case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
      case BLOCK_FIELD_IS_BYREF:
        *dest = _Block_byref_copy(object);
        break;

      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
        *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:
        *dest = object;
        break;

      default:
        break;
    }
}

  • 如果是BLOCK_FIELD_IS_OBJECT车猬,指針指向該對象霉猛,將對該對象進行持有,引用計數(shù)加1
  • BLOCK_FIELD_IS_BLOCK珠闰,捕獲一個block惜浅,則進行_Block_copy操作
  • BLOCK_FIELD_IS_BYREF:用__block聲明,則調(diào)用_Block_byref_copy

_Block_byref_copy源碼

static struct Block_byref *_Block_byref_copy(const void *arg) {
    struct Block_byref *src = (struct Block_byref *)arg;

    // __block 內(nèi)存是一樣 同一個家伙
    if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
        // src points to stack
        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;
        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;
            }
            // 捕獲到了外界的變量 - 內(nèi)存處理 - 生命周期的保存
            (*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;
}
  • 這里是將外部對象封裝成Block_byref *src
  • 然后去malloc一個Block_byref *copy
  • 設置 forwarding 保證內(nèi)外部Block_byref對象指向同一個對象,這也是為什么__block修飾的對象具有修改能力的原因
copy->forwarding = copy; 
src->forwarding = copy; 
  • 拷貝等操作完成以后伏嗜,調(diào)用了byref_keep函數(shù)坛悉,這個函數(shù)又干了些啥事情
Block_byref 中的 object 拷貝分析

先源碼看下 Block_byref相關結構:
// 結構體
struct Block_byref {
void * __ptrauth_objc_isa_pointer isa; // 8
struct Block_byref *forwarding; // 8
volatile int32_t flags; // contains ref count//4
uint32_t size; // 4
};

struct Block_byref_2 {
// requires BLOCK_BYREF_HAS_COPY_DISPOSE
BlockByrefKeepFunction byref_keep; // 8
BlockByrefDestroyFunction byref_destroy; // 8
};

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

__block底層是__Block_byref_obj_0結構體,其聲明的對象,我們可以看到void (*__Block_byref_id_object_copy)(void*, void*)這個屬性其實就是byref_keep變量承绸,傳入的是__Block_byref_id_object_copy_131裸影,
這里byref_keep調(diào)用__Block_byref_id_object_copy_131

image.png

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_byref_id_object_copy_131,我們發(fā)現(xiàn)最終還是進入_Block_object_assign

static void __Block_byref_id_object_copy_131(void *dst, void *src) {
 _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
  • 結合__Block_byref_obj_0結構和Block_byref結構,向下平移40個字節(jié)军熏,得到的是NSObject *obj轩猩,對obj也是調(diào)用了_Block_object_assign函數(shù),傳入的flags是131荡澎。
    image.png
  • 也就是堆中object和棧中object指向同一片內(nèi)存空間均践。

三層拷貝總結

  • block通過_Block_copy函數(shù)從棧拷貝到堆上衔瓮。
  • block捕獲變量Block_byref,通過_Block_object_assign函數(shù)從椂陡剩拷貝到堆上热鞍。
  • Block_byref中的object也是通過_Block_object_assign函數(shù)進行相關拷貝。

注意:只有用__block聲明的對象才會有三層拷貝

block釋放

釋放時調(diào)用的__Block_byref_id_object_dispose_131函數(shù):

static void __Block_byref_id_object_dispose_131(void *src) {
    _Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}

_Block_object_dispose

void _Block_object_dispose(const void *object, const int flags) {
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      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
        _Block_byref_release(object);
        break;
      case BLOCK_FIELD_IS_BLOCK:
        _Block_release(object);
        break;
      case BLOCK_FIELD_IS_OBJECT:
        _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;
    }
}


`

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末衔彻,一起剝皮案震驚了整個濱河市薇宠,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌艰额,老刑警劉巖澄港,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異柄沮,居然都是意外死亡回梧,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進店門祖搓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來狱意,“玉大人,你說我怎么就攤上這事拯欧∠甓冢” “怎么了?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵镐作,是天一觀的道長藏姐。 經(jīng)常有香客問我隆箩,道長,這世上最難降的妖魔是什么羔杨? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任捌臊,我火速辦了婚禮,結果婚禮上问畅,老公的妹妹穿的比我還像新娘娃属。我一直安慰自己,他們只是感情好护姆,可當我...
    茶點故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布矾端。 她就那樣靜靜地躺著,像睡著了一般卵皂。 火紅的嫁衣襯著肌膚如雪秩铆。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天灯变,我揣著相機與錄音殴玛,去河邊找鬼。 笑死添祸,一個胖子當著我的面吹牛滚粟,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播刃泌,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼凡壤,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了耙替?” 一聲冷哼從身側響起亚侠,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎俗扇,沒想到半個月后硝烂,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡铜幽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年滞谢,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片除抛。...
    茶點故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡爹凹,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出镶殷,到底是詐尸還是另有隱情禾酱,我是刑警寧澤,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站颤陶,受9級特大地震影響颗管,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜滓走,卻給世界環(huán)境...
    茶點故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一垦江、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧搅方,春花似錦比吭、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至涛漂,卻和暖如春赏表,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背匈仗。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工瓢剿, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人悠轩。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓间狂,卻偏偏與公主長得像,于是被迫代替她去往敵國和親火架。 傳聞我的和親對象是個殘疾皇子鉴象,可洞房花燭夜當晚...
    茶點故事閱讀 45,685評論 2 360

推薦閱讀更多精彩內(nèi)容