iOS基礎(chǔ)(六) - 弄懂Object-C中block的實現(xiàn)

banner.jpg

前言:之前寫swift進階(二) - 閉包(Closure),看了大量關(guān)于block的文檔,尋思著把自己這段時間關(guān)于block的理解,整理出來,以后忘了還能找回來。??

1.基礎(chǔ)知識

鞏固一下基礎(chǔ)知識,大學學的差不多都還給老師了。??

  • 指針與地址:p VS *p VS &p
    看一段代碼:
char *p = "abc";
NSLog(@"&p: %p----p: %p----*p: %c", &p, p, *p);
char **p1 = &p;
NSLog(@"&p1: %p----p1: %p----*p1: %s----**p1: %c", &p1, p1, *p1, **p1);
//&p: 0x7ffeefbff5a8----p: 0x100001cad----*p: a
//&p1: 0x7ffeefbff5a0----p1: 0x7ffeefbff5a8----*p1: abc----**p1: a

&是取址操作,以上面代碼為例:p是指向字符數(shù)組的指針,&p則是指針p在內(nèi)存的地址,*p是指針p指向內(nèi)存地址的存放內(nèi)容,這里是字符數(shù)組的首地址也是第一個字符的內(nèi)存地址,所以返回的是a,而*p1返回abc,是因為*p1其實就是p所指向內(nèi)存的內(nèi)容,所以返回的是abc。下面p1是指針的指針,p1是指向指針p的指針,所以,p,*p和&p的關(guān)系如下:


p|*p|&p.png

驗證一下:

NSLog(@"p1: %p == &(*p1): %p", p1, &(*p1));
NSLog(@"&(*p1): %p == &p: %p", &(*p1), &p);
//p1: 0x7fff5fbff708 == &(*p1): 0x7fff5fbff708
//&(*p1): 0x7fff5fbff708 == &p: 0x7fff5fbff708
//滿足p = &(*p)

注意:引用類型(對象)和值類型(基本數(shù)據(jù)類型,如int,float等等)的儲存不一樣,引用類型初始化返回的是指向該類型分配內(nèi)存地址的指針,而基本數(shù)據(jù)類型返回的是改類型分配的內(nèi)存地址。

  • iOS編譯的程序占用內(nèi)存分布的結(jié)構(gòu)
    程序占用內(nèi)存的分布結(jié)構(gòu)如下圖:(圖片來源
    內(nèi)存結(jié)構(gòu).jpg

    棧區(qū):一般存放函數(shù)參數(shù),局部變量等值,由系統(tǒng)自動分配和管理,程序員不必關(guān)心。存放里面的數(shù)據(jù),遵從先進后出的原則。
    堆區(qū):由程序員申請,管理和內(nèi)存回收。數(shù)據(jù)儲存的結(jié)構(gòu)是鏈表。
    全局區(qū)/靜態(tài)區(qū):儲存全局變量和靜態(tài)變量。
    文字常量區(qū):主要儲存字符串常量。
    程序代碼區(qū):存放程序的二進制代碼。
    舉個例子:(代碼來源
//main.cpp
int a = 0; // 全局初始化區(qū)
char *p1; // 全局未初始化區(qū)
main {
    int b; // 棧
    char s[] = "abc"; // 棧,字符串常量一般在文字常量區(qū),但是該字符串從文字常量區(qū)復制到了棧區(qū)
    char *p2; // 棧
    char *p3 = "123456"; // 123456\0在常量區(qū),p3在棧上
    static int c =0; // 全局靜態(tài)初始化區(qū)
    p1 = (char *)malloc(10);
    p2 = (char *)malloc(20); // 分配得來的10和20字節(jié)的區(qū)域就在堆區(qū)
    strcpy(p1, "123456"); // 123456\0在常量區(qū),這個函數(shù)的作用是將"123456" 這串字符串復制一份放在p1申請的10個字節(jié)的堆區(qū)域中。
    // p3指向的"123456"與這里的"123456"可能會被編譯器優(yōu)化成一個地址。
}

內(nèi)存結(jié)構(gòu)從低地址端到高地址端:


內(nèi)存結(jié)構(gòu)地址.png

2.block數(shù)據(jù)結(jié)構(gòu)

block的數(shù)據(jù)結(jié)構(gòu)定義如下圖:(圖片來源

block-struct.jpg

下面來驗證一下block的數(shù)據(jù)結(jié)構(gòu),看一段代碼:

NSString *str = @"hello";
void(^block)() = ^{
        NSLog(@"%@", str);
};
block();

用clang命令反編譯Object-C文件成C++源文件,結(jié)果如下:

//字符串str初始化
NSString *str = (NSString *)&__NSConstantStringImpl__var_folders_pn_xqk2zmnx559bmvnwm35rzb4h0000gn_T_main_2cb64e_mi_0;
//block定義
void(*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, str, 570425344));
//block調(diào)用
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

可以看出,block是一個函數(shù)指針,指向_main_block_impl_0的內(nèi)存地址,并且_main_block_imp_0傳入了__main_block_func_0一個空的函數(shù)指針,定義了block里的操作;__main_block_desc_0_DATA數(shù)據(jù)描述;str指針,指向初始化字符串的內(nèi)存地址,這個后面寫block里如何修該外面的值的時候會提到;還有一個標識碼。
上面提到了__main_block_impl_0__main_block_func_0__main_block_desc_0_DATA,下面一個一個看一下它們是什么。

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

//__main_block_func_0
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  NSString *str = __cself->str; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_pn_xqk2zmnx559bmvnwm35rzb4h0000gn_T_main_2cb64e_mi_1, str);
        }

//__main_block_desc_0_DATA
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_impl
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

block初始化返回來的是結(jié)構(gòu)體 __main_block_impl_0的內(nèi)存地址,該結(jié)構(gòu)體的構(gòu)造函數(shù)接收了四個參數(shù),分別是__main_block_func_0,__main_block_desc_0_DATA,str和標識符。因為block里捕獲的是該變量指向的內(nèi)存地址,而不是直接把當前的對象的地址,所以,理論上是兩個指向同一個內(nèi)存地址的變量,所以,修改block里面的變量地址,并不能改變外面的變量,編譯器就干脆禁止了??匆幌?strong>__block_impl這個結(jié)構(gòu)體,是不是和圖block-struct中的Block_layout很像,把結(jié)構(gòu)體__block_impl里面的變量接在結(jié)構(gòu)體__main_block_impl_0上,就一模一樣了,Block_descriptor結(jié)構(gòu)體就是__main_block_desc_0結(jié)構(gòu)體,主要儲存block的大小等信息。再看一下__main_block_func_0,相信大家都看出來了吧,這就是block執(zhí)行的操作,打印獲取的str。

3.block的類型

  1. _NSConcreteGlobalBlock全局靜態(tài)block,不訪問任何外部變量
  2. _NSConcreteStackBlock保存在棧中的block,當函數(shù)返回時會被銷毀
  3. _NSConcreteMallocBlock保存在堆中的block,當引用計數(shù)為0時會被銷毀

舉例說明:
_NSConcreteGlobalBlock

void(^block)() = ^{
        };
block();
NSLog(@"%@", block);    
//<__NSGlobalBlock__: 0x1000030d0>

上面的block在ARC和非ARC打印出來都是NSGlobalBlock,但是clang一下C++源文件確是_NSConcreteStackBlock,不清楚為什么?有可能編譯出的偏差也有可能是蘋果系統(tǒng)做了什么,知道的同學請告訴我一聲,感激不盡。

_NSConcreteStackBlock/ _NSConcreteMallocBlock

NSString *str = @"hello";
void(^block)() = ^{
       NSLog(@"%@", str);
};
block();
NSLog(@"%@", block);
//ARC:<__NSMallocBlock__: 0x100208f40>
//非ARC:<__NSStackBlock__: 0x7fff5fbff6d8>

ARC環(huán)境下會自動把棧里的block復制到堆里,非ARC環(huán)境下需要自己調(diào)用copy,復制到堆里,例:Block myBlock = [[block copy] autorelease]。

4.block獲取外部變量

1.沒有__block修飾的外部變量,block里面不能修改
在前面講block的結(jié)構(gòu)的時候,提到block里面獲取的變量并不是外部變量的本身內(nèi)存地址,而是指向的內(nèi)存地址,下面代碼驗證一下:

NSString *str = @"hello";
NSLog(@"before----&str: %p----str: %p", &str, str);
void(^block)() = ^{
        NSLog(@"block----&str: %p----str: %p", &str, str);
};
block();
NSLog(@"after----&str: %p----str: %p", &str, str);
2017-03-27 10:05:33.557243 BasicTest[35056:2385069] before----&str: 0x7fff5fbff708----str: 0x100003100
2017-03-27 10:05:33.557494 BasicTest[35056:2385069] block----&str: 0x7fff5fbff6f8----str: 0x100003100
2017-03-27 10:05:33.557516 BasicTest[35056:2385069] after----&str: 0x7fff5fbff708----str: 0x100003100

block里面的str內(nèi)存地址和外部變量不一樣,但是block里面的str指向的內(nèi)存地址則和外部str指向的內(nèi)存地址一樣,所以我們修改不了外部變量的內(nèi)存地址。
2.有__block修飾的外部變量,block里面可以修改外部變量的內(nèi)存地址
代碼驗證:

__block NSString *str = @"hello";
NSLog(@"before----&str: %p----str: %p", &str, str);
void(^block)() = ^{
        NSLog(@"block----&str: %p----str: %p", &str, str);
        str = nil;
};
block();
NSLog(@"after----&str: %p----str: %p", &str, str);
2017-03-27 10:10:42.929639 BasicTest[35092:2391935] before----&str: 0x7fff5fbff708----str: 0x100003110
2017-03-27 10:10:42.929831 BasicTest[35092:2391935] block----&str: 0x7fff5fbff708----str: 0x100003110
2017-03-27 10:10:42.929853 BasicTest[35092:2391935] after----&str: 0x7fff5fbff708----str: 0x0

外部變量使用__block修飾之后,block里面訪問的變量地址和外部變量的地址是一樣的,也就是說,外部變量和block里面的捕獲的變量是同一個,所以修改block里面的變量地址,外部的變量地址也會發(fā)生變化。這到底是怎么實現(xiàn)的呢?我們反編譯一下面的代碼:

__block NSMutableString *str = [NSMutableString stringWithString: @"hello"];
__block NSInteger number = 1;
void(^block)() = ^{
       NSLog(@"str: %@----number: %ld", str, number);
};
block();

得到:

//str
__attribute__((__blocks__(byref))) __Block_byref_str_0 str = {(void*)0,(__Block_byref_str_0 *)&str, 33554432, sizeof(__Block_byref_str_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSMutableString *(*)(id, SEL, NSString *))(void *)objc_msgSend)((id)objc_getClass("NSMutableString"), sel_registerName("stringWithString:"), (NSString *)&__NSConstantStringImpl__var_folders_pn_xqk2zmnx559bmvnwm35rzb4h0000gn_T_main_95b039_mi_0)};
//number
__attribute__((__blocks__(byref))) __Block_byref_number_1 number = {(void*)0,(__Block_byref_number_1 *)&number, 0, sizeof(__Block_byref_number_1), 1};
//block
void(*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_str_0 *)&str, (__Block_byref_number_1 *)&number, 570425344));
//block()
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

__block將字符串str和整形number變成了__Block_byref_str_0和__Block_byref_number_1類型,并且block傳進去的是&str和&number,也就是外部變量的內(nèi)存地址,block里捕獲的外部變量就是外部變量本身,所以,block里面可以修改外部變量的地址。我們來看一下__Block_byref_str_0和__Block_byref_number_1是什么類型:

struct __Block_byref_str_0 {
  void *__isa;
__Block_byref_str_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 NSMutableString *str;
};
struct __Block_byref_number_1 {
  void *__isa;
__Block_byref_number_1 *__forwarding;
 int __flags;
 int __size;
 NSInteger number;
};

它們都是保存了block捕獲的外部變量內(nèi)存地址的結(jié)構(gòu)體,Object-C中,帶有isa指針的結(jié)構(gòu)體可以看作對象,所以,它們都是對象。

打印一下__block修飾的變量,編譯成C代碼,會看到:

//Objective-C
__block NSInteger number = 123;
NSLog(@"number2: %ld", number);

//C
__attribute__((__blocks__(byref))) __Block_byref_number_0 number = {(void*)0,(__Block_byref_number_0 *)&number, 0, sizeof(__Block_byref_number_0), 123};
NSLog((NSString *)&__NSConstantStringImpl__var_folders_pn_xqk2zmnx559bmvnwm35rzb4h0000gn_T_main_7d3b3c_mi_1, (number.__forwarding->number));

可以看到,number并不是直接訪問__Block_byref_number_0結(jié)構(gòu)體里面的number,而是通過指向本身的指針__forwarding->number獲取對應的值。那么?這個有什么意義?為什么要再繞一圈取值?別急,先來看一組代碼:

//ARC
@interface Person : NSObject
@property (nonatomic, assign) void(^block)(void);
@end

@implementation Person
- (void)test {
    self.object = [NSObject new];
    NSLog(@"%p--retainCount1: %ld", &self, CFGetRetainCount((CFTypeRef)self));
    self.block = ^{
        NSLog(@"%p--retainCount2: %ld", &self, CFGetRetainCount((CFTypeRef)self));
    };
    self.block();
    NSLog(@"%p--retainCount3: %ld", &self, CFGetRetainCount((CFTypeRef)self));
}
@end

//輸出
2018-04-12 15:00:07.908685+0800 BasicDemo[18910:2888235] 0x7ffeefbff538--retainCount1: 1
2018-04-12 15:00:07.909099+0800 BasicDemo[18910:2888235] 0x7ffeefbff528--retainCount2: 2
2018-04-12 15:00:07.909129+0800 BasicDemo[18910:2888235] 0x7ffeefbff538--retainCount3: 2

將block的修飾關(guān)鍵字換成copy,再編譯
//輸出
2018-04-12 15:02:42.246133+0800 BasicDemo[18923:2891037] 0x7ffeefbff538--retainCount1: 1
2018-04-12 15:02:42.246526+0800 BasicDemo[18923:2891037] 0x100526c40--retainCount2: 3
2018-04-12 15:02:42.246557+0800 BasicDemo[18923:2891037] 0x7ffeefbff538--retainCount3: 3

看到了什么?嘿嘿,當使用assign修飾block時,該block不會被復制到堆上去,所以,該block捕獲的self會放在棧上,所以retain了一下當前對象,retainCount=2;當使用copy修飾block時,該block會被復制到堆上去,所以,該block先捕獲的self放在棧上,然后被復制到堆上再次retain當前對象,所以retainCount=3。想一想使用assign修飾的block會不會造成循環(huán)引用?下面解釋循環(huán)引用再回答。

回到上一個問題:為什么用_forwarding -> number?
上面解釋了,當用copy/strong修飾block時,block不僅在棧上,還會被復制到堆上,那么會有兩個block對象。所以,為了保證訪問的變量都是同一個,就會把棧上的_forwarding指向堆block里的對象,保證兩個block都是同一個對象。

5.block在ARC和MRC的區(qū)別

ARC和MRC最主要的區(qū)別就是,ARC是系統(tǒng)自動為對象插入-retain和-release方法,MRC則需要使用者自己插入。先上代碼:

NSMutableString *str = [NSMutableString stringWithString: @"hello"];
NSInteger number = 1;
NSLog(@"&str: %p----str: %p----&number: %p", &str, str, &number);
void(^block)() = ^{
        NSLog(@"&str: %p----str: %p----&number: %p", &str, str, &number);
};
block();
//
//MRC下的輸出:
2017-03-27 11:28:11.835575 BasicTest[36692:2513331] &str: 0x7fff5fbff708----str: 0x100209080----&number: 0x7fff5fbff700
2017-03-27 11:28:11.835770 BasicTest[36692:2513331] &str: 0x7fff5fbff6e8----str: 0x100209080----&number: 0x7fff5fbff6f0
//block外部:&str存放在棧區(qū),而str則存放在堆區(qū)(因為&str內(nèi)存地址比str要大很多,棧區(qū)在高地址,堆區(qū)在低地址,并且一般棧區(qū)比較小,1~2M左右,這個不用的操作系統(tǒng)分配的內(nèi)存大小不一樣,但是不會差很遠)
//block內(nèi)部:&str地址也在棧區(qū),但是和外部的&str地址不一樣,但是str的地址確實一樣的,這也驗證了外部變量在沒有__block關(guān)鍵字修飾的情況下,block里捕獲該變量會復制一份,而不是直接引用
//&number也一樣
//
//ARC下的輸出:
2017-03-27 11:35:26.885352 BasicTest[36731:2523310] &str: 0x7fff5fbff708----str: 0x100302f30----&number: 0x7fff5fbff700
2017-03-27 11:35:26.885547 BasicTest[36731:2523310] &str: 0x100400170----str: 0x100302f30----&number: 0x100400178
//block里面捕獲的變量也會復制一份,和MRC環(huán)境下一樣,但是不同的是,復制的內(nèi)存地址從棧區(qū)跑到了堆區(qū)
//&str = 0x100400170. &number = 0x100400178
//因此,該block不會隨著函數(shù)的調(diào)用而被釋放掉,而MRC則需要自己把block從棧區(qū)copy到堆區(qū)

注意:
__block在MRC下不會retain對象,在ARC下會retain對象,MRC可以通過__block來解決循環(huán)引用問題。

6.block的循環(huán)引用

先看圖:


cycle-retain.png

上面最簡單的循環(huán)引用,A引用B,B引用A或者A對象內(nèi)部擁有block,block里對A強引用。還有更復雜的情況,就是多個對象引用形成一個環(huán),比如:A->B->C->D->A。循環(huán)引用有什么壞處?造成資源浪費,因為雙方都不能釋放,類似線程的死鎖。

  • 為什么會造成循環(huán)引用?舉個例子:
- (void)test: (NSString *)str {
    self.name = str;
    self.block = ^{
        NSLog(@"name: %@", self.name);
    };
}
//編譯器報警告:
Capturing 'self' strongly in this block is likely to lead to a retain cycle

上面的代碼在ARC下會報警告,但是在MRC下卻不會報警告,為什么呢?
在第四小節(jié)block獲取外部變量可以知道,__block的區(qū)別在于對象的內(nèi)存地址還是對象指向的內(nèi)存地址,而ARC和MRC的區(qū)別除了把棧區(qū)的block自動復制到堆區(qū),還有一個重要區(qū)別就是自動插入-retain和-release函數(shù),所以,ARC是強引用,MRC是弱引用,因為MRC并沒有自動將block獲取的外部變量的引用計數(shù)器的值增加。

  • 如何解決循環(huán)引用
    1.在執(zhí)行完操作,將其中一個對象設(shè)置為nil
    2.其中一個對象的引用設(shè)置為弱引用,比如上面的例子,可以將self設(shè)置為弱引用
    3.先設(shè)置弱引用,在block里再強引用,防止該引用提前被回收掉

回答:使用assign修飾的block會不會造成循環(huán)引用?
答案不會
想想block是在哪分配的內(nèi)存?是在棧上,不是由程序員管理內(nèi)存,由系統(tǒng)管理,所以,block出了作用域就會被系統(tǒng)釋放掉,所以,不會造成循環(huán)引用的問題。

針對第三點,說說個人理解,為什么要先weak,再strong,不會引起循環(huán)引用,而且在block使用期間,weak引用不會被提前釋放掉。先看圖:


block-cycle-retain.png

首先,對象A和block之間是不會形成循環(huán)引用的,這點是明確的。在block內(nèi)部再對對象A強引用,看起來好像是形成了循環(huán)引用,其實,別忘記這是在block里面,這里的強引用只是一個局部變量,存儲在棧上,也就是說,一旦出了函數(shù)域,該強引用就會被系統(tǒng)釋放掉,也就不存在循環(huán)引用的問題。

總結(jié):

Object-C因為有了block使本身語言更為便利,block及時返回的特性,也讓代碼的上下文結(jié)構(gòu)更為清晰,某種程度比delegate更適合用在網(wǎng)絡(luò)請求的功能實現(xiàn)。

參考
談Objective-C block的實現(xiàn)
對Objective-C中Block的追探
block源碼
Ry’s Objective-C Tutorial/blocks
Objective-C中的Block
《iOS 與OS X多線程和內(nèi)存管理》筆記:Blocks實現(xiàn)(二)

最后編輯于
?著作權(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)容