優れたパフォーマンスは誰もが喜びます。素晴らしいウォッチ フェイスのアイデアが浮かんだら、公開するそのウォッチ フェイスが細部にまで注意されているか確認しましょう。
ウォッチ フェイスは、キャンバスを操作する onDraw メソッドを中心に処理されます。これは非常に柔軟にデザインできることを意味しますが、その一方でパフォーマンスにも注意が必要になります。このブログ記事では、サンタ トラッカーのウォッチ フェイスを最適化していく過程を通じて、主にパフォーマンスの向上について説明していきます。fps の数値を 18 fps から 42 fps へと 2 倍以上にすることで、アニメーションのサブピクセルがなめらかに表示されるようになります。
開始時点 - 18 fps
このサンタ ウォッチ フェイスは、ビットマップが何層も重ねられて最終的な画像を形成しています。各層には、下から上まで次のような画像が並んでいます。
- 背景(固定)
- 中心に向かって移動する雲
- 時間の目盛り(固定)
- サンタとそりの画像(固定)
- サンタの腕 - 時針と秒針
- サンタの頭(固定)
これらの画像をそれぞれ実際に見ていきます。
大きな画像はパフォーマンスを低下させる(+14 fps)
大きさを変えたり回したりする場合は特に、画像のサイズは Wear アプリケーションのパフォーマンスにとってきわめて重要です。無駄なピクセル領域(下図のサンタの腕など)は、よく見られる誤ったアセットの使い方です。
修正前: 584 x 584 = 341,056 ピクセル | 修正後: 48 x 226 = 10,848 ピクセル(97% 減少) |
|
|
最終画像を構成する各層の画像について、それぞれ同じ大きさにすれば細かい位置の調整は必要ありませんので、そうしたいと思うのは当然です。ただ上図のサンタの腕と同じように、このやり方では問題を生じます。腕が正しい場所に位置する一方で、透過的なものであっても画像のサイズは増加し、メモリのフェッチ時にパフォーマンスの問題が生じます。デザイン チームとの協力のもと、付け加えたり動かしたりする部分だけを画像から切り抜いて使ったとしても、各パーツが 1 つの層を構成するということはシステムに自動的に認識されます。
もとの画像は全画面の大きさであるため、たとえほとんどの領域が透明であっても、システムはすべてのピクセルの影響を確認しなければならず、処理にかかる負荷は増加します。スペースを小さくすることで、きわめて大きなパフォーマンスの向上が見込めます。両方の腕の画像を修正すると、サンタ ウォッチ フェイスのフレームレートは 10 fps 向上して 28 fps になります(fps 56% 上昇)。同様に、サンタの顔と体のレイヤーをクロップすることで、4 fps (fps 22% 上昇)改善されます。合計 14 fps、かなりの改善と言えます。
ビットマップを結合する(+7 fps)
時計の目盛りの層は雲の層の上部に重ねるのが理想ですが、雲の層は透過的ですので、目盛りの層を下部においても見かけ上はあまり変わりません。そのため、目盛りの層と背景の層を結合できるということになります。
+
2 つのビューを結合することで、それらをアルファブレンドする処理の時間がなくなり、重要な CPU の処理時間の低減にもつながります。パフォーマンスを向上するには、アルファブレンドされるリソースをできるかぎり折りたたんでまとめることが効果的です。2 つの全画面ビットマップを結合することで、7 fps 向上します(fps 39% 上昇)。
アンチ エイリアスとフィルタ ビットマップ フラグ - どちらが有効か(+2 fps)
Android Wear を搭載した時計は、さまざまな形状やサイズで存在します。そのため、ときにはビットマップを画面に表示する前に、画像のサイズの変更が必要になることもあります。ただし、ビットマップをなめらかに表示するためにどの方法を用いるべきかが明確でない場合もあります。
canvas.drawBitmap では、
Paint オブジェクトを用いる必要があります。ここでは、2 つの重要なオプション、
anti-alias (アンチ エイリアス)と
FilterBitmap (フィルタ ビットマップ)を設定します。以下、それぞれについて説明します。
- アンチ エイリアスは、端が透明なビットマップに対しては効果がありません。多くの場合、Paint オブジェクトを作成する際、アンチ エイリアスのオプションはデフォルトのオンのままになっています。しかし、このオプションはベクター オブジェクトに対してのみ効果があります。ビットマップに対しては、画像の曲線部で角張った端をぼやけさせるために使われますが、端のピクセルが透明である場合(ほとんどのウォッチ フェイスではそうですが)、なにも変化しません。左下の図はアンチ エイリアスをオンにしたもの、右下の図はオフにしたものです。あまり変わりはありませんので、アンチ エイリアスオプションをオフにすることは、パフォーマンスの向上に有効であるといえます。アンチ エイリアスをオフにすると、さらに 2 fps (fps 11% 上昇)改善します。
- 他のオブジェクトの上層にあるすべてのビットマップ オブジェクトで、フィルタ ビットマップをオンにします - このオプションは、drawBitmap がコールされる場合、端をなめらかにします。ビットマップのサイズを変更する Bitmap.createScaledBitmap のフィルタ オプションと似ていますので混同しないよう注意が必要です。どちらの設定も、オンにする必要があります。下図はサンタの腕の拡大図です。左の図はフィルタ ビットマップをオフにしたもの、右の図はオンにしたものです。
onDraw のループで負荷のかかる呼び出しを取り除く(+3 fps)
onDraw は、ウォッチ フェイスでもっとも重要な関数呼び出しです。すべてのドローアブル フレームで呼び出され、終了するまで実際の描画プロセスは実行されません。そのため onDraw メソッドはできるかぎり軽く効率よく実行されることが重要です。次に示すのは、デベロッパーが直面する、回避可能な共通の問題の例です。
- 重くて共通するコードを事前計算関数に移す - たとえば R.array.cloudDegrees を共通して要求している場合、その処理は onCreate で行うようにして onDraw ループでは参照のみにします。
- onDraw で同じ画像変換を繰り返さない - 実行時に画面サイズに合わせてビットマップのサイズを変更する処理は共通のものですが、onCreate.では行うことができません。onDraw で繰り返しビットマップのサイズを変更せずに済むように、幅と高さがわかる場合は onSurfaceChanged を上書きして画像のサイズを変更します。
- onDraw にオブジェクトを割り当てない - オブジェクトを割り当てるとメモリ領域が激しく消費され、ガベージ コレクションが引き起こされます。結果としてフレーム レートが低下します。
- Android Device Monitor などのツールを使って CPU パフォーマンスを解析する - onDraw は短い時間で定期的に実行されることが重要です。
上記のようなことに注意すると、レンダリングのパフォーマンスが劇的に向上します。
最初のバージョンでは、サンタの onDraw ルーティンに次のような不適切なコードが含まれていました。
int[] cloudDegrees =
getResources().getIntArray(R.array.cloudDegrees);
これではあらゆる呼び出しでリソースから int 配列が読み込まれ、高い負荷がかかります。この行を削除することで、さらに 3 fps 向上します(fps 17% 上昇)。
サブピクセルのなめらかなアニメーション
上記の対処の結果 fps は 44 となりますが、冒頭で述べた数値 42 fps とはまだ異なっています。その理由は、canvas.drawBitmap の制限から生じています。このコマンドでは、浮動する左上部詰めの配置設定がなされていますが、
後方互換性のため純粋に翻訳されたものであれば、API は実際には整数のみに対応します。結果として、雲はピクセル全体を 1 単位として移動することになり、不自然な動きのアニメーションになります。サブピクセルをなめらかにするには、雲を事前に回してからサンタに移動させるのではなく、実際に描画して回転させる必要があります。この回転動作の負荷として 2 fps がかかります。アニメーションがなめらかなサブピクセルで実現されるため、余分な負荷がかかってもその価値はあります。
修正前 - 高速だが不自然な動き
for (int i = 0; i < mCloudBitmaps.length; i++) {
float r = centerX - (timeElapsed / mCloudSpeeds[i]) % centerX;
float x = centerX +
-1 * (r * (float) Math.cos(Math.toRadians(cloudDegrees[i] + 90)));
float y = centerY -
r * (float) Math.sin(Math.toRadians(cloudDegrees[i] + 90));
mCloudFilterPaints[i].setAlpha((int) (r/centerX * 255));
Bitmap cloud = mCloudBitmaps[i];
canvas.drawBitmap(cloud,
x - cloud.getWidth() / 2,
y - cloud.getHeight() / 2,
mCloudFilterPaints[i]);
}
修正後 - 若干ゆっくりだがなめらかなサブピクセル
for (int i = 0; i < mCloudBitmaps.length; i++) {
canvas.save();
canvas.rotate(mCloudDegrees[i], centerX, centerY);
float r = centerX - (timeElapsed / (mCloudSpeeds[i])) % centerX;
mCloudFilterPaints[i].setAlpha((int) (r / centerX * 255));
canvas.drawBitmap(mCloudBitmaps[i], centerX, centerY - r,
mCloudFilterPaints[i]);
canvas.restore();
}
左が修正前: 整数翻訳値による不自然なアニメーション。右が修正後: なめらかなアニメーション。
腕時計の質を高める
ウォッチ フェイスは Android Wear のなかでも、一番目立つ UI です。それを使ってどのような作品を作るかは、製作者次第です。腕時計の質を高めていきましょう。
Posted by
Takeshi Hagikura - Developer Relations Team