block 源碼解析

什么是block统抬?

首先匪蝙,看一個極簡的block:

int main(int argc, const char * argv[]) {
@autoreleasepool {
void (^block)(void) = ^{
            NSLog(@"Hello, World!");
        };
        block();
}
return 0;
}
block編譯轉換結構

對其執(zhí)行clang -rewrite-objc編譯轉換成C++實現窟感,得到以下代碼:

struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;\
};

// 構造函數(類似于OC的init方法)吓著,返回結構體對象

struct __ViewController__viewDidLoad_block_impl_0 {

  struct __block_impl impl;

  struct __ViewController__viewDidLoad_block_desc_0* Desc;

  __ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

//封裝了block 的執(zhí)行邏輯的函數. 參數 為block

static void __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself) {

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_8v_w12n8yrs075g6wv21qzg72180000gn_T_ViewController_672106_mi_0);

    }

static struct __ViewController__viewDidLoad_block_desc_0 {

  size_t reserved;

  size_t Block_size;

} __ViewController__viewDidLoad_block_desc_0_DATA = { 0, sizeof(struct __ViewController__viewDidLoad_block_impl_0)};

* * **************************************** * **************************************** * **
static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {

    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));

    void (*block)(void) = ((void (*)())&)__ViewController__viewDidLoad_block_func_0(

                                     (void *)__ViewController__viewDidLoad_block_func_0,

                                     &__ViewController__viewDidLoad_block_desc_0_DATA)

                                        );

    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);   // 結構體的第一個變量地址即為結構體地址鲤嫡,所以block 可以直接強制轉換為__block_impl 送挑,然后調用FuncPtr 方法,
}
//移除強制轉換代碼
        // 定義block變量
        void (*block)(void) = &__ViewController__viewDidLoad_block_func_0(
                                                   __ViewController__viewDidLoad_block_func_0,
                                                   &__ViewController__viewDidLoad_block_desc_0_DATA
                                                   );
        // 執(zhí)行block內部的代碼
block->FuncPtr(block); //
}
}

不難看出其中的_ViewController__viewDidLoad_block_impl_0就是block的一個C++的實現(最后面的_0代表是main中的第幾個block)暖眼,也就是說block 的本質是結構體惕耕。

轉換過程

1、首先將block 內部的方法封裝到 static void __ViewController__viewDidLoad_block_func_0 中去

2诫肠、為block 生成一個結構體的數據結果 結構體內部會有兩個成員

struct __block_impl impl;

struct __ViewController__viewDidLoad_block_desc_0* Desc;

3司澎、調用結構體的構造函數創(chuàng)建出一個結構體對象,并且將結構體的地址賦值給對象

void (*block)(void) = &__ViewController__viewDidLoad_block_func_0(

                                                   __ViewController__viewDidLoad_block_func_0,

                                                   &__ViewController__viewDidLoad_block_desc_0_DATA

                                                   );

其中__block_impl的定義如下:

struct __block_impl {

void *isa; 指向所屬類的指針栋豫,也就是block的類型

int Flags; 標志變量挤安,在實現block的內部操作時會用到

int Reserved; 保留變量

void *FuncPtr; block執(zhí)行時調用的函數指針

};

可以看出,它包含了isa指針(包含isa指針的皆為對象)丧鸯,也就是說block也是一個對象(runtime里面蛤铜,對象和類都是用結構體表示)。

___ViewController__viewDidLoad_block_desc_0的定義如下:

static struct __ViewController__viewDidLoad_block_desc_0 {

  size_t reserved;      //保留字段

  size_t Block_size;    //block大小(sizeof(struct __main_block_impl_0))

} __ViewController__viewDidLoad_block_desc_0_DATA = { 0, 

                                  sizeof(struct __ViewController__viewDidLoad_block_impl_0)

 };

以上代碼在定義__ViewController__viewDidLoad_block_desc_0結構體時丛肢,同時創(chuàng)建了__ViewController__viewDidLoad_block_desc_0_DATA围肥,并給它賦值,以供在main函數中對__ViewController__viewDidLoad_block_desc_0_0進行初始化蜂怎。

__ViewController__viewDidLoad_block_impl_0定義了顯式的構造函數穆刻,其函數體如下:

 __ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, int flags=0) {

    impl.isa = &_NSConcreteStackBlock;

    impl.Flags = flags;

    impl.FuncPtr = fp;

    Desc = desc;

  }

可以看出,

  • __ViewController__viewDidLoad_block_impl_0的isa指針指向了_NSConcreteStackBlock派敷,

  • 從main函數中看蛹批, __ViewController__viewDidLoad_block_impl_0的FuncPtr指向了函數__ViewController__viewDidLoad_block_func_0

  • __ViewController__viewDidLoad_block_impl_0的Desc也指向了定義__ViewController__viewDidLoad_block_desc_0 時就創(chuàng)建的 __ViewController__viewDidLoad_block_desc_0_DATA撰洗,其中紀錄了block結構體大小等信息篮愉。

以上就是根據編譯轉換的結果,對一個簡單block的解析差导,后面會將block操作不同類型的外部變量试躏,對block結構的影響進行相應的說明。

block實際結構

接下來觀察下Block_private.h文件中對block的相關結構體的真實定義:

/* Revised new layout. */

struct Block_descriptor {

unsigned long int reserved;

unsigned long int size;

void (*copy)(void *dst, void *src);

void (*dispose)(void *);

};

struct Block_layout {

void *isa;

int flags;

int reserved;

void (*invoke)(void *, ...);

struct Block_descriptor *descriptor;

/* Imported variables. */

};

有了上文對編譯轉換的分析设褐,這里只針對略微不同的成員進行分析:

  • invoke颠蕴,同上文的FuncPtr,block執(zhí)行時調用的函數指針助析,block定義時內部的執(zhí)行代碼都在這個函數中

  • Block_descriptor犀被,block的詳細描述

  • copy/dispose,輔助拷貝/銷毀函數外冀,處理block范圍外的變量時使用

總體來說寡键,block就是一個里面存儲了指向函數體中包含定義block時的代碼塊的函數指針,以及block外部上下文變量等信息的結構體雪隧。

block的類型

Block 本質類型:繼承關系 NSGlobalBlock : NSBlock : NSObject

block的常見類型有3種:

  • _NSConcreteGlobalBlock(全局)

  • _NSConcreteStackBlock(棧)

  • _NSConcreteMallocBlock(堆)

image.png
image.png

每一種類型的block調用copy后的結果如下所示

image.png

附上APUE的進程虛擬內存段分布圖:

image.png

進程虛擬內存空間分布

其中前2種在Block.h種聲明西轩,后1種在Block_private.h中聲明员舵,所以最后1種基本不會在源碼中出現。

由于無法直接創(chuàng)建_NSConcreteMallocBlock類型的block藕畔,所以這里只對前面2種進行手動創(chuàng)建分析马僻,最后1種通過源代碼分析。

NSConcreteGlobalBlock和NSConcreteStackBlock

首先注服,根據前面兩種類型韭邓,編寫以下代碼:

void (^globalBlock)() = ^{
};

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

void (^stackBlock1)() = ^{

};

}
return 0;
}

對其進行編譯轉換后得到以下縮略代碼:

// globalBlock

struct __globalBlock_block_impl_0 {

struct __block_impl impl;

struct __globalBlock_block_desc_0* Desc;

__globalBlock_block_impl_0(void *fp, struct __globalBlock_block_desc_0 *desc, int flags=0) {

impl.isa = &_NSConcreteGlobalBlock;

impl.Flags = flags;

impl.FuncPtr = fp;

Desc = desc;

}

};

...

// stackBlock

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;

}

};

...

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

/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;

void (*stackBlock)() = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);

}

return 0;

}

可以看出globalBlock的isa指向了_NSConcreteGlobalBlock,即在全局區(qū)域創(chuàng)建祠汇,編譯時具體的代碼就已經確定在上圖中的代碼段中了仍秤,block變量存儲在全局數據存儲區(qū);stackBlock的isa指向了_NSConcreteStackBlock可很,即在棧區(qū)創(chuàng)建诗力。

NSConcreteMallocBlock

接下來是在堆中的block,堆中的block無法直接創(chuàng)建我抠,其需要由_NSConcreteStackBlock類型的block拷貝而來(也就是說block需要執(zhí)行copy之后才能存放到堆中)苇本。由于block的拷貝最終都會調用_Block_copy_internal函數,所以觀察這個函數就可以知道堆中block是如何被創(chuàng)建的了:

static void *_Block_copy_internal(const void *arg, const int flags) {

struct Block_layout *aBlock;

...

aBlock = (struct Block_layout *)arg;

...

// Its a stack block. Make a copy.

if (!isGC) {

// 申請block的堆內存

struct Block_layout *result = malloc(aBlock->descriptor->size);

if (!result) return (void *)0;

// 拷貝棧中block到剛申請的堆內存中

memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first

// reset refcount

result->flags &= ~(BLOCK_REFCOUNT_MASK); // XXX not needed

result->flags |= BLOCK_NEEDS_FREE | 1;

// 改變isa指向_NSConcreteMallocBlock菜拓,即堆block類型

result->isa = _NSConcreteMallocBlock;

if (result->flags & BLOCK_HAS_COPY_DISPOSE) {

//printf("calling block copy helper %p(%p, %p)...\n", aBlock->descriptor->copy, result, aBlock);

(*aBlock->descriptor->copy)(result, aBlock); // do fixup

}

return result;

}

else {

...

}

}

從以上代碼以及注釋可以很清楚的看出瓣窄,函數通過memmove將棧中的block的內容拷貝到了堆中,并使isa指向了_NSConcreteMallocBlock纳鼎。

block主要的一些學問就出在棧中block向堆中block的轉移過程中了俺夕。

捕捉變量對block結構的影響

接下來會編譯轉換捕捉不同變量類型的block,以對比它們的區(qū)別贱鄙。

image.png
 局部變量
 void test()
 {
     int age = 10;   //auto 
     static int height = 10; 
     block = ^{

        // age的值捕獲進來(capture)
         NSLog(@"age is %d, height is %d", age, height);
     };
     age = 20;
     height = 20;
 }
 int main(int argc, const char * argv[]) {
     @autoreleasepool {

        test,
        block();
     }
     return 0;
 }
當調用test() 的時候劝贸,age 會從內存中銷毀,當block 再次調用的時候逗宁,只能調用封裝在block 內部的值映九,在內存中找到不到,
但是static 修飾后會在常量區(qū)存在瞎颗,不會被銷毀件甥,什么時候調用都會存在
前:
- (void)test

{

int a = 10;

^{a;};

a = 20;

}

后:

struct __Person__test_block_impl_0 {

struct __block_impl impl;

struct __Person__test_block_desc_0* Desc;

int a;
// a(_a)是構造函數的參數列表初始化形式,相當于a = _a哼拔。從_I_Person_test看引有,傳入的就是a

__Person__test_block_impl_0(void *fp, struct __Person__test_block_desc_0 *desc, int _a, int flags=0) : a(_a) {

impl.isa = &_NSConcreteStackBlock;

impl.Flags = flags;

impl.FuncPtr = fp;

Desc = desc;

}

};

static void __Person__test_block_func_0(struct __Person__test_block_impl_0 *__cself) {

int a = __cself->a; // bound by copy

a;}

static struct __Person__test_block_desc_0 {

size_t reserved;

size_t Block_size;

} __Person__test_block_desc_0_DATA = { 0, sizeof(struct __Person__test_block_impl_0)};

static void _I_Person_test(Person * self, SEL _cmd) {

int a;

(void (*)())&__Person__test_block_impl_0((void *)__Person__test_block_func_0, &__Person__test_block_desc_0_DATA, a);

}

可以看到,block相對于文章開頭增加了一個int類型的成員變量倦逐,他就是用來存儲外部變量a的譬正。可以看出,這次拷貝只是一次值傳遞导帝。并且當我們想在block中進行以下操作時守谓,將會發(fā)生錯誤

^{a = 10;};

編譯器會提示

image.png

錯誤提示

。因為_I_Person_test函數中的a和__Person__test_block_func_0函數中的a并沒有在同一個作用域您单,所以在block對a進行賦值是沒有意義的斋荞,所以編譯器給出了錯誤。我們可以通過地址傳遞來消除以上錯誤:

- (void)test

{

int a = 0;

// 利用指針p存儲a的地址

int *p = &a;

^{

// 通過a的地址設置a的值

*p = 10;

};

}

但是變量a的生命周期是和方法test的棧相關聯的虐秦,當test運行結束平酿,棧隨之銷毀,那么變量a就會被銷毀悦陋,p也就成為了野指針蜈彼。如果block是作為參數或者返回值,這些類型都是跨棧的俺驶,也就是說再次調用會造成野指針錯誤幸逆。

###### 局部靜態(tài)變量

前

- (void)test

{

static int a;

^{

a = 10;

};

}

后:

struct __Person__test_block_impl_0 {

struct __block_impl impl;

struct __Person__test_block_desc_0* Desc;

int *a;

__Person__test_block_impl_0(void *fp, struct __Person__test_block_desc_0 *desc, int *_a, int flags=0) : a(_a) {

impl.isa = &_NSConcreteStackBlock;

impl.Flags = flags;

impl.FuncPtr = fp;

Desc = desc;

}

};

static void __Person__test_block_func_0(struct __Person__test_block_impl_0 *__cself) {

int *a = __cself->a; // bound by copy

// 這里通過局部靜態(tài)變量a的地址來對其進行修改

(*a) = 10;

}

static struct __Person__test_block_desc_0 {

size_t reserved;

size_t Block_size;

} __Person__test_block_desc_0_DATA = { 0, sizeof(struct __Person__test_block_impl_0)};

static void _I_Person_test(Person * self, SEL _cmd) {

static int a;

// 傳入a的地址

(void (*)())&__Person__test_block_impl_0((void *)__Person__test_block_func_0, &__Person__test_block_desc_0_DATA, &a);

}
需要注意一點的是靜態(tài)局部變量是存儲在靜態(tài)數據存儲區(qū)域的,也就是和程序擁有一樣的生命周期暮现,也就是說在程序運行時还绘,都能夠保證block訪問到一個有效的變量。但是其作用范圍還是局限于定義它的函數中栖袋,所以只能在block通過靜態(tài)局部變量的地址來進行訪問拍顷。
關于變量的存儲我原來的這篇博客有提及:c語言臆想--全局---局部變量
全局變量
前:

// 全局靜態(tài)

static int a;

// 全局

int b;

- (void)test

{

^{

a = 10;

b = 10;

};

}

后:

static int a;

int b;

struct __Person__test_block_impl_0 {

struct __block_impl impl;

struct __Person__test_block_desc_0* Desc;

__Person__test_block_impl_0(void *fp, struct __Person__test_block_desc_0 *desc, int flags=0) {

impl.isa = &_NSConcreteStackBlock;

impl.Flags = flags;

impl.FuncPtr = fp;

Desc = desc;

}

};

static void __Person__test_block_func_0(struct __Person__test_block_impl_0 *__cself) {

a = 10;

b = 10;

}

static struct __Person__test_block_desc_0 {

size_t reserved;

size_t Block_size;

} __Person__test_block_desc_0_DATA = { 0, sizeof(struct __Person__test_block_impl_0)};

static void _I_Person_test(Person * self, SEL _cmd) {

(void (*)())&__Person__test_block_impl_0((void *)__Person__test_block_func_0, &__Person__test_block_desc_0_DATA);

}

可以看出,因為全局變量都是在靜態(tài)數據存儲區(qū)塘幅,在程序結束前不會被銷毀昔案,所以block直接訪問了對應的變量,而沒有在__Person__test_block_impl_0結構體中給變量預留位置电媳。

###### 當block內部訪問了對象類型的auto變量時

###### 如果block是在棧上踏揣,將不會對auto變量產生強引用 

###### 如果block被拷貝到堆上 會調用block內部的copy函數 copy函數內部會調用_Block_object_assign函數 _Block_object_assign函數會根據auto變量的修飾符(__strong、__weak匆背、__unsafe_unretained)做出相應的操作呼伸,形成強引用(retain)或者弱引用 

###### 如果block從堆上移除 會調用block內部的dispose函數 dispose函數內部會調用_Block_object_dispose函數 _Block_object_dispose函數會自動釋放引用的auto變量(release) 

###### __block修飾的變量

前:

- (void)test

{

__block int a;

^{

a = 10;

};

}

后:

struct __Block_byref_a_0 {

void *__isa;

__Block_byref_a_0 *__forwarding;

int __flags;

int __size;

int a;

};

struct __Person__test_block_impl_0 {

struct __block_impl impl;

struct __Person__test_block_desc_0* Desc;

__Block_byref_a_0 *a; // by ref

__Person__test_block_impl_0(void *fp, struct __Person__test_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {

impl.isa = &_NSConcreteStackBlock;

impl.Flags = flags;

impl.FuncPtr = fp;

Desc = desc;

}

};

static void __Person__test_block_func_0(struct __Person__test_block_impl_0 *__cself) {

__Block_byref_a_0 *a = __cself->a; // bound by ref

// 注意身冀,這里的_forwarding用來保證操作的始終是堆中的拷貝a钝尸,而不是棧中的a

(a->__forwarding->a) = 10;

}

static void __Person__test_block_copy_0(struct __Person__test_block_impl_0*dst, struct __Person__test_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __Person__test_block_dispose_0(struct __Person__test_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __Person__test_block_desc_0 {

size_t reserved;

size_t Block_size;

void (*copy)(struct __Person__test_block_impl_0*, struct __Person__test_block_impl_0*);

void (*dispose)(struct __Person__test_block_impl_0*);

} __Person__test_block_desc_0_DATA = { 0, sizeof(struct __Person__test_block_impl_0), __Person__test_block_copy_0, __Person__test_block_dispose_0};

static void _I_Person_test(Person * self, SEL _cmd) {

// __block將a包裝成了一個對象

__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0)};

;

(void (*)())&__Person__test_block_impl_0((void *)__Person__test_block_func_0, &__Person__test_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344);

}

可以看到,對比上面的結果搂根,明顯多了__Block_byref_a_0結構體珍促,這個結構體中含有isa指針,所以也是一個對象剩愧,它是用來包裝局部變量a的猪叙。當block被copy到堆中時,__Person__test_block_impl_0的拷貝輔助函數__Person__test_block_copy_0會將__Block_byref_a_0(__block 變量)拷貝至堆中,所以即使局部變量所在堆被銷毀穴翩,block依然能對堆中的局部變量進行操作犬第。其中__Block_byref_a_0成員指針__forwarding用來指向它在堆中的拷貝,其依據源碼如下:

static void _Block_byref_assign_copy(void *dest, const void *arg, const int flags) {

struct Block_byref **destp = (struct Block_byref **)dest;

struct Block_byref *src = (struct Block_byref *)arg;

...

// 堆中拷貝的forwarding指向它自己

copy->forwarding = copy; // patch heap copy to point to itself (skip write-barrier)

// 棧中的forwarding指向堆中的拷貝

src->forwarding = copy; // patch stack to point to heap copy

...

}

這樣做是為了保證操作的值始終是堆中的拷貝芒帕,而不是棧中的值歉嗓。(處理在局部變量所在棧還沒銷毀,就調用block來改變局部變量值的情況背蟆,如果沒有__forwarding指針鉴分,則修改無效)

__block 的內存管理

  • 1、引用

當block在棧上時带膀,并不會對__block變量產生強引用

當block被copy到堆時

  • 會調用block內部的copy函數

  • copy函數內部會調用_Block_object_assign函數

  • _Block_object_assign函數會對__block變量形成強引用(retain)

image.png

__block變量 在 block0 的時候已經被拷貝到堆上志珍,當Block1 調用__Block變量時,不在重復拷貝垛叨,直接調用

  • 2伦糯、銷毀

當block從堆中移除時

  • 會調用block內部的dispose函數

  • dispose函數內部會調用_Block_object_dispose函數

  • _Block_object_dispose函數會自動釋放引用的__block變量(release)

image.png

至于block如何實現對局部變量的拷貝,下面會詳細說明嗽元。

self隱式循環(huán)引用

前:

@implementation Person

{

int _a;

void (^_block)();

}

- (void)test

{

void (^_block)() = ^{

_a = 10;

};

}

@end

后:

struct __Person__test_block_impl_0 {

struct __block_impl impl;

struct __Person__test_block_desc_0* Desc;

// 可以看到舔株,block強引用了self

Person *self;

__Person__test_block_impl_0(void *fp, struct __Person__test_block_desc_0 *desc, Person *_self, int flags=0) : self(_self) {

impl.isa = &_NSConcreteStackBlock;

impl.Flags = flags;

impl.FuncPtr = fp;

Desc = desc;

}

};

static void __Person__test_block_func_0(struct __Person__test_block_impl_0 *__cself) {

Person *self = __cself->self; // bound by copy

(*(int *)((char *)self + OBJC_IVAR_$_Person$_a)) = 10;

}

static void __Person__test_block_copy_0(struct __Person__test_block_impl_0*dst, struct __Person__test_block_impl_0*src) {_Block_object_assign((void*)&dst->self, (void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __Person__test_block_dispose_0(struct __Person__test_block_impl_0*src) {_Block_object_dispose((void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static struct __Person__test_block_desc_0 {

size_t reserved;

size_t Block_size;

void (*copy)(struct __Person__test_block_impl_0*, struct __Person__test_block_impl_0*);

void (*dispose)(struct __Person__test_block_impl_0*);

} __Person__test_block_desc_0_DATA = { 0, sizeof(struct __Person__test_block_impl_0), __Person__test_block_copy_0, __Person__test_block_dispose_0};

static void _I_Person_test(Person * self, SEL _cmd) {

void (*_block)() = (void (*)())&__Person__test_block_impl_0((void *)__Person__test_block_func_0, &__Person__test_block_desc_0_DATA, self, 570425344);

}

如果在編譯轉換前,將_a改成self.a还棱,能很明顯地看出是產生了循環(huán)引用(self強引用block载慈,block強引用self)。那么使用_a呢珍手?經過編譯轉換后办铡,依然可以在__Person__test_block_impl_0看見self的身影。且在函數_I_Person_test中琳要,傳入的參數也是self寡具。通過以下語句,可以看出稚补,不管是用什么形式訪問實例變量童叠,最終都會轉換成self+變量內存偏移的形式。所以在上面例子中使用_a也會造成循環(huán)引用课幕。

static void __Person__test_block_func_0(struct __Person__test_block_impl_0 *__cself) {

Person *self = __cself->self; // bound by copy

// self+實例變量a的偏移值

(*(int *)((char *)self + OBJC_IVAR_$_Person$_a)) = 10;

}

## 循環(huán)引用

## int main(int argc, const char # argv[]) {

##   @autoreleasepool {

##     MJPerson *person = [ [MJPerson alloc] init];

##     person.age = 10;

##     person.block = rf

##     NSLog(@"age is %d", person.age);

##   };

##     NSLog(@"11111111111" );

##     return 0;

## }

image.png
上述代碼的內存圖

image.png

當程序結束局部運行的時候厦坛,person 變量銷毀,但是block 和MIJPerson 還存在相互引用

  • block輔助函數

上文提及到了block輔助copy與dispose處理函數乍惊,這里分析下這兩個函數的內部實現杜秸。在捕獲變量為__block修飾的基本類型,或者為對象時润绎,block才會有這兩個輔助函數撬碟。

block捕捉變量拷貝函數為_Block_object_assign诞挨。在調用復制block的函數_Block_copy_internal時,會根據block有無輔助函數來對捕捉變量拷貝函數_Block_object_assign進行調用呢蛤。而在_Block_object_assign函數中惶傻,也會判斷捕捉變量包裝而成的對象(Block_byref結構體)是否有輔助函數,來進行調用其障。

__block修飾的基本類型的輔助函數

編寫以下代碼:

typedef void(^Block)();

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

@autoreleasepool {

__block int a;

Block block = ^ {

a;

};

}

轉換成C++代碼后:

typedef void(*Block)();

// __block int a

struct __Block_byref_a_0 {

void *__isa;

__Block_byref_a_0 *__forwarding;

int __flags;

int __size;

int a;

};

// block

struct __main_block_impl_0 {

struct __block_impl impl;

struct __main_block_desc_0* Desc;

__Block_byref_a_0 *a; // by ref

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {

impl.isa = &_NSConcreteStackBlock;

impl.Flags = flags;

impl.FuncPtr = fp;

Desc = desc;

}

};

// block函數體

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

__Block_byref_a_0 *a = __cself->a; // bound by ref

(a->__forwarding->a);

}

// 輔助copy函數

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

// 輔助dispose函數

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __main_block_desc_0 {

size_t reserved;

size_t Block_size;

void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);

void (*dispose)(struct __main_block_impl_0*);

} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

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

/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;

// 這里創(chuàng)建了达罗,并將a的flags設置為0

__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0)};

;

Block block = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344);

}

return 0;

}

從上面代碼中,被__block修飾的a變量變?yōu)榱薩_Block_byref_a_0類型静秆,根據這個格式粮揉,從源碼中查看得到相似的定義:

struct Block_byref {

void *isa;

struct Block_byref *forwarding;

int flags; /* refcount; */

int size;

void (*byref_keep)(struct Block_byref *dst, struct Block_byref *src);

void (*byref_destroy)(struct Block_byref *);

/* long shared[0]; */

};

// 做下對比

struct __Block_byref_a_0 {

void *__isa;

__Block_byref_a_0 *__forwarding;

int __flags;

int __size;

int a;

};

// flags/_flags類型

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

BLOCK_FIELD_IS_BLOCK = 7, /* a block variable */

// 被__block修飾的變量

BLOCK_FIELD_IS_BYREF = 8, /* the on stack structure holding the __block variable */

// 被__weak修飾的變量,只能被輔助copy函數使用

BLOCK_FIELD_IS_WEAK = 16, /* declared __weak, only used in byref copy helpers */

// block輔助函數調用(告訴內部實現不要進行retain或者copy)

BLOCK_BYREF_CALLER = 128 /* called from __block (byref) copy/dispose support routines. */

};

可以看出抚笔,__block將原來的基本類型包裝成了對象扶认。因為以上兩個結構體的前4個成員的類型都是一樣的,內存空間排列一致殊橙,所以可以進行以下操作:

// 轉換成C++代碼

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

// _Block_object_assign源碼

void _Block_object_assign(void *destAddr, const void *object, const int flags) {

...

else if ((flags & BLOCK_FIELD_IS_BYREF) == BLOCK_FIELD_IS_BYREF) {

// copying a __block reference from the stack Block to the heap

// flags will indicate if it holds a __weak reference and needs a special isa

_Block_byref_assign_copy(destAddr, object, flags);

}

...

}

// _Block_byref_assign_copy源碼

static void _Block_byref_assign_copy(void *dest, const void *arg, const int flags) {

// 這里因為前面4個成員的內存分布一樣辐宾,所以直接轉換后,使用Block_byref的成員變量名膨蛮,能訪問到__Block_byref_a_0的前面4個成員

struct Block_byref **destp = (struct Block_byref **)dest;

struct Block_byref *src = (struct Block_byref *)arg;

...

else if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {

// 從main函數對__Block_byref_a_0的初始化叠纹,可以看到初始化時將flags賦值為0

// 這里表示第一次拷貝,會進行復制操作敞葛,并修改原來flags的值

// static int _Byref_flag_initial_value = BLOCK_NEEDS_FREE | 2;

// 可以看出誉察,復制后,會并入BLOCK_NEEDS_FREE惹谐,后面的2是block的初始引用計數

...

copy->flags = src->flags | _Byref_flag_initial_value;

...

}

// 已經拷貝到堆了持偏,只增加引用計數

else if ((src->forwarding->flags & BLOCK_NEEDS_FREE) == BLOCK_NEEDS_FREE) {

latching_incr_int(&src->forwarding->flags);

}

// 普通的賦值,里面最底層就*destptr = value;這句表達式

_Block_assign(src->forwarding, (void **)destp);

}

主要操作都在代碼注釋中了氨肌,總體來說鸿秆,__block修飾的基本類型會被包裝為對象,并且只在最初block拷貝時復制一次怎囚,后面的拷貝只會增加這個捕獲變量的引用計數卿叽。

###### 對象的輔助函數

*   沒有__block修飾

typedef void(^Block)();

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

@autoreleasepool {

NSObject *a = [[NSObject alloc] init];

Block block = ^ {

a;

};

}

return 0;

}

首先,在沒有__block修飾時恳守,對象編譯轉換的結果如下考婴,刪除了一些變化不大的代碼:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

NSObject *a = __cself->a; // bound by copy

a;

}

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static struct __main_block_desc_0 {

size_t reserved;

size_t Block_size;

void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);

void (*dispose)(struct __main_block_impl_0*);

} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0),

對象在沒有__block修飾時,并沒有產生__Block_byref_a_0結構體井誉,只是將標志位修改為BLOCK_FIELD_IS_OBJECT蕉扮。而在_Block_object_assign中對應的判斷分支代碼如下:

...

else if ((flags & BLOCK_FIELD_IS_OBJECT) == BLOCK_FIELD_IS_OBJECT) {

_Block_retain_object(object);

_Block_assign((void *)object, destAddr);

}

...

可以看到整胃,block復制時颗圣,會retain捕捉對象喳钟,以增加其引用計數。

*   有__block修飾

typedef void(^Block)();

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

@autoreleasepool {

__block NSObject *a = [[NSObject alloc] init];

Block block = ^ {

a;

};

}

return 0;

}

在這種情況下在岂,編譯轉換的部分結果如下:

struct __Block_byref_a_0 {

void *__isa;

__Block_byref_a_0 *__forwarding;

int __flags;

int __size;

void (*__Block_byref_id_object_copy)(void*, void*);

void (*__Block_byref_id_object_dispose)(void*);

NSObject *a;

};

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

/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;

attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 33554432, sizeof(__Block_byref_a_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131,....};

Block block = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344);

}

// 以下的40表示__Block_byref_a_0對象a的位移(4個指針(32字節(jié))+2個int變量(8字節(jié))=40字節(jié))

static void __Block_byref_id_object_copy_131(void *dst, void *src) {

_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);

}

static void __Block_byref_id_object_dispose_131(void *src) {

_Block_object_dispose(*(void * *) ((char*)src + 40), 131);

}

可以看到奔则,對于對象,__Block_byref_a_0另外增加了兩個輔助函數__Block_byref_id_object_copy蔽午、__Block_byref_id_object_dispose,以實現對對象內存的管理易茬。其中兩者的最后一個參數131表示BLOCK_BYREF_CALLER|BLOCK_FIELD_IS_OBJECT,BLOCK_BYREF_CALLER表示在內部實現中不對a對象進行retain或copy及老;以下為相關源碼:

if ((flags & BLOCK_BYREF_CALLER) == BLOCK_BYREF_CALLER) {

...

else {

// do *not* retain or *copy* __block variables whatever they are

_Block_assign((void *)object, destAddr);

}

}

_Block_byref_assign_copy函數的以下代碼會對上面的輔助函數(__Block_byref_id_object_copy_131)進行調用抽莱;570425344表示BLOCK_HAS_COPY_DISPOSE|BLOCK_HAS_DESCRIPTOR,所以會執(zhí)行以下相關源碼:

if (src->flags & BLOCK_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

copy->byref_keep = src->byref_keep;

copy->byref_destroy = src->byref_destroy;

(*src->byref_keep)(copy, src);

}

ARC中block的工作

image.png

蘋果說明

蘋果文檔提及骄恶,在ARC模式下食铐,在棧間傳遞block時,不需要手動copy棧中的block僧鲁,即可讓block正常工作虐呻。主要原因是ARC對棧中的block自動執(zhí)行了copy,將_NSConcreteStackBlock類型的block轉換成了_NSConcreteMallocBlock的block寞秃。

block試驗

下面對block做點實驗:

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

@autoreleasepool {

int i = 10;

void (^block)() = ^{i;};

__weak void (^weakBlock)() = ^{i;};

void (^stackBlock)() = ^{};

// ARC情況下

// 創(chuàng)建時斟叼,都會在棧中

// <__NSStackBlock__: 0x7fff5fbff730>

NSLog(@"%@", ^{i;});

// 因為block為strong類型,且捕獲了外部變量春寿,所以賦值時朗涩,自動進行了copy

// <__NSMallocBlock__: 0x100206920>

NSLog(@"%@", block);

// 如果是weak類型的block,依然不會自動進行copy

// <__NSStackBlock__: 0x7fff5fbff728>

NSLog(@"%@", weakBlock);

// 如果block是strong類型绑改,并且沒有捕獲外部變量馋缅,那么就會轉換成__NSGlobalBlock__

// <__NSGlobalBlock__: 0x100001110>

NSLog(@"%@", stackBlock);

// 在非ARC情況下,產生以下輸出

// <__NSStackBlock__: 0x7fff5fbff6d0>

// <__NSStackBlock__: 0x7fff5fbff730>

// <__NSStackBlock__: 0x7fff5fbff700>

// <__NSGlobalBlock__: 0x1000010d0>

}

return 0;

}

可以看出绢淀,ARC對類型為strong且捕獲了外部變量的block進行了copy萤悴。并且當block類型為strong,但是創(chuàng)建時沒有捕獲外部變量皆的,block最終會變成NSGlobalBlock類型(這里可能因為block中的代碼沒有捕獲外部變量覆履,所以不需要在棧中開辟變量,也就是說费薄,在編譯時硝全,這個block的所有內容已經在代碼段中生成了,所以就把block的類型轉換為全局類型)

block作為參數傳遞

再來看下使用在棧中的block需要注意的情況:

NSMutableArray *arrayM;

void myBlock()

{

int a = 5;

Block block = ^ {

NSLog(@"%d", a);

};

[arrayM addObject:block];

NSLog(@"%@", block);

}

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

@autoreleasepool {

arrayM = @[].mutableCopy;

myBlock();

Block block = [arrayM firstObject];

// 非ARC這里崩潰

block();

}

// ARC情況下輸出

// <__NSMallocBlock__: 0x100214480>

// 非ARC情況下輸出

// <__NSStackBlock__: 0x7fff5fbff738>

// 崩潰楞抡,野指針錯誤

可以看到伟众,ARC情況下因為自動執(zhí)行了copy,所以返回類型為NSMallocBlock召廷,在函數結束后依然可以訪問凳厢;而非ARC情況下账胧,需要我們手動調用[block copy]來將block拷貝到堆中,否則因為棧中的block生命周期和函數中的棧生命周期關聯先紫,當函數退出后治泥,相應的堆被銷毀,block也就不存在了遮精。

如果把block的以下代碼刪除:

NSLog(@"%d", a);

那么block就會變成全局類型居夹,在main中訪問也不會出崩潰。

block作為返回值

在非ARC情況下本冲,如果返回值是block准脂,則一般這樣操作:

return [[block copy] autorelease];

對于外部要使用的block,更趨向于把它拷貝到堆中檬洞,使其脫離棧生命周期的約束意狠。

block屬性

這里還有一點關于block類型的ARC屬性。上文也說明了疮胖,ARC會自動幫strong類型且捕獲外部變量的block進行copy环戈,所以在定義block類型的屬性時也可以使用strong,不一定使用copy澎灸。也就是以下代碼:

/** 假如有棧block賦給以下兩個屬性 **/

// 這里因為ARC院塞,當棧block中會捕獲外部變量時,這個block會被copy進堆中

// 如果沒有捕獲外部變量性昭,這個block會變?yōu)槿诸愋?/p>

// 不管怎么樣拦止,它都脫離了棧生命周期的約束

@property (strong, nonatomic) Block *strongBlock;

// 這里都會被copy進堆中

@property (copy, nonatomic) Block *copyBlock;


面試題

  • block的原理是怎樣的?本質是什么糜颠?

封裝了函數調用以及調用環(huán)境的OC對象

  • __block的作用是什么汹族?有什么使用注意點?

變量會被包裝成一個block 對象其兴,解決block 無法修改內部auto 變量的問題顶瞒,需要注意內存管理,

  • __block說明符類似static元旬、auto榴徐、register一樣,只要觀察到該變量被 block 所持有,就將“外部變量”在棧中的內存地址放到了堆中匀归。 進而在block內部也可以修改外部變量的值坑资。

  • __block可以用于解決block內部無法修改auto變量值的問題

  • __block不能修飾全局變量、靜態(tài)變量(static)

  • 編譯器會將__block變量包裝成一個對象

  • block的屬性修飾詞為什么是copy穆端?使用block有哪些使用注意袱贮?

block一旦沒有進行copy操作,就不會在堆上 使用注意:循環(huán)引用問題 block在体啰,在ARC下攒巍,用strong 和copy 沒有區(qū)別嗽仪,在MRC 下有區(qū)別,copy 會拷貝到堆上窑业,strong不會拷貝钦幔,引用計數會加1

  • 修改NSMutableArray枕屉,需不需要添加__block常柄?

    不需要,當變量是一個指針的時候搀擂,block里只是復制了一份這個指針西潘,兩個指針指向同一個地址。所以哨颂,在block里面對指針指向內容做的修改喷市,在block外面也一樣生效。

image.png

  1. Strong 修飾weakself 為了編譯器編譯威恼,weakSelf 無法直接調用_age,

  2. 為了防止self 提前釋放品姓,

參考博文

Block技巧與底層解析http://www.reibang.com/p/51d04b7639f1


?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末沸移,一起剝皮案震驚了整個濱河市章贞,隨后出現的幾起案子看幼,更是在濱河造成了極大的恐慌弛槐,老刑警劉巖醋旦,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件粟按,死亡現場離奇詭異阻桅,居然都是意外死亡翔烁,警方通過查閱死者的電腦和手機弦牡,發(fā)現死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門友驮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人驾锰,你說我怎么就攤上這事卸留。” “怎么了椭豫?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵艾猜,是天一觀的道長。 經常有香客問我捻悯,道長匆赃,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任今缚,我火速辦了婚禮算柳,結果婚禮上,老公的妹妹穿的比我還像新娘姓言。我一直安慰自己瞬项,他們只是感情好蔗蹋,可當我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著囱淋,像睡著了一般猪杭。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上妥衣,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天皂吮,我揣著相機與錄音,去河邊找鬼税手。 笑死蜂筹,一個胖子當著我的面吹牛,可吹牛的內容都是我干的芦倒。 我是一名探鬼主播艺挪,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼兵扬!你這毒婦竟也來了麻裳?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤器钟,失蹤者是張志新(化名)和其女友劉穎津坑,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體俱箱,經...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡国瓮,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了狞谱。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片乃摹。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖跟衅,靈堂內的尸體忽然破棺而出孵睬,到底是詐尸還是另有隱情,我是刑警寧澤伶跷,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布掰读,位于F島的核電站,受9級特大地震影響叭莫,放射性物質發(fā)生泄漏蹈集。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一雇初、第九天 我趴在偏房一處隱蔽的房頂上張望拢肆。 院中可真熱鬧,春花似錦、人聲如沸郭怪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鄙才。三九已至颂鸿,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間攒庵,已是汗流浹背嘴纺。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留叙甸,地道東北人颖医。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓位衩,卻偏偏與公主長得像裆蒸,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子糖驴,可洞房花燭夜當晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內容

  • Block源碼解析和深入理解 Block的本質 Block是"帶有自動變量值的匿名函數". 我們通過Clang(L...
    十三億少女夢丶閱讀 3,146評論 1 18
  • 目錄 Block底層解析什么是block僚祷?block編譯轉換結構block實際結構block的類型NSConcre...
    tripleCC閱讀 33,189評論 32 388
  • 1: 什么是block?1.0: Block的語法1.1: block編譯轉換結構1.2: block實際結構 2...
    iYeso閱讀 840評論 0 5
  • 目錄 Block底層解析什么是block贮缕?block編譯轉換結構block實際結構block的類型NSConcre...
    陽明先生_X自主閱讀 217評論 0 1
  • iOSBlock底層原理解析 目錄 Block底層解析什么是block辙谜?block編譯轉換結構block實際結構b...
    荒漠現甘泉閱讀 940評論 0 0