上篇講到了Block
其實就是oc對象
[toc]
本文主要講解Block值捕獲
以及如何修改block
捕獲的變量,說明下本文的目錄結(jié)構(gòu)(簡書竟然沒做目錄功能,有點失望菲嘴,給個簡書生成目錄的鏈接吧)
注意把示例中的這行代碼改成下面的形式饿自,否則無效
// @match http://www.reibang.com/p/*
一、Block是如何捕獲變量的
1.1 局部變量
1.1.1 auto類型變量
1.1.1.1 基本數(shù)據(jù)類型
修改下main.m
的代碼
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10;
void (^block) (void) = ^{
NSLog(@"----%d-",a);
};
a = 20;
block();
}
return 0;
}
發(fā)現(xiàn)打印的是
10
轉(zhuǎn)成C++
代碼發(fā)現(xiàn)有些變化了
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a; //變化1
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) { // 這也是c++的語法表示 將 參數(shù)`_a` 賦值給 變量`a`
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // 變化3
NSLog((NSString *)&__NSConstantStringImpl__var_folders_s2_zmz_wcdj2px2kh6hm1zkcd780000gp_T_main_4fb442_mi_0,a);
}
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int a = 10;
void (*block) (void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));// 變化2
a = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
變化1
:發(fā)現(xiàn)block
多了個int
類型的變量a
;
變化2
:調(diào)用block的時候除了傳遞block本身外龄坪,把a
的值也就是10傳遞給了里面的變量a
變化3
:最后方法執(zhí)行的地方__main_block_func_0
也是把block
的變量a
取出
1.1.1.2 對象數(shù)據(jù)類型
原來block
的結(jié)構(gòu)還不是定的昭雌,會隨著擁有的變量改變內(nèi)存結(jié)構(gòu)
再舉個例子來進(jìn)一步理解下值捕獲
,改造下代碼
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSString *name = @"jack";
NSLog(@"進(jìn)入block前的地址----%p",name);
void (^block) (void) = ^{
NSLog(@"進(jìn)入block的地址----%@--%p",name,name);
};
name = @"rose";
NSLog(@"修改name之后的地址----%p",name);
block();
}
return 0;
}
輸出
進(jìn)入block前的地址----0x100001078
修改name之后的地址----0x1000010d8
進(jìn)入block的地址----jack--0x100001078
c++
代碼健田,這次簡單寫下就是多了個*name
屬性
struct __main_block_impl_0 {
...
NSString *name;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSString *_name, int flags=0) : name(_name) {
...
}
};
解釋下為什么是輸出jack
在進(jìn)入block前name
地址為0x100001078
烛卧,當(dāng)?shù)搅?code>__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, name))的時候相當(dāng)于把name
的值也就是0x100001078
給了__main_block_impl_0
里面的name
變量,當(dāng)再次name = @"rose";
的時候之前的name
的值已經(jīng)不是0x100001078
,而是0x1000010d8
了总放,所以后來當(dāng)調(diào)用的時候訪問的是__main_block_impl_0
的name
變量的值0x100001078
所存的值是jack
弄了一張圖幫助理解下
1.1.2 static類型變量
1.1.2.1 對象類型
接著我們再次修改下代碼
int main(int argc, const char * argv[]) {
@autoreleasepool {
static NSString *name = @"jack";
void (^block) (void) = ^{
NSLog(@"----%@--",name);
};
name = @"rose";
block();
}
return 0;
}
就是加了一個static
輸出
----rose--
這次的結(jié)果完全不一樣了呈宇,這又是為什么呢,繼續(xù)看c++的實現(xiàn)
struct __main_block_impl_0 {
...
NSString **name; // 1
...
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSString **name = __cself->name; // 3
NSLog((NSString *)&__NSConstantStringImpl__var_folders_s2_zmz_wcdj2px2kh6hm1zkcd780000gp_T_main_9d6d5e_mi_1,(*name));//4
}
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
static NSString *name = (NSString *)&__NSConstantStringImpl__var_folders_s2_zmz_wcdj2px2kh6hm1zkcd780000gp_T_main_9d6d5e_mi_0;
void (*block) (void) = &__main_block_impl_0(__main_block_func_0,
&__main_block_desc_0_DATA,
&name, //2
570425344));
name = (NSString *)&__NSConstantStringImpl__var_folders_s2_zmz_wcdj2px2kh6hm1zkcd780000gp_T_main_9d6d5e_mi_2;
block->FuncPtr(block);
}
return 0;
}
看到代碼1
處已經(jīng)是二重指針局雄,2
處這個時候給的不是name
指向的內(nèi)存地址甥啄,而是name
變量的地址,3
處在調(diào)用的時候取出的是指向name
變量地址的內(nèi)存炬搭,而不是name
所指向的內(nèi)存蜈漓,所以要想獲取name
所指向的內(nèi)存,4
處通過*name
取值進(jìn)行傳參
也就是紅色箭頭的變化
1.2 全局變量
1.2.1 非static修飾的全局變量
1.2.1.1 對象類型
當(dāng)變量是全局的變量時
NSString *name = @"jack";
int main(int argc, const char * argv[]) {
@autoreleasepool {
void (^block) (void) = ^{
NSLog(@"----%@--",name);
};
name = @"rose";
block();
}
return 0;
}
發(fā)現(xiàn)也是輸出
---rose--
額宫盔,這又是怎么回事呢融虽,看源碼吧
NSString *name = (NSString *)&__NSConstantStringImpl__var_folders_s2_zmz_wcdj2px2kh6hm1zkcd780000gp_T_main_3b419d_mi_0;
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_s2_zmz_wcdj2px2kh6hm1zkcd780000gp_T_main_3b419d_mi_2,name);
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
void (*block) (void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
name = (NSString *)&__NSConstantStringImpl__var_folders_s2_zmz_wcdj2px2kh6hm1zkcd780000gp_T_main_3b419d_mi_3;
block)->FuncPtr(block);
}
return 0;
}
我們可以發(fā)現(xiàn)這個時候block
內(nèi)部并沒有像之前那樣生成一個同名的變量,也就是對于全局變量block
是不會捕獲的,當(dāng)變量是static
全局變量時也和全局變量一樣灼芭,留給讀者自行測試了
1.2.1.2 基本類型
略...
1.2.2 static修飾的全局變量
略... 讀者自行寫demo
說了這么多我們來梳理下有额,我們思考一個問題,block
為什么要捕獲變量姿鸿,是因為里面有個方法谆吴,方法需要使用變量,
- 如果是局部變量的話苛预,如果不持有他的話是不是過了作用域就釋放了句狼,那就不能完成方法的正常調(diào)用,所以對于局部變量热某,一定會捕獲的腻菇;
- 對于全局變量,剛才說了
block
捕獲變量的原因要使用變量昔馋,既然是全局變量筹吐,那在哪都可以訪問,所以不需要捕獲秘遏; - 那為什么局部變量有的是傳地址有的是傳值呢丘薛,對于非
static
修飾的局部變量其實是auto
的,這種變量是放在棧區(qū)的邦危,過了作用域就會被系統(tǒng)回收洋侨,如果block
捕獲變量的地址的話,那可能捕獲的地址已經(jīng)被系統(tǒng)回收倦蚪,或者已經(jīng)被其他的對象占用了希坚,這個時候程序會出現(xiàn)無法預(yù)料的異常,但是如果是static
修飾的陵且,是放在數(shù)據(jù)區(qū)的裁僧,不會隨著作用域的而銷毀,從而放地址是安全的
總結(jié)就是下面這張圖
二、 Block捕獲對對象的引用計數(shù)的影響
2.1 __NSMallocBlock__
對對象的引用計數(shù)的影響 ARC
環(huán)境
我們知道基本數(shù)據(jù)類型是放在棧中的聊疲,回收是由系統(tǒng)自動回收的無需考慮茬底,所以我們這里只考慮auto類型的對象類型的引用計數(shù)
需要新增一個MyPerson
類
//MyPerson.h
#import <Foundation/Foundation.h>
@interface MyPerson : NSObject
@end
//MyPerson.m
#import "MyPerson.h"
@implementation MyPerson
- (void)dealloc {
NSLog(@"-%s",__func__);
}
@end
main.m
代碼如下
int main(int argc, const char * argv[]) {
@autoreleasepool {
void (^myBlock) (void);
{
MyPerson *p = [[MyPerson alloc] init];
myBlock = ^ {
NSLog(@"----%@",p);
};
NSLog(@"block類型---%@",[myBlock class]);
}
NSLog(@"----");
}
return 0;
}
輸出
__NSMallocBlock__
這里p
并沒有釋放掉,按道理過了120
行應(yīng)該就釋放的获洲,其實此時MyPerson *p = [[MyPerson alloc] init];
和__strong MyPerson *p = [[MyPerson alloc] init];
是等價的
說明__NSMallocBlock__
類型的myBlock
會對__strong
修飾的p
對象的引用計數(shù)產(chǎn)生影響
再修改main.m
函數(shù)
此時輸出
__NSMallocBlock__
--[MyPerson dealloc]
發(fā)現(xiàn)p
正常釋放了
說明__NSMallocBlock__
類型的myBlock
不會對__weak
修飾的 p
對象的引用計數(shù)產(chǎn)生影響
2.2 __NSStackBlock__
對對象的引用計數(shù)的影響 MRC
環(huán)境
把項目改成MRC
MyPerson
#import "MyPerson.h"
@implementation MyPerson
- (void)dealloc {
[super dealloc];
NSLog(@"-%s",__func__);
}
@end
mian.m
如圖
此時打印
block類型---__NSStackBlock__
--[MyPerson dealloc]
發(fā)現(xiàn)p
正常釋放了桩警,此時的myBlock
的類型為__NSStackBlock__
類型的,
說明__NSStackBlock__
類型的myBlock
不會對__strong
修飾的 p
對象的引用計數(shù)產(chǎn)生影響
再修改一下main.m
的代碼如圖,新增的代碼看紅色處
會發(fā)現(xiàn)此時只會輸出
block類型---__NSMallocBlock__
但是p
并沒有釋放
說明__NSMallocBlock__
類型的myBlock
會對__strong
修飾的p
對象的引用計數(shù)產(chǎn)生影響
2.3 結(jié)論
得出以下結(jié)論:
當(dāng)block內(nèi)部訪問了對象類型的auto變量時
如果block是在棧上昌妹,將不會對auto變量產(chǎn)生強(qiáng)引用如果block被拷貝到堆上
會調(diào)用block內(nèi)部的copy函數(shù);
copy函數(shù)內(nèi)部會調(diào)用_Block_object_assign函數(shù)_Block_object_assign函數(shù)會根據(jù)auto變量的修飾符(__strong、__weak握截、__unsafe_unretained)做出相應(yīng)的操作飞崖,形成強(qiáng)引用(retain)或者弱引用如果block從堆上移除
會調(diào)用block內(nèi)部的dispose函數(shù);
dispose函數(shù)內(nèi)部會調(diào)用_Block_object_dispose函數(shù)_Block_object_dispose函數(shù)會自動釋放引用的auto變量(release)
三、__block如何做到可以修改變量的
由前面的了解我們知道block要想可以修改變量谨胞,那么就不能值捕獲固歪,也就是不能放在棧內(nèi)存中,因為棧內(nèi)存是的釋放無法控制胯努,所以要買放在全局區(qū)牢裳,要么放在堆區(qū)篷店,來看下蘋果是放在哪里的表蝙。
3.1 __block變量的內(nèi)存結(jié)構(gòu)
int main(int argc, const char * argv[]) {
@autoreleasepool {
MyPerson *p = [[MyPerson alloc] init];
p.name = @"jack";
void (^myBlock) (void) = ^ {
p = [[MyPerson alloc] init];
NSLog(@"---%@",p.name);
};
myBlock();
}
return 0;
}
像這樣在block
的內(nèi)部直接修改變量是會報錯的, 要想修改需要借助__block
修飾符
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block MyPerson *p = [[MyPerson alloc] init];
p.name = @"jack";
void (^myBlock) (void) = ^ {
p = [[MyPerson alloc] init];
NSLog(@"---%@",p.name);
};
myBlock();
}
return 0;
}
我們看看轉(zhuǎn)成的c++
的代碼
可以看到和之前沒有__block
修飾的不同的是這次的p變成了類型為__Block_byref_p_0 *p
再看初始化的地方
一行__block MyPerson *p = [[MyPerson alloc] init];
就變成了初識化一個__Block_byref_p_0
類型的結(jié)構(gòu)體恐仑,然后把該結(jié)構(gòu)體的指針給到myBlock
灰署,而我們初始化的那個p
則給了__Block_byref_p_0
內(nèi)部的p
對象
3.2 __block變量的內(nèi)存管理
當(dāng)__block變量在棧上時判帮,不會對指向的對象產(chǎn)生強(qiáng)引用
當(dāng)__block變量被copy到堆時
會調(diào)用__block變量內(nèi)部的copy函數(shù),copy函數(shù)內(nèi)部會調(diào)用_Block_object_assign函數(shù)_Block_object_assign函數(shù)會根據(jù)所指向?qū)ο蟮男揎椃╛_strong溉箕、__weak晦墙、__unsafe_unretained)做出相應(yīng)的操作,形成強(qiáng)引用(retain)或者弱引用(MRC除外肴茄,MRC時不會retain)如果__block變量從堆上移除
會調(diào)用__block變量內(nèi)部的dispose函數(shù)晌畅,dispose函數(shù)內(nèi)部會調(diào)用_Block_object_dispose函數(shù)
_Block_object_dispose函數(shù)會自動釋放指向的對象(release)
和沒有__block
修飾的auto對象變量差不多,只是第二條中對MRC
不起作用
這里的Block0
相當(dāng)于myBlock
寡痰,__block
變量就是p
,15
行的時候變量MyPerson
類型的變量p
抗楔,就變成了指向__Block_byref_p_0
類型的指針了,且處于棧中氓癌,到了21
行結(jié)束谓谦,由于是arc環(huán)境,myBlock
就是為右邊的block
copy后的處于堆
上了,這是變量p
也會被拷貝到堆上,當(dāng)23
執(zhí)行的時候調(diào)用的就是堆上的block
贪婉,訪問的也是堆上的內(nèi)容反粥,對于block
內(nèi)部的NSLog(@"---%@",p.name);
則是結(jié)構(gòu)體p
內(nèi)部的MyPerson
類型的p
對象
3.3 __block的__forwarding指針
可以看到__Block_byref_p_0
結(jié)構(gòu)如下
有一個__forwarding
,在main.m
中初始化的時候傳的就是__Block_byref_p_0
自身的地址,取值的時候也是通過p->__forwarding->p
去取值豈不是多此一舉才顿?
其實這是不管當(dāng)__block
修飾的結(jié)構(gòu)體變量莫湘,處于棧上還是被復(fù)制到堆上,都可以訪問到同一個p
變量
我們把代碼改下
此時會輸出rose
代碼過了21
行郑气,myBlock
就已經(jīng)在堆
上了幅垮,到了23
行訪問還是棧中的結(jié)構(gòu)體變量,那為何還是打印rose
呢尾组,就是因為有了__forwarding
指針的作用忙芒,保證了此時不管在棧
中還是在堆
中都可以訪問到同一個MyPerson
類型的變量p
,當(dāng)__block
修飾的變量從棧中copy到堆中的時候發(fā)送的事情入下圖
可以看出蘋果的實現(xiàn)是通過把變量放在堆區(qū)的方式來實現(xiàn)修改__block
捕獲的變量的讳侨,也可以看出__block
對對象的內(nèi)存影響還是蠻大的呵萨。
(完)