• iOS 多线程的使用


    iOS 多线程

         先看一篇阮一峰写关于进程和线程的文章,快速了解线程的相关概念。

          随着现在计算机硬件的发展,多核心、高频率的cpu越来越普及,为了充分发挥cpu的性能,在不通的环境下实现cpu的利用最大化,多线程技术在这个时候显得越发重要。同时,在程序中合理的使用多线程,可以让程序变得更加有效、靠谱。所以学习这一知识是一项有意义的事情。

         iOS中,只有主线程跟Cocoa关联,也即是在这个线程中,更新UI总是有效的,如果在其他线程中更新UI有时候会成功,但可能失败。所以苹果要求开发者在主线程中更新UI。但是如果我们吧所有的操作都放置在主线程中执行,当遇到比较耗时的操作的时候,势必会阻塞线程,出现界面卡顿的情况。这时候采取将耗时的操作放入后台线程中操作,且保持主线程只更新UI是我们推荐的做法。

         在iOS中,要实现多线程,一共有四种方式。  它们分别是:

    • pthreads         POSIX线程(POSIX threads),简称Pthreads,是线程的POSIX标准。该标准定义了创建和操纵线程的一整套API。在类Unix操作系统(Unix、Linux、Mac OS X等)中,都使用Pthreads作为操作系统的线程。Windows操作系统也有其移植版pthreads-win32[1]这篇文章不介绍
    • NSThread       需要管理线程的生命周期、同步、加锁问题,这会导致一定的性能开销
    • NSOperation & NSOperationQueue    
    • GCD               iOS4开始,苹果发布了GCD,可以自动对线程进行管理。极大的方便了多线程的开发使用

    备注:本文中相关的代码在https://github.com/lufubinGit/Multithreading。 

    一、pthreads 

         pthread是一套基于C的API,它不接受cocoa框架的控制:当手动创建pthread的时候,cocoa框架并不知道。 苹果不建议在cocoa中使用pthread,但是如果为了方便不得不使用,我们应该小心的使用。 

         下面这些方法可以创建pthread

    OC
    pthread_attr_t qosAttribute;
    pthread_attr_init(&qosAttribute);
    pthread_attr_set_qos_class_np(&qosAttribute, QOS_CLASS_UTILITY, 0);
    pthread_create(&thread, &qosAttribute, f, NULL);
    SWIFT var thread = pthread_t() var qosAttribute = pthread_attr_t() pthread_attr_init(&qosAttribute) pthread_attr_set_qos_class_np(&qosAttribute, QOS_CLASS_UTILITY, 0) pthread_create(&thread, &qosAttribute, f, nil)

        并且,可以使用下面的API对一个pthread进行修改。

        苹果的文档中,有一篇文档讲述了GCD中使用pthread的禁忌:Compatibility with POSIX Threads。

    OBJECTIVE-C
    
    pthread_set_qos_class_self_np(QOS_CLASS_BACKGROUND,0);
    SWIFT
    
    pthread_set_qos_class_self_np(QOS_CLASS_BACKGROUND, 0)
    

    二、NSThread

          对于NSThread,在使用的过程中,我们需要手动完成很多动作才能确保线程的顺利运行。但与此同时,它给我们带来更大的定制化空间。

     1.创建NSThread。

          对于NSThread的创建,苹果给出了三种使用方式。

    detachNewThreadSelector(_:toTarget:with:)   detachNewThreadSelector会创建一个新的线程,并直接进入线程执行。
    
    initWith(Target:selector:object:)          iOS10.0之前的创建方式,需要手动执行。                     
    
    initWithBlock                                      iOS10.0之后,可以创建一个执行block的线程。
    

    2.NSThread线程通信。

         如果我们想对已经存在的线程进行操作,可以使用 

    performSelector:onThread:withObject:waitUntilDone:  

       跳转到目标线程执行,实现线程间跳转,达到线程通信的目的。但是需要注意的是这个方法不适合频繁的进行通信,尤其是对于一些敏感的操作

     3.NSThread线程的状态。

        在一个线程中,可以通过相关的函数获取到它的当前状态。

    + isMainThread:判断当前线程是不是主线程。
    + mainThread:获取当前的主线程。
    + isMultiThreaded :判断当前环境是不是多线程环境   
    + threadDictionary :获取包含项目中的线程的字典
    @property (readonly, getter=isExecuting) BOOL executing NS_AVAILABLE(10_5, 2_0);     是否处于运行状态
    @property (readonly, getter=isFinished) BOOL finished NS_AVAILABLE(10_5, 2_0);          是否处于完成状态
    @property (readonly, getter=isCancelled) BOOL cancelled NS_AVAILABLE(10_5, 2_0);      是否处于取消状态
    

    4.NSThread线程的优先级。

         可通过给NSThread设置优先级。以便让开发者更灵活的控制程序的执行。

    + threadPriority  Returns the current thread’s priority. 返回当前线程的优先级别
    threadPriority        The receiver’s priority  消息发送者的优先级,这个发送者是一个NSThread对象
    + setThreadPriority:     Sets the current thread’s priority. 设置线程的优先级
    

    5.停止线程/终止线程

    + sleepUntilDate:   Blocks the current thread until the time specified.  直到某时刻执行
    + sleepForTimeInterval:     Sleeps the thread for a given time interval.   暂停线程
    + exit    Terminates the current thread.  关闭线程,这里调用之前,为了确保程序的安全,我们应在明确线程的状态是isFinished 和 isCancelled的时候执行。
    - cancel    Changes the cancelled state of the receiver to indicate that it should exit.  主动进入取消状态,如果当时线程没有完成,会继续执行完成。
    

    6.使用NSThread

    - (void)viewDidLoad {
        [super viewDidLoad];
        _testCount = 100;
        _t1 =  [[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil];
        _t1.name = @"线程一";
        [_t1 start];
        NSInvocationOperation
    }
    
    -(void)test{
        for (int i = 0; i < 5 ; i++) {
            [NSThread sleepForTimeInterval:0.05];
            NSLog(@"%ld,%@",(long)_testCount--,[[NSThread currentThread] name]);
        }
    
    }
    

    三、NSOperation & NSOperationQueue   

     1.NSOperation

          NSOperation是对于线程对象的抽象封装,不会被直接使用,在日常的开发中,会使用它的两个子类:NSInvocationOperation 和 NSBlockOperationNSInvocationOperation类是NSOperation的具体子类,用于管理指定为调用的单个封装任务的执行。 您可以使用此类来启动包含在指定对象上调用选择器的操作。 此类实现非并发操作。NSBlockOperation类也是NSOperation的具体子类,用于管理一个或多个block块的并发执行。 您可以使用此对象一次执行多个block,而无需为每个块创建单独的操作对象。 当执行多个程序段时,只有当所有程序段执行完毕时,才会将操作本身完成。

        NSInvocationOperation实现非并发操作。

       _invCationOp = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(test2:) object:nil];
        _invCationOp.name = @"invocation线程";
        [_invCationOp start];

    打印:

        <NSThread: 0x608000076fc0>{number = 1, name = main},

        <NSThread: 0x608000076fc0>{number = 1, name = main}

        从打印结果可以看出,NSInvocationOperation实现的是非并法的操作,至于在哪个线程中操作,取决于start的当前调用时的线程。

        如果我们需要创建一个并发的Queue,可以使用NSBlockOperation。如果我们像这样创建:

    - (void)blockOperation{
        NSBlockOperation *blockOp = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"%@,%@",[NSThread currentThread],[NSThread mainThread]);
        }];
        
        [blockOp addExecutionBlock:^{
    
            NSLog(@"%@,%@",[NSThread currentThread],[NSThread mainThread]);
        }];
        [blockOp addExecutionBlock:^{
            NSLog(@"%@,%@",[NSThread currentThread],[NSThread mainThread]);
        }];
        
        [blockOp start];
    }
    

    打印: 

        2017-05-23 15:11:00.289 多线程[5377:589808] <NSThread: 0x60000006fec0>{number = 1, name = main},<NSThread: 0x60000006fec0>{number = 1, name = main}

        2017-05-23 15:11:00.289 多线程[5377:589808] <NSThread: 0x60000006fec0>{number = 1, name = main},<NSThread: 0x60000006fec0>{number = 1, name = main}

        2017-05-23 15:11:00.289 多线程[5377:589808] <NSThread: 0x60000006fec0>{number = 1, name = main},<NSThread: 0x60000006fec0>{number = 1, name =  main}

        显然,这里都在主线程中执行,不能证明NSBlockOperation具有并发的能力,这是因为,每个NSBlockOperation对象的会优先在主线程中执行。如果主线程受到阻塞的时候才会开辟另一个线程去执行其他的操作。比如向下面这样:

     1 //NSBlockOperation
     2 - (void)blockOperation{
     3     NSBlockOperation *blockOp = [NSBlockOperation blockOperationWithBlock:^{
     4         NSLog(@"%@,%@",[NSThread currentThread],[NSThread mainThread]);
     5     }];
     6     
     7     [blockOp addExecutionBlock:^{
     8         [NSThread sleepForTimeInterval:2.0];
     9 
    10         NSLog(@"%@,%@",[NSThread currentThread],[NSThread mainThread]);
    11     }];
    12     [blockOp addExecutionBlock:^{
    13         [NSThread sleepForTimeInterval:2.0];
    14         NSLog(@"%@,%@",[NSThread currentThread],[NSThread mainThread]);
    15     }];
    16     
    17    
    18     [blockOp start];
    19 }
    

     打印:

        2017-05-23 15:28:37.780 多线程[5645:617976] <NSThread: 0x60800006f700>{number = 1, name = main},<NSThread: 0x60800006f700>{number = 1, name = main}

        2017-05-23 15:28:39.848 多线程[5645:618027] <NSThread: 0x60800007bf80>{number = 3, name = (null)},<NSThread: 0x60800006f700>{number = 1, name = (null)}

        2017-05-23 15:28:39.848 多线程[5645:618028] <NSThread: 0x60800007bfc0>{number = 4, name = (null)},<NSThread: 0x60800006f700>{number = 1, name = (null)}=

        这里就是异步执行了。 NSBlockOperation在使用的过程中,会针对主线程当前的使用情况,选择性的创建其他的线程。在提升流畅度的同时,还节约了资源。

      2.NSOperationQueue

          NSOperationQueue:手动管理异步执行。  如果我们想使用并发,并且要作到精确掌握并发的线程。可以使用NSOperationQueue。这是一个操作队列,如果将NSOperation的具体子类对象添加进来的时候,开启之后,所有的对象没有先后,会异步执行各自的代码。

    - (void)operationQueue{
        NSOperationQueue *queue = [[NSOperationQueue alloc] init];
        NSBlockOperation *blockOp = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"%ld,%@,%@",(long)_testCount--,[NSThread currentThread],[NSThread mainThread]);
        }];
    
        NSInvocationOperation *invCationOp = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(test2:) object:nil];
        invCationOp.name = @"invocation线程";
        [queue addOperation:invCationOp];
        [queue addOperation:blockOp];
    
    }
     
    

      打印:

        2017-05-23 15:38:45.110 多线程[5772:633972] 100,<NSThread: 0x6080000773c0>{number = 3, name = (null)},<NSThread: 0x60000006bc80>{number = 1, name = (null)}

        2017-05-23 15:38:45.203 多线程[5772:633975] 99,<NSThread: 0x608000078800>{number = 4, name = (null)},<NSThread: 0x60000006bc80>{number = 1, name = (null)}

       在NSOperationQueue中,正常情况下,所有的operation的执行次序是随机,如果我们想要某个operation被率先执行,可以将这个operation的优先级调高。对于优先级有以下的选择:

     [invCationOp setQueuePriority:NSOperationQueuePriorityVeryHigh];

          对于优先级有以下的选择:

    typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
        NSOperationQueuePriorityVeryLow = -8L,   最低
        NSOperationQueuePriorityLow = -4L,       次低
        NSOperationQueuePriorityNormal = 0,      普通  不做任何操作的operation的优先级是这个
        NSOperationQueuePriorityHigh = 4,        次高
        NSOperationQueuePriorityVeryHigh = 8     最高
    };
    

          当然,如果有很多operation,使用的优先级不能满足的时候,还可以设置 operation的依赖关系。 设置依赖之后,将会先执行依赖对象。 

     [invCationOp addDependency:blockOp];

        值得注意的是:优先级和依赖关系不是冲突的。 优先级的选择会在依赖关系下发生效果,也就是,在依赖关系成立的情况下,优先级的才会有效。

    三、GCD  -  Grand Central Dispatch

         Grand Central Dispatch早在Mac OS X 10.6雪豹上就已经存在了。后在iOS4.0的时候被引入。Grand Central Dispatch是OS X中的一个低级框架,用于管理整个操作系统中的并发和异步执行任务。本质上,随着处理器核心的可用性,任务排队等待执行。通过允许系统控制对任务的线程分配,GCD更有效地使用资源,这有助于系统和应用程序运行更快,高效和响应。

         GCD的一个重要的对象是队列:Dispatch Queue。跟Operationqueue类似,通过将Operation加入到队列中,执行相应的单元。在GCD中,大量采用了block的形式创建类似的operation。

      1. Dispatch Queue  创建

          Dispatch Queue 分为两类,主要是根据并行和串行来区分:

          a. Serial Dispatch Queue: 线性执行的线程队列,遵循FIFO(First In First Out)原则; 又叫private dispatch queues,同时只执行一个任务。Serial queue常用于同步访问特定的资源或数据。当你创建多个Serial queue时,虽然各自是同步,但serial queue之间是并发执行。 main dipatch属于这个类别。

      b. Concurrent Dispatch Queue: 并发执行的线程队列,并发执行的处理数取决于当前状态。又叫global dispatch queue,可以并发的执行多个任务,但执行完成顺序是随机的。系统提供四个全局并发队列,这四个队列有这对应的优先级,用户是不能够创建全局队列的,只能获取。

          我们可以自定义队列,默认创建的队列是串行的,但是也可以指定创建一个并行的队列:

    //串行队列
    dispatch_queue_create("com.deafultqueue", 0)
    //串行队列
    dispatch_queue_create("com.serialqueue", DISPATCH_QUEUE_SERIAL)
    //并行队列
    dispatch_queue_create("com.concurrentqueue", DISPATCH_QUEUE_CONCURRENT)

           除了自定义队列,系统其实也为有一些已经公开的队列。这些队列不需要我们显示的创建,只能通过获取的方式得到:

    dispatch_get_main_queue()   获取当前的APP主队列,这个队列在主线程中,通常我们调用它进行界面的刷新。

    dispatch_get_global_queue(<#long identifier#>, <#unsigned long flags#>)   获取全局的Concurrent队列,这里苹果提供了四种不同的优先级,

             #define DISPATCH_QUEUE_PRIORITY_HIGH 2

            #define DISPATCH_QUEUE_PRIORITY_DEFAULT 0

            #define DISPATCH_QUEUE_PRIORITY_LOW (-2)

            #define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN

    也即时有四个不同的并行队列。

    2. Dispatch Queue  执行

         GCD的队列有串行和并行两种队列,同时我们可以同步和异步两种方式执行队列,所以最多有四种不同的场景。

         (1)串行同步。 凡涉及到同步的的都会阻塞线程。 UI线程—也即是我们的所说的主线程默认情况下其实就是执行同步的。这个时候如果有一些耗时间的操作,则会出现卡顿的现象。这种方式大部分情况用于能快速响应和后台线程的耗时场景中。

      //串行同步
        dispatch_queue_t  serialQ = dispatch_queue_create("串行", DISPATCH_QUEUE_SERIAL);  //创建一个串行队列
        NSLog(@"%@",[NSThread currentThread].description);
        
        dispatch_sync(serialQ, ^{
            [NSThread sleepForTimeInterval:3];
            NSLog(@"%@ --  %@队列",[NSThread currentThread].description,[NSString stringWithUTF8String:dispatch_queue_get_label(serialQ)]);
        });
        dispatch_sync(serialQ, ^{
            NSLog(@"%@ --  %@队列",[NSThread currentThread].description,[NSString stringWithUTF8String:dispatch_queue_get_label(serialQ)]);
        });   
    

         (2)串行异步。 这种情况下,GCD会开辟另一个新的线程,让队列中的内容在新的线程中按顺序执行。

     //串行异步
        dispatch_async(serialQ, ^{
            NSLog(@"%@ 1-- 队列:%@",[NSThread currentThread].description,[NSString stringWithUTF8String:dispatch_queue_get_label(serialQ)]);
        });
        dispatch_async(serialQ, ^{
            NSLog(@"%@ 2-- 队列:%@",[NSThread currentThread].description,[NSString stringWithUTF8String:dispatch_queue_get_label(serialQ)]);
        });
        dispatch_async(serialQ, ^{
            NSLog(@"%@ 3-- 队列:%@",[NSThread currentThread].description,[NSString stringWithUTF8String:dispatch_queue_get_label(serialQ)]);
        });
        dispatch_async(serialQ, ^{
            NSLog(@"%@ 4-- 队列:%@",[NSThread currentThread].description,[NSString stringWithUTF8String:dispatch_queue_get_label(serialQ)]);
        });

         (3)并行同步。 因为是同步执行,所以实际上这里的并行是没有意义的。 依然在当前的线程中按顺序执行,并阻塞。

      dispatch_queue_t  conCurrentQ = dispatch_queue_create("并行", DISPATCH_QUEUE_CONCURRENT);  //创建一个并行行队列
        //并行同步
        dispatch_sync(conCurrentQ, ^{
            [NSThread sleepForTimeInterval:0.2];
            NSLog(@"%@ 1-- 队列:%@",[NSThread currentThread].description,[NSString stringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] );
        });
        dispatch_sync(conCurrentQ, ^{
            [NSThread sleepForTimeInterval:0.2];
            NSLog(@"%@ 2-- 队列:%@",[NSThread currentThread].description,[NSString stringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] );
        });
        dispatch_sync(conCurrentQ, ^{
            [NSThread sleepForTimeInterval:0.2];
            NSLog(@"%@ 3-- 队列:%@",[NSThread currentThread].description,[NSString stringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] );
        });
    

         (4)并行异步。 并行异步将极大的利用资源。首先会开辟新的线程,并且,当所有线程备占用的情况下,会继续开辟(如果没有限制的话)。所以这里还涉及线程的最大值的问题。

     //并行异步
        dispatch_async(conCurrentQ, ^{
            NSLog(@"%@ 1-- 队列:%@",[NSThread currentThread].description,[NSString stringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] );
        });
        dispatch_async(conCurrentQ, ^{
            NSLog(@"%@ 2-- 队列:%@",[NSThread currentThread].description,[NSString stringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] );
        });
        dispatch_async(conCurrentQ, ^{
            NSLog(@"%@ 3-- 队列:%@",[NSThread currentThread].description,[NSString stringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] );
        });
        dispatch_async(conCurrentQ, ^{
            NSLog(@"%@ 4-- 队列:%@",[NSThread currentThread].description,[NSString stringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] );
        }); 

     

    3. Dispatch Queue  暂停和继续

         我们可以使用dispatch_suspend函数暂停一个queue以阻止它执行block对象;使用dispatch_resume函数继续dispatch queue。调用dispatch_suspend会增加queue的引用计数,调用dispatch_resume则减少queue的引用计数。当引用计数大于0时,queue就保持挂起状态。因此你必须对应地调用suspend和resume函数。挂起和继续是异步的,而且只在执行block之间(比如在执行一个新的block之前或之后)生效。挂起一个queue不会导致正在执行的block停止。  

    4. Dispatch Queue  的销毁

         每个队列在执行完添加到其中的所有的block事件的时候,在ARC模式下,会被自动销毁。 但是在手动管理内存的时候,我们需要调用 

         dispatch_release(queue);

         来释放。    

    5.队列组  Dispatch Group (这些内容来自http://blog.csdn.net/q199109106q/article/details/8566300

         多数情况下,我们可能会遇到这种问题: 对一个页面中的多张图片,每张图片要单独的进行网络请求,我们没有办法保证每次的请求时间是一样的,但是项目经理说必须要在获取所有的图片的情况下,才可以进行对页面的刷新。这里有个很好例子可以解决这个问题。

    // 根据url获取UIImage  
    - (UIImage *)imageWithURLString:(NSString *)urlString {  
        NSURL *url = [NSURL URLWithString:urlString];  
        NSData *data = [NSData dataWithContentsOfURL:url];  
        return [UIImage imageWithData:data];  
    }  
      
    - (void)downloadImages {  
        // 异步下载图片  
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{  
            // 下载第一张图片  
            NSString *url1 = @"http://car0.autoimg.cn/upload/spec/9579/u_20120110174805627264.jpg";  
            UIImage *image1 = [self imageWithURLString:url1];  
              
            // 下载第二张图片  
            NSString *url2 = @"http://hiphotos.baidu.com/lvpics/pic/item/3a86813d1fa41768bba16746.jpg";  
            UIImage *image2 = [self imageWithURLString:url2];  
              
            // 回到主线程显示图片  
            dispatch_async(dispatch_get_main_queue(), ^{  
                self.imageView1.image = image1;  
                  
                self.imageView2.image = image2;  
            });  
        });  
    }  

          但是我们发现,事实上,图片一和 二两者在请求的过程中是完全独立的, 但是这里明显的,图片一的下载将阻塞,直到下载完才会开始图片二的下载。这种方式毕竟还是有瑕疵的啊哈。

          Dispatch Group可以帮助解决这个问题。 它是Dispatch Queue的组合,被加入到group的queue会在组内其他的queue也执行完操作的时候,有group统一调用预设好的一个block。最重要的是,在group中的内容是可以异步执行的。也即是多个队列在不同的线程执行。 如果图片大小差不多的话,这种方式将节省我们不一半的时间。  我们来看看这个模型。         

     //dispaach group
        dispatch_queue_t queue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        dispatch_queue_t queue2 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        dispatch_group_t group = dispatch_group_create();
        dispatch_group_async(group, queue1, ^{
            [NSThread sleepForTimeInterval:5.0];
            NSLog(@"第一个项目执行完成。");
        });
    
        dispatch_group_async(group, queue2, ^{
            [NSThread sleepForTimeInterval:10.0];
            NSLog(@"第二个项目执行完成。");
        });
        dispatch_group_notify(group, dispatch_get_main_queue(), ^{
            NSLog(@"集体回调");
        });
    

    6.GCD的其他的用法 

         (1)控制一段代码只执行一次。用在创建单例的时候再好不过了。

     //控制代码只执行一次数
        for(int i = 1 ;i <= 10 ;i++){
            static dispatch_once_t onceToken;
            dispatch_once(&onceToken, ^{
                NSLog(@"被执行 %d次",i);
            });
        }

            打印结果:2017-05-24 18:15:17.704 多线程[10542:898532] 被执行 1次  

           (2) 只能控制执行一次是不是有点不够完美 。dispatch_apply 可以让你控制一段代码执行任意多次。这里的执行是异步执行的,如果为了确保顺序执行,应该对执行的内容进行加锁。

       //控制执行任意多次
        dispatch_queue_t queueX = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        __block int count = 0;
        NSLock *lock = [[NSLock alloc]init];
        dispatch_apply(5, queueX, ^(size_t index) {
            [lock lock];
            NSLog(@"%d,%zu",count++,index);
            [lock unlock];
        }); 

        (3)做一个block式的延时。 除了使用performSelector之外,我们还以使用dispatch_after进行延时,并且是以block的形式进行。

      dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"五秒钟之后执行的代码。");
        });
    

    相关参考:

    http://blog.csdn.net/feisongfeiqin/article/details/50282273

    http://blog.csdn.net/q199109106q/article/details/8566300

  • 相关阅读:
    OSG-提示“error reading file e:1.jpg file not handled”
    OSG-加载地球文件报0x00000005错误,提示error reading file simple.earth file not handled
    QT-找开工程后,最上方提示the code model could not parse an included file, which might lead to incorrect code completion and highlighting, for example.
    我的书《Unity3D动作游戏开发实战》出版了
    java中无符号类型的第三方库jOOU
    Windows批处理备份mysql数据
    使用 DevTools 时,通用Mapper经常会出现 class x.x.A cannot be cast to x.x.A
    Java版本,Java版本MongoDB驱动,驱动与MongoDB数据库,Spring之间的兼容性
    Jrebel本地激活方法
    wget下载指定网站目录下的所有内容
  • 原文地址:https://www.cnblogs.com/FBiOSBlog/p/6864042.html
Copyright © 2020-2023  润新知