iOS 題目詳解 部分三

主要講解Block 內(nèi)部使用strongSelf的理由和用法

iOS 題目詳解 部分一
iOS 題目詳解 部分二
iOS 題目詳解 部分三

iOS 題目簡述 部分一


Block 內(nèi)部 self 的正確用法

問題背景:

當(dāng)前Controller有一個 Block屬性

@interface ViewController2 ()
@property (nonatomic, strong) NSMutableArray *nameArr;
@property (nonatomic, copy) Block block;
@end

而在 self.block中要訪問當(dāng)前 Controller也就是self;
Block沒有強(qiáng)硬用的問題背景, 分析思路類似, 不再探討;


我們?nèi)粘i_發(fā)中最常用的就是Block內(nèi)部使用weakSelf可以防止循環(huán)引用, 但是這樣會有個問題, 如果Block內(nèi)部有延時(shí)任務(wù)執(zhí)行時(shí)這樣就不滿足需求了, 因?yàn)閳?zhí)行延時(shí)任務(wù)時(shí)self已經(jīng)被釋放;
外部使用weak后內(nèi)部使用strongSelf可以解決這個問題;

__weak typeof(self) weakSelf = self;
self.block =  ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    NSLog(@"%@ ", strongSelf);
  });
};
self.block();

但是為什么這樣寫可以保證不產(chǎn)生循環(huán)引用呢? 就這個問題通過clang看下以下幾種情況其底層代碼研究下;

  • 1.1 首先直接在Block內(nèi)部使用self會造成循環(huán)引用, 這點(diǎn)毋庸置疑;
self.block =   ^{
    NSLog(@"%@", self);
};

通過clang后的C++代碼如下:

struct __ViewController2__viewDidLoad_block_impl_1 {
  struct __block_impl impl;
  struct __ViewController2__viewDidLoad_block_desc_1* Desc;
   #Block 內(nèi)部對 self 強(qiáng)硬用
  ViewController2 *const __strong self;
  __ViewController2__viewDidLoad_block_impl_1(void *fp, struct __ViewController2__viewDidLoad_block_desc_1 *desc, ViewController2 *const __strong _self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

由于Block是當(dāng)前 Controller的屬性, 所以selfBlock強(qiáng)引用, 而Block內(nèi)部又對self強(qiáng)引用;

  • 1.2 在Block內(nèi)部中使用weakSelf有效的解決循環(huán)引用問題;

- (void)viewDidLoad {
    [super viewDidLoad];
#weak 修飾 self 不會造成循環(huán)引用
    __weak typeof(self) weakSelf = self;
    self.block =   ^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [weakSelf delayTask];
        });
    };
    self.block();
}
#延時(shí)任務(wù)
- (void)delayTask {
    NSLog(@"%s", __func__);
}
#副本 Block
struct __ViewController2__viewDidLoad_block_impl_1 {
  struct __block_impl impl;
  struct __ViewController2__viewDidLoad_block_desc_1* Desc;
  #Block 對 Controller 弱引用
  ViewController2 *const __weak weakSelf;
  __ViewController2__viewDidLoad_block_impl_1(void *fp, struct __ViewController2__viewDidLoad_block_desc_1 *desc, ViewController2 *const __weak _weakSelf, int flags=0) : weakSelf(_weakSelf) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
#原始 Block
struct __ViewController2__viewDidLoad_block_impl_0 {
  struct __block_impl impl;
  struct __ViewController2__viewDidLoad_block_desc_0* Desc;
  ViewController2 *const __weak weakSelf;
  __ViewController2__viewDidLoad_block_impl_0(void *fp, struct __ViewController2__viewDidLoad_block_desc_0 *desc, ViewController2 *const __weak _weakSelf, int flags=0) : weakSelf(_weakSelf) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

這種方式我們都知道不會造成循環(huán)引用,但是造成的問題隨之而來, 如果我們在Block內(nèi)部執(zhí)行了延時(shí)的任務(wù)(目的是為了執(zhí)行任務(wù)時(shí)Controller已經(jīng)被銷毀); 則會發(fā)現(xiàn), 這個延時(shí)任務(wù)并不會被執(zhí)行, 因?yàn)閳?zhí)行[weakSelf delayTask]這句代碼時(shí)controller已經(jīng)被銷毀, 給一個 nil發(fā)送消息, 是不會響應(yīng)的;

  • 1.3 正確用法: 外部使用weak修飾, 內(nèi)部使用strongSelf;
- (void)viewDidLoad {
    [super viewDidLoad];
    __weak typeof(self) weakSelf = self;
    self.block =   ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [strongSelf delayTask];
        });
    };
    self.block();
}
#延時(shí)任務(wù)
- (void)delayTask {
    NSLog(@"%s", __func__);
}

退出當(dāng)前Controller后延時(shí)任務(wù)可以正常執(zhí)行, 而且Controller可以正常釋放;

#延時(shí)任務(wù)被執(zhí)行
2020-09-08 10:27:32.253938+0800 Test[12274:2082073] -[ViewController2 delayTask]
#延時(shí)任務(wù)被執(zhí)行后, Controller正常銷毀, 說明沒有循環(huán)引用
2020-09-08 10:27:32.254415+0800 Test[12274:2082073] -[ViewController2 dealloc]

但是, 為什么這樣寫就可以保證沒有循環(huán)引用呢?首先請了解下Block底層各個函數(shù)的含義
首先看下ViewDidLoad通過clang后轉(zhuǎn)化為如下, 因?yàn)?code>ARC下我們都知道系統(tǒng)會幫我們把Block從棧區(qū)拷貝到堆區(qū), 我們實(shí)際操作的是堆區(qū)的那一份Block副本;

static void _I_ViewController2_viewDidLoad(ViewController2 * self, SEL _cmd) {
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController2"))}, sel_registerName("viewDidLoad"));
    __attribute__((objc_ownership(weak))) typeof(self) weakSelf = self;
    #Block 的實(shí)現(xiàn)
    ((void (*)(id, SEL, Block))(void *)objc_msgSend)((id)self, sel_registerName("setBlock:"), ((void (*)())&__ViewController2__viewDidLoad_block_impl_1((void *)__ViewController2__viewDidLoad_block_func_1, &__ViewController2__viewDidLoad_block_desc_1_DATA, weakSelf, 570425344)));
    #調(diào)用 Block
    ((Block (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("block"))();
}

可以確認(rèn)這個self.block 的底層實(shí)現(xiàn)是__ViewController2__viewDidLoad_block_impl_1, 在Block內(nèi)部調(diào)用的函數(shù)是__ViewController2__viewDidLoad_block_func_1;
首先看下self.block的底層結(jié)構(gòu)實(shí)現(xiàn)

struct __ViewController2__viewDidLoad_block_impl_1 {
  struct __block_impl impl;
  struct __ViewController2__viewDidLoad_block_desc_1* Desc;
  #對 self 弱引用
  ViewController2 *const __weak weakSelf;
  __ViewController2__viewDidLoad_block_impl_1(void *fp, struct __ViewController2__viewDidLoad_block_desc_1 *desc, ViewController2 *const __weak _weakSelf, int flags=0) : weakSelf(_weakSelf) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

其實(shí)到這一步我們已經(jīng)可以斷定知道為什么不會造成循環(huán)引用了, 因?yàn)?code>Controller對self.block是強(qiáng)引用, 而self.block的底層實(shí)現(xiàn)如上, 對Controllerweak修飾的弱引用;
不過我們還是繼續(xù)往下看探究下具體的調(diào)用流程, 內(nèi)部調(diào)用函數(shù)__ViewController2__viewDidLoad_block_func_1的實(shí)現(xiàn)如下;

static void __ViewController2__viewDidLoad_block_func_1(struct __ViewController2__viewDidLoad_block_impl_1 *__cself) {
  ViewController2 *const __weak weakSelf = __cself->weakSelf; // bound by copy
      #調(diào)用原始 Block, 注意副本 Block 只是調(diào)用原始 Block的實(shí)現(xiàn), 并不對其強(qiáng)引用或者持有
        __attribute__((objc_ownership(strong))) typeof(weakSelf) strongSelf = weakSelf;
        dispatch_after(dispatch_time((0ull), (int64_t)(5 * 1000000000ull)), dispatch_get_main_queue(), ((void (*)())&__ViewController2__viewDidLoad_block_impl_0((void *)__ViewController2__viewDidLoad_block_func_0, &__ViewController2__viewDidLoad_block_desc_0_DATA, strongSelf, 570425344)));
    }

原始Block的實(shí)現(xiàn)和調(diào)用函數(shù)封裝邏輯如下;

#原始 Block的底層結(jié)構(gòu)
struct __ViewController2__viewDidLoad_block_impl_0 {
  struct __block_impl impl;
  struct __ViewController2__viewDidLoad_block_desc_0* Desc;
  #對 self 強(qiáng)硬用, 但是沒有關(guān)系, 因?yàn)?self 不對原始 block 強(qiáng)引用;
  ViewController2 *const __strong strongSelf;
  __ViewController2__viewDidLoad_block_impl_0(void *fp, struct __ViewController2__viewDidLoad_block_desc_0 *desc, ViewController2 *const __strong _strongSelf, int flags=0) : strongSelf(_strongSelf) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
#實(shí)際執(zhí)行延時(shí)任務(wù)
static void __ViewController2__viewDidLoad_block_func_0(struct __ViewController2__viewDidLoad_block_impl_0 *__cself) {
  ViewController2 *const __strong strongSelf = __cself->strongSelf; // bound by copy
#調(diào)用延時(shí)任務(wù)
  ((void (*)(id, SEL))(void *)objc_msgSend)((id)strongSelf, sel_registerName("delayTask"));
        }

下面我們來總結(jié)下這個流程; 首先是在 ARC環(huán)境下我們知道系統(tǒng)會對棧區(qū)的原始Block執(zhí)行拷貝操作到堆區(qū), 我們實(shí)際操作的是堆區(qū)那份副本;

  • self強(qiáng)硬用拷貝后的副本Block, 副本Block中對self是弱引用;
  • 副本Block中調(diào)用原始Block;
  • 原始Blockself強(qiáng)引用;
    大致的持有/調(diào)用關(guān)系如圖

至此我們可以確定, 因?yàn)樵?code>Block對self有一個強(qiáng)引用, 肯定會導(dǎo)致self的引用計(jì)數(shù)+1 , 關(guān)于這點(diǎn)引用計(jì)數(shù)+1的驗(yàn)證, 由于驗(yàn)證篇幅較長不再貼出, 大家可以自己驗(yàn)證, 或者移步看下這篇文章

下面探討下釋放的流程: ARC 環(huán)境下為什么可以正常釋放(, MRC下類似):

  • 我們假設(shè)當(dāng) controller即將銷毀時(shí), 也就是退出當(dāng)前界面后, 因?yàn)樵?code>Block對其有個強(qiáng)引用, 所以它不能銷毀, 需要等待延時(shí)任務(wù)執(zhí)行;
  • 執(zhí)行到延時(shí)任務(wù)時(shí),也就是執(zhí)行到副本Block時(shí), 副本Blcok調(diào)用原始Blockfunptr執(zhí)行具體Block內(nèi)部的實(shí)現(xiàn)邏輯;
  • 我們知道原始Block處于棧區(qū), 棧區(qū)的內(nèi)存管理是系統(tǒng)進(jìn)行的, 所以延時(shí)任務(wù)執(zhí)行完畢, 原始Block自動銷毀, 同時(shí)對強(qiáng)硬用的self釋放(引用計(jì)數(shù)-1);
  • 當(dāng)self的引用計(jì)數(shù)為0時(shí), 就可以被銷毀了, 因?yàn)?code>self對副本Block有強(qiáng)引用, 所以self釋放的同時(shí)會對堆區(qū)的副本Block銷毀;
  • 至此, 不論是Controller(self), 原始Block, 副本Block都可以正常的釋放; 而且可以正常的在Block內(nèi)部執(zhí)行延時(shí)任務(wù);

補(bǔ)充部分

1. 便捷宏定義使用waekfSelf 和 stongSelf;

每次使用 Block都寫一遍weakstrong的定義太麻煩, 將其定義成宏, 方便快捷;

/**
弱引用/強(qiáng)引用 宏定義
示例:
@weakify(self)
[self block^{
    @strongify(self)
    if (!self) return;
    ...
}];
*/
#ifndef weakify
    #if DEBUG
       #if __has_feature(objc_arc)
            #define weakify(object) autoreleasepool{} __weak __typeof__(object) weak##_##object = object;
       #else
           #define weakify(object) autoreleasepool{} __block __typeof__(object) block##_##object = object;
       #endif
   #else
       #if __has_feature(objc_arc)
           #define weakify(object) try{} @finally{} {} __weak __typeof__(object) weak##_##object = object;
       #else
           #define weakify(object) try{} @finally{} {} __block __typeof__(object) block##_##object = object;
       #endif
   #endif
#endif

#ifndef strongify
    #if DEBUG
        #if __has_feature(objc_arc)
            #define strongify(object) autoreleasepool{} __typeof__(object) object = weak##_##object;
        #else
            #define strongify(object) autoreleasepool{} __typeof__(object) object = block##_##object;
        #endif
    #else
       #if __has_feature(objc_arc)
           #define strongify(object) try{} @finally{} __typeof__(object) object = weak##_##object;
       #else
          #define strongify(object) try{} @finally{} __typeof__(object) object = block##_##object;
       #endif
    #endif
#endif

使用示例:

- (void)test {
    @weakify(self)
    self.block =   ^{
        @strongify(self);
        if (!self) {
            return;
        }
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [self delayTask];
        });
    };
    self.block();
    NSLog(@"Class: %@", [self.block class]);
}
2. 驗(yàn)證下: 關(guān)于ARC下系統(tǒng)自動幫我們把Block棧區(qū)拷貝到了堆區(qū);
  • MRC 環(huán)境下, 如下代碼:
- (void)viewDidLoad {
    [super viewDidLoad];
    Block block =   ^{
        NSLog(@"%@", self);
    };
    block();
    NSLog(@"Class: %@", [block class]);
}

打印結(jié)果如下

#這是一個棧區(qū) Block
2020-09-08 12:21:12.140994+0800 Test[12396:2111280] Class: __NSStackBlock__
  • ARC 環(huán)境下, 同樣的代碼:
- (void)viewDidLoad {
    [super viewDidLoad];
    Block block =   ^{
        NSLog(@"%@", self);
    };
    block();
    NSLog(@"Class: %@", [block class]);
}

打印結(jié)果如下

#這是一個堆區(qū) Block
2020-09-08 12:22:07.533464+0800 Test[12399:2111818] Class: __NSMallocBlock__

至此可以驗(yàn)證, 在ARC環(huán)境下系統(tǒng)幫我們默認(rèn)執(zhí)行了拷貝操作把Block棧區(qū)拷貝到了堆區(qū);


備注: 補(bǔ)上Block的拷貝流程
參考文章
iOS Block 部分一
iOS Block內(nèi)部使用 strongSelf引用計(jì)數(shù)
基本的Clang語句
iOS Block 的拷貝底層實(shí)現(xiàn)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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