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 字符串。
参考资料:
- NSPredicates for Fun and Profit,非常不错的 NSPredicate 介绍
- 苹果文档 Predicate Programming Guide.
Core Data Notes
两篇很不错的 Core Data Tutorial, Getting Started,How 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 加载图片的流程。
- 入口
setImageWithURL:placeholderImage:options:
会先把 placeholderImage 显示,然后 SDWebImageManager 根据 URL 开始处理图片。 - 进入 SDWebImageManager-
downloadWithURL:delegate:options:userInfo:
,交给SDImageCache
从缓存查找图片是否已经下载queryDiskCacheForKey:delegate:userInfo:
. - 先从内存图片缓存查找是否有图片,如果内存中已经有图片缓存,SDImageCacheDelegate 回调
imageCache:didFindImage:forKey:userInfo:
到 SDWebImageManager。 - SDWebImageManagerDelegate 回调
webImageManager:didFinishWithImage:
到 UIImageView+WebCache 等前端展示图片。 - 如果内存缓存中没有,生成 NSInvocationOperation 添加到队列开始从硬盘查找图片是否已经缓存。
- 根据 URLKey 在硬盘缓存目录下尝试读取图片文件。这一步是在 NSOperation 进行的操作,所以回主线程进行结果回调
notifyDelegate:
。 - 如果上一操作从硬盘读取到了图片,将图片添加到内存缓存中(如果空闲内存过小,会先清空内存缓存)。SDImageCacheDelegate 回调
imageCache:didFindImage:forKey:userInfo:
。进而回调展示图片。 - 如果从硬盘缓存目录读取不到图片,说明所有缓存都不存在该图片,需要下载图片,回调
imageCache:didNotFindImageForKey:userInfo:
。 - 共享或重新生成一个下载器
SDWebImageDownloader
开始下载图片。 - 图片下载由 NSURLConnection 来做,实现相关 delegate 来判断图片下载中、下载完成和下载失败。
connection:didReceiveData:
中利用 ImageIO 做了按图片下载进度加载效果。connectionDidFinishLoading:
数据下载完成后交给SDWebImageDecoder
做图片解码处理。- 图片解码处理在一个 NSOperationQueue 完成,不会拖慢主线程 UI。如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多。
- 在主线程
notifyDelegateOnMainThreadWithInfo:
宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo:
回调给 SDWebImageDownloader。 imageDownloader:didFinishWithImage:
回调给 SDWebImageManager 告知图片下载完成。- 通知所有的 downloadDelegates 下载完成,回调给需要的地方展示图片。
- 将图片保存到 SDImageCache 中,内存缓存和硬盘缓存同时保存。写文件到硬盘也在以单独 NSInvocationOperation 完成,避免拖慢主线程。
- SDImageCache 在初始化的时候会注册一些消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过期图片。
- SDWI 也提供了
UIButton+WebCache
和MKAnnotationView+WebCache
,方便使用。 SDWebImagePrefetcher
可以预先下载图片,方便后续使用。
AFNetworking 学习笔记
- 这篇笔记是在 AFN v0.10.1 时候写的,AFN v1.0 以后加入了不少新东西,比如 SSL 支持,不过整体结构没有变化。
- 后续跟进了一篇 AFNetworking Notes 2
上图来自 @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,提供 AFJSONEncode
和 AFJSONDecode
两个方法,通过 NSClassFromString
和 NSSelectorFromString
来查找项目中使用的 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 参考帖子:
What I have learned from Cheddar for iOS
- Code Style.
- DRY,整理适合自己的代码库(SSToolkit)。
application:didFinishLaunchingWithOptions:
里尽量少操作,减少 launch 时间。只做界面展示工作,数据层用 dispatch_async 异步操作。- 多用
[image stretchableImageWithLeftCapWidth:5 topCapHeight:0]
图片拉伸,减小 App size。效果上并没有缺失很多。很多效果都可以用代码实现,不一定非得贴图。 - 数据层封装不同的对象,方便各种调用。直接用 dict 传来传去不够清晰。
- Core Data 和 UIViewController 可以很好的结合,深度封装后的确很方便,参见 SSManagedViewController = UIViewController+SSManagedObject(NSManagedObject),SSDataKit。但这样感觉 ViewController 很沉重,也可能是因为我对 Core Data 不熟悉,以后有机会加深一下 CD 的学习使用。
- KVO 是个好东西。
- 定义一些内部 scheme 来做界面跳转,
x-cheddar-tag
. UIColor+CheddariOSAdditions.h
-cheddarTextColor
,定义整体风格配色,很方便使用。UIFont+CheddariOSAdditions.h
同理。cellHeightForText:
用dispatch_once_t
生成一个单例 label,然后sizeThatFits:
计算。prepareForReuse
释放数据。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+ 没有这个问题。