前言
上一篇我們講完了block的基礎(chǔ)知識,這一篇我們就來看看block的底層原理。話不多說,我們創(chuàng)建一個testBlock.c的文件,輸入以下代碼:
#include "stdio.h"
int main() {
int a = 5;
void(^block)(void) = ^{
printf("BLOCK_TEST==%d", a);
};
block();
return 0;
}
然后對代碼編譯成.cpp文件,此時main函數(shù)就變成下面的樣子:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : 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; // bound by copy
printf("BLOCK_TEST==%d", a);
}
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 a = 5;
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}
使用clang指令的時候,需要注意不要使用字面量。
block分析
在對代碼的分析之前我們先拋出幾個問題:
-
block的本質(zhì)到底是什么 -
block為什么需要調(diào)用block() -
block是如何截獲外界變量的 -
__block的實現(xiàn)
block本質(zhì)
在Objective-C中,block是一個對象,從編譯的結(jié)果來看,block在本質(zhì)上還是一個結(jié)構(gòu)體struct。而我們通常所說的block是一個匿名函數(shù)也能提現(xiàn)出來,比如例子中,系統(tǒng)默認給block分配了一個函數(shù)名稱__main_block_impl_0,其參數(shù)分別是(void *)__main_block_func_0、__main_block_desc_0_DATA、a;
調(diào)用block()
而block的聲明是:
((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a))
實現(xiàn)則是:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy 值拷貝
printf("BLOCK_TEST==%d", a);
}
使用函數(shù)就需要調(diào)用,block的最終執(zhí)行調(diào)用就是block->FuncPtr(block)。
捕獲外界變量
通過編譯結(jié)果,可以看到__main_block_impl_0這個結(jié)構(gòu)體有一個int a的元素,而結(jié)構(gòu)體內(nèi)部的__main_block_impl_0函數(shù)有: a(_a)這么一個賦值過程。我們可以把源代碼中int a相關(guān)的代碼刪除,重新編譯,會發(fā)現(xiàn)得到的結(jié)果中并沒有相關(guān)的參數(shù)和賦值過程。這說明block捕獲外界變量的時候是自己創(chuàng)建一個同名變量將其進行復(fù)制操作,賦值之后,block內(nèi)部的變量已經(jīng)和外部變量沒有了關(guān)系。所以我們在《block(一)-初探》中有一個例子:
typedef int(^myBlock)(int a, int b);
int d = 10;
myBlock mb = ^int(int a, int b) {
return a + b + d;
};
d = 5;
NSLog(@"==myBlock==%d==", mb(1, 2)); // 13
而且我們可以看到,在block調(diào)用函數(shù)的時候,還會執(zhí)行一次創(chuàng)建臨時變量賦值的操作。int a = __cself->a,這又不是同一個變量。所以當(dāng)我們在block內(nèi)部直接對外部的變量進行操作(賦值)的時候,其實操作的內(nèi)部的同名臨時變量,而不是外部的變量。所以block無法直接給外部截獲的變量賦值,因為它在自己內(nèi)部生成了一個同名臨時變量,所有的操作都是內(nèi)部的臨時變量。那么我們需要在block內(nèi)部處理截獲的外部變量該怎么辦呢?答案是使用__block。
block源碼
通過源碼我們可以看出block在底層的結(jié)構(gòu)如下:
struct Block_layout {
void *isa; // isa指向
volatile int32_t flags; // contains ref count 標志狀態(tài)
int32_t reserved;
BlockInvokeFunction invoke; // 函數(shù)執(zhí)行
struct Block_descriptor_1 *descriptor; // block的附加描述信息 如size等
// imported variables
};
#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
uintptr_t reserved;
uintptr_t size;
};
flags是block的狀態(tài)標志位。含義如下:
enum {
BLOCK_DEALLOCATING = (0x0001), // runtime 標記正在釋放
BLOCK_REFCOUNT_MASK = (0xfffe), // runtime 存儲引用計數(shù)的值
BLOCK_NEEDS_FREE = (1 << 24), // runtime 是否增加或減少引用計數(shù)的值
BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler 是否擁有拷貝輔助函數(shù) 確定block是否存在Block_descriptor_2這個參數(shù)
BLOCK_HAS_CTOR = (1 << 26), // compiler: helpers have C++ code 是否有C++析構(gòu)函數(shù)
BLOCK_IS_GC = (1 << 27), // runtime 是否有垃圾回收
BLOCK_IS_GLOBAL = (1 << 28), // compiler 是否是全局block
BLOCK_USE_STRET = (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
BLOCK_HAS_SIGNATURE = (1 << 30), // compiler 是否擁有簽名
BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31) // compiler 確定Block_descriptor_3中的layout參數(shù)
};
另外還有兩個可選的參數(shù):
#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
// requires BLOCK_HAS_COPY_DISPOSE
BlockCopyFunction copy;
BlockDisposeFunction dispose;
};
#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
// requires BLOCK_HAS_SIGNATURE
const char *signature;
const char *layout; // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};
這兩個參數(shù)是在某種條件下才會存在,Block_descriptor_2需要flags是BLOCK_HAS_COPY_DISPOSE才可以,而Block_descriptor_3需要flags是BLOCK_HAS_SIGNATURE。
了解了block的內(nèi)部結(jié)構(gòu),我們再來看看block從全局block、棧block到堆block是怎么變化的。
首先,我們實現(xiàn)一個全局block:
void(^myBlock)(void) = ^ {
NSLog(@"==myBlock==");
};
在block處設(shè)置一個斷點,進入?yún)R編,objc_retainBlock、我們在此時讀一下寄存器,繼續(xù)跳轉(zhuǎn)會進入,_Block_copy,在_Block_copy的return處讀一下寄存器。結(jié)果如下圖所示:
這說明調(diào)用_Block_copy使得棧block變成了堆block。下面我們來看看其源碼:
// 傳入的對象
void *_Block_copy(const void *arg) {
struct Block_layout *aBlock;
if (!arg) return NULL;
aBlock = (struct Block_layout *)arg;
if (aBlock->flags & BLOCK_NEEDS_FREE) {
// 如果需要對引用計數(shù)進行處理,那就直接處理,處理完就返回
// block的引用計數(shù)是不由runtime下層處理,需要自己處理
// 這個地方處理的是堆區(qū)block
latching_incr_int(&aBlock->flags);
return aBlock;
}
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
// 如果是全局block 直接返回
return aBlock;
}
else {
// Its a stack block. Make a copy
// 棧區(qū)block 使用copy
// 先在堆區(qū)初始化一塊內(nèi)存空間
struct Block_layout *result =
(struct Block_layout *)malloc(aBlock->descriptor->size);
if (!result) return NULL;
// 將棧區(qū)的數(shù)據(jù)copy到堆區(qū)的空間
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
#if __has_feature(ptrauth_calls)
// Resign the invoke pointer as it uses address authentication.
result->invoke = aBlock->invoke;
#endif
// 設(shè)置標志位
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed
result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1
_Block_call_copy_helper(result, aBlock);
// 設(shè)置為_NSConcreteMallocBlock
result->isa = _NSConcreteMallocBlock;
return result;
}
}
// 處理引用計數(shù)
static int32_t latching_incr_int(volatile int32_t *where) {
while (1) {
int32_t old_value = *where;
if ((old_value & BLOCK_REFCOUNT_MASK) == BLOCK_REFCOUNT_MASK) {
return BLOCK_REFCOUNT_MASK;
}
if (OSAtomicCompareAndSwapInt(old_value, old_value+2, where)) {
return old_value+2;
}
}
}
從代碼,我們可以看出block的copy動作主要如下:
- 全局
block不做任何事情直接返回 - 堆區(qū)
block增加引用計數(shù)然后返回 - 棧區(qū)
block- 在堆區(qū)申請一塊內(nèi)存空間
- 將棧區(qū)的數(shù)據(jù)拷貝到堆區(qū)申請的空間
- 給相關(guān)標志位賦值,對
Block_descriptor_2做copy動作,將isa設(shè)置為_NSConcreteMallocBlock
block的引用計數(shù)是不由runtime下層處理,需要自己處理。
__block
下面我們再根據(jù)編譯的代碼來看看__block到底做了什么。先把下面代碼進行編譯:
int main(int argc, char * argv[]) {
@autoreleasepool {
__block NSString *name = [NSString stringWithFormat:@"%@", @"AAA"];
void(^myBlock)(void) = ^{
name = @"BBB";
NSLog(@"==name==%@==", name);
};
myBlock();
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
我們先來看看mian函數(shù)編譯之后的代碼:
int main(int argc, char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
__attribute__((__blocks__(byref))) __Block_byref_name_0 name = {
(void*)0,
(__Block_byref_name_0 *)&name,
33554432,
sizeof(__Block_byref_name_0),
__Block_byref_id_object_copy_131,
__Block_byref_id_object_dispose_131,
((NSString * _Nonnull (*)(id, SEL, NSString * _Nonnull, ...))(void *)objc_msgSend)((id)objc_getClass("NSString"), sel_registerName("stringWithFormat:"), (NSString *)&__NSConstantStringImpl__var_folders_nw_tqjtztpn1yq6w0_wmgdvn_vc0000gn_T_main_41740c_mi_0, (NSString *)&__NSConstantStringImpl__var_folders_nw_tqjtztpn1yq6w0_wmgdvn_vc0000gn_T_main_41740c_mi_1)
};
void(*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_name_0 *)&name, 570425344));
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
return UIApplicationMain(argc, argv, __null, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class"))));
}
}
__block修飾的name對象被轉(zhuǎn)化成了一個__Block_byref_name_0的結(jié)構(gòu)體,其源碼如下:
struct Block_byref {
void *isa;
struct Block_byref *forwarding;
volatile int32_t flags; // contains ref count
uint32_t size;
};
struct Block_byref_2 {
// requires BLOCK_BYREF_HAS_COPY_DISPOSE
BlockByrefKeepFunction byref_keep;
BlockByrefDestroyFunction byref_destroy;
};
struct Block_byref_3 {
// requires BLOCK_BYREF_LAYOUT_EXTENDED
const char *layout;
};
可以看出,__Block_byref_name_0中的(void*)0就是Block_byref的void *isa,(__Block_byref_name_0 *)&name即為struct Block_byref *forwarding,__Block_byref_id_object_copy_131即為BlockByrefKeepFunction byref_keep函數(shù)。
上面我們講述了block從棧到堆的拷貝過程。下面再來看看__Block_byref_name_0的拷貝動作。在block里我們傳入了一個&__main_block_desc_0_DATA的結(jié)構(gòu)體地址,該結(jié)構(gòu)體在初始化的時候傳入了__main_block_copy_0方法進行拷貝操作,實現(xiàn)如下:
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};
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->name, (void*)src->name, 8/*BLOCK_FIELD_IS_BYREF*/);
}
enum {
BLOCK_FIELD_IS_OBJECT = 3, // 截獲的是對象 __attribute__((NSObject)), block, ...
BLOCK_FIELD_IS_BLOCK = 7, // 截獲的是block變量
BLOCK_FIELD_IS_BYREF = 8, // 截獲的是__block修飾的對象
BLOCK_FIELD_IS_WEAK = 16, // 截獲的是__weak修飾的對象
BLOCK_BYREF_CALLER = 128, // called from __block (byref) copy/dispose support routines.
};
// 根據(jù)傳入的對象的類型
void _Block_object_assign(void *destArg, const void *object, const int flags) {
const void **dest = (const void **)destArg;
switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
case BLOCK_FIELD_IS_OBJECT:
_Block_retain_object(object);
*dest = object;
break;
case BLOCK_FIELD_IS_BLOCK:
*dest = _Block_copy(object);
break;
case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
case BLOCK_FIELD_IS_BYREF:
*dest = _Block_byref_copy(object);
break;
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
*dest = object;
break;
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK | BLOCK_FIELD_IS_WEAK:
*dest = object;
break;
default:
break;
}
}
分析上述源碼,可知,針對block或block_byrefs截獲的對象的類型進行不同的內(nèi)存管理處理:
- 截獲的變量是對象,只需要賦值,引用計數(shù)不做任何處理,因為對象的引用計數(shù)是
runtime底層自己處理的。
- 截獲的變量是對象,只需要賦值,引用計數(shù)不做任何處理,因為對象的引用計數(shù)是
static void (*_Block_retain_object)(const void *ptr) = _Block_retain_object_default;
static void _Block_retain_object_default(const void *ptr __unused) { }
- 如果截獲的變量是
block對象,調(diào)用_Block_copy方法。
- 如果截獲的變量是
void *_Block_copy(const void *arg) {
// 詳見上面的分析
}
- 如果截獲的變量是
__block對象,需要重新申請一塊堆內(nèi)存,然后將截獲的對象也就是上述例子中的__Block_byref_name_0結(jié)構(gòu)體賦值給新的結(jié)構(gòu)體,并將它們的forwarding指針都指向新生成的結(jié)構(gòu)體。其實也就是對__block修飾的對象做了一次拷貝動作,然后讓他們都指向同一塊內(nèi)存區(qū)域達到修改其中一個兩個都改變的目的。
static struct Block_byref *_Block_byref_copy(const void *arg) {
// 創(chuàng)建一個臨時變量
struct Block_byref *src = (struct Block_byref *)arg;
if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
// src points to stack
// 1.申請堆內(nèi)存空間
struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
// 2. 給新申請的空間賦值
copy->isa = NULL;
// byref value 4 is logical refcount of 2: one for caller, one for stack
copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
// copy的對象和源對象都指向堆內(nèi)存的拷貝地址
copy->forwarding = copy; // 堆拷貝指向自己
src->forwarding = copy; // ??截愔赶蚨褍?nèi)存
copy->size = src->size;
if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
// 處理desc2 內(nèi)存偏移取值
struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
copy2->byref_keep = src2->byref_keep;
copy2->byref_destroy = src2->byref_destroy;
if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) {
// 處理desc3
struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1);
struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1);
copy3->layout = src3->layout;
}
(*src2->byref_keep)(copy, src);
} else {
memmove(copy+1, src+1, src->size - sizeof(*src));
}
} else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
latching_incr_int(&src->forwarding->flags);
}
return src->forwarding;
}
在新生成的結(jié)構(gòu)體__Block_byref_name_0中,還有一個名為__Block_byref_id_object_copy_131的方法,該方法的實現(xiàn)如下:
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
該方法依然是調(diào)用了_Block_object_assign方法,不過是傳遞的參數(shù)不同,是目標對象偏移了40字節(jié),由于此時目標對象就是__Block_byref_name_0,偏移40字節(jié)正好是NSString *name。也就是說此時又是對name這個對象的內(nèi)存地址做了一次拷貝。
struct __Block_byref_name_0 {
void *__isa; // 8
__Block_byref_name_0 *__forwarding; // 8
int __flags; // 4
int __size; // 4
void (*__Block_byref_id_object_copy)(void*, void*); // 8
void (*__Block_byref_id_object_dispose)(void*); // 8
NSString *name;
};
所以,我們可以得出結(jié)論:__block修飾的外部變量,在block內(nèi)部能夠修改的主要原因在于3次拷貝。
-
block的拷貝,從棧內(nèi)存到堆內(nèi)存。
-
- 對新生成的結(jié)構(gòu)體的拷貝。
__block修飾的變量會生成一個名為__Block_byref_XXX_0結(jié)構(gòu)體,將原來的進行了封裝,然后把整個結(jié)構(gòu)體地址指針傳入block內(nèi)部。
- 對新生成的結(jié)構(gòu)體的拷貝。
- 對原來的對象的內(nèi)存的拷貝。
block的釋放
上面我們講過了block的持有過程,既然有持有,那就肯定有釋放。下面我們來看看block的釋放過程。
在&__main_block_desc_0_DATA的定義中,會傳入__main_block_dispose_0這樣一個函數(shù)與void (*dispose)(struct __main_block_impl_0*)方法相對應(yīng)。其函數(shù)實現(xiàn)如下:
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->name, 8/*BLOCK_FIELD_IS_BYREF*/);
}
void _Block_object_dispose(const void *object, const int flags) {
switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
case BLOCK_FIELD_IS_BYREF:
_Block_byref_release(object);
break;
case BLOCK_FIELD_IS_BLOCK:
_Block_release(object);
break;
case BLOCK_FIELD_IS_OBJECT:
_Block_release_object(object);
break;
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK | BLOCK_FIELD_IS_WEAK:
break;
default:
break;
}
}
- 需要釋放的如果是
__block修飾的對象,判斷如果應(yīng)該釋放,則調(diào)用方法銷毀創(chuàng)建的__block結(jié)構(gòu)。
- 需要釋放的如果是
static void _Block_byref_release(const void *arg) {
struct Block_byref *byref = (struct Block_byref *)arg;
byref = byref->forwarding;
if (byref->flags & BLOCK_BYREF_NEEDS_FREE) {
int32_t refcount = byref->flags & BLOCK_REFCOUNT_MASK;
os_assert(refcount);
if (latching_decr_int_should_deallocate(&byref->flags)) {
if (byref->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
struct Block_byref_2 *byref2 = (struct Block_byref_2 *)(byref+1);
(*byref2->byref_destroy)(byref);
}
free(byref);
}
}
}
- 需要釋放的如果是
block,判斷是否該釋放,如果應(yīng)該釋放則調(diào)用釋放block的方法。
- 需要釋放的如果是
void _Block_release(const void *arg) {
struct Block_layout *aBlock = (struct Block_layout *)arg;
if (!aBlock) return;
if (aBlock->flags & BLOCK_IS_GLOBAL) return;
if (! (aBlock->flags & BLOCK_NEEDS_FREE)) return;
if (latching_decr_int_should_deallocate(&aBlock->flags)) {
_Block_call_dispose_helper(aBlock);
_Block_destructInstance(aBlock);
free(aBlock);
}
}
- 需要釋放的如果是對象,則什么都不用做,
ARC下runtime底層自己處理。
- 需要釋放的如果是對象,則什么都不用做,
static void (*_Block_release_object)(const void *ptr) = _Block_release_object_default;
static void _Block_release_object_default(const void *ptr __unused) { }
block的簽名
我們在講述block的本質(zhì)的時候說了,block是匿名函數(shù),那么作為一個函數(shù),block是否也有自己的簽名。答案是肯定,上面我們的打印結(jié)果也體現(xiàn)出來了。
我們在講述block的源碼中提到block有兩個可選的參數(shù)Block_descriptor_2和Block_descriptor_3。而block的簽名信息就放在Block_descriptor_3中,一個名為signature的元素。
struct Block_descriptor_3 {
// requires BLOCK_HAS_SIGNATURE
const char *signature;
const char *layout; // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};
從上面的打印結(jié)果可以看出block的簽名樣子如下:
signature: "v8@?0"
其中v表示返回值是void,@?表示未知的對象,即為block。這和方法簽名是有所不同的,方法簽名一般是v@:這樣的形式(此處只說返回值為void的場景),:表示SEL。
總結(jié)
-
block的本質(zhì)是一個匿名函數(shù),也是一個對象,其底層實現(xiàn)是結(jié)構(gòu)體。 -
block既有聲明,也有調(diào)用。 -
block也有簽名,和方法簽名略有不同。 -
block截獲外部變量是在定義的時候生成一個同名的中間變量,該變量的初值就是外部變量在被截獲的時候的值,之后就與外部變量沒有關(guān)系。 -
__block修飾的外部變量在block內(nèi)部能夠修改的原因在于3次拷貝:-
block的拷貝,從棧內(nèi)存到堆內(nèi)存 - 將修飾的對象轉(zhuǎn)化為一個結(jié)構(gòu)體,將其拷貝到堆內(nèi)存。
- 將修飾的對象的內(nèi)存地址也進行了拷貝用以修改。
-
參考文獻:
蘋果官方文檔
《Objective-C高級編程iOS和OS X多線程和內(nèi)存管理》