1. 程式人生 > >iOS block 機制

iOS block 機制

keyword root 作用域 queue lock 技術分享 sta code 這樣的

本文要將block的以下機制,並配合具體代碼詳細描述:

  • block 與 外部變量

  • block 的存儲域:棧塊、堆塊、全局塊

定義

塊與函數類似,只不過是直接定義在另一個函數裏,和定義它的那個函數共享同一個範圍內的東西。

訪問外部變量

堆塊內部,棧是紅燈區,堆是綠燈區。

根據塊的存儲位置,可將塊分為全局塊、棧塊、堆塊。這裏先主要針對堆塊講解。

  • Block不允許修改外部變量的值。Apple這樣設計,應該是考慮到了block的特殊性,block也屬於“函數”的範疇,變量進入block,實際就是已經改變了作用域。在幾個作用域之間進行切換時,如果不加上這樣的限制,變量的可維護性將大大降低。又比如我想在block內聲明了一個與外部同名的變量,此時是允許呢還是不允許呢?只有加上了這樣的限制,這樣的情景才能實現。於是棧區變成了紅燈區,堆區變成了綠燈區。

幾種演算

  • block調用 基本數據類型

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 { NSLog(@"\n--------------------block調用 基本數據類型---------------------\n"); int a = 10; NSLog(@"block定義前a地址=%p", &a); void (^aBlock)() = ^(){ NSLog(@"block定義內部a地址=%p", &a); }; NSLog(@"block定義後a地址=%p", &a); aBlock();
} /* 結果: block定義前a地址=0x7fff5bdcea8c block定義後a地址=0x7fff5bdcea8c block定義內部a地址=0x7fa87150b850 */ /* 流程: 1. block定義前:a在棧區 2. block定義內部:裏面的a是根據外面的a拷貝到堆中的,不是一個a 3. block定義後:a在棧區 */ { NSLog(@"\n--------------------block調用 __block修飾的基本數據類型---------------------\n"); __block int b = 10; NSLog(@"block定義前b地址=%p", &b); void (^bBlock)() = ^(){ b = 20; NSLog(@"block定義內部b地址=%p", &b); }; NSLog(@"block定義後b地址=%p", &b); NSLog(@"調用block前 b=%d", b); bBlock(); NSLog(@"調用block後 b=%d", b); } /* 結果: block定義前b地址=0x7fff5bdcea50 block定義後b地址=0x7fa873b016d8 調用block前 b=10 block定義內部b地址=0x7fa873b016d8 調用block後 b=20 */ /* 流程: 1. 聲明 b 為 __block (__block 所起到的作用就是只要觀察到該變量被 block 所持有,就將“外部變量”在棧中的內存地址放到了堆中。) 2. block定義前:b在棧中。 3. block定義內部: 將外面的b拷貝到堆中,並且使外面的b和裏面的b是一個。 4. block定義後:外面的b和裏面的b是一個。 5. block調用前:b的值還未被修改。 6. block調用後:b的值在block內部被修改。 */ { NSLog(@"\n--------------------block調用 指針---------------------\n"); NSString *c = @"ccc"; NSLog(@"block定義前:c=%@, c指向的地址=%p, c本身的地址=%p", c, c, &c); void (^cBlock)() = ^{ NSLog(@"block定義內部:c=%@, c指向的地址=%p, c本身的地址=%p", c, c, &c); }; NSLog(@"block定義後:c=%@, c指向的地址=%p, c本身的地址=%p", c, c, &c); cBlock(); NSLog(@"block調用後:c=%@, c指向的地址=%p, c本身的地址=%p", c, c, &c); } /* c指針本身在block定義中和外面不是一個,但是c指向的地址一直保持不變。 1. block定義前:c指向的地址在堆中, c指針本身的地址在棧中。 2. block定義內部:c指向的地址在堆中, c指針本身的地址在堆中(c指針本身和外面的不是一個,但是指向的地址和外面指向的地址是一樣的)。 3. block定義後:c不變,c指向的地址在堆中, c指針本身的地址在棧中。 4. block調用後:c不變,c指向的地址在堆中, c指針本身的地址在棧中。 */ { NSLog(@"\n--------------------block調用 指針並修改值---------------------\n"); NSMutableString *d = [NSMutableString stringWithFormat:@"ddd"]; NSLog(@"block定義前:d=%@, d指向的地址=%p, d本身的地址=%p", d, d, &d); void (^dBlock)() = ^{ NSLog(@"block定義內部:d=%@, d指向的地址=%p, d本身的地址=%p", d, d, &d); d.string = @"dddddd"; }; NSLog(@"block定義後:d=%@, d指向的地址=%p, d本身的地址=%p", d, d, &d); dBlock(); NSLog(@"block調用後:d=%@, d指向的地址=%p, d本身的地址=%p", d, d, &d); } /* d指針本身在block定義中和外面不是一個,但是d指向的地址一直保持不變。 在block調用後,d指向的堆中存儲的值發生了變化。 */ { NSLog(@"\n--------------------block調用 __block修飾的指針---------------------\n"); __block NSMutableString *e = [NSMutableString stringWithFormat:@"eee"]; NSLog(@"block定義前:e=%@, e指向的地址=%p, e本身的地址=%p", e, e, &e); void (^eBlock)() = ^{ NSLog(@"block定義內部:e=%@, e指向的地址=%p, e本身的地址=%p", e, e, &e); e = [NSMutableString stringWithFormat:@"new-eeeeee"]; }; NSLog(@"block定義後:e=%@, e指向的地址=%p, e本身的地址=%p", e, e, &e); eBlock(); NSLog(@"block調用後:e=%@, e指向的地址=%p, e本身的地址=%p", e, e, &e); } /* 從block定義內部使用__block修飾的e指針開始,e指針本身的地址由棧中改變到堆中,即使出了block,也在堆中。 在block調用後,e在block內部重新指向一個新對象,e指向的堆中的地址發生了變化。 */ { NSLog(@"\n--------------------block調用 retain cycle---------------------\n"); View *v = [[View alloc] init]; v.tag = 1; v.frame = CGRectMake(100, 100, 100, 100); [self.view addSubview:v]; //self->view->v void (^block)() = ^{ v.backgroundColor = [UIColor orangeColor]; //定義內部:block->v }; v.block = block; //v->block block(); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ //預計3秒後釋放v對象。 [v removeFromSuperview]; }); } /* 結果: 不會輸出 dealloc. */ /* 流程: 1. self->view->v 2. block定義內部:block->v 因為block定義裏面調用了v 3. v->block 結論: 引起循環引用的是block->v->block,切斷其中一個線即可解決循環引用,跟self->view->v這根線無關 */ { NSLog(@"\n--------------------block調用self---------------------\n"); View *v = [[View alloc] init]; v.tag = 2; v.frame = CGRectMake(100, 220, 100, 100); [self.view addSubview:v]; //self->view->v void (^block)() = ^{ self.view.backgroundColor = [UIColor redColor]; //定義內部:block->self _count ++; //調用self的實例變量,也會讓block強引用self。 }; v.block = block; //v->block block(); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ //預計3秒後釋放self這個對象。 AppDelegate *appDelegate = [UIApplication sharedApplication].delegate; appDelegate.window.rootViewController = nil; }); } /* 結果: 不會輸出 dealloc. */ /* 流程: 1. self->view->v 2. v->block 3. block->self 因為block定義裏面調用了self 結論: 在block內引用實例變量,該實例變量會被block強引用。 引起循環引用的是self->view->v->block->self,切斷一個線即可解決循環引用。 */

棧塊、堆塊、全局塊

塊本身也是對象,由isa指針、塊對象正常運轉所需的信息、捕獲到的變量組成。

根據Block創建的位置不同,Block有三種類型,創建的Block對象分別會存儲到棧、堆、全局數據區域。

技術分享

block_storage.png

上面講了塊會把它所捕獲的所有變量都拷貝一份,這些拷貝放在 descriptor 變量後面,捕獲了多少個變量,就要占據多少內存空間。請註意,拷貝的並不是對象本身,而是指向這些對象的指針變量。

1. 在全局數據區的Block對象

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 { NSLog(@"\n--------------------block的存儲域 全局塊---------------------\n"); void (^blk)(void) = ^{ NSLog(@"Global Block"); }; blk(); NSLog(@"%@", [blk class]); } /* 結果:輸出 __NSGlobalBlock__ */ /* 結論: 全局塊:這種塊不會捕捉任何狀態(外部的變量),運行時也無須有狀態來參與。塊所使用的整個內存區域,在編譯期就已經確定。 全局塊一般聲明在全局作用域中。但註意有種特殊情況,在函數棧上創建的block,如果沒有捕捉外部變量,block的實例還是會被設置在程序的全局數據區,而非棧上。 */

2. 在堆上創建的Block對象

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 { NSLog(@"\n--------------------block的存儲域 堆塊---------------------\n"); int i = 1; void (^blk)(void) = ^{ NSLog(@"Malloc Block, %d", i); }; blk(); NSLog(@"%@", [blk class]); } /* 結果:輸出 __NSMallocBlock__ */ /* 結論: 堆塊:解決塊在棧上會被覆寫的問題,可以給塊對象發送copy消息將它拷貝到堆上。復制到堆上後,塊就成了帶引用計數的對象了。 在ARC中,以下幾種情況棧上的Block會自動復制到堆上: - 調用Block的copy方法 - 將Block作為函數返回值時(MRC時此條無效,需手動調用copy) - 將Block賦值給__strong修飾的變量時(MRC時此條無效) - 向Cocoa框架含有usingBlock的方法或者GCD的API傳遞Block參數時 上述代碼就是在ARC中,block賦值給__strong修飾的變量,並且捕獲了外部變量,block就會自動復制到堆上。 */

3. 在棧上創建的Block對象

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 { NSLog(@"\n--------------------block的存儲域 棧塊---------------------\n"); int i = 1; __weak void (^blk)(void) = ^{ NSLog(@"Stack Block, %d", i); }; blk(); NSLog(@"%@", [blk class]); } /* 結果:輸出 __NSStackBlock__ */ /* 結論: 棧塊:塊所占內存區域分配在棧中,編譯器有可能把分配給塊的內存覆寫掉。 在ARC中,除了上面四種情況,並且不在global上,block是在棧中。 */

內存泄漏

堆塊訪問外部變量時會拷貝一份指針到堆中,相當於強引用了指針所指的值。如果該對象又直接或間接引用了塊,就出現了循環引用。

解決方法:要麽在捕獲時使用__weak解除引用,要麽在執行完後置nil解除引用(使用後置nil的方式,如果未執行,則仍會內存泄漏)。

  • 註意:使用__block並不能解決循環引用問題。

優缺點

優點:

  • 捕獲外部變量

  • 降低代碼分散程度

缺點:

  • 循環引用引起內存泄露

總結

  • 在block內部,棧是紅燈區,堆是綠燈區。

  • 在block內部使用的是將外部變量的拷貝到堆中的(基本數據類型直接拷貝一份到堆中,對象類型只將在棧中的指針拷貝到堆中並且指針所指向的地址不變。)

  • __block修飾符的作用:是將block中用到的變量,拷貝到堆中,並且外部的變量本身地址也改變到堆中。

  • 循環引用:分析實際的引用關系,block中直接引用self也不一定會造成循環引用。

  • __block不能解決循環引用,需要在block執行尾部將變量設置成nil(但問題很多,比如block永遠不執行,外面變量變了裏面也變,裏面變了外面也變等問題)

  • __weak可以解決循環引用,block在捕獲weakObj時,會對weakObj指向的對象進行弱引用。

  • 使用__weak時,可在block開始用局部__strong變量持有,以免block執行期間對象被釋放。

  • 塊的存儲域:全局塊、棧塊、堆塊

  • 全局塊不引用外部變量,所以不用考慮。

  • 堆塊引用的外部變量,不是原始的外部變量,是拷貝到堆中的副本。

  • 棧塊本身就在棧中,引用外部變量不會拷貝到堆中。

參考

  • iOS Block用法和實現原理

  • __weak與__block區別

iOS block 機制