うな(。・ε・。)

Android, iOS, AppEngine まわりのめもめも

MagicalRecord で非同期クエリを行う

MagicalRecord の + MR_findAll などの同期メソッドは非常に便利なのですが、UI を非常にブロックしやすい性質があります。大量のオブジェクトが保存されうる場合に特に注意が必要です。 めやすとしては、1000 件以上のデータは同期メソッドでは顕著になります。

特に、- viewDidLoad でクエリを行うと、画面遷移が起こる前に UI がブロックされます。結果、「タップしたのになかなか移動しない」イライラ感に繋がります。

非同期にクエリを行う

非同期にクエリを行うには、NSManagedContextNSFetchRequest を直接取り扱う必要があります。

// XXXManager.m

- (void)fetchAllXXXWithPredicate:(NSPredicate *)predicate completionBlock:(void(^)(NSArray *fetchResults))completionBlock {
    NSFetchRequest *request = [XXX MR_createFetchRequest];
    request.predicate = predicate;
    
    NSManagedObjectContext *context = [NSManagedObjectContext MR_contextForCurrentThread];
    [context performBlock:^{
        NSArray *result = [context executeFetchRequest:request error:nil];

        completionBlock(result);
    }];
}

NSFetchedResultsController で非同期にクエリを行う

NSFetchedResultsController を使う場合は、利用する ViewController 内で NSFetchedResultsController を初期化してから、以下のようにします。(エラー処理は簡略化してます)

@weakify(self);
[self.fetchedResultsController.managedObjectContext performBlock:^{
    @strongify(self);
    [self.fetchedResultsController performFetch:nil];

    @weakify(self);
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        @strongify(self);
        [self.collectionView reloadData];
    }];
}];

これは NSFetchedResultsController のカテゴリを作ってもよいでしょう。

// NSFetchedResultsController+AsyncFetch.m

- (void)ki_performFetchWithCompletionBlock:(void(^)(NSError *errorOrNil))completionBlock {
    @weakify(self);
    [self.managedObjectContext performBlock:^{
        @strongify(self);
        NSError *errorOrNil = nil;
        [self performFetch:&errorOrNil];
    
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            completionBlock(errorOrNil);
        }];
    }];
}

参考文献

App Store Views / App Units は「インストール率」ではありません

iTunes Connect Analytics で以下のようなハマりどころがあったので共有します(*´ω`*)

  • まず、iOS8 以降の端末での情報のみがカウントされます。iOS7 以下の情報を含まないので正確な数字ではありません。
  • App Store Views / App Units = インストール率 ではありません
    • なぜなら、検索結果から直接インストールすることが出来るからです。それらは、App Store Views には反映されず、App Units だけが増えてしまいます。
  • App Usage のセクションの情報は実情に比べて少ないです。
    • なぜなら、「アナリティクスによってトラッキングされることを承諾したユーザ」のみがカウントされているからです。
  • WebSite からの流入は、ドメインまでしかわかりません。
    • 広告などでリンクの貼り方を指定できる場合は、必ず campaign リンクを貼ってもらいましょう。
  • App Store 内での検索ワードがわかりません。
    • (出来るようになってるって宣言してたのに…)
    • 対策は存在しません。
    • Searchman などを利用すれば、それぞれのキーワードに対して、ある程度ボリュームを見積もることができます。

その他

  • App Units はデバイスによりユニークにカウントされます。

小ネタ

Source 毎のエンゲージメントのトラッキングなどは、App Analytics では特定のリンクとデバイスとの紐付けができないのでかなり不自由です。

Yozio などを使うことをおすすめします。 Yozio は、いい感じにメタデータをつけたリンクを発行できるサービスです。このリンクからアプリをインストールしてもらうと、初回起動時にそのメタデータをコールバックにて受け取ることができます。

Yozio を使えば、Source 毎にユニークなメタデータをつけて、初回起動時に計測ツールと結びつけてやれば OK です。

www.yozio.com

参考

デバッグ用に、ログと CoreData のダンプを送信する

さいきん、デバッグ用に、ログと CoreData のダンプを送信出来る機能を実装しました。

デバッグ情報の送信機能を実装することで、クラッシュレポートでは検知出来ないようなロジックのバグやデータの不整合、その他様々な不具合のデバッグをサポートする事が出来ます。 今までは、このような不具合に対するデバッグを行なう際、データベースとログの内容を見る事が出来ないので非常に歯がゆい思いをしていました。

上記のような、モバイルアプリデバッグの難しさを減らすことを目的とし、表題の機能を実装しました。

CoreData のダンプを送る

NSPersistentStore の永続先として設定してある .sqlite をそのままアップロードしてやれば OK です。

私は MagicalRecord を使っていたので少し工夫が必要でした。 MagicalRecord では、特別なことをしていない場合、次のようにして .sqlite の filepath を取得する事が出来ます。

NSURL *filePath = [NSPersistentStore MR_urlForStoreName:[MagicalRecord MR_defaultStoreName]];

これを AFNetworking とかで適当にアップロードしてやれば OK です。

[self.httpOperationManager POST:url parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
    [formData appendPartWithFileURL:filePath name:@"database" fileName:@"film-model.sqlite" mimeType:@"application/x-sqlite3" error:nil];
} success:^(AFHTTPRequestOperation *operation, id responseObject) {
    FLMLog(@"Debug: Sending dump succeed!");
    
    success(responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
    FLMLog(@"Debug: Sending dump failed!");
    
    failure(error);
}];

ログを送信する

CocoaLumberjack/CocoaLumberjack · GitHub を使います。

CocoaLumberjack は、ログを複数のロガーに流せたりするナイスな古参ライブラリです。CocoaLumberjack の FileLogger を使って送信用ログをファイルに貯めます。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [DDLog addLogger:[DDTTYLogger sharedInstance]]; // ふつうのロガー
    DDFileLogger *fileLogger = [[DDFileLogger alloc] init];
    [DDLog addLogger:fileLogger]; // ファイルに書き込むロガー。遅くない。

    // ...
}

初期化時にこのようにロガーの設定を行ないます。

以降は DDLogDebug() などのマクロでログを行なう事が出来ます。 そのため、NSLog を直接使っている場合は、DDLogDebug などに書き換える必要があります。

面倒ですが、どうせ NSLog を直接使うと「プロダクション時にログを吐かない」などの調整が出来ないのでやめましょう。

ログファイルのアップロード

ログファイルの取得には一工夫必要です。なぜなら、DDFileLogger はログローテーションに対応しているため、ログファイルが一つに定まりません。

対象のログ全てを送ればよいのですが、面倒くさいです。最新のログを送っておけば大体 OK です。たぶん。そのためには次のようなハックが必要になります。

ios - Where is Logfile stored using cocoaLumberjack - Stack Overflow

// @see http://stackoverflow.com/questions/6411549/where-is-logfile-stored-using-cocoalumberjack
// CocoaLumberjack の FileLogger から現在の .log ファイルを取得する為のハック
@interface DDFileLogger (CurrentLogFileHack)
- (DDLogFileInfo *)currentLogFileInfo;
@end
//

このようにすれば、fileLogger.currentLogFileInfo.filePath でファイルパスを取得する事が出来ます。

この filePath からファイル用の NSURL を得るには次のようにします。

- (NSURL *)logFileURLForCurrentLogFilePath:(NSString *)filePath {
    // @see http://stackoverflow.com/questions/11798537/reading-a-file-from-documents-directory-objective-c
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSString *filename = [filePath componentsSeparatedByString:@"/"].lastObject;
    NSString *logFilePath = [documentsDirectory stringByAppendingPathComponent:[NSString stringWithFormat:@"/Logs/%@", filename]];
    
    NSURL *fileURL = [NSURL fileURLWithPath:logFilePath];
    
    return fileURL;
}

このようにして取得した FileURL を AFNetworking 等を用いてアップロードしてやります。

[self.httpOperationManager POST:url parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
    [formData appendPartWithFileURL:fileURL name:@"log" fileName:@"film-model.log" mimeType:@"text/plain" error:nil];
} success:^(AFHTTPRequestOperation *operation, id responseObject) {
    FLMLog(@"Debug: Sending log succeed!");
    
    success(responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
    FLMLog(@"Debug: Sending log failed!");
    
    failure(error);
}];

AFNetworking の部分はバラバラに書きましたが、実際は一緒に送ります。

以上。

おち:手に入る情報が増えて遥かにマシになりました

デバッグでは情報が多い事がとにかく大事で、何はともあれまずはログとデータです。ログとデータがあればどんな事が起こったか想像がつきますが、無ければ状況から想像するしかありません。

デバッグに関して、面倒くさい事を考えたくなければとにかく様々な情報をたくさん送信しましょう。(*´ω`*)

Google Cloud Monitoring を使って障害のアラートを受け取る(*´ω`*)

App Engine でサービスを作っていると、ログは確かにいつでも見られて安心ですが、何かが起こった時にすぐに知れないのが辛いですね。 そんな時には Google Cloud Monitoring です。

下の図のようにアラートを設定し、メール, Slack, SMS, 電話など様々な手段で受け取る事が出来ます。

f:id:liedderoptik:20150512214649p:plain

f:id:liedderoptik:20150512220316p:plain

つかいかた

Google Cloud Console のメニューより、Monitoring > Dashboard & alerts をクリックすることで、Google Cloud Monitoring のダッシュボードを表示する事が出来ます。

いわゆる障害監視のサービスですので、色々出来る事がありますが、まずは表題の通り、アラートを設定しましょう。

"Set Alerting Policies" と見出しのある場所のボタン "Create Policy" を押して、アラートを作る事が出来ます。

f:id:liedderoptik:20150512220514p:plain

アラートを作成するページでは、まず「どんな種類の条件か」を聞かれます。最初から設定されている "Threshold Conditions" を使うので、青い "Next" ボタンを押してください。

Resource Type (監視対象) を聞かれるので、"App Engine" に設定し、自分のアプリ名を選択します。その後 "Next" です。

f:id:liedderoptik:20150512215207p:plain

最後に監視する指標を決めます。App Engine で困るのはだいたい、「エラーがたくさん発生してる」「課金が足りなくて Over Quota になってる」の二つなので、設定しておきましょう。

下の図は、「エラーがたくさん発生している」の設定例です。実際のエラー率を元に数字を決める事が出来ます。

f:id:liedderoptik:20150512215353p:plain

「課金が足りなくて Over Quota になってる」は "Quota Based Request Denials" という Metrics で監視出来ます。これは Threshold を 1 など低い値にしておくといいでしょう。

あとは適当にアラート先(メール, Slack, SMS, 電話)を設定すれば、障害の検知を行なう事が出来ます。

便利(*´ω`*)

追記

課金アカウントを変えると App Engine の Daily Budget が $0.00 になります。(わお)他にも、課金の支払いを何回も失敗すると同様の事が起こります。

この謎仕様により Over Quota になってしまい障害になるという、笑えない失敗事例がありました。不測の事態に対応するためアラートは設定しておきましょう…

ソーシャル共有の数を一括で取得して JSONP で返す API

何に使うわけでもありませんがつくってみた

GET https://socialcountapi.appspot.com/count?callback=jsonp&url=https://facebook.com/

jsonp({"twitter":31135,"facebook":234483"pocket":503041});

github.com

ページを速く表示する為に、ソーシャルプラグインを使わずに独自で実装する、というメンドクサイことをやるときに使えます。

おち: ない

ActivityLifecycleCallbacks で view イベントを自動でログる

ActivityLifecycleCallbacks は Activity のライフサイクルを監視して、処理を挟み込める機構です。

使い方は至って単純で、Application.ActivityLifecycleCallbacks を implement し、Applicationインスタンスメソッド registerActivityLifecycleCallbacks() を呼ぶだけ。

public class YourApplication extends Application implements Application.ActivityLifecycleCallbacks {
    @Override
    public void onCreate() {
        super.onCreate();
        
        registerActivityLifecycleCallbacks(this);
    }

    @Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        final Bundle logArgs = new Bundle();
        logArgs.putString("screen", activity.getClass().getSimpleName());

        someTracker.track("view", logArgs);
    }
}

全ての Activity に対して、コールバックが発動する事に注意します。

BaseActivity との違い?

こういった使い方では、BaseActivity に色々処理を書くよりも、ActivityLifecycleCallbacks のほうが継承レースにならなくて良いと思います。 「共通処理」というよりは、「ライフサイクルを監視して何かする」ときに嬉しいです。

ですから、「EventBus に register, unregister する」「ButterKnife.inject() する」とかはやめた方が良いと思います。Activity のコードだけを読んだ際に自明ではありません。

飛び道具なので用法容量を守りましょう。(*´ω`*)

Internet.org は参加しないと表示されないよ(。・ε・。)

Internet.org は Facebook 社が主導する、途上国向けのインターネットプロバイダーです。 利用者は無料もしくは格安でインターネットを利用する事が出来ます。

途上国向けに低コストでインターネットを提供するため、Internet.org はアクセス出来るサイトを制限しています。正確に言えば、Internet.org 互換であり、かつ Internet.org に参画する Web サイトのみを閲覧することが出来ます。

これらの Web サイトは Internet.org アプリにてリストされます。

国際的に展開する Web サービスは Internet.org に互換し、参画することでよりユーザを増やすことの出来る可能性があります。

ここでは、Internet.org の定める仕様と、参画のための条件をメモりたいと思います。

利用技術の制約

Internet.org ユーザはスマートフォン、フィーチャフォンその他の機器を利用することが出来ます。また、これらの機器は必ずしも最新のものではなく、ブラウザの互換性も低いです。ゆえに、以下の技術は利用出来ません。

また、利用帯域を増大させる以下の要素は利用出来ません。

  • 動画
  • 大きな画像 (一つの画像につき、1 MB 以下に限定されます)

(注: 画像の数、合計量の制約は書いていませんでしたが、何らかの制約があると思います)

つまるところ、i-mode のサイトを作れば OK です。

参画手段

レビューがあります。ガイドラインを良く読みましょう。

Internet.org Participation Guidelines

サブミットは下のリンクから可能です。

Submit your service to be in Internet.org

より詳しくは Facebook Developers の Internet.org のページにて。

Internet.org Platform

細かい注意点

  • Internet.org 対応の Web サイトは、常に Internet.org Proxy を通じて配信されます。キャッシュはされません。
  • ドメインの埋め込みコンテンツ(YouTube など)は Proxy 側で切り捨てられます。
  • JavaScript を用いた Analytics が不可能なので、古き良き <img> タグビーコンで代用する必要があります。
  • とにかくあらゆる JavaScript が動きません。
  • Internet.org からのリクエストは、Via ヘッダーに internet.org の文字列が入っているかどうかをみることで、振り分けが可能です。User Agent は各ユーザのそれぞれの値が入ります。
  • Chrome Device Emulator で疑似テストが可能です。エミュレートするために、次の設定をします。

Facebook 的なベストプラクティス

  • 軽い体験を意識してください。動画を再生したり音楽をストリーミングしたりするサイトは、通信が制限されているケースでは酷い体験になります。
  • App Store などにユーザを誘導するのをやめましょう。「アプリ」の概念が不慣れなユーザが沢山います。
  • ログインや、登録しないと利用する事の出来ないコンテンツを提供しないようにしましょう。(Facebookェ…)