說明: <1>閱讀本問,請參照block前述文章加以理解; <2>環境:ARC; <3>變數類型:基本數據類型或者對象類型的auto局部變數; 一、三種情形 //代碼 //列印 //clang:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc - ...
說明:
<1>閱讀本問,請參照block前述文章加以理解;
<2>環境:ARC;
<3>變數類型:基本數據類型或者對象類型的auto局部變數;
一、三種情形
//代碼
void test1() { int num = 10; __block int age = 20; Person *per = [[Person alloc] init]; void(^block)(void) = ^{ NSLog(@"%d %d %p", num, age, per); }; block(); }
//列印
2019-01-16 15:42:38.974947+0800 MJ_TEST[2405:192414] 10 20 0x100654220 2019-01-16 15:42:38.975258+0800 MJ_TEST[2405:192414] -[Person dealloc] Program ended with exit code: 0
//clang:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-9.0.0 main.m
struct __Block_byref_age_0 { void *__isa; __Block_byref_age_0 *__forwarding; int __flags; int __size; int age; }; struct __test1_block_impl_0 { struct __block_impl impl; struct __test1_block_desc_0* Desc; int num; Person *__strong per; __Block_byref_age_0 *age; // by ref __test1_block_impl_0(void *fp, struct __test1_block_desc_0 *desc, int _num, Person *__strong _per, __Block_byref_age_0 *_age, int flags=0) : num(_num), per(_per), age(_age->__forwarding) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __test1_block_copy_0(struct __test1_block_impl_0*dst, struct __test1_block_impl_0*src) {_Block_object_assign((void*)&dst->age, (void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_assign((void*)&dst->per, (void*)src->per, 3/*BLOCK_FIELD_IS_OBJECT*/);} static void __test1_block_dispose_0(struct __test1_block_impl_0*src) {_Block_object_dispose((void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_dispose((void*)src->per, 3/*BLOCK_FIELD_IS_OBJECT*/);} static struct __test1_block_desc_0 { size_t reserved; size_t Block_size; void (*copy)(struct __test1_block_impl_0*, struct __test1_block_impl_0*); void (*dispose)(struct __test1_block_impl_0*); } void test1() { int num = 10; __attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 20}; Person *per = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init")); void(*block)(void) = ((void (*)())&__test1_block_impl_0((void *)__test1_block_func_0, &__test1_block_desc_0_DATA, num, per, (__Block_byref_age_0 *)&age, 570425344)); ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); }
分析:根據前述文章可知
<1>ARC模式下,block對象(等號右邊大括弧)被強指針(等號左邊)變數持有(預設為strong)時,系統會自動將block對象從棧區copy到堆區;
<2>當訪問實例對象或者__block修飾的基礎數據類型變數時,block對象結構體中自動會增加兩個函數指針——copy和dispose,分別指向__test1_block_copy_0函數和__test1_block_dispose_0函數;
<3>__block修飾的基礎數據類型的變數,系統會自動生成一個新的對象(__Block_byref_age_0),block對象通過該新的對象指針變數(__Block_byref_age_0 *age)來訪問基礎數據類型變數(int age);
<4>num:直接被block捕捉到其結構體內部,隨block一起copy到堆區;其隨block對象本身一起銷毀(不管是棧區的block還是堆區的block);
<5>per:類對象的訪問,當block對象被copy到堆區時,block對象會通過copy函數指針來調用__test1_block_copy_0函數完成對per對象的拷貝;同時此時堆區的block對象會根據該類對象的類型(strong還是weak)來決定對其強引用還是弱引用;
說明:所謂的強引用,是指指針指向某個對象(實質為,存放指針變數的記憶體指向對象的記憶體);所謂的指向,即是對該對象的引用計數加1即retain操作(即此時該指針持有該對象),但是一個對象可能被多個指針持有,因此realease並不必然導致對象銷毀(記憶體回收)而只是被釋放即某個指針變數不再指向(持有)該記憶體區域,只有當對象的引用計數為0時,系統才會自動回收其記憶體;
註:引用計數為對象的屬性,而非指針;
<6>__block修飾的基本數據類型的變數和對象類型變數,block被拷貝時,都會通過調用__test1_block_copy_0函數中_Block_object_assign函數完成對自身的拷貝——其中,_Block_object_assign函數的第三個參數,8表示由__block修飾的基本數據類型變數,3表示訪問的事對象類型變數(那麼,__block修飾的對象類型是多少呢?往下看);
當block對象銷毀時,二者都是通過調用__test1_block_dispose_0函數中的_Block_object_dispose函數來被銷毀;
問題:為什麼要增加copy和dispose兩個函數指針呢?因為block對象要持有上述兩種對象(__block修飾的基本數據類型生成的對象和per實例對象),那麼自然要對其進行記憶體管理,達到持有/釋放的可控目的;
因此,__block修飾生成的對象(會隨著block的copy而一起被copy到堆區,而拷貝後的棧區的結構體依然會存在,只不過其作用域結束後,系統會對其記憶體自動回收),block對象要對其持有,肯定是強引用,否則弱引用,該對象的記憶體管理不受控制,那麼block內部修改變數的值存在極大風險——這點沒問題;
補充:__weak不能修飾基礎類型變數
如上,我麽知道,__weak僅能修飾對象類型變數和block指針類型——為什麼?
以上我們已經分析過,__strong和__weak:前者指針持有對象達到對該對象記憶體管理可控的目的(只要該對象的引用計數>0,其記憶體就不可能被回收,指針就可以合法指向該記憶體),會進行retain操作即對象的retainCount會自動+1;後者不持有,該對象的記憶體管理不可控(什麼時候釋放,跟該指針沒關係),不會retain,對象的引用計數不會自動+1;
所以,__strong和__weak修飾的目的是對堆區的記憶體管理是否管控,而只有對象類型的變數(在堆區創建)才會有管控的問題,基礎數據類型變數起始是在棧區存儲,其記憶體(創建/回收)由系統自動管理;
二、__forwarding指針
我們在前面的文章提到,block對int類型的age變數的訪問,為什麼還要通過__forwarding指針而不是直接訪問__Block_byref_age_0結構體中的age變數呢?
分析:
<1>__forwarding指針本身是指向__Block_byref_age_0結構體本身;第一個age又是__Block_byref_age_0結構體類型的指針;第二個age是__Block_byref_age_0結構體中int型成員變數;
<2>在棧區:age->__forwarding->age <=> age->age 沒有任何問題;但是在堆區:因為block對象結構體會被copy到堆區,而原先留在棧區的block中的__forwarding指針會自動指向堆區的__Block_byref_age_0結構體;
<3>從上述分析,我們很清楚地知道,將__Block_byref_age_0結構體一併copy到堆區的目的就是堆區的block對象強引用該結構體,所以指向堆區的block對象的各類指針(包括對象本身)都可以通過該block對象達到對__Block_byref_age_0結構體中age變數值的改變等操作的目的,而不必擔心int型age變數記憶體隨時會被系統回收的風險
————問題來了:如果棧區的指針或者block對象本身要對age變數的值進行修改,是要面臨該風險的,那如何規避呢?
就是通過__forwarding指針,因為此時棧區的_Block_byref_age_0結構體中的__forwarding指針變數是指向堆區的_Block_byref_age_0結構體,除非堆區_Block_byref_age_0結構體記憶體被手動銷毀,否則會一直存在;
//圖解
三、__block修飾對象類型變數
//代碼
void test2() { Person *per = [[Person alloc] init]; per.age = 20; __block Person *blockPer = per; void(^block)(void) = ^{ blockPer.age = 30; NSLog(@"%d", blockPer.age); }; block(); }
//列印
2019-01-17 11:17:12.020409+0800 MJ_TEST[1304:76856] 30 2019-01-17 11:17:12.020869+0800 MJ_TEST[1304:76856] -[Person dealloc] Program ended with exit code: 0
//clang
struct __Block_byref_blockPer_1 { void *__isa; __Block_byref_blockPer_1 *__forwarding; int __flags; int __size; void (*__Block_byref_id_object_copy)(void*, void*); void (*__Block_byref_id_object_dispose)(void*); Person *blockPer; }; struct __test2_block_impl_0 { struct __block_impl impl; struct __test2_block_desc_0* Desc; __Block_byref_blockPer_1 *blockPer; // by ref __test2_block_impl_0(void *fp, struct __test2_block_desc_0 *desc, __Block_byref_blockPer_1 *_blockPer, int flags=0) : blockPer(_blockPer->__forwarding) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __test2_block_copy_0(struct __test2_block_impl_0*dst, struct __test2_block_impl_0*src) {_Block_object_assign((void*)&dst->blockPer, (void*)src->blockPer, 8/*BLOCK_FIELD_IS_BYREF*/);} static void __test2_block_dispose_0(struct __test2_block_impl_0*src) {_Block_object_dispose((void*)src->blockPer, 8/*BLOCK_FIELD_IS_BYREF*/);} static struct __test2_block_desc_0 { size_t reserved; size_t Block_size; void (*copy)(struct __test2_block_impl_0*, struct __test2_block_impl_0*); void (*dispose)(struct __test2_block_impl_0*); }
分析:
<1>我們發現,__block修飾的blockPerl實例對象,系統也會自動生成一個新的對象__Block_byref_blockPer_1;被引用的類對象以指針的形式存在於該結構體中(Person *blockPer),該指針指向[[Person alloc] init]這個實例對象(位於堆區);
<2>在__Block_byref_blockPer_1結構體中,還存在__Block_byref_id_object_copy和__Block_byref_id_object_dispose兩個函數指針,分別指向__Block_byref_id_object_copy_131函數和__Block_byref_id_object_dispose_131函數(作用同上),如下:
static void __Block_byref_id_object_copy_131(void *dst, void *src) { _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131); } static void __Block_byref_id_object_dispose_131(void *src) { _Block_object_dispose(*(void * *) ((char*)src + 40), 131); }
很顯然,這兩個函數的作用也是針對某個對象的記憶體管理,那是哪個對象呢?
首先,_Block_object_assign和_Block_object_dispose的第三個參數為131,是指__block修飾實例對象的情形;
其次,dst是__Block_byref_blockPer_1對象的地址,加40是什麼?我們算出Person *blockPer指針的地址偏移量正好為40(8+8+4+4+8+8,指針變數占8個位元組),那麼可以肯定,上述兩個函數就是對Person *blockPer指向的實例對象[[Person alloc] init]的記憶體管理;
所以,我們可以推測出以下結構:block對象__test2_block_impl_0通過其內部成員變數blockPer持有__Block_byref_blockPer_1對象,而__Block_byref_blockPer_1對象又通過其內部成員變數blockPer持有[[Person alloc] init]實例對象;
我們知道,前者通過_test2_block_copy_0函數和__test2_block_dispose_0函數進行記憶體管理,其持有必定是強引用,,這點沒問題;而後者的持有是通過__Block_byref_id_object_copy_131函數和__Block_byref_id_object_dispose_131函數進行記憶體管理,但其持有是強引用還是弱引用呢?往下看;
//__weak修飾
__block __weak Person *blockPer = per;
//clang:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-9.0.0 main.m(註:要設置runtime,否則編譯會報錯)
struct __Block_byref_blockPer_1 { void *__isa; __Block_byref_blockPer_1 *__forwarding; int __flags; int __size; void (*__Block_byref_id_object_copy)(void*, void*); void (*__Block_byref_id_object_dispose)(void*); Person *__weak blockPer; };
//列印
2019-01-17 12:17:20.776779+0800 MJ_TEST[1598:105742] 30 2019-01-17 12:17:20.777113+0800 MJ_TEST[1598:105742] -[Person dealloc] Program ended with exit code: 0
分析:
<1>我們發現,如果沒有__weak修飾,blockPer格式為Person *blockPer,預設為strong類型;__weak修飾後,則會變成weak類型;
<2>根據之前的分析,其實__Block_byref_blockPer_1對象對[[Person alloc] init]實例對象的引用,取決於指向該實例對象的指針類型(因為對象引用是指針傳遞,前面已講過);
------!!!但是,該規則僅限於ARC模式的情形,MRC模式下,如果指針是strong類型,系統並不會執行retain操作!!!
這裡有個問題,為什麼__weak修飾後,Person實例對象列印前沒有被銷毀呢?因為該實例對象的作用域在test2()函數體內,而block的回調也在函數體內,因此回調時,該實例對象並沒有被銷毀;
接下來,我們可以驗證下:
分析:此時,block回調前,Person實例對象就被銷毀了,說明block對象對實例對象的引用取決於Person對象指針的引用類型;
我們再切換到MRC模式下看看:
//代碼
void test4() { Person *per = [[Person alloc] init]; MyBlock block = [^{ NSLog(@"%p", per); } copy]; [per release]; block(); [block release]; }
//列印
2019-01-17 13:52:05.667590+0800 MJ_TEST[2106:151409] 0x10061add0 2019-01-17 13:52:05.668200+0800 MJ_TEST[2106:151409] -[Person dealloc] Program ended with exit code: 0
分析:當per指針象release時,[[Person alloc] init]實例對象並沒有被釋放,而當block指針release時,[[Person alloc] init]實例對象才被釋放(block對象不再持有該實例對象),這也印證了上述的分析;
四、註意
針對第三點(__block修飾對象類型的auto局部變數),系統生成的__block對象(__Block_byref_blockPer_1),其通過內部成員指針變數(Person *__weak blockPer)來持有實例對象([[Person alloc] init]),持有的類型取決於成員指針變數blockPer的類型(強指針則強引用,弱指針則弱引用)——該規則,在第三點情形下並且在MRC模式下,如果是強指針類型,則系統不會進行retain操作(除此,其他情況不受影響)!————所以,在MRC模式下,__block修飾對象類型的auto局部指針變數,新生成的__block對象永遠不可能強引用該指針指向的實例對象!