2013年9月6日金曜日

UIAppearanceの使い方

iOS6の時点の知識です。ついでに半年ほどAndroidしかやってないです。

iOS7でどう変わるんでしょう。試しにDeveloperプレビュー落としたら、手元のlionでは動かなかった。さすがAppleさん。

---

iOS5からUIAppearance Protocolが追加され、ほとんどのUI部品が準拠しました。

これはAppearanceプロキシの提供を宣言するプロトコルです。Appearanceプロキシに対して背景画像やテキストの属性を指定すると、同一クラスのUI部品全てに変更を加えられます。

フレームワークのUI部品に対して直接使用しない方が無難

チュートリアルではUINavigationBarなどを直接書き換えていますが、これは予期せぬ結果を招くことがあります。

例えば、UINavigationBarを書き換えると、UIPopover下で表示されるNavigationBarも変更されてしまいます。UIPopoverの外観を変更する手段は提供されておらず、残念な見栄えになること請け合いです。

+appearanceWhenContainedIn:メソッドを使用して、UIPopoverとして表示されたときには変更を無かったことにするという手法も考えられます。-setBackgroundImage:を書き換えても、nilで上書きすれば元に戻すことができます。

しかしそんな小細工をするくらいなら、最初からUINavigationBarのカスタムなサブクラスを作り、そのクラスのAppearanceプロキシを操作した方が良いと思います。

カスタムなUINavigationBarを用意したならば、それを利用するUINavigationControllerサブクラスも作成する必要があるのでは?と思うところですが、iOS5からUINavigationControllerの生成メソッドとして、任意のUINavigationBarおよびUIToolBarサブクラスを指定できる-initWithNavigationBarClass:toolbarClass:が追加されています。

またカスタムサブクラスを使うメリットとして、Appearance操作の影響範囲を自分のアプリのみに限定することができます。外部のSDK等が出した画面まで書き換わる事態を防ぐことができます。

UIAppearanceを操作するタイミング

大前提として、Viewが表示される前に操作する必要があります。

UIViewControllerのコールバックメソッド、-viewDidLoadで直接Viewに対して-setTintColor:などを実行した場合、その場で変更が適用されますが、Appearanceプロキシを通して加えた変更は、再度-loadViewが実行されないと適用されません。

そのため、より上位のApplicationのスコープ、-application:willFinishLaunchingWithOptions:のタイミングで行うのが一つ目の選択肢です。常套手段としてはこちらになります。

もう一つの選択肢として、当該のViewクラスがObjective-Cランタイムに読み込まれた時点で行う手があります。つまり+loadまたは+initalizeをオーバーライドして、Appearance操作を行います。

後者の方法には、独自のサブクラスを経由して操作する、という規約を作る場合、そのクラス内部に外観変更操作が隠蔽されるというメリットがあります。

しかし、Appearanceプロキシに対する操作を特定のクラスの責務とした方が、規模が大きくなった場合の混乱が小さいかもしれません。

UIButtonへのAppearance操作について

ボタンのサイズは文字列長に応じて変わるため、通常不定です。

-resizableImageWithCapInsets:resizingMode:でマージンと拡縮方式を指定して背景画像とすることで、リサイズに対応したカスタムボタンを作れます。用意するのは一つの画像と、Appearanceプロキシを操作する数行のコードだけです。

しかもどっかの9Patchと違って、パターンの繰り返しに対応しています。いまや完全にどっかの9patchの上位互換です。(ただしiOS6以上限定)

例えば、HGDetermineButtonやらHGCancelButtonのようなサブクラスを作成し、個々の見栄えを変更することで、-loadViewから装飾のためのコードが一掃されます。生成するクラス名から各ボタンの役割も明確となります。

xibを使っている場合は、UIButonを配置した後で、ボタンのクラスを変更するだけです。

ただしAppearanceプロキシによる変更は実行時に行われるため、InterfaceBuilder上ではただのRoundRectButtonとして表示されるので、xibを見ただけでは実際の表示が分からないという事態を引き起こしますが…。

ボタンのLabelを変更する場合、Appearanceプロキシに対してsetTitleColor:forState:で行います。

---

余談として、UIButton内のラベルはUILabelで表示している、ということを利用して、

[[[UILabel class] appearanceWhenContainedIn:[HogeButton class], nil] setTextColor:[UIColor redColor]];

のような記述ができると考えるかもしれませんが、このコードは無視されます。setTitleColor:forState:で設定された色をボタンに適用するタイミングより、前に実行されてしまうためでしょうか。

しかし、2011年の10月に書かれたUIAppearance で色や画像を変える - Cocoaの日々ではUILabelの色を操作するとボタンの色も変わると書いてある(iOS6では変わりません)ので、iOS5.0時点だと挙動が変わるかもしれません。

どの属性がUIAppearanceに対応しているのか?

UIAppearanceが使いにくい理由の9割は、Appearanceプロキシに変更を加えたとして、実際にその処理が適用されるかどうかが分からない点です。

「UIAppearance Protocolに準拠している」という事実は+appearanceメソッドでAppearanceプロキシを返却することを宣言しているだけで、実際どのプロパティ、どのメソッドがプロキシ経由の操作を受け付けるのかについては分からないのです。

これについては当該Viewのヘッダを見て、プロパティやメソッドの定義にUI_APPEARANCE_SELECTORが付いているか確認する、という手段しかないと思います。

あれ、さっき使ったsetTitle:forState:にはUI_APPEARANCE_SELECTORは付いてなかったんですけど…。

---

このUI_APPEARANCE_SELECTORは、UI Appearance Proxy APIに参加していることを宣言するだけで、実際に何らかの機能を伴っているわけではないようです。

なぜか抜けているプロパティやメソッドもあるため、書いていないのに機能するケースもあります。

しかもリファレンスにはどのメソッド/プロパティがAppearance Proxy API対応しているのかの記述はなく、またバージョンによって対応箇所が増えている一方、API Diffsでは確認できないようです。

もしiOS5対応が必要な場合はiOS5のframeworkのヘッダを読むわけですが、しかしそのヘッダの情報が正しいという保証もなく、結局のところiOS5実機を用意して確認するのが一番確実でしょう。

UIAppearanceContainerプロトコルの意味は?

UIAppearance準拠クラスを見ると、UIAppearanceContainerプロトコルにも準拠しているものがあると思うんですけど、何も宣言していないんですよね。

何かのマーカーだったと思いますけど、忘れました。

結局のところUIAppearanceは使うべきなのか?

プロトタイプをさくっと完成させるのには超便利だと思います。

しかしプロダクションコードで使うには、リファレンスに書いてないブラックボックスが怖すぎるのと、そもそも国内開発者でこの機能を把握してる人の割合が謎で、多人数開発ではとても怖くて使えないかなぁ…。

2013年9月1日日曜日

Geocoderクラスは確実に使えるとは限らない

Androidにはジオコーディング/逆ジオコーディング(地理座標から住所の逆引きなど)を行うことのできるandroid.location.Geocoderというクラスが用意されています。

Google APIs付きのSDKでビルドするだけで利用できるので、非常にお手軽なのですが、Geocoderはプロキシクラスで、実際の処理を行っているのはバックエンドで動作しているLocationManagerServiceとなります。

このLocationManagerServiceが何らかの理由で死ぬ(メモリや電源が極端に消耗するとシステムが自動的に殺すことがある)と、端末を再起動するまでGeocoderは使用不可能になります。この状況でもGeocoder.isPresent()はtrueを返すので、実行するまで成功するかどうか分かりません。

Service not AvailableというメッセージのIOExceptionで失敗した場合、大体これのせいです。

どうもLocationManagerServiceをServiceManagerに登録にする処理が、端末起動時のSystemServer起動時に一度だけ行われるらしく、LocationManagerServiceが死ぬ事態を想定していないのが原因っぽいのですが…。

自力で復帰させるコードを書くことは可能なんでしょうか。まあ仮にできたとしても、今後Androidの起動プロセスが不変だと保証することができない以上、やるべきではないんでしょうけども。

---

そんなわけで、確実性に欠けるGeocoderクラスは使わない方がいいと思います。

代替として、Web APIとして用意されているThe Google Geocoding APIを使うことができます。

このGeocoding API v3の呼び出しをJavaから行えるラッパーライブラリ、Java API for Google geocoder v3も提供されているのですが、やっていることは結局maps/api/geocode/jsonを叩いて結果のJSONをパースするだけなので、自前で軽量な実装を書いた方がいいと思います。

なお、APIの使用制限として、2500リクエスト/日という制限が書いてありますが、その他に1リクエスト/1~2秒程度の制限もあるようで、2件以上の住所データを同時に変換しようとすると失敗します。もし複数件の住所を処理する必要がある場合、ExecutorService等で制御する必要があります。

2013年8月31日土曜日

LayerListを利用してカードUIを作る

カードUI(流行ってるの?)を作る手法はいくらか考えられるけど、
  1. Layout.xml上でカード風に見せるViewを定義する
  2. カードUI風の画像を用意する
  3. ShapeDrawableを利用する
  4. カスタムViewを実装してonDrawメソッドを実装する
個人的な考えとしては、1を選択するのは、ないなぁ。

とはいえ、本業がプログラマだと2のように素材をさくっと作ってしまおう!とはいかない。4が恐らく処理速度上でもっとも軽量で、カスタマイズ性も高いものの、ちゃんとしたカスタムViewを作成するには知識がいるので、一番お手軽なのは3の方法でしょうか。

Layer ListでShape Drawableを重ねれば、そこそこな見栄えのカードUIが作れると思います。

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
  <item android:id="@+id/shadow" android:top="1dp" android:left="1dp">
    <shape >
      <solid android:color="#868686" />
    </shape>
  </item>
  <item android:id="@+id/front" android:bottom="1dp" android:right="1dp">
    <shape >
      <gradient android:startColor="#FFFFFF" android:endColor="#EDEDED" android:angle="270"/>
      <stroke android:color="#DBDBDB" android:width="1px" />
    </shape>
  </item>
</layer-list>

xmlに上のように定義して、利用する際はbackgroundのdrawableとして指定するだけ。なお、色はあまり考慮していないのと、State Drawableとして定義していないので、実用はしないでください。


AndroidにはiOSのCore Graphicsのレイヤ操作に相当する機能がないので、もう少しリッチにドロップシャドウを実現したい場合、9patchを用意するか、4を採用して、onDrawメソッドで実装することになります。

---

ところで「9patch画像を作成したら、/drawable/ディレクトリに保存する」とは公式にもそう書いてあるのだけど、「だから9patch画像は一つだけ用意すればいいんだ」という誤解が生じているような気がする。(9patch画像は生の/drawable/に保存する、と書いてあるチュートリアルが異様に多い)

公式ドキュメントが指す/drawable/ディレクトリとは、リソース分岐(/drawable-hdpi/とか)も含めた全体を指していて、9patch画像もピクセル密度に応じて複数の画像を用意する必要がある

このことはAndroid SDKをインストールしたディレクトリの、platforms/android-XX/data/res内を見れば分かります。Androidはボタンなどで9patchを多用しているのですが、ピクセル密度に応じた9patchを用意しているのです。

---

/drawable/に突っ込んだ9patch画像がなぜ問題かというと、実際には/drawable-mdpi/か何かとして認識されているから、例えばNexus 7などの中途半端なピクセル密度で表示されてしまうと、まず画像の本体部分が約1.3倍に引き伸ばされた後、9patchの指定位置ピクセルに補正が掛かるため、ここでズレが生じて破綻するケースがある。

それはまだマシなケースで、画像の拡大縮小の結果、9patchとして違法となった場合、getNinePatchChunk()でnullが返却され、Runtime Exceptionでアプリが強制終了することになる。

もっとも、ピクセル密度に応じて引き伸ばし位置を定義するのは、画像が複雑になると非常に面倒ですし、引き伸ばしだけでタイルパターンには使えないですし、デザイナーの方に9patchの詳細な仕様(Android開発者も知っているか怪しい)を伝える手間もありますし、なかなか難しいところです。

2013年8月30日金曜日

Effective Androidを読んでる。

「Effective~」というタイトルから、「デザインを定義するならばThemeを利用し、レイアウトと分離せよ」的な、格言めいた実用系ノウハウが載っているんだろうと思っていたんだけど、1章でいきなりその辺を外部の書籍に投げていて(というより自著の宣伝か)、出オチ感が…。

「すぐに使えるテクニック集」と「その辺に転がってるチュートリアル」と「マニアックな知識」が玉石混交で、ネイティブアプリ開発者もWeb系開発者もデザイナーもごった煮で対象としていて、ノリとしてはAndroid関係者向けの専門誌のような感じの本でした。

以下は、読んでて「う~~ん…」とか、「うおー!」ってなった部分。

DP値を実行時に取得するならば、dimension resourceで名前をつけて取得しよう。

<resources>
    <dimen name="card_padding">8dp</dimen>
</resources>
getResource().getDimension(R.dimen.card_padding);

Androidはレイアウトとスタイルとコードを分離できるとはいえ、コード上でdpとpx間の相互変換を行うこともよくある。ユーティリティメソッドを用意してもいいけれど、dimension resourceを使用し、dp値に名前をつけて管理した方がソースの可読性も高く保つことができる。

dimension resourceとリソースのバージョン分岐を組み合わせると、端末のフラグメンテーション対策にも応用できる。dimension resourceはもっと普及されるべきだと思うんだけど、なぜかプッシュする人が少ない印象…。何かトラップでもあるのだろうか。

AndroidでレイアウトをするならMetrics and Gridsは読もう。

最近Illustratorなども触っているので、3章のデザイナー視点で使いやすいpxなどを述べたお話は、面白い視点を提供してくれたのだけど、もうちょっと掘り下げて欲しいなという部分もあった。

  • 現実としてAndroidの案件の多くは、iOSとのマルチや移植であり、UITableViewCellが44ポイントな限り、3で割れない素材との戦いは終わらない。44ポイント(44px&88px)の素材に対してどう対抗すればいいのか?
  • さらにtvdpi(というかNexus 7)の登場せいで3の倍数で用意すれば万事解決、というほど単純でなくなってしまった。
  • dpに対してspが存在する説明がゼロだけど、spはユーザーがフォントサイズに対する多少の自由度を得るためのものだから、pt固定値素材を用意してしまうのは、ユーザービリティの低下に繋がるデメリットがあることも説明すべきでは。
  • iOSのユーザーインターフェースガイドラインほど厳密ではないものの、Androidにも類似したデザインガイドラインは提供されている。特にMetrics and Gridsは全ての開発者が読むべき。48dpのリズム、8dpの「mind the gap」(この言葉遊びのセンスが素晴らしい)は知っていて当然であって欲しい。

The AndroidManifest.xml Fileに一度は目を通そう

章が進むごとに内容が濃くなっていって、5章辺りからすごく面白くなる。

ただ、「AndroidManifestを活用しよう」というタイトルとは裏腹に、マニアックすぎて実用的か疑問符をつけたくなるような、AndroidManifest.xmlのマイナー設定の解説記事なのがもったいない。

著者的にはリファレンスに書いてあるからそれを読めば分かるでしょ?ってことなのかもしれないけど、現実的にはclearTaskOnLaunchとかnoHistoryとかの存在を知らずに、コード上で実装してるのをちらほらみたことがある。

実際、リファレンスは長い上に意味が通じない箇所もちらほらあるし、真面目に読むと非常に疲れるからね。

それでも、一度でいいからThe AndroidManifest.xml Fileを通して見るべきだと思う。

流し読みで「こんな設定があったのかー」と知るだけでも十分。それだけでもAndroidの可能性が広がったように感じる。まあ錯覚で終わると思うけど。

Eclipseの設定ファイルが何をやっているのか理解しよう

6章ではEclipseの設定ファイルに関する解説を行っているのだけど、これは必見だと思う。

Androidのプロジェクトをバージョン管理して、多人数で開発する方法論として、自分レベルのヌルい知識で語ると、
  • Eclipse関連のファイルをcommitせず、pullした純粋なAndroidコードをExisting Android Code Into Workspaceでインポートする
  • .projectや.classpathもバージョン管理の対象とした上で、eclipse import existing projects into workspaceでインポートする
の二通りが思いつく。

前者はpureなAndroidコードのみをバージョン管理の対象とするため美しく思えるけど、この方法はまずプロジェクトの規模が大きくなるとまず破綻する。

複数のプロジェクトが絡んで、class pathが入り乱れているような場合、Eclipseの設定ファイルが何をやっているのか理解していないと、自力でビルドパスを解決できずにハマったりするからだ。

幸いなことにEclipseの設定ファイルは人間にも読みやすい形式となっていて、マージツールで差分を取ることで原因追及することはできた。この記事にもう少し早く出会えていれば…。

gcm.jarはdeprecatedなので、Google Play Service APIを使いましょう

11章の内容はやや古い(7月ごろにGoogle Cloud Messaging APIの一部刷新があったのですが、反映されていない)ため、公式のものを読んだ方がいいです。有志の方による日本語翻訳も最新仕様には対応してません。

また用語も公式に定義されているものと対応していない(Registration IDをIDと大雑把に扱ったり、Sender IDをproductIDと呼称するなど)ので、技術書としても微妙な内容と思います。

結論.Theme ResourceとかFragmentの活用とかそういう話がもっと欲しかった

Android開発者の標準レベルって、流行り始めた2.3とかで止まってるような気がする。

巷には基本中の基本しか触れていない入門書が多いし、面白い本も何冊か知ってはいるけれど、進化スピードの速いモバイルの世界ではすぐに陳腐化してしまう。「先」を目指すとなると、英語の公式ドキュメントを読み漁るしかない。

「Effective~」系書籍は小手先のテクニックじゃなくて、フレームワークを十全に引き出すエッセイ集のようなイメージがあって、しかも同人というスピード感が加われば、「先」を切り拓いてくれるんじゃないかという期待があった。まあ勝手にイメージを押し付けてるだけなんだけど。

例えば、Activityと密結合を回避するための様々なテクニック、モデルクラスを作成することでActivity非依存のテストコードを書けるようにするとか、AsyncTaskをAsyncTaskLoaderにリプレースするとか、AlertDialogよりもDialogFragmentを好むべきだとか、それで全て上手くいくわけではなく、メリットもあれば落とし穴もある…。

…そういうノウハウが載っているのかな~と期待して買ったので、満足はできなかったけど、一部のマニアックな話については面白かった。増補改定にも期待。

2013年8月25日日曜日

iOS開発とAndroid開発の比較

iOS開発 vs. Android開発

という記事を読んで面白かったので、両端末開発が可能な(しかし最近は.NETが面白いので仕事以上では触っていない。iOS7についても少し調べる時間を作らないと…)自分も何か書いてみる。

iOS開発


最初に覚えることは多い。Objective-C、Cocoa Touch API。一般的な入門書で扱っている知識はここまでだけど、iOS Developerを名乗るのであれば、blocksやGCDなどモダンな知識、MRCやObj-C2.0以前のKVO、KVCなどレガシーな知識、Cocoa Touchの土台であるCore系技術についても習得する必要がある。

Core Imageは現状ではiOS端末で実用的な動作を実現できないのでどうでもいいとして、最低限Core GraphicsとCore Animationは覚えたい。Core Dataを覚えればフレームワークの性能を引き出し、iCloudとの連携についても取り計らってくれるのだけど、Core Dataフレームワークを理解するのは結構しんどいので、業務上で使うときには、FMDB経由でSQLiteを使うという方法が安牌かもしれない。

まあとにかく覚えることは多いけど、言語として面白いのでそれほど苦にならなかった。上の記事ではドキュメントの質が低いとあるけれど、確かにiOSのドキュメントはあまり整備されないのでバージョンがバラバラなドキュメントが入り乱れていて、困るときもあるのだけれど、Class Referenceに関して言えばAndroid DevelperのJava Docよりも充実していると思う。Discussionに何度助けられたことか。

ネイティブレベルの英語力があれば、読めるドキュメントの幅も広がるのでまた見えるものが違うのかもしれない。

Interface Builderはデザイナーとしては使えない(特にUIAppearanceが実行時にしか反映されないのは厳しい)けど、「オブジェクトを配置して相関を定義してシリアライズするもの」として割り切れば、「使い物にならない」と切り捨てるほどでもないと思う。ただ4インチ対応が必要になって以降、Auto Layout関連の挙動が直感的じゃないので、ひどく使いにくくなったのは事実。

Android開発


Javaの知識がある程度(Effective Javaが読める程度)あれば、Androidフレームワークを齧りながらでも開発ができるので敷居は低い。

しかしiOSのように低レベルフレームワークにアクセスすることができないのは致命的な弱点でもあって、例えばAndroidにはCore Textに相当するような低レベルで高品質なテキストレンダリングエンジンがないので、Androidの電子書籍アプリは端末によって見栄えの違いが生じたり、動作がもっさりしていたりと総じて質が低くなってしまっている。

「Androidといえばフラグメンテーション(断片化)問題」と思い込んでいる人が多いけれど、実際のところ多画面、多バージョン対応用向けの機能は充実していて、リソース分岐や9patchを駆使することができれば、遥かに多種多様なデバイスに対応しなければならないWebサイトよりも、ずっとずっと楽だと思う。

この辺り、Androidフレームワークを理解した上でそれに合わせて多画面でも動作するアプリケーションを作る、というやり方をせずに、固定サイズの画面に向けた既存のデザインをAndroidに流用しようとするから無理が出るだけじゃないのかなぁと思っている。

Androidの画面作りは、Webの画面作りに似たところがあって、まずボタンなどのコンポーネント見栄えをThemeリソース(CSSに相当)で定義して、それをXMLで定義していくというボトムアップなアプローチが向いている。GUIのレイアウトエディタは実際の編集に使うのではなく、実機出力のプレビューとして使う方がいい。Android Studioで実用的な速度で使えるようになったけど、XMLでの書き方を覚えた方が効率的なビューを作れる。

そういえば昔はエミュレータでしか階層ビューワが使えなくて、そのためだけにエミュレータを使っていたのだけど、今では実機でも使えるようになったので久しくエミュレータを起動していない。

長期的視点


Androidは2から4でまるで別物のように生まれ変わったけど、開発者視点ではJDK7への対応が異様に遅かったり(そういえばGAEもなぜかJDK7対応は異様に遅かった)や、JUnitはいまだに3で書かないといけないし、進化はのんびりしている。

それでいて、バックポートライブラリが提供されるため、4系で新規に追加された機能群を2系に対応することができる。遅すぎる感はあるものの、ようやくAction Barも対応されました。

このためAndroid 2.3で作成したアプリを4で動かすこともできるし、4向けAPIであるAsyncTaskLoaderやActionBarを利用したアプリを作っても、2系でも起動することができる。

対するiOSはバージョンが変わる度にAPIの内部仕様が目ぐるましく変化し、言語の構文や規則までもが変わるといったことがままにある。このためiOSの開発者で居続けるためには、常に最新の情報にアップデートしていく必要がある。

「ユーザーの端末もほとんどが最新のものに切り替わっていくので、iOSにはフラグメンテーションの問題はない」みたいな話を良く聞くけど、それは環境などによる。iOS4以前に作られたレガシーなフレームワークを利用したいときもあるし、流行り始めたiOS4時代のAPIで書かれたアプリをiOS6~7に対応せよというメンテナンス案件やら、サポートするリスクリターン考えずにiOS4からの対応にせよという命令も多いので。

このため長期的な保守については、現時点ではAndroidの方が圧倒的に楽だと思う。もっともiOSも成熟期に入り、今後マルチタスク化などの大きなパラダイムシフトはないと思うし、XCode5でテスト環境が充実していくという話もあるので、今後の見通しは分からないけれど。

2013年7月26日金曜日

AndroidのSSL通信をキャプチャする方法

Burp ProxyCharlesなどのリバースプロキシを使いましょう。

リバースプロキシとは、外部サーバーへの接続を肩代わりするプロキシサーバーです。

http通信ならば、これらのプログラムを立ち上げればそれで通信をキャプチャすることができますが、SSL通信では一手間必要です。サーバーとクライアント間でお互いに承認した乱数(マスターシークレット)を用いて暗号化するため、中間に入って通信を傍受することはできないからです。

そこでリバースプロキシのCA証明書を入手し、端末に信頼できるルートCAとして登録する必要があります。

Charlesの場合、FAQにiPhone用の証明書ファイルへのリンクがありますので、Androidのブラウザでアクセスしてインストールします。iPhone用と書いてありますが、Androidでも問題なく使えます。

Burp Proxyの場合、Burp Proxyが生成した証明書を取得する必要があります。

Androidの標準ブラウザやChoromeでは証明書の取得を行うことができないので、適当なデスクトップ用ブラウザを仕様します。Burp Proxyを起動->ブラウザ(OS)でプロキシサーバを設定->ブラウザでhttpsのサイトへアクセス->ルート証明書をエクスポート(firefoxの場合、「接続の安全性を確認できません」という警告が出るので、「危険性を理解した上で接続するには」->「例外を追加...」をクリックします。「セキュリティ例外の追加」ダイアログが表示されるので、証明書の状態の「表示(V)...」をクリックすると、証明書の詳細が表示されます。ここでルート証明書となっているBurp Suite PortSwiggerを選択し出力します。拡張子が付いていない場合.cerを付けます)で、証明書を取得することができます。あとは証明書をメール等でAndroid端末に送付してインストールします。

CA証明書のインストールは、SDカードのルートに保存(adb push hoge.cer /mnt/sdcard)して、「設定」->「セキュリティ」->「SDカードからのインストール」からでもできます。

CA証明書のインストールができたら、Macの設定から「インターネット共有」をONにしてWifiスポットにし、AndroidのWifi接続先をMacにします。そしてWifi接続の設定からプロキシを手動で追加し、Macのアドレスと8080番ポートを指定します。(同一のネットワークセグメントに接続されていれば、「インターネット共有」を使う必要はありません)

なおWifi接続の設定には、「HTTPプロキシはブラウザで使用されていますが、他のアプリでは使用できません」というような説明が書いてあるかと思いますが、実際には普通にアプリの通信でも有効です。(説明が間違っているのか、端末依存があるかどうかは分かりませんが、手元のAndroid4.0以降の端末だと可能です。httpClientのProxy設定を変更することでそれ以前のAndroidでも可能です)

---

リバースプロキシを使えば、通信内容をキャプチャするだけでなく、いろいろとやれます。
  • リクエスト内容を書き換える。
  • 特定の通信処理のみをInterceptすることができる。例えば「通信Aが成功したらその結果を使って通信Bを行う」といった処理で、通信Bが失敗したときの挙動を確実にテストできる(タイミングよくWifiを切る、とかそういう原始的な方法でテストしている人も世の中にはいるらしい)
  • CharlesのMap Localを使うと、レスポンスとしてローカルファイルを返すことができる。本番環境サーバーからデータを取得する処理を、ローカルファイルに切り替えることで、コードを変更せずにテストデータの変更を行える。

2013年7月21日日曜日

Java FX2のアニメーションをラムダで書き直してみる

Java FX2 を触ってみた。の続き。

Java8のラムダを使って記述したら、確かにTimelineにKeyFrameが並び、アニメーションが順番に実行される、という流れが非常に分かりやすくなりました。コード量も約半減。

@FXML
private Label resultText;

private TranslateTransition slideInAnimation;
private FadeTransition fadeInAnimation;
private FadeTransition fadeOutAnimation;

final private Timeline animation = new Timeline(
 new KeyFrame(Duration.seconds(0),e -> slideInAnimation.play()),
 new KeyFrame(Duration.seconds(0),e -> fadeInAnimation.play()),
 new KeyFrame(Duration.seconds(2),e -> fadeOutAnimation.play())
);

@Override
public void initialize(URL arg0, ResourceBundle arg1) {
 slideInAnimation = TranslateTransitionBuilder.create()
  .node(resultText)
  .duration(Duration.seconds(0.4))
  .fromX(100.0)
  .toX(0)
  .interpolator(Interpolator.EASE_OUT)
  .build();

 fadeInAnimation = FadeTransitionBuilder.create()
  .node(resultText)
  .duration(Duration.seconds(0.5))
  .fromValue(0)
  .toValue(1)
  .build();

 fadeOutAnimation = FadeTransitionBuilder.create()
  .node(resultText)
  .duration(Duration.seconds(0.5))
  .fromValue(1)
  .toValue(0)
  .build();
}

private void showMessage(String text){
 animation.stop();
 resultText.setOpacity(0);
 resultText.setVisible(true);
 resultText.setStyle("-fx-text-fill:#606060");
 resultText.setText(text);
 animation.play();
}


あとnew Duration()ではなくて、Duration#seconds()の方が分かりやすいね。

ついでにアニメーションをinitializeのタイミングで一度だけ生成するように修正するとともに、TransitionBuilderを利用するように修正しました。

しかし、JDK1.8にした際にJava FXも同梱のものに変えてみたのですが、TransitionBuilderはDeprecatedになっているようです。どうもjavaFX8のドキュメントを見るとBuilder系のクラスが軒並みDeprecatedになってるようで、次期バージョンでは消えるらしいのですが、理由が見当たらない…。

Java FX2 を触ってみた。

JavaのGUIといえば、貧弱なSwingの印象が強すぎるのですが、JDK1.7からはJava FX2が非常にプッシュされていまして、こちらはOSネイティブ描画を利用するので、ちょっとしたツールにGUIを付けたいといった場合に非常に便利となりました。

JDK1.7u2ころには、Java FX2を別途インストールするよう求められていたと思うのですが、最近では自動的にバンドルされるようになったようで、インポートするライブラリの場所が変更されていることに注意です(Program Files/Oracle/Java FX 2.0/以下ではなくて、Program Files/Java/jdk1.7.0_XX/jre/libに変更)。そのせいでしばらくは勘違いしてJava FX2.0を弄っていました。

よかった探し

Swingと比較すると記述が非常に楽です。

GUIはXMLで記述しますが、GUIビルダーも用意されています。このGUIビルダーでViewを作り、Controllerクラスを設定すれば、自動的にGUIやアクションがbindされるため、お約束的なコードを書く必要がほとんどありません。

Modelクラスのロジックとの関連付けを、Controllerクラスに少し書くだけでGUIアプリケーションが作れてしまいます。Android Annotationに近い感じでしょうか。

見栄えの変更にCSSを使用できるのも、非常に面白いと思います。.NETのXAMLにしても、AndroidのThemeResourceにしても、標準UIから外れたデザインを作ろうとしたときに、独自記法を学ぶコストが必要になってしまうのに対して、FXMLではWeb標準となっているCSSの知識を流用できます。

イケてないところ

標準UIコンポーネントの貧弱さはSwingから退化してさえいます。FileChooserやDirectoryChooserはOSネイティブのものが利用できるようになったのに、確認警告のダイアログは自作しなければいけないちぐはぐさがよく分かりません。

Labelは単行しか表示できません。改行文字を使って2行に見せること自体はできますが、長い文章を折り返すような処理ができないのです。それでいながらWindowsとMacでそれぞれ起動したときに、Systemフォントの10ptの大きさが違って困りました。これはレイアウトの組み方次第でどうにかなる範疇かもしれませんが…。

最後にデプロイ方法に難を抱えています。JRE7がインストールされている環境はまだ少ないでしょう。Java FX2.2でネイティブデプロイを実現する方法自体は提供されたのですが、その方法というのがパッケージ内にJRE7をまるごと突っ込むという力業で、当然ながらファイルサイズが膨れ上がってしまいます。

微妙なところ

例えば「フェードイン+フレームイン」のアニメーションを実現したいとき、FadeTransitionとTranslateTransitionの複合アニメーションを実行する手段がないので、それぞれのアニメーションをキーフレームアニメーションとして実行することで、擬似的に複合アニメーションを実現しています。

iOSのblocksの記述や、AndroidのAnimationResourceに慣れているのもあると思うのですが、この記述はものすごく気持ち悪い…。

例えば以下は50ミリ秒後(0ミリ秒後だとうまくいかない。原因調査中)にフェードとスライドでテキストを表示して、2000ミリ秒後にフェードアウトさせる、といったコードです。覚えたてで、雑に書いてるのもあるのですが、それにしても無意味に冗長な感があります。

    private Timeline mLastTimeline = null;

    private void showText(String text){
        if(mLastTimeline != null){
            mLastTimeline.stop();
        }
        resultText.setVisible(true);
        resultText.setOpacity(0);
        resultText.setText(text);
        mLastTimeline = new Timeline(
                new KeyFrame(new Duration(50), new EventHandler<ActionEvent>(){
                    @Override
                    public void handle(ActionEvent action) {
                        slideAnimation();
                    }
                }),
                new KeyFrame(new Duration(50),new EventHandler<ActionEvent>(){
                    @Override
                    public void handle(ActionEvent action) {
                        showAnimation();
                    }
                }),
                new KeyFrame(new Duration(2000),new EventHandler<ActionEvent>(){
                    @Override
                    public void handle(ActionEvent action) {
                        hideText();
                    }
                })
                );
        mLastTimeline.setOnFinished(new EventHandler<ActionEvent>(){
            @Override
            public void handle(ActionEvent arg0) {
                mLastTimeline = null;                
            }
            
        });
        mLastTimeline.play();
    }

    private void slideAnimation(){
        final TranslateTransition translate = new TranslateTransition(new Duration(500));
        translate.setNode(resultText);
        translate.setFromX(100.0);
        translate.setToX(0);
        translate.setInterpolator(Interpolator.EASE_OUT);
        translate.play();
    }

    private void showAnimation(){
        final FadeTransition fadein = new FadeTransition(new Duration(500));
        fadein.setNode(resultText);
        fadein.setFromValue(0.0);
        fadein.setToValue(1.0);
        fadein.play();
    }

    private void hideText(){
        final FadeTransition fadein = new FadeTransition(new Duration(500));
        fadein.setNode(resultText);
        fadein.setFromValue(1.0);
        fadein.setToValue(0.0);
        fadein.setOnFinished(new EventHandler<ActionEvent>(){
            @Override
            public void handle(ActionEvent e) {
                resultText.setText("");
                resultText.setVisible(false);
            }
        });
        fadein.play();
    }

可変長引数のKeyFrameでアニメーションタイムラインを構成する、という方法論自体は悪くないと思いますし、Flashなどに馴染みがあるとむしろとっつきやすいと思います。この辺、JDK1.8のラムダを使えばもう少しマシになるかもしれませんけど…。

同じようなことをiOSでやろうとするとこんな感じかな。
 
self.resultText.text =@"text";
self.resultText.hidden = NO;
self.resultText.alpha = 0.0f;
self.resultText.transform = CGAffinetransformMakeTranslation(1.0f, 0.0f);
[UIView animateWithDuration:0.5f
                 animations:^{
                     self.resultText.alpha = 1.0f;
                     self.resultText.transform = CGAffineTransformIdentity;
                 }
                 completion:^(BOOL finished){
                     //dispatchAfterとかで1.5f秒後にラベルを消す
                     //最初からCoreAnimation.frameworkを使ってキーフレームアニメーションで実現してもいい
                 }

…うん。

結局どうなの

Swing、Java FX1.0の失敗を踏まえつつ、ネイティブの使い心地を実現した、使い勝手のよいUIフレームワークになっているようには思います。Modelロジックに集中することができるため、ちょっとした開発支援ツールを作るのには最適だとすら思っています。

ただし少なくとも国内ではマイナーで終わると思うので、まず業務で使える可能性は低いでしょうし、英語圏の情報をガンガン収集して第一人者になってやるぜ!ってやる気がなければ手を出さない方がいいような気がひしひしとしてます。

次は何に手を出そうかな。

2013年4月3日水曜日

iPadのSplit Keyboardに関するあれこれ。

iOS5以降存在するSplit Keyboard(キーボードをスクロールさせると分割するアレ。以下分割キーボードと呼称する)はいろいろ厄介なんだけど、あまりその辺を語っているのを見かけない。

まず、分割キーボードはShowNotificationを通知しない。さらに固定キーボードから分割キーボードに移行すると、HideNotificationを通知する。つまり単純にShowとHideのNotificationでキーボードの表示/非表示判定する方法は使えなくなった。この辺までは常識。

分割キーボードか固定キーボードかを判定するには、Show/HideのNotificationのuserInfoに格納されている、UIKeyboardFrameChangedByUserInteractionの値を利用する。この値が1であれば分割キーボード、0であれば固定キーボード。

しかし「最初から分割キーボードで表示される」場合はそもそもShowNotificationの通知が発行されないため、上記の方法で「出現したキーボードが分割キーボードである」と判定することはできない。(HideNotificationも固定→分割に切り替わったときだけで、最初から分割キーボードとして表示されるときには発行されない)

ではどうやって「分割キーボードが表示された」ことを取得すればいいのか。

それはFrameChangeNotificationを利用することで実現できる。この通知はキーボードのframeに変化が発生した際(表示時/消去時/分割時の移動時)に発行されるもので、iOS5で追加された。このため、iOS4以前との互換性が必要な場合はNSNotificationCenterの購読/解除の処理でバージョン判定を行わなければならない。

また、FrameChangeNotificationは状況に応じてuserInfoに格納する値が変化する点に注意。

Show、HideNotificationと同時に呼ばれたときは、UIKeyboardFrameChangedByUserInteractionを含めた多くの情報を持っているので、UIKeyboardFrameEndUserInfoKeyと合わせることで、キーボードの状態を把握することができる。

単体で呼び出される場合(=分割キーボードの位置変更時)には、will/didそれぞれ一つのframe値のみが格納されている。

  • UIKeyboardFrameChangedByUserInteractionが0であれば固定キーボード、1であれば分割キーボードであると判定。
  • UIKeyboardFrameEndUserInfoKeyのframeが、self.view.windowのframe内に含まれるかを、CGRectContainsRect関数で判定する。キーボード出現/分割キーボード移動であればYES、キーボード消去であればNOが返る。
  • iOS5未満にも対応するのであれば、バージョン判定を行ってiOS5以上はFrameChangeNotificationを、iOS4以下はShow/HideNotificationを受信し、それぞれに処理を書く。
という感じでシンプルに解決できそう。

なお、private API上等ならば、self.view.window.firstResponderをキー値監視してしまうのが手っ取り早い。

2013年3月31日日曜日

カスタムビューを作る際に重要なonMeasure()とは?

  • 親側やレイアウトxmlの属性で、任意の大きさを指定して、その大きさに応じて描画させるにはどうしたらいいのか?
  • onMeasure()で指定したとおりの大きさになっているのに、onDraw()メソッドの引数canvasのgetWidth()やgetHeight()の返す値と一致していないのはどういうことなのか?
みたいな話を書く予定が、前提のonMeasure()の内容が長くなりすぎたのであった。

androidでカスタムなViewを作りたいときは、Viewを継承したカスタムクラスを作って、onDraw()に描画内容を記述すればいい。ただそのままだと親のViewGroupのサイズをフルに占有しようとしてしまうので、onMeasure()をオーバーライドして、自身のViewが描画したいサイズを指定する。

その辺の話までは前提知識として持っているものとする。

---


onMeasure()とは何なのか?

それを理解するには、androidがビュー描画する際に行っている処理を理解するのが手っ取り早い。公式のHow android Draws Views日本語訳)にその説明がある。

androidのビュー描画は計測フェーズとレイアウトフェーズの二段階に分かれていて、このうち計測フェーズでルートとなるViewは自身のビュー階層の大きさを計測するメソッドmeasure()を呼び出す。

このmeasure()メソッドそのものはViewクラスでfinalメソッドとして宣言されており、この実装をサブクラスであるカスタムビュー側で変更することはできない。measure()メソッド内で呼び出されるメソッド、onMeasure()をサブクラス側でオーバーライドすることで、自身の大きさをmeasure()メソッドに通知するのである。

その通知方法はsetMeasuredDimension()に自身の大きさを渡すという方法を取っている。setMeasuredDimension()で渡した値はViewに保持され、getMeasuredWidth()またはgetMeasuredHeight()で取得できるようになる。(measure()内でsetMeasuredDimension()が呼び出されなかった場合は、例外が発生する)

最後にonMeasure()のsuper実装を呼び出す。これによって、必要があれば親クラス側でさらにsetMeasuredDimension()が呼び出され、値が更新される。

つまり、本当はsuper.onMeasure()はonMeasure()の最後に呼び出す必要がある(先頭で呼び出すと、親が設定したsetMeasuredDimension()の値を上書きしてしまうため。ネット上の日本語情報だとここを間違えてる人が多い)。

では子が設定したmeasuredDimensionの値を、親で更新すべきケースとは何だろう?それはbackgroundに大きさを持つdrawableを指定した場合だ。もしbackgroundのdrawableよりも、子がmeasuredDimensionで宣言した大きさの方が小さい場合、backgroundのdrawableの大きさを取得して上書きする。


…つまり、間違えてonMeasure()の冒頭で呼び出したところでほとんど実害はない。


しかし「backgroundに指定したdrawableの大きさが、Viewの最小の大きさになる」という性質は、知らないとレイアウトを組むときにハマる可能性があるので、覚えておいて損はない。


---

onMeasure()で重要なこととして、onMeasure()の引数として渡される値も、setMeasuredDimension()に渡す値も、単純なサイズではない、ということだ。

ここで引き渡す値は、サイズと制約条件を一つのintの値として保持したものである。上位2ビットに制約を、下位30ビットにサイズを保持している。

ビット演算によってサイズや制約条件を扱う必要があるが、幸いにもビット演算を使わなくとも変わりに処理を行ってくれるView.MeasureSpecクラスが存在するので、そちらを利用すればいい。

  • MeasureSpec.getSize(int measuredSpec);
    • measuredSpecから下位30ビットを取り出す(=サイズを取り出す)
  • MeasureSpec.getMode(int measuredSpec);
    • measuredSpecから上位2ビットを取り出す(=制約定数を取り出す)
  • MeasureSpec.makeMeasureSpec(int size, int mode);
    • 実はsizeとmodeを足しているだけ(=制約フラグ付きの値を生成)

setMeasuredDimension()は、サイズと制約定数を足した値を必要とする。ただしmakeMeasureSpec()と書いておいた方がソースコード上で意味も通りやすくなるだろう。

ところで、ネット上の日本語情報では、makeMeasureSpecを行わずにダイレクトにsetMeasuredDimension()に値を渡しているものもある。実はそれでも動く。

なぜなら、制約条件の定数の一つUNSPECIFIEDの値が0x00000000なので、そのまま値を渡した場合でも、「上位2ビットが0だからUNSPECIFIEDだ」と解釈されるだけだからだ。


しかしそれはMeasureSpecの存在を知らないのか、知った上で意図したものなのか区別が付かないので、明示的にMeasureSpec()で値を生成した方がより良い実装だろう。


---

ここで二つ新しい疑問が出てくる。

  • onMeasure()の引数として引き渡される値は何を意味するのか?
  • 制約条件とは一体何なのか?
onMeasure()の引数の値も、サイズと制約条件を保持したintの値なので、先に制約条件について説明する。この制約条件は計測モードなどとも呼ばれ、次の3つの値を取る。
  • AT_MOST
    • at mostは「多くても」「~以下」という意味。親からこの制約条件で値を受け取った場合、それより大きい値を指定してはいけない。
  • EXACTLY
    • exactlyは「ぴったり」という意味。親からこの制約条件で値を受け取った場合、子はこのサイズに合わせなければならない。
  • UNSPECIFIED
    • unspecifiedは「指示していない」という意味。つまり制約条件として何も指定されていないので、子は自身の好きなサイズを宣言することができる。
onMeasure()は引数として渡されるのは、親のサイズと制約である。

つまり本当は渡された制約条件を判定し、AT_MOSTやEXACTLYが指定された場合には、親のサイズに収まるようにビューの大きさを宣言しなければならない…が、自身のアプリ内で使うカスタムビューであれば、ほとんどの場合は制約条件を無視してUNSPECIFIEDとして大きさを宣言するだけで十分だろう。

また、必要があればここでレイアウトパラメータを考慮して大きさを宣言することもできる。デフォルトの実装ではそういうことを全く行っていない。そのため、自作したカスタムビューは(backgroundDrawableを指定しない限り)、親のViewGroupのサイズをフルに専有しようとする。

---

onMeasure()は複数回呼ばれることがあり、これがビュー描画速度を落とすボトルネックとなる場合がある。

例えばlayout_widthを0にして、weightでレイアウトしている場合。親ビューは子ビューのサイズを知る目的で、UNSPECIFIEDでonMeasure()を呼び出し、もし子ビューの総サイズが親ビューよりも大きくなった場合、AT_MOSTで子の取れる最大サイズを指定した上でもう一度onMeasure()を呼び出す。

レイアウトが入り組んでいる場合、再帰的にこの処理が行われてしまう。

そこでビュー描画処理を高速化するのに、相対レイアウト(Relative Layout)が良いと言われる。相対レイアウトは相対的に位置を決定するだけなので、計測フェーズが一度しか行われないことが保証される。

しかしレイアウトXMLエディタが絶望的に使いにくい限りは、LinearLayoutを選ぶよね…。


---

んで、冒頭の疑問なんだけど、setMeasuredDimension()で自身のサイズを宣言して、実際にそうレイアウトされていても、端末によってはonDraw()の引数canvasからwidth()やheight()で値を取ると一致しないケースがある。

結局view自身のgetWidth()やgetHeight()を使えば正しい値が取れるので、それで解決したのだけどスッキリしない。

canvasのwidth()やheight()の値を正しい値で更新するにはどうしたらいいんだろう。そもそもcanvasのwidthやheightが何を参照しているのかについてコードを見てみたものの、CanvasはC++のSkiaライブラリで書かれているので、ちょっと保留。

2013年3月26日火曜日

「OpenGL ES 2.0 Androidグラフィクスプログラミング」読んでる。

「初めてのOpenGL ES」は読んだことがあったんだけど、2.0系についても理解しておこうと思って購入。

どうせ最近のAndroidだとCanvasを使ってもOpen GLに変換して描画してくれるっぽいし、本格的に3DやるならUnityとかのゲーム向けライブラリ使う方がいいんだろうしで、OpenGL ESを直接学ぶ必要性があるのかよく分からないんだけど、まぁ趣味的に。

OpenGL関係の翻訳本などを手がけている著者だけあって、用語がちゃんとしてて、説明もこなれている感じがある。

ただこの本、プログラマブルシェーダーの初期化等を独自のUtilityライブラリに回しており、その部分に関する説明がバッサリ飛ばされているので、後半の章を読むまで分からない。

そのUtilityライブラリを手に入れるには、会員登録必須のサイトに有効期限のあるダウンロードコードを入力するという手続きを踏まなければならないので、そうしないとUtilityライブラリがブラックボックスのせいで写経して動かすことすらできないのが、なんだかなーって感じ。

面倒な手続きをひとまず隠蔽することで、動くものを作り、それを改造して学んでいくというモチベーションを保ちやすい学習スタイルは入門書として凄くいい本っぽいんだけど、個人的には「おまじない」というマジックワードで濁されて、ブラックボックスのままにされてしまうとそっちが気になってしまう。この辺は趣味の問題かな。

---

ところでOpenGL ES 2.0の本を読んでいて気になるのは、2.0はGL20クラスのstaticメソッドの利用が中心になっているのだから、静的import使えばいちいちGL20.~って打たなくていいし、コードの見栄えがC言語に近くなっていいと思うんだけど、なぜかそう書いてるケースが見当たらない。なんでなんだろ。

GL10(ES 1.0)とメソッド名が同じ(1.0はインスタンスメソッドだから区別は付くとはいえ、非常に紛らわしいことに違いはない)とか、静的importを使うことによって変数が汚染されてトラップに嵌るとか、なんかいろいろあるのかもしれないけど…。

2013年3月15日金曜日

blocksのclosures的特性を使ったGCDキャンセル法。

例えばゲームの当たり判定処理と画面描画処理のように「必ずこなさなくてはならないタスクと、処理能力によってはスキップすべき重いタスク」がセットになっている場合や、リアルタイム検索のように「ネットワーク検索タスクを実行するたびに、古いタスクが陳腐化してキャンセル処理が必要になる」などのケースで微妙に使えそうな気がします。

-(void) queue{
  static u_int32_t token;

  @synchronized(self) {
    token = arc4random_uniform(UINT32_MAX);
  }

  u_int32_t itsToken = token;

  dispatch_async(_serialQueue, ^{

    //タスク開始までにtoken値が更新されていれば、それ以上処理しない
    @synchronized(self) {
      if(token != itsToken) return
    }

    //重いタスク
    [self doHeavy];

  });
}

上記のqueueメソッドは呼び出す度に、静的ローカル変数であるtokenの値を、arc4random()関数を使ってランダムな値で書き換えます。そしてその値をローカル変数であるitsTokenにコピーします。

その後、dispatch_async()関数で、直列キューに対して何らかのタスクを与えます。

dispatch_async()に渡されたblocksは、コンテキストをキャプチャするときに、静的ローカル変数であるtokenは参照コピーをして、一方__block修飾子もないただのローカル変数であるitsTokenは、constとしてコピーします。

結果として、このblocksはtokenの値が更新されたかどうか(=キューに新たなタスクが積まれたかどうか)をitsTokenとの比較によって知ることができるのです。

応用例1

再描画命令など、タスク同士の実行間隔に時間の制約を付けたいときに、ローカル静的変数に最終処理時間を保持しておくとスマートに記述できます。

-(void) doOneSecondInterval{
  static NSTimeInterval lastProcessDate;
  
  dispatch_async(queue, ^{
    NSTimeInterval now = [[NSDate date] timeIntervalSinceReferenceDate];

    //もし前回の処理から1秒未満ならリターン
    @synchronized(self) {
      if(lastProcessDate != 0 || now - lastProcessDate > 1) return;
    }

    [self doProcess];

    //最終処理時間を更新
    @synchronized(self) {
      lastProcessDate = [[NSDate date] timeIntervalSinceReferenceDate];
    }
  });
}

ブロックにキャプチャされたstatic変数は、__blocks修飾子がなくても読み書きが可能です。static変数は0で初期化されることを利用して、初回実行時には必ずdoProcessメソッドを呼ぶように判定を書く必要があります。

応用例2

例えば、データベースなど排他制御が必要な資源に対して、インスタンスの現在の状態を保存したいとき、stateSaveメソッドを呼び出した「その瞬間の状態」が保存されることが保証されて欲しいならば、コールスタックに一時コピーを作ってキャプチャさせるというテクニックが使えます。

-(void) stateSave{
  id instanceState = [[self.state copy] autorelease];

  dispatch_barrier_async(queue, ^{
    [databaseHelper writeData:instanceState];
  });
}

コピーされたオブジェクト、instanceStateはその場でautoreleaseされますが、ブロックにretainされ処理が完了するまでの生存が保証されます。そして、実際のブロックの実行時には既にself.stateが変更されていたとしても、キューがデータベースに保存するのはstateSaveが呼び出された瞬間のコピーなのです。


GCDとblocksの組み合わせは、思わぬ循環参照を招いて問題を起こすこともありますが、static変数、自動変数、__blocks修飾子変数、オブジェクトなどがそれぞれどのようにキャプチャされるのか、ということを理解して使いこなせば、非常に強力だと思います。

2013年1月21日月曜日

アプリのパケット解析をする

「パケット解析」と呼ぶと敷居が高そうに思えるけど、実際のところはネットワークプロトコルに関する、技術者としては標準的な知識さえ持っていれば、あとはツールの使い方を理解するだけ。

お手軽な割には、アプリ開発において絶大な威力を発揮するのでオススメです。

特にネットワークを使ったアプリでは、「ネットワーク環境が問題なのか、サーバに問題があるのか」「サーバが送るデータに問題があるのか、クライアントのパーサに問題があるのか」など犯人を絞り込むの作業が必要になります。

いずれも、通信内容を把握できていれば自明なことです。

実用的にもそれ以外でも覚えて損のないスキルセットです。
  • 「前任者が作った殺したはずの機能が実は生きていて、余計な通信を発生させていた」というような発覚しづらい厄介なバグを、通信内容を把握しておくことで解決できる。
  • WebViewの通信は、読み込んでいるURL程度ならすぐ分かるものの、XHRの通信まで完全に把握するにはアプリに小細工が必要だったりする。しかしパケットを監視すれば手を加えなくても一発でOK。
  • 自分の書いたコードと違って、キャプチャされたパケットは嘘を付かない。
  • 「通信内容に問題があるんじゃ?」と話題になったアプリの挙動を、自分の手で解析できる。ハッカー気分を味わえて非常に楽しい。

ツール

Wireshark

総合的なパケットキャプチャツールとして非常に優秀。ただし非常にとっつきづらい。

オライリーの「実戦パケット解析 第2版」が、Wiresharkの使い方からはじまり、トラブルを解析によって解決する事例、そして主要ネットワークプロトコルの入門解説まで解説していて、パケットの世界に入門したい人に非常にオススメできると思う。

Burp Proxy

WiresharkはSSL通信の中身を除くのがやや手間なので、MacでSSL通信をキャプチャするならこれがオススメ。(JavaアプリケーションなのでOSを問わずに動く)

http通信のリクエストとレスポンスが纏められるので直感的で、Wiresharkよりも見た目はとっつきやすい。ただしリバースプロキシツールなので、Burp Proxyが通信処理のボトルネックになるため、本格的な通信障害調査には使えない。

Fiddler2

Windowsを使っているならBurp ProxyよりもFiddler2が良い。同じリバースプロキシツールだけど、http/https通信のキャプチャにとどまらず、Webページを動的に書き換えるなどいろいろ面白いことができる。

もともとはMSのIEチームが開発したツールみたいだけど、IEの滅亡を願うWeb開発者はこのツールに対して複雑な感情に抱いてるに違いない。