iOS 棧對(duì)象改艇、堆對(duì)象和Block

棧對(duì)象、堆對(duì)象

是一塊保存局部變量或函數(shù)參數(shù)值的內(nèi)存區(qū)域坟岔。在現(xiàn)代的操作系統(tǒng)中谒兄,每個(gè)線程都有一個(gè)對(duì)應(yīng)的棧。當(dāng)函數(shù)調(diào)用時(shí)炮车,一個(gè)棧幀Stack Frame會(huì)被放入棧內(nèi)。棧幀保存了這個(gè)函數(shù)涉及的參數(shù)酣溃、局部變量瘦穆、返回地址等相關(guān)信息。當(dāng)函數(shù)返回后這個(gè)棧幀就會(huì)被銷(xiāo)毀赊豌。而這一切都是系統(tǒng)自動(dòng)完成的扛或,程序員無(wú)需關(guān)心。

同樣是一塊內(nèi)存區(qū)域碘饼。在這里內(nèi)存可以隨時(shí)分配和銷(xiāo)毀熙兔,但需要我們自己去請(qǐng)求悲伶,而不是系統(tǒng)幫我們完成。不像Stack里面的變量會(huì)隨著函數(shù)執(zhí)行而銷(xiāo)毀住涉,堆上面的東西在函數(shù)執(zhí)行之外依然能夠存活麸锉。

關(guān)于棧對(duì)象和堆對(duì)象,首先要搞清楚什么是對(duì)象舆声。事實(shí)上對(duì)象就是一段連續(xù)的內(nèi)存空間花沉,它有固定的布局。對(duì)象可以放在棧上也可以放在堆上媳握。對(duì)象被放在棧上就是棧對(duì)象碱屁,被放在堆上就是堆對(duì)象。但是在Object-C中蛾找,對(duì)象都是在堆上面創(chuàng)建的娩脾,比如執(zhí)行以下代碼:

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

obj這個(gè)指針變量本身是保存在棧上面的,它的本質(zhì)就是int類(lèi)型打毛,但是它指向的對(duì)象是保存在堆上面的柿赊。[NSObject alloc]就是請(qǐng)求在堆上面分配內(nèi)存。

那Object-C有沒(méi)有辦法在棧上面創(chuàng)建對(duì)象呢隘冲?Object-C沒(méi)有提供直接的方法闹瞧,但是還是可以通過(guò)以下方式來(lái)創(chuàng)建一個(gè)棧對(duì)象:

struct {
   Class isa;
} fakeNSObject;
fakeNSObject.isa = [NSObject class];
    
NSObject *obj = (__bridge NSObject *)&fakeNSObject;
NSLog(@"%@", [obj description]);

fakeNSObject這個(gè)結(jié)構(gòu)體是保存在棧上面的,但是它有isa展辞,所以根據(jù)對(duì)象的定義它也是對(duì)象奥邮,所以它就成了一個(gè)棧對(duì)象。

事實(shí)上有些語(yǔ)言是支持在棧上面創(chuàng)建對(duì)象的罗珍,比如C++洽腺。在C++中類(lèi)的對(duì)象建立分為兩種,一種是靜態(tài)建立覆旱,如A a蘸朋;另一種是動(dòng)態(tài)建立,如A* p=new A()扣唱,A*p=(A*)malloc()藕坯;

  • 靜態(tài)建立一個(gè)類(lèi)對(duì)象,是由編譯器為對(duì)象在椩肷常空間中分配內(nèi)存炼彪,通過(guò)直接移動(dòng)棧頂指針挪出適當(dāng)?shù)目臻g,然后在這片內(nèi)存空間上調(diào)用構(gòu)造函數(shù)形成一個(gè)棧對(duì)象正歼。

  • 動(dòng)態(tài)建立類(lèi)對(duì)象辐马,是使用new運(yùn)算符將對(duì)象建立在堆空間中,在棧中只保留了指向該對(duì)象的指針局义。

棧對(duì)象有好處也有不好的地方喜爷。好處是內(nèi)存分配很快冗疮,而且它有確切的生命周期,不會(huì)內(nèi)存泄漏檩帐,因?yàn)楹瘮?shù)執(zhí)行完變量會(huì)被自動(dòng)銷(xiāo)毀术幔。但它的生命周期也是它的劣勢(shì)之一,因?yàn)楹瘮?shù)執(zhí)行完就被銷(xiāo)毀了轿塔,沒(méi)辦法在函數(shù)之外再使用它特愿。

那為什么Object-C總是在堆上面創(chuàng)建對(duì)象呢?

  • 1勾缭、因?yàn)?strong>Object-C使用引用計(jì)數(shù)來(lái)管理內(nèi)存揍障。這種方式的好處是一個(gè)對(duì)象可以有多個(gè)Owner,而且只有當(dāng)這個(gè)對(duì)象的Owner個(gè)數(shù)為0時(shí)它才會(huì)被銷(xiāo)毀俩由。而棧對(duì)象有一個(gè)固有的Owner毒嫡,就是它所在的函數(shù)。當(dāng)我們把一個(gè)棧對(duì)象傳給另一段代碼時(shí)幻梯,即使是引用它也沒(méi)辦法阻止它在原有函數(shù)執(zhí)行完畢后被銷(xiāo)毀兜畸,當(dāng)它被銷(xiāo)毀后再去訪問(wèn)它就會(huì)發(fā)生錯(cuò)誤。簡(jiǎn)單而言碘梢,就是棧對(duì)象的生命周期不適合Objective-C的引用計(jì)數(shù)內(nèi)存管理方法咬摇。
  • 2、棧對(duì)象不夠靈活煞躬,它在創(chuàng)建時(shí)長(zhǎng)度就已經(jīng)是固定的肛鹏。而Runtime的靈活性都是基于堆對(duì)象的。

但是Objective-C也不是完全不存在棧對(duì)象恩沛,Block就是一個(gè)特例在扰。

Block

Block的本質(zhì)

先創(chuàng)建一個(gè)簡(jiǎn)單的Block:

int blockStudy() {
    void (^blk)(void) = ^{
        int a = 0;
    };
    blk();
    return 0;
}

將該代碼轉(zhuǎn)為C語(yǔ)言源碼:

// MRC
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc BlockStudy.m

// ARC
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.3.0 BlockStudy.m
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

struct __blockStudy_block_impl_0 {
  struct __block_impl impl;
  struct __blockStudy_block_desc_0* Desc;

  // 構(gòu)造函數(shù)
  __blockStudy_block_impl_0(void *fp, struct __blockStudy_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __blockStudy_block_func_0(struct __blockStudy_block_impl_0 *__cself) {
    int a = 0;
}

static struct __blockStudy_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __blockStudy_block_desc_0_DATA = { 0, sizeof(struct __blockStudy_block_impl_0)};

int blockStudy() {
    void (*blk)(void) = ((void (*)())&__blockStudy_block_impl_0((void *)__blockStudy_block_func_0, &__blockStudy_block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

可以看到

void (^blk)(void) = ^{int a = 0;};

被轉(zhuǎn)換成了

void (*blk)(void) = ((void (*)())&__blockStudy_block_impl_0((void *)__blockStudy_block_func_0, &__blockStudy_block_desc_0_DATA));

去掉類(lèi)型轉(zhuǎn)換的部分,就變成了

void (*blk)(void) = &__blockStudy_block_impl_0(__blockStudy_block_func_0, &__blockStudy_block_desc_0_DATA);

其實(shí)就是調(diào)用__blockStudy_block_impl_0的構(gòu)造函數(shù)在棧區(qū)創(chuàng)建了一個(gè)結(jié)構(gòu)體雷客。

首先看__blockStudy_block_impl_0的定義:

struct __blockStudy_block_impl_0 {
  struct __block_impl impl;
  struct __blockStudy_block_desc_0* Desc;

  // 構(gòu)造函數(shù)
  __blockStudy_block_impl_0(void *fp, struct __blockStudy_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

調(diào)用構(gòu)造函數(shù)時(shí)的第一個(gè)參數(shù)是一個(gè)函數(shù)的指針:

static void __blockStudy_block_func_0(struct __blockStudy_block_impl_0 *__cself) {
    int a = 0;
}

這個(gè)函數(shù)里面的代碼就是我們定義Block時(shí)的執(zhí)行函數(shù)芒珠,這個(gè)函數(shù)指針被保存在impl.FuncPtr。這個(gè)函數(shù)接收的參數(shù)__cself其實(shí)就是Block本身的地址搅裙,相當(dāng)于我們平時(shí)在函數(shù)里面用的self皱卓。

第二個(gè)參數(shù)是一個(gè)靜態(tài)全局變量__blockStudy_block_desc_0_DATA

static struct __blockStudy_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __blockStudy_block_desc_0_DATA = { 0, sizeof(struct __blockStudy_block_impl_0)};

里面保存了Block的大小

執(zhí)行Block的代碼

blk();

被轉(zhuǎn)換為:

((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

去掉類(lèi)型轉(zhuǎn)換的部分

(blk->FuncPtr)(blk);

就是簡(jiǎn)單的使用函數(shù)指針調(diào)用函數(shù)。

這里有個(gè)地方需要注意部逮,就是__blockStudy_block_impl_0被強(qiáng)轉(zhuǎn)換為了__block_impl娜汁,這在C語(yǔ)言是可行的,因?yàn)?code>__block_impl位于__blockStudy_block_impl_0的最頂部甥啄,就相當(dāng)于__block_impl的變量直接排列在__blockStudy_block_impl_0的頂部存炮。

再看__block_impl的定義:

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

它有一個(gè)isa變量炬搭,而且這里它被賦值為&_NSConcreteStackBlock蜈漓,結(jié)合前面我們定義Object-C中的棧對(duì)象穆桂,我們可以知道Block其實(shí)就是一個(gè)棧對(duì)象,它的類(lèi)就是_NSConcreteStackBlock融虽。

自由變量的截取

一般我們創(chuàng)建的局部變量都是自由變量享完,因?yàn)橄到y(tǒng)會(huì)自動(dòng)在前面加上auto

int age = 10;

auto int age = 10;
int global_val = 1;
static int static_global_val = 1;

int blockStudy() {
    int var = 10; // auto 變量
    static int static_val = 2; // static 變量
    void (^blk)(void) = ^{
        NSLog(@"%@ %@ %@ %@", global_val, static_global_val, var, static_val);
    };
    blk();
    return 0;
}

同樣把上面的截取局部變量的代碼轉(zhuǎn)為C語(yǔ)言源碼:

int global_val = 1;
static int static_global_val = 1;


struct __blockStudy_block_impl_0 {
    struct __block_impl impl;
    struct __blockStudy_block_desc_0* Desc;
    int var;
    int *static_val;
    __blockStudy_block_impl_0(void *fp, struct __blockStudy_block_desc_0 *desc, int _var, int *_static_val, int flags=0) : var(_var), static_val(_static_val) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

static void __blockStudy_block_func_0(struct __blockStudy_block_impl_0 *__cself) {
    int var = __cself->var; // bound by copy
    int *static_val = __cself->static_val; // bound by copy
    
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_j2__6smntvs6ng1ww18kf8713tc0000gn_T_BlockStudy_7f4b92_mi_0, global_val, static_global_val, var, (*static_val));
}

static struct __blockStudy_block_desc_0 {
    size_t reserved;
    size_t Block_size;
} __blockStudy_block_desc_0_DATA = { 0, sizeof(struct __blockStudy_block_impl_0)};

int blockStudy() {
    int var = 10;
    static int static_val = 2;
    void (*blk)(void) = ((void (*)())&__blockStudy_block_impl_0((void *)__blockStudy_block_func_0, &__blockStudy_block_desc_0_DATA, var, &static_val));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

可以看到我們?cè)贐lock里面使用到的局部變量被作為成員變量追加到了__blockStudy_block_impl_0結(jié)構(gòu)體中(值傳遞),靜態(tài)局部變量也是(指針傳遞)有额,而全局變量和未用到的局部變量則不會(huì)般又。

結(jié)論:我們創(chuàng)建一個(gè)block時(shí),實(shí)際上是創(chuàng)建了一個(gè)結(jié)構(gòu)體巍佑,這個(gè)結(jié)構(gòu)體保存了block的執(zhí)行函數(shù)的地址以及這個(gè)函數(shù)里面使用到的局部變量(自由變量)茴迁,一般情況下這些局部變量是被拷貝進(jìn)去的。

結(jié)合前面Block的源碼可以知道Block的內(nèi)存布局如下:

再看block執(zhí)行函數(shù)的實(shí)現(xiàn):

static void __blockStudy_block_func_0(struct __blockStudy_block_impl_0 *__cself) {
    const char *fmt = __cself->fmt; // bound by copy
    int var = __cself->var; // bound by copy
    int *static_val = __cself->static_val; // bound by copy

    const char *a = fmt;
    int b = var;
    int c = global_val;
    int d = static_global_val;
    int e = (*static_val);
}

自動(dòng)生成的注釋已經(jīng)說(shuō)明了: bound by copy萤衰,block對(duì)它引用的局部變量做了只讀拷貝堕义,也就是說(shuō)block引用的是局部變量的副本。所以在執(zhí)行block語(yǔ)法后脆栋,即使改寫(xiě)block中使用的局部變量的值也不會(huì)影響block執(zhí)行時(shí)局部變量的值倦卖。這也就是為什么在block里面修改局部變量的值時(shí)要額外加上__block修飾符。

__block

我們都知道要在Block內(nèi)修改外面的局部變量就得給局部變量加上__block修飾符椿争,不然編譯器會(huì)報(bào)錯(cuò)怕膛。接下來(lái)看看__block的本質(zhì)。

先定義一個(gè)簡(jiǎn)單的__block變量:

void blockStudy() {
    __block int myNumber = 10;
}

轉(zhuǎn)換為源碼:

struct __Block_byref_myNumber_0 {
  void *__isa;
__Block_byref_myNumber_0 *__forwarding;
 int __flags;
 int __size;
 int myNumber;
};

void blockStudy() {
    __attribute__((__blocks__(byref))) __Block_byref_myNumber_0 myNumber = {
    (void*)0,(__Block_byref_myNumber_0 *)&myNumber, 
    0, 
    sizeof(__Block_byref_myNumber_0), 
    10};
}

可以看到__block變量實(shí)際是生成了一個(gè)結(jié)構(gòu)體的實(shí)例秦踪,而這個(gè)實(shí)例最后的一個(gè)成員變量才是我們定義自由變量褐捻,而且還多了一個(gè)特別的__forwarding變量。再看在block里面引用這個(gè)__block變量的情況:

int blockStudy() {
    __block int number = 10;
    
    void (^blk)(void) = ^{
        number = 5;
    };
    blk();
    return 0;
}
struct __Block_byref_myNumber_0 {
  void *__isa;
__Block_byref_myNumber_0 *__forwarding;
 int __flags;
 int __size;
 int myNumber;
};

struct __blockStudy_block_impl_0 {
  struct __block_impl impl;
  struct __blockStudy_block_desc_0* Desc;
  __Block_byref_myNumber_0 *myNumber; // by ref
    
  __blockStudy_block_impl_0(void *fp, struct __blockStudy_block_desc_0 *desc, __Block_byref_myNumber_0 *_myNumber, int flags=0) : myNumber(_myNumber->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __blockStudy_block_func_0(struct __blockStudy_block_impl_0 *__cself) {
    __Block_byref_myNumber_0 *myNumber = __cself->myNumber; // bound by ref
    (myNumber->__forwarding->myNumber) = 5;
}

static void __blockStudy_block_copy_0(struct __blockStudy_block_impl_0*dst, struct __blockStudy_block_impl_0*src) {
    _Block_object_assign((void*)&dst->myNumber, (void*)src->myNumber, 8/*BLOCK_FIELD_IS_BYREF*/);
}

static void __blockStudy_block_dispose_0(struct __blockStudy_block_impl_0*src) {
    _Block_object_dispose((void*)src->myNumber, 8/*BLOCK_FIELD_IS_BYREF*/);
}

static struct __blockStudy_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __blockStudy_block_impl_0*, struct __blockStudy_block_impl_0*);
  void (*dispose)(struct __blockStudy_block_impl_0*);
} __blockStudy_block_desc_0_DATA = { 0, sizeof(struct __blockStudy_block_impl_0), __blockStudy_block_copy_0, __blockStudy_block_dispose_0};

int blockStudy() {
    __attribute__((__blocks__(byref))) __Block_byref_myNumber_0 myNumber = {
        (void*)0,
        (__Block_byref_myNumber_0 *)&myNumber,
        0,
        sizeof(__Block_byref_myNumber_0),
        10
    };

    void (*blk)(void) = ((void (*)())&__blockStudy_block_impl_0((void *)__blockStudy_block_func_0, &__blockStudy_block_desc_0_DATA, (__Block_byref_myNumber_0 *)&myNumber, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

同樣的__block變量被block捕獲了洋侨,但這次是引用傳遞:

__Block_byref_myNumber_0 *myNumber; // by ref

給__block變量賦值的執(zhí)行函數(shù)被轉(zhuǎn)換為:

static void __blockStudy_block_func_0(struct __blockStudy_block_impl_0 *__cself) {
    __Block_byref_myNumber_0 *myNumber = __cself->myNumber; // bound by ref
    (myNumber->__forwarding->myNumber) = 5;
}

這里的注釋變?yōu)榱?code>bound by ref舍扰,說(shuō)明是引用傳遞。而且這里是通過(guò)myNumber的__forwarding去修改值的希坚,而不是直接通過(guò)myNumber->myNumber來(lái)修改边苹,這里就涉及到block的存儲(chǔ)域問(wèn)題了。

Block存儲(chǔ)域

我們前面創(chuàng)建的Block都是存在棧上面的裁僧,而且它的類(lèi)是_NSConcreteStackBlock个束。但除此之外還有另外兩種Block:_NSConcreteGlobalBlock_NSConcreteMallocBlock聊疲。

_NSConcreteGlobalBlock和全局變量一樣他是存在程序的數(shù)據(jù)區(qū)域的茬底,這種Block不捕獲任何自由變量,相當(dāng)于代碼片段获洲。

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

int blockStudy() {
    return 0;
}

// C語(yǔ)言源碼
struct __blk_block_impl_0 {
  struct __block_impl impl;
  struct __blk_block_desc_0* Desc;
  __blk_block_impl_0(void *fp, struct __blk_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteGlobalBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

_NSConcreteMallocBlock是保存在堆上的阱表。

為什么需要_NSConcreteMallocBlock呢?因?yàn)闂I系腂lock會(huì)隨著其所屬的變量作用域結(jié)束而被廢棄。這時(shí)候就需要把棧上的Block拷貝到堆上面最爬,使得Block語(yǔ)法記述的變量的作用域結(jié)束后堆上的Block依舊可以繼續(xù)存在涉馁。這也是為什么要把block的屬性聲明為copy的緣故。

但是在ARC之后block會(huì)被自動(dòng)復(fù)制到堆上爱致,即使你聲明的修飾符是strong,實(shí)際上效果是與聲明為copy一樣的烤送,也就不強(qiáng)求聲明為copy了,但官方仍然建議我們使用copy以顯示相關(guān)拷貝行為糠悯。

大部分的情況編譯器會(huì)自動(dòng)幫我們把block復(fù)制到堆上面帮坚,比如以下代碼:

typedef int (^blk_t)(int);

blk_t func(int rate) {
    return ^(int count)(return rate * count);
}

// 轉(zhuǎn)換后的源碼
blk_t func(int rate) {
    blk_t tmp = &__func_block_impl_0(
        __func_block_func_0, &__func_block_desc_0_DATA, rate);

    tmp = objc_retainBlock(tmp);

    return objc_autoreleaseReturnValue(tmp);
}

再比如我們平常調(diào)用接口的方法:

- (void)loadData {
    __block id myData;
    [self getMyData:^(id result, NSError *error) {
        myData = result;
    }];
}

- (void)getMyData:(void(^)(id result, NSError *error))completionBlock {
    if (completionBlock) { // __NSStackBlock__
    }
    [@[@"0"] enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        completionBlock(nil, nil); // __NSStackBlock__
    }];
    dispatch_sync(dispatch_queue_create(nil, DISPATCH_QUEUE_SERIAL), ^{
        completionBlock(nil, nil); // __NSStackBlock__
    });
    dispatch_async(dispatch_get_main_queue(), ^{
        completionBlock(nil, nil); // __NSMallocBlock__
    });
}

當(dāng)執(zhí)行到dispatch_async時(shí)block就被復(fù)制到堆上面了,如果還在棧上面互艾,那getMyData函數(shù)執(zhí)行完成后這個(gè)block就會(huì)被釋放试和,等異步回來(lái)執(zhí)行時(shí)就會(huì)出錯(cuò)。

那block被拷貝到堆上后纫普,__block變量會(huì)發(fā)生什么呢灰署?

當(dāng)Block被復(fù)制到堆上面后,它所使用的__block變量也會(huì)被復(fù)制到堆上面局嘁。除此之外還有一個(gè)__forwarding變量需要注意溉箕。一開(kāi)始__block變量在棧上面創(chuàng)建時(shí),它的__forwarding是指向它自己的悦昵,當(dāng)它被復(fù)制到堆上面后肴茄,原本的棧上面的__block變量__forwarding會(huì)指向被復(fù)制出來(lái)的堆上面的__block變量,而被復(fù)制到堆上面的__block變量__forwarding則會(huì)指向它自己但指。

void blockStudy()
{
    dispatch_block_t myBlock;
    {
        __block int a = 0;
        myBlock = ^() {
            a = 1;
        };
        a = 2;
    }
}

把上面這段代碼轉(zhuǎn)成C語(yǔ)言源碼后:

void blockStudy()
{
    dispatch_block_t myBlock;
    {
        __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 0};
        myBlock = ((void (*)())&__blockStudy_block_impl_0((void *)__blockStudy_block_func_0, &__blockStudy_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
        (a.__forwarding->a) = 2;
    }
}

可以看到原本的a = 2;被轉(zhuǎn)成了(a.__forwarding->a) = 2;寡痰,而a其實(shí)是個(gè)結(jié)構(gòu)體__Block_byref_a_0。創(chuàng)建__Block_byref_a_0時(shí)的第二個(gè)參數(shù)就是__forwarding棋凳,這里傳進(jìn)去的值是(__Block_byref_a_0 *)&a拦坠,也就是它自己本身。

小結(jié):當(dāng)用__block修飾一個(gè)局部變量時(shí)剩岳,實(shí)際上是在棧上創(chuàng)建了一個(gè)包含該變量的結(jié)構(gòu)體A1贞滨,該結(jié)構(gòu)體包含一個(gè)變量__forwarding,此時(shí)棧上的A1的__forwarding指向A1本身拍棕。block通過(guò)引用傳遞捕獲__block變量晓铆,當(dāng)block被拷貝到堆上時(shí),__block變量也被拷貝到堆上面有了A2绰播,這時(shí)就會(huì)同時(shí)存在A1和A2骄噪,讀取和修改就會(huì)有同步問(wèn)題,這時(shí)__forwarding變量就起作用了蠢箩。復(fù)制到堆上面后链蕊,A1和A2的__forwarding變量都會(huì)指向A2事甜,外部統(tǒng)一通過(guò)__forwarding變量來(lái)讀取和修改變量就能保持一致了。

block捕獲對(duì)象類(lèi)型的自由變量

先看以下ARC環(huán)境下的簡(jiǎn)單例子:

當(dāng)超出實(shí)例的作用域后它就會(huì)被銷(xiāo)毀滔韵,如果我們用一個(gè)block來(lái)對(duì)這個(gè)變量進(jìn)行引用會(huì)發(fā)生什么呢讳侨?

這個(gè)時(shí)候myObject變量不會(huì)被銷(xiāo)毀,而且這個(gè)時(shí)候block已經(jīng)被復(fù)制到堆上了:

我們可以理解為堆上的block對(duì)myObject進(jìn)行了引用奏属,所以myObject不會(huì)被銷(xiāo)毀。

ARC環(huán)境下潮峦,一旦Block賦值就會(huì)觸發(fā)copy囱皿,__block就會(huì)copy到堆上,Block也是__NSMallocBlock忱嘹。

把ARC改為MRC:

myObject又被銷(xiāo)毀了嘱腥,而且block沒(méi)有復(fù)制到堆上:

我們手動(dòng)進(jìn)行copy:

myObject又被持有了。

結(jié)論:棧上的block不會(huì)持有對(duì)象類(lèi)型的自由變量拘悦,而堆上的block則會(huì)齿兔。

返回ARC再對(duì)改代碼進(jìn)行一些修改:

用__weak來(lái)修飾myObject,可以看到myObject在走到NSLog時(shí)會(huì)被回收础米,所以block內(nèi)部并沒(méi)有強(qiáng)引用myObject分苇。

查看源碼:

struct __blockStudy_block_impl_0 {
  struct __block_impl impl;
  struct __blockStudy_block_desc_0* Desc;
  MyObject *__weak weakMyObject;
    
  __blockStudy_block_impl_0(void *fp, struct __blockStudy_block_desc_0 *desc, MyObject *__weak _weakMyObject, int flags=0) : weakMyObject(_weakMyObject) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

block內(nèi)部同樣用__weak來(lái)修飾。

為什么棧上的block不會(huì)持有對(duì)象型的自由變量呢屁桑?

回到前面的代碼:

void blockStudy() {
    dispatch_block_t myBlock;
    {
        MyObject *myObject = [MyObject new];
        myBlock = ^() {
            NSLog(@"%@", NSStringFromClass(myObject.class));
        };
    }
    NSLog(@"End");
}

轉(zhuǎn)為源碼:

struct __blockStudy_block_impl_0 {
    struct __block_impl impl;
    struct __blockStudy_block_desc_0* Desc;
    MyObject *myObject;
    
    __blockStudy_block_impl_0(void *fp, struct __blockStudy_block_desc_0 *desc, MyObject *_myObject, int flags=0) : myObject(_myObject) {
        impl.isa = &_NSConcreteStackBlock; // 棧上面的Block
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};
static void __blockStudy_block_func_0(struct __blockStudy_block_impl_0 *__cself) {
    MyObject *myObject = __cself->myObject; // bound by copy
    
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_j2__6smntvs6ng1ww18kf8713tc0000gn_T_BlockStudy_f6b706_mi_0, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)myObject, sel_registerName("class"))));
}
static void __blockStudy_block_copy_0(struct __blockStudy_block_impl_0*dst, struct __blockStudy_block_impl_0*src) {
    _Block_object_assign((void*)&dst->myObject, (void*)src->myObject, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static void __blockStudy_block_dispose_0(struct __blockStudy_block_impl_0*src) {
    _Block_object_dispose((void*)src->myObject, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static struct __blockStudy_block_desc_0 {
    size_t reserved;
    size_t Block_size;
    void (*copy)(struct __blockStudy_block_impl_0*, struct __blockStudy_block_impl_0*);
    void (*dispose)(struct __blockStudy_block_impl_0*);
} __blockStudy_block_desc_0_DATA = { 0, sizeof(struct __blockStudy_block_impl_0), __blockStudy_block_copy_0, __blockStudy_block_dispose_0};

void blockStudy() {
    dispatch_block_t myBlock;
    {
        MyObject *myObject = ((MyObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MyObject"), sel_registerName("new"));
        myBlock = ((void (*)())&__blockStudy_block_impl_0((void *)__blockStudy_block_func_0, &__blockStudy_block_desc_0_DATA, myObject, 570425344));
    }
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_j2__6smntvs6ng1ww18kf8713tc0000gn_T_BlockStudy_f6b706_mi_1);
}

創(chuàng)建block的時(shí)候把myObject傳了進(jìn)去

myBlock = ((void (*)())&__blockStudy_block_impl_0((void *)__blockStudy_block_func_0, &__blockStudy_block_desc_0_DATA, myObject, 570425344));

保存在了__blockStudy_block_impl_0中:

struct __blockStudy_block_impl_0 {
    struct __block_impl impl;
    struct __blockStudy_block_desc_0* Desc;
    MyObject *myObject;
    
    __blockStudy_block_impl_0(void *fp, struct __blockStudy_block_desc_0 *desc, MyObject *_myObject, int flags=0) : myObject(_myObject) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

這時(shí)候并沒(méi)有對(duì)myObject進(jìn)行持有医寿,也就是沒(méi)有增加它的引用計(jì)數(shù)。只有當(dāng)block被復(fù)制到堆上面時(shí)蘑斧,這個(gè)時(shí)候__blockStudy_block_copy_0函數(shù)會(huì)被調(diào)用靖秩,才會(huì)通過(guò)_Block_object_assign會(huì)根據(jù)自由變量的修飾符(__strong__weak竖瘾,__unretained)進(jìn)行相應(yīng)的操作沟突,形成強(qiáng)引用(retain)或者弱引用。__blockStudy_block_dispose_0則在block被銷(xiāo)毀時(shí)進(jìn)行調(diào)用捕传。這就是捕獲對(duì)象型自由變量時(shí)__blockStudy_block_desc_0會(huì)多出兩個(gè)函數(shù)指針的原因了惠拭。

static void __blockStudy_block_copy_0(struct __blockStudy_block_impl_0*dst, struct __blockStudy_block_impl_0*src) {
    _Block_object_assign((void*)&dst->myObject, (void*)src->myObject, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static void __blockStudy_block_dispose_0(struct __blockStudy_block_impl_0*src) {
    _Block_object_dispose((void*)src->myObject, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static struct __blockStudy_block_desc_0 {
    size_t reserved;
    size_t Block_size;
    
    void (*copy)(struct __blockStudy_block_impl_0*, struct __blockStudy_block_impl_0*);
    void (*dispose)(struct __blockStudy_block_impl_0*);
} __blockStudy_block_desc_0_DATA = { 0, sizeof(struct __blockStudy_block_impl_0), __blockStudy_block_copy_0, __blockStudy_block_dispose_0};

事實(shí)上捕獲__block變量時(shí)也會(huì)有這兩個(gè)函數(shù)指針,只是調(diào)用_Block_object_assign__blockStudy_block_dispose_0函數(shù)時(shí)第三個(gè)參數(shù)有所不同庸论。它們?cè)赺_block變量被復(fù)制到堆上面時(shí)會(huì)被調(diào)用求橄。

static void __blockStudy_block_copy_0(struct __blockStudy_block_impl_0*dst, struct __blockStudy_block_impl_0*src) {
    _Block_object_assign((void*)&dst->myNumber, (void*)src->myNumber, 8/*BLOCK_FIELD_IS_BYREF*/);
}

static void __blockStudy_block_dispose_0(struct __blockStudy_block_impl_0*src) {
    _Block_object_dispose((void*)src->myNumber, 8/*BLOCK_FIELD_IS_BYREF*/);
}

而__block變量在ARC和MRC下是不一樣的。

  • MRC 環(huán)境下葡公,block 截獲外部用 __block 修飾的變量罐农,不會(huì)增加對(duì)象的引用計(jì)數(shù);
  • ARC 環(huán)境下催什,block 截獲外部用 __block 修飾的變量涵亏,會(huì)增加對(duì)象的引用計(jì)數(shù)。

主要原因是_Block_object_assign方法,它根據(jù)第三個(gè)參數(shù)來(lái)決定是否需要增加對(duì)象的引用計(jì)數(shù)气筋。所以MRC下可以利用 __block來(lái)打破循環(huán)引用拆内,在 ARC 環(huán)境下,則需要用__weak 來(lái)打破循環(huán)引用宠默。

小結(jié):ARC環(huán)境下麸恍,block會(huì)強(qiáng)引用捕獲到的局部變量,是因?yàn)閎lock被拷貝到堆上面時(shí)搀矫,會(huì)執(zhí)行內(nèi)部的copy函數(shù)抹沪,而這個(gè)copy函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_assign,該函數(shù)會(huì)根據(jù)局部變量的修飾符(__strong瓤球,__weak融欧,__unretained)來(lái)對(duì)該變量進(jìn)行強(qiáng)引用或弱引用。棧上的block在拷貝到堆上面之前是不會(huì)強(qiáng)引用局部變量的卦羡。

總結(jié):__block修飾的自由變量噪馏,不管是對(duì)象類(lèi)型的還是非對(duì)象類(lèi)型的,其實(shí)都是定義了一個(gè)結(jié)構(gòu)體绿饵,我們?cè)鞠胍x的變量是被封裝到這個(gè)結(jié)構(gòu)體里面的欠肾,最后使用的都是這個(gè)結(jié)構(gòu)體,通過(guò)__forwarding引用值拟赊。這個(gè)結(jié)構(gòu)體可以被復(fù)制到堆上面董济,所以值可以被修改。當(dāng)被復(fù)制到堆上時(shí)要门,__forwarding變量指向的是__block變量自己虏肾,而棧上的__forwarding也是指向堆上的__block變量,這就保證了讀取和修改的同步欢搜。

如果不用__block修飾封豪,block捕獲非對(duì)象類(lèi)型的自由變量時(shí)只是把它的值拷貝進(jìn)來(lái),自然就無(wú)法修改炒瘟,修改了也不會(huì)影響到外面吹埠。如果是對(duì)象類(lèi)型的,捕獲的也是對(duì)象的指針疮装,同樣是拷貝這個(gè)地址就行了缘琅。但是block可能會(huì)增加對(duì)象類(lèi)型的引用計(jì)數(shù),是因?yàn)閎lock被拷貝到堆上面時(shí)廓推,會(huì)執(zhí)行block的copy函數(shù)刷袍,也就是會(huì)執(zhí)行_Block_object_assign這個(gè)方法,這個(gè)方法會(huì)根據(jù)自由變量的修飾符來(lái)決定對(duì)該變量進(jìn)行強(qiáng)引用或弱引用樊展,這里的引用當(dāng)然是堆上的變量呻纹,和棧上的指針無(wú)關(guān)堆生。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市雷酪,隨后出現(xiàn)的幾起案子淑仆,更是在濱河造成了極大的恐慌,老刑警劉巖哥力,帶你破解...
    沈念sama閱讀 206,482評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蔗怠,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡吩跋,警方通過(guò)查閱死者的電腦和手機(jī)寞射,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)钞澳,“玉大人,你說(shuō)我怎么就攤上這事涨缚≡冢” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,762評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵脓魏,是天一觀的道長(zhǎng)兰吟。 經(jīng)常有香客問(wèn)我,道長(zhǎng)茂翔,這世上最難降的妖魔是什么混蔼? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,273評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮珊燎,結(jié)果婚禮上惭嚣,老公的妹妹穿的比我還像新娘。我一直安慰自己悔政,他們只是感情好晚吞,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評(píng)論 5 373
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著谋国,像睡著了一般槽地。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上芦瘾,一...
    開(kāi)封第一講書(shū)人閱讀 49,046評(píng)論 1 285
  • 那天捌蚊,我揣著相機(jī)與錄音,去河邊找鬼近弟。 笑死缅糟,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的祷愉。 我是一名探鬼主播溺拱,決...
    沈念sama閱讀 38,351評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼逃贝,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了迫摔?” 一聲冷哼從身側(cè)響起沐扳,我...
    開(kāi)封第一講書(shū)人閱讀 36,988評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎句占,沒(méi)想到半個(gè)月后沪摄,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,476評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡纱烘,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評(píng)論 2 324
  • 正文 我和宋清朗相戀三年杨拐,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片擂啥。...
    茶點(diǎn)故事閱讀 38,064評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡哄陶,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出哺壶,到底是詐尸還是另有隱情屋吨,我是刑警寧澤,帶...
    沈念sama閱讀 33,712評(píng)論 4 323
  • 正文 年R本政府宣布山宾,位于F島的核電站至扰,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏资锰。R本人自食惡果不足惜敢课,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望绷杜。 院中可真熱鬧直秆,春花似錦、人聲如沸鞭盟。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,264評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)懊缺。三九已至疫稿,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間鹃两,已是汗流浹背遗座。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,486評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留俊扳,地道東北人途蒋。 一個(gè)月前我還...
    沈念sama閱讀 45,511評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像馋记,于是被迫代替她去往敵國(guó)和親号坡。 傳聞我的和親對(duì)象是個(gè)殘疾皇子懊烤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評(píng)論 2 345