在了解Block之前贝奇,我們有必要先了解一下一些基礎(chǔ)知識。
我們都知道靠胜,Objective-C是由C語言擴(kuò)展而來的掉瞳。在Objective-C中毕源,引用是指向?qū)ο蟮囊粋€指針。即引用是一個變量陕习,也是一個指針霎褐,存儲的是對象的地址。那么该镣,引用本身其實也是存在地址的冻璃。所以引用和引用指向的對象是兩個不同的概念。
1. 在Objective-C中损合,引用一般分為強(qiáng)引用類型和弱引用類型省艳。即由__strong
和__weak
修飾的引用,不顯式指定引用類型時嫁审,默認(rèn)為__strong
修飾的強(qiáng)引用類型跋炕。強(qiáng)引用會使指向的對象引用計數(shù)加1,而弱引用則不會律适,且弱引用在指向的對象被釋放時枣购,會被置為nil。
2. Block塊分為定義時和運(yùn)行時兩種狀態(tài)擦耀。定義時指的是Block塊作為類進(jìn)行編譯處理和Block塊作為對象進(jìn)行創(chuàng)建的時候(Block塊代碼相當(dāng)于對象方法)棉圈,這個時候Block塊還沒有運(yùn)行。運(yùn)行時指的是Block塊代碼執(zhí)行的時候眷蜓。(此概念是為了理解Block而定義的分瘾,并不是官方定義的。)
3. 非局部引用指的是定義在Block塊外的局部引用吁系,但是在Block塊中有該引用的作用域德召。
了解完這些基礎(chǔ)知識后,我們就可以開始深入了解Block汽纤。這篇文章主要研究的是Block塊的正確使用上岗,以及為什么要這樣使用,并不對Block的實現(xiàn)原理做深入了解蕴坪。
首先肴掷,對于在Block塊中使用非局部引用的情況,我們先看一下下面這個例子:
NSString *string = @"Smith";
void(^block)() = ^(){
NSLog(@"%@",string);
};
string = @"Jackyson";
block();
我們知道背传,運(yùn)行結(jié)果是Smith呆瞻。那么,為什么會是這樣呢径玖?我們可以嘗試的打印string引用的地址痴脾,注意,是string引用的地址梳星,而不是string指向的對象的地址赞赖,兩者是有很大的不同的滚朵。通過下面這個例子:
NSString *string = @"Smith";
NSLog(@"1-----string對象:%@,string引用的地址:%p",string,&string);
void(^block)() = ^(){
NSLog(@"3-----string對象:%@,string引用的地址:%p",string,&string);
};
string = @"Jackyson";
NSLog(@"2-----string對象:%@,string引用的地址:%p",string,&string);
block();
下面是我的運(yùn)行結(jié)果:
2016-09-11 16:43:23.179 Block[73629:3669954] 1-----string對象:Smith,string引用的地址:0x7fff5e6f3a18
2016-09-11 16:43:23.179 Block[73629:3669954] 2-----string對象:Jackyson,string引用的地址:0x7fff5e6f3a18
2016-09-11 16:43:23.180 Block[73629:3669954] 3-----string對象:Smith,string引用的地址:0x7fca60c37260
從上面我們可以看到,在Block塊內(nèi)部的string和Block塊外面定義的string是不一樣的前域,它們是兩個不同的引用始绍,只是都指向了“Smith”這個字符串對象。
其實话侄,在Block塊內(nèi)部通過非局部引用訪問了外部對象時亏推,因為非局部引用在當(dāng)前作用域結(jié)束時會失效,Block如果不持有外部對象年堆,外部對象會被回收吞杭,而為了Block在真正運(yùn)行時能正確訪問外部對象,所以在Block塊定義時变丧,會在Block塊內(nèi)部復(fù)制非局部引用定義一個內(nèi)部引用(Block成員變量)芽狗。默認(rèn)情況下沒有使用__block
修飾,則內(nèi)部引用是const修飾的引用痒蓬,這意味著內(nèi)部引用的值是不可改變的(無法指向新的對象)童擎;反之使用__block
修飾,則內(nèi)部引用是沒有const修飾的引用攻晒,這意味著內(nèi)部引用的值是可以改變的(可以指向新的對象)顾复。同時,內(nèi)部引用和非局部引用的強(qiáng)弱類型一致鲁捏。
讓我們回到上面的例子中芯砸,在Block塊中通過string訪問了“Smith”對象,那么Block塊定義時给梅,會自動生成名稱為string的內(nèi)部引用(作用域在Block塊內(nèi))假丧,且該指針的值不可修改(const),即引用不能指向新的對象动羽,所以內(nèi)部引用string一直指向字符串“Smith”包帚。而非局部引用string指向新的字符串對象“Jackyson”,與內(nèi)部引用string沒有關(guān)系运吓,所以就相當(dāng)于將“Smith”字符串復(fù)制一份到Block內(nèi)渴邦,且無法修改。
那么羽德,我們?nèi)绻M麅?nèi)部引用指向新的對象几莽,或者在Block塊內(nèi)外修改引用值能互相影響迅办,應(yīng)該怎么做呢宅静?答案就是使用__block
修飾引用,代碼例子如下:
__block NSString *string = @"Smith";
NSLog(@"1-----string對象:%@,string引用的地址:%p",string,&string);
void(^block)() = ^(){
NSLog(@"3-----string對象:%@,string引用的地址:%p",string,&string);
string = @"SmithJackyson";
NSLog(@"4-----string對象:%@,string引用的地址:%p",string,&string);
};
string = @"Jackyson";
NSLog(@"2-----string對象:%@,string引用的地址:%p",string,&string);
block();
NSLog(@"5-----string對象:%@,string引用的地址:%p",string,&string);
下面是我的運(yùn)行結(jié)果:
2016-09-11 17:15:33.063 Block[74333:3683201] 1-----string對象:Smith,string引用的地址:0x7fff5510aa18
2016-09-11 17:15:33.066 Block[74333:3683201] 2-----string對象:Jackyson,string引用的地址:0x7fb351c1fb08
2016-09-11 17:15:33.066 Block[74333:3683201] 3-----string對象:Jackyson,string引用的地址:0x7fb351c1fb08
2016-09-11 17:15:33.067 Block[74333:3683201] 4-----string對象:SmithJackyson,string引用的地址:0x7fb351c1fb08
2016-09-11 17:15:33.068 Block[74333:3683201] 5-----string對象:SmithJackyson,string引用的地址:0x7fb351c1fb08
這里站欺,我對__block
修飾符的影響做出了一個假設(shè):
非局部引用是否使用__block
修飾會影響內(nèi)部引用的訪問修飾符姨夹,因為Block塊也可以作為對象處理纤垂,所以當(dāng)不使用__block
修飾時,內(nèi)部引用是@protected
或者@private
修飾的成員變量磷账,當(dāng)使用__block
修飾時峭沦,內(nèi)部引用是@public
修飾的成員變量。
這一假設(shè)能較好的解釋為什么使用__block
修飾后逃糟,在Block塊內(nèi)外修改引用值能相互影響吼鱼。如果有人有更好的解釋,歡迎與我分享4卵省9剿唷!
從結(jié)果分析來看取募,使用__block
修飾string引用琐谤,那么在Block塊定義時,在Block塊內(nèi)部定義一個內(nèi)部引用string(@public
修飾的成員變量)玩敏。那么string指向“Jackyson”字符串對象為什么會影響B(tài)lock塊內(nèi)string的值呢斗忌?因為Block定義后,編譯器對Block定義之后使用的string進(jìn)行一些處理旺聚,使用的string是訪問到Block塊中的內(nèi)部引用string(類似block->string)织阳,所以string指向字符串“Jackyson”就是將Block塊內(nèi)string指向字符串“Jackyson”。同樣砰粹,在Block內(nèi)將string指向字符串“SmithJackyson”就是將Block塊外的string指向字符串“SmithJackyson”陈哑。
所以__block
修飾引用后,在Block塊內(nèi)外改變引用值會相互影響伸眶,因為它們是同一個引用惊窖。
值得注意的是,基本數(shù)據(jù)類型的內(nèi)存管理不同于對象厘贼。對于Block塊使用定義在Block塊外面的局部變量(基本數(shù)據(jù)類型)的情況界酒,當(dāng)不使用__block
修飾的時候,那么Block塊會定義一個const修飾的內(nèi)部變量嘴秸,并將Block塊外面的局部變量的值拷貝過來毁欣,所以Block塊外面修改局部變量的值并不影響B(tài)lock塊的內(nèi)部變量的值。當(dāng)使用__block
修飾的時候岳掐,那么Block塊會直接使用外面的局部變量凭疮,這樣相當(dāng)于局部變量變?yōu)槿肿兞浚贐lock塊內(nèi)外修改變量的值會相互影響串述。
另外执解,對于在Block塊中使用成員變量的情況,我們再來看一下下面的例子:
@interface ViewController ()
{
NSString *_string;
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_string = @"Smith";
NSLog(@"1-----string對象:%@,string引用的地址:%p",_string,&_string);
NSLog(@"1*****self引用的地址:%p",&self);
void(^block)() = ^(){
NSLog(@"3-----string對象:%@,string引用的地址:%p",_string,&_string);
NSLog(@"3*****self引用的地址:%p",&self);
_string = @"SmithJackyson";
NSLog(@"4-----string對象:%@,string引用的地址:%p",_string,&_string);
NSLog(@"4*****self引用的地址:%p",&self);
};
_string = @"Jackyson";
NSLog(@"2-----string對象:%@,string引用的地址:%p",_string,&_string);
NSLog(@"2*****self引用的地址:%p",&self);
block();
NSLog(@"5-----string對象:%@,string引用的地址:%p",_string,&_string);
NSLog(@"5*****self引用的地址:%p",&self);
}
@end
下面是我的運(yùn)行結(jié)果:
2016-09-13 17:37:11.162 Block[98324:4200964] 1-----string對象:Smith,string引用的地址:0x7f90b0cf9280
2016-09-13 17:37:11.162 Block[98324:4200964] 1*****self引用的地址:0x7fff57817a28
2016-09-13 17:37:11.163 Block[98324:4200964] 2-----string對象:Jackyson,string引用的地址:0x7f90b0cf9280
2016-09-13 17:37:11.163 Block[98324:4200964] 2*****self引用的地址:0x7fff57817a28
2016-09-13 17:37:11.163 Block[98324:4200964] 3-----string對象:Jackyson,string引用的地址:0x7f90b0cf9280
2016-09-13 17:37:11.163 Block[98324:4200964] 3*****self引用的地址:0x7f90b0d86fb0
2016-09-13 17:37:11.163 Block[98324:4200964] 4-----string對象:SmithJackyson,string引用的地址:0x7f90b0cf9280
2016-09-13 17:37:11.163 Block[98324:4200964] 4*****self引用的地址:0x7f90b0d86fb0
2016-09-13 17:37:11.163 Block[98324:4200964] 5-----string對象:SmithJackyson,string引用的地址:0x7f90b0cf9280
2016-09-13 17:37:11.163 Block[98324:4200964] 5*****self引用的地址:0x7fff57817a28
從結(jié)果可以看到纲酗,Block中使用的_string引用就是成員變量_string衰腌,這是因為其實在Block塊中是通過self->_string來訪問成員變量_string新蟆,所以在Block中定義了內(nèi)部引用self,并通過內(nèi)部引用self->_string來訪問成員變量_string右蕊。所以Block塊中_string和Block塊外_string兩個引用是同一個琼稻,Block塊內(nèi)外改變引用值會互相影響。
分析完__block
修飾符的作用后饶囚,我們再來看看__strong
和__weak
為什么可以避免循環(huán)引用呢帕翻?
在前面我們已經(jīng)了解到了,當(dāng)Block通過外部引用訪問外部對象時萝风,在Block定義的時候熊咽,會復(fù)制外部引用定義一個內(nèi)部引用,且內(nèi)部引用強(qiáng)弱類型與外部引用一致闹丐。當(dāng)對象本身存在一個成員變量持有Block横殴,而Block內(nèi)部又訪問了對象本身,那么可能會造成循環(huán)引用卿拴。這里我們以UIViewController為例衫仑,如下圖:
因為可能會出現(xiàn)這么一種情況,當(dāng)沒有外部強(qiáng)引用持有UIViewController對象和Block塊時堕花,那么成員變量block和成員變量self默認(rèn)是無法被置空的文狱,因為一般情況下兩個成員變量在Block塊和UIViewController對象dealloc時才被置為nil,那么兩個對象相互持有缘挽,無法被釋放瞄崇。但是如果成員變量block在某時刻可以被置為nil,那么兩個對象是能夠被釋放的壕曼。
那么苏研,為什么使用weakSelf能夠防止循環(huán)引用呢?因為這樣Block塊成員變量為weakSelf(弱類型)腮郊,當(dāng)沒有強(qiáng)引用持有UIViewController時摹蘑,那么,UIViewController對象會被釋放轧飞,成員變量block和weakSelf會被置為nil衅鹿,Block塊也會被釋放。如下圖:
如果UIViewController對象沒有成員變量block持有Block塊對象時过咬,為什么我們也需要使用weakSelf呢大渤?這里其實并不是為了防止循環(huán)引用,而是希望Block塊不持有UIViewController對象掸绞,讓UIViewController對象能夠及時被釋放泵三。因為如果Block塊持有UIViewController對象,而持有Block塊對象的引用沒有被置為nil的話,那么Block塊沒有被釋放切黔,也就會一直持有UIViewController對象砸脊,造成UIViewController對象無法被及時釋放具篇。而我們希望當(dāng)點擊返回按鈕時纬霞,UIViewController對象能夠被及時釋放,所以需要使用weakSelf驱显。如下圖:
那么诗芜,我們又為什么要使用strongSelf呢?這是因為當(dāng)Block塊回調(diào)時埃疫,有可能在回調(diào)過程中伏恐,UIViewController對象被釋放了,造成數(shù)據(jù)的狀態(tài)和邏輯出現(xiàn)錯誤栓霜。所以需要在Block塊執(zhí)行時翠桦,定義一個局部強(qiáng)引用strongSelf來持有UIViewController對象,讓Block塊執(zhí)行過程中胳蛮,UIViewController對象即使沒有被其他強(qiáng)引用持有销凑,也無法被釋放,確保數(shù)據(jù)的狀態(tài)和邏輯不會出現(xiàn)錯誤仅炊。當(dāng)Block執(zhí)行完成后斗幼,strongSelf就會被置為nil,這時UIViewController對象引用計數(shù)為0抚垄,就會被釋放了蜕窿。如下圖:
那么,什么情況下需要使用weakSelf和strongSelf呢呆馁?一般來說桐经,當(dāng)我們將Block塊作為參數(shù)傳入方法中,而方法本身進(jìn)行的是耗時操作(文件讀寫或網(wǎng)絡(luò)請求)浙滤,如果需要Block塊執(zhí)行時對象還存在次询,那么直接使用self;如果Block塊代碼是更新界面數(shù)據(jù)這種非必需的操作瓷叫,那么使用weakSelf屯吊;如果是回調(diào)時對象還存在需要執(zhí)行必要的數(shù)據(jù)操作和邏輯操作,那么使用weakSelf和strongSelf確保Block執(zhí)行過程中對象不會被釋放掉摹菠。即根據(jù)Block塊代碼要進(jìn)行的操作和操作是不是必要的來決定是使用self盒卸、weakSelf或者weakSelf和strongSelf。但是有一點需要注意的是次氨,必須確保Block塊在某一時刻會被置為nil蔽介,這樣持有的對象才能夠被釋放掉。確保Block執(zhí)行完后置為nil,或者持有Block塊的對象會被釋放掉虹蓄。
總結(jié)來說犀呼,當(dāng)Block塊通過外部引用訪問外部對象時,在Block塊定義的時候薇组,會復(fù)制外部引用定義一個強(qiáng)弱類型一致的內(nèi)部引用外臂。外部引用如果使用__block
修飾,則內(nèi)部引用是可變的律胀,即可以指向新的對象宋光;且訪問修飾符為@public
,即Block塊定義后炭菌,使用的不再是外部引用罪佳,而是通過類似Block->_internalReference使用內(nèi)部引用。如果不使用__block
修飾黑低,則內(nèi)部引用是不可變的赘艳,即不可以指向新的對象;且訪問修飾符為@protected
或者@private
克握,即Block塊定義后蕾管,使用的還是外部引用,與內(nèi)部引用沒有聯(lián)系玛荞,兩者互不影響娇掏。