OC底層-深刻理解Block

基本使用

block常見的使用方式以下:ios

// 無參無返回值
void(^MyBlockOne)(void) = ^(void) {
    NSLog(@"無參數, 無返回值");
};
MyBlockOne();

// 有參無返回值
void (^MyBlockTwo)(int a) = ^(int a) {
    NSLog(@"a = %d", a);
};
MyBlockTwo(10);

//有參有返回值
int (^MyBlockThree)(int, int) = ^(int a, int b) {
    
    NSLog(@"return %d", a + b);
    return 10;
};
MyBlockThree(10, 20);

// 無參有返回值
int (^MyBlockFour)(void) = ^(void) {
    NSLog(@"return 10");
    return 10;
};

// 聲明爲某種類型
typedef int (^MyBlock) (int, int);
@property (nonatomic, copy) MyBlock block;
複製代碼

Block的本質 - OC對象

結論: block的內部存在isa指針,其本質就是封裝了函數調用函數調用環境OC對象objective-c

證實方法一:底層結構窺探

main函數中定義一個block,以下swift

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void(^block)(void) = ^(void) {
            NSLog(@"this is first block");
        };
        block();
    }
    return 0;
}
複製代碼

終端進入項目所在目錄,經過xcrun 命令將OC代碼轉爲C++代碼:markdown

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
複製代碼

轉換結果以下:iphone

// 1. block 的結構體
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

// block 內部impl結構體,存儲isa指針,block方法的地址。
struct __block_impl {
  void *isa;      
  int Flags;
  int Reserved;
  void *FuncPtr;  // 方法地址
};

// block 的描述信息,如:block的大小
![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c441779a345740a98accb31ac195d61f~tplv-k3u1fbpfcp-zoom-1.image)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)};

// 2. block 的方法實現
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_hc_wwwl26516td3w0ds9cx80c280000gp_T_main_cf18a7_mi_0);
}

// 3. main方法的實現
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}
複製代碼

將生成的main方法簡化後得:async

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

看簡化後的代碼,你是否是有疑問, 爲何block->FuncPtr(block) 這句話能調用成功,明明FuncPtr__block_impl 類型裏的成員,爲何能夠直接使用block調用函數

緣由其實很簡單,由於在block結構體__main_block_impl_0內,__block_impl是第一個成員變量,所以block的地址和impl的地址是相同的。二者能夠進行強制轉換。oop

根據轉換結果:測試

  1. OC中定義的block底層其實就是一個C++ 的結構體__main_block_impl_0。結構體有兩個成員變量implDesc,分別是結構體類型 __block_impl__main_block_desc_0
  2. 結構體__block_impl內包含了isa指針和指向函數實現的指針FuncPtr
  3. 結構體__main_block_desc_0Block_size成員存儲着Block的大小

由上可知,block內部有一個isa指針,所以,block本質其實就是一個OC對象ui

證實方法二:代碼層面

若是block是一個OC對象,那它最終確定繼承自NSObject類(NSProxy除外),所以咱們能夠直接打印出block繼承鏈看一下就知道了。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void(^block)(void) = ^(void) {
            NSLog(@"this is first block");
        };
        
        NSLog(@"class = %@", [block class]); 
        NSLog(@"superclass = %@", [block superclass]); 
        NSLog(@"superclass superclass = %@", [[block superclass] superclass]);
        NSLog(@"superclass superclass superclass = %@", [[[block superclass] superclass] superclass]);
    }
    return 0;
}
輸出結果:
2020-07-28 19:25:24.475317+0800 LearningBlock[39445:591948] class = __NSGlobalBlock__
2020-07-28 19:25:24.475707+0800 LearningBlock[39445:591948] superclass = __NSGlobalBlock
2020-07-28 19:25:24.475762+0800 LearningBlock[39445:591948] superclass superclass= NSBlock
2020-07-28 19:25:24.475808+0800 LearningBlock[39445:591948] superclass superclass superclass= NSObject
複製代碼

block 的繼承鏈: __NSGlobalBlock -> NSBlock -> NSObject

能夠看出block最終繼承自NSObject的isa指針其實就是由NSObject來的。 所以block本質就是一個OC對象。

Block 的變量捕獲(Capture)

爲了保證block內部可以正常訪問外部的值,block有個變量捕獲的機制。下面來一塊兒來探索如下block的變量捕獲機制

代碼:

int a = 10;   // 全局變量, 程序運行過程一直存在內存。
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        auto int b = 20;       // 局部變量,默認是auto修飾,通常能夠不寫auto,所在做用域結束後會被銷燬。
        static int c = 30;     // 靜態變量,程序運行過程當中一直存在內存。
        
        void(^block)(void) = ^(void) {
            NSLog(@"a = %d, b = %d, c = %d", a, b, c);
        };
        
        // 觀察調用block時,a,b,c 的值是多少呢?
        a = 11;
        b = 21;
        c = 31;   
        
        block();  // 調用block
    }
    return 0;
}

打印輸出:
2020-07-28 19:43:41.729849+0800 LearningBlock[39648:603167] a = 11, b = 20, c = 31
複製代碼

由打印結果來看,b沒有改變, 而ac 的值都發生了變化。 緣由是什麼呢?下面一塊兒看下

運行下面的轉換語句,將當前的OC代碼轉換C++, 方便咱們看到更本質的東西:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
複製代碼

轉換後的代碼以下:

int a = 10;  // 全局變量

struct __main_block_impl_0 {   // block的結構體
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int b;    // 新生成的成員變量b,用於存放外部局部變量b的值
  int *c;   // 新生成的成員變量c,指針類型, 用於存儲外部靜態局部變量c的引用。
  
  // 構造函數
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _b, int *_c, int flags=0) : b(_b), c(_c) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int b = __cself->b;  // 經過cself進行訪問內部的成員變量b
  int *c = __cself->c;   // 經過cself獲取靜態局部變量c的引用
  
  // 直接訪問全局變量a
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_vt_j2sf07q142992_z55yg_170w0000gp_T_main_256a11_mi_0, a , b, (*c)); 
}

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; 

        auto int b = 20;
        static int c = 30;

        void (*Myblock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, b, &c));

        a = 11;
        b = 21;
        c = 31;

        ((void (*)(__block_impl *))((__block_impl *)Myblock)->FuncPtr)((__block_impl *)Myblock);

    }
    return 0;
}
複製代碼

有上能夠觀察到:

block結構體__main_block_impl_0內部生成了新的成員變量b*c, 分別用於存放外部傳進來的bc的地址,這就是咱們所說的捕獲。而對於全局變量a則沒有進行捕獲,在使用時是直接訪問的。

由此可得出:

  1. block 內部對autostatic類型的變量進行了捕獲,可是不會捕獲全局變量
  2. 雖然block對autostatic變量都進行了捕獲,可是不一樣的是,auto 變量是值傳遞,而static變量則是地址傳遞。所以當外部的static變量值發生變化時,block內部也跟着會改變,而外部的auto變量值發生變化,block內部的值不會發生改變。

avatar

思考🤔

相信你會有這樣的疑問,爲何block會捕獲autostatic類型的局部變量,而不會捕獲全局變量呢?(全局變量表示不服,block你怎麼搞區別對待呢?), 那麼block的變量捕獲究竟有什麼講究呢?

實際上是這樣的

  • 首先對於auto類型的局部變量,其生命週期過短了,離開了其所在的做用域後,auto變量的內存就會被系統回收了,而block的調用時機是不肯定的,若是block不對它進行捕獲,那麼當block運行時再訪問auto變量時,由於變量已被系統回收,那麼就會出現壞內存訪問或者獲得不正確的值
  • 對於局部的static變量,由於其初始化以後,在程序運行過程當中就會一直存在內存中,而不會被系統回收,可是因爲由於是局部變量的緣由,其訪問的做用域有限,block想訪問它就要知道去哪裏訪問,因此block才須要對其進行捕獲,但與auto變量不一樣的是,block只需捕獲static變量的地址便可。
  • 對於全局變量,由於其在程序運行過程一直都在,而且其訪問做用域也是全局的,因此block能夠直接找到它,而不須要對它進行捕獲。

因此,block的變量捕獲原則其實很簡單,若是block內部能直接訪問到的變量,那就不捕獲(捕獲也是浪費空間), 若是block內部不能直接訪問到變量,那就須要進行捕獲(不捕獲就沒得用)。

Block的類型

block有3種類型,能夠經過調用class方法或者isa指針查看具體類型,最終都是繼承自NSBlock類型.

  • NSGlobalBlock
  • NSStackBlock
  • NSMallocBlock

爲了準確分析block的類型,先把ARC給關閉,使用MRCavatar

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        auto int age = 10;       // 局部變量,默認是auto,通常能夠不寫auto,所處做用域結束後會被銷燬。
        static int height = 20;  // 靜態變量,程序運行過程當中一直存在內存。

        void(^block1)(void) = ^(void) {
            NSLog(@"1111111111");  // 沒有捕獲了auto變量
        };
        
        void(^block2)(void) = ^(void) {
            NSLog(@"age = %d", age);   // 捕獲了auto變量
        };
        
        void(^block3)(void) = ^(void) {
            NSLog(@"height = %d", height);   // 捕獲了static變量
        };
        
        NSLog(@"block1 class: %@", [block1 class]);             // __NSGlobalBlock__
        NSLog(@"block2 class: %@", [block2 class]);             // __NSStackBlock__
        NSLog(@"block2 copy class: %@", [[block2 copy] class]); //__NSMallocBlock__
        NSLog(@"block3 class: %@", [block3 class]);             //__NSGlobalBlock__
    }
    return 0;
}

// 輸出結果:
2020-07-28 20:41:43.283331+0800 LearningBlock[40390:637401] block1 class: __NSGlobalBlock__
2020-07-28 20:41:43.283755+0800 LearningBlock[40390:637401] block2 class: __NSStackBlock__
2020-07-28 20:41:43.283877+0800 LearningBlock[40390:637401] block2 copy class: __NSMallocBlock__
2020-07-28 20:41:43.283924+0800 LearningBlock[40390:637401] block3 class: __NSGlobalBlock__
複製代碼

由上可知:

  1. block類型取值以下:

    • 沒有捕獲auto變量,那麼block的爲__NSGlobalBlock__類型。
    • 捕獲了auto變量,那麼block__NSStackBlock__類型。
    • __NSStackBlock__類型的block進行copy操做,則block就會變成__NSMallocBlock__ 類型.

  2. block 這幾種類型的主要區別是:在內存中的存放區域不一樣。(即生命的週期不一樣)

    • __NSGlobalBlock__ 存在數據段。
    • __NSStackBlock__ 存放在棧空間。
    • __NSMallocBlock__ 存放在堆空間。

檢驗題:

新建一個Person類, 以下:

@interface Person : NSObject

@property (nonatomic, strong) NSString *name;

- (void)test;

@end

@implementation Person

- (void)test {
    void (^block)(void) = ^{
        NSLog(@"person name = %@", _name);
    };
}
@end
複製代碼

問題: 在Person.mtest方法中的blockself有沒有進行捕獲呢?

答案是有,block會捕獲self. 分析以下:

首先將Person.m 經過xcrun命令轉換爲C++, 獲得以下內容:

//test 方法內的block方法
struct __Person__test_block_impl_0 {
  struct __block_impl impl;
  struct __Person__test_block_desc_0* Desc;
  Person *self;
  __Person__test_block_impl_0(void *fp, struct __Person__test_block_desc_0 *desc, Person *_self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

//test 方法
static void _I_Person_test(Person * self, SEL _cmd) {
    void (*block)(void) = ((void (*)())&__Person__test_block_impl_0((void *)__Person__test_block_func_0, &__Person__test_block_desc_0_DATA, self, 570425344));

    NSLog((NSString *)&__NSConstantStringImpl__var_folders_hc_wwwl26516td3w0ds9cx80c280000gp_T_Person_14871d_mi_1, ((Class (*)(id, SEL))(void *)objc_msgSend)((id)block, sel_registerName("class")));
}
複製代碼

觀察轉換後的代碼能夠看到:

  1. 咱們日常寫的OC方法,其實默認就有隱藏的兩個參數,(Person *self, SEL _cmd), 分別是方法的調用者 self方法選擇器 sel
  2. 方法的參數通常是局部變量,block會對局部變量進行捕獲的。

Block的copy操做

咱們平常使用的block通常是__NSMallocBlock__類型的,緣由有以下:

  • 對於__NSGlobalBlock__類型的block, 由於沒有捕獲auto變量, 因此正常通常都是直接使用函數實現。
  • 對於__NSStackBlock__類型的block, 由於其存放在棧上,其內部使用變量容易被系統回收掉,從而致使一些異常的狀況。好比下面:(要先將項目切成MRC,由於ARC下編譯器會根據狀況作copy操做,會影響分析)
typedef void (^MJBlock)(void);
MJBlock block;
void test() {
    int a = 10;  // test方法結束後,a的內存就被回收了。
    block = ^(void) {
        NSLog(@"a = %d", a);
    };
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        test();
        block();   // block裏打印的是被回收了的a
    }
    return 0;
}
輸出結果:
2020-09-27 10:05:28.616920+0800 Interview01-block的copy[7134:29679] a = -272632776
複製代碼
  • 對於__NSMallocBlock__類型的block, 由於它是存儲在堆上,因此就不存在__NSStackBlock__類型block的問題。

上面演示的是在MRC環境下的, 那麼在ARC環境下又是如何的呢?

ARC環境下,編譯器會自動根據狀況將棧上的block複製到堆上。好比一下狀況:

  • block做爲函數返回值時。
  • block賦值給__strong指針時。
  • block做爲Cocoa API中方法名含有usingBlock的方法參數時。
  • block做爲GCD API方法參數時。
typedef void (^MJBlock)(void);
MJBlock myblock()
{
    int a = 10;
    return ^{
        NSLog(@"--------- %d", a);   // 1. 做爲方法返回值時。會自動copy
    };
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 10;
        MJBlock block = ^{    // 2.賦值給strong指針時,會自動copy
            NSLog(@"---------%d", age);
        };
        
        NSArray *arr = @[@10, @20];
        [arr enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            // 3. block做爲Cocoa API中方法名含有usingBlock的方法參數時。會自動copy
        }];

        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            // 4. block做爲GCD API方法參數時。會自動copy
        });
    }
    return 0;
}
複製代碼

根據上面的狀況,在MRCARCblock屬性的寫法能夠有差別:

MRC下block屬性的建議寫法
@property (copy, nonatomic) void (^block)(void);  // 賦值時會自動copy到堆上

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

對象類型的auto變量

基本數據類型auto變量咱們已經分析了,那麼對象類型auto變量是否是和基本數據類型的同樣仍是有什麼特別之處呢?下面咱們一塊兒來分析下:(記得先將工程切回ARC模式)

以下代碼:

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

@implementation LCPerson
- (void)dealloc {
    NSLog(@"%s", __func__);   // 銷燬代碼
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"11111111");
        
        {
            LCPerson *person = [[LCPerson alloc] init];
            person.age = 10;
        }
        
        NSLog(@"22222222");
    }
    return 0;
}

// 輸出結果:
2020-09-27 10:36:43.856070+0800 LearningBlock[16016:56873] 11111111
2020-09-27 10:36:43.856442+0800 LearningBlock[16016:56873] -[LCPerson dealloc]
2020-09-27 10:36:43.856474+0800 LearningBlock[16016:56873] 22222222
複製代碼

咱們定義了一個LCPerson類,在main.m中作測試,由輸出結果能夠看出,person對象的釋放是在111111122222222之間, 這咱們應該均可以理解。(局部做用域)

咱們繼續~

加入Block以後,咱們再觀察一下。

typedef void (^MyBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"11111111");
        
        MyBlock block;
        {
            LCPerson *person = [[LCPerson alloc] init];
            person.age = 10;
            
            block = ^(void){
                NSLog(@"person age = %d", person.age);
            };
        }
        
        NSLog(@"22222222");
    }
    
    NSLog(@"3333333");
    return 0;
}

輸出結果:

2020-09-27 10:52:27.578241+0800 LearningBlock[20478:70040] 11111111
2020-09-27 10:52:27.578627+0800 LearningBlock[20478:70040] 22222222
2020-09-27 10:52:27.578688+0800 LearningBlock[20478:70040] -[LCPerson dealloc]
2020-09-27 10:52:27.578729+0800 LearningBlock[20478:70040] 3333333
複製代碼

根據結果,咱們能夠發現加入了block以後,person的銷燬是在222222以後發生的,即person所在的做用域結束後,person對象沒有當即釋放。 那麼block究竟對person幹了什麼,致使person對象沒能及時釋放呢? 爲了分析,咱們將上面的代碼先簡化一下。簡化以下

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LCPerson *person = [[LCPerson alloc] init];
        person.age = 10;
        
        void (^block)(void) = ^(void){
            NSLog(@"person age = %d", person.age);
        };
        
        block();
    }
    return 0;
}
複製代碼

將上面OC代碼轉換爲C++代碼:(支持ARC、指定運行時系統版本)

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

轉換後的C++代碼以下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  LCPerson *__strong person; // strong類型的指針
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, LCPerson *__strong _person, int flags=0) : person(_person) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  LCPerson *__strong person = __cself->person; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_vt_j2sf07q142992_z55yg_170w0000gp_T_main_5882d6_mi_0, ((int (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("age")));
        }
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*/);}

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; 

        LCPerson *person = ((LCPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((LCPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LCPerson"), sel_registerName("alloc")), sel_registerName("init"));
        ((void (*)(id, SEL, int))(void *)objc_msgSend)((id)person, sel_registerName("setAge:"), 10);

        void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, person, 570425344)); 

        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}
複製代碼

經過觀察能夠發現,block內部對person進行了捕獲。而且與捕獲基本數據類型的auto變量不一樣的是,捕獲對象類型時__main_block_desc_0結構體多了兩個函數,分別是copydispose,這兩個函數與被捕獲對象的引用計數的處理有關。

  • block上拷貝到上時,copy函數被調用,接着它會調用_Block_object_assign函數,處理被捕獲對象的引用計數,若是捕獲變量時是使用__strong修飾,那麼對象的引用計數就會+1. 若是捕獲時是__weak修飾,則引用計數不變。(下面會驗證)
  • block被回收,即釋放時,dispose函數被調用,接着它會調用_Block_object_dispose函數,若是捕獲變量時是使用__strong修飾,那麼對象的引用計數就會-1. 若是捕獲變量時是__weak修飾,則引用計數不變。(下面會驗證)

咱們知道,在ARC環境下,將block賦值給__strong指針,block會自動調用copy函數。因此 person對象離開了局部做用域後沒有釋放的緣由就很明確了,是由於block調用copy函數時,將person對象的引用計數增長了1,因此當局部做用域結束時,person對象的引用計數並不爲0,所以不會釋放。 而當block的做用域結束,block調用dispose函數,將person的引用計數減爲0,而後person纔會釋放。

如上面所說,那若是是在MRC環境下,person對象離開局部做用域後就會銷燬了, 由於在MRC環境下,將block賦值給__strong指針是不會觸發copy函數的,因此person對象應該能夠正常釋放。

驗證一: 將工程切換到MRC模式下,測試剛纔的代碼,以下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"11111111");
        
        MyBlock block;
        {
            LCPerson *person = [[LCPerson alloc] init];
            person.age = 10;
            
            block = ^(void){
                NSLog(@"person age = %d", person.age);
            };
            
            [person release]; // MRC下須要手動管理內存
        }
        
        NSLog(@"22222222");
    }
    
    NSLog(@"3333333");
    return 0;
}

// 輸出結果:
2020-09-27 11:39:05.493388+0800 LearningBlock[33422:105156] 11111111
2020-09-27 11:39:05.493800+0800 LearningBlock[33422:105156] -[LCPerson dealloc]
2020-09-27 11:39:05.493833+0800 LearningBlock[33422:105156] 22222222
2020-09-27 11:39:05.493857+0800 LearningBlock[33422:105156] 3333333
複製代碼

觀察輸出結果,和預料中的同樣。person對象離開局部做用域後正常釋放。

驗證二:weak修飾的對象類型的auto變量. (記得切回ARC環境)

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"11111111");
        
        MyBlock block;
        {
            LCPerson *person = [[LCPerson alloc] init];
            person.age = 10;
            
            // 弱指針
            __weak LCPerson *weakPerson = person;
            block = ^(void){
                NSLog(@"person age = %d", weakPerson.age);
            };
        }
        
        NSLog(@"22222222");
    }
    
    NSLog(@"3333333");
    return 0;
}
// 輸出結果:
2020-09-27 12:00:20.461929+0800 LearningBlock[39325:122309] 11111111
2020-09-27 12:00:20.462321+0800 LearningBlock[39325:122309] -[LCPerson dealloc]
2020-09-27 12:00:20.462361+0800 LearningBlock[39325:122309] 22222222
2020-09-27 12:00:20.462391+0800 LearningBlock[39325:122309] 3333333
複製代碼

觀察輸出結果,和預料中的同樣。person對象離開局部做用域後正常釋放。

總結:

  • block內部訪問了對象類型的auto變量時

    • 若是block是在棧上,將不會對auto變量產生強引用
  • 若是block被拷貝到堆上

    • 會調用block內部的copy函數
    • copy函數內部會調用_Block_object_assign函數
    • _Block_object_assign函數會根據auto變量的修飾符(__strong__weak__unsafe_unretained)作出相應的操做,造成強引用(retain)或者弱引用
  • 若是block從堆上移除

    • 會調用block內部的dispose函數
    • dispose函數內部會調用_Block_object_dispose函數
    • _Block_object_dispose函數會自動釋放引用的auto變量(release

__block修飾符

  • __block能夠用於解決block內部沒法修改auto變量值的問題
  • __block不能修飾全局變量靜態變量static
  • 編譯器會將__block變量包裝成一個對象.

下面一塊兒驗證一下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block int a = 10;
        void (^block)(void) = ^{
            a = 20;
            NSLog(@"a = %d", a);
        };
        block();
    }
    return 0;
}
// 輸出結果:
a = 20
複製代碼

將上面OC代碼轉換爲C++代碼:(支持ARC、指定運行時系統版本)

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

獲得轉換後結果:

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

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref 這就捕獲到的a的引用
  __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; // bound by ref

            (a->__forwarding->a) = 20;     // 修改值a的值。
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_vt_j2sf07q142992_z55yg_170w0000gp_T_main_ca9eb0_mi_0, (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, 0, sizeof(__Block_byref_a_0), 10};  // 這就是__block 修飾的a變量。

        void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));  // 傳入的是a變量的地址。

        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}
複製代碼

由上面能夠看到,OC代碼 __block int a = 10 轉換爲C++以後變爲了:

__Block_byref_a_0 a = {0, &a, 0, sizeof(__Block_byref_a_0), 10};
複製代碼

__Block_byref_a_0是一個結構體,結構以下:

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
}
複製代碼

因此在OC中用__block修飾一個變量, 編譯器會自動生成一個全新的OC對象。

__block的內存管理

__blockblock中的內存管理和對象類型的auto變量相似(但也有區別)。

  • block在棧上時,並不會對__block變量產生強引用
  • blockcopy到堆時
    • 會調用block內部的copy函數
    • copy函數內部會調用_Block_object_assign函數
    • _Block_object_assign函數會對__block變量造成強引用(retain)。(這點就是和對象類型的auto變量有區別的地方,對於對象類型的auto變量, _Block_object_assign函數會根據auto變量的修飾符(__strong__weak__unsafe_unretained)作出相應的操做, 而__block則是直接強引用 )

  • block從堆中移除時
    • 會調用block內部的dispose函數
    • dispose函數內部會調用_Block_object_dispose函數
    • _Block_object_dispose函數會自動釋放引用的__block變量(release

__block的__forwarding指針

被__block修飾的對象類型

經過上面咱們知道了用__block修飾的基本數據類型的處理。那用__block修飾的對象類型的處理是否是同樣的呢? 下面咱們一塊兒看下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        __block LCPerson *person = [[LCPerson alloc] init];
        person.age = 10;
        
        void(^block)(void) = ^(void) {
            NSLog(@"person age %d", person.age);
        };
        block();
    }
    return 0;
}
複製代碼

經過xcrun命令:

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

轉換成C++後,獲得結果以下:

struct __Block_byref_person_0 {
  void *__isa;
__Block_byref_person_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);  // 管理person的內存
 void (*__Block_byref_id_object_dispose)(void*);      // 管理person的內存
 LCPerson *__strong person;   //arc環境下, copy 和 dispose函數,會根據person的修飾類型(__strong、__weak)來對person作相應的內存管理。
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_person_0 *person; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_person_0 *_person, int flags=0) : person(_person->__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_person_0 *person = __cself->person; // bound by ref // 這裏就是強引用

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_hc_wwwl26516td3w0ds9cx80c280000gp_T_main_213c56_mi_0, ((int (*)(id, SEL))(void *)objc_msgSend)((id)(person->__forwarding->person), sel_registerName("age")));
        }
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, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->person, 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_person_0 person = {(void*)0,(__Block_byref_person_0 *)&person, 33554432, sizeof(__Block_byref_person_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((LCPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((LCPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LCPerson"), sel_registerName("alloc")), sel_registerName("init"))};
        ((void (*)(id, SEL, int))(void *)objc_msgSend)((id)(person.__forwarding->person), sel_registerName("setAge:"), 10);

        void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_person_0 *)&person, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}
複製代碼

block拷貝到上時,會調用blockcopy方法,同時還會調用__Block_byref_person_0結構體裏的__Block_byref_id_object_copy方法,__Block_byref_id_object_copy內部會調用_Block_object_assign方法,處理結構體__Block_byref_person_0內部的person指針所指對象的引用計數。

總結以下:

  • __block變量在棧上時,不會對指向的對象產生強引用

  • __block變量被copy到堆時

    • 會調用__block變量內部的copy函數
    • copy函數內部會調用_Block_object_assign函數
    • _Block_object_assign函數會根據所指向對象的修飾符(__strong__weak__unsafe_unretained)作出相應的操做,造成強引用(retain)或者弱引用(注意:這裏僅限於ARC時會retain,MRC時不會retain
  • 若是__block變量從堆上移除

    • 會調用__block變量內部的dispose函數
    • dispose函數內部會調用_Block_object_dispose函數
    • _Block_object_dispose函數會自動釋放指向的對象(release

對象類型的 auto變量 和 __block變量處理的異同:

  • 當block在棧上時,對它們都不會產生強引用

  • 當block拷貝到堆上時,都會經過copy函數來處理它們

    • __block變量(假設變量名叫作a)

    • _Block_object_assign((void*)&dst->a, (void*)src->a, 8/BLOCK_FIELD_IS_BYREF/);

    • 對象類型的auto變量(假設變量名叫作p)

    • _Block_object_assign((void*)&dst->p, (void*)src->p, 3/BLOCK_FIELD_IS_OBJECT/);

  • 當block從堆上移除時,都會經過dispose函數來釋放它們

    • __block變量(假設變量名叫作a)

    • _Block_object_dispose((void*)src->a, 8/BLOCK_FIELD_IS_BYREF/);

    • 對象類型的auto變量(假設變量名叫作p)

    • _Block_object_dispose((void*)src->p, 3/BLOCK_FIELD_IS_OBJECT/);

循環引用問題

在開發過程當中咱們常常會遇到block循環引用的問題, 以下:

typedef void (^MyBlock)(void);

@interface LCPerson : NSObject
@property (nonatomic, assign) int age;
@property (nonatomic, copy) MyBlock block;
@end

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

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

// 輸出結果:
2020-09-28 20:01:48.358822+0800 LearningBlock[41115:298402] 211212121122
複製代碼

由打印結果能夠看出,person並無釋放(沒有調用person的dealloc方法)。那是什麼緣由致使的呢?是循環引用。 下面咱們來分析一下:

  • @property (nonatomic, copy) MyBlock block;從這句話能夠看出,person 強引用着block.
  • block內部訪問了person對象的age屬性,根據上面所學,咱們知道block會對person進行捕獲,而且在arc環境下,block賦值給__strong指針時會自動調用copy方法,將block從棧拷貝到堆上, 這樣會致使person的引用計數加1,即block強引用着person

因此personblock相互強引用着,出現了循環引用,因此person對象不會釋放。

那麼該如何解決呢? 下面說下在ARC環境和MRC環境分別如何處理?

解決循環引用問題 - ARC

ARC環境下,咱們能夠經過使用關鍵字__weak__unsafe_unretained來解決。以下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        LCPerson *person = [[LCPerson alloc] init];
        person.age = 10;

        __weak LCPerson *weakPerson = person;
        // 或者 __unsafe_unretained LCPerson *weakPerson = person;
        person.block = ^{
            NSLog(@"person age %d", weakPerson.age);
        };
        
        NSLog(@"211212121122");
    }
    return 0;
}
// 打印結果:
2020-09-28 20:30:19.659679+0800 LearningBlock[41212:307877] 211212121122
2020-09-28 20:30:19.660256+0800 LearningBlock[41212:307877] -[LCPerson dealloc]
複製代碼

示意圖以下:

還可使用__block解決, 以下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        __block LCPerson *person = [[LCPerson alloc] init];
        person.age = 10;
        
        person.block = ^{
            NSLog(@"person age %d", person.age);
            person = nil;
        };
        
        person.block();  // 必須調用
        NSLog(@"211212121122");
    }
    return 0;
}

// 打印結果:
2020-09-28 20:35:32.531704+0800 LearningBlock[41256:310297] person age 10
2020-09-28 20:35:32.532221+0800 LearningBlock[41256:310297] -[LCPerson dealloc]
2020-09-28 20:35:32.532310+0800 LearningBlock[41256:310297] 211212121122
複製代碼

使用__block解決,必須調用block,否則沒法將循環引用打破。

疑問: __weak__unsafe_unretained關鍵字有什麼區別呢?

使用__weak__unsafe_unretained關鍵字都能達到弱引用的效果。這二者主要的區別在於,使用__weak關鍵字修飾的指針,在所指的對象銷燬時,指針存儲的地址會被清空(即置爲nil), 而__unsafe_unretained則不會。

解決循環引用問題 - MRC

  • MRC環境是沒有__weak關鍵字的,因此可使用__unsafe_unretained關鍵字解決。(與ARC差很少,這裏就不演示了)
  • 一樣也能夠是__block關鍵字解決。以下:
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block LCPerson *person = [[LCPerson alloc] init];
        person.age = 10;
        
        person.block = ^{
            NSLog(@"person age %d", person.age);
            person = nil;
        };
        
        [person release];  // MRC須要手動添加內存管理代碼
        NSLog(@"211212121122");
    }
    return 0;
}
複製代碼

ARC不一樣的是,MRC下使用__block解決循環引用問題,不要求必定要調用block。緣由上面__block修飾的對象類型裏有說到:

_Block_object_assign函數會根據所指向對象的修飾符(__strong__weak__unsafe_unretained)作出相應的操做,造成強引用(retain)或者弱引用(注意:這裏僅限於ARC時會retain,MRC時不會retain

後話

這篇文章有點亂,還有待改進。寫博客真的費時間,不過能加深印象,也不錯。

輕鬆一刻: image

參考

  1. MJ 底層原理
相關文章
相關標籤/搜索