Block底層學習

Block底層本質(zhì)

  • block就是Objective-C對閉包的實現(xiàn),閉包就是一個沒有名字的函數(shù)或者指向函數(shù)的指針。block本質(zhì)上也是一個OC對象,它內(nèi)部有isa指針;
  • block是封裝了函數(shù)調(diào)用以及函數(shù)調(diào)用環(huán)境(參數(shù))的OC對象;

我們來看一段代碼

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int a = 10;
        void(^block)(int, int) = ^(int b,int c){
            NSLog(@"%d",a);
            NSLog(@"Hello World!");
        };       
        block(10,10);
    }
    return 0;
}

把上面這段代碼轉(zhuǎn)化為C++底層語言,轉(zhuǎn)化后:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        int a = 10;
        //定義block變量
        void(*block)(int, int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
        //執(zhí)行block內(nèi)部的代碼
        ((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 10, 10);
    }
    return 0;
}

在C++代碼中,block代碼塊底層調(diào)用__main_block_impl_0。這句代碼調(diào)用以下代碼

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

我們發(fā)現(xiàn),block的底層也是一個結(jié)構(gòu)體。搜索struct __block_impl

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

block的第一個結(jié)構(gòu)體成員是一個isa指針。這說明,block也是一個OC對象。
__main_block_desc_0結(jié)構(gòu)體成員包括兩個參數(shù),如下:

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)};

__main_block_func_0函數(shù)內(nèi)部封裝了block執(zhí)行邏輯的函數(shù)

static void __main_block_func_0(struct __main_block_impl_0 *__cself, int b, int c) {
  int a = __cself->a; // bound by copy
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_2n_ksb7n0n131n2y7v9xcf411fm0000gn_T_main_7cbb05_mi_0,a);
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_2n_ksb7n0n131n2y7v9xcf411fm0000gn_T_main_7cbb05_mi_1);
        }

struct __block_impl結(jié)構(gòu)體內(nèi),有一個成員void *FuncPtr,

Block變量捕獲

為了保證block內(nèi)部能夠正常訪問外部的變量,block有個變量捕獲機制。
先來看一段代碼

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int a = 10;
        void (^block)(void) = ^{
            NSLog(@"%d",a);
        };
        a = 20;
        block();
    }
    return 0;
}

執(zhí)行上面這段代碼,打印值是10,而不是20。之所以執(zhí)行block(),結(jié)果是10,而不是20,這個就使用了變量捕獲。
我們來看下C++底層代碼

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        int a = 10;
        void (*block)(void) = ((void (*)())&__main_block_impl_0(
                                                                (void *)__main_block_func_0,
                                                                &__main_block_desc_0_DATA,
                                                                a));
        a = 20;
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

在上面的代碼中,編譯時,在block內(nèi)部已經(jīng)捕獲到a值。然后傳遞到__main_block_impl_0。

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

在上面代碼中,根據(jù)傳遞過來的值,賦值給NSLog(@"%d",a);。當a = 20;時,僅僅是改變int a = 10;的值。而block內(nèi)部獲取不到a的值。

變量捕獲

  • int a = 10;默認是auto修飾,是值傳遞;auto,自動變量,auto修飾的變量,內(nèi)存會自動消失。所以,block內(nèi)部不會捕獲會自動消失的內(nèi)存。
  • 如果int a = 10;使用static修飾,則傳遞的是地址,block內(nèi)部捕獲到變量的地址,如果在外部修改變量的值,則根據(jù)地址找到變量存儲的值。
  • 全局變量并不會被捕獲到block內(nèi)部。在block內(nèi)部會直接訪問全部變量。
  • self是一個局部變量,在block內(nèi)部,也會捕獲self。

Block類型

因為block是一個對象,所以block也是有類型的。block有三種類型,可以通過class方法或者isa指針查看具體類型,最終都是繼承自NSBlock類型。

  • NSGlobalBlock
    存儲在數(shù)據(jù)區(qū)域,沒有訪問auto。
  • NSStackBlock
    存儲在棧區(qū),會自動銷毀,訪問了auto。
  • NSMallocBlock
    存儲在堆區(qū),需要程序員銷毀,NSStackBlock調(diào)用了copy(ARC環(huán)境下,block會自動調(diào)用copy,從棧上賦值到堆上,所以一般block類型是NSMallocBlock)。

ARC環(huán)境下,編譯器會根據(jù)情況自動將棧上的block復制到堆上,比如以下情況:

  • block作為函數(shù)返回值時;

  • 將block賦值給__strong指針時;

  • block作為Cocoa API中方法名含有usingBlock的方法參數(shù)時;

  • block作為GCD API的方法參數(shù)時。

  • MRC下block屬性的建議寫法
    @property (copy,nonatomic) void (^block)(void);

  • ARC下block屬性的建議寫法
    @property (copy,nonatomic) void (^block)(void);
    @property (strong,nonatomic) void (^block)(void);

對象類型的auto變量以及Block的內(nèi)存管理

類似于局部變量,有auto修飾的對象在block內(nèi)部,也會存在block類型。來看一段代碼

#import <Foundation/Foundation.h>

@interface Person : NSObject
@property (nonatomic,  assign) int age;
@end

#import "Person.h"

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

main.m
#import <Foundation/Foundation.h>
#import "Person.h"
typedef void(^HYBlock) (void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        HYBlock myBlock;
        {
            Person *p = [[Person alloc] init];
            p.age = 10;
            myBlock = ^{
                NSLog(@"%d",p.age);
            };
            myBlock();
        }
    }
    return 0;
}
  • 當block內(nèi)部訪問了對象類型的auto變量時
    如果block是在棧上,將不會對auto變量產(chǎn)生強引用。

  • 當block被拷貝到堆上
    ??會調(diào)用block內(nèi)部的copy函數(shù);
    ??copy函數(shù)內(nèi)部會調(diào)用_Block_object_assign;

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

??_Block_object_assign函數(shù)會根據(jù)auto變量的修飾符(__strong、__weak、__unsafe unretained)做出相應的操作,形成強引用(retain)或者弱引用。

  • 如果block從堆上移除
    ??會調(diào)用blokc內(nèi)部的dispose函數(shù)
    ??dispose函數(shù)內(nèi)部會調(diào)用_Block_object_dispose函數(shù)
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

??_Block_object_dispose函數(shù)會自動釋放引用的auto變量。

Block修飾符

我們知道不能再block內(nèi)部修改外部變量的值,我們來看下原因:

#import <Foundation/Foundation.h>
typedef void(^HBlock) (void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int a = 10;
        HBlock  block= ^{
            NSLog(@"%d",a);
        };
        block();
    }
    return 0;
}

轉(zhuǎn)化為C++代碼

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        int a = 10;
        HBlock block= ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

在上面的代碼中,定義了int a = 10;。而輸出這個變量值是在下面這個函數(shù)中

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_2n_ksb7n0n131n2y7v9xcf411fm0000gn_T_main_5b923f_mi_0,a);
        }

因為變量a不是全局變量,只是局部變量,所以不能在另外一個函數(shù),修改變量值。

  • 如果int a = 10;使用static修飾,可以在block內(nèi)部修改變量的值。
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;
  }
};

在上面的代碼中,int *a;傳遞的是變量的地址值,在block內(nèi)部,先找到變量的地址值,直接修改變量a的值,而不是直接修改變量值。

  • 如果int a = 10;是全局變量,則在當前文件的函數(shù)中,都可以修改變量值。
  • 使用static修飾變量,或者使用全局變量,則這個變量一直在內(nèi)存中。如果使用__block修改,也可以在block內(nèi)部修改變量值,并且,變量會自動釋放,不會一直存在內(nèi)存中。
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref
...
};

從上面的代碼可以看出,使用__block修飾變量,在__main_block_impl_0內(nèi)部,變量a為__Block_byref_a_0 *a; // by ref。而__Block_byref_a_0是一個對象(內(nèi)部有isa指針)。在這個結(jié)構(gòu)體內(nèi)部,有成員變量,存儲變量值。

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

當修改變量值時,利用__Block_byref_a_0指針先找到結(jié)構(gòu)體,通過變量名找到__forwarding,在通過__forwarding找到變量,來修改變量值。

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) = 20;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_2n_ksb7n0n131n2y7v9xcf411fm0000gn_T_main_b8c75c_mi_0,(a->__forwarding->a));
        }
  • 注意
    ??使用__block修飾int a,則在block內(nèi)部a成為對象。
    ??創(chuàng)建NSMutableArray *array = [NSMutableArray array];,在block內(nèi)部使用[array addObject:@123]是使用這個指針,而不是改變array的值。

block循環(huán)引用

循環(huán)引用是指兩個或以上對象互相強引用,導致所有對象無法釋放的現(xiàn)象。這是內(nèi)存泄露的一種情況。

#import <Foundation/Foundation.h>

typedef void(^HYBlock)(void);
@interface Person : NSObject
@property (nonatomic, assign) int age;
@property (nonatomic, copy) HYBlock block;
@end

#import "Person.h"

@implementation Person
- (void)dealloc{
    NSLog(@"%s",__func__);
}
@end


#import <Foundation/Foundation.h>
#import "Person.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        person.age = 20;
        person.block = ^{
            NSLog(@"%d", person.age);
        };
        person.block();
    }
    return 0;
}

在上面的代碼中,當執(zhí)行person.block();時,Person對象并沒有釋放,產(chǎn)生循環(huán)引用。
我們來看下,產(chǎn)生循環(huán)引用的原因,首先轉(zhuǎn)化為C++代碼

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Person *__strong person;
...
};

在ARC環(huán)境下,HYBlock被拷貝到堆上,當內(nèi)部調(diào)用person時,則在函數(shù)__main_block_impl_0內(nèi)部,Person對象生成Person *__strong person;也即是強引用這個對象。Person對象強引用HYBlockHYBlock又強引用Person對象,則HYBlock不釋放,Person對象也不會釋放。

解決循環(huán)引用

  • 使用__weak,__unsafe_unretained解決;
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Person *__weak weakPerson;
...
};

使用weak修飾對象,則在函數(shù)__main_block_impl_0內(nèi)部,不在強引用Person對象。__unsafe_unretained同理,也不在強引用Person對象。

  • 使用__block解決(必須調(diào)用block);
#import <Foundation/Foundation.h>
#import "Person.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
       __block Person *person = [[Person alloc] init];
        person.age = 20;
        person.block = ^{
            NSLog(@"%d", person.age);
            person = nil;
        };
        person.block();
    }
    return 0;
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

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