Heybot - My Gtalk Hubot

Github 是非常好的学习地方,Github Inc 这家公司也很有意思,一帮 Geek 程序员做了很多很好玩的东西,比如 Hubot, Play。Hubot 是一个机器人,可以音乐、搜索、搞怪逗乐等,在 Github 内部他们还用 Hubot 部署代码。开源版本的 Hubot 目前不支持代码部署等高级命令,不过可以自己写脚本(CoffeeScript)进行扩展。

Hubot 原生支持 Campfire、Shell 作接口,通过 npm 扩展可以用 Gtalk、IRC 等等。在 Heroku 上部署了一个用 Gtalk 作接口的 Heybot(Heyward’s Hubot),简单纪录一下。

wget https://github.com/downloads/github/hubot/hubot-2.3.2.tar.gz
tar xzvf hubot-2.3.2.tar.gz
cd hubot

vim Procfile 修改 adapter:
    app: bin/hubot -a gtalk -n Hubot
vim package.json 添加 hubot-gtalk 到 dependencies:
    "hubot-gtalk": ">= 0.0.1",

git init
git add *
git commit -m "init"

heroku apps:create
git push heroku master
heroku ps:scale app=1
heroku addons:add redistogo:nano
heroku config:add HUBOT_GTALK_USERNAME="xxx" HUBOT_GTALK_PASSWORD="xxx"
heroku ps:restart

添加 Gtalk 好友,hubot help 可以查看目前支持的命令。

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.