iOS開發進階:block的分析及循環引用的解決方案

iOS開發中block隨處可見,什麼是block呢?block是一個匿名函數,是一個代碼塊,把代碼放在這個代碼塊中,在需要使用的時候進行調用。block會封裝函數以及函數的調用環境:
-封裝函數:是指block會把block內部的參數返回值、執行體封裝成一個函數,並且存儲該函數的內存地址。
-封裝函數的調用環境:是指block會捕獲變量,並把這些變量存儲起來。

一、block的類型和使用

iOS開發中有三種類型的block,他們分別是全局block,堆block、棧block。
全局block(NSGlobalBlock):沒有訪問外界局部普通變量的block就是全局block,存儲在全局區。
堆block(NSMallocBlock):對棧block進行copy操作返回的block就是堆block,存儲在堆區。
棧block(NSStackBlock):訪問了外界普通局部變量的block就是棧block,存儲在棧區。

注意:本章節的代碼均在MRC環境下開啓的調試,MRC下新建的僅訪問局部普通變量的block是棧block,進行copy之後變成堆block。ARC下編譯器會自動將創建的棧block轉換成堆block,如果不做轉換可以在block前添加__weak關鍵字。例如void(^__weak testBlock)() = ^{}這樣定義即可。

測試代碼

- (void)main{
    int sValue = 20;
    static int gValue = 20;
    
    void(^gBlock)(void) = ^(){
        NSLog(@"value=%d", gValue);
    };
    id cgBlock = [gBlock copy];

    
    void(^sBlock)(void) = ^(){
        NSLog(@"value=%d", sValue);
    };
    
    id csBlock = [sBlock copy];
    
    NSLog(@"gBlock=%@,copy之後%@",gBlock,cgBlock);
    NSLog(@"sBlock=%@,copy之後%@",sBlock,csBlock);
}

運行結果:

gBlock=<__NSGlobalBlock__: 0x107728490>,copy之後<__NSGlobalBlock__: 0x107728490>
sBlock=<__NSStackBlock__: 0x7ffee84e1b68>,copy之後<__NSMallocBlock__: 0x600002e20960>

通過如上代碼,我們知道如果僅訪問的局部靜態變量,那麼他仍然是個全部block,全部block copy之後仍然是個全局block(仍指向原來的內存區域)。如果訪問了局部普通變量,那麼他就是棧block,拷貝之後成了一個全新的堆block(指向的內存區域地址發生了變化)。

在使用的過程中,包括三個部分block的聲明(定義)、block的實現,block的調用:
block的聲明的完整寫法:返回值類型 + ( + ^ + 屬性名稱 + ) + 參數列表:

//作爲屬性
@property (nonatomic, copy) int (^calcHashValue)(id value);
//作爲函數參數:
- (void)getHashValue:(int(^)(id value)) calcHashValue


//可以採用別名的方式簡化block的定義:
typeof  int (^CalcHashValue)(id value);
//作爲屬性
@property (nonatomic, copy) CalcHashValue calcHashValue;
//作爲函數參數:
- (void)getHashValue:(CalcHashValue)calcHashValue;

block的實現完整寫法:^ + 返回值類型 + 參數列表 + 函數體 +;

//返回值類型:沒有的話可以不寫,也可以寫`void`;
//參數列表 :用小括號把參數列表包裹起來,參數之間用逗號分隔。沒有參數的話可以寫成`(void)`或者不寫。
self.calcHashValue = ^int(id value){ return 22222222;};

//無參數,無返回值案例:
typedef void(^TodoValue)(void);
@property (nonatomic, copy) TodoValue todoValue;
self.todoValue = ^void(void){};或者self.todoValue = ^{};

block的調用:

//需要注意的是在不能保證block有實現的時候,一定要檢查是否爲空
if(self.calcHashValue) self.calcHashValue(self);

block在使用的時候,會有一些需要注意的問題:
1.block內部不能修改局部變量(直接報錯:Variable is not assignable (missing __block type specifier))。如果需要修改則需要在局部變量的定義時加上__block關鍵字。

__block int localValue = 10;
void(^sBlock)(void) = ^(){
    NSLog(@"localValue=%d", localValue);//localValue=10
    localValue = 20;
};
sBlock();
NSLog(@"localValue=%d", localValue);//localValue=20

2.block實現之後調用之前修改局部變量,block中拿到的仍然是舊值。如果要獲得最新值則需要在局部變量定義時加上__block關鍵字。

//不加block關鍵字
int localValue = 10;
void(^sBlock)(void) = ^(){ 
    NSLog(@"localValue=%d", localValue); //localValue=10
};
localValue = 20;
sBlock();

//加block關鍵字
__block int localValue = 10;
void(^sBlock)(void) = ^(){
    NSLog(@"localValue=%d", localValue);//localValue=20
};
localValue = 20;
sBlock();

這個block關鍵字到底是做了什麼呢,我們通過clang看下block底層是如何實現的。

xcrun -sdk iphoneos clang -arch arm64e -rewrite-objc NXBlock.m

其中原始代碼如下:

@interface NXBlock : NSObject
- (void)t1;
- (void)t2;
@end

@implementation NXBlock

- (void)t1{
    int localValue = 10;
    void(^noneBlock)(void) = ^(){
        NSLog(@"localValue=%d", localValue);
    };
    noneBlock();
}

- (void)t2{
    __block int localValue = 10;
    void(^withBlock)(void) = ^(){
        NSLog(@"localValue=%d", localValue);
    };
    withBlock();
}
@end

生成.cpp文件後我們拷貝出相關的代碼片段:
如下本段是localValue沒有使用__block修飾的:

struct __NXBlock__t1_block_impl_0 {
    struct __block_impl impl;
    struct __NXBlock__t1_block_desc_0* Desc;
    int localValue;
    __NXBlock__t1_block_impl_0(void *fp, struct __NXBlock__t1_block_desc_0 *desc, int _localValue, int flags=0) : localValue(_localValue) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

static void __NXBlock__t1_block_func_0(struct __NXBlock__t1_block_impl_0 *__cself) {
    int localValue = __cself->localValue; // bound by copy
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_fz_f2dd5f4545ggwp26d9pyjfwc0000gn_T_NXBlock_7c795f_mi_0, localValue);
}

static struct __NXBlock__t1_block_desc_0 {
    size_t reserved;
    size_t Block_size;
} __NXBlock__t1_block_desc_0_DATA = { 0, sizeof(struct __NXBlock__t1_block_impl_0)};

static void _I_NXBlock_t1(NXBlock * self, SEL _cmd) {
    int localValue = 10;
    void(*noneBlock)(void) = ((void (*)())&__NXBlock__t1_block_impl_0((void *)__NXBlock__t1_block_func_0, &__NXBlock__t1_block_desc_0_DATA, localValue));
    ((void (*)(__block_impl *))((__block_impl *)noneBlock)->FuncPtr)((__block_impl *)noneBlock);
}

如下本段是localValue使用__block修飾的:

struct __Block_byref_localValue_0 {
    void *__isa;
    __Block_byref_localValue_0 *__forwarding;
    int __flags;
    int __size;
    int localValue;
};

struct __NXBlock__t2_block_impl_0 {
    struct __block_impl impl;
    struct __NXBlock__t2_block_desc_0* Desc;
    __Block_byref_localValue_0 *localValue; // by ref
    __NXBlock__t2_block_impl_0(void *fp, struct __NXBlock__t2_block_desc_0 *desc, __Block_byref_localValue_0 *_localValue, int flags=0) : localValue(_localValue->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

static void __NXBlock__t2_block_func_0(struct __NXBlock__t2_block_impl_0 *__cself) {
    __Block_byref_localValue_0 *localValue = __cself->localValue; // bound by ref
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_fz_f2dd5f4545ggwp26d9pyjfwc0000gn_T_NXBlock_7c795f_mi_1, (localValue->__forwarding->localValue));
}

static void __NXBlock__t2_block_copy_0(struct __NXBlock__t2_block_impl_0*dst, struct __NXBlock__t2_block_impl_0*src) {
    _Block_object_assign((void*)&dst->localValue, (void*)src->localValue, 8/*BLOCK_FIELD_IS_BYREF*/);
}

static void __NXBlock__t2_block_dispose_0(struct __NXBlock__t2_block_impl_0*src) {
    _Block_object_dispose((void*)src->localValue, 8/*BLOCK_FIELD_IS_BYREF*/);
}

static struct __NXBlock__t2_block_desc_0 {
    size_t reserved;
    size_t Block_size;
    void (*copy)(struct __NXBlock__t2_block_impl_0*, struct __NXBlock__t2_block_impl_0*);
    void (*dispose)(struct __NXBlock__t2_block_impl_0*);
} __NXBlock__t2_block_desc_0_DATA = { 0, sizeof(struct __NXBlock__t2_block_impl_0), __NXBlock__t2_block_copy_0, __NXBlock__t2_block_dispose_0};

static void _I_NXBlock_t2(NXBlock * self, SEL _cmd) {
    __attribute__((__blocks__(byref))) __Block_byref_localValue_0 localValue = {(void*)0,(__Block_byref_localValue_0 *)&localValue, 0, sizeof(__Block_byref_localValue_0), 10};
    void(*withBlock)(void) = ((void (*)())&__NXBlock__t2_block_impl_0((void *)__NXBlock__t2_block_func_0, &__NXBlock__t2_block_desc_0_DATA, (__Block_byref_localValue_0 *)&localValue, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)withBlock)->FuncPtr)((__block_impl *)withBlock);
}

我們通過一個表格來對比兩者的異同:

對比 無修飾 __block修飾 static修飾 __weak修飾
定義 int localValue = 10; __block int localValue = 10; static int localValue = 10; __weak id weak self = self;
完整結構與構造函數 struct __NXBlock__t1_block_impl_0{}:
1.包括impl、Desc和localValue三個變量。
2.其中localValue這個外部局部變量在內部的定義仍然是int localValue。
3.構造函數包括調用的函數指針+描述+int型參數_localValue+flags,分別賦值給impl.FuncPtr、Desc、localValue和impl.Flags。
struct __NXBlock__t2_block_impl_0{}:
1.包括impl、Desc和localValue三個變量。
2.其中localValue這個外部局部變量在內部的定義爲__Block_byref_localValue_0 *localValue;是一個結構體指針;
3.構造函數包括調用的函數指針+描述+__Block_byref_localValue_0型參數_localValue+flags,分別賦值給impl.FuncPtr、Desc、localValue和impl.Flags。
struct __NXBlock__t3_block_impl_0{};
1.包括impl、Desc和localValue三個變量。
2.其中localValue這個外部變量在內部的定義是int localValue。
3.構造函數包括調用的函數指針+描述+int
類型參數_localValue+flags,分別賦值給impl.FuncPtr、Desc、localValue和impl.Flags。
struct __NXBlock__t4_block_impl_0{};
1.包括impl、Desc和localValue三個變量。
2.其中localValue這個外部變量在內部的定義是__weak id localValue。
3.構造函數包括調用的函數指針+描述+weak id類型參數_localValue+flags,分別賦值給impl.FuncPtr、Desc、localValue和impl.Flags
外部變量在內部的定義 struct->localValue訪問原始數據; 封裝在struct __Block_byref_localValue_0{}結構體中
1. 整形int類型變量localValue存儲了外部傳入的localValue值.
2.結構體指針類型__Block_byref_localValue_0 *的變量指向自己(localValue(_localValue->__forwarding)說明了這一點)。
3.struct->localValue->__forwarding->localValue即是原始localValue;
(*struct.localValue)訪問原始數據。 struct.localValue訪問原始數據
block函數指針 __NXBlock__t1_block_func_0(...)即是外部調用block的執行函數。
1.函數包括一個完整block的參數__cself。
2.通過__cself->localValue獲得外界的值。
__NXBlock__t2_block_func_0(...)即是外部調用block的執行函數。
1.函數包括一個完整block的參數__cself。
2.通過__cself->localValue->__forwarding->localValue獲得外界的值。
__NXBlock__t3_block_func_0(...)即是外部調用block的執行函數。
1.函數包括完整block的參數__cself。
2.通過(*__cself.localvalue)獲得外界的值。
__NXBlock__t4_block_func_0(...)即是外部調用block的執行函數。
1.函數包括完整block的參數__cself。
2.通過__cself.localvalue獲得外界的值
描述結構 __NXBlock__t1_block_desc_0:
1:保留字段reserved默認值爲0;
2.Block_size記錄完成結構體的大小;
__NXBlock__t2_block_desc_0;
1.保留字段reserved默認值爲0。
2.Block_size記錄完成結構體的大小;
3.copy函數指針;
4.dispose函數指針;
__NXBlock__t3_block_desc_0:
1:保留字段reserved默認值爲0;
2.Block_size記錄完成結構體的大小;
__NXBlock__t4_block_desc_0:
1:保留字段reserved默認值爲0;
2.Block_size記錄完成結構體的大小;
copy函數 / __NXBlock__t2_block_copy_0; / /
dispose函數 / __NXBlock__t2_block_dispose_0; / /

__Block_byref_localValue_0構造過程中傳給__forwarding的是__Block_byref_localValue_0類型,是變量自身,也就是byref.__forwarding指向的是自己,這個是block在棧區的情況。當棧區block被複制到堆區的時候,byref結構體也會被複制一份,並且複製出來的copy. __forwarding = copy;並且byref. __forwarding = copy了。這樣以來,無論是通過原有的訪問原有的block還是拷貝的block,那麼通過block. byref. __forwarding獲取到的都是堆內存中的那一份。
我們可以通過一段代碼驗證這個結果:

__block int localValue = 10;
NSString *(^stackBlock)(void) = ^NSString *(){
    return [NSString stringWithFormat:@"&localValue=%p", &localValue];
};
NSLog(@"拷貝前:%@-%@", stackBlock, stackBlock());
NSString *(^mallocBlock)(void) = [stackBlock copy];
NSLog(@"拷貝後:%@-%@", stackBlock, stackBlock());
NSLog(@"拷貝後:%@-%@", mallocBlock, mallocBlock());

打印結果

拷貝前:<__NSStackBlock__: 0x7ffee17f6b60>-&localValue=0x7ffee17f6ba8
拷貝後:<__NSStackBlock__: 0x7ffee17f6b60>-&localValue=0x600000472878
拷貝後:<__NSMallocBlock__: 0x600000a5d200>-&localValue=0x600000472878

可以看到拷貝後,新生成了一個堆block對象並且block捕獲的localValue的地址發生了變化,由0x7ffee17f6ba8變成了0x600000472878,而且原有的棧block捕獲的localValue的地址也由0x7ffee17f6ba8變成了0x600000472878,跟堆block保持一致。

細節邏輯可以在源碼中看到:

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

簡單總結:
1.沒有使用__block關鍵字修飾的localValue是一個簡單int類型,傳入內部也是的int類型的localValue變量.
2.使用__block關鍵字修飾的localValue會生成爲__Block_byref的結構體,傳入內部也是__Block_byref類型的localValue結構體指針。
3.棧上的block進行copy:block本身會在堆上開闢內存;__Block_byref在堆上新開闢內存;捕獲的外部變量也會在堆上新開闢內存。原有的棧block的__Block_byref的__forwarding會指向堆block的__Block_byref。

二、block中的循環引用

如果對象A強持有對象B:
-初始化完成後A的引用計數爲1,B的引用計數爲1。B的引用計數會因爲A的持有而+1變成2。
-在AB出作用域後系統會給AB分別發送一個release消息,A的引用計數-1變成0;B的引用計數-1變成1。
-A引用計數變成0調用dealloc方法進行銷燬,A調用dealloc方法時也會給B發送一個release消息,B的引用計數-1變成0,則B會調用dealloc方法進行銷燬。

如果對象A強持有對象B,B也強持有A:
-初始化完成後AB引用計數爲1,相互賦值後兩者的引用計數都變成2。
-AB出作用域後系統分別給AB發送release消息。A的引用計數變成1,B的引用計數變成1。
-兩者維持引用計數爲1而得不到釋放,造成內存泄漏。

而block中的循環引用出現的通常是由於self強制有block,而block又強持有self造成的循環引用。如果能通過weak斷開引用環那麼問題就解決了。

2.1.使用weak(/strong)修飾斷開環的方式避免循環引用:

準備一段代碼:

@interface NXTester: NSObject
@property (nonatomic, copy) void(^work)(void);
@property (nonatomic, copy) void(^test)(void);
@end

@implementation NXTester
- (void)dealloc{
    NSLog(@"NXTester-dealloc");
}
@end

案例1:如下代碼會造成循環引用,tester持有work,work持有tester,形成環構成循環引用。解決方案看案例2.

NXTester *tester = [[NXTester alloc] init];
tester.work = ^{
   tester;
};
tester.work();

案例2:如下代碼不會造成循環引用,tester持有work,work持有weakself, weakself弱持有tester。弱引用斷開了這個閉環,不會循環引用。

NXTester *tester = [[NXTester alloc] init];
__weak NXTester *weakself = tester;
tester.work = ^{
   weakself;
};
tester.work();

案例3:如下代碼會造成循環引用,註釋掉的部分也會循環引用。因爲strongself強持有test,test又強持有strongself,形成環構成星環引用。

NXTester *tester = [[NXTester alloc] init];
__weak NXTester *weakself = tester;
tester.work = ^{
   //weakself.test = ^{
   //   weakself;
   //};
   //weakself.test();
  //或者
   __strong NXTester *strongself = weakself;
   strongself.test = ^{
      strongself;
   };
   strongself.test();
};
tester.work();

案例4:如下代碼會造成循環引用。通過weak斷開了引用環。

NXTester *tester = [[NXTester alloc] init];
__weak NXTester *weakself = tester;
tester.work = ^{
   __strong NXTester *strongself = weakself;
   __weak NXTester *weakweakself = strongself;
   strongself.test = ^{
      weakweakself;
   };
   strongself.test();
};
tester.work();

如上我們看到第一種解決循環引用的方式是weak-stong修飾。block內部需要訪問的變量用在block外先用weak修飾,內部使用時在用strong修飾。

2.2、使用參數傳遞來解決循環引用的問題:

以上案例中,我們需要在block內部捕獲外部的變量。我們也可以通過參數傳遞的方法來解決。

@interface NXTester: NSObject
@property (nonatomic, copy) void(^work)(NXTester *value);
@end

@implementation NXTester
- (void)dealloc{
    NSLog(@"NXTester-dealloc");
}
@end

使用:

NXTester *tester = [[NXTester alloc] init];
tester.work = ^(NXTester *value){
    value;
};
tester.work(tester);

2.3.使用完畢後將對象設置爲nil

如果一個通過block執行任務後不再需要保留了,那麼可以在block中將對象設置爲nil,或在不使用之後把block或者對象設置爲nil都可以解決問題。這種方案不推薦。

注意事項:
有些場景下不會活着不一定會造成循環引用,關鍵看有沒有無法斷開的引用環。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章