[文章]RunLoop学习笔记,从CF层面了解由于CFRunLoopMode机制iOS程序ScrollView的滑动为何如此平滑的原因。还有介绍AFNetworking如何单独发起一个global thread内置runloop达到不占用主线程又不耗CPU资源的。

简介

简单的说run loop是事件驱动的一个大循环,如下代码所示

int main(int argc, char * argv[]) {
     //程序一直运行状态
     while (AppIsRunning) {
          //睡眠状态,等待唤醒事件
          id whoWakesMe = SleepForWakingUp();
          //得到唤醒事件
          id event = GetEvent(whoWakesMe);
          //开始处理事件
          HandleEvent(event);
     }
     return 0;
}

Cocoa会涉及到Run Loops的

  • 系统级:GCD,mach kernel,block,pthread
  • 应用层:NSTimer,UIEvent,Autorelease,NSObject(NSDelayedPerforming),NSObject(NSThreadPerformAddition),CADisplayLink,CATransition,CAAnimation,dispatch_get_main_queue()(GCD中dispatch到main queue的block会被dispatch到main RunLoop执行),NSPort,NSURLConnection,AFNetworking(这个第三方网络请求框架使用在开启新线程中添加自己的run loop监听事件)

在Main thread堆栈中所处位置

堆栈最底层是start(dyld),往上依次是main,UIApplication(main.m) -> GSEventRunModal(Graphic Services) -> RunLoop(包含CFRunLoopRunSpecific,CFRunLoopRun,__CFRunLoopDoSouces0,__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION) -> Handle Touch Event

RunLoop原理

CFRunLoop开源代码:http://opensource.apple.com/source/CF/CF-855.17/

执行顺序的伪代码

SetupThisRunLoopRunTimeoutTimer(); // by GCD timer
do {
     __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
     __CFRunLoopDoObservers(kCFRunLoopBeforeSources);

     __CFRunLoopDoBlocks();
     __CFRunLoopDoSource0();

     CheckIfExistMessagesInMainDispatchQueue(); // GCD

     __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
     var wakeUpPort = SleepAndWaitForWakingUpPorts();
     // mach_msg_trap
     // Zzz...
     // Received mach_msg, wake up
     __CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
     // Handle msgs
     if (wakeUpPort == timerPort) {
          __CFRunLoopDoTimers();
     } else if (wakeUpPort == mainDispatchQueuePort) {
          // GCD
          __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
     } else {
          __CFRunLoopDoSource1();
     }
     __CFRunLoopDoBlocks();
} while (!stop && !timeout);

构成

Thread包含一个CFRunLoop,一个CFRunLoop包含一种CFRunLoopMode,mode包含CFRunLoopSource,CFRunLoopTimer和CFRunLoopObserver。

CFRunLoopMode

RunLoop只能运行在一种mode下,如果要换mode当前的loop也需要停下重启成新的。利用这个机制,ScrollView过程中NSDefaultRunLoopMode的mode会切换UITrackingRunLoopMode来保证ScrollView的流畅滑动不受只能在NSDefaultRunLoopMode时处理的事件影响滑动。同时mode还是可定制的。

  • NSDefaultRunLoopMode:默认,空闲状态
  • UITrackingRunLoopMode:ScrollView滑动时
  • UIInitializationRunLoopMode:启动时
  • NSRunLoopCommonModes:Mode集合 Timer计时会被scrollView的滑动影响的问题可以通过将timer添加到NSRunLoopCommonModes来解决
//将timer添加到NSDefaultRunLoopMode中
[NSTimer scheduledTimerWithTimeInterval:1.0
     target:self
     selector:@selector(timerTick:)
     userInfo:nil
     repeats:YES];
//然后再添加到NSRunLoopCommonModes里
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0
     target:self
     selector:@selector(timerTick:)
     userInfo:nil
     repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

CFRunLoopTimer

NSTimer是对RunLoopTimer的封装

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray *)modes;

+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;
- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSString *)mode;

CFRunLoopSource

  • source0:处理如UIEvent,CFSocket这样的事件
  • source1:Mach port驱动,CFMachport,CFMessagePort

CFRunLoopObserver

Cocoa框架中很多机制比如CAAnimation等都是由RunLoopObserver触发的。observer到当前状态的变化进行通知。

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
     kCFRunLoopEntry = (1UL << 0),
     kCFRunLoopBeforeTimers = (1UL << 1),
     kCFRunLoopBeforeSources = (1UL << 2),
     kCFRunLoopBeforeWaiting = (1UL << 5),
     kCFRunLoopAfterWaiting = (1UL << 6),
     kCFRunLoopExit = (1UL << 7),
     kCFRunLoopAllActivities = 0x0FFFFFFFU
};

使用RunLoop的案例

AFNetworking

使用NSOperation+NSURLConnection并发模型都会面临NSURLConnection下载完成前线程退出导致NSOperation对象接收不到回调的问题。AFNetWorking解决这个问题的方法是按照官方的guidhttps://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSURLConnection_Class/Reference/Reference.html#//apple_ref/occ/instm/NSURLConnection/initWithRequest:delegate:startImmediately:上写的NSURLConnection的delegate方法需要在connection发起的线程runloop中调用,于是AFNetWorking直接借鉴了Apple自己的一个Demohttps://developer.apple.com/LIBRARY/IOS/samplecode/MVCNetworking/Introduction/Intro.html的实现方法单独起一个global thread,内置一个runloop,所有的connection都由这个runloop发起,回调也是它接收,不占用主线程,也不耗CPU资源。

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
     @autoreleasepool {
          [[NSThread currentThread] setName:@"AFNetworking"];

          NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
          [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
          [runLoop run];
     }
}

+ (NSThread *)networkRequestThread {
     static NSThread *_networkRequestThread = nil;
     static dispatch_once_t oncePredicate;
     dispatch_once(&oncePredicate, ^{
          _networkRequestThread =
          [[NSThread alloc] initWithTarget:self
               selector:@selector(networkRequestThreadEntryPoint:)
               object:nil];
          [_networkRequestThread start];
     });

     return _networkRequestThread;
}

类似的可以用这个方法创建一个常驻服务的线程。

TableView中实现平滑滚动延迟加载图片

利用CFRunLoopMode的特性,可以将图片的加载放到NSDefaultRunLoopMode的mode里,这样在滚动UITrackingRunLoopMode这个mode时不会被加载而影响到。

UIImage *downloadedImage = ...;
[self.avatarImageView performSelector:@selector(setImage:)
     withObject:downloadedImage
     afterDelay:0
     inModes:@[NSDefaultRunLoopMode]];

接到程序崩溃时的信号进行自主处理例如弹出提示等

CFRunLoopRef runLoop = CFRunLoopGetCurrent();
NSArray *allModes = CFBridgingRelease(CFRunLoopCopyAllModes(runLoop));
while (1) {
     for (NSString *mode in allModes) {
          CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
     }
}

异步测试

- (BOOL)runUntilBlock:(BOOL(^)())block timeout:(NSTimeInterval)timeout
{
     __block Boolean fulfilled = NO;
     void (^beforeWaiting) (CFRunLoopObserverRef observer, CFRunLoopActivity activity) =
     ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
          fulfilled = block();
          if (fulfilled) {
               CFRunLoopStop(CFRunLoopGetCurrent());
          }
     };

     CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopBeforeWaiting, true, 0, beforeWaiting);
     CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

     // Run!
     CFRunLoopRunInMode(kCFRunLoopDefaultMode, timeout, false);

     CFRunLoopRemoveObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
     CFRelease(observer);

     return fulfilled;
}

[文章]iOS并发编程笔记,包含GCD,Operation Queues,Run Loops,如何在后台绘制UI,后台I/O处理,最佳安全实践避免互斥锁死锁优先级反转等,以及如何使用GCD监视进程文件文件夹,并发测试的方案等

线程

使用Instruments的CPU strategy view查看代码如何在多核CPU中执行。创建线程可以使用POSIX 线程API,或者NSThread(封装POSIX 线程API)。下面是并发4个线程在一百万个数字中找最小值和最大值的pthread例子:

#import <pthread.h>

struct threadInfo {
     uint32_t * inputValues;
     size_t count;
};

struct threadResult {
     uint32_t min;
     uint32_t max;
};

void * findMinAndMax(void *arg)
{
     struct threadInfo const * const info = (struct threadInfo *) arg;
     uint32_t min = UINT32_MAX;
     uint32_t max = 0;
     for (size_t i = 0; i < info->count; ++i) {
          uint32_t v = info->inputValues[i];
          min = MIN(min, v);
          max = MAX(max, v);
     }
     free(arg);
     struct threadResult * const result = (struct threadResult *) malloc(sizeof(*result));
     result->min = min;
     result->max = max;
     return result;
}

int main(int argc, const char * argv[])
{
     size_t const count = 1000000;
     uint32_t inputValues[count];

     // 使用随机数字填充 inputValues
     for (size_t i = 0; i < count; ++i) {
          inputValues[i] = arc4random();
     }

     // 开始4个寻找最小值和最大值的线程
     size_t const threadCount = 4;
     pthread_t tid[threadCount];
     for (size_t i = 0; i < threadCount; ++i) {
          struct threadInfo * const info = (struct threadInfo *) malloc(sizeof(*info));
          size_t offset = (count / threadCount) * i;
          info->inputValues = inputValues + offset;
          info->count = MIN(count - offset, count / threadCount);
          int err = pthread_create(tid + i, NULL, &findMinAndMax, info);
          NSCAssert(err == 0, @"pthread_create() failed: %d", err);
     }
     // 等待线程退出
     struct threadResult * results[threadCount];
     for (size_t i = 0; i < threadCount; ++i) {
          int err = pthread_join(tid[i], (void **) &(results[i]));
          NSCAssert(err == 0, @"pthread_join() failed: %d", err);
     }
     // 寻找 min 和 max
     uint32_t min = UINT32_MAX;
     uint32_t max = 0;
     for (size_t i = 0; i < threadCount; ++i) {
          min = MIN(min, results[i]->min);
          max = MAX(max, results[i]->max);
          free(results[i]);
          results[i] = NULL;
     }

     NSLog(@"min = %u", min);
     NSLog(@"max = %u", max);
     return 0;
}

使用NSThread来写

@interface FindMinMaxThread : NSThread
@property (nonatomic) NSUInteger min;
@property (nonatomic) NSUInteger max;
- (instancetype)initWithNumbers:(NSArray *)numbers;
@end

@implementation FindMinMaxThread {
     NSArray *_numbers;
}

- (instancetype)initWithNumbers:(NSArray *)numbers
{
     self = [super init];
     if (self) {
          _numbers = numbers;
     }
     return self;
}

- (void)main
{
     NSUInteger min;
     NSUInteger max;
     // 进行相关数据的处理
     self.min = min;
     self.max = max;
}
@end

//启动一个新的线程,创建一个线程对象
SMutableSet *threads = [NSMutableSet set];
NSUInteger numberCount = self.numbers.count;
NSUInteger threadCount = 4;
for (NSUInteger i = 0; i < threadCount; i++) {
     NSUInteger offset = (count / threadCount) * i;
     NSUInteger count = MIN(numberCount - offset, numberCount / threadCount);
     NSRange range = NSMakeRange(offset, count);
     NSArray *subset = [self.numbers subarrayWithRange:range];
     FindMinMaxThread *thread = [[FindMinMaxThread alloc] initWithNumbers:subset];
     [threads addObject:thread];
     [thread start];
}

Grand Central Dispatch

GCD概要

  • 和operation queue一样都是基于队列的并发编程API,他们通过集中管理大家协同使用的线程池。
  • 公开的5个不同队列:运行在主线程中的main queue,3个不同优先级的后台队列(High Priority Queue,Default Priority Queue,Low Priority Queue),以及一个优先级更低的后台队列Background Priority Queue(用于I/O)
  • 可创建自定义队列:串行或并列队列。自定义一般放在Default Priority Queue和Main Queue里。

dispatch_once用法

+ (UIColor *)boringColor;
{
     static UIColor *color;
     //只运行一次
     static dispatch_once_t onceToken;
     dispatch_once(&onceToken, ^{
          color = [UIColor colorWithRed:0.380f green:0.376f blue:0.376f alpha:1.000f];
     });
     return color;
}

延后执行

使用dispatch_after

- (void)foo
{
     double delayInSeconds = 2.0;
     dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t) (delayInSeconds * NSEC_PER_SEC));
     dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
          [self bar];
     });
}

GCD队列

队列默认是串行的,只能执行一个单独的block,队列也可以是并行的,同一时间执行多个block

- (id)init;
{
     self = [super init];
     if (self != nil) {
          NSString *label = [NSString stringWithFormat:@"%@.isolation.%p", [self class], self];
          self.isolationQueue = dispatch_queue_create([label UTF8String], 0);

          label = [NSString stringWithFormat:@"%@.work.%p", [self class], self];
          self.workQueue = dispatch_queue_create([label UTF8String], 0);
     }
     return self;
}

多线程并发读写同一个资源

//创建队列
self.isolationQueue = dispatch_queue_create([label UTF8String], DISPATCH_QUEUE_CONCURRENT);
//改变setter
- (void)setCount:(NSUInteger)count forKey:(NSString *)key
{
     key = [key copy];
     //确保所有barrier都是async异步的
     dispatch_barrier_async(self.isolationQueue, ^(){
          if (count == 0) {
               [self.counts removeObjectForKey:key];
          } else {
               self.counts[key] = @(count);
          }
     });
}

都用异步处理避免死锁,异步的缺点在于调试不方便,但是比起同步容易产生死锁这个副作用还算小的。

异步API写法

设计一个异步的API调用dispatch_async(),这个调用放在API的方法或函数中做。让API的使用者设置一个回调处理队列

- (void)processImage:(UIImage *)image completionHandler:(void(^)(BOOL success))handler;
{
     dispatch_async(self.isolationQueue, ^(void){
          // do actual processing here
          dispatch_async(self.resultQueue, ^(void){
               handler(YES);
          });
     });
}

dispatch_apply进行快速迭代

for (size_t y = 0; y < height; ++y) {
     for (size_t x = 0; x < width; ++x) {
          // Do something with x and y here
     }
}
//使用dispatch_apply可以运行的更快
dispatch_apply(height, dispatch_get_global_queue(0, 0), ^(size_t y) {
     for (size_t x = 0; x < width; x += 2) {
          // Do something with x and y here
     }
});

Block组合

dispatch_group_t group = dispatch_group_create();

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_async(group, queue, ^(){
     // 会处理一会
     [self doSomeFoo];
     dispatch_group_async(group, dispatch_get_main_queue(), ^(){
          self.foo = 42;
     });
});
dispatch_group_async(group, queue, ^(){
     // 处理一会儿
     [self doSomeBar];
     dispatch_group_async(group, dispatch_get_main_queue(), ^(){
          self.bar = 1;
     });
});

// 上面的都搞定后这里会执行一次
dispatch_group_notify(group, dispatch_get_main_queue(), ^(){
     NSLog(@"foo: %d", self.foo);
     NSLog(@"bar: %d", self.bar);
});

如何对现有API使用dispatch_group_t

//给Core Data的-performBlock:添加groups。组合完成任务后使用dispatch_group_notify来运行一个block即可。
- (void)withGroup:(dispatch_group_t)group performBlock:(dispatch_block_t)block
{
     if (group == NULL) {
          [self performBlock:block];
     } else {
          dispatch_group_enter(group);
          [self performBlock:^(){
               block();
               dispatch_group_leave(group);
          }];
     }
}

//NSURLConnection也可以这样做
+ (void)withGroup:(dispatch_group_t)group
     sendAsynchronousRequest:(NSURLRequest *)request
     queue:(NSOperationQueue *)queue
     completionHandler:(void (^)(NSURLResponse*, NSData*, NSError*))handler
{
     if (group == NULL) {
          [self sendAsynchronousRequest:request
               queue:queue
               completionHandler:handler];
     } else {
          dispatch_group_enter(group);
          [self sendAsynchronousRequest:request
                    queue:queue
                    completionHandler:^(NSURLResponse *response, NSData *data, NSError *error){
               handler(response, data, error);
               dispatch_group_leave(group);
          }];
     }
}

注意事项

  • dispatch_group_enter() 必须运行在 dispatch_group_leave() 之前。
  • dispatch_group_enter() 和 dispatch_group_leave() 需要成对出现的

用GCD监视进程

NSRunningApplication *mail = [NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.apple.mail"];
if (mail == nil) {
     return;
}
pid_t const pid = mail.processIdentifier;
self.source = dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC, pid, DISPATCH_PROC_EXIT, DISPATCH_TARGET_QUEUE_DEFAULT);
dispatch_source_set_event_handler(self.source, ^(){
     NSLog(@"Mail quit.");
});
//在事件源传到你的事件处理前需要调用dispatch_resume()这个方法
dispatch_resume(self.source);

监视文件夹内文件变化

NSURL *directoryURL; // assume this is set to a directory
int const fd = open([[directoryURL path] fileSystemRepresentation], O_EVTONLY);
if (fd < 0) {
     char buffer[80];
     strerror_r(errno, buffer, sizeof(buffer));
     NSLog(@"Unable to open "%@": %s (%d)", [directoryURL path], buffer, errno);
     return;
}
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, fd,
DISPATCH_VNODE_WRITE | DISPATCH_VNODE_DELETE, DISPATCH_TARGET_QUEUE_DEFAULT);
dispatch_source_set_event_handler(source, ^(){
     unsigned long const data = dispatch_source_get_data(source);
     if (data & DISPATCH_VNODE_WRITE) {
          NSLog(@"The directory changed.");
     }
     if (data & DISPATCH_VNODE_DELETE) {
          NSLog(@"The directory has been deleted.");
     }
});
dispatch_source_set_cancel_handler(source, ^(){
     close(fd);
});
self.source = source;
dispatch_resume(self.source);
//还要注意需要用DISPATCH_VNODE_DELETE 去检查监视的文件或文件夹是否被删除,如果删除了就停止监听

GCD版定时器

dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,0, 0, DISPATCH_TARGET_QUEUE_DEFAULT);
dispatch_source_set_event_handler(source, ^(){
     NSLog(@"Time flies.");
});
dispatch_time_t start
dispatch_source_set_timer(source, DISPATCH_TIME_NOW, 5ull * NSEC_PER_SEC,100ull * NSEC_PER_MSEC);
self.source = source;
dispatch_resume(self.source);

GCD深入操作

  • 缓冲区:dispatch_data_t基于零碎的内存区域,使用dispatch_data_apply来遍历,还可以用dispatch_data_create_subrange来创建一个不做任何拷贝的子区域
  • I/O调度:使用GCD提供的dispatch_io_read,dispatch_io_write和dispatch_io_close
  • 测试:使用dispatch_benchmark小工具
  • 原子操作: libkern/OSAtomic.h里可以查看那些函数,用于底层多线程编程。

Operation Queues

  • Operation Queue是在GCD上实现了一些方便的功能。
  • NSOperationQueue有主队列和自定义队列两种类型队列。主队列在主线程上运行,自定义队列在后台。
  • 重写main方法自定义自己的operations。较简单,不需要管理isExecuting和isFinished,main返回时operation就结束了。
@implementation YourOperation
     - (void)main
     {
          // 进行处理 ...
     }
@end
  • 重写start方法能够获得更多的控制权,还可以在一个操作中执行异步任务
@implementation YourOperation
     - (void)start
     {
          self.isExecuting = YES;
          self.isFinished = NO;
          // 开始处理,在结束时应该调用 finished ...
     }

     - (void)finished
     {
          self.isExecuting = NO;
          self.isFinished = YES;
     }
@end
//使操作队列有取消功能,需要不断检查isCancelled属性
- (void)main
{
     while (notDone && !self.isCancelled) {
          // 进行处理
     }
}
  • 定义好operation类以后,将一个operation加到队列里:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
YourOperation *operation = [[YourOperation alloc] init];
[queue addOperation:operation];
  • 如果是在主队列中进行一个一次性任务,可以将block加到操作队列
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
     // 代码...
}];
  • 通过maxConcurrentOperationCount属性控制一个特定队列中并发执行操作的数量。设置为1就是串行队列。
  • 对operation优先级排序,指定operation之间的依赖关系。
//确保operation1和operation2是在intermediateOperation和finishOperation之前执行
[intermediateOperation addDependency:operation1];
[intermediateOperation addDependency:operation2];
[finishedOperation addDependency:intermediateOperation];

Run Loops

  • Run loop比GCD和操作队列要容易,不必处理并发中复杂情况就能异步执行。
  • 主线程配置main run loop,其它线程默认都没有配置run loop。一般都在主线程中调用后分配给其它队列。如果要在其它线程添加run loop至少添加一个input source,不然一运行就会退出。

在后台操作UI

使用操作队列处理

//weak引用参照self避免循环引用,及block持有self,operationQueue retain了block,而self有retain了operationQueue。
__weak id weakSelf = self;
[self.operationQueue addOperationWithBlock:^{
     NSNumber* result = findLargestMersennePrime();
     [[NSOperationQueue mainQueue] addOperationWithBlock:^{
          MyClass* strongSelf = weakSelf;
          strongSelf.textLabel.text = [result stringValue];
     }];
}];

drawRect在后台绘制

drawRect:方法会影响性能,所以可以放到后台执行。

//使用UIGraphicsBeginImageContextWithOptions取代UIGraphicsGetCurrentContext:方法
UIGraphicsBeginImageContextWithOptions(size, NO, 0);
// drawing code here
UIImage *i = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return i;

可以把这个方法运用到table view中,使table view的cell在滚出边界时能在didEndDisplayingCell委托方法中取消。WWDC中有讲解:Session 211 -- Building Concurrent User Interfaces on iOS https://developer.apple.com/videos/wwdc/2012/

还有个使用CALayer里drawsAsynchronously属性的方法。不过有时work,有时不一定。

网络异步请求

网络都要使用异步方式,但是不要直接使用dispatch_async,这样没法取消这个网络请求。dataWithContentsOfURL:的超时是30秒,那么这个线程需要干等到超时完。解决办法就是使用NSURLConnection的异步方法,把所有操作转化成operation来执行。NSURLConnection是通过run loop来发送事件的。AFNetworking是建立一个独立的线程设置一个非main run loop。下面是处理URL连接重写自定义operation子类里的start方法

- (void)start
{
     NSURLRequest* request = [NSURLRequest requestWithURL:self.url];
     self.isExecuting = YES;
     self.isFinished = NO;
     [[NSOperationQueue mainQueue] addOperationWithBlock:^
     {
          self.connection = [NSURLConnectionconnectionWithRequest:request
               delegate:self];
     }];
}

重写start方法需要管理isExecuting和isFinished状态。下面是取消操作的方法

- (void)cancel
{
     [super cancel];
     [self.connection cancel];
     self.isFinished = YES;
     self.isExecuting = NO;
}
//连接完成发送回调
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
     self.data = self.buffer;
     self.buffer = nil;
     self.isExecuting = NO;
     self.isFinished = YES;
}

后台处理I/O

异步处理文件可以使用NSInputStream。官方文档:http://developer.apple.com/library/ios/#documentation/FileManagement/Conceptual/FileSystemProgrammingGUide/TechniquesforReadingandWritingCustomFiles/TechniquesforReadingandWritingCustomFiles.html 实例:https://github.com/objcio/issue-2-background-file-io

@interface Reader : NSObject
- (void)enumerateLines:(void (^)(NSString*))block
     completion:(void (^)())completion;
- (id)initWithFileAtPath:(NSString*)path;

//采用main run loop的事件将数据发到后台操作线程去处理
- (void)enumerateLines:(void (^)(NSString*))block
completion:(void (^)())completion
{
     if (self.queue == nil) {
          self.queue = [[NSOperationQueue alloc] init];
          self.queue.maxConcurrentOperationCount = 1;
     }
     self.callback = block;
     self.completion = completion;
     self.inputStream = [NSInputStream inputStreamWithURL:self.fileURL];
     self.inputStream.delegate = self;
     [self.inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
     forMode:NSDefaultRunLoopMode];
          [self.inputStream open];
     }
@end

//input stream在主线程中发送代理消息,接着就可以在操作队列加入block操作
- (void)stream:(NSStream*)stream handleEvent:(NSStreamEvent)eventCode
{
     switch (eventCode) {
          ...
          case NSStreamEventHasBytesAvailable: {
               NSMutableData *buffer = [NSMutableData dataWithLength:4 * 1024];
               NSUInteger length = [self.inputStream read:[buffer mutableBytes]
                    maxLength:[buffer length]];
               if (0 < length) {
                    [buffer setLength:length];
                    __weak id weakSelf = self;
                    [self.queue addOperationWithBlock:^{
                         [weakSelf processDataChunk:buffer];
                    }];
               }
               break;
          }
          ...
     }
}

//处理数据chunk,原理就是把数据切成很多小块,然后不断更新和处理buffer缓冲区,逐块读取和存入方式来处理大文件响应快而且内存开销也小。
- (void)processDataChunk:(NSMutableData *)buffer;
{
     if (self.remainder != nil) {
          [self.remainder appendData:buffer];
     } else {
          self.remainder = buffer;
     }
     [self.remainder obj_enumerateComponentsSeparatedBy:self.delimiter
               usingBlock:^(NSData* component, BOOL last) {
          if (!last) {
               [self emitLineWithData:component];
          } else if (0 < [component length]) {
               self.remainder = [component mutableCopy];
          } else {
               self.remainder = nil;
          }
     }];
}

并发开发会遇到的困难问题

多个线程访问共享资源

比如两个线程都会把计算结果写到一个整型数中。为了防止,需要一种互斥机制来访问共享资源

互斥锁

同一时刻只能有一个线程访问某个资源。某线程要访问某个共享资源先获得共享资源的互斥锁,完成操作再释放这个互斥锁,然后其它线程就能访问这个共享资源。

还有需要解决无序执行问题,这时就需要引入内存屏障。

在Objective-C中如果属性声明为atomic就能够支持互斥锁,但是因为加解锁会有性能代价,所以一般是声明noatomic的。

死锁

当多个线程在相互等待对方锁结束时就会发生死锁,程序可能会卡住。

void swap(A, B)
{
     lock(lockA);
     lock(lockB);
     int a = A;
     int b = B;
     A = b;
     B = a;
     unlock(lockB);
     unlock(lockA);
}
//一般没问题,但是如果两个线程使用相反的值同时调用上面这个方法就可能会死锁。线程1获得X的一个锁,线程2获得Y的一个锁,它们会同时等待另一个锁的释放,但是却是没法等到的。
swap(X, Y); // 线程 1
swap(Y, X); // 线程 2

为了防止死锁,需要使用比简单读写锁更好的办法,比如write preferencehttp://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock,或read-copy-update算法http://en.wikipedia.org/wiki/Read-copy-update

优先级反转

运行时低优先级任务由于先取得了释放了锁的共享资源而阻塞了高优先级任务,这种情况叫做优先级反转

最佳安全实践避免问题的方法

从主线程中取到数据,利用一个操作队列在后台处理数据,完后返回后台队列中得到的数据到主队列中。这样的操作不会有任何锁操作。

并发测试

[文章]Auto Layout笔记

基础

原理

视图显示前会有两个步骤,顺序是updating constraints -> laying out views -> 显示。

  • Updating constraints:从子视图到父视图,布局会在实际设置frame时使用,调用setNeedsUpdateConstraints触发操作。自定义视图的话可以重写updateConstraints增加本地约束。
  • Laying out views:布局视图是从父视图到子视图,通过setNeedsLayout触发。调用layoutIfNeeded可以强制系统立刻更新视图布局。

自定义视图自动布局的过程

Instrinsic Content Size

实现Instrinsic Content Size需要重写intrinsicContentSize返回合适的大小,有会影响尺寸改变的时候调用invalidateInstrinsicContentSize。一个方向设置Instrinsic Content Size,另一个方向尺寸返回UIViewNoIntrinsicMetric

Compression Resistance 和 Content Hugging

定义了Instrinsic Content Size 才能够在视图两个方向上分配 Compression Resistance 和 Content Hugging 。比如一个Instrinsic Content Size为{100,30}的label,Compression Resistance为750,Content Hugging为250,约束条件可视格式语言如下

H:[label(<=100@250)]
H:[label(>=100@750)]
V:[label(<=30@250)]
V:[label(>=30@750)]

Frame和Alignment Rect

如果需要可以重写alignmentRectForFrame:和frameForAlignmentRect:,Instrinsic Content Size尺寸引用它的alignment rect而不是frame

Baseline Alignment

通过viewForBaselineLayout来激活基线对齐。

控制布局

  • 本地约束:添加本地约束的地方是updateConstraints。增加布局子视图约束条件后调用[super updateConstraints]。
  • 控制子视图布局:如果不能利用布局约束条件达到子视图预期布局可以重写layoutSubviews。可以参看WWDC视频的一个例子WWDC session 228 – Best Practices for Mastering Auto Layout http://onevcat.com/2012/09/autoayout/
- layoutSubviews
{
     [super layoutSubviews];
     if (self.subviews[0].frame.size.width <= MINIMUM_WIDTH)
     {
          [self removeSubviewConstraints];
          self.layoutRows += 1; [super layoutSubviews];
     }
}

- updateConstraints
{
     // 根据 self.layoutRows 添加约束...
     [super updateConstraints];
}

对于不固定高度的多行文本处理

比如说UILabel和NSTextField文本的高度取决于行的宽度,这两个类有个perferredMaxLayoutWidth的属性,可以指定行宽度的最大值,以便计算固有内容尺寸。

- (void)layoutSubviews
{
     //第一次调用获得label的frame
     [super layoutSubviews];
     myLabel.preferredMaxLayoutWidth = myLabel.frame.size.width;
     //第二次调用为了改变后更新布局
     [super layoutSubviews];
}

//也可以在label子类本身这样做
@implementation MyLabel
- (void)layoutSubviews
{
     self.preferredMaxLayoutWidth = self.frame.size.width;
     [super layoutSubviews];
}
@end


- (void)viewDidLayoutSubviews
{
     [super viewDidLayoutSubviews];
     myLabel.preferredMaxLayoutWidth = myLabel.frame.size.width;
     [self.view layoutIfNeeded];
}

动画

  • 使用Core Animation方法
//非Auto Layout的写法
[UIView animateWithDuration:1 animations:^{
     myView.frame = newFrame;
}];
// 更新约束,Auto Layout的写法,主要不要更改view的frame,因为view使用了Auto Layout后frame的设置任务已经由布局系统代劳了。
[UIView animateWithDuration:1 animations:^{
     [myView layoutIfNeeded];
}];
  • 使用transform来产生动画,将这个view嵌入到一个view的容器内,然后在这个容器内重写layoutSubviews
- (void)layoutSubviews
{
     [super layoutSubviews];
     static CGPoint center = {0,0};
     if (CGPointEqualToPoint(center, CGPointZero)) {
          // 在初次布局后获取中心点
          center = self.animatedView.center;
     } else {
          // 将中心点赋回给动画视图
          self.animatedView.center = center;
     }
}

调试

不可满足的约束条件

遇到不可满足的约束条件只能在输入的日志中看到视图的内存地址。

(lldb) po 0x7731880
$0 = 124983424 <UIView: 0x7731880; frame = (90 -50; 80 100);
layer = <CALayer: 0x7731450>>

(lldb) po [0x7731880 superview]
$2 = 0x07730fe0 <UIView: 0x7730fe0; frame = (32 128; 259 604);
layer = <CALayer: 0x7731150>>

(lldb) po [[0x7731880 superview] recursiveDescription]
$3 = 0x07117ac0 <UIView: 0x7730fe0; frame = (32 128; 259 604); layer = <CALayer: 0x7731150>>
| <UIView: 0x7731880; frame = (90 -50; 80 100); layer = <CALayer: 0x7731450>>
| <UIView: 0x7731aa0; frame = (90 101; 80 100); layer = <CALayer: 0x7731c60>>

可以在控制台修改有问题的视图

(lldb) expr ((UIView *)0x7731880).backgroundColor = [UIColor purpleColor]

这里有Danielhttps://twitter.com/danielboedewadt的一个调试Auto Layout的范例:https://github.com/objcio/issue-3-auto-layout-debugging

有歧义的布局

UIView提供三种方法:hasAmbiguousLayout,exerciseAmbiguityInLayout和_autolayoutTrace(私有方法,正式产品里不要包含)。如果有歧义那么hasAmbiguousLayout返回YES。

@implementation UIView (AutoLayoutDebugging)
- (void)printAutoLayoutTrace {
     #ifdef DEBUG
     NSLog(@"%@", [self performSelector:@selector(_autolayoutTrace)]);
     #endif
}
@end

_autolayoutTrace打印如下:

2013-07-23 17:36:08.920 FlexibleLayout[4237:907]
*<UIWindow:0x7269010>
| *<UILayoutContainerView:0x7381250>
| | *<UITransitionView:0x737c4d0>
| | | *<UIViewControllerWrapperView:0x7271e20>
| | | | *<UIView:0x7267c70>
| | | | | *<UIView:0x7270420> - AMBIGUOUS LAYOUT
| | <UITabBar:0x726d440>
| | | <_UITabBarBackgroundView:0x7272530>
| | | <UITabBarButton:0x726e880>
| | | | <UITabBarSwappableImageView:0x7270da0>
| | | | <UITabBarButtonLabel:0x726dcb0>

使用exerciseAmbiguityInLayout

@implementation UIView (AutoLayoutDebugging)
- (void)exerciseAmiguityInLayoutRepeatedly:(BOOL)recursive {
     #ifdef DEBUG
     if (self.hasAmbiguousLayout) {
          [NSTimer scheduledTimerWithTimeInterval:.5
               target:self
               selector:@selector(exerciseAmbiguityInLayout)
               userInfo:nil
               repeats:YES];
     }
     if (recursive) {
          for (UIView *subview in self.subviews) {
               [subview exerciseAmbiguityInLayoutRepeatedly:YES];
          }
     }
     #endif
} @end

约束条件代码

UIView *superview = theSuperView;
NSDictionary *views = NSDictionaryOfVariableBindings(superview, subview);
NSArray *c = [NSLayoutConstraint
     constraintsWithVisualFormat:@"V:[superview]-(<=1)-[subview]"]
     options:NSLayoutFormatAlignAllCenterX
     metrics:nil
     views:views];
[superview addConstraints:c];
  • 垂直的排列一系列view,想要它们垂直方向间距一致,水平方向上所有view以他们的左边缘对齐
@implementation UIView (AutoLayoutHelpers)
+ leftAlignAndVerticallySpaceOutViews:(NSArray *)views
     distance:(CGFloat)distance
{
     for (NSUInteger i = 1; i < views.count; i++) {
          UIView *firstView = views[i - 1];
          UIView *secondView = views[i];
          firstView.translatesAutoResizingMaskIntoConstraints = NO;
          secondView.translatesAutoResizingMaskIntoConstraints = NO;

          NSLayoutConstraint *c1 = constraintWithItem:firstView
               attribute:NSLayoutAttributeBottom
               relatedBy:NSLayoutRelationEqual
               toItem:secondView
               attribute:NSLayoutAttributeTop
               multiplier:1
               constant:distance];

          NSLayoutConstraint *c2 = constraintWithItem:firstView
               attribute:NSLayoutAttributeLeading
               relatedBy:NSLayoutRelationEqual
               toItem:secondView
               attribute:NSLayoutAttributeLeading
               multiplier:1
               constant:0];

          [firstView.superview addConstraints:@[c1, c2]];
     }
}
@end

[文章]UICollectionView笔记,包括如何自定义布局

Layout Objects

使用UICollectionViewFlowLayout类进行具体的布局实现,类的官方文档说明:https://developer.apple.com/library/ios/documentation/uikit/reference/UICollectionViewFlowLayout_class/Reference/Reference.html#//apple_ref/occ/cl/UICollectionViewFlowLayout,进一步定制可参考官方这个文档:https://developer.apple.com/library/ios/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/UsingtheFlowLayout/UsingtheFlowLayout.html#//apple_ref/doc/uid/TP40012334-CH3-SW4

Cells和其它View

collection view管理者cells,supplementary views和decoration views

  • cells:和tableview的cell类似。
  • Supplementary views:相当于tableview的section header和footer views,不同的是数量和位置由布局控制
  • Decoration views:装饰用

基于内容的布局计算自定义布局

子类重写的方法可以参考官方文档:https://developer.apple.com/library/ios/documentation/uikit/reference/UICollectionViewLayout_class/Reference/Reference.html

collectionViewContentSize

- (CGSize)collectionViewContentSize
{
     //不能水平滚动
     CGFloat contentWidth = self.collectionView.bounds.size.width;

     //垂直滚动显示完整数据
     CGFloat contentHeight = DayHeaderHeight + (HeightPerHour * HoursPerDay);

     CGSize contentSize = CGSizeMake(contentWidth, contentHeight);
     return contentSize;
}

布局中最重要的方法layoutAttributesForElementsInRect:

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
     NSMutableArray *layoutAttributes = [NSMutableArray array];
     // Cells
     // We call a custom helper method -indexPathsOfItemsInRect: here
     // which computes the index paths of the cells that should be included
     // in rect.
     NSArray *visibleIndexPaths = [self indexPathsOfItemsInRect:rect];
     for (NSIndexPath *indexPath in visibleIndexPaths) {
          UICollectionViewLayoutAttributes *attributes =
          [self layoutAttributesForItemAtIndexPath:indexPath];
          [layoutAttributes addObject:attributes];
     }

     // Supplementary views
     NSArray *dayHeaderViewIndexPaths = [self indexPathsOfDayHeaderViewsInRect:rect];
     for (NSIndexPath *indexPath in dayHeaderViewIndexPaths) {
          UICollectionViewLayoutAttributes *attributes =
          [self layoutAttributesForSupplementaryViewOfKind:@“DayHeaderView" atIndexPath:indexPath];
     [layoutAttributes addObject:attributes];
     }

     NSArray *hourHeaderViewIndexPaths = [self indexPathsOfHourHeaderViewsInRect:rect];
     for (NSIndexPath *indexPath in hourHeaderViewIndexPaths) {
          UICollectionViewLayoutAttributes *attributes =
          [self layoutAttributesForSupplementaryViewOfKind:@"HourHeaderView"
atIndexPath:indexPath];
          [layoutAttributes addObject:attributes];
     }
     return layoutAttributes;
}

layoutAttributesForItemAtIndexPath:

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
     CalendarDataSource *dataSource = self.collectionView.dataSource;
     id event = [dataSource eventAtIndexPath:indexPath];
     UICollectionViewLayoutAttributes *attributes =
     [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
     attributes.frame = [self frameForEvent:event];
     return attributes;
}

shouldInvalidateLayoutForBoundsChange:

- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
     CGRect oldBounds = self.collectionView.bounds;
     if (CGRectGetWidth(newBounds) != CGRectGetWidth(oldBounds)) {
          return YES;
     }
     return NO;
}

插入,删除和布局间切换的动画

插入,删除

  • initialLayoutAttributesForAppearingItemAtIndexPath:
  • initialLayoutAttributesForAppearingSupplementaryElementOfKind:atIndexPath:
  • initialLayoutAttributesForAppearingDecorationElementOfKind:atIndexPath:
  • finalLayoutAttributesForDisappearingItemAtIndexPath:
  • finalLayoutAttributesForDisappearingSupplementaryElementOfKind:atIndexPath:
  • finalLayoutAttributesForDisappearingDecorationElementOfKind:atIndexPath: 发送setCollectionViewLayout:animated:消息,collection view会为cells在新布局参数,动态将每个cell从旧变到新。

范例

[文章]Core Data笔记,包含了如何让UITableView,UICollectionView和CoreData完美结合进行增删改操作,如何导入大量数据,如何利用NSEntityMigrationPolicy进行跨多版本的数据迁移和最后如何进行性能测试

包含组件

最底层File System -> SQLite -> NSPersistent Store(可有多个) -> NSPersistent StoreCoordinator -> NSManagedObjectContext(可有多个,每个可包含多个NSManagedObject)

设置堆栈

范例:https://github.com/objcio/issue-4-full-core-data-application

- (void)setupManagedObjectContext
{
     //使用initWithConcurrencyType:来明确使用的是基于队列的并发模型
     self.managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
     self.managedObjectContext.persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.managedObjectModel];
     NSError* error;
     [self.managedObjectContext.persistentStoreCoordinator
          addPersistentStoreWithType:NSSQLiteStoreType
          configuration:nil
          URL:self.storeURL
          options:nil
          error:&error];
     if (error) {
          NSLog(@"error: %@", error);
     }
     self.managedObjectContext.undoManager = [[NSUndoManager alloc] init];
}

创建模型

在xcode新建的Core Data选项中选择Data Model template,模型文件会被编译成.momd文件。模型创建完毕就可以创建与之对应的NSManagedObject子类。从菜单选择Editor > NSManagedObject subclass。

模型的属性

  • 默认/可选:建议不使用带默认值的可选属性
  • Transient:方便撤销操作和故障处理,建议使用transient属性
  • 索引:提高读取速度
  • 标量类型:默认NSNumber,也可以使用int64_t,float_t或BOOL。

创建Store类

存储类除了managed object context还有rootItem方法,程序启动时会查找这root item然后传给root view controller。

- (Item*)rootItem
{
     NSFetchRequest* request = [NSFetchRequest fetchRequestWithEntityName:@"Item"];
     request.predicate = [NSPredicate predicateWithFormat:@"parent = %@", nil];
     NSArray* objects = [self.managedObjectContext executeFetchRequest:request error:NULL];
     Item* rootItem = [objects lastObject];
     if (rootItem == nil) {
          rootItem = [Item insertItemWithTitle:nil
               parent:nil
               inManagedObjectContext:self.managedObjectContext];
     }
     return rootItem;
}

//增加一个item
+ (instancetype)insertItemWithTitle:(NSString*)title
     parent:(Item*)parent
     inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext
{
     NSUInteger order = parent.numberOfChildren;
     Item* item = [NSEntityDescription insertNewObjectForEntityForName:self.entityName
          inManagedObjectContext:managedObjectContext];
     item.title = title;
     item.parent = parent;
     item.order = @(order);
     return item;
}

//获得子节点数量
- (NSUInteger)numberOfChildren
{
     return self.children.count;
}

//创建一个fetched results controller的方法方便自动更新table view
- (NSFetchedResultsController*)childrenFetchedResultsController
{
     NSFetchRequest* request = [NSFetchRequest fetchRequestWithEntityName:[self.class entityName]];
     request.predicate = [NSPredicate predicateWithFormat:@"parent = %@", self];
     request.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"order" ascending:YES]];
     return [[NSFetchedResultsController alloc] initWithFetchRequest:request
          managedObjectContext:self.managedObjectContext
          sectionNameKeyPath:nil
          cacheName:nil];
}

和Table View无缝结合

创建一个NSFetchedResultsController作为table view的data source

- (id)initWithTableView:(UITableView*)tableView
{
     self = [super init];
     if (self) {
          self.tableView = tableView;
          self.tableView.dataSource = self;
     }
     return self;
}

- (void)setFetchedResultsController:(NSFetchedResultsController*)fetchedResultsController
{
     _fetchedResultsController = fetchedResultsController;
     fetchedResultsController.delegate = self;
     [fetchedResultsController performFetch:NULL];
}

- (NSInteger)numberOfSectionsInTableView:(UITableView*)tableView
{
     return self.fetchedResultsController.sections.count;
}

- (NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)sectionIndex
{
     id<NSFetchedResultsSectionInfo> section = self.fetchedResultsController.sections[sectionIndex];
     return section.numberOfObjects;
}

- (UITableViewCell*)tableView:(UITableView*)tableView
cellForRowAtIndexPath:(NSIndexPath*)indexPath
{
     id object = [self.fetchedResultsController objectAtIndexPath:indexPath];
     id cell = [tableView dequeueReusableCellWithIdentifier:self.reuseIdentifier
          forIndexPath:indexPath];
     [self.delegate configureCell:cell withObject:object];
     return cell;
}

创建Table View Controller

在新建的Table view的viewDidLoad里写:

fetchedResultsControllerDataSource = [[FetchedResultsControllerDataSource alloc] initWithTableView:self.tableView];
self.fetchedResultsControllerDataSource.fetchedResultsController = self.parent.childrenFetchedResultsController;
fetchedResultsControllerDataSource.delegate = self;
fetchedResultsControllerDataSource.reuseIdentifier = @"Cell";

实现delegate

- (void)configureCell:(id)theCell withObject:(id)object
{
     UITableViewCell* cell = theCell;
     Item* item = object;
     cell.textLabel.text = item.title;
}

添加

在textFieldShouldReturn:里

[Item insertItemWithTitle:title
          parent:self.parent
          inManagedObjectContext:self.parent.managedObjectContext];
     textField.text = @"";
     [textField resignFirstResponder];

增删改后table view也会更改显示

- (void)controller:(NSFetchedResultsController*)controller
     didChangeObject:(id)anObject
     atIndexPath:(NSIndexPath*)indexPath
     forChangeType:(NSFetchedResultsChangeType)type
     newIndexPath:(NSIndexPath*)newIndexPath
{
     if (type == NSFetchedResultsChangeInsert) {
          [self.tableView insertRowsAtIndexPaths:@[newIndexPath]
               withRowAnimation:UITableViewRowAnimationAutomatic];
     }
}

- (void)controllerWillChangeContent:(NSFetchedResultsController*)controller
{
     [self.tableView beginUpdates];
}

- (void)controllerDidChangeContent:(NSFetchedResultsController*)controller
{
     [self.tableView endUpdates];
}

和Collection View的结合

范例:https://github.com/AshFurrow/UICollectionView-NSFetchedResultsControllercollection view没有beginUpdates和endUpdates方法,所以只能用performBatchUpdate方法收集所有更新,然后在controllerDidChangeContent中用block执行所有更新。

如何传递Table view里的Model对象到新的view controller中

- (void)prepareForSegue:(UIStoryboardSegue*)segue sender:(id)sender
{
     [super prepareForSegue:segue sender:sender];
     if ([segue.identifier isEqualToString:selectItemSegue]) {
          [self presentSubItemViewController:segue.destinationViewController];
     }
}

- (void)presentSubItemViewController:(ItemViewController*)subItemViewController
{
     Item* item = [self.fetchedResultsControllerDataSource selectedItem];
     subItemViewController.parent = item;
}

- (void)viewWillAppear:(BOOL)animated
{
     [super viewWillAppear:animated];
     self.fetchedResultsControllerDataSource.paused = NO;
}

- (void)viewWillDisappear:(BOOL)animated
{
     [super viewWillDisappear:animated];
     self.fetchedResultsControllerDataSource.paused = YES;
}

- (void)setPaused:(BOOL)paused
{
     _paused = paused;
     if (paused) {
          self.fetchedResultsController.delegate = nil;
     } else {
          self.fetchedResultsController.delegate = self;
          [self.fetchedResultsController performFetch:NULL];
          [self.tableView reloadData];
     }
}

删除

//让table view支持滑动删除
- (BOOL)tableView:(UITableView*)tableView
canEditRowAtIndexPath:(NSIndexPath*)indexPath
{
     return YES;
}

- (void)tableView:(UITableView *)tableView
     commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
     forRowAtIndexPath:(NSIndexPath *)indexPath {
     if (editingStyle == UITableViewCellEditingStyleDelete) {
          id object = [self.fetchedResultsController objectAtIndexPath:indexPath];
          [self.delegate deleteObject:object];
     }
}

//强制order变化,可以重写prepareForDeletion方法
- (void)prepareForDeletion
{
     NSSet* siblings = self.parent.children;
     NSPredicate* predicate = [NSPredicate predicateWithFormat:@"order > %@", self.order];
     NSSet* siblingsAfterSelf = [siblings filteredSetUsingPredicate:predicate];
     [siblingsAfterSelf enumerateObjectsUsingBlock:^(Item* sibling, BOOL* stop)
     {
          sibling.order = @(sibling.order.integerValue - 1);
     }];
}

增加删除的动画效果

...
else if (type == NSFetchedResultsChangeDelete) {
     [self.tableView deleteRowsAtIndexPaths:@[indexPath]
          withRowAnimation:UITableViewRowAnimationAutomatic];
}

增加晃动撤销功能

//第一步告诉application支持这个
application.applicationSupportsShakeToEdit = YES;
//重写UIResponder类中的两个方法
- (BOOL)canBecomeFirstResponder {
     return YES;
}

- (NSUndoManager*)undoManager
{
     return self.managedObjectContext.undoManager;
}

//在持续化stack中设置一个undo manager
self.managedObjectContext.undoManager = [[NSUndoManager alloc] init];

//实现上面几步后晃动时会得到两个按钮的提醒框,可以给让用户体验更加友好些
NSString* title = textField.text;
NSString* actionName = [NSString stringWithFormat:NSLocalizedString(@"add item "%@"", @"Undo action name of add item"), title];
[self.undoManager setActionName:actionName];
[self.store addItem:title parent:nil];

排序

可以参考官方文档:https://developer.apple.com/library/ios/documentation/CoreData/Reference/NSFetchedResultsControllerDelegate_Protocol/Reference/Reference.html#//apple_ref/doc/uid/TP40008228-CH1-SW14

保存

Fetch获取对象

基础

官方文档:https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CoreData/Articles/cdFetching.html

范例

request.result = [NSPredicate predicateWithFormat:
     @"(%@ <= longitude) AND (longitude <= %@)"
     @"AND (%@ <= latitude) AND (latitude <= %@)",
     @(minLongitude), @(maxLongitude), @(minLatitude), @(maxLatitude)];
//取消将值放到row cache中。
request.returnsObjectsAsFaults = NO;
request.fetchLimit = 200;
//执行fetch
NSError *error = nil;
NSArray *stops = [moc executeFetchRequest:request error:&error];
NSAssert(stops != nil, @"Failed to execute %@: %@", request, error);
//二次遍历
NSPredicate *exactPredicate = [self exactLatitudeAndLongitudePredicateForCoordinate:self.location.coordinate];
stops = [stops filteredArrayUsingPredicate:exactPredicate];

- (NSPredicate *)exactLatitudeAndLongitudePredicateForCoordinate:(CLLocationCoordinate2D)pointOfInterest;
{
     return [NSPredicate predicateWithBlock:^BOOL(Stop *evaluatedStop, NSDictionary *bindings) {
          CLLocation *evaluatedLocation = [[CLLocation alloc] initWithLatitude:evaluatedStop.latitude           longitude:evaluatedStop.longitude];
          CLLocationDistance distance = [self.location distanceFromLocation:evaluatedLocation];
          return (distance < self.distance);
     }];
}

//子查询
NSPredicate *timePredicate = [NSPredicate predicateWithFormat:@"(%@ <= departureTime) && (departureTime <= %@)”, startDate, endDate];

NSPredicate *predicate = [NSPredicate predicateWithFormat:
     @"(SUBQUERY(stopTimes, $x, (%@ <= $x.departureTime) && ($x.departureTime <= %@)).@count != 0)”, startDate, endDate];

//文本搜索
NSString *searchString = @"U Görli";
predicate = [NSPredicate predicateWithFormat:@"name BEGINSWITH %@", searchString];

导入大量数据

导入应用Bundle里的SQLite文件

NSFileManager* fileManager = [NSFileManager defaultManager];
NSError *error;

if([fileManager fileExistsAtPath:self.storeURL.path]) {
     NSURL *storeDirectory = [self.storeURL URLByDeletingLastPathComponent];
     NSDirectoryEnumerator *enumerator = [fileManager enumeratorAtURL:storeDirectory
          includingPropertiesForKeys:nil
          options:0
          errorHandler:NULL];
     NSString *storeName = [self.storeURL.lastPathComponent stringByDeletingPathExtension];
     //遍历目录下是否有重复
     for (NSURL *url in enumerator) {
          if (![url.lastPathComponent hasPrefix:storeName]) continue;
          [fileManager removeItemAtURL:url error:&error];
     }
      // 处理错误
}

NSString* bundleDbPath = [[NSBundle mainBundle] pathForResource:@"seed" ofType:@"sqlite"];
[fileManager copyItemAtPath:bundleDbPath toPath:self.storeURL.path error:&error];

//真机删除会失效,所以使用版本号来进行区分新旧
NSString* bundleVersion = [infoDictionary objectForKey:(NSString *)kCFBundleVersionKey];
NSString *seedVersion = [[NSUserDefaults standardUserDefaults] objectForKey@"SeedVersion"];
if (![seedVersion isEqualToString:bundleVersion]) {
     // 复制源数据库
}

// ... 导入成功后
NSDictionary *infoDictionary = [NSBundle mainBundle].infoDictionary;
[[NSUserDefaults standardUserDefaults] setObject:bundleVersion forKey:@"SeedVersion"];

导入范例

https://github.com/objcio/issue-4-importing-and-fetching

版本迁移

Mapping Models

NSMigrationManager能够推断两个版本模型的映射关系,但是如果版本跨度大了就力不从心了。

Progressive Migrations渐进式迁移

实现原理是两个版本之间确保正常,升级时按照一个版本一个版本渐进式的升级方式,比如最新的版本是第四版,如果用户使用的是第二版的,那么升级是就是先从第二版升级到第三版,然后再从第三版升级到第四版。完整范例:https://github.com/objcio/issue-4-core-data-migration 主要代码来自Marcus Zarrahttps://twitter.com/mzarra ,他的书关于Core Data的值得一看,http://pragprog.com/book/mzcd2/core-data

迁移策略

NSEntityMigrationPolicy这个类不光能够修改Entity的属性和关系,还能够自定义一些操作完成每个Entity的迁移。例如在Entity Mapping的Custom Polity里写上自定义的polity的方法

NSNumber *modelVersion = [mapping.userInfo valueForKey:@"modelVersion"];
if (modelVersion.integerValue == 2) {
     NSMutableArray *sourceKeys = [sourceInstance.entity.attributesByName.allKeys mutableCopy];
     NSDictionary *sourceValues = [sourceInstance dictionaryWithValuesForKeys:sourceKeys];
     NSManagedObject *destinationInstance = [NSEntityDescription insertNewObjectForEntityForName:mapping.destinationEntityName
          inManagedObjectContext:manager.destinationContext];
     NSArray *destinationKeys = destinationInstance.entity.attributesByName.allKeys;
     for (NSString *key in destinationKeys) {
          id value = [sourceValues valueForKey:key];
          // 避免value为空
          if (value && ![value isEqual:[NSNull null]]) {
               [destinationInstance setValue:value forKey:key];
          }
     }
}

NSMutableDictionary *authorLookup = [manager lookupWithKey:@"authors"];
// 检查该作者是否已经被创建了
NSString *authorName = [sourceInstance valueForKey:@"author"];
NSManagedObject *author = [authorLookup valueForKey:authorName];
if (!author) {
     // 创建作者
     // ...

     // 更新避免重复
     [authorLookup setValue:author forKey:authorName];
}
[destinationInstance performSelector:@selector(addAuthorsObject:) withObject:author];

//源存储和目的存储之间的关系
[manager associateSourceInstance:sourceInstance
     withDestinationInstance:destinationInstance
     forEntityMapping:mapping];
return YES;

NSmigrationManager的category方法

@implementation NSMigrationManager (Lookup)

- (NSMutableDictionary *)lookupWithKey:(NSString *)lookupKey
{
     NSMutableDictionary *userInfo = (NSMutableDictionary *)self.userInfo;
     // 这里检查一下是否已经建立了 userInfo 的字典
     if (!userInfo) {
          userInfo = [@{} mutableCopy];
          self.userInfo = userInfo;
     }
     NSMutableDictionary *lookup = [userInfo valueForKey:lookupKey];
     if (!lookup) {
          lookup = [@{} mutableCopy];
          [userInfo setValue:lookup forKey:lookupKey];
     }
     return lookup;
}

@end

更复杂的迁移

NSArray *users = [sourceInstance valueForKey:@"users"];
for (NSManagedObject *user in users) {

     NSManagedObject *file = [NSEntityDescription insertNewObjectForEntityForName:@"File"
          inManagedObjectContext:manager.destinationContext];
     [file setValue:[sourceInstance valueForKey:@"fileURL"] forKey:@"fileURL"];
     [file setValue:destinationInstance forKey:@"book"];

     NSInteger userId = [[user valueForKey:@"userId"] integerValue];
     NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"User"];
     request.predicate = [NSPredicate predicateWithFormat:@"userId = %d", userId];
     NSManagedObject *user = [[manager.destinationContext executeFetchRequest:request error:nil] lastObject];
     [file setValue:user forKey:@"user"];
}

数据量大时的迁移改造,利用CoreData提供的chunks数据块方式。官方文档https://developer.apple.com/library/ios/documentation/cocoa/Conceptual/CoreDataVersioning/Articles/vmCustomizing.html#//apple_ref/doc/uid/TP40004399-CH8-SW9

NSArray *mappingModels = @[mappingModel]; // 我们之前建立的那个模型
if ([self.delegate respondsToSelector:@selector(migrationManager:mappingModelsForSourceModel:)]) {
     NSArray *explicitMappingModels = [self.delegate migrationManager:self
          mappingModelsForSourceModel:sourceModel];
     if (0 < explicitMappingModels.count) {
          mappingModels = explicitMappingModels;
     }
}
for (NSMappingModel *mappingModel in mappingModels) {
     didMigrate = [manager migrateStoreFromURL:sourceStoreURL
          type:type
          options:nil
          withMappingModel:mappingModel
          toDestinationURL:destinationStoreURL
          destinationType:type
          destinationOptions:nil
          error:error];
}

- (NSArray *)migrationManager:(MHWMigrationManager *)migrationManager
     mappingModelsForSourceModel:(NSManagedObjectModel *)sourceModel
{
     NSMutableArray *mappingModels = [@[] mutableCopy];
     NSString *modelName = [sourceModel mhw_modelName];
     if ([modelName isEqual:@"Model2"]) {
          // 把该映射模型加入数组
     }
     return mappingModels;
}

- (NSString *)mhw_modelName
{
     NSString *modelName = nil;
     NSArray *modelPaths = // get paths to all the mom files in the bundle
     for (NSString *modelPath in modelPaths) {
          NSURL *modelURL = [NSURL fileURLWithPath:modelPath];
          NSManagedObjectModel *model = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
          if ([model isEqual:self]) {
               modelName = modelURL.lastPathComponent.stringByDeletingPathExtension;
               break;
          }
     }
     return modelName;
}

建立单元测试,

- (void)setUpCoreDataStackMigratingFromStoreWithName:(NSString *)name
{
     NSURL *storeURL = [self temporaryRandomURL];
     [self copyStoreWithName:name toURL:storeURL];

     NSURL *momURL = [[NSBundle mainBundle] URLForResource:@"Model" withExtension:@"momd"];
     self.managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:momURL];

     NSString *storeType = NSSQLiteStoreType;

     MHWMigrationManager *migrationManager = [MHWMigrationManager new];
     [migrationManager progressivelyMigrateURL:storeURL
          ofType:storeType
          toModel:self.managedObjectModel
          error:nil];

     self.persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.managedObjectModel];
     [self.persistentStoreCoordinator addPersistentStoreWithType:storeType
          configuration:nil
          URL:storeURL
          options:nil
          error:nil];

     self.managedObjectContext = [[NSManagedObjectContext alloc] init];
     self.managedObjectContext.persistentStoreCoordinator = self.persistentStoreCoordinator;
}

- (NSURL *)temporaryRandomURL
{
     NSString *uniqueName = [NSProcessInfo processInfo].globallyUniqueString;
     return [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingString:uniqueName]];
}

- (void)copyStoreWithName:(NSString *)name toURL:(NSURL *)url
{
     // 每次创建一个唯一的url以保证测试正常运行
     NSBundle *bundle = [NSBundle bundleForClass:[self class]];
     NSFileManager *fileManager = [NSFileManager new];
     NSString *path = [bundle pathForResource:[name stringByDeletingPathExtension] ofType:name.pathExtension];
     [fileManager copyItemAtPath:path
          toPath:url.path error:nil];
}

//在测试类中复用
- (void)setUp
{
     [super setUp];
     [self setUpCoreDataStackMigratingFromStoreWithName:@"Model1.sqlite"];
}

调试迁移一个有用的启动参数是-com.apple.CoreData.MigrationDebug,设置1就会在console收到迁移数据时会出现的特殊的情况的信息。如果设置-com.apple.CoreData.SQLDebug 为 1还能够在console看到实际操作的SQL语句。

性能测试

  • 加上-com.apple.CoreData.SQLDebug1 作为启动参数传递给应用程序可以得到的输出
sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZIDENTIFIER, t0.ZLATITUDE, t0.ZLONGITUDE, t0.ZNAME FROM ZSTOP t0 WHERE (? <= t0.ZLONGITUDE AND t0.ZLONGITUDE <= ? AND ? <= t0.ZLATITUDE AND t0.ZLATITUDE <= ?) LIMIT 100
annotation: sql connection fetch time: 0.0008s
annotation: total fetch execution time: 0.0013s for 15 rows.

实际生成的SQL是:

SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZIDENTIFIER, t0.ZLATITUDE, t0.ZLONGITUDE, t0.ZNAME FROM ZSTOP t0
WHERE (? <= t0.ZLONGITUDE AND t0.ZLONGITUDE <= ? AND ? <= t0.ZLATITUDE AND t0.ZLATITUDE <= ?)
LIMIT 200
% cd TrafficSearch
% sqlite3 transit-data.sqlite
SQLite version 3.7.13 2012-07-17 17:46:21
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> EXPLAIN QUERY PLAN SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZIDENTIFIER, t0.ZLATITUDE, t0.ZLONGITUDE, t0.ZNAME FROM ZSTOP t0
...> WHERE (13.30845219672199 <= t0.ZLONGITUDE AND t0.ZLONGITUDE <= 13.33441458422844 AND 52.42769566863058 <= t0.ZLATITUDE AND t0.ZLATITUDE <= 52.44352370653525)
...> LIMIT 100;
0|0|0|SEARCH TABLE ZSTOP AS t0 USING INDEX ZSTOP_ZLONGITUDE_INDEX (ZLONGITUDE>? AND ZLONGITUDE<?) (~6944 rows)

输出

0|0|0|SEARCH TABLE ZSTOP AS t0 USING INDEX ZSTOP_ZLONGITUDE_ZLATITUDE (ZLONGITUDE>? AND ZLONGITUDE<?) (~6944 rows)