iOS之Operation Queues 和 Grand Central Dispatch
2015-08-28 23:49
441 查看
Operation Queues vs. Grand Central Dispatch (GCD)
本文主要介绍了iOS中两个比较常用的两种提高应用程序并发性的方法:Operation Queues 和 Grand Central Dispatch (GCD),以下是本文的内容提纲:基本概念
Operation Queues
Grand Central Dispatch
总结
基本概念
首先,我们先来了解一下在 iOS 并发编程中非常重要的几个概念,这是我们理解 iOS 并发编程的基础:(以下基本概念的内容来自这里.)进程(Process)
进程指的是一个正在运行中的可执行文件。每一个进程都拥有独立的虚拟内存空间和系统资源,包括端口权限等,且至少包含一个主线程和任意数量的辅助线程。另外,当一个进程的主线程退出时,这个进程就结束了。线程(Thread)
线程指的是一个独立的代码执行路径,也就是说线程是代码执行路径的最小分支。在 iOS 中,线程的底层实现是基于 POSIX threads API 的,也就是我们常说的 pthreads 。任务(Task)
即我们需要执行的工作,是一个抽象的概念,用通俗的话说,就是一段代码。串行 vs. 并发
从本质上来说,串行和并发的主要区别在于允许同时执行的任务数量。串行,指的是一次只能执行一个任务,必须等一个任务执行完成后才能执行下一个任务;并发,则指的是允许多个任务同时执行。同步 vs. 异步
同样的,同步和异步操作的主要区别在于是否等待操作执行完成,亦即是否阻塞当前线程。同步操作会等待操作执行完成后再继续执行接下来的代码,而异步操作则恰好相反,它会在调用后立即返回,不会等待操作的执行结果。队列 vs. 线程
有一些对 iOS 并发编程不太了解的同学可能会对队列和线程产生混淆,不清楚它们之间的区别与联系,因此,我觉得非常有必要在这里简单地介绍一下。在 iOS 中,有两种不同类型的队列,分别是串行队列和并发队列。正如我们上面所说的,串行队列一次只能执行一个任务,而并发队列则可以允许多个任务同时执行。iOS 系统就是使用这些队列来进行任务调度的,它会根据调度任务的需要和系统当前的负载情况动态地创建和销毁线程,而不需要我们手动地管理。Operation Queues
Operation Queues :是一个建立在 GCD 的基础之上的,面向对象的解决方案。相对 GCD 来说,使用 Operation Queues 会增加一点点额外的开销,但是我们却换来了非常强大的灵活性和功能,我们可以给 operation 之间添加依赖关系、取消一个正在执行的 operation 、暂停和恢复 operation queue 等。Operation 对象
在iOS开发中,我们需要把我们的操作(或者说任务)添加到一个NSOperation的对象中,但是NSOperation是一个抽象类,我们无法直接初始化一个NSOperation的实例对象。不过,iOS给我们提供了两种三个方法来获取operation对象:NSInvocationOperation (系统自带):我们可以通过一个 object 和 selector 非常方便地创建一个 NSInvocationOperation ,这是一种非常动态和灵活的方式。
NSBlockOperation (系统自带):我们可以使用 NSBlockOperation 来并发执行一个或多个 block ,只有当一个 NSBlockOperation 所关联的所有 block 都执行完毕时,这个 NSBlockOperation 才算执行完成,有点类似于 dispatch_group 的概念。
自定义 Operation 对象:如果系统自带的operation的两个子类不能满足我的需求时,我们可以定义自己的 NSOperation 子类
NSInvocationOperation
//初始化方法 NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationTask) object:nil]; //我们需要执行的任务 -(void)invocationTask { NSLog(@"这个是invocationOperation调用的函数(Task)"); sleep(3); }
NSBlockOperation
//初始化方法,通过追加block的方式添加任务,比较方便 NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{ NSLog(@"这个是blockOperation初始化时添加的block任务"); sleep(3); }];
自定义Operation对象
每一个 operation 都应该至少实现以下两个方法:一个自定义的初始化方法。
main 方法。
@implementation CustomOperation //一个自定义的初始化方法 -(id)initWithData:(NSData *)data { self = [super init]; if (self) { //自定义初始化操作.... self.data = data; } return self; } //main 方法 -(void)main { NSLog(@"自定义Operation类,执行了"); sleep(3); } @end
一个 operation 开始执行后,它会一直执行它的任务直到完成或被取消为止。假设这样一种情况,我们在某一时刻想要取消一个自定义的operation中的一个任务,显然上面的一段代码,无论如何我们都无法看出它支持中途取消。
为了让我们自定义的 operation 能够支持取消事件,我们需要在代码中定期地检查 isCancelled 方法的返回值,一旦检查到这个方法返回 YES ,我们就需要立即停止执行接下来的任务。根据苹果官方的说法,isCancelled 方法本身是足够轻量的,所以就算是频繁地调用它也不会给系统带来太大的负担。所以,我们可以对上述代码进行如下修改:(比较简陋,只为说明思路)
@implementation CustomOperation -(void)main { //如果已被取消,则直接返回 if (self.isCancelled) { return; } // for(NSUInteger i = 0 ; i < 100 ; i++) // { // if (self.isCancelled) // { // return; // } // // NSLog(@"for循环:%lu",i); // } NSLog(@"自定义Operation类,执行了"); sleep(3); [self setFinished:YES]; } @end
其实,以上代码只是实现了一个非并发的NSOperation子类。既然用到了自定义,我们当然可以实现一个可以并发的子类:
上面的一段代码,只是实现了一个非并发的NSOperation子类,当然,我们也可以实现一个可以并发的子类。
配置并发执行的 Operation
在默认情况下,operation 是同步执行的,也就是说在调用它的 start 方法的线程中执行它们的任务。而在 operation 和 operation queue 结合使用时,operation queue 可以为非并发的 operation 提供线程,因此,大部分的 operation 仍然可以异步执行。但是,如果你想要手动地执行一个 operation ,又想这个 operation 能够异步执行的话,你需要做一些额外的配置来让你的 operation 支持并发执行。下面列举了一些你可能需要重写的方法:start :(必须)
所有并发执行的 operation 都必须要重写这个方法,替换掉 NSOperation 类中的默认实现。start 方法是一个 operation 的起点,我们可以在这里配置任务执行的线程或者一些其它的执行环境。另外,需要特别注意的是,在我们重写的 start 方法中一定不要调用-父类的实现;
main :(可选)
通常这个方法就是专门用来实现与该 operation 相关联的任务的。尽管我们可以直接在 start 方法中执行我们的任务,但是用 main 方法来实现我们的任务可以使设置代码和任务代码得到分离,从而使 operation 的结构更清晰;
isExecuting 和 isFinished :(必须)
并发执行的 operation 需要负责配置它们的执行环境,并且向外界客户报告执行环境的状态。因此,一个并发执行的 operation 必须要维护一些状态信息,用来记录它的任务是否正在执行,是否已经完成执行等。此外,当这两个方法所代表的值发生变化时,我们需要生成相应的 KVO 通知,以便外界能够观察到这些状态的变化;
isConcurrent :(必须)
这个方法的返回值用来标识一个 operation 是否是并发的 operation ,我们需要重写这个方法并返回 YES 。
@interface CustomOperation() //用于标记operation是否结束 @property(nonatomic,assign)BOOL is_finished; //用于标记operation是否正在执行 @property(nonatomic,assign)BOOL is_executing; @end @implementation CustomOperation -(id)initWithData:(NSData *)data { self = [super init]; if (self) { _is_finished = NO; _is_executing = NO; } return self; } //重写NSOperation的start方法 - (void)start { if (self.isCancelled) { [self willChangeValueForKey:@"isFinished"]; _is_finished = YES; [self didChangeValueForKey:@"isFinished"]; return; } [self willChangeValueForKey:@"isExecuting"]; //新开启一个线程执行用户定任务 [NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil]; _is_executing = YES; [self didChangeValueForKey:@"isExecuting"]; } //这个函数的名称可以自定义,没有要求必须是main -(void)main { NSLog(@"自定义Operation类,执行了"); sleep(3); [self willChangeValueForKey:@"isExecuting"]; _is_executing = NO; [self didChangeValueForKey:@"isExecuting"]; [self willChangeValueForKey:@"isFinished"]; _is_finished = YES; [self didChangeValueForKey:@"isFinished"]; } - (BOOL)isConcurrent { return YES; } - (BOOL)isExecuting { return _is_executing; } -(BOOL)isFinished { return _is_finished; } @end
维护 KVO 通知
NSOperation 类的以下 key paths 支持 KVO 通知,我们可以通过观察这些 key paths 非常方便地监听到一个 operation 内部状态的变化:isCancelled
isConcurrent
isExecuting
isFinished
isReady
dependencies
queuePriority
completionBlock
与重写 main 方法不同的是,如果我们重写了 start 方法或者对 NSOperation 类做了大量定制的话,我们需要保证自定义的 operation 在这些 key paths 上仍然支持 KVO 通知。比如,当我们重写了 start 方法时,我们需要特别关注的是 isExecuting 和 isFinished 这两个 key paths ,因为这两个 key paths 最可能受重写 start 方法的影响。
执行 Operation 对象
最终,我们需要执行 operation 来调度与其关联的任务。目前,主要有两种方式来执行一个 operation :将 operation 添加到一个 operation queue 中,让 operation queue 来帮我们自动执行;
//初始化方法,通过追加block的方式添加任务,比较方便 NSOperationQueue *myQueue = [[NSOperationQueue alloc] init]; //把1个operation添加到队列中,自动执行 [myQueue addOperation:invocationOperation]; //把众多个operation添加到队列中,自动执行 [myQueue addOperations:@[invocationOperation,blockOperation,customOperation] waitUntilFinished:NO];
直接调用 start 方法手动执行 operation 。
//直接调用start方法执行operation [invocationOperation start];
取消 Operation
取消单个operation//取消单个operation [invocationOperation cancel];
取消operation queue的所有operation
//取消operation queue的所有operation [myQueue cancelAllOperations];
operation之间的依赖关系
依赖关系可以顺序地执行相关的operation对象,依赖于其它操作,则必须等到该操作完成之后自己才能开始。你可以创建一对一的依赖关系,也可以创建多个对象之间的依赖图。使用 NSOperation 的 addDependency: 方法在两个operation对象之间建立依赖关系。表示当前operation对象将依赖于参数指定的目标operation对象。依赖关系不局限于相同queue中的operations对象,Operation对象会管理自己的依赖,因此完全可以在不同的queue之间的Operation对象创建依赖关系:
self.customOperation = [[CustomOperation alloc] initWithData:nil]; //添加依赖关系,使invocationOperation 依赖于 customOperation,在customOperation完成之前,不会执行invocationOperation(即使invocationOperation的优先级大于customOperation)。 [self.invocationOperation addDependency:_customOperation];
Grand Central Dispatch(GCD)
Grand Central Dispatch:异步执行任务的技术之一,一般是将应用程序中记述的线程管理用代码在系统级实现。开发者只需要定义要执行的任务并追加到适当的队列(Dispatch Queue)中,GCD就能生成必要的线程并计划执行任务。GCD中的API
Dispatch Queue
”开发者要做的只是定义想执行的任务并追加到适当的Dispatch Queue中“——苹果官方对GCD的说明。这句话用源码表示如下:
/*该代码使用Block语法,通过dispatch_async把要执行的任务追加到 Dispatch Queue的对象queue中,仅此就可以让任务在另一个线程中执行 */ dispatch_async(queue,^{ //想执行的任务 });
PS:Dispatch Queue按照追加的顺序执行处理(即FIFO)
Serial Dispatch Queue(串行) :等待现在执行的处理结束后,才会继续执行新的任务。
//串行队列创建方式 dispatch_queue_t mySerialQueue; mySerialQueue = dispatch_queue_create("mySerialQueue",NULL); //or dispatch_queue_create("mySerialQueue",DISPATCH_QUEUE_SERIAL);
Concurrent Dispatch Queue(并行):不用等待现在执行的处理结束后,才会继续执行新的任务。
//并行队列创建方式 dispatch_queue_t myConcurrentQueue; myConcurrentQueue = dispatch_queue_create("myConcurrentQueue",DISPATCH_QUEUE_CONCURRENT);
对于SDK版本大于iOS6.0的情况下,GCD对象已经纳入了ARC的管理范围,我们不需要再手动调用 dispatch_release,否则,若SDK小于iOS6.0的时候,即使我们开启了ARC,GCD对象还必须得自己管理(即dispatch_queue_create之后,在结束之前需要手动调用dispatch_release)
来看看如下的代码:
//并行队列创建方式 dispatch_queue_t myConcurrentQueue; myConcurrentQueue = dispatch_queue_create("myConcurrentQueue",DISPATCH_QUEUE_CONCURRENT);
dispatch_async(myConcurrentQueue, ^{
NSLog("要执行的任务");
});
dispatch_release(myConcurrentQueue);
“要执行的任务”这句话能否执行呢?因为我们在把这个任务追加到队列中之后,就释放了myConcurrentQueue,这样会不会导致我们追加的任务不能执行呢?答案是否定的:
在dispatch_async函数中追加Block到Dispatch Queue中,该Block会通过dispatch_retain自动持有Dispatch Queue,一旦Block执行结束,就会通过dispatch_release函数释放所持有的Dispatch Queue。然后,谁都不会持有Dispatch Queue。
Main Dispatch Queue / Global Dispatch Queue
实际上,我们并不用特意生成一个Dispatch Queue系统也会给我们提供几个,那就是:Main Dispatch Queue / Global Dispatch Queue。Main Dispatch Queue :正如其名称中含有“Main”一样,是在主线程中执行的Dispatch Queue。因为主线程只有一个,所以Main Dispatch Queue自然就是Serial Dispatch Queue。而追加到Main Dispatch Queue的处理在主线程的Runloop中执行,因此,一些用户界面(UI)更新等处理必须追加到Main Dispatch Queue中。
//获取Main Dispatch Queue方式 dispatch_queue_t mainQueue; mainQueue = dispatch_get_main_queue();
Global Dispatch Queue:是所有程序都可以使用的Concurrent Dispatch Queue。它共拥有4个优先级:DISPATCH_QUEUE_PRIORITY_HIGHT、DISPATCH_QUEUE_PRIORITY_DEFAULT、DISPATCH_QUEUE_PRIORITY_LOW和DISPATCH_QUEUE_PRIORITY_BACKGROUND。
//获取Global Dispatch Queue方式 dispatch_queue_t globalQueue; //默认优先级 globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
dispatch_after
想在指定时间后,执行处理的情况下,可以使用dispatch_after://设定为3秒钟之后 dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull*NSEC_PER_SEC); dispatch_after(time,dispatch_get_main_queue(),^{ NSLog(@"3秒钟后,将会打印这句话"); });
需要注意的是,dispatch_after并不是在指定的时间后执行处理任务,而是在指定时间后把处理任务追加到Dispatch Queue中。
拿上面的代码来说,当任务被追加到Main Dispatch Queue后,因为Main Dispatch Queue在Runloop中执行,假设Runloop每隔1/60秒执行一次,那么,这段代码最早会在3秒钟之后立即执行,最晚会在3 + 1/60秒之后执行。
Dispatch Group
在追加到Dispatch Queue中的多个处理任务都结束后向执行结束处理,这种情况很常见。在一个串行队列中(Serial Dispatch Queue)中很容易实现,但是在并行队列中(Concurrent Dispatch Queue)就比较难办了。这种情况下,我们可以使用Dispatch Group:dispatch_queue_t myQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_group_t myGroup = dispatch_group_create(); dispatch_group_async(myGroup, myQueue, ^{ NSLog(@"这是第1个block,执行3秒"); sleep(3); }); dispatch_group_async(myGroup, myQueue, ^{ NSLog(@"这是第2个block,执行7秒"); sleep(7); }); dispatch_group_async(myGroup, myQueue, ^{ NSLog(@"这是第3个block,执行1秒"); sleep(1); }); dispatch_group_async(myGroup, myQueue, ^{ NSLog(@"这是第4个block,执行5秒"); sleep(5); }); dispatch_group_notify(myGroup, myQueue, ^{ //当检测到所有的处理都已经完成 NSLog(@"所有的block都执行完了,就会执行这里"); });
另外,在Dispatch Group中也可以使用dispatch_group_wait函数等待全部处理都执行结束
dispatch_queue_t myQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_group_t myGroup = dispatch_group_create(); dispatch_group_async(myGroup, myQueue, ^{ NSLog(@"这是第1个block,执行3秒"); sleep(3); }); dispatch_group_async(myGroup, myQueue, ^{ NSLog(@"这是第2个block,执行7秒"); sleep(7); }); dispatch_group_async(myGroup, myQueue, ^{ NSLog(@"这是第3个block,执行1秒"); sleep(1); }); dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull*NSEC_PER_SEC); //myGroup所在线程会阻塞3秒钟 long result = dispatch_group_wait(myGroup, time); //myGroup所在线程会永久阻塞,直到所有处理都结束 //long result = dispatch_group_wait(myGroup, DISPATCH_TIME_FOREVER); if (result == 0) { NSLog(@"myGroup中的所有处理都已经完成"); } else { NSLog(@"myGroup仍有处理未完成"); }
dispatch_barrier_async
在涉及到数据库和文件操作的时候,并发会不可避免的带来数据竞争的问题。虽然使用串行队列可以解决这种竞争,但其效率太低,因为读取操作可以并行处理。GCD为我们提供了比较高效简单的方法——dispatch_barrier_async函数。dispatch_barrier_async和Concurrent Dispatch Queue一起使用:
dispatch_queue_t myQueue = dispatch_queue_create("muConcurrentQueu", DISPATCH_QUEUE_CONCURRENT); __block int flag = 100; dispatch_async(myQueue, ^{ NSLog(@"1. 读操作,可以并行%d",flag); sleep(3); }); dispatch_async(myQueue, ^{ NSLog(@"2. 读操作,可以并行%d",flag); sleep(3); }); dispatch_async(myQueue, ^{ NSLog(@"3. 读操作,可以并行%d",flag); sleep(3); }); dispatch_async(myQueue, ^{ NSLog(@"4. 读操作,可以并行%d",flag); sleep(3); }); dispatch_barrier_async(myQueue, ^{ NSLog(@"---写入操作,不可以并行"); flag = 200; sleep(10); }); dispatch_async(myQueue, ^{ NSLog(@"5. 读操作,可以并行%d",flag); sleep(3); }); dispatch_async(myQueue, ^{ NSLog(@"6. 读操作,可以并行%d",flag); sleep(3); }); dispatch_async(myQueue, ^{ NSLog(@"7. 读操作,可以并行%d",flag); sleep(3); }); dispatch_async(myQueue, ^{ NSLog(@"8. 读操作,可以并行%d",flag); sleep(3); }); dispatch_async(myQueue, ^{ NSLog(@"9. 读操作,可以并行%d",flag); sleep(3); });
其执行结果如下(注意执行时间):
2015-09-07 23:05:39.593 NSOperationTestDemo[727:47140] 1. 读操作,可以并行100 2015-09-07 23:05:39.593 NSOperationTestDemo[727:47139] 2. 读操作,可以并行100 2015-09-07 23:05:39.593 NSOperationTestDemo[727:47141] 3. 读操作,可以并行100 2015-09-07 23:05:39.593 NSOperationTestDemo[727:47146] 4. 读操作,可以并行100 2015-09-07 23:05:42.603 NSOperationTestDemo[727:47146] ---写入操作,不可以并行 2015-09-07 23:05:52.607 NSOperationTestDemo[727:47185] 7. 读操作,可以并行200 2015-09-07 23:05:52.606 NSOperationTestDemo[727:47184] 6. 读操作,可以并行200 2015-09-07 23:05:52.607 NSOperationTestDemo[727:47186] 8. 读操作,可以并行200 2015-09-07 23:05:52.606 NSOperationTestDemo[727:47146] 5. 读操作,可以并行200 2015-09-07 23:05:52.607 NSOperationTestDemo[727:47188] 9. 读操作,可以并行200
dispatch_barrier_async函数会等待追加到Concurrent Dispatch Queue (此并发dispatch queue必须是使用dispatch_queue_create函数创建的,不能使用dispatch_get_global_queue) 上的并行执行的处理(比如读取操作)全部结束后,再将指定的处理(比如写入操作)追加到Concurrent Dispatch Queue中。然后,当由dispatch_barrier_async追加的处理执行完毕后,Concurrent Dispatch Queue才会恢复为一般的队列,然后可以继续追加处理到Concurrent Dispatch Queue中并行执行。
dispatch_async / dispatch_sync
dispatch_async :将block发送到指定线程去执行,当前线程不会等待,会继续向下执行,比较常用。dispatch_sync :也是将block发送到指定的线程去执行,但是当前的线程会阻塞,等待block在指定线程执行完成后才会继续向下执行
dispatch_apply
dispathc_apply 是dispatch_sync 和dispatch_group的关联API。它以指定的次数将指定的Block加入到指定的队列中,并等待队列中操作全部完成(与dispatch_sync相同),所以推荐在dispatch_async函数中非同步的执行dispatch_apply函数。dispatch_queue_t myQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); __block NSMutableDictionary *dict = [[NSMutableDictionary alloc] initWithCapacity:10]; NSLog(@"dispatch_apply执行之前的字典:%@",dict); dispatch_async(myQueue, ^{ dispatch_apply(10, myQueue, ^(size_t index) { [dict setObject:@"value" forKey:[NSString stringWithFormat:@"%zu",index]]; }); dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"主线程内打印字典:%@",[dict allKeys]); }); });
上述代码的第一个参数是重复的次数,第二个参数是追加处理的Dispatch Queue,第三个参数是追加的处理,其执行结果如下:
2015-08-30 22:45:26.176 NSOperationTestDemo[582:31009] dispatch_apply执行之前的字典:{ } 2015-08-30 22:45:26.179 NSOperationTestDemo[582:31009] 主线程内打印字典:( 7, 3, 8, 4, 0, 9, 5, 1, 6, 2 )
dispatch_suspend / dispatch_resume
当追加大量的处理到Dispatch Queue中时,我们可以控制Dispatch Queue的挂起或者恢复执行:dispatch_suspend(aQueue):此操作后,在aQueue中已经追加,但未执行的处理将不会执行,已经开始执行(或正在执行的处理)不会受到影响。
dispatch_resume(aQueue):恢复aQueu的执行。
dispatch_once
dispatch_once函数是保证在应用程序执行中只执行一次制定处理的API,比较常见的用处,就是单例:+ (id)sharedInstance { static dispatch_once_t onceToken; static id sharedInstance; dispatch_once(&onceToken, ^{ sharedInstance = [[self alloc] init]; }); return sharedInstance; }
Dispatch Source
使用Dispatch Source可以实现一个计时准确的定时器:dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue()); dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, 3ull*NSEC_PER_SEC), DISPATCH_TIME_FOREVER, 0); dispatch_source_set_event_handler(timer, ^{ NSLog(@"3秒后,进来了"); //定时触发的处理结束后,要取消定时器 dispatch_source_cancel(timer); }); dispatch_source_set_cancel_handler(timer, ^{ NSLog(@"进来后,取消了timer"); }); //开始执行定时 dispatch_resume(timer);
总结
Operation Queue:是标准的NSObject对象,可以开发人员继承以实现更多的自定义功能,拥有更高的自由度,其要执行的任务也可以被取消。在NSOperation中,还能够设置NSOperation的priority优先级,能够使同一个并行队列中的任务区分先后地执行,而且,可以通过添加依赖(addDependency:)使同一个并行队列中的任务有顺序的先后执行。GCD:是底层的C语言构成的API,任务以Block的形式追加,代码简洁,执行效率高(本人比较喜欢GCD)。
相关文章推荐
- PHP生成UUID
- .NET 的 Debug 和 Release build 对执行速度的影响
- 关于CodeFirst异常:无法确定类型'XXX'和类型‘YYY’之间的关联的主体端,必须使用关系 Fluent API 或数据注释显式配置此关联的主体端。
- 【我们都爱Paul Hegarty】斯坦福IOS8公开课个人笔记44 Popover Segue
- Android UI设计:Notification
- Android UI设计:PopupWindow
- MongoVUE对json数据的导入和导出
- Unable to run Vmware workstation 11 - failed to build vmnet
- [leetcode-187]Repeated DNA Sequences(java)
- easyui-editing datagrid 批量保存数据 二
- 野人学Android基础篇之初探UI控件第五课--RadioGroup
- 从客户端中检测到有潜在危险的 Request.Form 值
- easyui-editing datagrid 批量保存数据 一
- Android UI设计:DatePickerDialog与TimePickerDialog
- 【APUE】Chapter12 Thread Control
- UIAlertController
- 黑马程序员——31,GUI图形用户界面
- iOS UIImagePickerController拍照与摄像
- [LeetCode] 232 - Implement Queue using Stacks
- iPhone开发之UIScrollView滚动组件的使用(六)通过代理实现手势缩放——(拖线实现)