NSPredicate Notes

在 Core Data 中可以给 NSFetchRequest 指定一个 predicate 来对数据进行过滤以方便查找,比如:

fetchRequest.predicate = [NSPredicate predicateWithFormat:@"id == %@", 123];

NSPredicate 的过滤查询规则不仅仅适用于 Core Data,字符串过滤也很方便。比如:

NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF CONTAINS %@", @"hello"];
BOOL b = [predicate evaluateWithObject:@"hello world"]; // YES

字符串支持的判断语法有 contains beginswith endswith like matches and/or/not/in

NSPredicate *predicate1 = [NSPredicate predicateWithFormat:@"SELF BEGINSWITH %@", @"hello"];
BOOL b = [predicate1 evaluateWithObject:@"hello world"]; // YES
BOOL n = [predicate1 evaluateWithObject:@"nohello world"]; // NO

like 匹配,支持 * 任意字符(可无),? 有且仅有一个字符:

NSPredicate *like = [NSPredicate predicateWithFormat:@"SELF LIKE %@", @"*like?"];
NSLog(@"%d", [like evaluateWithObject:@"alike"]); // 0-NO
NSLog(@"%d", [like evaluateWithObject:@"000liked"]); // 1-YES
NSLog(@"%d", [like evaluateWithObject:@"likes"]); // 1-YES

matches 正则匹配:

NSPredicate *match = [NSPredicate predicateWithFormat:@"SELF MATCHES '\\\\d+[a-z]'"];
NSLog(@"%d", [match evaluateWithObject:@"0A"]); // NO
NSLog(@"%d", [match evaluateWithObject:@"0a"]); // YES
NSLog(@"%d", [match evaluateWithObject:@"000000ab"]); // NO
NSLog(@"%d", [match evaluateWithObject:@"000000c"]); // YES

NSPredicate 可以组合起来用,这也是最为方便的地方,比如下面这个例子:

字符串以 CH 开头,长度大于 3 而小于 20 字符,包含至少一个数字,不包含 broken,不包含空格。

NSPredicate *one = [NSPredicate predicateWithFormat:@"SELF BEGINSWITH 'CH'"];
NSPredicate *two = [NSPredicate predicateWithFormat:@"SELF.length > 3 AND SELF.length < 20"];
NSPredicate *three = [NSPredicate predicateWithFormat:@"SELF MATCHES '.*\\\\d.*'"];
NSPredicate *four = [NSPredicate predicateWithFormat:@"NOT(SELF CONTAINS 'broken') AND NOT(SELF CONTAINS ' ')"];

NSArray *array = [NSArray arrayWithObjects:one, two, three, four, nil];
NSPredicate *predicate = [NSCompoundPredicate andPredicateWithSubpredicates:array];
NSLog(@"%d", [predicate evaluateWithObject:@"CH998broken"]); // NO
NSLog(@"%d", [predicate evaluateWithObject:@"CH998"]); //YES

@"attributeName == %@": the value of the key attributeName is the same as the value of the object(NSDate, NSNumber, NSDecimalNumber, or NSString). 完全相等判断。

@"%K == %@": the value of the key %K is the same as the value of the object %@. key 对应的值和给定的值相等。

@"name IN $NAME_LIST": the value of the key name is in the variable $NAME_LIST. @"'name' IN $NAME_LIST": the constant value ‘name’ (note the quotes around the string) is in the variable $NAME_LIST. 判断值是否在指定列表中,前者判断是 name 对应的值,后者 'name' 就是判断 name 字符串。

参考资料:

Core Data Notes

两篇很不错的 Core Data Tutorial, Getting StartedHow to use NSFetchedResultsController

NSPersistentStoreCoordinator 是持久化存储, NSManagedObjectModel 指明存储数据结构和关系,NSManagedObjectContext 来读取、存储操作。

- (NSManagedObjectContext *)managedObjectContext
{
    if (_managedObjectContext != nil) {
        return _managedObjectContext;
    }

    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil) {
        _managedObjectContext = [[NSManagedObjectContext alloc] init];
        [_managedObjectContext setPersistentStoreCoordinator:coordinator];
    }
    return _managedObjectContext;
}

- (NSManagedObjectModel *)managedObjectModel
{
    if (_managedObjectModel != nil) {
        return _managedObjectModel;
    }
    NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"CDTest" withExtension:@"momd"];
    _managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
    return _managedObjectModel;
}

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
    if (_persistentStoreCoordinator != nil) {
        return _persistentStoreCoordinator;
    }

    NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"CDTest.sqlite"];

    NSError *error = nil;
    _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
    if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    }

    return _persistentStoreCoordinator;
}

新增数据:

Person *person = [NSEntityDescription insertNewObjectForEntityForName:@"Person"
                                               inManagedObjectContext:_managedObjectContext];
person.name = @"fannheyward";
person.age = [NSNumber numberWithInt:25];
[_managedObjectContext save:NULL];

通过 NSFetchRequest 查找,配合 NSPredicate 对数据进行过滤判断:

NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:@"Person"];
request.predicate = [NSPredicate predicateWithFormat:@"age == %@", age];;
NSArray *arr = [_managedObjectContext executeFetchRequest:request error:NULL];
for (NSManagedObject *obj in arr) {
    //...
}

NSFetchedResultsController 和 UITableView 做了很好的整合,可以根据 tableView 位置进行动态查询取数据。比如一共 100 个 cell,传统方式需要一次性全部拿到 DataSource 数据到内存,数据量过大的话会占用不少内存;用 NSFetchedResultsController 可以设置一次取数据的大小,然后根据滑动位置动态读取数据。

- (NSFetchedResultsController *)fetchController
{
    if (_fetchController != nil) {
        return _fetchController;
    }

    NSFetchRequest *request = [[NSFetchRequest alloc] init];
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Place"
                                              inManagedObjectContext:_managedContext];
    request.entity = entity;
    request.fetchBatchSize = 15;

    NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:@"date" ascending:NO];
    request.sortDescriptors = [NSArray arrayWithObject:sort];

    _fetchController = [[NSFetchedResultsController alloc] initWithFetchRequest:request
                                                           managedObjectContext:_managedContext
                                                             sectionNameKeyPath:nil
                                                                      cacheName:@"Place"];
    _fetchController.delegate = self;

    NSError *error = nil;
    if (![_fetchController performFetch:&error]) {
        DLog(@"fetch error: %@", [error description]);
        abort();
    }

    return _fetchController;
}

和 UITableView 的整合:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    id <NSFetchedResultsSectionInfo> info = [[_fetchController sections] objectAtIndex:section];
    return [info numberOfObjects];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    //...
    Place *place = [_fetchController objectAtIndexPath:indexPath];

    return cell;
}

SDWebImage 笔记

SDWebImage 支持异步的图片下载+缓存,提供了 UIImageView+WebCacha 的 category,方便使用。纪录一下 SDWebImage 加载图片的流程。

  1. 入口 setImageWithURL:placeholderImage:options: 会先把 placeholderImage 显示,然后 SDWebImageManager 根据 URL 开始处理图片。
  2. 进入 SDWebImageManager-downloadWithURL:delegate:options:userInfo:,交给 SDImageCache 从缓存查找图片是否已经下载 queryDiskCacheForKey:delegate:userInfo:.
  3. 先从内存图片缓存查找是否有图片,如果内存中已经有图片缓存,SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo: 到 SDWebImageManager。
  4. SDWebImageManagerDelegate 回调 webImageManager:didFinishWithImage: 到 UIImageView+WebCache 等前端展示图片。
  5. 如果内存缓存中没有,生成 NSInvocationOperation 添加到队列开始从硬盘查找图片是否已经缓存。
  6. 根据 URLKey 在硬盘缓存目录下尝试读取图片文件。这一步是在 NSOperation 进行的操作,所以回主线程进行结果回调 notifyDelegate:
  7. 如果上一操作从硬盘读取到了图片,将图片添加到内存缓存中(如果空闲内存过小,会先清空内存缓存)。SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo:。进而回调展示图片。
  8. 如果从硬盘缓存目录读取不到图片,说明所有缓存都不存在该图片,需要下载图片,回调 imageCache:didNotFindImageForKey:userInfo:
  9. 共享或重新生成一个下载器 SDWebImageDownloader 开始下载图片。
  10. 图片下载由 NSURLConnection 来做,实现相关 delegate 来判断图片下载中、下载完成和下载失败。
  11. connection:didReceiveData: 中利用 ImageIO 做了按图片下载进度加载效果。
  12. connectionDidFinishLoading: 数据下载完成后交给 SDWebImageDecoder 做图片解码处理。
  13. 图片解码处理在一个 NSOperationQueue 完成,不会拖慢主线程 UI。如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多。
  14. 在主线程 notifyDelegateOnMainThreadWithInfo: 宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo: 回调给 SDWebImageDownloader。
  15. imageDownloader:didFinishWithImage: 回调给 SDWebImageManager 告知图片下载完成。
  16. 通知所有的 downloadDelegates 下载完成,回调给需要的地方展示图片。
  17. 将图片保存到 SDImageCache 中,内存缓存和硬盘缓存同时保存。写文件到硬盘也在以单独 NSInvocationOperation 完成,避免拖慢主线程。
  18. SDImageCache 在初始化的时候会注册一些消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过期图片。
  19. SDWI 也提供了 UIButton+WebCacheMKAnnotationView+WebCache,方便使用。
  20. SDWebImagePrefetcher 可以预先下载图片,方便后续使用。

AFNetworking 学习笔记

  1. 这篇笔记是在 AFN v0.10.1 时候写的,AFN v1.0 以后加入了不少新东西,比如 SSL 支持,不过整体结构没有变化。
  2. 后续跟进了一篇 AFNetworking Notes 2

AFN.jpg

上图来自 @mattt 对 AFN 的介绍:Everybody Loves AFNetworking And So Can You!. 学习 AFN,简单记录一下以加深自己理解。

AFN 的基础部分是 AFURLConnectionOperation,一个 NSOperation subclass,实现了 NSURLConnection 相关的 delegate+blocks,网络部分是由 NSURLConnection 完成,然后利用 NSOperation 的 state (isReady→isExecuting→isFinished) 变化来进行网络控制。网络请求是在一个指定的线程(networkRequestThread)完成。

AFURLConnectionOperation 是一个很纯粹的网络请求 operation,可以对他进行 start/cancel/pause/resume 操作,可以获取对应的 NSURLRequest 和 NSURLResponse 数据。支持 NSInputStream/NSOutputStream,提供了 uploadPress 和 downloadProgress 以方便其他使用。

NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://httpbin.org/ip"]];
AFURLConnectionOperation *operation = [[AFURLConnectionOperation alloc] initWithRequest:request];
operation.completionBlock = ^ {
    NSLog(@"Complete: %@", operation.responseString);
};
[operation start];

插播:@mattt 在 NSHipster 里有一篇 NSOperation 详细介绍了 NSOperation 的 state、priority、dependency 等,对理解 AFURLConnectionOperation 很有帮助。


理解了 AFURLConnectionOperation 再看 AFHTTPRequestOperation 就简单很多。AFHTTPRequestOperation 是 AFURLConnectionOperation 的子类,针对 HTTP+HTTPS 协议做了一层封装,比如 statusCode、Content-Type 等,添加了请求成功和失败的回调 block,提供了 addAcceptableContentTypes: 以方便上层使用。

NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://httpbin.org/robots.txt"]];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
    NSLog(@"Success: %@", operation.responseString);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
    NSLog(@"Failure: %@", error);
}];
[operation start];

AFJSONRequestOperation 是 AFHTTPRequestOperation 的子类,针对 JSON 类型请求做了特殊处理,在有了 AFHTTPRequestOperation+AFURLConnectionOperation 的基础工作后,AFJSONRequestOperation 已经非常方便直接使用了。指定 acceptableContentTypes: 以支持 JSON,responseJSON 直接返回已经解析好的 JSON 数据对象。下载到 JSON 数据后在一单独线程 queue(json_request_operation_processing_queue)对 JSON 数据进行解析处理,处理完成后由主线程回调 success block。

AFN 的 JSON encode/decode 处理做的非常巧妙,现在有很多 JSON 解析库,第三方的 JSONKit、SBJSON 等,iOS 5+ 自带的 NSJSONSerialization,不同的项目可能会因为不同的需求而用不同的库,AFN 就封装了一个 AFJSONUtilities,提供 AFJSONEncodeAFJSONDecode 两个方法,通过 NSClassFromStringNSSelectorFromString 来查找项目中使用的 JSON 库然后进行 encode/decode。

NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://httpbin.org/get"]];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request
     success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
        NSLog(@"Success :%@", JSON);
     } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
        NSLog(@"Failure: %@", error);
     }];
[operation start];

AFXMLRequestOperation/AFPropertyListRequestOperation/AFImageRequestOperation 和 AFJSONRequestOperation 类似,针对 XML、Plist、image 类型请求做了一些处理。其中 AFImageRequestOperation 额外有一个 imageProcessingBlock,取到图片后可以在一个单独线程 queque 对图片进行处理,比如缩放、切圆角、图片特效等,然后再交给 main_queue success block.

AFN 还提供了一个 UIImageView+AFNetworking category,可以用 setImageWithURL: 来设置图片。这个 cagetory 和 SDWebImage 类似但更简单一些,图片下载由 AFN 完成,图片缓存由 NSCache 处理。


直接用上面这些已经可以方便的做网络请求,AFN 在这些基础上还提供了一个 AFHTTPClient,把 HTTP 请求的 Headers、User-Agent 等再次包装,方便使用。AFHTTPClient 是一个单例,对请求参数做了 URL 编码;维护一个 NSOperationQueue,不同的请求生成各自的 AFHTTPRequestOperation 然后 enqueueHTTPRequestOperation: 添加的队列顺序执行;registerHTTPOperationClass: 方法用来注册上面的 JSON/XML/Plist/image operation,拿到请求结果后交给对应的 operation 处理。AFHTTPClient 还针对 GET/POST/HEAD/PUT/DELETE 等不同的请求做了不同的 URL 参数和 Headers 处理,包括 multipart/form-data 类型。

AFHTTPClient 支持批量添加 operations,生成一个 batchedOperation,把所有 operations 作为 batchedOperation 的 dependency,再依次把所有 operations 和 batchedOperation 都添加到 operationQueue,这样每一个 operation 完成后都可以做一个 progressBlock 来返回当前已完成的 operations 数和总数,等所有 operations 都完成后会做 batchedOperation 的 completionBlock,就可以在这一批 operations 都完成后做一些善后处理。

AFHTTPClient 提倡对同一应用(同一 baseURL)的网络请求封装自己的 HTTPClient 子类,这样会方便很多。参考 WBKHTTPClient.


AFN 还提供了很多模块,可以很方便的和 AFN 整合做一些工作,比如 OAuth,Amazon S3 等,详见 AFNetworking-Extensions.


AFN 作者 @mattt 做东西很有自己一套思想在里面,推荐 What I Learned From AFNetworking’s GitHub Issues视频

Don't use accessor methods in init and dealloc

苹果在 WWDC 2012 Session 413 - Migrating to Modern Objective-C 里强调不要在 init 和 dealloc 里使用 accessor methods:

Always use accessor methods. Except in initializer methods and dealloc.

之前没有注意过这种情况,稍微搜索学习了一下。文档 Memory Management 里确实有这说法:

The only places you shouldn’t use accessor methods to set an instance variable are in initializer methods and dealloc.

提倡下面这种写法:

- (id)init {
    self = [super init];
    if (self) {
        _count = [[NSNumber alloc] initWithInteger:0];
    }
    return self;
}

- (id)initWithCount:(NSNumber *)startingCount {
    self = [super init];
    if (self) {
        _count = [startingCount copy];
    }
    return self;
}

dealloc 不能用比较好理解,self.property 是向 property 发了一个消息,有可能该对象的生命周期已经结束,不能再接受消息。init 不能用比较靠谱的说法是如果有 subClass 并重载了 accessor,那么 init 里 self.property 就无效;另外也可能会有其他影响,比如 KVC notifications 等。

SO 参考帖子:

  1. Should I refer to self.property in the init method with ARC?
  2. Using properties to access iVars in init?
  3. Initializing a property, dot notation
  4. Objective-C Dot Syntax and Init

What I have learned from Cheddar for iOS

  1. Code Style.
  2. DRY,整理适合自己的代码库(SSToolkit)。
  3. application:didFinishLaunchingWithOptions: 里尽量少操作,减少 launch 时间。只做界面展示工作,数据层用 dispatch_async 异步操作。
  4. 多用 [image stretchableImageWithLeftCapWidth:5 topCapHeight:0] 图片拉伸,减小 App size。效果上并没有缺失很多。很多效果都可以用代码实现,不一定非得贴图。
  5. 数据层封装不同的对象,方便各种调用。直接用 dict 传来传去不够清晰。
  6. Core Data 和 UIViewController 可以很好的结合,深度封装后的确很方便,参见 SSManagedViewController = UIViewController+SSManagedObject(NSManagedObject),SSDataKit。但这样感觉 ViewController 很沉重,也可能是因为我对 Core Data 不熟悉,以后有机会加深一下 CD 的学习使用。
  7. KVO 是个好东西。
  8. 定义一些内部 scheme 来做界面跳转,x-cheddar-tag.
  9. UIColor+CheddariOSAdditions.h-cheddarTextColor,定义整体风格配色,很方便使用。UIFont+CheddariOSAdditions.h 同理。
  10. cellHeightForText:dispatch_once_t 生成一个单例 label,然后 sizeThatFits: 计算。
  11. prepareForReuse 释放数据。
  12. CDKHTTPClient 学习 AFN 的好例子。单实例,用 block 封装接口。Block is better than delegate, simple, clear and powerful.

iOS Background Task Notes

iOS 4+ 支持 audio、location、voip 后台常驻任务,除此以外 App 还可以向系统申请额外一段时间(十分钟)在后台执行某些任务,比如进入后台后发送操作日志等。

注册消息通知,或者直接实现 - (applicationDidEnterBackground:(UIApplication *)application delegate。

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(appDidEnterBackground)
                                             name:UIApplicationDidEnterBackgroundNotification
                                           object:nil];

向系统申请 background task 并执行:

- (void)appDidEnterBackground
{
    if (![UIDevice currentDevice].multitaskingSupported) {
        return;
    }

    UIApplication *app = [UIApplication sharedApplication];
    __block UIBackgroundTaskIdentifier bgTask = [app beginBackgroundTaskWithExpirationHandler:^{
        dispatch_async(dispatch_get_main_queue(), ^{
            if (bgTask != UIBackgroundTaskInvalid) {
                [app endBackgroundTask:bgTask];
                bgTask = UIBackgroundTaskInvalid;
            }
        });
    }];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //Do tasks you want.

        dispatch_async(dispatch_get_main_queue(), ^{
            if (bgTask != UIBackgroundTaskInvalid) {
                [app endBackgroundTask:bgTask];
                bgTask = UIBackgroundTaskInvalid;
            }
        });
    });
}

注意:beginBackgroundTaskWithExpirationHandler: 生成的 task 在执行完以后必须要用 endBackgroundTask: 告诉系统任务已结束,不然在申请时间用完以后 App 会被系统直接终止,而不是挂起(suspended)。

20120730

公司最早的一批人,就剩我了。

相信她们的选择,祝福她们的明天,可心里还是难受。

你们一定要过的比今天好。

Symbol not found: _objc_storeStrong

Crash log:

dyld: lazy symbol binding failed: Symbol not found: _objc_storeStrong
  Referenced from: /var/mobile/Applications/6E4A4771-B39A-48B9-A7B7-0EA0108DCAF4/X.app/X
  Expected in: /usr/lib/libobjc.A.dylib

dyld: Symbol not found: _objc_storeStrong
  Referenced from: /var/mobile/Applications/6E4A4771-B39A-48B9-A7B7-0EA0108DCAF4/X.app/X
  Expected in: /usr/lib/libobjc.A.dylib

在 Non-ARC 项目中使用 ARC-enabled 库的时候,需要对库文件在 Build Phases->Compile Sources 添加 -fobjc-arc Compiler Flags,在 Build Settings->Other Linker Flags 添加 -fobjc-arc.

via libobjc.A.dylib compile error on iOS 4.3, Static library with ARC support linked to non-ARC project causing linker errors.

UISegmentedControl error on iOS 4.x

Code 1:

self.segmentedControl = [[[UISegmentedControl alloc] init] autorelease];
[_segmentedControl setSelectedSegmentIndex:0];
[_segmentedControl addTarget:self
                      action:@selector(segmentedControlSwitch)
            forControlEvents:UIControlEventValueChanged];

Code 2:

self.segmentedControl = [[[UISegmentedControl alloc] init] autorelease];
[_segmentedControl addTarget:self
                      action:@selector(segmentedControlSwitch)
            forControlEvents:UIControlEventValueChanged];
[_segmentedControl setSelectedSegmentIndex:0];

注意 [_segmentedControl setSelectedSegmentIndex:0]; 的位置,在 iOS 4.x 下,Code 2 代码在设置 selectedSegmentIndex 的时候会执行一次 segmentedControlSwitch 方法。在 iOS 5+ 没有这个问题。