Block內(nèi)引用外部變量的問題
#define BLog(prefix,obj) {NSLog(@"位置和指針變量名:%@ ,指針內(nèi)存地址:%p, 指針值:%p ,指向的對象:%@ ",prefix,&obj,obj,obj);}
// 強引用
- (void)blockVariableStrongReferenceTest
{
NSLog(@"\n");
NSObject *obj = [[NSObject alloc] init];
BLog(@"StrongRef obj",obj);
void(^testBlock)()= ^(){
BLog(@"StrongRef in block",obj);
};
testBlock();
// Block外部嘗試將obj置為nil
obj = nil;
testBlock(); // 第二次調(diào)用block
}
運行結(jié)果
位置和指針變量名:StrongRef obj ,指針內(nèi)存地址:0x7fff543d0c98, 指針值:0x7fcb1bd22390 ,指向的對象:<NSObject: 0x7fcb1bd22390>
位置和指針變量名:StrongRef in block ,指針內(nèi)存地址:0x7fcb1c903fb0, 指針值:0x7fcb1bd22390 ,指向的對象:<NSObject: 0x7fcb1bd22390>
位置和指針變量名:StrongRef in block ,指針內(nèi)存地址:0x7fcb1c903fb0, 指針值:0x7fcb1bd22390 ,指向的對象:<NSObject: 0x7fcb1bd22390
分析
方法內(nèi)部的obj變量在棧中偿渡,變量內(nèi)存地址0x7fff543d0c98,所指向的對象<NSObject: 0x7fcb1bd22390>在堆中伪朽,內(nèi)存地址0x7fcb1bd22390,它的引用計數(shù)+1汛蝙。
Block中obj指針已經(jīng)不是外部的obj指針了,它是外部變量obj的拷貝
烈涮,它的內(nèi)存地址是0x7fcb1c903fb0,跟外部obj不一樣,但是所指向的對象也是0x7fcb1bd22390窖剑,0x7fcb1bd22390對象引用計數(shù)再+1=2坚洽。block中的obj指針(內(nèi)存地址是0x7fcb1c903fb0)對對象(<NSObject: 0x7fcb1bd22390>)的引用是強引用,在外部將obj(地址0x7fff543d0c98)置為nil后西土,外部的obj不再指向<NSObject: 0x7fcb1bd22390>對象讶舰,0x7fcb1bd22390對象的引用計數(shù)-1,引用計數(shù)為1需了,
0x7fcb1bd22390對象內(nèi)存不被自動回收
跳昼,所以第二次調(diào)用block,0x7fcb1bd22390對象還在內(nèi)存中肋乍。
結(jié)論
block內(nèi)部的obj 指針是外部obj指針的拷貝鹅颊,有2個指針指向同一個NSObject對象,但只將外部的obj指針置為nil,NSObject對象的引用計數(shù)不為0墓造,無法回收堪伍。
// 弱引用
- (void)blockVariableWeakReferenceTest
{
NSLog(@"\n");
NSObject *obj = [[NSObject alloc] init];
BLog(@"StrongRef obj",obj);
__weak NSObject *weakObj = obj;
BLog(@"WeakRef weakObj", weakObj);
void(^testBlock)()= ^(){
BLog(@"weakObj in block",weakObj);
};
testBlock();
obj = nil;
testBlock();
}
運行結(jié)果
位置和指針變量名:StrongRef obj ,指針內(nèi)存地址:0x7fff543d0c98, 指針值:0x7fcb1bd2fee0 ,指向的對象:<NSObject: 0x7fcb1bd2fee0>
位置和指針變量名:WeakRef weakObj ,指針內(nèi)存地址:0x7fff543d0c90, 指針值:0x7fcb1bd2fee0 ,指向的對象:<NSObject: 0x7fcb1bd2fee0>
位置和指針變量名:weakObj in block ,指針內(nèi)存地址:0x7fcb1bc072b0, 指針值:0x7fcb1bd2fee0 ,指向的對象:<NSObject: 0x7fcb1bd2fee0>
位置和指針變量名:weakObj in block ,指針內(nèi)存地址:0x7fcb1bc072b0, 指針值:0x0 ,指向的對象:(null)
結(jié)論
分析:
- 方法內(nèi)部的weakObj變量在棧中,變量內(nèi)存地址0x7fff543d0c98觅闽,所指向的對象<NSObject: 0x7fcb1bd2fee0>在堆中帝雇,內(nèi)存地址0x7fcb1bd2fee0,它的引用計數(shù)+1谱煤。
- weakObj變量也在棧中摊求,內(nèi)存為0x7fff543d0c90,所指向的對象也是<NSObject: 0x7fcb1bd2fee0>,弱引用禽拔,所以0x7fcb1bd2fee0對象的引用計數(shù)不增加刘离,仍然為1.
- Block中weakObj它的內(nèi)存地址是0x7fcb1bc072b0室叉,跟外部的weakObj不同,但是所指向的對象也是0x7fcb1bd2fee0硫惕,弱引用茧痕,0x7fcb1bd2fee0對象引用計數(shù)還是不增加,仍然是1恼除。
-在外部將obj(地址0x7fff543d0c98)置為nil后踪旷,外部的obj不再指向<NSObject: 0x7fcb1bd2fee0>對象,0x7fcb1bd2fee0對象的引用計數(shù)-1豁辉,引用計數(shù)為0令野,ARC回收0x7fcb1bd2fee0對象內(nèi)存,并將指向它的弱引用指針賦值為nil,所以第二次調(diào)用block徽级,0x7fcb1bd2fee0對象不在在內(nèi)存中气破。
Block生命周期內(nèi)的對象安全
在block中__weak聲明的指針去引用對象 可以避免循環(huán)引用的問題,但是當(dāng)外部對象被釋放了餐抢,block 內(nèi)部會訪問不到這個對象. 這種問題如何解決呢现使?先來看一段代碼:
//多線程時Block生命周期內(nèi)對象安全
- (void)blockVariableMutiThreadTest
{
NSObject *obj = [[NSObject alloc]init]; //obj強引用,<NSObject: 0x7f9413c1c040>對象引用計數(shù)+1,=1
BLog(@"obj", obj);
__weak NSObject *weakObj = obj;//weakObj弱引用,<NSObject: 0x7f9413c1c040>對象引用計數(shù)不變旷痕,=1
BLog(@"weakObj-0", weakObj);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
__strong NSObject *strongObj = weakObj; //strongObj強引用,<NSObject: 0x7f9413c1c040>對象引用計數(shù)+1碳锈,=2
sleep(3);
BLog(@"weakObj - block", weakObj);
BLog(@"strongObj - block", strongObj);
});
sleep(1);
obj = nil; //obj被置為nil,<NSObject: 0x7f9413c1c040>對象引用計數(shù)-1欺抗,=1
BLog(@"weakObj-1", weakObj); //沒被釋放
sleep(4); //block在異步線程中執(zhí)行完畢(在另一塊內(nèi)存中執(zhí)行)售碳,block內(nèi)存被釋放,<NSObject: 0x7f9413c1c040>對象引用計數(shù)-1绞呈,=0团滥;ARC開始把0x7f9413c1c040對象內(nèi)存回收,把弱引用weakObj置為nil
BLog(@"weakObj-2", weakObj);
}
執(zhí)行結(jié)果如下:
位置和指針變量名:obj ,指針內(nèi)存地址:0x7fff51888c98, 指針值:0x7f9413c1c040 ,指向的對象:<NSObject: 0x7f9413c1c040>
位置和指針變量名:weakObj-0 ,指針內(nèi)存地址:0x7fff51888c90, 指針值:0x7f9413c1c040 ,指向的對象:<NSObject: 0x7f9413c1c040>
位置和指針變量名:weakObj-1 ,指針內(nèi)存地址:0x7fff51888c90, 指針值:0x7f9413c1c040 ,指向的對象:<NSObject: 0x7f9413c1c040>
位置和指針變量名:weakObj - block ,指針內(nèi)存地址:0x7f9413d9a880, 指針值:0x7f9413c1c040 ,指向的對象:<NSObject: 0x7f9413c1c040>
位置和指針變量名:strongObj - block ,指針內(nèi)存地址:0x1187e2e08, 指針值:0x7f9413c1c040 ,指向的對象:<NSObject: 0x7f9413c1c040>
位置和指針變量名:weakObj-2 ,指針內(nèi)存地址:0x7fff51888c90, 指針值:0x0 ,指向的對象:(null)
總結(jié):
多線程的時候报强,在 block 外部
用__weak
聲明的變量指向一個對象, 通過把__weak聲明的變量值賦值給block內(nèi)部
的__strong
變量```灸姊,實現(xiàn)在block內(nèi)對該對象進行強引用,這樣可以在block生命周期內(nèi)保留該對象不被釋放秉溉,在block生命周期結(jié)束后力惯,對象內(nèi)存被釋放。
block修改外部變量 __block變量
- (void)blockVariable
{
// 使用__block
NSObject *obj = [[NSObject alloc]init];
BLog(@"obj",obj); // 1
__block NSObject *blockObj = obj;
obj = nil;
BLog(@"外部blockObj -1",blockObj); // 2
void(^testBlock)() = ^(){
BLog(@"內(nèi)部blockObj - block",blockObj); // 5
NSObject *obj2 = [[NSObject alloc]init];
BLog(@"內(nèi)部obj2",obj2); // 6
blockObj = obj2;
BLog(@"blockObj - block",blockObj); // 7
};
NSLog(@"%@",testBlock); // 3
BLog(@"外部blockObj -2",blockObj); // 4
testBlock();
BLog(@"外部blockObj -3",blockObj); // 8
?
}
?
運行結(jié)果
位置和指針變量名:obj ,指針內(nèi)存地址:0x7fff5dd2ec78, 指針值:0x7fa082661a00 ,指向的對象:<NSObject: 0x7fa082661a00>
位置和指針變量名:外部blockObj -1 ,指針內(nèi)存地址:0x7fff5dd2ec70, 指針值:0x7fa082661a00 ,指向的對象:<NSObject: 0x7fa082661a00>
<__NSMallocBlock__: 0x7fa084906fa0> ------ 這是block地址和類型
位置和指針變量名:外部blockObj -2 ,指針內(nèi)存地址:0x7fa084905838, 指針值:0x7fa082661a00 ,指向的對象:<NSObject: 0x7fa082661a00>
位置和指針變量名:內(nèi)部blockObj - block ,指針內(nèi)存地址:0x7fa084905838, 指針值:0x7fa082661a00 ,指向的對象:<NSObject: 0x7fa082661a00>
位置和指針變量名:內(nèi)部obj2 ,指針內(nèi)存地址:0x7fff5dd2eba8, 指針值:0x7fa084916660 ,指向的對象:<NSObject: 0x7fa084916660>
位置和指針變量名:blockObj - block ,指針內(nèi)存地址:0x7fa084905838, 指針值:0x7fa084916660 ,指向的對象:<NSObject: 0x7fa084916660>
位置和指針變量名:外部blockObj -3 ,指針內(nèi)存地址:0x7fa084905838, 指針值:0x7fa084916660 ,指向的對象:<NSObject: 0x7fa084916660>
分析:
- 第3處日志打印了一個testBlock對象召嘶,blockObj的地址發(fā)生變化父晶。此時,
block對象
從椗拷貝到堆上甲喝,__block變量blockObj,也被拷貝到堆上铛只。block對象擁有
blockObj指針指向的對象埠胖。注意:這是個強引用哦糠溜。 - 關(guān)注4到8 處日志,用__block關(guān)鍵字聲明blockObj指針后直撤,block內(nèi)外的變量blockObj都是0x7fa084905838非竿,也就是block內(nèi)外的blockObj指針是同一個指針。
- block內(nèi)部改變 blockObj指針指向的對象谋竖,改動在 block外部可見红柱。
關(guān)于 block訪問外部變量原理
在oc,在block中直接訪問外部變量蓖乘,訪問的是外部變量的copy锤悄。用clang后將 .m翻譯為.cpp文件后發(fā)現(xiàn),外部函數(shù)是通過傳值方式
將變量值傳給block(block結(jié)構(gòu)體嘉抒、block最終要執(zhí)行的函數(shù)代碼).
使用了__block后铁蹈,外部函數(shù)是通過指針傳遞
,將變量傳遞到 block 內(nèi)众眨,所以可以修改變量值.
Block在內(nèi)存中的位置
Block作為C語言的擴展握牧,并不是高新技術(shù),和其他語言的閉包或lambda表達式是一回事娩梨。需要注意的是由于Objective-C在iOS中不支持GC機制沿腰,使用Block必須自己管理內(nèi)存,而內(nèi)存管理正是使用Block坑最多的地方狈定,錯誤的內(nèi)存管理 要么導(dǎo)致return cycle內(nèi)存泄漏要么內(nèi)存被提前釋放導(dǎo)致crash颂龙。 Block的使用很像函數(shù)指針,不過與函數(shù)最大的不同是:Block可以訪問函數(shù)以外纽什、詞法作用域以內(nèi)的外部變量的值措嵌。換句話說,Block不僅 實現(xiàn)函數(shù)的功能芦缰,還能攜帶函數(shù)的執(zhí)行環(huán)境企巢。
可以這樣理解,Block其實包含兩個部分內(nèi)容:
- Block執(zhí)行的代碼让蕾,這是在編譯的時候已經(jīng)生成好的浪规;
- 一個包含Block執(zhí)行時需要的所有外部變量值的數(shù)據(jù)結(jié)構(gòu)。 Block將使用到的探孝、作用域附近的變量建立一份快照拷貝
根據(jù)Block在內(nèi)存中的位置分為三種類型:_NSConcreteGlobalBlock笋婿,_NSConcreteMallocBlock, _NSConcreteStackBlock。
-
_NSConcreteGlobalBlock
:類似函數(shù)顿颅,位于text段缸濒; -
_NSConcreteStackBlock
:位于棧內(nèi)存,函數(shù)返回后Block將無效; -
_NSConcreteMallocBlock
:位于堆內(nèi)存庇配。
_NSConcreteGlobalBlock:
Blocks that don't capture any variables are global blocks. Since all instances of the block are the same, the compiler can just allocate one copy statically for the life of the program斩跌。
_NSConcreteStackBlock 和 _NSConcreteMallocBlock:
Blocks that capture variables (closures) are either stack or heap (malloc) blocks. Blocks start out on the stack, as stack blocks. When a stack block is copied for the first time, it is moved to the heap. Copying a heap block does not create another copy; but simply retains it.
在開啟 ARC 時,大部分情況下編譯器通常會將創(chuàng)建在棧上的 block 自動拷貝到堆上讨永。
- 當(dāng) block 作為函數(shù)返回值返回時,編譯器自動將 block 作為 _Block_copy 函數(shù)遇革,效果等同于 block 直接調(diào)用 copy 方法卿闹;
- 當(dāng) block 被賦值給 __strong id 類型的對象或 block 的成員變量時,編譯器自動將 block 作為 _Block_copy 函數(shù)萝快,效果等同于 block 直接調(diào)用 copy 方法锻霎;
- 當(dāng) block 作為參數(shù)被傳入方法名帶有 usingBlock 的 Cocoa Framework 方法或 GCD 的 API 時。這些方法會在內(nèi)部對傳遞進來的 block 調(diào)用 copy 或 _Block_copy 拷貝;
ARC中自動copy block的例子
- (void)blockObjectInMemory
{
// global block
void (^globalBlockInMemory)(int number) = ^(int number){
printf("%d \n",number);
};
globalBlockInMemory(90);
BLog(@"global block %@", globalBlockInMemory);
// malloc block
int outVariable = 100;
void (^mallocBlockInMemory)(int number) = ^(int number){
printf("%d \n",outVariable+number);
};
BLog(@"stackBlock block %@", mallocBlockInMemory); // ARC 自動將棧中block拷貝到堆上
}
位置和指針變量名:global block %@ ,指針內(nèi)存地址:0x7fff5b422c78, 指針值:0x1047e01f0 ,指向的對象:<__NSGlobalBlock__: 0x1047e01f0>
位置和指針變量名:stackBlock block %@ ,指針內(nèi)存地址:0x7fff5b422c68, 指針值:0x7fe6849085f0 ,指向的對象:<__NSMallocBlock__: 0x7fe6849085f0>
- (id)returnBlock
{
int outVariable = 100;
void (^mallocBlockInMamory)(void) = ^(void){
NSLog(@"in block");
};
BLog(@" block ", mallocBlockInMamory);
return mallocBlockInMamory;
}
- (void)blockInmemory
{
id block = [self returnBlock];
BLog(@"a block %@", block);
}
位置和指針變量名: block ,指針內(nèi)存地址:0x7fff516a9c30, 指針值:0x10e559250 ,指向的對象:<__NSGlobalBlock__: 0x10e559250>
位置和指針變量名:a block %@ ,指針內(nèi)存地址:0x7fff516a9c78, 指針值:0x10e559250 ,指向的對象:<__NSGlobalBlock__: 0x10e559250>
在沒有ARC之前揪漩,由于ARC 自動將棧中block拷貝到堆上旋恼,所以當(dāng)returnBlock函數(shù)退出,在棧中內(nèi)存釋放后奄容,仍然可以訪問到block對象冰更。
ARC 中需要手動拷貝Block的例子
在以下情形中, block 會從棸豪眨拷貝到堆:
- 當(dāng) block 調(diào)用 copy 方法時蜀细,如果 block 在棧上,會被拷貝到堆上戈盈;
- 當(dāng) block 作為函數(shù)返回值返回時奠衔,編譯器自動將 block 作為 _Block_copy 函數(shù),效果等同于 block 直接調(diào)用 copy 方法塘娶;
- 當(dāng) block 被賦值給 __strong id 類型的對象或 block 的成員變量時归斤,編譯器自動將 block 作為 _Block_copy 函數(shù),效果等同于 block 直接調(diào)用 copy 方法刁岸;
- 當(dāng) block 作為參數(shù)被傳入方法名帶有 usingBlock 的 Cocoa Framework 方法或 GCD 的 API 時脏里。這些方法會在內(nèi)部對傳遞進來的 block 調(diào)用 copy 或 _Block_copy 進行拷貝;
其他情況需要手動拷貝。
- (void)stackBlockInMemory
{
NSArray *array = [self getBlockArray];
id block = array[0];
BLog(@"block %@", block);
}
- (id)getBlockArray
{
int val = 10;
return [[NSArray alloc] initWithObjects:
^{NSLog(@"value:%d", val);},
^{NSLog(@"value:%d", val);}, nil];
}
程序會報EXC_BAD_ACCESS ,getBlockArray返回的數(shù)組里面的 block 是不可訪問的虹曙。
手動copy后膝宁,block拷貝到堆上,getBlockArray函數(shù)返回的棧幀被銷毀后根吁,仍可以訪問堆中的block拷貝员淫。
- (id)getBlockArray
{
int val = 10;
return [[NSArray alloc] initWithObjects:
[^{NSLog(@"value:%d", val);} copy],
[^{NSLog(@"value:%d", val);} copy], nil];
}
Block中造成內(nèi)存泄漏的一些場景
推薦文章:
http://www.tanhao.me/pieces/310.html/