2012/04/28

AWS Java SDK用のGuice Provider

AWS Java SDKに含まれるクライアントはすべてThread-Safeであり、クライアントを再利用すべきです。もちろん自前でClientを保持するクラスを書いても良いのですが、GuiceによるDIを使っているのであればProviderを定義するのがオススメです。

AWS SDKはもともとインタフェイスが定義されているので、そのインタフェイスに対してGuiceのProviderを定義し、共有クライアントをProvider内で管理します。クライアントを使う部分では、下記のように注入するだけなので、コードの見通しが非常に良くなります。

@Inject
AmazonDynamoDBAsync client;

もしくは

@Inject
Provider<AmazonDynamoDBAsync> clientProvider;

Provider自体はすぐに書けるコードなのですが、再利用性が高そうなのでgithubで公開しました。

参考URL

2012/04/27

Snowflake的なID生成方法

作っているプログラムでランダムなIDが必要となったため、ちょっとリサーチしました。久しぶりにJavaでコードを書いています。

UUID

完全に分散した環境で使用するのには、UUID(Universally Unique Identifier)が最適です。1IDあたり16バイトの容量が必要となる点をのぞけば、理想的なIDと言えると思います。ただIDは大量に使用されるので、16バイトというサイズはちょっと気になります。

Snowflake

SnowflakeはTwitterが使用しており、Apache Licenseで公開しているID生成方法です。ある程度分散した環境でも、1IDあたり8バイトの容量で利用できます。また非常に重要な特徴としてIDの先頭部分にタイムスタンプ利用しており、生成されたIDはある程度時系列に並びます。8バイト、つまり64bitを下記のように振り分けて使用します。

  • 先頭42bitにタイムスタンプ(ms)を割り当て、時刻を分散
  • 10bitのマシンIDを割り当て、ID生成を行うマシンを分散
  • 12bitのシーケンスIDを割り当て、同一時刻(ms)、同一マシンでのIDを分散
タイムスタンプに42bitしか割り当てないのは心もとないですが、基準時刻をシフトさせることで利用できる期間をのばしています。

参考

Snowflake的なID生成方法

Snowflakeは魅力的なのですが、テスト環境等も含めてマシンIDを管理するのは意外に面倒です。ただ、この体系の本質的な特徴は「先頭にタイムスタンプを割り当てた分散ID生成」であり、このテクニックはいろいろ応用できそうです。

今回使用する構成ではAmazon DynamoDBでDHCPのようにworkerを管理することにしました。DynamoDBをアトミックカウンターとして使用するという超贅沢仕様で、ついでにidの取得時刻、開放時刻等も記録してみました。worker idが枯渇するリスクを下げるため、worker idは24時間に一度更新しています。

2012/04/25

@propertyとAutomatic Reference Counting (ARC)

Objective-Cは段階的に進化してきたため、ネットで情報を検索する際には対象としている世代に注意する必要があります。特に、ARC導入前後はかなり考え方が変わっているので、簡単にまとめてみました。

共通の考え方

オブジェクトには参照カウンタが存在し、参照カウンタが0になった時点でオブジェクトは解放されます。JavaのGCのように定期的にメモリ解放が行われるのではなく、カウントが0になった時点ですぐにメモリが解放されます。

@property導入前

Next STEP時代から続くメモリ管理手法で、参照カウンタはすべてプログラマが手動で管理します。retainで参照カウンタを増やし、releaseもしくはautoreleaseで参照カウンタを減らします。setterもプログラマが明示的に記述し、必要に応じてretain/releaseを行います。

ARC導入前の@property (retain,assign世代)

@propertyに「retain」を指定した状態で@synthesizeを利用してメソッドを生成すると、「setするオブジェクトをretainし、以前setされていたオブジェクトをreleaseする」という処理を自動的に生成してくれます。あくまでも「プログラマの代わりにsetterを書いてくれる」だけなので、@synthesizeされたsetter以外では効果がありません。

ARC導入後の@property (strong,weak世代)

Automatic Reference Countingの名が示す通り、retain/releaseはすべて自動的にコンパイラが生成します。逆に、プログラマは明示的にretain/releaseを行うことはできません。簡単に書くとポインタを強参照(strong)と弱参照(weak)で区別し、強参照が増える場合にはretain、減る場合にはreleaseがコンパイラによって挿入されます。@propetyのstrong,weakで、その変数を強参照にするか弱参照にするかを定義します。当然、@synthesizeされたsetter以外でも効果があります。

ARCを使用するべきか?

使わない理由はないと思います。
ARCはコンパイル時のオプションであり、プロジェクトの一部分だけにARCを使用することもできます。また、ターゲットがiOS4の場合でも使用可能です。(ただしiOS4ではweakポインタが使えないので、zeroingのない__unsafe_unretainedポインタを使用します。)

XCode4.3環境でCocoaPodsをインストール

CocoaPods ではじめる Objective-C ライブラリ管理を参考にCocoaPodsをインストールしようとしたら、最初からハマりました...

sudo gem install cocoapods
Building native extensions.  This could take a while...
ERROR:  Error installing cocoapods:
 ERROR: Failed to build gem native extension.

/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby extconf.rb
mkmf.rb can't find header files for ruby at /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/ruby.h


Gem files will remain installed in /Library/Ruby/Gems/1.8/gems/xcodeproj-0.1.0 for inspection.
Results logged to /Library/Ruby/Gems/1.8/gems/xcodeproj-0.1.0/ext/xcodeproj/gem_make.out

最初はmacrubyをインストールしてmacjemでも試したのですが、同じようなエラーでした。調べてみると、まずXCode4.3環境ではmacrubyは必要ないようです。さらに、XCode Command Line Toolを追加でインストールする必要があります。さっそくインストールして再度トライ!

sudo gem install cocoapods
Password:
Building native extensions.  This could take a while...
[!] If this is your first time install of CocoaPods, or if you are upgrading, first run: $ pod setup
Successfully installed xcodeproj-0.1.0
Successfully installed cocoapods-0.5.1
2 gems installed
Installing ri documentation for xcodeproj-0.1.0...

No definition for generate_uuid

No definition for read_plist

No definition for write_plist
Installing ri documentation for cocoapods-0.5.1...
Installing RDoc documentation for xcodeproj-0.1.0...

No definition for generate_uuid

No definition for read_plist

No definition for write_plist
Installing RDoc documentation for cocoapods-0.5.1...

今度は無事にインストールできました。XCode4.3環境の人は、「XCode上でCommand Line Tools」をインストールしてから「sudo gem install cocoapods」するだけなのでとってもお手軽です。

参考リンク

2012/04/24

DelegateをBlockを使って実装

iOS、というかCocoa Frameworkが利用しているdelegateは非常に便利なのですが、複数のDelegateに対応する場合コンポーネントに関する記述が分散して読みにくくなる面があると思います。

iOS4からはBlockが利用できるので、delegateで実現されている一部の機能はBlockで記述した方が簡潔に記述できます。ちょっと調べてみると、やはりUIActionSheet等でdelegateを使うことに不満を持ち、サブクラスやカテゴリを利用してBlockに対応させている人も多いようです。

調べている過程でBlockも@propertyに設定できることがわかったので、それを利用してDelegateをBlock対応にするクラスを書いてみました。このDelegateオブジェクトにプロトコルを中継させることで、呼び出し元からはBlockのみで記述できるようになります。Delegateは通常弱参照しかされないので、オブジェクトの参照管理には注意してください。

具体的な実装例があった方がわかりやすいと思うので、UIImagePickerController用のコードを引用します。@property + @synthesizeを利用することで、かなり簡潔に記述できることがわかると思います。ダイアログ表示、通常処理、キャンセル処理が一カ所で記述できるので、非常に読みやすいコードになりました。

2012/5/12 追記
Delegateを実装していない場合の挙動に影響を与えないように、Blockを設定しているかどうかをrespondsToSelectorで確認するようにしました。


#import <Foundation/Foundation.h>

@interface BTKUIImagePickerControllerDelegate : 
    NSObject<UIImagePickerControllerDelegate,UINavigationControllerDelegate>

//UIImagePickerControllerDelegate
@property(readwrite,copy) void (^didFinishPickingMediaWithInfo)(UIImagePickerController *,NSDictionary *);
@property(readwrite,copy) void (^imagePickerControllerDidCancel)(UIImagePickerController *);

//UINavigationControllerDelegate
@property(readwrite,copy) void (^willShowViewController)(UINavigationController *, UIViewController *,BOOL);
@property(readwrite,copy) void (^didShowViewController)(UINavigationController *, UIViewController *,BOOL) ;

@end


#import "BTKUIImagePickerControllerDelegate.h"

@implementation BTKUIImagePickerControllerDelegate

@synthesize didFinishPickingMediaWithInfo=_didFinishPickingMediaWithInfo;
@synthesize imagePickerControllerDidCancel=_imagePickerControllerDidCancel;

- (BOOL)respondsToSelector:(SEL)aSelector
{
    if(aSelector == @selector(imagePickerController:didFinishPickingMediaWithInfo:)){
        return self.didFinishPickingMediaWithInfo!=nil;
    }
    if(aSelector == @selector(imagePickerControllerDidCancel:)){
        return self.imagePickerControllerDidCancel!=nil;
    }
    if(aSelector == @selector(navigationController:willShowViewController:animated:)){
        return self.willShowViewController!=nil;
    }
    if(aSelector == @selector(navigationController:didShowViewController:animated:)){
        return self.didShowViewController!=nil;
    }
    return [super respondsToSelector:aSelector];
}

- (void)        imagePickerController:(UIImagePickerController *)picker 
        didFinishPickingMediaWithInfo:(NSDictionary *)info
{
    self.didFinishPickingMediaWithInfo(picker,info);
}

- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker;
{
    self.imagePickerControllerDidCancel(picker);
}

@synthesize willShowViewController=_willShowViewController;
@synthesize didShowViewController=_didShowViewController;

- (void)navigationController:(UINavigationController *)navigationController
      willShowViewController:(UIViewController *)viewController
                    animated:(BOOL)animated
{
    self.willShowViewController(navigationController,viewController,animated);
}

- (void)navigationController:(UINavigationController *)navigationController
       didShowViewController:(UIViewController *)viewController
                    animated:(BOOL)animated
{
    self.didShowViewController(navigationController,viewController,animated);
}

@end

呼び出し元からは、下記のようにBlockを設定できます。(ARC使用)

self.imagePickerDelegate = [[BTKUIImagePickerControllerDelegate alloc]init];
UIImagePickerController *imagePicker = [[UIImagePickerController alloc] init];
imagePicker.delegate = self.imagePickerDelegate;

__weak BTKViewController* bself = self;       
imagePickerDelegate.imagePickerControllerDidCancel = 
        ^(UIImagePickerController *imagePicker){
            //Close Dialog
            [imagePicker dismissModalViewControllerAnimated:YES];
            
            //Cleanup
            bself.imagePickerDelegate = nil;
        };

2012/04/22

MKUserLocationのcalloutを非表示にする

MKMapViewには現在地を表示する機能があります。現在地には見慣れた青丸のマーカーが自動的に表示され、タップすると「現在地」と表示されます。これは便利なんですが、マーカーが現在地というのは自明なので吹き出しは不要に思えます。そこで、この表示(callout)を消せないか調べてみました。
MKMapViewでは、Mapに追加されたAnnotationを表示する際に、delegateのviewForAnnotationが呼び出されます。通常はここで対応するviewを生成し、calloutの設定も行うのですが、MKUserLocationではnilを返すことで標準のMKUserLocationViewが生成される仕組みになっています。
そこで、MKUserLocationViewを自分で生成しようと思ったのですが、どうやらこのクラスは非公開のようです。とはいえ、実際にviewが生成されるのはこの関数の呼び戻し後なので、viewの設定を書き換えることはできません。
そこで、このviewを返すタイミングでMKUserLocationのタイトルをnilに設定してみました。予想通りMKUserLocationViewはtitle属性をチェックしてcalloutの表示、非表示を決めているようで、タップに応答しなくなりました。
- (MKAnnotationView *) mapView:(MKMapView *)targetMapView 
             viewForAnnotation:(id ) annotation
{
    if([annotation isKindOfClass:[MKUserLocation class]]){
        ((MKUserLocation *)annotation).title = nil;
    }
    return nil;
}

Network Link Conditioner

Appleの配布している「Network Link Conditioner」を利用すると、ネットワークの遅延、帯域等を自由に制限することができます。もちろんiOSシミュレータにも制限は適用されるので、より実機に近い環境での検証が可能になります。もっと早くこの存在を知りたかった...

以前はXCodeと一緒に配布されていたようなのですが、今は追加ダウンロードになっています。XCode上でメニューからXcode > Open Developer Tool > More Developer Tools...を選択するとダウンロードページに飛ぶので、「Hardware IO Tools for Xcode」をダウンロードしてください。

参考

2012/04/21

gcdタスクを途中で中断する

iOS上で時間のかかる処理を実行する場合には、GCDを利用してバックグラウンド処理するのが効果的です。 ただ、GCDにはcancelの仕組みがないため、cancel方法は自分で実装する必要があります。

良くあるのは、viewのdidChange系通知を使用する場合です。ユーザレスポンスを高めるためにはdidChangeにはすぐ応答したいですが、ユーザの操作によってはdidChangeは連続して送られてきます。使用されないデータを計算するのは時間的にも消費電力的にも無駄になるため、最後の一回だけを実行することが望ましいです。

僕の場合には、下記のようなキャンセル状態を保存するクラスを作成し、それをGCDで実行するブロックに渡すようにしました。

@interface Sample : NSObject
@property(readonly) BOOL isCanceled;
-(void)cancel;
@end

@implementation Sample
@synthesize isCanceled=_isCanceled;

-(void)cancel
{
  @synchronized(self){
    _isCanceled = true;
  }
}
@end

新しくGCDに処理を投入する場合には、cancelFlagを生成してそれをブロックに渡します。もしすでに使用中のcancelFlagがある場合には、先にロックを取得した上でFlagをキャンセルしておきます。

 -- ジョブは完了済みか、実行中 --
[cancelFlag cancel];
 -- ジョブは完了済みか、キャンセル済み --
cancelFlag = [[Sample alloc]init];

GCDで実行するブロックの中では処理の合間にcancelFlagを確認します。この際に必ずしもロックは必要ありませんが、処理を完了する時には必ずロックを取得することで、ジョブの二重実行をさけることができます。

参考