アーカイブ

‘開発’ カテゴリーのアーカイブ

iPhoneでのピクセル処理をNEON(ベクタ演算)を使って4倍高速化する

目次


はじめに


IMG_0001
ARMのNEONというベクタ演算を使ってコードを書いたところ、C言語で書いたコードの4倍の速度で動作する事ができました。この記事では、C言語でのコードの紹介、アセンブラでのコードの紹介、そしてNEONを利用したコードの紹介を行い、高速化を実現させた手法を書きます。


どのような処理をするのか?


吾輩の小説
弊社からリリースしている自炊系読書アプリ「吾輩の小説 for iPhone」は、画像のピクセルを全てチェックして文章を整形して表示しています。この処理がとても重いので最適化の一貫として研究した情報を公開します。

このプログラムでは 1024×768 ピクセルでRGBAのフォーマットをもつデータから、RGBの各色の合計値が256未満であるピクセルの数を計算します。つまり、画像データから黒っぽいピクセルがいくつあるのかを計算します。また、計算速度の違いが分かりやすくなるように、これを100回繰り返し合計で約8000万ピクセルの計算を行ないます。また、その計算時間を表示します。


コードの入手先

githubにプロジェクトファイルも含めてiPhoneで実行できるファイル一式をアップしましたのでご利用ください。gitコマンドを使用しない場合はこちらからzip化したものを落としてください。


C言語でのコード


以下がコードになります。

#define IMAGE_SIZE_W		(1024)
#define IMAGE_SIZE_H		(768)
#define CHECK_COLOR		(0xFF)
#define LOOP_COUNT		(100)
#define ELEMENT_OF_PIXEL	(4)
 
NSString* Test::testC(){
	int pixelCount = width * height;
	int hitCount = 0;
	int startTime = getTime();
	for(int i = 0; i < LOOP_COUNT; i++){
		unsigned char* imageWork = image;
		for(int j = 0; j < pixelCount; j++){
			int color = imageWork[0]+imageWork[1]+imageWork[2];
			if(color < CHECK_COLOR){
				hitCount++;
			}
			imageWork += ELEMENT_OF_PIXEL;
		}
	}
	int endTime = getTime();
	NSString* string = [NSString stringWithFormat:@"Pixcel: %d\nHit Pixel: %d\nTime: %d msec\n", pixelCount * LOOP_COUNT, hitCount, endTime-startTime];
	return string;
}

RGBの合計値を算出し、CHECK_COLOR(0xFF) よりも小さいものをカウントしていくだけのシンプルなプログラムです。


アセンブラでのコードとgccの最適化の素晴らしさ


以下がコードになります。

#define IMAGE_SIZE_W		(1024)
#define IMAGE_SIZE_H		(768)
#define CHECK_COLOR		(0xFF)
#define LOOP_COUNT		(100)
#define ELEMENT_OF_PIXEL	(4)
 
NSString* Test::testAsm(){
	int pixelCount = width * height;
	int hitCount = 0;
	int checkColor = CHECK_COLOR;
	int startTime = getTime();
	for(int i = 0; i < LOOP_COUNT; i++){
		__asm__ volatile (
				"mov	r0, #0 \n\t"
 
				// ループ開始
				"1: \n\t"
				"add	r0, r0, #1 \n\t"
 
				"ldrb	r3, [%[image]] \n\t"
				"ldrb	r2, [%[image], #1] \n\t"
				"add	r2, r2, r3  \n\t"
 
				"ldrb	r3, [%[image], #2] \n\t"
				"add	r2, r2, r3  \n\t"
				"add	%[image], %[image], #4  \n\t"
 
				// 色判定とカウント
				"cmp	r2, %[checkColor] \n\t"
				"addlt %[hitCount], %[hitCount], #1 \n\t"
 
				// 「ループ開始」へ戻る
				"cmp	r0, %[pixelCount] \n\t"
				"bne	1b \n\t"
 
				: [hitCount] "+r" (hitCount)
				: [pixelCount] "r" (pixelCount), [image] "r" (image), [checkColor] "r" (checkColor)
				: "r0", "r1", "r2", "r3", "cc", "memory"
				);
	}
	int endTime = getTime();
	NSString* string = [NSString stringWithFormat:@"Pixcel: %d\nHit Pixel: %d\nTime: %d msec\n", pixelCount * LOOP_COUNT, hitCount, endTime-startTime];
	return string;
}

時間を計測したところ、驚くことに C言語 で書いたものよりも速度が遅くなりました。gccがCからアセンブラにコンパイルする時の最適化がとても優れているのだと思います。NEONなどの特殊な機能を使わないかぎりは、アセンブラ化せず C言語 で書くほうが良いと思います。


NEON : 資料


今回のコードでお見苦しい点があればご容赦願います。アセンブラでの開発経験が全く無く2日前に勉強しながら作成したものです。そこで、使用した資料を列挙しておきます。

  • ARM アセンブリ
    ARMの基本的なプログラミングの手法を知る事ができます。


NEON : 処理の流れと制限

NEONはベクタ演算です。ベクタ演算とは復数の演算を1命令で実行します。今回の方法では、16回の足し算、16回の比較などを1命令で実行します。それにともなって、今までの単純なループや比較ではなくて、それなりに複雑なプログラムになってきます。処理の大まかな流れは以下の順番になります。

  1. 16ピクセル(64バイト)分のデータを詠込む
  2. 16ピクセルのRGBを一気に足す
  3. 16ピクセルの0xFF未満の値を一気にカウンタに追加
  4. 4096ピクセルの処理が終わるまで1-3を繰返す
  5. カウンタの合計を取る
  6. 全てのピクセルの処理が終わるまで1-5を繰返す

ポイントは「16ピクセルを一気に処理」している所と、「4096ピクセルの処理」を一つの境目としている所です。以下で詳しく説明していきます。


NEON : コード


以下がコードになります。

#define IMAGE_SIZE_W		(1024)
#define IMAGE_SIZE_H		(768)
#define CHECK_COLOR			(0xFF)
#define LOOP_COUNT			(100)
#define ELEMENT_OF_PIXEL	(4)
 
NSString* Test::testNeon(){
	int pixelCount = 4096;
	int innerLoop = ((width*height)/pixelCount);
	int totalHitCount = 0;
	unsigned int checkColor =
		CHECK_COLOR << 24 |
		CHECK_COLOR << 16 |
		CHECK_COLOR << 8 |
		CHECK_COLOR << 0;
	int startTime = getTime();
	unsigned int addMask = 0x01010101;
	for(int i = 0; i < LOOP_COUNT; i++){
		unsigned char* _image = image;
		for(int j = 0; j < innerLoop; j++){
			int hitCount = 0;
			__asm__ volatile (
					// 初期化
					"mov	r0, #0 \n\t" // 0クリア用
					"mov	r1, #0 \n\t" // 処理済みピクセルのカウンタ
					"vmov.u32 d8, r0, r0 \n\t"
					"vmov.u32 d9, r0, r0 \n\t"
					"vmov.u32 d10, %[checkColor], %[checkColor] \n\t"
					"vmov.u32 d11, %[checkColor], %[checkColor] \n\t"
					"vmov.u32 d12, %[addMask], %[addMask] \n\t"
					"vmov.u32 d13, %[addMask], %[addMask] \n\t"
 
					// ループ開始
					"1: \n\t"
					"add	r1, r1, #16 \n\t"
 
					// データの読込と色の加算
					"add		r2, %[image], #32 \n\t"
					"vld4.8	{d0, d2, d4, d6}, [%[image]] \n\t"
					"vld4.8	{d1, d3, d5, d7}, [r2] \n\t"
					"vqadd.u8	q0, q1 \n\t"
					"vqadd.u8	q0, q2 \n\t"
 
					// 色の判定とカウント
					"vclt.u8 q1, q0, q5 \n\t"
					"vand q1, q6 \n\t"
					"vadd.u8 q4, q4, q1 \n\t"
 
					// データのアドレスを進める
					"add	%[image], #64 \n\t"
 
					// 「ループ開始」へ
					"cmp	r1, %[pixelCount] \n\t"
					"bcc	1b \n\t"
 
					// 色数の合計
					"mov	r0, #0 \n\t"
					"vmov.u32	r1, d8[0] \n\t"
					"2: \n\t"
					"and		r2, r1, #0xFF \n\t"
					"add		%[hitCount], r2 \n\t"
					"lsr		r1, #8 \n\t"
					"add		r0, #1 \n\t"
					"cmp		r0, #4 \n\t"
					"bne		2b \n\t"
 
					"mov	r0, #0 \n\t"
					"vmov.u32	r1, d8[1] \n\t"
					"3: \n\t"
					"and		r2, r1, #0xFF \n\t"
					"add		%[hitCount], r2 \n\t"
					"lsr		r1, #8 \n\t"
					"add		r0, #1 \n\t"
					"cmp		r0, #4 \n\t"
					"bne		3b \n\t"
 
					"mov	r0, #0 \n\t"
					"vmov.u32	r1, d9[0] \n\t"
					"4: \n\t"
					"and		r2, r1, #0xFF \n\t"
					"add		%[hitCount], r2 \n\t"
					"lsr		r1, #8 \n\t"
					"add		r0, #1 \n\t"
					"cmp		r0, #4 \n\t"
					"bne		4b \n\t"
 
					"mov	r0, #0 \n\t"
					"vmov.u32	r1, d9[1] \n\t"
					"5: \n\t"
					"and		r2, r1, #0xFF \n\t"
					"add		%[hitCount], r2 \n\t"
					"lsr		r1, #8 \n\t"
					"add		r0, #1 \n\t"
					"cmp		r0, #4 \n\t"
					"bne		5b \n\t"
 
					: [hitCount] "+r" (hitCount)
					: [pixelCount] "r" (pixelCount), [image] "r" (_image), [checkColor] "r" (checkColor), [addMask] "r" (addMask)
					: "r0", "r1", "r2", "q0", "q1", "q2", "q3", "q4", "cc", "memory"
					);
			totalHitCount += hitCount;
		}
	}
	int endTime = getTime();
	NSString* string = [NSString stringWithFormat:@"Pixcel: %d\nHit Pixel: %d\nTime: %d msec\n", width * height * LOOP_COUNT, totalHitCount, endTime-startTime];
	return string;
}


NEON : コード解説-処理限界数でのループ


上記しましたが、この処理でのポイントに「4096ピクセルの処理」というものがあります。これはピクセル数のカウントを1バイトで行い、それを16個保持しているからです。「256(1バイト)x16=4096」となります。なぜ1バイトで計算するのか?と思われるとおもいますが、これがベクタ演算を使った高速化のカギになります。以下で説明します。


NEON : コード解説-レジスタの役割


この最適化コードでは7本のNEON128ビットレジスタを使用しています。それぞれの主な役割は以下のようになります。

  • q0(d0,d1): R値を保持
  • q1(d2,d3): G値を保持
  • q2(d4,d5): B値を保持
  • q3(d6,d7): A値を保持。読込むだけで計算には使わない。
  • q4(d8,d9): 色判定の結果をカウント
  • q5(d10,d11): 色判定で使用する値(0xFFが詰まっている)
  • q6(d12,d13): 結果をカウントするときに使用するビットマスク(0×01が詰まっている)


NEON : コード解説-データの読込


まず、計算の前にメモリからNEONレジスタにデータを読み込みます。

// データの読込と色の加算
"add	r2, %[image], #32 \n\t"
"vld4.8	{d0, d2, d4, d6}, [%[image]] \n\t"
"vld4.8	{d1, d3, d5, d7}, [r2] \n\t"

%[image]レジスタがデータの先頭のアドレスを指し、r2レジスタがそこから32バイトずらした8個目以降のピクセルのアドレスを指しています。そして vld4.8 命令を呼び以下の画像のような配置でデータが読み込まれます。

neon_1_registers


NEON : コード解説-加算


そして、RGBの値を加算します。

"vqadd.u8	q0, q1 \n\t"
"vqadd.u8	q0, q2 \n\t"

Rの値が入っている q0 レジスタに、G値の q1 と、B値の q2 を加算します。以下の図ような処理になります。

neon_2_adding_colors

この時に vadd.u8 ではなく vqadd.u8 を使います。前者は加算時にビットが溢れても気にせずに計算がされます。後者はビットが溢れた場合に最大値が設定されます。計算式でたとえると以下のような違いになります。

vadd.u8 : 0xFF + 0×01 = 0×0
vqadd.u8 : 0xFF + 0×01 = 0xFF

これで次に行う比較演算が問題なくおこなえます。

しかし、正確にいうと場合によっては問題が大ありです。上記のとおり、この最適化コードでは256以上の値での色判定は行えません。どんな値でも判定できるコードも書いてみたのですが、これがあまり高速化できなかったため今回はバッサリと切り捨てました。


NEON : コード解説-ピクセルの色チェックとカウント


ピクセルの色チェックとカウントは多少やっかいなので、図をまじえつつ説明していきます。

// 色の判定とカウント
"vclt.u8 q1, q0, q5 \n\t"
"vand q1, q6 \n\t"
"vadd.u8 q4, q4, q1 \n\t"

vclt.u8 は比較命令です。 RGBの合計値が入っている q0 と、比較用の値を詰め込んである q5 を比較し、 q0 の値の方が小さかった場合に対応するビットに 1 が設定された値を q1 に入れます。意味が分かりにくいと思うので図では以下のようになります。例として q0 にはRGB値が加算されたとして適当な値を入れています。

neon_3_vclt

次は、比較判定にヒットしたピクセルをカウントします。vclt.u8 では比較にヒットしたピクセルに対応する箇所のビットが全て立つ、つまり 0xFF が入っています。これを 1 になるように 0×1 で AND を取ります。

neon_4_vand

最後に vadd.u8 で q4 に q1 を足してあげれば、16ピクセル分のカウントが完了します。

neon_5_vadd

上記の図ではカウント用の q4 レジスタが空でしたが、ループを重ねるごとに値が加算されていくことになります。


NEON : コード解説-カウントの合計


上記したように、このコードでは4096ピクセルまで計算すると、q4 でのカウンタが最大値である 0xFF になるものが出てきます。ですので、ここで一度別のレジスタに待避させます。

// 色数の合計
"mov		r0, #0 \n\t"
"vmov.u32	r1, d8[0] \n\t"
"2: \n\t"
"and		r2, r1, #0xFF \n\t"
"add		%[hitCount], r2 \n\t"
"lsr		r1, #8 \n\t"
"add		r0, #1 \n\t"
"cmp	r0, #4 \n\t"
"bne		2b \n\t"
 
以下の同様の処理は省略

まず vmov.u32 で d8[0](d8の上位4バイト) を r1 レジスタにコピーします。and で 下位1バイトに格納されているカウンタを取り出し %[hitCount] に足します。lsr r1, #8 で1バイト分右にシフトし次のカウンタの値を取得する準備をします。and と lsr の処理は以下の図のようになります。

neon_6_shift_count

これらの処理をループで4回まわして、合計4個のカウント値を合計します。

その後 d8[1] d9[0] d9[1] にも同様の処理を行ない、16個を合計します。

これまでの処理を画像データの最後まで繰り返すと、RGB合計値が256未満の色をもつピクセル(黒っぽいピクセル)の個数が、C言語で書いたプログラムの約4倍の速度で取得できます。


終りに

今回最適化を行うにあたって上記のコード量の10倍程度を書き検証しました。そこで分かった注意点が、NEONで最適化するなら、メモリロードなどにARMレジスタや命令を使わずに、直接NEONレジスタと命令を使わないと意味が無いという事です。

決して汎用的な最適化コードではないですが、iPhoneで大量の処理を行う必要がある場合に、NEONでの高速化は有効だと思います。その時の参考になれば幸いです。

最後に、NEONを使いながらC言語の処理速度とほぼ同じだったコードを書いておきます。

#define IMAGE_SIZE_W		(1024)
#define IMAGE_SIZE_H		(768)
#define CHECK_COLOR		(0xFF)
#define LOOP_COUNT		(100)
#define ELEMENT_OF_PIXEL	(4)
 
NSString* Test::testNeon(){
	int pixelCount = width * height;
	int totalHitCount = 0;
	int checkColor = CHECK_COLOR;
	int startTime = getTime();
	unsigned int a = 0;
	for(int i = 0; i < LOOP_COUNT; i++){
		int hitCount = 0;
		__asm__ volatile (
				// 初期化
				"mov	r0, #0 \n\t"
				"vmov.u32 d8, r0, r0 \n\t"
				"vmov.u32 d9, r0, r0 \n\t"
				"vmov.u32 d6, %[checkColor], %[checkColor] \n\t"
				"vmov.u32 d7, %[checkColor], %[checkColor] \n\t"
 
				// ループ開始
				"1: \n\t"
				"add	r0, r0, #4 \n\t"
 
				"ldrb r1, [%[image]] \n\t"  
				"ldrb r2, [%[image], #1] \n\t"  
				"ldrb r3, [%[image], #2] \n\t"  
 
				"ldrb r4, [%[image], #4] \n\t"  
				"ldrb r5, [%[image], #5] \n\t"  
				"ldrb r6, [%[image], #6] \n\t"  
 
				"vmov d0, r1, r4 \n\t"
				"vmov d2, r2, r5 \n\t"
				"vmov d4, r3, r6 \n\t"
 
				"ldrb r1, [%[image], #8] \n\t"  
				"ldrb r2, [%[image], #9] \n\t"  
				"ldrb r3, [%[image], #10] \n\t"  
 
				"ldrb r4, [%[image], #12] \n\t"  
				"ldrb r5, [%[image], #13] \n\t"  
				"ldrb r6, [%[image], #14] \n\t"  
 
				"vmov d1, r1, r4 \n\t"
				"vmov d3, r2, r5 \n\t"
				"vmov d5, r3, r6 \n\t"
 
				"add %[image], %[image], #16 \n\t"  
 
				"vadd.s32	q0, q0, q1 \n\t"
				"vadd.s32	q0, q0, q2 \n\t"
 
				// カウント
				"vclt.s32 q1, q0, q3 \n\t"
				"vsub.s32 q4, q4, q1 \n\t"
 
				// 「ループ開始」へ
				"cmp	r0, %[pixelCount] \n\t"
				"bcc	1b \n\t"
 
				// 色判定とカウント
				"vmov.s32 %[hitCount], d8[0] \n\t"
				"vmov.s32 r0, d8[1] \n\t"
				"vmov.s32 r1, d9[0] \n\t"
				"vmov.s32 r2, d9[1] \n\t"
				"add %[hitCount], r0 \n\t"
				"add %[hitCount], r1 \n\t"
				"add %[hitCount], r2 \n\t"
 
				: [a] "+r" (a), [hitCount] "+r" (hitCount)
				: [pixelCount] "r" (pixelCount), [image] "r" (image), [checkColor] "r" (checkColor)
				: "r0", "r1", "r2", "r3", "r4", "r5", "r6", "q0", "q1", "q2", "q3", "q4", "cc", "memory"
				);
		totalHitCount += hitCount;
	}
	int endTime = getTime();
	NSString* string = [NSString stringWithFormat:@"Pixcel: %d\nHit Pixel: %d\nTime: %d msec\n", pixelCount * LOOP_COUNT, totalHitCount, endTime-startTime];
	return string;
}
カテゴリー: 開発 タグ:

CALayerの好きなプロパティをアニメーションさせる

2010 年 3 月 14 日 ento コメントはありません

iPhone 上の描画システムとして、弊社ではこれまで UIKit、OpenGL を使ってきました。 UIKit を使ったのは最初のアプリだけで、パフォーマンス上の理由からそれ以降のアプリはすべて OpenGL で描画しています。

しかし先週から開始した次のアプリの開発では、Quartz と Core Animation を使ってみています。簡単な時計アプリになる予定で、これらのフレームワークでどこまでできるかの試作段階といったところです。その中で 1 つ、詰まった点があったので紹介します。

それはというと、時計の針を描画するところまではよかったのですが、その角度を変えながらアニメーションさせるところで行き詰まったのです。CALayer の「アニメーション可能なプロパティ」 animatable properties (不透明度やサイズなどが含まれます) ならば、値を変えるだけで自動的にアニメーション implicit animation が実行されます。時計の針の角度のような、自分で定義したプロパティでもそれをするにはどうすれば?

Google 検索に 3 時間お付き合いいただいた結果、iPhone OS 3.0 以降なら、CALayer の needsDisplayForKey: クラスメソッドで「変更されたら再描画が必要になるプロパティ」を指定できること、actionForKey: メソッドで「変更されたら実行するアクション (アニメーション)」を指定できることが分かりました。ここまでは簡単に調べられたのですが、時計の針をアニメーションさせるにはもう 1 つ注意すべきことがありました。こちらは後で紹介します。

それより何よりソースコードを見てみましょう。

はじめに、時計の針用のレイヤーを作るコードです:

@implementation ClockView
 
- (void)awakeFromNib {
    [self setupLayers];
    [self start];
}
 
- (void)start {
    // tick メソッドを呼び出すタイマーを作成
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0/60.0 
        target:self
        selector:@selector(tick)
        userInfo:nil
        repeats:YES];
    self.animationTimer = timer;
}
 
- (void)setupLayers {
    /*
     各針用のレイヤーを追加。
     angle を変化させてアニメーションする。
     */
    {
        HandLayer *hand = [[HandLayer alloc] init];
        hand.frame = self.frame;
        self.secondHand = hand;
        [hand release];
        [self.layer addSublayer:self.secondHand];
    }
    // ...
}
 
- (void) tick {
    // 各針に新しい角度を通知
}

ここまでは大したことはありませんね。次がこの記事の心臓、アニメーション部分です:

@interface HandLayer : CALayer {
}
 
@property CGFloat angle;
 
@end
 
 
@implementation HandLayer
 
@dynamic angle;
 
+ (BOOL)needsDisplayForKey:(NSString*)aKey {
    // angle が変わったら再描画がいるよ
    if ([aKey isEqualToString:@"angle"]) {
        return YES;
    } else {
        return [super needsDisplayForKey:aKey];
    }
}
 
- (id)actionForKey:(NSString *) aKey {
    if ([aKey isEqualToString:@"angle"]) {
        // angle 用の補間アニメーションを作る
        CABasicAnimation *theAnimation = [CABasicAnimation
            animationWithKeyPath:aKey];
        // 注意: fromValue を設定しないと正しくアニメーションしない
        theAnimation.fromValue = [[self presentationLayer] valueForKey:aKey];
        return theAnimation;
    } else {
        return [super actionForKey:aKey];
    }
}
 
- (void)drawInContext:(CGContextRef)context {
    // angle プロパティを参照しながら針を描画
}

theAnimation.fromValue を現在表示中の針の角度に設定していることに注意してください。これがドキュメントを探しても見つけられなかったポイントです。解決できたのは、Omni Group のブログ記事 “Animating CALayer content”のおかげでした。その当時は自分で needsDisplayForKey: 周辺の仕組みを実装しないといけなかったんですね。

最後に、動画をどうぞ:

※ 動画中の針のアニメーションは、アニメーションのローカル時間を決める関数 timingFunction にデフォルトの線形タイプではなく kCAMediaTimingFunctionEaseIn を使っています。

参考:

追記:
実際に実行されている補間アニメーションの開始値 fromValue → 終了値 toValue / 増加値 byValue をログに出してみました。

2010-03-13 11:12:41.179 app[14615:207] angle \
<CABasicAnimation: 0x3b02620>, 6.283185 -> (null) / (null)

fromValue が設定されていて、その他は設定されていないのが分かります。終了値が設定されていないのにどうやって補間しているのかは謎です…。

カテゴリー: 開発 タグ:

iPhoneアプリで半自動の画面回転インターフェイス

2009 年 12 月 11 日 akiraak コメントはありません

iPhone の Safari などを使っていて、自分が意図していないのに画面が横向きに変わったりしてイライラする事はないでしょうか?私は何度もあります。自分が iPhone を微妙に傾けてしまうのが原因ですが、これを解決しようと思い動画のようなインターフェイスを作成しました。

傾きを感知すると回転用のボタンが表示され、それをタッチすると画面が回転。ボタン以外をタッチすると回転ボタンが消えます。また、今回はまだ実装していませんが、ボタンが表示され2秒ほど経過するとボタンが自動で消えるようにしようと思います。

カテゴリー: 開発 タグ:

GHUnit と NSInvocation を使って非同期通信の単体テストをする

2009 年 12 月 1 日 ento コメントはありません

1. 通信への依存を切る

さて、アプリのコンテンツの5段階評価をサーバに送信して保存する機能を作っているとします。例えばこんなコードです:

@implementation StarService
 
- (void)star:(NSUInteger)pictureNumber count:(NSUInteger)count {
	// リクエストオブジェクトをつくる
	NSString *url = [NSString stringWithFormat:@"http://%@:%d/api/star/", serviceHostname, servicePort, nil];
	NSMutableURLRequest *theRequest=[NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]
											  cachePolicy:NSURLRequestUseProtocolCachePolicy
										  timeoutInterval:60.0];
	// ... ここで呼び出しパラメータやHTTPヘッダを設定 ...
	StarRequestDelegate *requestDelegate = [[StarRequestDelegate alloc] initWithService:self delegate:serviceDelegate];
	/* API呼び出し開始! */
	NSURLConnection *theConnection = [NSURLConnection connectionWithRequest:aRequest delegate:requestDelegate];
}
...
// API呼び出し用のNSURLConnectionデリゲート
@implementation StarRequestDelegate
 
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
	if (delegate && [(NSObject*)delegate respondsToSelector:@selector(starService:didFinishStar:)]) {
		/* APIが正常に終わったので、デリゲートのコールバックを呼ぶ */
		[delegate starService:service didFinishStar:receivedData];
	}
	[super connectionDidFinishLoading:connection];
}

こんなコードを見るとまず作りたくなるのは、同じインターフェイスだけれど、実際はネットワークにアクセスしないクラスです。そうですよね。そうすれば、アプリの他の部分を実装するときも、サーバを立てたりすることなく開発をちゃきちゃき進めることができます。

@implementation FakeStarService
 
- (void)star:(NSUInteger)pictureNumber count:(NSUInteger)count {
	/* デリゲートのコールバックを直接呼び出すNSInvocationをつくる */
	NSInvocation *invocation;
	[[NSInvocation retainedInvocationWithTarget:serviceDelegate invocationOut:&invocation]
	 starService:self didFinishStar:nil];
 
	/* ネットワーク遅延を装うためにさらにNSInvocationをかぶせる */
	NSInvocation *delayInvocation;
	[[NSInvocation retainedInvocationWithTarget:invocation invocationOut:&delayInvocation]
	 performSelector:@selector(invoke) withObject:nil afterDelay:delay];
	[delayInvocation performSelectorOnMainThread:@selector(invoke) withObject:nil waitUntilDone:NO];
}

上の例では、偽のネットワーク遅延を実現するために、NSInvocationを2つ使っています。待て待て、NSInvocationを使うのはもっとめんどくさかったはず?たしかにその通りです。上のコードは ForwardedConstruction という拡張の助けを借りて書かれています。

iPhoneでこの拡張を使うには、リンク先からコードをダウンロードした上で、以下の変更を加える必要があります。

--- Downloads/NSInvocationForwardedConstruction/NSInvocation(ForwardedConstruction).h	2009-05-04 11:55:34.000000000 +0900
+++ NSInvocation(ForwardedConstruction).h	2009-12-02 10:17:07.000000000 +0900
@@ -11,7 +11,7 @@
 //  appreciated but not required.
 //
 
-#import <Cocoa/Cocoa.h>
+#import <UIKit/UIKit.h>
 
 @interface NSInvocation (ForwardedConstruction)
 
@@ -21,3 +21,10 @@
 	invocationOut:(NSInvocation **)invocationOut;
 
 @end
+
+#if (TARGET_OS_IPHONE)
+@interface NSObject (ForwardedConstruction)
+- (NSString *)className;
++ (NSString *)className;
+@end
+#endif
--- Downloads/NSInvocationForwardedConstruction/NSInvocation+ForwardedConstruction.m	2009-05-04 11:55:34.000000000 +0900
+++ NSInvocation(ForwardedConstruction).m	2009-12-02 10:17:43.000000000 +0900
@@ -12,7 +12,9 @@
 //
 
 #import "NSInvocation(ForwardedConstruction).h"
-#import <objc/objc-runtime.h>
+//#import <objc/objc-runtime.h>
+#import <objc/runtime.h>
+#import <objc/message.h>
 
 //
 // InvocationProxy is a private class for receiving invocations via the
@@ -376,4 +378,21 @@
 	return invocationProxy;
 }
 
+@end 
+
+#if (TARGET_OS_IPHONE)
+
+@implementation NSObject (ForwardedConstruction)
+
+- (NSString *)className
+{
+	return [NSString stringWithUTF8String:class_getName([self class])];
+}
++ (NSString *)className
+{
+	return [NSString stringWithUTF8String:class_getName(self)];
+}
+
 @end
+
+#endif

2. スレッドに注意する

単体テストフレームワークとして、GHUnitを使います。これは Objective-C 向けのフレームワークで、Mac OS X 10.5 と iPhone 2.x/3.x で動作します。テスト実行用のGUIも付いています。さらに、自分自身を独立したスレッドで動かす機能もあり、これが NSURLConnection がからむテストで効いてきます。

というのも、NSURLConnectionの内部仕様的に、メインスレッド上で接続開始メソッドを呼ばないといけないらしく、テストフレームワークに別スレッドで走ってもらうことで、ネットワーク関連のコードをメインスレッドで動かしつつ、テスト実行用UIもスムーズに使うことができます。

ということで辿り付いたのが以下の構成です:

// 実際にネットワーク接続するクラスでテストをするクラス。
// 偽クラスをテストするテストクラスも別にある
@implementation HttpNetTest
 
- (BOOL)shouldRunOnMainThread {
	/* GHUnitは別スレッドで */
	return NO;
}
 
- (void)test_send_star {
	[tester do_test_send_star:service];
}
 
@implementation StarServiceTests
 
- (void)do_test_send_star:(id)service {
	// NSInvocationをつくり、
	NSInvocation *invocation;
	[[NSInvocation retainedInvocationWithTarget:service invocationOut:&invocation]
	  star:0 count:1];
	/* メインスレッドで呼び出す */
	[invocation performSelectorOnMainThread:@selector(invoke) withObject:nil waitUntilDone:NO];
 
	/* 終了を待つ */
	BOOL notTimeout = [AsyncTestHelper wait:service.delegate property:@selector(receivedDidFinishGetStarsCount) atLeast:1];
 
	/* アサートたち */
	GHAssertTrue(notTimeout, @"Should not timeout");
	GHAssertEquals((NSUInteger)1, [service.delegate receivedDidFinishStarCount], @"delegate should receive star callback");
}

3. 通信の終了を待つ

非同期通信のテストでやっかいなのが、いつ通信が終了したかを検知する必要がある点です。テストコードでは以下の部分になります。

@implementation StarServiceTests
 
- (void)do_test_send_star:(id)service {
	// ..
	[AsyncTestHelper wait:service.delegate property:@selector(receivedDidFinishStarCount) atLeast:1];
	// ..
}

このメソッドの中身は、ただ与えられたオブジェクトのプロパティが指定の値以上になるのを待つだけのものです:

@implementation AsyncTestHelper
 
+ (BOOL)wait:(id)target property:(SEL)getter atLeast:(NSUInteger)count {
	int tried = 0;
	while((NSUInteger)[target performSelector:getter]  10) {
			return FALSE;
		}
		[NSThread sleepForTimeInterval:0.5];
	}
	return TRUE;
}

テスト実行画面は以下のような感じ。

ghunit_test_runner

最後に、登場したクラス群の関係を示す簡単な図を作ってみました。XcodeのCore Dataモデリングツールを使用しています。

カテゴリー: 開発 タグ:

写真と地図で見る京都が「スタッフのおすすめアプリケーション」に登場

2009 年 11 月 26 日 akiraak コメントはありません

スタッフのおすすめ

カテゴリー: 開発 タグ:

iPhone で Crash Reports を利用したバグの発見方法

2009 年 10 月 19 日 akiraak コメント 2 件

Crash Reports とは

iPhoneアプリ開発者は、開発をおこなったアプリをユーザーが使用している時にクラッシュした場合は、その情報を iTunes connect からDLして観覧する事が可能です。

Crash Reports の入手方法

iTunes connect に接続し、以下の画像の赤枠のボタンを順番にクリックしていきます。
“Manage Your Applications” -> “App Details” -> “View Crash Reports”

crash01
crash02
crash03

ここまで来るとOSのバージョンごとにレポートを見る事ができます。どうやら OS3.1 で 2 つのバグが出ているようです。今回は 2 つ目の “PMS2: ms::GLKeyedSprite::setRenderKey + 192 38% of submitted crashes” のバグ修正を行っていきますので、その右の DOWNLOAD ボタンをクリックします。

crash04

バグ修正

レポートにはいろいろな情報が書かれていますが、今回は関数呼び出しの履歴の箇所を見ると、一発で原因がわかりました。

Thread 0 Crashed:
0   libSystem.B.dylib             	0x32a229ac __kill + 8
1   libSystem.B.dylib             	0x32a2299c kill
2   libSystem.B.dylib             	0x32a2298e raise
3   libSystem.B.dylib             	0x32a3763a abort
4   libSystem.B.dylib             	0x32a24f30 __assert_rtn
5   PMS2                          	0x00008d68 ms::GLKeyedSprite::setRenderKey(int const*, unsigned int) + 192
6   PMS2                          	0x00008bec ms::GLKeyedSprite::setRenderKeyWithCString(char const*) + 228
7   PMS2                          	0x000174c4 Time::update(unsigned int) + 180
8   PMS2                          	0x000071bc Scene::update(unsigned int) + 388
9   PMS2                          	0x00009a94 ms::GLScene::onUpdate() + 36
10  UIKit                         	0x30d5d574 -[UIView(CALayerDelegate) _layoutSublayersOfLayer:]

自前で実装しているクラス関数 “ms::GLKeyedSprite::setRenderKey()” 中の “assert()” に引っかかりプロセスが殺されています。その前の関数呼び出しが “Time::update()” からなので、アプリ画面の上部に表示している時計を描画している時に問題があるようです。

IMG_0210

“ms::GLKeyedSprite::setRenderKey()” 内の “assert()” を if 文に変更し、引数に問題があってもとりあえず動作するようにします。本来ならそもそも低レベル関数を使用している “Time::update()” あたりのバグを修正することろですが、できるだけユーザーの端末でクラッシュする確率を減らすため、今回は “assert()” を削るという修正になりました。

それでは、よりよいアプリ開発を!!

カテゴリー: 開発 タグ:

「写真と地図で見る京都 秋」がAppStoreトップページに

2009 年 10 月 7 日 akiraak コメントはありません

写真と地図で見る京都 秋

「ニューリリースと注目作品」としてAppStoreのトップページに載りました。

京都の写真がとても綺麗で素敵ですので、多くのかたにお楽しみ頂けると思うと嬉しいです。まずは無料版からどうぞ。



「写真と地図で見る京都 無料版」

カテゴリー: 開発 タグ:

京都、、、えぇのう

2009 年 10 月 6 日 akiraak コメントはありません
カテゴリー: 開発 タグ:

Flash for iPhone でマルチプラットフォーム環境

2009 年 10 月 6 日 akiraak コメントはありません

Adobe から Flash を iPhone のネイティブアプリに変換するソフトが出るようです。

これにより、Flash でアプリを作成しておけば、携帯アプリ市場がある「iPhone, Android, BlackBerry, GREE, モバゲー」そして、WEBアプリの「Facebook, mixi」 などに簡単に移植できるアプリが作成できるかもしれない。

http://blogs.adobe.com/akamijo/archives/2009/10/flash_professio_1.html
http://labs.adobe.com/technologies/flashcs5/appsfor_iphone/

カテゴリー: 開発 タグ:

「京都 夏」におけるiPhoneアプリで下位互換を維持する手法

2009 年 9 月 17 日 akiraak コメント 1 件

OS別普及率

「京都 夏」の開発当時、最新の iPhone OS は 3.0 でした。3.0 からはアプリ内でマップを表示できる機能が追加されたため、この機能を使い、写真から地図を表示するという機能を実装する事にしました。しかし、OS バージョン別の普及率を調べてみると、2.2.1 が圧倒的に多く、これでは購入者を大幅に減らせてしまうと思いました。

こちらのサイトで OS バージョン別の普及率が出ています。
iPhoneOS percentages 08/2009

iPhone OS 3.0 Adoption Rate Estimates All Over The Place : 2009/06/29

iPhone では無料で OS の更新ができるため 3.0 のユーザーが多いと思うのですが、iPod touch は有料アップデートのためか依然として2.2.1が多いようで、その割合は2009年8月末時点で全体の 40% ほどを占めています。

そこで我々は、iPhone SDK 3.0 で開発を行いならが、2.2 でも動作が可能な手法をとりましたので、紹介したいと思います。

下位互換の手法

まず、開発のポイントは2つあります。

  • 3.0 にしか無いライブラリは Weak リンクをする
  • 実行時に OS のバージョンを調べ動作を変える

ライブラリは通常 Required というモードで追加されます。このモードでは、実機での実行時にライブラリが存在していない場合はアプリが起動しません。 これを Weak というモードに変更する事で、実機での実行時にライブラリが存在していなくても起動できるようになります。今回は 3.0 から追加されたライブラリである MapKit.framework を Weak に設定します。以下の画像が設定例です。

PMS1-001

そして、次に OS のバージョンによって動作を変えます。MapKit.framework がリンクされていない 2.2 などでは、その機能を使おうとするとアプリが落ちてしまいます。以下のコードが OS のバージョンを調べ、3.0 以下の場合は Map に関するクラスを作成しないものになります。

float version = [[[UIDevice currentDevice] systemVersion] floatValue];
if(version >= 3.0){
	MapController	*_map = [[MapController alloc] init];
	self.mapController = _map;
	[_map release];
}

また、2.2 でもマップ表示を実装するために、外部マップアプリで表示する手法をとっています。

float version = [[[UIDevice currentDevice] systemVersion] floatValue];
if(version >= 3.0){
	[g_Instance.mapController setTitle:NSLocalizedString([NSString stringWithCString:title encoding:NSUTF8StringEncoding], @"") latitude:latitude longitude:longitude];
	[g_Instance push:g_Instance.mapController];
}else{
	NSString* url8 = [NSString stringWithFormat:@"%@%f,%f (%@)", @"http://maps.google.com/map?f=q&q=", latitude, longitude, NSLocalizedString([NSString stringWithCString:title encoding:NSUTF8StringEncoding], @"")];
	NSString *uelEncode = (NSString*)CFURLCreateStringByAddingPercentEscapes(
									kCFAllocatorDefault,
									(CFStringRef)url8,
									NULL,
									NULL,
									kCFStringEncodingUTF8); 
 
	NSURL* url = [NSURL URLWithString:uelEncode];
	[[UIApplication sharedApplication] openURL:url];
}

こちらが 3.0 と 2.2系 での動作を比較した動画です。

カテゴリー: 開発 タグ: