深入淺出理解 Block

1. 關(guān)于 Block 的幾道題

1. void exampleA() {
    char a = 'A';
    ^{
        printf("%c\n", a);
    }()
}
The example ()
A.  always works.
B.  only works with ARC.
C.  only works without ARC.
D.  never works.
2. void exampleB_addBlockToArray(NSMutableArray *array) {
    char b = 'B';
    [array addObject:^{
        printf("%c\n", b);
    }];
}

void exampleB() {
    NSMutableArray *array = [NSMutableArray array];
    exampleB_addBlockToArray(array);
    void (^block)() = [array objectAtIndex:0];
    block()
}
The example ()
A.  always works.
B.  only works with ARC.
C.  only works without ARC.
D.  never works.
3. void exampleC_addBlockToArray(NSMutableArray *array) {
    [array addObject:^{
        printf("C\n");
    }];
}

void exampleC() {
    NSMutableArray *array = [NSMutableArray array];
    exampleC_addBlockToArray(array);
    void (^block)() = [array objectAtIndex:0];
    block();
}
The example ()
A.  always works.
B.  only works with ARC.
C.  only works without ARC.
D.  never works.
4. typedef void (^dBlock)();
dBlock exampleD_getBlock() {
    char d = 'D';
    return ^{
        printf("%c\n", d);
    };
}

void exampleD() {
    exampleD_getBlock()();
}
The example ()
A.  always works.
B.  only works with ARC.
C.  only works without ARC.
D.  never works.
5. typedef void (^eBlock)();

eBlock exampleE_getBlock() {
    char e = 'E';
    void (^block)() = ^{
        printf("%c\n", e);
    };
    return block;
}

void exampleE() {
    eBlock block = exampleE_getBlock();
    block;
}
The example ()
A.  always works.
B.  only works with ARC.
C.  only works without ARC.
D.  never works.

結(jié)果分別為:A尽楔、B、A第练、B阔馋、B

解釋:

  1. 第一題中,Block 訪問外部變量 a娇掏,之后在棧上生成了一個同名的只讀變量 a呕寝。需要注意的是,兩個 a 的生命周期是相同的婴梧,同樣都存在于棧上下梢。由于調(diào)用函數(shù) exampleA() 中調(diào)用了 Block,故 ARC 與非 ARC 都是可以運行的塞蹭。
  2. 第二題中怔球,如果不是在 ARC 模式下調(diào)用,則 Block 為__NSStackBlock類型 浮还,當(dāng)在 exampleB() 中調(diào)用 Block 的時候,Block
    是不可用的闽巩。在 ARC 模式下钧舌,Block 會自動 copy 到堆上面(也是自動釋放的),是__NSMallocBlock類型的涎跨。
  3. 第三題中洼冻,Block 沒有訪問任何的外部變量,則 Block 為__NSGlobalBlock類型的隅很。它既不在堆上也不在棧上撞牢,而是在全局區(qū)中。
  4. 第四題中叔营,與第二題類似屋彪,Block 訪問了棧上的變量 d,但是在 exampleD() 函數(shù)中調(diào)用了 Block绒尊。在非 ARC 模式下畜挥,會報錯:error: returning block that lives on the local stack。在 ARC 模式下婴谱,Block 會自動 copy 到堆上并且自動
    release蟹但,其類型為 __NSMallocBlock躯泰。
  5. 第五題中,與第四題其實是一樣的华糖,只不過將 Block 返回給了一個局部變量麦向,之后將這個局部變量返回。這樣在非 ARC 模式下編譯器不會再報錯客叉,但是還是會出現(xiàn)問題诵竭。ARC 模式下,Block 會自動 copy 到堆上十办,并且自動 release秀撇,類型為__NSMallocBlock

2. Block 的分類

  1. __NSGlobalBlock 類型(全局 Block)

    __NSGlobalBlock 類型

    總結(jié):對于沒有引用外部變量的 Block向族,無論是在 ARC 還是在非 ARC 下呵燕,類型都是__NSGlobalBlock。這種類型的 Block 可以理解為一種全局的 Block件相,不需要考慮作用域的問題再扭。同時,對它進(jìn)行 copy 或者 retain 操作也都是無效的夜矗。

  2. __NSStackBlock 類型(棧 Block)

    MRC 下 __NSStackBlock 類型

    ARC下 __NSStackBlock 類型

總結(jié):對于引用了外部局部變量(注意不是局部靜態(tài)變量)的 Block泛范,在 MRC 下如果沒有對它進(jìn)行 copy 操作,它的作用域只會在定義它的函數(shù)棧內(nèi)(類型為__NSStackBlock)紊撕。在 ARC 下罢荡,由于對象指針默認(rèn)為__strong修飾,故直接賦值的話 Block 會被 copy 到堆上对扶。如果對象指針用__weak修飾区赵,則 Block 不會被拷貝到堆上面。

  1. __NSMallocBlock 類型(堆 Block)
    MRC 下 __NSMallocBlock 類型

    ARC下 __NSMallocBlock 類型

總結(jié):在 MRC 下浪南,對 Block 進(jìn)行 copy 操作后笼才,Block 會被拷貝到堆上。在 ARC
下络凿,由于對象指針默認(rèn)由__strong修飾骡送,則程序默認(rèn)會將 Block 拷貝到堆上(如何使用__weak則程序不會拷貝 Block 到堆上)。

3. __block 關(guān)鍵字

  1. 默認(rèn)情況
    沒有使用 __block 關(guān)鍵字的情況

總結(jié):由圖中可以得知絮记,Block 中的 a 與外部變量 a 它們的地址并不相同摔踱,但是值是相同的。實際上 Block 中的 a 是外部變量 a 的一個拷貝到千,且為常量昌渤。

  1. 使用 __block 關(guān)鍵字
    使用 __block 關(guān)鍵字

總結(jié):由圖中可知,在使用了__block關(guān)鍵字后憔四,Block 中的 a 與外部變量 a 的地址相同膀息。這種情況下 Block 中使用的就是貨真價實的外部變量 a般眉,而且可以對
a 進(jìn)行賦值操作稚照。

  1. copy 的情況
    對 __block 修飾的變量的 Block 進(jìn)行 copy 操作

總結(jié):由圖可知巷折,局部變量 a 在 Block 進(jìn)行 copy 前和 copy 后的地址不同了。實際上對 Block 對象進(jìn)行 copy 后趟据,Block 中引用的變量都會被復(fù)制到堆上冗酿。而被標(biāo)記為__block的變量埠对,實際被移動到了堆上。為什么要將 a 移動到堆上面呢裁替?歸根結(jié)底是因為 a 被聲明了__block项玛。copy 之后,Block 中的 a 將被放置到堆上面弱判,但是程序需要保持 Block 中的 a 與外部的 a 的一致性 (生命周期與作用域)襟沮,故外部的 a 也就移動到了堆上面。
ps: 哪里有 copy 呢昌腰?

4. Block 的循環(huán)引用

Block 對外部引用的對象都會進(jìn)行持有开伏,直到 Block 執(zhí)行完。如果此時 Block 持有的對象正好持有 Block遭商,則會發(fā)生內(nèi)存泄漏固灵。

  1. 常見的 Block 循環(huán)引用
    定義學(xué)生類

    循環(huán)引用的產(chǎn)生

解釋:學(xué)生類中定義了一個 Block 屬性 work,此時學(xué)生類對象 s 持有 work 屬性劫流。在 work 的 Block 中巫玻,又訪問了外部對象 s,則 work 持有對象 s祠汇,這就形成了一個環(huán)大审。

環(huán)示意圖
  1. 觀察者模式引起的循環(huán)引用

    觀察者模式引起的循環(huán)引用

    解釋:在通知中心的這個方法中,Block 中引用了 self 或者 self 的成員變量(無論是通過點方法還是直接訪問實例變量)座哩,Block 都會持有當(dāng)前對象。如果在對象的 dealloc 方法中將通知移除粮彤,則會形成循環(huán)引用根穷。通知中心會一直持有該對象,直到解除 Observer 的注冊导坟。

  2. NSTimer 引起的循環(huán)引用

    NSTimer 循環(huán)引用

解釋:由圖中可知屿良,self 對象對 NSTimer 對象有強引用,而且又作為 NSTimer 對象的 target惫周。而 NSTimer 對象會一直持有 target 對象尘惧,直到 NSTimer 對象不再有效 (invalidate方法) ,這樣就形成了一個循環(huán)引用递递。代碼又在 self 對象的 dealloc 方法中對 NSTimer 進(jìn)行銷毀喷橙,而 self 對象的 dealloc 方法是在 self 對象將要釋放的時候調(diào)用的啥么,所以 self 對象和 NSTimer 對象永遠(yuǎn)都無法釋放。

  1. NSURLSession 對象引起的循環(huán)引用
    NSURLSession 循環(huán)引用

解釋:NSURLSession 對象會對 delegate 保持強引用贰逾,直到程序釋放或者調(diào)用了NSURLSession 對象的finishTasksAndInvalidate方法或者invalidateAndCancel方法悬荣。與上面的例子相似,在 dealloc 中調(diào)用 invalidate 相關(guān)方法疙剑,是無法解決循環(huán)引用的情況的氯迂,在使用的時候要多加注意。

5. Block 的底層結(jié)構(gòu)

Block 的底層結(jié)構(gòu)大體是由結(jié)構(gòu)體以及額外的函數(shù)來構(gòu)成言缤,具體可以使用 clang 的命令編譯 .m 文件來查看:clang -rewirte-objc xxx.m嚼蚀。網(wǎng)上有很多分析的文章,這里就不再一一的分析了管挟。

6. 解決循環(huán)引用

  1. 通過將Block中訪問的對象設(shè)置為weak轿曙。

    weak 方式

    或者更加安全的方式:
    更加安全的方式

  2. 使用完 Block 的時候及時將 Block 置為空

    將 Block 置空

解釋:循環(huán)引用的發(fā)生一般都是由于 Block 持有了對象,而對象又持有了 Block 哮独。這樣我們可以在使用完 Block 之后拳芙,立刻釋放對象對 Block 的持有,將 Block 置空就可以打破引用環(huán)皮璧。

將 Block 置空打破環(huán)
  1. 根據(jù)情況將環(huán)的任一強引用置空即可
    在實際的開發(fā)中舟扎,有可能存在多個節(jié)點的復(fù)雜的環(huán),除了可以將 Block 置空外悴务,還可以打破環(huán)中的任一強引用睹限,就可以解決掉循環(huán)引用的問題。
6. 練習(xí)

下面這四種情況中讯檐,哪個會存在內(nèi)存泄漏的情況羡疗?

- (void)test1
{
    self.student = [Student new];
    self.student.work = ^{
        self.name = @"Hello";
    };
    self.student.work = nil;
}
- (void)test2
{
    self.student = [Student new];
    self.student.work = ^{
        self.name = @"Hello";
    };
    self.student = nil;
}
- (void)test3
{
    Student *student = [Student new];
    student.work = ^{
        student.name = @"小明";
    };
    student.work = nil;
}
- (void)test4
{
    Student *student = [Student new];
    student.work = ^{
        student.name = @"小明";
    };
    student = nil;
}

7. 總結(jié)

  1. ARC 下 Block 會從棧上自動拷貝到堆上,原因是由于__strong的原因别洪。
  2. Block 由于會持有外部引用的變量叨恨,容易引發(fā)循環(huán)引用的問題。解決的辦法是把環(huán)打破挖垛,或者根本不讓環(huán)生成痒钝。
  3. iOS 開發(fā)中使用 NSTimerNSURLSession痢毒、NSNotificationCenter 的時候一定要注意送矩。
  4. 需要注意,非 ARC下 __block可以解決循環(huán)引用的問題哪替。在 ARC 下不可以栋荸,需要使用__weak來解決。

8. 參考鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市晌块,隨后出現(xiàn)的幾起案子爱沟,更是在濱河造成了極大的恐慌,老刑警劉巖摸袁,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件钥顽,死亡現(xiàn)場離奇詭異,居然都是意外死亡靠汁,警方通過查閱死者的電腦和手機蜂大,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蝶怔,“玉大人奶浦,你說我怎么就攤上這事√咝牵” “怎么了澳叉?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長沐悦。 經(jīng)常有香客問我成洗,道長,這世上最難降的妖魔是什么藏否? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任瓶殃,我火速辦了婚禮,結(jié)果婚禮上副签,老公的妹妹穿的比我還像新娘遥椿。我一直安慰自己,他們只是感情好淆储,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布冠场。 她就那樣靜靜地躺著,像睡著了一般本砰。 火紅的嫁衣襯著肌膚如雪碴裙。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天点额,我揣著相機與錄音青团,去河邊找鬼。 笑死咖楣,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的芦昔。 我是一名探鬼主播诱贿,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了珠十?” 一聲冷哼從身側(cè)響起料扰,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎焙蹭,沒想到半個月后晒杈,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡孔厉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年拯钻,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片撰豺。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡粪般,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出污桦,到底是詐尸還是另有隱情亩歹,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布凡橱,位于F島的核電站小作,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏稼钩。R本人自食惡果不足惜顾稀,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望变抽。 院中可真熱鬧础拨,春花似錦、人聲如沸绍载。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽击儡。三九已至塔沃,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間阳谍,已是汗流浹背蛀柴。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留矫夯,地道東北人鸽疾。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像训貌,于是被迫代替她去往敵國和親制肮。 傳聞我的和親對象是個殘疾皇子冒窍,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容