重學OC第二十三篇:block
文章目錄
一、Block是什麼?
int main(int argc, const char * argv[]) {
int a = 0;
void (^aBlock)(void) = ^{
NSLog(@"%d", a);
};
aBlock();
}
用clang轉為cpp
- Block是帶有自動變數(區域性變數)的匿名函式,在OC中Block本質上就是一個物件(__main_block_impl_0結構體類比objc_object結構體)。
- Block截獲自動變數是Block語法表示式所使用的自動變數值被儲存到了Block的結構體例項中。
1.1 __block結構
上面的Block中的變數a是不允許進行修改的,原因是__main_block_func_0中把a進行了值拷貝,此時修改的只是臨時變數a,而不是__cself->a,會引發歧義,所以不允許更改。那__block為什麼可以更改變數
呢?把上面的int a = 0;用__block修飾,然後再來看clang後的程式碼。
可以看到加上__block後a變為了一個__Block_byref_a_0
型別的變數,對它的操作也變成了a->__forwarding->a
。具體可看下面1.2.2小節。
1.2 儲存域
建議直接去看《Obj-C高階程式設計》書中相關內容。
1.2.1 Block儲存域
將Block當作OC物件來看時,可看到它的isa指向_NSConcreteStackBlock,常用到的有3種:
類 | 設定物件的儲存域 | 區分 |
---|---|---|
_NSConcreteStackBlock | 棧 | 截獲自動變數 並且在該變數作用域內 |
_NSConcreteGlobalBlock | 程式的資料區域(.data區) | 記述全域性變數的地方有Block語法時(在定義全域性變數的地方定義Block);Block語法的表示式中不使用 應截獲的自動變數 時 |
_NSConcreteMallocBlock | 堆 | 當_NSConcreteStackBlock超出變數作用域ARC下 大多數情況下編譯器 進行適當判斷後 呼叫_Block_copy拷貝到堆上 |
在ARC下編譯器大多數情況會適當地進行判斷然後自動從棧複製到堆,那編譯器在什麼情況下不能判斷需要手動複製呢?
- 向方法或函式的引數中傳遞Block時
但是如果在方法或函式中適當地複製了傳遞過來的引數,那麼就不必在呼叫該方法或函式前手動複製了。以下方法或函式不用手動複製
。- Cocoa框架的方法且方法名中含有usingBlock等時
- GCD的API
下面來具體看段程式碼來理解
typedef void (^blk_t)(void);
NSArray *getBlockArray() {
int val = 10;
//ARC不會自動複製,需手動複製
return [[NSArray alloc] initWithObjects:^{NSLog(@"blk0: %d", val);}, ^{NSLog(@"blk1: %d", val);}, nil];
// return [[NSArray alloc] initWithObjects:[^{NSLog(@"blk0: %d", val);} copy], [^{NSLog(@"blk1: %d", val);} copy],nil];
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
void (^globalBlock)(void) = ^{ };
//__NSGlobalBlock__
NSLog(@"GlobalBlock is %@", globalBlock);
__block int a = 10;
void (^stackBlock)(void) = ^void { a++; };
//MRC __NSStackBlock__
NSLog(@"StackBlock is %@", stackBlock);
//ARC __NSMallocBlock__
NSLog(@"MallocBlock is %@", stackBlock);
NSArray *array = getBlockArray();
blk_t blk = (blk_t)[array objectAtIndex:0];
blk(); //如果沒有手動複製,崩潰。因getBlockArray()執行完後,棧上的Block被廢棄。
}
return 0;
}
1.2.2 __block變數儲存域
使用__block變數的Block從棧複製到堆上時,__block變數也會受到影響。
__block變數的配置儲存域 | Block從棧複製到堆時的影響 |
---|---|
棧 | 從棧複製到堆並被Block持有 |
堆 | 被Block持有 |
在一個Block中使用__block變數
在多個Block中使用__block變數
堆上Block被廢棄,它所使用的__block變數也就被釋放。
- 那麼__block變數用結構體成員變數__forwarding的原因是什麼?
通過Block的複製,__block變數也從棧複製到堆,此時可同時訪問棧上的__block變數和堆上的__block變數。原始碼如下:
__block int val = 0;
void (^blk)(void) = [^{++val;} copy];
++val;
blk();
NSLog(@"%d", val);
用clang轉以後
可以看出都是++(val.__forwarding->val),但是棧上的__block變數用結構體例項在___block從棧複製到堆上時,會將成員變數__forwording的值替換為複製目標上的block變數用結構體例項的地址。
通過該功能,無論是在Block語法中、Block語法外使用__block變數,還是__block變數配置在棧上或堆上,都可以順利地訪問同一個__block變數。
1.3 捕獲物件
NSNumber *val = @(0);
void (^blk)(void) = ^{NSLog(@"%@", val);};
blk();
在OC中,C語言結構體不能含有附有__strong修飾符的變數,因為編譯器不知道應何時時行C語言結構體的初始化和廢棄操作,不能很好地管理記憶體。但是OC的執行時庫能準確把握從棧複製到堆以及堆上的Block被廢棄的時機,因此Block結構體中即使含有附有__strong修飾符或__weak修飾符的變數,也可以恰當地進行初始化和廢棄。為此需要使用在__main_block_desc_0結構體中增加的成員變數copy和dispose,以及作為指標賦值給該成員變數的__main_block_copy_0函式和__main_block_dispose_0函式。
呼叫copy函式和dispose函式的時機:
函式 | 呼叫時機 |
---|---|
copy函式 | 棧上的Block複製到堆時 |
dispose函式 | 堆上的Block被廢棄時 |
那麼什麼時候棧上的Block會複製到堆呢?
- 呼叫Block的copy例項方法時
- Block作為函式返回值返回時
- 將Block賦值給附有__strong修飾符id型別的類或Block型別成員變數時
- 在方法名中含有usingBlock的Cocoa框架方法或GCD的API中傳遞Block時
那麼問題來了,在使用__block修飾的int a時也產生了copy、dispose方法,怎麼與物件區分的呢?
基本型別 | 物件 | __block修飾基本型別 | __block修飾物件 | |
---|---|---|---|---|
Block結構體 | 不加入copy、dispose成員變數 | 加入copy、dispose成員變數,flag為BLOCK_FIELD_IS_OBJECT | 加入copy、dispose成員變數,flag為BLOCK_FIELD_IS_BYREF | 加入copy、dispose成員變數,flag為BLOCK_FIELD_IS_BYREF |
__block變數結構體 | – | – | 不加入copy、dispose成員變數 | 加入copy、dispose成員變數 |
1.4 Block迴圈引用
typedef void (^block_t)(void);
@interface TestA : NSObject
@property(nonatomic, copy) block_t aBlock;
@property(nonatomic, copy) NSString *name;
- (void)test;
@end
@implementation TestA
- (void)test {
self.name = @"TestA";
//self->block->self 產生迴圈引用
self.aBlock = ^{
NSLog(@"name is %@", self.name);
};
//第一種解決方式:weak-strong-dance
//__weak typeof(self) weakSelf = self;
self.aBlock = ^{
typeof(self) strongSelf = weakSelf;
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
//如果不進行強引用,aBlock釋放後weakSelf取不到值。
//NSLog(@"name is %@", weakSelf.name);
NSLog(@"name is %@", strongSelf.name);
});
};
//第二種解決方式: 加臨時變數然後手動釋放
__block TestA *tempA = self;
self.aBlock = ^{
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
NSLog(@"name is %@", tempA.name);
tempA = nil;
});
};
self.aBlock();
//第三種解決方式:傳引數
//這個需要把block_t定義改為typedef void(^block_t)(id)
self.aBlock = ^(TestA *tempA){
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
NSLog(@"name is %@", tempA.name);
});
};
//從clang中可知呼叫的(aBlock->Funptr)(aBlock),就是普通的函式呼叫,aBlock作為引數被傳了進去
self.aBlock(self);
}
- (void)dealloc
{
NSLog(@"TestA dealloc");
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
TestA *a = [[TestA alloc] init];
[a test];
}
while (1) {}
return 0;
}
打破迴圈引用的三種方式:
- weak-strong-dance
- 加臨時變數手動置nil
- 用傳參的方式
1.5 Block的簽名
在block的呼叫前打上斷點,讀取暫存器中的block的指標,然後列印
Block簽名為@?,@代表物件,?代表是函式指標。
二、Block原始碼分析
2.1 Block結構
enum {
BLOCK_DEALLOCATING = (0x0001), // runtime
BLOCK_REFCOUNT_MASK = (0xfffe), // runtime
BLOCK_NEEDS_FREE = (1 << 24), // runtime
BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler
BLOCK_HAS_CTOR = (1 << 26), // compiler: helpers have C++ code
BLOCK_IS_GC = (1 << 27), // runtime
BLOCK_IS_GLOBAL = (1 << 28), // compiler
BLOCK_USE_STRET = (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
BLOCK_HAS_SIGNATURE = (1 << 30), // compiler
BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31) // compiler
};
#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
uintptr_t reserved;
uintptr_t size;
};
#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
};
//Block結構體
struct Block_layout {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
BlockInvokeFunction invoke;
struct Block_descriptor_1 *descriptor;
// imported variables
};
可以看到block資料結構體Block_layout,layout中只包含了Block_descriptor_1,並未包含Block_descriptor_2和Block_descriptor_3,這是因為在沒有引用外部變數或捕獲到不同型別變數時,編譯器會改變結構體的結構,按需新增Block_descriptor_2和Block_descriptor_3,所以才需要
當flags包含BLOCK_HAS_COPY_DISPOSE時,會加入Block_descriptor_2;當flags包含BLOCK_HAS_SIGNATURE時,會加入Block_descriptor_3。Block_descriptor_2和Block_descriptor_3是通過Block_descriptor_1的指標偏移來訪問的。
static struct Block_descriptor_2 * _Block_descriptor_2(struct Block_layout *aBlock)
{
if (! (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)) return NULL;
uint8_t *desc = (uint8_t *)aBlock->descriptor;
desc += sizeof(struct Block_descriptor_1);
return (struct Block_descriptor_2 *)desc;
}
static struct Block_descriptor_3 * _Block_descriptor_3(struct Block_layout *aBlock)
{
if (! (aBlock->flags & BLOCK_HAS_SIGNATURE)) return NULL;
uint8_t *desc = (uint8_t *)aBlock->descriptor;
desc += sizeof(struct Block_descriptor_1);
if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE) {
desc += sizeof(struct Block_descriptor_2);
}
return (struct Block_descriptor_3 *)desc;
}
早期的一張Block結構圖
2.2 __block變數引用結構體
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;
};
enum {
// see function implementation for a more complete description of these fields and combinations
BLOCK_FIELD_IS_OBJECT = 3, // id, NSObject, __attribute__((NSObject)), block, ...
BLOCK_FIELD_IS_BLOCK = 7, // a block variable
BLOCK_FIELD_IS_BYREF = 8, // the on stack structure holding the __block variable
BLOCK_FIELD_IS_WEAK = 16, // declared __weak, only used in byref copy helpers
BLOCK_BYREF_CALLER = 128, // called from __block (byref) copy/dispose support routines.
};
2.3 Block的三次copy
- Block在ARC下會進行適當地判斷來自動處理Block從棧複製到堆,此時呼叫的是_Block_copy函式;
- 當捕獲到物件變數或__block基本型別變數時會在Block結構的描述中加入copy、dispose來管理記憶體;
- 當要捕獲的變數是物件型別且被__block修飾時,就會在__block變數用結構體中加入copy、dispose來管理物件。
這樣算下來一共會進行三次copy,下面先來看_Block_copy函式。
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) {
// latches on high
//Block在堆上,flags值+2,這裡+2應該是BLOCK_DEALLOCATING把1佔用了
latching_incr_int(&aBlock->flags);
return aBlock;
}
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
//全域性Block, 直接返回
return aBlock;
}
else {
// 棧Block,進行copy
struct Block_layout *result =
(struct Block_layout *)malloc(aBlock->descriptor->size);
if (!result) return NULL;
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
// reset refcount 重置引用計數, BLOCK_REFCOUNT_MASK = 0xfffe, BLOCK_DEALLOCATING = 0x0001, 它倆或上取反等於0x0;
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed
//初始化flags = 0x1000002
result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1
//看看有沒有Block_descriptor_2,也就是copy、dispose,如果有呼叫copy方法
_Block_call_copy_helper(result, aBlock);
// 最後修改isa指向從_NSConcreteStackBlock變為_NSConcreteMallocBlock,以便記憶體分析工具可以看到完全初始化的物件
result->isa = _NSConcreteMallocBlock;
return result;
}
}
static void _Block_call_copy_helper(void *result, struct Block_layout *aBlock)
{
//通過flags中是否有BLOCK_HAS_COPY_DISPOSE值來判斷是否需要copy、dispose
struct Block_descriptor_2 *desc = _Block_descriptor_2(aBlock);
if (!desc) return;
(*desc->copy)(result, aBlock); // do fixup
}
static struct Block_descriptor_2 * _Block_descriptor_2(struct Block_layout *aBlock)
{
if (! (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)) return NULL;
uint8_t *desc = (uint8_t *)aBlock->descriptor;
desc += sizeof(struct Block_descriptor_1);
return (struct Block_descriptor_2 *)desc;
}
如果是堆Block就進行引用計數增加;如果是全域性Block直接返回;如果是棧Block進行copy,此時如果有_Block_descriptor_2結構,直接呼叫copy方法,從clang後的程式碼中可看到其實是呼叫的是_Block_object_assign()。
//對捕獲的變數進行操作
void _Block_object_assign(void *destArg, const void *object, const int flags) {
const void **dest = (const void **)destArg;
//判斷flags中是否有BLOCK_FIELD_IS_OBJECT、BLOCK_FIELD_IS_BLOCK、BLOCK_FIELD_IS_BYREF、BLOCK_FIELD_IS_WEAK、BLOCK_BYREF_CALLER這幾個值
switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
case BLOCK_FIELD_IS_OBJECT:
/*******
id object = ...;
[^{ object; } copy];
********/
_Block_retain_object(object);
*dest = object;
break;
case BLOCK_FIELD_IS_BLOCK:
/*******
void (^object)(void) = ...;
[^{ object; } copy];
********/
*dest = _Block_copy(object);
break;
case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
case BLOCK_FIELD_IS_BYREF:
/*******
// copy the onstack __block container to the heap
// Note this __weak is old GC-weak/MRC-unretained.
// ARC-style __weak is handled by the copy helper directly.
__block ... x;
__weak __block ... x;
[^{ x; } copy];
********/
*dest = _Block_byref_copy(object);
break;
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
/*******
// copy the actual field held in the __block container
// Note this is MRC unretained __block only.
// ARC retained __block is handled by the copy helper directly.
__block id object;
__block void (^object)(void);
[^{ object; } copy];
********/
*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:
/*******
// copy the actual field held in the __block container
// Note this __weak is old GC-weak/MRC-unretained.
// ARC-style __weak is handled by the copy helper directly.
__weak __block id object;
__weak __block void (^object)(void);
[^{ object; } copy];
********/
*dest = object;
break;
default:
break;
}
}
上面的官方註釋已經作出了說明,接著主要來看下BLOCK_FIELD_IS_BYREF情況,也就是被__block修飾變數時,會呼叫_Block_byref_copy。
static struct Block_byref *_Block_byref_copy(const void *arg) {
struct Block_byref *src = (struct Block_byref *)arg;
if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
// src指向棧
struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
copy->isa = NULL;
// byref value 4 is logical refcount of 2: one for caller, one for stack
//這裡或上4是因為棧的forwarding通過下面的程式碼指向了堆
copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
copy->forwarding = copy; // 堆上的forwarding指向堆自身
src->forwarding = copy; // 棧上的forwarding也指向堆
copy->size = src->size;
if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
//有copy、dispose,通過src偏移一個struct Block_byref結構體大小拿到src2, 也就是包含copy和dispose成員變數的Block_byref_2結構體
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) {
//從src2偏移一個struct Block_byref_2大小拿到src3, 也就是包含layout成員變數的Block_byref_3結構體
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;
}
//呼叫外部的__Block_byref_id_object_copy_131
(*src2->byref_keep)(copy, src);
}
else {
// Bitwise copy.
// This copy includes Block_byref_3, if any.
memmove(copy+1, src+1, src->size - sizeof(*src));
}
}
// already copied to heap
else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
latching_incr_int(&src->forwarding->flags);
}
return src->forwarding;
}
從 copy->forwarding = copy; src->forwarding = copy; 這兩句程式碼可以看出,堆上的變數的forwarding指向了自己,而棧上的forwarding也指向了堆,這樣就實現了對同一變數的操作,也就是__block為什麼可以修改持有的外部變數的原因。
接下來,還可以看到(*src2->byref_keep)(copy, src);這句程式碼呼叫了byref_keep,而byref_keep為
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
又一次呼叫了_Block_object_assign, 而此時的偏移+40是因為val在__Block_byref_val_0中的地址偏移量為40,
131代表的是128+3, 128是BLOCK_BYREF_CALLER代表__block變數有copy/dispose的記憶體管理輔助函式,3是BLOCK_FIELD_IS_OBJECT代表變數是物件型別。
2.4 Block釋放
沒有copy、dispose結構的作用域結束就釋放掉了,而有copy、dispose結構的會通過呼叫_Block_object_dispose函式。
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相應的
_Block_byref_release(object);
break;
case BLOCK_FIELD_IS_BLOCK:
_Block_release(object);
break;
......
}
}
static bool latching_decr_int_should_deallocate(volatile int32_t *where) {
while (1) {
int32_t old_value = *where;
if ((old_value & BLOCK_REFCOUNT_MASK) == BLOCK_REFCOUNT_MASK) {
return false; // latched high
}
if ((old_value & BLOCK_REFCOUNT_MASK) == 0) {
return false; // underflow, latch low
}
int32_t new_value = old_value - 2;
bool result = false;
if ((old_value & (BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING)) == 2) { //只有在flags值等於2時說明只剩下一個物件在持有,此時呼叫dispose方法可以釋放了
new_value = old_value - 1;
result = true;
}
if (OSAtomicCompareAndSwapInt(old_value, new_value, where)) {
return result;
}
}
}
_Block_byref_release和_Block_release都通過latching_decr_int_should_deallocate方法中的(old_value & (BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING)) == 2來判斷是否釋放物件,只有在flags值等於2時說明只剩下一個物件在持有,此時才去呼叫相應的dispose去釋放相應的物件。
總結
-
Block是帶有自動變數(區域性變數)的匿名函式,在OC中Block本質上就是一個物件。
-
Block截獲自動變數是Block語法表示式所使用的自動變數值被儲存到了Block的結構體例項中。
-
Block的儲存域:
-
__block變數儲存域會從隨著Block儲存域一起復制,棧Block複製到堆的時機:
- 呼叫Block的copy例項方法時
- Block作為函式返回值返回時
- 將Block賦值給附有__strong修飾符id型別的類或Block型別成員變數時
- 在方法名中含有usingBlock的Cocoa框架方法或GCD的API中傳遞Block時
-
打破迴圈引用的三種方式:
- weak-strong-dance
- 加臨時變數手動置nil
- 用傳參的方式
-
Block的簽名型別為@?,@代表物件,?代表函式指標
-
Block的三次Copy
- Block在ARC下會進行適當地判斷來自動處理Block從棧複製到堆,此時呼叫的是_Block_copy函式;
- 當捕獲到物件變數或__block基本型別變數時會在Block結構的描述中加入copy、dispose來管理記憶體;
- 當要捕獲的變數是物件型別且被__block修飾時,就會在__block變數用結構體中加入copy、dispose來管理物件。
-
只有在flags值等於2時說明只剩下一個物件在持有,此時才去呼叫相應的dispose去釋放相應的物件。
參考文章:
原始碼地址:https://opensource.apple.com/source/libclosure/
文中部分內容摘自《Obj-C高階程式設計》書中
相關文章
- 重學OC第二十六篇:RunLoopOOP
- 重學OC第二十四篇:啟動優化優化
- 重學OC第二十一篇:@synchronized分析synchronized
- Block學習①--block的本質BloC
- Swift與OC真正去理解Block解決迴圈引用的技巧SwiftBloC
- Block學習②--block的變數捕獲BloC變數
- Block學習⑤--block對物件變數的捕獲BloC物件變數
- iOS開發之OC篇(3)—— NSArray、NSMutableArrayiOS
- OC-從記憶體角度理解block可作為方法傳入引數的原因記憶體BloC
- SpringBoot非官方教程 | 第二十三篇: 非同步方法Spring Boot非同步
- SpringBoot第二十三篇:安全性之Spring SecuritySpring Boot
- iOS Block學習筆記iOSBloC筆記
- 13. iOS開發小細節--OC篇iOS
- Block深入學習,授人以漁。—— Block與各種變數BloC變數
- [iOS] [OC] 關於block回撥、高階函式“回撥再呼叫”及專案實踐iOSBloC函式
- 安心學習,重學前端之(js基礎篇(1))前端JS
- OC常用數學函式及常量函式
- Unity Application Block 1.2 學習筆記UnityAPPBloC筆記
- BlockBloC
- Unused Block Compression和Null Block CompressionBloCNull
- 深度學習在OC中的應用深度學習
- OC學習之旅------第九天
- 重學Swift第一篇:類結構探索Swift
- __block使用BloC
- iOS block巢狀block中weakify的使用iOSBloC巢狀
- iOS逆向之旅(基礎篇) — 彙編(五) — 彙編下的BlockiOSBloC
- 重構之路:開篇
- 如何避免UITableView重寫大量delegate以及n多if-else判斷和BlockUIViewBloC
- iOS Block探究iOSBloC
- OC(二)字串、方法字串
- 前端週刊第二十三期前端
- Block的型別BloC型別
- block實現原理BloC
- E. Block SequenceBloC
- display:block display:inline-block 的屬性、呈現和作用BloCinline
- 服務治理之重試篇
- 重學VueVue
- 重學前端前端