iOS底層系列21 -- Block

  • Block又稱為代碼塊,匿名函數(shù),函數(shù)指針,下面來詳細介紹Block的相關內容;

Block的聲明定義

【第一種:在property屬性中定義block】
  • 定義的是具有特定參數(shù)與返回值類型的block變量,屬于定義block變量;
@interface ViewController ()

//定義了一個block變量testOne,類型為void(^)(void)
@property(nonatomic,copy)void(^testOne)(void);
//定義了一個block變量testTwo,類型為void(^)(int)
@property(nonatomic,copy)void(^testTwo)(int a);
//定義了一個block變量testThree,類型為NSString *(^)(int,int)
@property(nonatomic,copy)NSString *(^testThree)(int a,int b);

@end
【第二種:使用typedef定義block類型】
  • 定義的是具有特定參數(shù)與返回值類型的block,屬于定義block類型;
//定義了一個YYBlock類型
typedef void(^YYBlock)(void);
//定義了一個XXBlock類型
typedef void(^XXBlock)(int a);
//定義了一個ZZBlock類型
typedef NSString *(^ZZBlock)(int a,int b);
【第三種:在函數(shù)內部定義局部block變量并初始化其代碼塊的實現(xiàn)】
  • 定義的是block變量且初始化了代碼塊;
- (void)viewDidLoad {
    [super viewDidLoad];
    
    //定義了一個block變量add,類型為void(^)(void)
    void(^add)(void) = ^{
        NSLog(@"add");
    };
    
    //定義了一個block變量minus,類型為void(^)(int)
    void(^minus)(int) = ^(int a){
        NSLog(@"%d",a);
    };
    
    //定義了一個block變量multiply,類型為int(^)(int,int)
    int(^multiply)(int,int) = ^(int a,int b){
        int c = a * b;
        NSLog(@"%d",c);
        return c;
    };
    //block代碼塊的調用
    add();
    minus(10);
    multiply(10,5);
}
  • 等號左邊是block變量名+block類型;
  • 等號右邊是block代碼塊初始化實現(xiàn);
  • 等號左邊在定義block類型時,形參的參數(shù)名可以省略,只保留形參的類型,從這里可以看出block與函數(shù)指針十分相似;
【第四種:block作為函數(shù)方法的參數(shù)】
  • 這里引用AFNetworking中發(fā)送網絡請求的函數(shù)定義;
- (NSURLSessionDataTask *)GET:(NSString *)URLString
                   parameters:(id)parameters
                      success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
                      failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure;
  • successfailure是block變量名,其作為函數(shù)參數(shù);success的block類型為
    (void (^)(NSURLSessionDataTask *task, id responseObject))
  • 外界業(yè)務層調用此網絡層函數(shù),業(yè)務層會初始化block變量success的代碼塊,網絡層在發(fā)送網絡請求獲取網絡數(shù)據(jù)之后,會調用success代碼塊,并傳入task與responseObject兩個參數(shù);
  • block作為函數(shù)參數(shù)時,block中的參數(shù)名通常要寫上,便于理解參數(shù)的含義;

Block類型

  • block通常有三種類型,分別如下:

    • 【第一種:__NSGlobalBlock__】:全局區(qū)block,存儲在全局區(qū);
    • 【第二種:__NSMallocBlock__】:堆區(qū)block,因為block既是函數(shù),也是對象;
    • 【第三種:__NSStackBlock__】:棧區(qū)block,存儲在棧區(qū);
  • 新建C語言工程,配置成MRC環(huán)境:設置target --> Build Setting --> 搜索gar --> Objective-C Automatic Reference Counting - NO,LLDB調試如下:

Snip20210317_162.png
Snip20210317_163.png
Snip20210317_164.png
  • 在MRC環(huán)境下的總結如下:
    • 全局block變量 訪問外界變量 NSGlobalBlock
    • 全局block變量 未訪問外界變量 NSGlobalBlock
    • 局部block變量 未訪問外界變量 NSGlobalBlock
    • 局部block變量 訪問外界局部變量 NSStackBlock
    • 局部block變量 訪問外界全局變量 NSGlobalBlock
  • 將C語言工程設置成ARC環(huán)境,LLDB調試如下:
Snip20210317_165.png
Snip20210317_166.png
Snip20210317_168.png
  • 在ARC環(huán)境下的總結如下:

    • 全局block變量 訪問外界變量 NSGlobalBlock
    • 全局block變量 未訪問外界變量 NSGlobalBlock
    • 局部block變量 未訪問外界變量 NSGlobalBlock
    • 局部block變量 訪問外界局部變量 NSMallocBlock
    • 局部block變量 訪問外界全局變量 NSGlobalBlock
  • 比較MRC與ARC兩種環(huán)境,可以看出當局部 block變量在訪問外界局部變量時,block變量會從棧區(qū)拷貝(copy)到堆區(qū)__NSStackBlock__ --> __NSMallocBlock__

  • 我們知道一個引用在默認情況下是強引用即strong,若在ARC環(huán)境下用弱引用weak修飾block變量時,其訪問外界局部變量,依然是棧區(qū)block,并沒有拷貝到堆區(qū);

Snip20210317_170.png
  • 最終總結針對ARC環(huán)境,局部block變量(最常見):

    • 創(chuàng)建的block為空實現(xiàn)時,默認存儲在全局區(qū),即全局區(qū)block;
    • 如果block訪問外界局部變量時:
      • 如果此時的block是強引用(默認強引用),則block存儲在堆區(qū),即堆區(qū)block;
      • 如果此時的block通過__weak修飾即弱引用,則block存儲在棧區(qū),即棧區(qū)block;
  • 現(xiàn)看一案例代碼,在MRC情況下:

#import <Foundation/Foundation.h>

void(^block)(void);

void test(){
    int age = 10;
    
    block = ^{
        NSLog(@"age = %d",age);
    };
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        test();
        block();
    }
    return 0;
}
  • 在test函數(shù)中定義全局的block變量,且block訪問了外界局部變量a,由于是在MRC環(huán)境下,所以此block是在棧區(qū)分配內存;
  • 當test函數(shù)執(zhí)行完,block就會被回收,當下面再調用block時,會出現(xiàn)數(shù)據(jù)錯亂的問題;
  • 若在ARC環(huán)境下,系統(tǒng)默認會將訪問了外界局部變量的block從棧區(qū)拷貝到堆區(qū),那么再調用block時,就不會出現(xiàn)數(shù)據(jù)錯亂的問題;

Block的copy操作

  • 不同類型block的copy操作如下:
Snip20210703_38.png
  • 在ARC環(huán)境下,系統(tǒng)會對MRC環(huán)境下的棧區(qū)block做默認的copy操作;
  • 在ARC環(huán)境下,用強指針指向棧區(qū)block,會將其copy到堆區(qū);
  • 在ARC環(huán)境下,棧區(qū)block作為返回值時,會將其copy到堆區(qū);
  • 在ARC環(huán)境下,棧區(qū)block作為方法名含有usingBlock的方法參數(shù)時,會將其copy到堆區(qū);
  • 在ARC環(huán)境下,棧區(qū)block作為GCD函數(shù)的參數(shù)時,會將其copy到堆區(qū);

Block循環(huán)引用

  • 廢話不多說先上案例如下所示:
Snip20210317_171.png
  • 第一個例子:控制器NextVC持有block,對block強引用,block內部出現(xiàn)self,即對NextVC強引用,NextVC與block彼此之間強引用,形成強引用環(huán)即循環(huán)引用,兩者都不能釋放,造成內存泄漏;

  • 第二個例子:animation的block對self(NextVC)強引用,只是單方面的強引用,沒有形成循環(huán)引用,不會造成內存泄漏;

  • 解決循環(huán)引用的常見方案有以下幾種方式:

    • 【方式一】使用弱引用__weak修飾符;
    • 【方式二】__block修飾對象(需要注意的是在block內部需要置空對象,而且block必須調用);
    • 【方式三】傳遞對象self作為block的參數(shù),提供給block內部使用;
    • 【方式四】使用NSProxy;
  • 下面來詳細介紹著幾種方式:

方式一:使用弱引用__weak修飾符
  • 當block內部為嵌套block時,直接使用__weak修飾符即可;
Snip20210317_174.png
  • self與weakSelf指向的是同一塊內存,即兩者是同一個對象;
  • block對weakSelf不會強引用,從而打破了強引用環(huán),不會造成內存泄漏;
  • 當block內部嵌套block,需要同時使用__weak 和 __strong;
Snip20210317_175.png
  • self.block內部一定不能出現(xiàn)self,出現(xiàn)就會對self強引用,造成循環(huán)引用;
  • strongSelf是一個臨時變量,在self.block的作用域內,即內部block執(zhí)行完就釋放strongSelf;
方式二:__block修飾變量
Snip20210317_176.png
  • 注意的是這里的block必須調用,如果不調用block,vc就不會置空,那么依舊是循環(huán)引用,self和block都不會被釋放;
方式三:傳遞對象self作為block的參數(shù)
Snip20210317_177.png
  • 將對象self作為block參數(shù),提供給block內部使用,不會有引用計數(shù)問題;
方式四:使用NSProxy(這里只做簡單介紹)
  • 首先來介紹一下NSProxy類;
    • NSProxy 和 NSObject是同級的一個類,也可以說是一個虛擬類,只是實現(xiàn)了NSObject的協(xié)議;
    • NSProxy 其實是一個消息重定向封裝的一個抽象類,類似一個代理人,中間件,可以通過繼承它,并重寫下面兩個方法來實現(xiàn)消息轉發(fā)到另一個實例;
- (void)forwardInvocation:(NSInvocation *)invocation;
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel
NSProxy的使用場景
  • 實現(xiàn)多繼承功能;

  • 解決了NSTimer&CADisplayLink創(chuàng)建時對self強引用問題,參考YYKit的YYWeakProxy;

  • 下面通過代碼實例來實現(xiàn)上述的兩種場景:

  • 定義三個類YYProxy,YYStudent,YYTeacher代碼實現(xiàn)如下:

#import <Foundation/Foundation.h>

@interface YYProxy : NSProxy

- (id)transformObjc:(NSObject *)objc;

+ (instancetype)proxyWithObjc:(id)objc;

@end
#import "YYProxy.h"

@interface YYProxy ()

@property(nonatomic,weak,readonly)NSObject *objc;

@end

@implementation YYProxy

+ (instancetype)proxyWithObjc:(id)objc{
    return  [[self alloc] transformObjc:objc];
}

- (id)transformObjc:(NSObject *)objc{
   _objc = objc;
    return self;
}

//1.查詢該方法的方法簽名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
    NSMethodSignature *signature;
    if (self.objc) {
        signature = [self.objc methodSignatureForSelector:sel];
    }else{
        signature = [super methodSignatureForSelector:sel];
    }
    return signature;
}

//2.有了方法簽名之后就會調用方法實現(xiàn)
- (void)forwardInvocation:(NSInvocation *)invocation{
    SEL sel = [invocation selector];
    if ([self.objc respondsToSelector:sel]) {
        [invocation invokeWithTarget:self.objc];
    }
}

- (BOOL)respondsToSelector:(SEL)aSelector{
    return [self.objc respondsToSelector:aSelector];
}

@end
#import <Foundation/Foundation.h>

@interface YYStudent : NSObject

- (void)study;

@end
#import "YYStudent.h"

@implementation YYStudent

- (void)study{
    NSLog(@"%s",__func__);
}

@end
#import <Foundation/Foundation.h>

@interface YYTeacher : NSObject

- (void)edcuate;

@end
#import "YYTeacher.h"

@implementation YYTeacher

- (void)edcuate{
    NSLog(@"%s",__func__);
}

@end
  • 通過YYProxy實現(xiàn)多繼承功能;
Snip20210317_178.png
  • YYProxy類獲取了YYStudent與YYTeacher類中的功能方法,主要是通過消息的慢速轉發(fā)實現(xiàn)的,原理在 iOS底層系列14 -- 消息流程的動態(tài)方法決議與轉發(fā)這篇文章中有詳細闡述,YYProxy由于沒有目標方法實現(xiàn),將其轉發(fā)給YYStudent與YYTeacher類;

  • 通過YYProxy解決定時器中self的強引用問題;

Snip20210317_179.png
  • 將定時器的target指定為YYProxy對象,然后YYProxy對象再將定時器的回調消息函數(shù)轉發(fā)給當前控制器;
  • 在當前控制器銷毀時,一定要銷毀定時器,從而釋放YYProxy對象;
Block導致循環(huán)引用的總結
  • 循環(huán)應用的解決方式從根本上來說就兩種,以self -> block -> self為例:
  • 打斷self 對 block的強引用,block屬性修飾符使用weak,但是這樣會導致block還未創(chuàng)建完就釋放了,所以從這里打斷強引用行不通;
  • 打斷block對self的強引用,主要就是self的作用域和block作用域的通訊,通訊有代理、傳值、通知、傳參等幾種方式,用于解決循環(huán),常見的解決方式如下:
    • weak-strong-修飾符;
    • __block(block內對象置空,且調用block);
    • 將對象self作為block的參數(shù);
    • 通過NSProxy的子類代替self;

Block的底層結構

  • 在C語言工程的main.文件中,定義一個block變量,且調用block;
Snip20210317_180.png
  • cd 到指定文件夾 輸入 clang -rewrite-objc main.m -o main.cpp即將main.m文件編譯成main.cpp文件,底層C++中的block的定義與調用如下所示:
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

//block中代碼塊 封裝在當前函數(shù)中
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    //bound by copy
    int a = __cself->a;
    printf("YY a = %d", a);
}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */
    {
        __AtAutoreleasePool __autoreleasepool;

        int a = 10;
        //block的定義
        void(*TestTwo)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
       //block的調用
        ((void (*)(__block_impl *))((__block_impl *)TestTwo)->FuncPtr)((__block_impl *)TestTwo);
    }
    return 0;
}
Block底層結構.png
  • block中定義的代碼塊在底層是封裝在__main_block_func_0C語言函數(shù)中;

  • 可以看到block本質是__main_block_impl_0結構體;

  • 定義block時,底層是調用__main_block_impl_0結構體的同名函數(shù)__main_block_impl_0(),第一個參數(shù)傳入是__main_block_func_0函數(shù)指針,也就是block中代碼塊實現(xiàn),然后將參數(shù)的函數(shù)指針賦值給成員變量implFuncPtr成員;

  • block的調用,最終也是通過__main_block_impl_0結構體的成員implFuncPtr函數(shù)指針來調用block代碼塊;

  • 由于impl__main_block_impl_0結構體的第一個成員,那么impl的內存地址就是__main_block_impl_0結構體的內存地址,所以在調用block時,可以將__main_block_impl_0結構體強轉成impl然后調用FuncPtr函數(shù)指針指向的函數(shù),也就是block中的代碼塊;

  • block捕獲外界局部變量時,從底層我們看到在__main_block_impl_0結構體中有一個int類型成員變量a,其就是用來接收捕獲的外界變量值的,在__main_block_func_0函數(shù)中完成接受賦值操作,也就是說block在捕獲外界變量時,在其底層結構體內部會自動生成同名成員變量來保存;

  • 上述捕獲的外界局部變量,在block底層結構體中會自動生成一個同名成員變量保存其值,屬于值傳遞,也就是說block內部捕獲的變量與外界的變量是兩塊不同的內存地址

  • 所以在外界更改變量的值是不會影響到block內部捕獲到的變量的值的;

  • 其次在block內部是不能更改捕獲變量的值的,因為內外變量的內存地址不同;

  • 下面嘗試捕獲全局變量,靜態(tài)變量,看看底層結構體如何操作:

Snip20210318_190.png
Snip20210318_193.png
  • block訪問全局變量,其底層結構體并沒有自動生成同名的成員變量,表明block不會捕獲全局變量,至于block代碼塊中出現(xiàn)全局變量的賦值,是因為全局變量可以在當前文件中如何地方都可訪問到;

  • block訪問靜態(tài)局部變量時,其底層結構體會自動生成一個同名指針的成員,屬于指針傳遞,所以在block內部可以更改捕獲的外界變量的值;

  • 總結:

    • block能捕獲外界局部變量,block底層結構體會自動生成同名的成員變量來接收外界局部變量的值,但在block內部不能修改外界局部變量的值,因為是值傳遞,內外內存地址不同;
    • block能捕獲外界靜態(tài)局部變量,其底層結構體會自動生成同名的成員變量來接收,且在block內部可以修改外界局部變量的值,因為是地址的傳遞,內外內存地址相同;
    • block不能捕獲外界全局變量,block底層結構體不會自動生成同名的成員變量來接收,但在block內部可以修改全局變量的值,因為全局變量是全局的可以直接訪問;
  • 由上面的內容我們知道,在block內部是不能修改局部變量的,但是可以修改靜態(tài)局部變量,那么如何修改局部變量呢?接下來就要引出__block修飾符;通過__block修飾符我們可以在block內部修改局部變量;

__block修飾符的底層實現(xiàn)
  • 先上代碼如下所示:
Snip20210318_194.png
  • 編譯之后的底層代碼:
struct __Block_byref_m_0 {
  void *__isa;
__Block_byref_m_0 *__forwarding;
 int __flags;
 int __size;
 int m;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_m_0 *m; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_m_0 *_m, int flags=0) : m(_m->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    __Block_byref_m_0 *m = __cself->m;
    (m->__forwarding->m) = 20;
    printf("YY m = %d",(m->__forwarding->m));
}

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

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->m, 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[]) {
    { __AtAutoreleasePool __autoreleasepool;
        __attribute__((__blocks__(byref))) __Block_byref_m_0 m = {(void*)0,(__Block_byref_m_0 *)&m, 0, sizeof(__Block_byref_m_0), 10};
        void(*TestTwo)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_m_0 *)&m, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)TestTwo)->FuncPtr)((__block_impl *)TestTwo);
    }
    return 0;
}
  • __block修飾的外界局部變量,在底層結構中會生成__Block_byref_m_0結構體,此結構體會保存局部變量的值和指針,且其內部還有一個__forwarding指針指向它自己;
  • 在block內部捕獲的不是__block變量的值,而是捕獲__block變量的地址,通過地址訪問內存修改其值;
  • 打印出__block修飾的局部變量m與內部捕獲的變量m的地址,如下所示:
Snip20210318_196.png
  • 可以看出__block修飾的局部變量存儲在棧區(qū),而內部捕獲到的局部變量存儲在堆區(qū);
  • block訪問__block修飾局部變量,其底層結構體之間的關系如下:
Snip20210704_40.png
  • __block修飾的局部變量,在底層會被包裝成一個__Block_byref_age_0結構體,此結構體有一個__forwarding成員是指向自己的,其分析如下:
Snip20210704_41.png
  • 當block還在棧區(qū)時,__forwarding指向的是棧區(qū)的__block變量的底層結構體;
  • 當block拷貝到堆區(qū)時,棧區(qū)的__forwarding指向堆區(qū)的__block變量的底層結構體,堆區(qū)的__forwarding也是指向__block變量的底層結構體,保證通過__forwarding指針訪問的一定是堆區(qū)的__block變量;
Snip20210704_42.png
block底層源碼實現(xiàn)
  • 在定義block處加上斷點,然后進入?yún)R編頁面看到如下:
Snip20210318_197.png
  • 在創(chuàng)建block時會調用objc_retainBlock函數(shù);
  • 在工程中配置符號斷點 objc_retainBlock,LLDB調試如下:
Snip20210318_198.png
  • 再在工程中配置符號斷點 _Block_copy,LLDB調試如下:
Snip20210318_199.png
  • 可以看到底層執(zhí)行的是libsystem_blocks.dylib中的_Block_copy函數(shù);
  • 到蘋果開源網站下載 libclosure-78 源碼,全局搜索_Block_copy,結果如下所示:
Snip20210318_200.png
  • 看到Block_layout結構體,它才是block的真正類型,Block_layout結構如下:
// Block 結構體
struct Block_layout {
    //指向表明block類型的類
    void *isa;//8字節(jié)
    //用來作標識符的,類似于isa中的位域,按bit位表示一些block的附加信息
    volatile int32_t flags; // contains ref count 4字節(jié)
    //保留信息,可以理解預留位置,用于存儲block內部變量信息
    int32_t reserved;//4字節(jié)
    //函數(shù)指針,指向具體的block實現(xiàn)的調用地址
    BlockInvokeFunction invoke;
    //block的附加信息
    struct Block_descriptor_1 *descriptor;
    // imported variables
};
  • isa:表明block是一種class類;
  • flags:標識符,按bit位表示一些block的附加信息,類似于isa中的位域,其中flags的種類有以下幾種,主要重點關注BLOCK_HAS_COPY_DISPOSE 和 BLOCK_HAS_SIGNATURE, BLOCK_HAS_COPY_DISPOSE 決定是否有 Block_descriptor_2,BLOCK_HAS_SIGNATURE 決定是否有 Block_descriptor_3;
    • 第1 位 - BLOCK_DEALLOCATING,釋放標記,-般常用 BLOCK_NEEDS_FREE 做 位與 操作,一同傳入 Flags , 告知該 block 可釋放。
    • 第16位 - BLOCK_REFCOUNT_MASK,存儲引用計數(shù)的值,是一個可選用參數(shù);
    • 第24位 - BLOCK_NEEDS_FREE,低16是否有效的標志,程序根據(jù)它來決定是否增加或是減少引用計數(shù)位的 值;
    • 第25位 - BLOCK_HAS_COPY_DISPOSE,是否擁有拷貝輔助函數(shù)(a copy helper function);
    • 第26位 - BLOCK_IS_GC,是否擁有 block 析構函數(shù);
    • 第27位,標志是否有垃圾回收;//OS X
    • 第28位 - BLOCK_IS_GLOBAL,標志是否是全局block;
    • 第30位 - BLOCK_HAS_SIGNATURE,與 BLOCK_USE_STRET 相對,判斷當前 block 是否擁有一個簽名。用于 runtime 時動態(tài)調用。
// flags 標識
// Values for Block_layout->flags to describe block objects
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)調用
    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
};
  • reserved:保留信息,可以理解預留位置,猜測是用于存儲block內部變量信息;
  • invoke:是一個函數(shù)指針,指向block的執(zhí)行代碼;
  • descriptor:block的附加信息,比如保留變量數(shù)、block的大小、進行copy或dispose的輔助函數(shù)指針,有三類如下所示:
    • Block_descriptor_1是必選的;
    • Block_descriptor_2 和 Block_descriptor_3都是可選的;
#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
    uintptr_t reserved;//保留信息
    uintptr_t size;//block大小
};

#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
    // requires BLOCK_HAS_COPY_DISPOSE
    BlockCopyFunction copy;//拷貝函數(shù)指針
    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 布局
};
  • 以上關于descriptor的可以從其構造函數(shù)中體現(xiàn),其中Block_descriptor_2和Block_descriptor_3都是通過Block_descriptor_1的地址,經過內存平移得到的;
static struct Block_descriptor_1 * _Block_descriptor_1(struct Block_layout *aBlock)
{
    return aBlock->descriptor;//默認打印
}
#endif

// CJL注釋:Block 的描述 : copy 和 dispose 函數(shù)
static struct Block_descriptor_2 * _Block_descriptor_2(struct Block_layout *aBlock)
{
    if (! (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)) return NULL;
    uint8_t *desc = (uint8_t *)aBlock->descriptor;//descriptor_1的地址
    desc += sizeof(struct Block_descriptor_1);//通過內存平移獲取
    return (struct Block_descriptor_2 *)desc;
}

//Block 的描述 : 簽名相關
static struct Block_descriptor_3 * _Block_descriptor_3(struct Block_layout *aBlock)
{
    if (! (aBlock->flags & BLOCK_HAS_SIGNATURE)) return NULL;
    uint8_t *desc = (uint8_t *)aBlock->descriptor;
    desc += sizeof(struct Block_descriptor_1);
    if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE) {
        desc += sizeof(struct Block_descriptor_2);
    }
    return (struct Block_descriptor_3 *)desc;
}
  • 下面來探索block在創(chuàng)建的時候,其存儲域的變化過程:
  • 初始化創(chuàng)建一個局部block變量add,內部未訪問外界局部變量,在創(chuàng)建處打下斷點:
- (void)viewDidLoad {
    [super viewDidLoad];
    [self.view addSubview:self.button];
    //定義了一個block變量add,類型為void(^)(void)
    __block int m = 10;
    void(^add)(void) = ^{
        
    };
    add();
}
  • 進入?yún)R編界面,執(zhí)行到objc_retainBlock斷點停住,LLDB調試如下:
Snip20210318_202.png
  • 看到此時的block類型為全局block,即__NSGlobalBlock__;
  • 若block內部捕獲外界局部變量時,代碼如下:
- (void)viewDidLoad {
    [super viewDidLoad];
    [self.view addSubview:self.button];
    //定義了一個block變量add,類型為void(^)(void)
    __block int m = 10;
    void(^add)(void) = ^{
        m = 20;
        NSLog(@" m = %d",m);
    };
    add();
}
  • 進入?yún)R編界面,執(zhí)行到objc_retainBlock斷點停住,LLDB調試如下:
Snip20210318_203.png
  • 看到此時的block類型為棧區(qū)block,即__NSStackBlock__
  • 添加一個符號斷點_Block_copy,當執(zhí)行到此函數(shù)匯編指令的ret指令時,停住斷點,LLDB調試結果如下:
Snip20210318_204.png
  • 可以看到底層源碼在調用_Block_copy函數(shù)之后,block類型變成了堆區(qū)block,即__NSMallocBlock__,完成了block從棧區(qū)到堆區(qū)的拷貝;

  • _Block_copy的函數(shù)源碼解析:

// Copy, or bump refcount, of a block.  If really copying, call the copy helper if present.
//這里是核心重點 block的拷貝操作: 棧Block -> 堆Block
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;//強轉為Block_layout類型對象,防止對外界造成影響
    if (aBlock->flags & BLOCK_NEEDS_FREE) {//是否需要釋放
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {//如果是全局block,直接返回
        return aBlock;
    }
    else {//為棧block 或者 堆block,由于堆區(qū)需要申請內存,所以只可能是棧區(qū)
        // Its a stack block.  Make a copy. 它是一個棧區(qū)block,執(zhí)行拷貝
        struct Block_layout *result =
            (struct Block_layout *)malloc(aBlock->descriptor->size);//申請空間并接收
        if (!result) return NULL;
        //通過memmove內存拷貝,將 aBlock 拷貝至result
        memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
#if __has_feature(ptrauth_calls)
        // Resign the invoke pointer as it uses address authentication.
        result->invoke = aBlock->invoke;//可以直接調起invoke
#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.
        result->isa = _NSConcreteMallocBlock;//設置block對象類型為堆區(qū)block
        return result;
    }
}
  • 首先判斷block是否需要free,需要free就直接釋放;

  • 然后判斷block是否為全局block,如果是則不需要copy,直接返回;

  • 最后專門時針對棧區(qū)block的邏輯操作,將棧區(qū)block拷貝到堆區(qū);

    • 通過malloc申請內存空間用于接收block;
    • 通過memmove將block拷貝至新申請的內存中;
    • 設置block對象的類型為堆區(qū)block,即result->isa = _NSConcreteMallocBlock
  • _Block_object_assign函數(shù)是在底層編譯代碼中,當block從棧區(qū)拷貝到堆區(qū)時,外部變量的拷貝(棧區(qū)-->堆區(qū))調用的方法就是它;

  • 外部變量的類型如下:

// Block 捕獲的外界變量的種類
// Runtime support functions used by compiler when generating copy/dispose helpers

// Values for _Block_object_assign() and _Block_object_dispose() parameters
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 弱引用變量
    BLOCK_FIELD_IS_WEAK     = 16,  // declared __weak, only used in byref copy helpers
    //返回的調用對象 - 處理block_byref內部對象內存會加的一個額外標記,配合flags一起使用
    BLOCK_BYREF_CALLER      = 128, // called from __block (byref) copy/dispose support routines.
};
  • 其中最常用的是BLOCK_FIELD_IS_BYREFBLOCK_FIELD_IS_OBJECT;

  • _Block_object_assign源碼如下:

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:
        /*******
        id object = ...;
        [^{ object; } copy];
        ********/

        _Block_retain_object(object);
        *dest = object;
        break;

      case BLOCK_FIELD_IS_BLOCK:
        /*******
        void (^object)(void) = ...;
        [^{ object; } copy];
        ********/

        *dest = _Block_copy(object);
        break;
    
      case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
      case BLOCK_FIELD_IS_BYREF:
        /*******
         // copy the onstack __block container to the heap
         // Note this __weak is old GC-weak/MRC-unretained.
         // ARC-style __weak is handled by the copy helper directly.
         __block ... x;
         __weak __block ... x;
         [^{ x; } copy];
         ********/

        *dest = _Block_byref_copy(object);
        break;
        
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
        /*******
         // copy the actual field held in the __block container
         // Note this is MRC unretained __block only. 
         // ARC retained __block is handled by the copy helper directly.
         __block id object;
         __block void (^object)(void);
         [^{ object; } copy];
         ********/

        *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:
        /*******
         // copy the actual field held in the __block container
         // Note this __weak is old GC-weak/MRC-unretained.
         // ARC-style __weak is handled by the copy helper directly.
         __weak __block id object;
         __weak __block void (^object)(void);
         [^{ object; } copy];
         ********/
        *dest = object;
        break;

      default:
        break;
    }
}
  • 針對普通對象(BLOCK_FIELD_IS_OBJECT),執(zhí)行_Block_retain_object(),引用計數(shù)+1;

  • 針對block變量(BLOCK_FIELD_IS_BLOCK),執(zhí)行_Block_copy()即拷貝;

  • 針對__block修飾的變量(BLOCK_FIELD_IS_BYREF),執(zhí)行_Block_byref_copy();

  • _Block_byref_copy的源碼實現(xiàn)如下:

static struct Block_byref *_Block_byref_copy(const void *arg) {
    //強轉為Block_byref結構體類型,保存一份
    struct Block_byref *src = (struct Block_byref *)arg;

    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;
        //block內部持有的Block_byref 和 外界的Block_byref 所持有的對象是同一個,這也是為什么__block修飾的變量具有修改能力
        //copy 和 scr 的地址指針達到了完美的同一份拷貝,目前只有持有能力
        copy->forwarding = copy; // patch heap copy to point to itself
        src->forwarding = copy;  // patch stack to point to heap copy
        copy->size = src->size;
        //如果有copy能力
        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
            //Block_byref_2是結構體,__block修飾的可能是對象,對象通過byref_keep保存,在合適的時機進行調用
            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;
            }
            //等價于 __Block_byref_id_object_copy
            (*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結構體類型對象;

  • 判斷傳入的對象是否已經拷貝到堆區(qū):

    • 已經拷貝到堆區(qū),處理直接返回;
    • 未拷貝到堆區(qū),首先申請內存,然后進行拷貝,再將copy(堆區(qū))與src(棧區(qū))的forwarding指針都指向堆區(qū)的內存地址;
  • 代碼調試:

int main(int argc, const char * argv[])
{
    @autoreleasepool{
        __block NSString *a = [NSString stringWithFormat:@"li"];
        void(^TestBlock)(void) = ^{
            NSLog(@" a = %@",a);
        };
        NSLog(@" TestBlock = %@",TestBlock);
    }
    return 0;
}
  • clang編譯成C++語言如下:
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*);
 NSString *a;
};

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;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    __Block_byref_a_0 *a = __cself->a;
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_c5_l8bnxw0d2w92f4439t_r8qjc0000gn_T_main_5b35fd_mii_1,(a->__forwarding->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, 8/*BLOCK_FIELD_IS_BYREF*/);}

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; 
        __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, ((NSString * _Nonnull (*)(id, SEL, NSString * _Nonnull, ...))(void *)objc_msgSend)((id)objc_getClass("NSString"), sel_registerName("stringWithFormat:"), (NSString *)&__NSConstantStringImpl__var_folders_c5_l8bnxw0d2w92f4439t_r8qjc0000gn_T_main_5b35fd_mii_0)};
        void(*TestBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_c5_l8bnxw0d2w92f4439t_r8qjc0000gn_T_main_5b35fd_mii_2,TestBlock);
    }
    return 0;
}
  • 看到__block修飾OC對象時,__Block_byref結構體會多生成出兩個函數(shù)分別為__Block_byref_id_object_copy__Block_byref_id_object_dispose;

  • 調試底層源碼libclosure,首先底層會執(zhí)行_Block_copy函數(shù),實現(xiàn)將棧區(qū)block拷貝到堆區(qū);

Snip20210321_12.png
  • 當執(zhí)行到_Block_call_copy_helper函數(shù)時,接下來會調用_Block_object_assign函數(shù),傳入的參數(shù)是__block修飾的局部變量被封裝成Block_byref結構體對象;
Snip20210321_14.png
  • 接下來進入_Block_byref_copy函數(shù)內部,實現(xiàn)將棧區(qū)的Block_byref結構體對象拷貝到堆區(qū);
Snip20210321_15.png
  • 上面已經提到如果__block修飾的是OC對象,在__Block_byref結構體會多生成出兩個函數(shù)分別為__Block_byref_id_object_copy__Block_byref_id_object_dispose;
//block自身拷貝(_Block_copy) 
// __block bref結構體拷貝(_Block_object_assign) //_Block_object_assign中對外部變量(存儲在bref)拷貝一份到內存
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);
}
  • if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) 這個判斷條件就與是否生成COPY_DISPOSE兩個函數(shù)有關,如果生成了就會執(zhí)行這兩個函數(shù)的相關邏輯;
  • 從COPY_DISPOSE兩個函數(shù)實現(xiàn)我們看到,dst是__Block_byref結構體,根據(jù)結構體成員布局(dst + 40)就是外界局部變量a;所以COPY_DISPOSE這兩個函數(shù)執(zhí)行的是外界局部變量a的拷貝與釋放;
總結:

__block修飾OC對象存在三層copy:

  • block對象的copy,調用_Block_copy函數(shù)實現(xiàn)將棧區(qū)block拷貝到堆區(qū);
  • 外界局部變量生成C++結構體Block_byref對象的copy,調用_Block_object_assign-->_Block_byref_copy實現(xiàn)將Block_byref結構體對象從棧區(qū)拷貝到堆區(qū);
  • 只針對OC對象類,即在結構體Block_byref中生成COPY_DISPOSE函數(shù),將外界局部變量從棧區(qū)拷貝到堆區(qū);
_Block_object_dispose
  • 同一般的retain和release一樣,_Block_object_assign其本質主要是retain,所以對應的還有一個release,即_Block_object_dispose方法,其源碼實現(xiàn)如下,也是通過區(qū)分block種類,進行不同釋放操作;
// When Blocks or Block_byrefs hold objects their destroy helper routines call this entry point
// to help dispose of the contents 當Blocks或Block_byrefs持有對象時,其銷毀助手例程將調用此入口點以幫助處置內容
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://__block修飾的變量,即bref類型的
        // get rid of the __block data structure held in a Block
        _Block_byref_release(object);
        break;
      case BLOCK_FIELD_IS_BLOCK://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;
    }
}
  • 進入_Block_byref_release源碼,主要就是對象、變量的釋放銷毀;
static void _Block_byref_release(const void *arg) {
    //對象強轉為Block_byref類型結構體
    struct Block_byref *byref = (struct Block_byref *)arg;

    // dereference the forwarding pointer since the compiler isn't doing this anymore (ever?)
    byref = byref->forwarding;//取消指針引用
    
    if (byref->flags & BLOCK_BYREF_NEEDS_FREE) {
        int32_t refcount = byref->flags & BLOCK_REFCOUNT_MASK;
        os_assert(refcount);
        if (latching_decr_int_should_deallocate(&byref->flags)) {
            if (byref->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {//是否有拷貝輔助函數(shù)
                struct Block_byref_2 *byref2 = (struct Block_byref_2 *)(byref+1);
                (*byref2->byref_destroy)(byref);//銷毀拷貝對象
            }
            free(byref);//釋放
        }
    }
}

Block的內存管理

  • 棧區(qū)block,訪問外界局部變量,不會對局部變量產生強引用;
  • 堆區(qū)block,會調用block內部的copy函數(shù),copy函數(shù)內部會調用_Block_object_assign函數(shù),此函數(shù)會根據(jù)局部變量的修飾符(__strong,__weak,__unsafe_unretained)做出相應的操作,
  • 堆區(qū)block從堆區(qū)移除時,會調用block內部的dispose函數(shù),dispose函數(shù)內部會調用_Block_object_dispose函數(shù),其會自動釋放引用的局部變量,相當于release局部變量;
Snip20210704_39.png
  • 棧區(qū)block拷貝到堆區(qū)時,__block修飾的變量也會從棧區(qū)復制到堆區(qū),并且堆區(qū)block會對__block變量進行引用;
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容