前文地址:《iOS基礎(chǔ)深入補(bǔ)完計劃》
在前文、我們提到了property中的關(guān)鍵字copy可以修飾block。
那么、block究竟有哪些特殊。
block該怎樣聲明
__block、__weak、__strong是如何工作的呢。
__block又是否可以解決循環(huán)引用呢?
首先、關(guān)于block的作用域
- 總的來說。block默認(rèn)是存檔在棧中(作用于被限制在當(dāng)前上下文(函數(shù), 方法...))、可能被隨時回收(比如代碼塊執(zhí)行完畢、會將塊內(nèi)變量內(nèi)存回收)。通過copy操作可以使其在堆中保留一份,、相當(dāng)于一直強(qiáng)引用著。以此保證在執(zhí)行block對象時、程序不會因為執(zhí)行一個空對象而崩潰。
舉個例子
@interface ViewController () @property (nonatomic, copy) void(^myblock)(); @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. [self test];//當(dāng)此代碼結(jié)束時,test函數(shù)中的所有存儲在棧區(qū)的變量都會被系統(tǒng)釋放。 self.myblock(); } - (void)test { int n = 5; [self setMyblock:^{ NSLog(@"%d",n); }]; }
之后、把copy聲明換成assign。
EXC_BAD_ACCESS、使用了一個野指針。
屏幕快照 2017-12-04 下午5.53.09.png此外、我們還可以進(jìn)一步確認(rèn)崩潰的原因。
屏幕快照 2017-12-05 上午10.12.01.png之后、到底是不是因為局部變量被釋放、導(dǎo)致的崩潰?
用成員變量試試。
WechatIMG210.jpeg
看來無論是否使用成員變量、內(nèi)部的變量指針都會被釋放
WechatIMG211.jpeg結(jié)論:
- copy聲明會把變量在block保存一份指針變量、指向原對象地址。保證block內(nèi)部所引用變量不會被自動釋放而造成崩潰。
- assign聲明的block、內(nèi)部引用的變量(無論是否是局部變量)在出了賦值當(dāng)前作用域之后、會被釋放。導(dǎo)致崩潰。
除此之外:
- block聲明為成員變量、用strong修飾。也是沒有問題的、作用和copy一樣。
- block聲明為成員變量、不加任何修飾。效果也和copy/strong一樣。
- block聲明為局部變量、不加任何修飾。效果也和copy/strong一樣。
關(guān)于copy之后block的一些特性
會對其內(nèi)部的使用對象引用計數(shù)+1
這也是為什么需要用__weak去修飾self以防止循環(huán)引用的原因。
更多的解釋可以參閱《__weak與__block區(qū)別,深層理解兩者區(qū)別》、《深入分析 Objective-C block、weakself、strongself 實現(xiàn)原理》原理總結(jié)下來就是
__weak會新開辟一個變量指針、指向被修飾的變量地址、但不會對該變量增加引用。
當(dāng)被修飾變量被釋放、__weak變量指針依舊會指向原有變量地址、而該地址則會返回空。
所以需要用__strong在block內(nèi)部修飾、讓block內(nèi)的變量不會在中途被釋放掉、保證block內(nèi)容一致性。
WechatIMG204.jpeg
默認(rèn)情況下、內(nèi)外部的變量數(shù)據(jù)不會互通。
int a = 1; int (^backValue)(int) = ^(int num) { return num+a; }; a += 1;//這次+=1不會被傳遞到block中a上。 int b = backValue(1); NSLog(@"b-->%d", b); //輸出結(jié)果b-->2
當(dāng)然、我們可以__block來修飾這個a、使其在block內(nèi)部的指針也可以被修改
屏幕快照 2017-12-04 下午6.11.00.png為什么用__block修飾就可以修改外部的變量、或者將外部變量的修改傳遞進(jìn)block內(nèi)了?
先寫結(jié)論、不愿意細(xì)看可以跳過。
無論是普通變量、還是OC變量。在不使用__block的情況下、無法對其本身進(jìn)行修改。
因為二者、都只會在block執(zhí)行代碼中copy一份值/指向該對象地址的指針。即使修改了這個值/指針指向、也無法對外部造成影響。所以編譯器干脆就禁止這個操作了(
雖然你可以通過直接操作指針的方式修改、但是被修改的依舊只是block內(nèi)部的指針變量而已
)
OC對象在不使用__block修飾的情況下、可以修改其屬性。
自身的指針會被捕獲到block代碼塊中、借此操作該指針指向地址所屬對象的屬性。(這也是為什么在block里可以修改self.xxx屬性的原因)。
OC對象以及普通變量在使用__block修飾的情況下、可以對自身進(jìn)行修改。
會將原變量的指針從棧移動至堆區(qū)。由block對象進(jìn)行捕獲保存、并借此進(jìn)行修改。
以下是相關(guān)分析
這里有一篇文章、借助編譯后的block闡釋了__block的工作原理
《你真的理解__block修飾符的原理么》
- 簡而言之、block捕獲普通變量其實將變量的值捕獲到了block執(zhí)行塊中。這個值已經(jīng)與原變量無關(guān)。(由于無法直接獲得原變量、技術(shù)上無法實現(xiàn)修改、所以編譯器直接禁止了)。
屏幕快照 2017-12-04 下午6.36.49.png
block內(nèi)部的指針指向、與外部完全分離__block聲明的變量、在被block捕獲時。會被block以指針變量的形式保存在自身處于堆中結(jié)構(gòu)體內(nèi)。同時、原對象的指針地址也被修改到了堆中。既然變量指針內(nèi)存地址相同、就代表是同一個指針、自然可以同步修改。
__block之后的效果
具體可見:《iOS中__block 關(guān)鍵字的底層實現(xiàn)原理》、《一篇文章看懂iOS代碼塊Block》
當(dāng)然、還有個其他情況。就是OC對象的屬性。
當(dāng)block執(zhí)行代碼需要捕獲的是OC對象時、block會自動在block代碼塊中copy一個對象指針(普通對象是copy值)、指向該對象當(dāng)前地址。
就會出現(xiàn)如下情況
屏幕快照 2017-12-04 下午6.57.07.png不過、并不允許你為這個存在棧中的變量指針直接重新指定一個新的指向。除非使用__block將這個對象的地址直接移動到堆中。
此外、還做了另外一個實驗。
屏幕快照 2017-12-04 下午8.18.27.png這也證明、即便是OC對象也僅僅是捕獲了當(dāng)前指針的指向。如果新生成了一個對象修改了外部的對象的指針指向地址、里面繼續(xù)修改也沒用了。
關(guān)于block的循環(huán)引用
-
__block是否可以防止循環(huán)引用。
可以、只需要在block內(nèi)部根據(jù)需要將變量置空。缺點是需要手動調(diào)用一次block
__block id tmp = self; self.block = ^{ tmp = nil; }; self.block();
需要注意的是。在tmp被block的捕獲的時候、和__weak一樣是不會增加tmp的引用計數(shù)的。之所以要手動釋放、是因為在__block id tmp = self;時、tmp對self進(jìn)行了一次強(qiáng)引用。需要消除的是這次引用。而且、當(dāng)tmp被置nil之后、下次block調(diào)用將會得到一個空地址。但原對象不受此影響。
WechatIMG207.jpeg
最后
本文主要是自己的學(xué)習(xí)與總結(jié)。如果文內(nèi)存在紕漏、萬望留言斧正。如果不吝賜教小弟更加感謝。