玩转iOS开发:实战开发中的GCD Tips小技巧 (二)

文章分享至我的个人技术博客: 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


最后

码字很费脑, 看官赏点饭钱可好

微信

支付宝