玩轉iOS開發:實戰開發中的GCD Tips小技巧 (二)

CainLuo發表於2017-11-03

文章分享至我的個人技術部落格: https://cainluo.github.io/15062229631553.html


上一篇, 我們簡單的講了一些使用GCD的小技巧, 如果沒有看的朋友, 可以去玩轉iOS開發:實戰開發中的GCD Tips小技巧 (一)看.

這次, 我們繼續講解小技巧.

轉載宣告:如需要轉載該文章, 請聯絡作者, 並且註明出處, 以及不能擅自修改本文.


佇列組的靈活使用

通常我們使用佇列組執行任務的時候是醬紫的:

- (void)queueGroup {

    dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_t group = dispatch_group_create();
    
    dispatch_group_async(group, queue, ^{
        NSLog(@"執行任務, 當前執行緒為:%@", [NSThread currentThread]);
    });
}
複製程式碼

列印的結果:

2017-09-24 11:34:44.766052+0800 GCD-Tips[59653:3481972] 開始執行
2017-09-24 11:34:44.766606+0800 GCD-Tips[59653:3482075] 執行任務, 當前執行緒為:<NSThread: 0x604000464980>{number = 3, name = (null)}
複製程式碼

但有時候, 我們會遇到一種情況, 就是沒有辦法直接使用佇列組變數, 這個時候, 還有另外一種方式, 就是dispatch_group_enterdispatch_group_leave, 注意, 這兩個方法是同時出現的:

- (void)gourpEnterAndLeave {
    
    dispatch_group_t group = dispatch_group_create();
    
    dispatch_group_enter(group);

    [self urlRequestSuccess:^{
        
        NSLog(@"網路請求成功");
        
        dispatch_group_leave(group);
    } failure:^{
        
        NSLog(@"網路請求失敗");

        dispatch_group_leave(group);
    }];
}

- (void)urlRequestSuccess:(void(^)())success
                  failure:(void(^)())failure {
    
    success();
//    failure();
}
複製程式碼

列印的結果:

2017-09-24 11:46:16.054410+0800 GCD-Tips[60002:3501228] 開始執行
2017-09-24 11:46:16.054721+0800 GCD-Tips[60002:3501228] 網路請求成功
複製程式碼

這樣子, 我們就可以把這個網路請求給打包起來, 但這裡要注意一下, 不能同時呼叫兩個dispatch_group_leave, 不然就會掛了.

如果我們要新增結束任務的話, 可以有兩種方式:

  • dispatch_group_notify
    • 當前的佇列組任務執行完畢之後, 就會呼叫dispatch_group_notify來通知, 任務已經結束.
  • dispatch_group_wait
    • dispatch_group_notify類似, 只不過是在可以新增延遲結束的時間, 但這裡需要注意一點, dispatch_group_wait會阻塞當前執行緒, 所以不要在主執行緒中呼叫, 不然會阻塞主執行緒.

dispatch_barrier_(a)sync使用的注意

我們都知道dispatch_barrier_(a)sync其實是一個柵欄方法, 它的作用就是在向某個佇列插入一個block, 等到該block執行完之後, 才會繼續執行其他佇列, 有點老大的味道.

- (void)queueBarrier {
    
    dispatch_queue_t queue = dispatch_queue_create("queueBarrier", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(queue, ^{
        
        NSLog(@"執行一, 當前執行緒:%@", [NSThread currentThread]);
    });
    
    dispatch_barrier_async(queue, ^{
        
        NSLog(@"大佬來了, 當前執行緒:%@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        
        NSLog(@"執行二, 當前執行緒:%@", [NSThread currentThread]);
    });
}
複製程式碼

列印的結果:

2017-09-24 12:44:36.151126+0800 GCD-Tips[61121:3585236] 開始執行
2017-09-24 12:44:36.151579+0800 GCD-Tips[61121:3585334] 執行一, 當前執行緒:<NSThread: 0x600000462e40>{number = 3, name = (null)}
2017-09-24 12:44:36.152335+0800 GCD-Tips[61121:3585334] 大佬來了, 當前執行緒:<NSThread: 0x600000462e40>{number = 3, name = (null)}
2017-09-24 12:44:36.154241+0800 GCD-Tips[61121:3585334] 執行二, 當前執行緒:<NSThread: 0x600000462e40>{number = 3, name = (null)}
複製程式碼

PS: dispatch_barrier_(a)sync只在自己建立的併發佇列的才會有效, 如果是在全域性併發佇列, 序列佇列, dispatch_(a)sync效果是一樣的, 這樣子的話, 就會容易造成執行緒死鎖, 所以這裡要注意.


dispatch_set_context與dispatch_set_finalizer_f

這裡要補充兩個東西, dispatch_set_contextdispatch_set_finalizer_f.

  • dispatch_set_context: 可以為佇列新增上下文資料
  • dispatch_set_finalizer_f: 轉移記憶體管理許可權

這裡要注意一點dispatch_set_context接受的context引數是為C語言引數, 所以這裡寫的時候, 要注意一下:

typedef struct _Info {
    int age;
} Info;

void cleanStaff(void *context) {
    
    NSLog(@"In clean, context age: %d", ((Info *)context)->age);
    
    //釋放,如果是new出來的物件,就要用delete
    free(context);
}

- (void)setContext {
    
    dispatch_queue_t queue = dispatch_queue_create("contextQueue", DISPATCH_QUEUE_SERIAL);
    
    // 初始化Data物件, 並且設定初始化值
    Info *myData = malloc(sizeof(Info));
    myData->age = 100;
    
    // 繫結Context
    dispatch_set_context(queue, myData);
    
    // 設定finalizer函式,用於在佇列執行完成後釋放對應context記憶體
    dispatch_set_finalizer_f(queue, cleanStaff);

    dispatch_async(queue, ^{
        
        //獲取佇列的context資料
        Info *data = dispatch_get_context(queue);
        //列印
        NSLog(@"1: context age: %d", data->age);
        //修改context儲存的資料
        data->age = 20;
    });
}
複製程式碼

列印一下結果:

2017-09-24 14:24:10.394129+0800 GCD-Tips[61881:3652088] 開始執行
2017-09-24 14:24:10.394547+0800 GCD-Tips[61881:3652274] 1: context age: 100
2017-09-24 14:24:10.394738+0800 GCD-Tips[61881:3652274] In clean, context age: 20
複製程式碼

PS: 我們設定了dispatch_set_context記得一定要釋放掉, 不然就會造成記憶體洩漏.

除了這個之外, 我們還可以對Core Foundation進行操作, 那麼該怎麼做呢?


NSObject與dispatch_set_context

這裡我們要建立一個Model類, 繼承與NSObject:

@interface GCDModel : NSObject

@property (nonatomic, assign) NSInteger age;

@end

@implementation GCDModel

- (void)dealloc {
    
    NSLog(@"%@ 釋放了", NSStringFromClass([self class]));
}

@end
複製程式碼

在這個類裡面, 我們就簡單操作, 只有一個屬性和一個dealloc方法, 具體操作:

void cleanObjectStaff(void *context) {
    
    GCDModel *model = (__bridge GCDModel *)context;
    
    NSLog(@"In clean, context age: %ld", model.age);
    
    // 釋放記憶體
    CFRelease(context);
}

- (void)objectAndContext {
    
    dispatch_queue_t queue = dispatch_queue_create("objectQueue", DISPATCH_QUEUE_SERIAL);
    
    // 初始化Data物件, 並且設定初始化值
    GCDModel *model = [[GCDModel alloc] init];
    model.age = 20;
    
    // 繫結Context, 這裡使用__bridge關鍵
    dispatch_set_context(queue, (__bridge_retained void *)(model));
    
    // 設定finalizer函式,用於在佇列執行完成後釋放對應context記憶體
    dispatch_set_finalizer_f(queue, cleanObjectStaff);
    
    dispatch_async(queue, ^{
        
        //獲取佇列的context資料
        GCDModel *model = (__bridge GCDModel *)(dispatch_get_context(queue));
        //列印
        NSLog(@"1: context age: %ld", model.age);
        //修改context儲存的資料
        model.age = 120;
    });
}
複製程式碼

列印一下結果:

2017-09-24 14:40:34.024509+0800 GCD-Tips[62448:3676807] 開始執行
2017-09-24 14:40:34.024915+0800 GCD-Tips[62448:3676887] 1: context age: 20
2017-09-24 14:40:34.025236+0800 GCD-Tips[62448:3676887] In clean, context age: 120
2017-09-24 14:40:34.025706+0800 GCD-Tips[62448:3676887] GCDModel 釋放了
複製程式碼

這裡我們要解釋一下__bridge關鍵字:

  • __bridge: 只做型別轉換, 不做記憶體管理許可權修改.
  • __bridge_retained: 記憶體轉換(CFBridgingRetain), 並且把記憶體管理許可權從ARC裡拿到自己手裡, 最後釋放時要用CFRelease來釋放物件.
  • __bridge_transfer: 將Core Foundation轉換成Objective-C物件(CFBridgingRelease), 並且將記憶體管理的許可權交給ARC.

看到這裡應該會有人問, 為什麼要把記憶體管理拿到自己的手裡, 而不是交給ARC?

其實道理很簡單, 如果是ARC管理的話, 一旦它檢測到作用於完了之後, 你的物件就會釋放了.

那麼你就無法將這個Context新增到佇列當中, 一旦新增就會給你報一個野指標錯誤, 所以我們為了確保不會被ARC給釋放掉, 我們就需要自己去操作了.

而上面那段程式碼的解釋也很簡單:

  • dispatch_set_context時候用__bridge_retained進行轉換, 將Context的記憶體管理許可權拿到我們自己手上進行管理.
  • 在佇列任務的中, 我們用dispatch_get_context來獲取context的時候用關鍵字__bridge進行轉換, 這樣子可以維持context的記憶體管理權不變, 防止出了作用域Context就會被釋放掉.
  • 最後用CFRelease來釋放掉context, 這樣子就可以保證記憶體得到釋放, 不會造成記憶體洩漏的問題.

總結

好了, 額外補充的GCD小技巧到這裡就差不多了, 如果以後還有更多的小技巧也會繼續更新, 歡迎各位小夥伴們和我分享其他的使用技巧~~

這裡推薦幾篇文章:

Grand Central Dispatch (GCD) Reference Concurrency Programming Guide Toll-Free Bridged Types


工程地址

專案地址: https://github.com/CainRun/iOS-Project-Example/tree/master/GCD-Tips/GCD-Tips-Two


最後

碼字很費腦, 看官賞點飯錢可好

微信

支付寶

相關文章