[讀書筆記]iOS與OS X多線程和內存管理 [Blocks部分-3]

2.3.2 截獲自動變量
通過轉換後的源碼可以發現,Block語法中使用的自動變量被作爲成員變量追加到__main_block_impl_0結構體中,Block中沒有使用的自動變量不會被追加,所以Block的變量截獲只針對Block使用的自動變量。
源碼:
#include <stdio.h>//不導入庫文件無法運行
int main() {
    int val1=0;
   
int val2=10;
   
void(^testBlock)(void)=^{
       
printf("i am testBlock %d",val1);
    };
    val1=
100;
    testBlock();
}
轉換之後的代碼與Block不使用變量時的代碼區別如下:
struct __main_block_impl_0 {
 
struct __block_impl impl;
 
struct __main_block_desc_0* Desc;
  int val1;
 __main_block_impl_0(void*fp, struct __main_block_desc_0 *desc, int _val1, int flags=0) : val1(_val1) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct__main_block_impl_0 *__cself) {
 
int val1 = __cself->val1; // bound by copy
printf("i am testBlock %d",val1);
}
//調用構造函數
void(*testBlock)(void)=(void(*)())&__main_block_impl_0((void*)__main_block_func_0, &__main_block_desc_0_DATA, val1);
在初始化結構體實例時,根據傳遞給構造函數的參數對(自動變量val0追加的)成員變量進行初始化。這樣就截獲了自動變量的值。
總的來說,所謂截獲自動變量的值意味着在執行Block語法時,Block語法所使用的自動變量的值被保存到Block的結構體實例(即Block自身這個對象)中。
如之前所說,Block不能使用c語言數組類型的自動變量。可能的原因如下:Block截獲自動變量的方式是將自動變量賦值給成員變量,假設Block中使用了整形數組a[2],需要將a[2]賦值給成員變量b[2],即b[2]=a[2],而C語言規範不允許這種賦值,導致無法編譯。當然有其他方法來截獲自動變量,但Block似乎更遵循C規範。

2.3.3 __block說明符
前面說過,Block僅截獲自動變量的值,相當於對這個值拷貝了一份。這導致一方面自動變量值的改變不會影響Block中使用的那個值,另一方面在Block中無法更改自動變量的值。這樣就無法在Block中保存值了。有兩種方法來解決這個問題:
     第一,C語言中的靜態變量、靜態全局變量、全局變量允許Block改寫值。Block語法部分變換爲了C語言函數,從這個函數訪問靜態全局變量、全局變量是沒有問題的,但是轉換後的語法在原Block語法所在的函數之外,超出了靜態變量作用域。所以對於靜態變量,將其靜態變量指針傳遞給__main_block_impl_0結構體的構造函數並保存,這樣就解決了超出作用域使用變量的問題。

轉換前代碼:
int global_val=1;//全局變量
static int static_global_val=2;//靜態全局變量
int main() {
    static int static_val=3;//靜態變量
    void(^testBlock)(void)=^{
        static_val=
4;
       
static_global_val=5;
        global_val=6;
printf("%d,%d,%d",global_val,static_global_val,static_val);
    };
    testBlock();
}












轉換後部分代碼:
struct __main_block_impl_0 {
  struct __block_impl impl;
 
struct __main_block_desc_0* Desc;
 
int *static_val;//靜態變量被加到結構體中
  __main_block_impl_0(
void*fp, struct __main_block_desc_0 *desc, int *_static_val, int flags=0) : static_val(_static_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct__main_block_impl_0 *__cself) {
  int *static_val = __cself->static_val;// bound by copy
        (*static_val)=4;
        static_global_val=
5;//靜態全局變量和全局變量使用和轉換前一致。
        global_val=
6;//
        printf(
"%d,%d,%d",global_val,static_global_val,(*static_val));
    }

看起來自動變量也可以使用此方法來進行超出作用域的訪問,實際情況是在變量作用域結束的時候自動變量會被廢棄(靜態變量雖然也有作用域的限制,但是生命週期與程序相同),不能通過指針訪問原來的變量。相關內容下節說明
     第二,使用“__block說明符”更準確的表達方式是”__block存儲域類說明符(__block storage-class-specifier)”。在C語言中,有以下存儲域類說明符:typedef、extern、static、auto、register。__block說明符類似於static、auto和register說明符,用於指定將變量值設置到哪個存儲域中,如auto表示作爲自動變量存儲在棧中,static表示作爲靜態變量存儲在數據區中。

原代碼:
//__block修飾符
int main() {
   
__block int val=0;
   
void(^tetsBlock)(void)=^{val=2;};
}
轉換之後:




struct __Block_byref_val_0 {
 
void *__isa;
__Block_byref_val_0 *__forwarding;
 
int __flags;
 
int __size;
 
int val;
};
struct __main_block_impl_0 {
 
struct __block_impl impl;
 
struct __main_block_desc_0* Desc;
  __Block_byref_val_0 *val;
// by ref
  __main_block_impl_0(
void*fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val,int flags=0) : val(_val->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct__main_block_impl_0 *__cself) {
  __Block_byref_val_0 *val = __cself->val;
// bound by ref
(val->__forwarding->val)=
2;}
static void __main_block_copy_0(struct__main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val,8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct__main_block_impl_0*src) {_Block_object_dispose((void*)src->val,8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
 
void (*copy)(struct __main_block_impl_0*, struct__main_block_impl_0*);
 
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = {
0, sizeof(struct__main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main() {
   
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0,sizeof(__Block_byref_val_0),0};
   
void(*tetsBlock)(void)=(void(*)())&__main_block_impl_0((void*)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val,570425344);
}

可以看到,__block變量val在轉換之後的代碼中成爲了結構體實例(__Block_byref_val_0 *val;)的自動變量,即棧上生成的__Block_byref_val_0結構體實例。__Block_byref_val_0實例和__block變量賦值的代碼如下:
struct __Block_byref_val_0 {
 
void *__isa;
__Block_byref_val_0 *__forwarding;
 
int __flags;
 
int __size;
 
int val;
};
static void __main_block_func_0(struct__main_block_impl_0 *__cself) {
  __Block_byref_val_0 *val = __cself->val;
// bound by ref
(val->__forwarding->val)=2;
}
Block的__main_block_impl_0結構體實例持有(指向__block修飾變量的)__Block_byref_val_0結構體實例的指針。__Block_byref_val_0的成員變量__forwarding持有指向該實例自身的指針。賦值的時候通過__forwarding訪問變量val.成員變量__forwarding的作用在下節說明。另外__Block_byraf_val_0結構體並不在__main_block_impl_0 中定義,見圖:


這樣做是爲了在多個Block中使用同一__block變量。如下:
轉換前代碼:
int main() {
   
__block int val=0;
   
void(^tetsBlock1)(void)=^{val=2;};
   
void(^testBlock2)(void)=^{val=8;};
}
轉換後關鍵代碼:
int main() {
   
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0,sizeof(__Block_byref_val_0),0};
   
void(*tetsBlock1)(void)=(void(*)())&__main_block_impl_0((void*)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val,570425344);//同時使用了val這個變量
   
void(*testBlock2)(void)=(void(*)())&__main_block_impl_1((void*)__main_block_func_1, &__main_block_desc_1_DATA, (__Block_byref_val_0 *)&val,570425344);//
}
同理,如果想要在一個Block中使用多個__block變量,只需要增加Block的結構體成員變量與構造函數參數。

2.3.4 Block存儲域
此節需說明兩個問題:
  • Block超出變量作用域存在的理由(例如函數返回Block,返回後Block應當被釋放/廢除,卻仍能夠使用)
  • __block變量的結構體成員變量__forwarding的作用
Block的實質是棧上Block的結構體實例,__block變量的實質是棧上__block變量的結構體實例。從另一方面來說,Block又是OC的對象,其類爲_NSConcreteStackBlock.與此相關的類還有 _NSConcreteGlobalBlock、 _NSConcreteMallocBlock.對應的存儲區域如下:


之前使用Block的例子出現的都是_NSConcreteStackBlock,在使用全局變量的地方使用Block語法時,生成的Block爲_NSConcreteGlobalBlock類對象。如下
void(^tetsBlock1)(void)=^{};
int main() {    }
該Block的類: 
impl.isa = &_NSConcreteGlobalBlock;
在使用全局變量的地方使用Block語法時,因爲Block不截獲自動變量,Block用結構體實例的內容不依賴於執行時的狀態,整個程序中只需一個實例,因此可將Block存儲在與全局變量相同的數據區域。實際上,即使在函數中, 當Block不截獲自動變量時,也可將Block用結構體實例存儲在程序的數據區域。總結:在Block語法位於全局變量位置時或Block語法不截獲自動變量時,Block爲_NSConcreteGlobalBlock類對象,存儲在程序的數據區域。除此之外生成的Block爲_NSConcreteStackBlock對象,存儲在棧上。還有一個_NSConcreteMallocBlock類沒有用到,正是本節要解決的兩個問題的答案。
配置在全局變量上的Block從變量作用域外通過指針也可以安全地使用。但是設置在棧上的Block,如果其所屬變量作用域結束,該Block就被廢棄,由於__block變量也在棧上,__block變量也會被廢棄。Blocks提供了將Block和__block變量複製到堆上來解決此問題。複製到堆上的Block就屬於_NSConcreteMallocBlock類的對象,而__block變量的結構體成員變量__forwarding可以實現無論__block存儲在棧上還是堆上都能訪問__block變量。下節說明
Blocks如何讓將Block複製到堆上呢?一般情況下,如果ARC處於開啓狀態,編譯器會自動將Block從棧上覆制到堆上,可以看下面的返回Block的函數:

typedef int (^type_block)(int);
type_block func(int rate){
   
return ^(int count){return rate*count;};//在非ARC環境下會報錯:Returning block that lives on the local stack,在ARC下正常。
}
大致流程:
源碼在對應的ARC編譯器下轉換爲 
使用objc_retainBlock函數處理生成的Block對象tmp_block=objc_retainBlock(tmp_block)
objc_retainBlock實際就是_Block_copy函數,
/**
 * 
將通過Block語法生成的Block賦值給變量tmp_block
 */
// _Block_copy函數將棧上的Block複製到堆上,複製後將堆上的地址作爲指針賦值給變量tmp_block
tmp_block=_Block_copy(tmp_block);
// 將堆上的Block作爲OC對象註冊到autoreleasepool中並返回該對象。
return objc_autoreleaseReturnValue(tmp_block);
特殊情況下編譯器並不會自動複製Block,我們需要使用copy實例方法。這個特殊情況就是向方法或函數的參數中傳遞Block時。有時候方法中已經進行了相應處理,就不用手動複製了,如
  • Cocoa框架的方法且方法名中含有usingBlock時
  • Grand Central Dispatch的API
需要手動複製的例子:
-(id)getBlockArray{
   
int val=10;
   
return [[NSArray alloc]initWithObjects:^{NSLog(@"%d",val);},^{NSLog(@"%d",val);},nil];
}
使用:
//由於getBlockArray函數執行結束,棧上的Block被廢除,程序崩潰
   NSArray*array= [selfgetBlockArray];
   void (^aBlock)(void)=array[0];
   aBlock();
修改後代碼:
-(id)getBlockArray{
   
int val=8;
    return [[NSArrayalloc]initWithObjects:[^{NSLog(@"%d",val);}copy],[^{NSLog(@"%d",val);}copy],nil];
}

對不同類型的Block調用copy方法效果如下:
Block類
原存儲位置
複製效果
_NSConcreteStackBlock
棧區
從棧複製到堆
_NSConcreteGlobalBlock
程序的數據區域
無任何變化
_NSConcreteMallocBlock
堆區
引用計數增加
在不確定是否需要拷貝時進行拷貝。
在ARC中多次調用copy方法是不會造成內存泄露的,可以使用,原因如下:

源代碼:
    typedef void (^aBlock)(void);
    aBlock oneBlock;
    oneBlock =[[[oneBlock copy]copy]copy];
該代碼可理解爲:
    {
       
aBlock tmp=[oneBlock copy];
        oneBlock=tmp;
    }
    {
       
aBlock tmp=[oneBlock copy];
        oneBlock=tmp;
    }
    {
       
aBlock tmp=[oneBlock copy];
        oneBlock=tmp;
    }


提示:

ARC模式下賦值時系統會自動增加強引用
當對象被賦值爲nil,對象相關的強引用將被銷燬
加入註釋:
    oneBlock =[[[oneBlockcopy]copy]copy];
    {
       
//將堆上的Block賦值給tmptmp持有強引用的Block
       
aBlock tmp=[oneBlock copy];//執行copy後從棧到堆
       
//由於此時爲ARC模式,賦值後oneBlock持有堆上的Block的強引用,此時Block的持有者爲oneBlocktmp。(在非ARC模式下爲簡單賦值,oneBlock並未持有強引用,需手動增加引用計數,否則如果tmp被釋放,oneBlock將不可使用);
        oneBlock=tmp;
    }
    //超出tmp變量作用域,tmparc自動釋放,但Block仍被oneBlock引用,未被釋放
    {
       
//tmp持有堆上的Block的強引用
       
aBlock tmp=[oneBlock copy];
       
//賦值給oneBlock,oneBlock原先指向的堆上的Block強引用被釋放,原先指向的堆上的Block並未廢棄(tmp強引用)。oneBlock現在持有與tmp相同的強引用。現在堆上的BlockoneBlocktmp持有。
        oneBlock=tmp;
    }
   
//tmp指向的強引用被銷燬,堆上的Block由於被oneBlock持有,仍然存在。
   
//下面過程同上
    {
       
aBlock tmp=[oneBlock copy];
        oneBlock=tmp;
    }

示意圖:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章