iOS原理篇(五):Block探究

  • Block原理
  • Block變量捕獲
  • Block類型
  • copy操作和Block內(nèi)部訪問對(duì)象類型的變量
  • __block修改變量及其本質(zhì)
  • __block內(nèi)存管理
  • Block循環(huán)引用問題

Block是一種可以在C、C++以及Objective-C代碼中使用,類似于“閉包(closure)”的代碼塊,借助Block機(jī)制,開發(fā)者可以將代碼像對(duì)象一樣在不同的上下文環(huán)境中進(jìn)行傳遞。
(這里說的不同上下文環(huán)境,我舉個(gè)例子:比如在A函數(shù)中定義了一個(gè)變量,它是一個(gè)局部變量,那么我要在B函數(shù)中去訪問,這里就屬于兩個(gè)不同的上下文環(huán)境)

一、Block原理

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

將上面main.m編譯生成C++代碼:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

main()函數(shù)

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

__main_block_impl_0結(jié)構(gòu)體

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

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

struct __maib_block_desc_0 {
    size_t reserved;
    size_t Block_size;
};

我們定義block變量,其實(shí)下面這句代碼:

void (*block)(int,int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));

就是調(diào)用結(jié)構(gòu)體__main_block_impl_0內(nèi)部的構(gòu)造函數(shù)初始化一個(gè)結(jié)構(gòu)體出來,然后取結(jié)構(gòu)體地址&__main_block_impl_0賦給block指針,所以block底層是下面結(jié)構(gòu)體;調(diào)用構(gòu)造函數(shù)傳了三個(gè)參數(shù):
(void *)__main_block_func_0&__main_block_desc_0_DATAage

其中(void *)__main_block_func_0是下面函數(shù)的地址,這個(gè)函數(shù)就是封裝了block執(zhí)行邏輯的函數(shù),通過上面的構(gòu)造函數(shù)傳給__block_impl結(jié)構(gòu)體的FuncPtr

static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
  int age = __cself->age; // bound by copy

    NSLog((NSString *)&__NSConstantStringImpl__var_folders__h_yv1h9mrx1155q6tq7brn8b9m0000gp_T_main_b54551_mi_0,a,b,age);
}

同樣,第二個(gè)參數(shù)類型&__main_block_desc_0_DATA是下面結(jié)構(gòu)體地址,最終通過構(gòu)造函數(shù)賦給了Desc,其中Block_size表示block結(jié)構(gòu)體的大小;

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()函數(shù)中調(diào)用block(3, 5)最終轉(zhuǎn)化為下面代碼,通過將block強(qiáng)制轉(zhuǎn)換為__block_impl(這里__block_impl類型是__main_block_impl_0結(jié)構(gòu)體第一個(gè)成員,所以可以轉(zhuǎn)) ,最終直接找到impl中的FuncPtr進(jìn)行調(diào)用

((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 3, 5);

二、Block變量捕獲

Block變量捕獲是指在Block內(nèi)部訪問外部變量時(shí),如果外部變量是局部變量,則Block內(nèi)部會(huì)將其捕獲,具體捕獲形式看外部的這個(gè)局部變量是auto類型還是static類型:
如果是auto類型,直接將變量的值傳遞給Block內(nèi)部,Block結(jié)構(gòu)體內(nèi)部會(huì)生成一個(gè)變量來存儲(chǔ)傳進(jìn)來的值,所以在Block外邊改變age=20,調(diào)用block()時(shí)內(nèi)部打印的結(jié)果依然是age=10,因?yàn)榇藭r(shí)進(jìn)行的是值傳遞;
如果是static類型,會(huì)將變量的地址傳遞給Block內(nèi)部,block結(jié)構(gòu)體內(nèi)部會(huì)生成一個(gè)指針變量來存儲(chǔ)傳進(jìn)來的地址值,所以在block外邊改變height=20,調(diào)用block()時(shí)內(nèi)部打印的結(jié)果是height=20,因?yàn)榇藭r(shí)進(jìn)行的是指針傳遞;

下面進(jìn)行驗(yàn)證:

  1. 局部變量?jī)煞N情況:
// 局部變量?jī)煞N情況
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // case-1: auto變量,離開作用域就銷毀
        auto int age = 10; //等價(jià)于 int age = 10;
        // case-2: static變量
        static int height = 10;
        
        void (^block)(void) = ^{
            NSLog(@"age is %d, height is %d",age, height);
        };
        age = 20;
        height = 20;
        
        block();
    }
    return 0;
}

打印結(jié)果:

age is 10, height is 20

編譯成C++:

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

從編譯生成的結(jié)構(gòu)體看出,age是值傳遞,height是指針傳遞;定義完block就將10&height捕獲到block內(nèi)部,后邊調(diào)用block時(shí)訪問的結(jié)構(gòu)體內(nèi)部age是捕獲到的值10,height是捕獲到的地址&height;

  1. 全局變量:因?yàn)槭窃谌謪^(qū),所以任何函數(shù)內(nèi)部可以直接訪問

總結(jié)一下:


Block的本質(zhì):Block本質(zhì)上也是一個(gè)OC對(duì)象,它內(nèi)部也有個(gè)isa指針,但它是一個(gè)封裝了函數(shù)調(diào)用以及函數(shù)調(diào)用環(huán)境的OC對(duì)象;

三、Block類型

Block有三種類型,可以通過調(diào)用class方法或者isa指針查看具體類型,最終都是繼承自NSBlock類型:

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

打印結(jié)果:
05-block類型[46881:69217078] __NSGlobalBlock__
05-block類型[46881:69217078] __NSGlobalBlock
05-block類型[46881:69217078] NSBlock
05-block類型[46881:69217078] NSObject

三種類型:

  • __NSGlobalBlock__ (_NSConcreteGlobalBlock)
  • __NSStackBlock___NSConcreteStackBlock
  • __NSMallocBlock___NSConcreteMallocBlock
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void (^block1)(void) = ^{
            NSLog(@"Hello");
        };
        int age = 10;
        void (^block2)(void) = ^{
            NSLog(@"Hello - %d", age);
        };
        NSLog(@"%@ %@ %@",[block1 class], [block2 class], [^{
            NSLog(@"%d",age);
        } class]);
    }
    return 0;
}

打印結(jié)果:
05-block類型[47475:69339707] __NSGlobalBlock__ __NSMallocBlock__ __NSStackBlock__

那不同類型Block分別對(duì)應(yīng)什么情況呢?

static int height = 30;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        // 沒有訪問外部變量
        void (^block1)(void) = ^{
            NSLog(@"--------------");
        };
        
        // 訪問 auto 變量
        int age = 10;
        void (^block2)(void) = ^{
            NSLog(@"--------------%d", age);
        };
        
        // 訪問 static 變量
        void (^block3)(void) = ^{
            NSLog(@"=-------------%d", height);
        };
        
        NSLog(@"%@ %@ %@",[block1 class], [block2 class], [block3 class]);
    }
    return 0;
}

打印結(jié)果:
05-block類型[48630:69576321] __NSGlobalBlock__ __NSMallocBlock__ __NSGlobalBlock__

可以看出,在沒有訪問外部變量的情況下,block1是一個(gè)__NSGlobalBlock__類型,存放在數(shù)據(jù)區(qū),此時(shí)的block1就相當(dāng)于我們定一個(gè)了一個(gè)函數(shù),函數(shù)中的代碼沒有訪問另外一個(gè)函數(shù)(此處為main())中的變量;同理,block3雖然訪問外部變量,但static變量是全局的,同樣相當(dāng)于單獨(dú)拿出去定義一個(gè)和main()函數(shù)上下文無關(guān)的函數(shù);
由于block2訪問了auto變量,相當(dāng)于在block2封裝的函數(shù)中訪問了另外一個(gè)函數(shù)內(nèi)部的變量(main()函數(shù)中的局部變量age),此時(shí)block2變?yōu)?code>__NSStackBlock__,因?yàn)樗枰4孢@個(gè)局部變量,由于是在ARC環(huán)境,會(huì)自動(dòng)對(duì)__NSStackBlock__類型進(jìn)行copy操作,所以 block2打印類型是一個(gè) __NSMallocBlock__類型;

關(guān)閉ARCMRC環(huán)境下打印:

打印結(jié)果:
05-block類型[49786:69814242] __NSGlobalBlock__ __NSStackBlock__ __NSGlobalBlock__

可以看出block2確實(shí)是一個(gè)__NSStackBlock__類型;

四、copy操作和Block內(nèi)部訪問對(duì)象類型的變量


copy操作分MRCARC兩種情況:
  • MRC環(huán)境:
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int age = 10;
        
        // 情況一:沒有訪問外部 auto 變量
        // 它是一個(gè) NSGlobalBlock
        // 內(nèi)存是放在數(shù)據(jù)段
        void (^block)(void) = ^{
            NSLog(@"---------");
        };
        

        // 情況二:訪問外部 auto 變量 age
        // 它是一個(gè) NSStackBlock 隨時(shí)會(huì)被回收
        // 內(nèi)存是在棧上
        // 通過 copy 操作轉(zhuǎn)變?yōu)?NSMallocBlock 把它放到堆上保活
        void (^block2)(void) = [^{
            NSLog(@"---------%d",age);
        } copy];
        
        // 因?yàn)樵?MRC 環(huán)境 不用時(shí)要進(jìn)行 release 操作
        [block2 release];
    
    }
    return 0;
}
  • ARC環(huán)境:
    ARC環(huán)境下,編譯器會(huì)根據(jù)情況自動(dòng)將棧上的Block拷貝到堆上,即自動(dòng)進(jìn)行一次copy操作,比如以下情況:
  1. 情況一:Block作為函數(shù)返回值
// 定義一個(gè)block類型
typedef void (^DJTBlock)(void);

// block作為函數(shù)返回值
DJTBlock myblock()
{
    // case1: 這里沒有訪問auto變量 是一個(gè)NSGlobalBlock
    return ^{
        NSLog(@"------------");
    };
    // 相當(dāng)于下面這樣寫
    // DJTBlock block = ^{
    //   NSLog(@"------------");
    // };
    // return block;
    
    //-----------------------------------------------------------------
    
    // case2: 這里訪問了auto 是一個(gè)NSSackBlock 作為函數(shù)返回值A(chǔ)RC下自動(dòng)copy成NSMallocBlock
    // int age = 10;
    // return ^{
    //   NSLog(@"------------%d",age);
    // };
}


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        // ARC環(huán)境下 調(diào)用myblock()函數(shù)
        // DJTBlock作為myblock函數(shù)的返回值 編譯器自動(dòng)進(jìn)行一次 copy 操作
        // 所以 block變量指向的 DTJBlock 此時(shí)已經(jīng)在堆上
        DJTBlock block = myblock();
        block();
        
        // 打印 block 類型
        NSLog(@"%@",[block class]);
    
    }
    return 0;
}
打印結(jié)果:
05-block--copy[64907:9167520] ------------
05-block--copy[64907:9167520] __NSGlobalBlock__

打印結(jié)果是一個(gè)NSGlobalBlock類型,這是因?yàn)樵诤瘮?shù)my block()內(nèi)部沒有訪問auto變量(上面block類型有闡述),而對(duì)NSGlobalBlock類型的Block執(zhí)行copy操作生成的Block還是NSGlobalBlock,所以如果將返回改為myblock()函數(shù)內(nèi)注釋部分,就會(huì)打印__NSMallocBlock__。

  1. 情況二:將Block賦值給__strong強(qiáng)指針時(shí)
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int age = 10;
        // Block被強(qiáng)指針指著
        DJTBlock block = ^{
            NSLog(@"------------%d",age);
        };
        block();
       
        // 打印 block 類型
        NSLog(@"%@",[block class]);
    }
    return 0;
}
打印結(jié)果:
05-block--copy[69520:9293376] ------------10
05-block--copy[69520:9293376] __NSMallocBlock__
  1. 情況三:Block作為Cocoa API 中方法各含有usingBlock的方法參數(shù)時(shí):
NSArray *array = @[];
[array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

}];

這個(gè)參數(shù)Block也是一個(gè)堆上的block;

  1. 情況四:Block作為GCD API的方法參數(shù)時(shí):
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
            
});
    
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0*NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            
});

Block內(nèi)部訪問對(duì)象類型的變量

先看一個(gè)有趣的現(xiàn)象:

// DJTPerson.h
@interface DJTPerson : NSObject
@property(nonatomic, assign) int age;
@end

// DJTPerson.m
@implementation DJTPerson
- (void)dealloc
{
    NSLog(@"DJTPerson----dealloc");
}
@end
// main.m
#import "DJTPerson.h"

typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        {
            DJTPerson *person = [[DJTPerson alloc] init];
            person.age = 10;
        }
        NSLog(@"-----------------");// 打斷點(diǎn)
    }
    return 0;
}

在上面NSLog(@"-----------------");處打斷點(diǎn),運(yùn)行程序發(fā)現(xiàn)控制臺(tái)打印:

05-block訪問對(duì)象類型的auto變量[77563:9561984] DJTPerson----dealloc
(lldb) 

說明在斷點(diǎn)前的中括號(hào)結(jié)束,person變量就已經(jīng)釋放,接著我們定義一個(gè)block,在內(nèi)部訪問personage屬性:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        DJTBlock block;
        
        {
            DJTPerson *person = [[DJTPerson alloc] init];
            person.age = 10;
            
            block = ^{
                NSLog(@"-----------%d",person.age);
            };
        }
        
        NSLog(@"-----------------");// 打斷點(diǎn)
        
    }
    return 0;
}

通用在NSLog(@"-----------------");處打斷點(diǎn),運(yùn)行程序發(fā)現(xiàn)控制臺(tái)無打印,說明person沒被回收。

為什么被第二種情況下person沒有被回收呢?為了驗(yàn)證我們將代碼簡(jiǎn)化并編譯成C++來進(jìn)行底層原理分析:

typedef void (^DJTBlock)(void);

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

日常操作命令:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

在編譯生成的C++文件中查看生成的block結(jié)構(gòu)體:

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

由于personDJTPerson *類型,所以捕獲到block內(nèi)部也是DJTPerson *類型,即struct __main_block_impl_0結(jié)構(gòu)體內(nèi)部可以看到有一個(gè)DJTPerson *類型變量person;下面先從一個(gè)角度理解為什么person沒有被釋放:

在上面代碼中,我們定義的Block是被一個(gè)DJTBlock類型的變量block強(qiáng)引用的,即這句代碼:

DJTBlock block = ^{
   NSLog(@"-----------%d",person.age);
};

ARC環(huán)境下,被強(qiáng)引用的這個(gè)Block(訪問了auto變量)會(huì)自動(dòng)拷貝到堆上,而這個(gè)Block內(nèi)部(編譯成C++即為struct __main_block_impl_0結(jié)構(gòu)體)又有一個(gè)DJTPerson*類型的指針指向外面這個(gè)person對(duì)象,所以只要這個(gè)Block在,那么這個(gè)強(qiáng)指針就在,所以外邊的person對(duì)象不會(huì)被釋放;

換成MRC環(huán)境:

// DJTPerson.h
@interface DJTPerson : NSObject
@property(nonatomic, assign) int age;
@end

// DJTPerson.m
@implementation DJTPerson
- (void)dealloc
{
   [super dealloc];
    NSLog(@"DJTPerson----dealloc");
}
@end
// main.m
#import "DJTPerson.h"

typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        DJTBlock block;
        {
            DJTPerson *person = [[DJTPerson alloc] init];
            person.age = 10;
            
            block = ^{
                NSLog(@"-----------%d",person.age);
            };
            
            [person release];
        }
        NSLog(@"---------------");// 打斷點(diǎn)
    }
    return 0;
}

依然在NSLog(@"---------------");打斷點(diǎn),發(fā)現(xiàn)控制臺(tái)打印結(jié)果:

05-block訪問對(duì)象類型的auto變量[83896:9803518] DJTPerson----dealloc

發(fā)現(xiàn)person被釋放,這是因?yàn)榧词?code>block內(nèi)部訪問了person對(duì)象,MRC環(huán)境下,block內(nèi)部訪問了auto變量,它是一個(gè)棧上block,但并不會(huì)自動(dòng)拷貝到堆上,由于它是一個(gè)NSStackBlock,內(nèi)部并不會(huì)對(duì)外部person強(qiáng)引用(這里說強(qiáng)引用并不準(zhǔn)確,在MRC環(huán)境沒有強(qiáng)引用說法,應(yīng)該描述為沒有對(duì)外邊person進(jìn)行retain操作,但為了好理解 so...),所以在執(zhí)行完[person release]以后,雖然Block還沒有離開其作用域(Block作用域到return 0;前到大括號(hào)),但person就被釋放;可以通過[block copy]將其復(fù)制到堆上,這樣內(nèi)部就會(huì)對(duì)外邊的person強(qiáng)引用(其實(shí)是retain操作)從而?;?code>person,當(dāng)然在Block銷毀的時(shí)候,內(nèi)部對(duì)person還會(huì)進(jìn)行一次release操作,這樣一加一減,就保持了平衡;

要點(diǎn):??臻g的BlockNSStackBlock)是不會(huì)對(duì)外邊auto對(duì)象進(jìn)行保活(ARC環(huán)境表現(xiàn)為不會(huì)強(qiáng)引用,MRC下表現(xiàn)為不會(huì)進(jìn)行retain操作),只有拷貝到堆上(NSMallocBlock)才會(huì)對(duì)其自動(dòng)?;睢?/p>

回到ARC環(huán)境:
看一下__weak作用:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        DJTBlock block;
        {
            DJTPerson *person = [[DJTPerson alloc] init];
            person.age = 10;
            
            // 這里使用 __weak 修飾
            __weak DJTPerson *weakPerson = person;
            block = ^{
                NSLog(@"-----------%d",weakPerson.age);
            };
            
        }
        NSLog(@"---------------"); // 打斷點(diǎn)
    }
    return 0;
}

依然在NSLog(@"---------------");處打斷點(diǎn),打印結(jié)果為:

05-block訪問對(duì)象類型的auto變量[87323:9930285] DJTPerson----dealloc

這說明,即使在ARC環(huán)境,Block被拷貝到堆上,由于我們用__weak類型的__weakPerson訪問了外部auto變量,它也不會(huì)對(duì)外部person進(jìn)行強(qiáng)引用。

同樣我們把上述代碼編譯成C++,由于弱引用需要運(yùn)行時(shí)機(jī)制來支持,所以我們不能進(jìn)行靜態(tài)編譯,還需要運(yùn)行時(shí)調(diào)用,指定運(yùn)行時(shí)系統(tǒng)版本,所以編譯命令如下:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

在生成的C++代碼中找到__main_block_impl_0結(jié)構(gòu)體,發(fā)現(xiàn)是一個(gè)弱引用:

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

總結(jié)一下:

  • Block在棧上,無論 ARCMRC環(huán)境,block內(nèi)部都不會(huì)對(duì)外部對(duì)象類型的auto變量產(chǎn)生強(qiáng)引用,就算Block內(nèi)部生成強(qiáng)指針,也不會(huì)對(duì)外部person產(chǎn)生強(qiáng)引用,因?yàn)?code>Block自己就在棧上,隨時(shí)可能被銷毀;
  • Block在堆上:
    ARC環(huán)境下,訪問外部對(duì)象類型的auto變量,編譯后:
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  DJTPerson *__strong person;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, DJTPerson *__strong _person, int flags=0) : person(_person) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

__main_block_desc_0結(jié)構(gòu)體中,多了兩個(gè)函數(shù):__main_block_copy_0__main_block_dispose_0

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

分別看它們實(shí)現(xiàn):

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

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);}

當(dāng)對(duì)Block進(jìn)行copy操作時(shí),會(huì)調(diào)用這個(gè)__main_block_copy_0函數(shù),在它內(nèi)部調(diào)用_Block_object_assign((void*)&dst->person, (void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/),會(huì)對(duì)外部person對(duì)象產(chǎn)生強(qiáng)引用或者弱引用,這取決于block內(nèi)部使用__strong指針還是__weak指針訪問。

當(dāng)Block從堆上移除,會(huì)調(diào)用__main_block_dispose_0函數(shù),它內(nèi)部調(diào)用_Block_object_dispose((void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);,會(huì)對(duì)外部person對(duì)象進(jìn)行一次release操作。

MRC環(huán)境下,也是由這兩個(gè)函數(shù)決定是否進(jìn)行retainrelease操作。

五、__block修改變量及其本質(zhì)

我們先看下面一段代碼:

typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int age = 10;
        
        DJTBlock block = ^{
            age = 20; // 報(bào)錯(cuò):Variable is not assignable (missing __block type specifier)
            NSLog(@"-----------%d",age);
        };   
    }
    return 0;
}

ARC下直接在Block內(nèi)部修改age會(huì)報(bào)錯(cuò),這就相當(dāng)于在block生成的結(jié)構(gòu)體中FuncPtr指向的函數(shù)中去修改main函數(shù)中的局部變量(如果這里agestatic或者全局變量,可以修改,因?yàn)檫@兩種變量一直在內(nèi)存中),上下文環(huán)境發(fā)生了改變,所以不能直接訪問age;我們使用__block修飾age變量,然后編譯成C++

typedef void (^DJTBlock)(void);

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

會(huì)生成下面結(jié)構(gòu)體:

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

可以看到在Block結(jié)構(gòu)體中多了__Block_byref_age_0 *age;,看一下 __Block_byref_age_0發(fā)現(xiàn)它也是一個(gè)結(jié)構(gòu)體:

struct __Block_byref_age_0 {
    void *__isa;
    __Block_byref_age_0 *__forwarding;
    int __flags;
    int __size;
    int age;
};

這里看出編譯器將 __block修飾的變量(這里是age)包裝成一個(gè)對(duì)象__Block_byref_age_0(因?yàn)樗鼉?nèi)部有isa指針,所以可以認(rèn)為它是個(gè)對(duì)象),Block內(nèi)部(__main_block_impl_0結(jié)構(gòu)體中)并不會(huì)直接擁有這個(gè)變量age,而是擁有__Block_byref_age_0這個(gè)結(jié)構(gòu)體,然后__Block_byref_age_0結(jié)構(gòu)體中有一個(gè)int age變量,我們?cè)贐lock內(nèi)部改變age = 20,實(shí)際上就是賦值給__Block_byref_age_0結(jié)構(gòu)體中的age變量。

我們對(duì)__block int age = 10轉(zhuǎn)化成的C++代碼進(jìn)行簡(jiǎn)化:

// __block int age = 10;對(duì)應(yīng)下面c++代碼:
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};

// 進(jìn)行簡(jiǎn)化
__Block_byref_age_0 age = {
     0,
     &age,
     0,
     sizeof(__Block_byref_age_0),
     10    
 };

對(duì)應(yīng)到下面結(jié)構(gòu)體初始化:

struct __Block_byref_age_0 {
   void *__isa;
   __Block_byref_age_0 *__forwarding;
   int __flags;
   int __size;
   int age;
};

__forwarding指針傳入&age指向__Block_byref_age_0 age結(jié)構(gòu)體自己(這里&age是結(jié)構(gòu)體地址,不要混淆),10賦值給了__Block_byref_age_0結(jié)構(gòu)體內(nèi)部的age變量;我們?cè)倏聪滦薷?code>age為20的代碼:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_age_0 *age = __cself->age; // bound by ref
 // 這里是 age = 20;
  (age->__forwarding->age) = 20;
   NSLog((NSString *)&__NSConstantStringImpl__var_folders__h_yv1h9mrx1155q6tq7brn8b9m0000gp_T_main_05a705_mi_0,(age->__forwarding->age));
}

可以發(fā)現(xiàn)先通過 __cself->age找到__Block_byref_age_0結(jié)構(gòu)體,然后(age->__forwarding->age) = 20;通過__forwarding指針修改結(jié)構(gòu)體內(nèi)部的age變量,__forwarding指向結(jié)構(gòu)體自己,那為什么要多此一舉通過__forwarding指針去修改內(nèi)部age,而不通過結(jié)構(gòu)體指針直接去修改呢?這是為了保證Blockcopy到堆上時(shí),不管訪問棧上還是堆上Block,通過forwarding指針都是找到堆上。

這里如果__block修飾的是一個(gè)對(duì)象類型,比如下面代碼:

typedef void (^DJTBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block int age = 10;
        __block NSObject *obj = [[NSObject alloc] init];
        DJTBlock block = ^{
            obj = nil;
            age = 20;
        };
    }
    return 0;
}

轉(zhuǎn)換為C++同樣會(huì)多生成一個(gè)對(duì)應(yīng)的結(jié)構(gòu)體,只不過內(nèi)部會(huì)多出兩個(gè)方法copy``和dispose方法來負(fù)責(zé)相應(yīng)的內(nèi)存管理:

// __block age 對(duì)應(yīng)的結(jié)構(gòu)體
struct __Block_byref_age_0 {
  void *__isa;
__Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
 int age;
};

// __block NSObject 對(duì)應(yīng)的結(jié)構(gòu)體
struct __Block_byref_obj_1 {
  void *__isa;
__Block_byref_obj_1 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 NSObject *__strong obj;
};

下面看一個(gè)例子:

typedef void (^DJTBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSMutableArray *mutarray = [NSMutableArray array];
        DJTBlock block = ^{
            [mutarray addObject:@(12)];
            [mutarray addObject:@(13)];
        };
    }
    return 0;
}

這里不會(huì)報(bào)錯(cuò),是因?yàn)槲覀儾]有修改mutarray指針,而是在使用mutarray指針,除非我們修改mutarray指針的值,比如 mutarray = nil;才需要__block來修飾;

六、__block的內(nèi)存管理

我們知道,當(dāng)Block內(nèi)部訪問外部對(duì)象類型的變量時(shí),如下面簡(jiǎn)單代碼:

typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *object = [[NSObject alloc] init];
        DJTBlock block = ^{
            NSLog(@"%p", object);
        };
        block();
    }
    return 0;
}

block編譯成C++后的結(jié)構(gòu):

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSObject *__strong object; //內(nèi)部強(qiáng)引用外部變量
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *__strong _object, int flags=0) : object(_object) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

可以看到在block結(jié)構(gòu)體內(nèi)部會(huì)生成一個(gè)強(qiáng)指針指向外邊的object對(duì)象,并且在block被拷貝到堆上時(shí),調(diào)用__main_block_desc_0中的copy函數(shù),對(duì)這個(gè)指針指向的對(duì)象進(jìn)行一次retain操作,即引用計(jì)數(shù)+1,當(dāng)然如果用__weak修飾object會(huì)生NSObject *__weak object;此時(shí)不會(huì)強(qiáng)引用;
那當(dāng)我們用__block修飾變量時(shí),比如分別修飾基礎(chǔ)數(shù)據(jù)類型age,和對(duì)象類型obj1,如下代碼:

typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
       
        __block int age = 10;
        __block NSObject *obj1 = [[NSObject alloc] init];
        NSObject *object = [[NSObject alloc] init];
        DJTBlock block = ^{
            NSLog(@"%d %p %p", age,obj1, object);
        };
        block();
    }
    return 0;
}

編譯成C++:

struct __Block_byref_age_0 {
  void *__isa;
__Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
 int age;
};
struct __Block_byref_obj1_1 {
  void *__isa;
__Block_byref_obj1_1 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 NSObject *__strong obj1;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSObject *__strong object;
  __Block_byref_age_0 *age; // by ref
  __Block_byref_obj1_1 *obj1; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *__strong _object, __Block_byref_age_0 *_age, __Block_byref_obj1_1 *_obj1, int flags=0) : object(_object), age(_age->__forwarding), obj1(_obj1->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

經(jīng)__block修飾的變量ageobj1分別生成構(gòu)體__Block_byref_age_0__Block_byref_obj1_1,它們的本質(zhì)就是OC對(duì)象,所以在block對(duì)應(yīng)的結(jié)構(gòu)體內(nèi)部生成兩個(gè)結(jié)構(gòu)體指針指向這兩個(gè)對(duì)象,即

__Block_byref_age_0 *age; // by ref
__Block_byref_obj1_1 *obj1; // by ref

它們其實(shí)和object一樣,因?yàn)?code>__block修飾的變量也是轉(zhuǎn)換成結(jié)構(gòu)體,而且內(nèi)部有isa指針,其實(shí)就是OC對(duì)象,所以也會(huì)在__main_block_desc_0中生成兩個(gè)函數(shù):copydispose,來管理對(duì)象的內(nèi)存,可以看下結(jié)構(gòu):

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) 
{
  _Block_object_assign((void*)&dst->age, (void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);
  _Block_object_assign((void*)&dst->obj1, (void*)src->obj1, 8/*BLOCK_FIELD_IS_BYREF*/);
  _Block_object_assign((void*)&dst->object, (void*)src->object, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) 
{
  _Block_object_dispose((void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);
  _Block_object_dispose((void*)src->obj1, 8/*BLOCK_FIELD_IS_BYREF*/);
  _Block_object_dispose((void*)src->object, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

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

所以__block內(nèi)存管理可以總結(jié)一下:

  • 當(dāng)Block在棧上時(shí),內(nèi)部并不會(huì)對(duì)__block修飾的外部變量產(chǎn)生強(qiáng)引用

  • 當(dāng)Blockcopy到堆上時(shí),會(huì)調(diào)用Block內(nèi)部的copy函數(shù),而copy函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_assign函數(shù),它內(nèi)部會(huì)對(duì)__block變量形成強(qiáng)引用(retain)。


  • 當(dāng)Block從堆上移除時(shí),會(huì)調(diào)用Block內(nèi)部的dispose函數(shù),dispose函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_dispose函數(shù),在_Block_object_dispose函數(shù)中會(huì)對(duì)引用的__block變量進(jìn)行引用計(jì)數(shù)-1release


下面我們對(duì)比下Block內(nèi)部訪問外部變量幾種情況:

typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
      
         //直接將20存放在Block生成的結(jié)構(gòu)體中
        int num = 20; 
        //Block結(jié)構(gòu)體內(nèi)部生成一個(gè)強(qiáng)指針 強(qiáng)引用object對(duì)象
        NSObject *object = [[NSObject alloc] init]; 
        // Block內(nèi)部生成一個(gè)弱指針 弱引用object對(duì)象
        __weak NSObject *weakObject = object; 
        // Block內(nèi)部生成一個(gè)結(jié)構(gòu)體指針,指針指向的結(jié)構(gòu)體內(nèi)部存儲(chǔ)著變量age
        __block int age = 10; 
        //Block內(nèi)部生成一個(gè)結(jié)構(gòu)體指針,指針指向的結(jié)構(gòu)體內(nèi)部存儲(chǔ)著變量obj1
        __block NSObject *obj1 = [[NSObject alloc] init];
        
        DJTBlock block = ^{
            NSLog(@"%d %d %p %p %p",num, age, obj1, object, weakObject);
        };
        block();
    }
    return 0;
}

編譯成C++看看block結(jié)構(gòu)體,和上邊注釋的一致:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int num;
  NSObject *__strong object;
  NSObject *__weak weakObject;
  __Block_byref_age_0 *age; // by ref
  __Block_byref_obj1_1 *obj1; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _num, NSObject *__strong _object, NSObject *__weak _weakObject, __Block_byref_age_0 *_age, __Block_byref_obj1_1 *_obj1, int flags=0) : num(_num), object(_object), weakObject(_weakObject), age(_age->__forwarding), obj1(_obj1->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

這里主要對(duì)比一下 對(duì)象類型的auto變量和__block修飾的變量?jī)?nèi)存管理的區(qū)別:

  • 相同點(diǎn):
    • 當(dāng)Block在棧上時(shí),對(duì)它們都不會(huì)產(chǎn)生強(qiáng)引用
    • 當(dāng)Block拷貝到堆上時(shí),都會(huì)通過copy函數(shù)來處理它們:
      (1)__block變量(假設(shè)變量名叫做a
      _Block_object_assign((void*)&dst->a, (void*)src->a,   8/*BLOCK_FIELD_IS_BYREF*/);
      
      (2)對(duì)象類型的auto變量(假設(shè)變量名叫做p
      _Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
      
    • 當(dāng)Block從堆上移除時(shí),都會(huì)通過dispose函數(shù)來釋放它們
      (1)__block變量(假設(shè)變量名叫做a
      _Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*
      
      (2)對(duì)象類型的auto變量(假設(shè)變量名叫做p
      _Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
      
  • 不同點(diǎn)(主要是在引用的問題上)
    (1)對(duì)象類型的auto變量,根據(jù)傳進(jìn)來時(shí)是__strong還是__weak類型決定調(diào)用copy函數(shù)時(shí)Block內(nèi)部對(duì)傳進(jìn)來的變量進(jìn)行強(qiáng)還是弱引用。
    (2)如果時(shí)__block類型的變量,比如__block int age = 20;,它被封裝成一個(gè)OC對(duì)象,調(diào)用copy函數(shù)時(shí)Block內(nèi)部直接對(duì)它產(chǎn)生強(qiáng)引用,對(duì)它的內(nèi)存進(jìn)行管理,不存在__weak修飾int age這種操作,所以沒有弱引用這一說。(這里強(qiáng)引用的是age轉(zhuǎn)換成的結(jié)構(gòu)體對(duì)象,真正的age變量的值存儲(chǔ)在結(jié)構(gòu)體里邊);

但是如果是下面代碼

typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
       
        NSObject *object = [[NSObject alloc] init];
        __block __weak NSObject *weakObject = object;
        DJTBlock block = ^{
            NSLog(@"%p %p", object, weakObject);
        };
        block();
    }
    return 0;
}

編譯成C++:

struct __Block_byref_weakObject_0 {
  void *__isa;
__Block_byref_weakObject_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 NSObject *__weak weakObject;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSObject *__strong object;
  __Block_byref_weakObject_0 *weakObject; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *__strong _object, __Block_byref_weakObject_0 *_weakObject, int flags=0) : object(_object), weakObject(_weakObject->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

可以看到在__block修飾變量生成的結(jié)構(gòu)體 __Block_byref_weakObject_0內(nèi)部,通過__weak弱引用變量weakObject,即Block結(jié)構(gòu)體內(nèi)部是一個(gè)強(qiáng)指針指向__block生成的結(jié)構(gòu)體,即這句代碼

__Block_byref_weakObject_0 *weakObject;
(注意雖然名字中有`weak`但這是一個(gè)強(qiáng)指針)

而在結(jié)構(gòu)體__Block_byref_weakObject_0內(nèi)部:

NSObject *__weak weakObject;

這才是一個(gè)弱指針,指向外部傳入的弱引用對(duì)象weakObject,它表達(dá)了外部傳入變量的類型是__weak還是__strong

注意:這里在MRC下有個(gè)特殊情況,在__block生成的結(jié)構(gòu)體內(nèi)部,始終都是弱引用,不會(huì)對(duì)外邊對(duì)象進(jìn)行強(qiáng)引用。


MRC環(huán)境下驗(yàn)證, 下面代碼在block();調(diào)用前person就已經(jīng)掛了,說明確實(shí)內(nèi)部沒有強(qiáng)引用:

#import "DJTPerson.h"

typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
       
        __block DJTPerson *person = [[DJTPerson alloc] init];
        DJTBlock block = [^{
            NSLog(@" %p", person);
        } copy];
        [person release];
        block();
    }
    return 0;
}

七、Block相關(guān)問題

  • Block的原理是怎樣的?本質(zhì)是什么?
  • __block的作用是什么?有什么使用注意點(diǎn)?
  • Block的屬性修飾詞為什么是copy?使用Block有哪些使用注意?
  • Block在修改NSMutableArray,需不需要添加__block

理解上邊原理再回答這些問題應(yīng)該不難吧。

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

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

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