アプリの起動パフォーマンスを向上することで、快適なユーザーエクスペリエンスを実現できます。アプリの起動が遅いとユーザーはストレスを感じ、ユーザー離脱にもつながります。対策として、コードの一部を最適化することで、起動時間を短縮できます。本記事では、最初の段階のアプリ起動パフォーマンスを測定し、改善するためのベストプラクティスを紹介します。
起動時間と読み込み時間を測定することで、起動全体のパフォーマンスを正確に把握できます。そのためのツールを実装するには、まずActivityで利用可能なAPIを呼び出します。その後ログを確認して、アプリが起動してからアプリのリソースとビュー階層の表示が完了するまでの経過時間を計算します。さまざまなアプリの起動シナリオをデバイスのローカルで再現することで、更新による影響をリアルタイムで簡単にテストし、測定できます。
Fire OSでは、アプリの起動時間を測定するためのデータポイントが2つあります。それは、「最初のフレームが表示されるまでの時間」と「使用可能な状態になるまでの時間」(RTU)です。アプリを操作するときの起動エクスペリエンスを向上するために、この2つを分析して比較しましょう。それぞれの測定値の詳細は、以下の通りです。
最初のフレームが表示されるまでの時間とは、アプリが起動してから最初のフレームがユーザーに表示されるまでにかかる時間です。アプリはこの間に、バックグラウンドの描画、ナビゲーションの開始、ローカルアセットの読み込み、ローカルに読み込まれたコンテンツまたはリモートのエンドポイントから取得したコンテンツのプレースホルダのレンダリングを行います。
Fire OS 5(APIレベル22)以上では、logcatからDisplayed
と呼ばれる値が提供されます。これは、プロセスを起動してから、対応するアクティビティの最初のフレームが画面上に完全に描画されるまでに要した時間を表します。この値がアプリの「最初のフレームが表示されるまでの時間」です。
使用可能な状態になるまでの時間(RTU)は、アプリが起動してから、完全に画面上に描写され、ユーザーが操作できるようになるまでにかかる時間です。RTUの時点で、アプリは画面上でのレンダリングをすべて完了しており、リモートのエンドポイントから取得したコンテンツが表示されます。ネットワーク経由でリモートのリソースを読み込む際に明らかな遅延が発生する場合は、ユーザーに進行状況を示すインジケーターを表示するとよいでしょう。
Fire TV対応アプリとAndroidアプリでは、「遅延読み込み」によってウィンドウの初期描画を行い、バックグラウンドでリソースを非同期に読み込んで、ビュー階層を更新できます。この場合、すべてのリソースの読み込み完了とビューの表示を別々の指標とみなし、手動でreportFullyDrawn()
をトリガーして、アクティビティが遅延読み込みによって終了したことをシステムに認識させます。この値がアプリの「使用可能な状態になるまでの時間」です。注:この指標を確実にレポートするには、reportFullyDrawn()
をトリガーする前に、アプリのログを確認して対象となるアクティビティを特定することをお勧めします。
Amazon Fire TV対応アプリやゲームをAmazonアプリストアに申請する際、アプリが一連の基準に満たない場合は公開できません。アプリを開発する際は、Amazonのドキュメントに記載されている起動時間テストの基準を、アプリの申請前に主なパフォーマンス指標を改善するためのガイドとして使用してください。
注: このセクションに示す各測定を実行する場合、まずアプリとそのプロセスをバックグラウンドで実行していないことを確認してください。また、平均値を取得するために、これらの値を測定してからアプリを強制終了するという操作を複数回行う必要があります。
アプリを起動し、ログを確認して最初のフレームが描画されるまでにかかった時間を測定します。
ActivityManager: Displayed {package}/{activity}: +1s534ms
アプリを起動し、ユーザーがアプリを完全に操作できるようになるまでにかかった時間をログから測定します。
ActivityManager: Fully drawn {package}/{activity}: +4s54ms
アプリを起動し、リモコンのホームボタンを押してアプリをバックグラウンドで実行するようにします。Fire TVの最近使用したアプリの項目でアプリを選択し、再起動してフォアグラウンドに移動します。ログを確認して最初のフレームが描画されるまでにかかった時間を測定します。
ActivityManager: Displayed {package}/{activity}: +1s534ms
アプリを起動し、リモコンのホームボタンを押してアプリをバックグラウンドで実行するようにします。Fire TVの最近使用したアプリの項目でアプリを選択し、再起動してフォアグラウンドに移動します。ログを確認して、アプリが完全に読み込まれるまでにかかる時間、つまりユーザーがアプリを操作できる画面になるまでの時間を測定します。
ActivityManager: Fully drawn {package}/{activity}: +4s54ms
system_server
は、ユーザーがアプリを使用し始めていない、最初のフレームが描画された時点のタイムスタンプを選択します。そのため、Activity.reportFullyDrawn()
を呼び出す必要があります。reportFullyDrawn()
を呼び出すと、system_server
は受け取った時点のタイムスタンプを登録します。その方が計測したい条件に近くなります。
使用するアーキテクチャによっては、reportFullyDrawn()
を呼び出す適切なタイミングの特定が困難になる場合があります。その場合は一般的に、技術面より、カスタマーエクスペリエンスを重視することをお勧めします。具体的には、ユーザーが快適に操作できるアプリが完成したら、reportFullyDrawn()
を呼び出す必要があります。たとえば、現在表示されていない画面の一部のアセットがまだバックグラウンドで読み込まれている場合に、プレースホルダー要素がレンダリングされている状態であれば、呼び出すことができます。
reportFullyDrawn()
を効果的に実装することで、アプリのユーザーエクスペリエンスが向上します。また、データ処理、ネットワークアクティビティ、マルチスレッドは、ユーザーエクスペリエンス全体の基盤として優先すべき重要な要素であり、その全体像を把握する必要があります。
「コールド」スタートと「ウォーム」スタート両方へのアプローチ
コールドスタートではActivity#onCreate
メソッドを呼び出します。アプリ開発の最終段階で、このメソッドをオーバーライドしてFire TV機能統合に固有のライブラリを初期化します(Alexa VSKやADMなど)。アプリのonCreate
からAlexa Client Libraryを初期化する詳細については、Amazonアプリストア開発者向けドキュメントを参照してください。
ウォームスタートは、アプリのビュー階層がActivity#onResume
の呼び出し時点にインフレートされる直前の、Activity#onCreate
が呼び出される時点から測定され始めます。状況によってはonCreate
が呼び出されないため、Activity#onResume
メソッドをオーバーライドし、reportFullyDrawn()
呼び出しを追加してウォームスタートの「使用可能な状態になるまでの時間」を追跡するとよいでしょう(例:ユーザーがホームボタンを押してFire TVの最近使用したアプリの行からアプリに戻った場合、アクティビティはフォアグラウンドに移動し、onResume
のみが呼び出されます)。
WebView
を使用する場合、リモートのウェブアプリのUIが完全に読み込まれた後にreportFullyDrawn()
を呼び出す必要があります。アプリでWebViewClient#onPageFinished
メソッドをオーバーライドすることを検討してください。それにより、使用可能時間がページの読み込み時にのみ報告されるようにし、タッチ、ボタンクリックなどのその他のユーザーイベントに適用されないようにします。
注: アプリのログを実証分析し、さまざまなアクティビティライフサイクル段階で実行されるアクションを特定します。これにより、適切な段階で確実にreportFullyDrawn()
をトリガーします。
WebViewの実装の例:
// MainActivity.java
public class MainActivity extends Activity {
...
private static final Uri mBaseUri = Uri.parse("https://mysampleapp.com");
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (savedInstanceState == null) {
getSupportFragmentManager().beginTransaction()
.replace(R.id.main_browse_fragment, new MainFragment())
.commitNow();
}
WebView mWebView = findViewById(R.id.web_layout);
mWebView.getSettings().setJavaScriptEnabled(true);
mWebView.loadUrl(mBaseUri.toString());
mWebView.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
reportFullyDrawn();
}
});
}
...
}
滞りなく素早く起動し、スムーズなユーザーエクスペリエンスを実現するには、アプリによるリソースの読み込み方法と、素早い起動を妨げる要因について検討します。また、Fire OS向けのスプラッシュスクリーンの開発に関する関連記事も参照してください。アプリの起動時間を改善するためのヒントをいくつか紹介します。
エミュレーターでなくデバイスで測定する
さまざまなデバイスとFire OSのバージョンでアプリをテストして、アプリが素早く起動し、期待通りに動作することを確認しましょう。これにより、特定のデバイスで発生する可能性のあるパフォーマンスの問題を特定できます。エミュレーターは開発段階の機能を簡単に評価できる便利なツールですが、パフォーマンスの測定には不向きです。アプリのパフォーマンスを最適化するうえで、CPU、メモリ、ストレージなどの物理的なデバイスの機能が重要な役割を果たすためです。
公開済みアプリの起動時間とサイドロードされたアプリの起動時間を比較しない
Amazonアプリストアからダウンロードしてインストールした場合と、デバイスに直接インストールした場合では、結果が異なります。たとえば、Amazonアプリストアで公開する場合、Amazonアプリストアのサービスに適応するためのコードが挿入されますが、これはサイドロードされたアプリには当てはまりません。
公開済みアプリとして正確なテストを実施するには、Amazonのライブアプリテスト(LAT)サービスを使用してください。アプリをLATにアップロードすると、Amazonアプリストアの本番環境でアプリが一連のAmazonサービスの対象となります。LATを利用することで、Amazonアプリストアでユーザーに公開されているアプリと同じようにアプリが動作します。
遅延読み込み、集中的なタスクの遅延、アプリ起動のプロファイリング
アプリの起動時間に影響を与える大きな要因の1つは、アプリのリソースサイズです。アプリが使用するリソースが多いほど、起動にかかる時間が長くなります。アプリ起動の最初の段階では特に、可能な限り使用するリソースの数を少なくするようにしてください。つまり、使用するイメージ、データ、スレッドを減らします。
「遅延読み込み」は、アプリが必要なリソースのみを読み込む手法です。アプリの起動時に一部のライブラリを初期化する必要がない場合は、それらを遅延読み込みするか、自動初期化を無効にします。Android Studio Profilerを使用すると、最も読み込み時間が長いリソースを特定できます。これにより、事前に読み込むデータの量を減らし、アプリの起動時間を短くします。
素早く描画できるシンプルで静的なUIから起動することをお勧めします。たとえば、実際のビューの代わりにビューのプレースホルダーを使用するとよいでしょう。reportFullyDrawn()
を呼び出した後にのみ、複雑なアニメーションおよび描写、UI操作を有効化してください。また、最初に静的データを表示して、アプリの起動をそれ以上遅らせずにユーザーの注意をアプリに向けることもできます。バックグラウンドのスレッドは、アプリの読み込み中にバックグラウンドでタスクを実行できるようにすることで、アプリの起動時間を改善します。これにより、アプリが利用可能になるまでのユーザーの待機時間を減らせます。
ユーザーにとって快適なデータ管理を検討する
データ管理は素早いアプリ起動のキーポイントであり、アプリのライフサイクルで包括的にアプローチする必要があります。 アプリの起動時にユーザーに提示するデータについて考慮し、そのデータを取得したら永続的に保存してください。
さまざまな保存形式の速度について検討します。たとえば、データベースがアプリ内で読み込まれる場合、そのプラットフォームに組み込まれたファイル操作によってデータを保存する場合と比べて、起動速度が遅くなる可能性があります。
対処方法として、アプリの起動時に、関連データをファイルシステムに直接シリアル化します。データ表現を定義してjava.io.Serializable
インターフェイスを実装し、次の例に示すようにAndroidのObjectOutputStreamクラスとObjectInputStreamクラスを使用してください。
public void serialize(Context context, String filename, Serializable object) throws Exception {
FileOutputStream fOut = context.openFileOutput(filename, Context.MODE_PRIVATE);
ObjectOutputStream oOut = new ObjectOutputStream(fOut);
oOut.writeObject(object);
oOut.close();
}
public Object deserialize(Context context, String filename) throws Exception {
FileInputStream fIn = context.openFileInput(filename);
ObjectInputStream oIn = new ObjectInputStream(fIn);
Object result = oIn.readObject();
oIn.close();
return result;
}
このアプローチでは、最初にユーザーに提示するデータの迅速なシリアル化と逆シリアル化が可能であり、追加のフレームワークを使用する場合と比べてアプリの起動時間が大幅に短くなります。
測定を複数回行って、平均値を算出する
reportFullyDrawn()
の呼び出しを最適化するには、アプリのライフサイクルのさまざまな段階で何が行われているかを注意深く分析する必要があります。そのために、Androidにはマクロベンチマーク指標を取り込むためのパフォーマンスツールとサンプルが用意されています。複数の測定を行った後で平均値を算出し、アプリの起動時の処理が多すぎないか、または起動時に初期化しようとするリソースが多すぎないかを把握する必要があります。
この点を改良できるように、アプリのベンチマークを自動化するサンプルスクリプトを用意しました。com.example.myapplication
と.MainActivity
を、それぞれ実際のパッケージ名とアクティビティ名に置き換えてください。
// app_start_times.kts
import java.util.Scanner
private val CMD_LOGCAT = "adb logcat"
private val CMD_HOME = "adb shell input keyevent 3"
private val COOLDOWN_MILIS: Long = 1 * 1000
//APP PARAMETERS
private val PACKAGE_NAME = "com.example.myapplication"
private val ACTIVITY_NAME = "MainActivity"
//SCRIPT PARAMETERS
private var MEASURE_TIMES = 10
private var MEASURE_COLD_START = true
private var MEASURE_WARM_START = false
val logcatScanner = startAdbLogcat()
prepareApp(MEASURE_COLD_START, logcatScanner)
runMeasureLoop(MEASURE_COLD_START, MEASURE_TIMES, logcatScanner)
prepareApp(MEASURE_WARM_START, logcatScanner)
runMeasureLoop(MEASURE_WARM_START, MEASURE_TIMES, logcatScanner)
fun startAdbLogcat(): Scanner{
val logcatProcess = Runtime.getRuntime().exec(CMD_LOGCAT)
println("ADB Logcat Started")
Thread.sleep(COOLDOWN_MILIS)
val stream = logcatProcess.inputStream
val scanner = Scanner(stream)
scanner.useDelimiter("\n")
return scanner
}
fun prepareApp(isWarmStart: Boolean, scanner: Scanner) {
println("Step 1 - PREPARATION")
val appStart = generateAppStartAdbCommand()
val appStop = generateAppStopAdbCommand()
val fullyDrawm = generateFullyDrawnLog()
if(isWarmStart) {
println(" - Opening app once and putting to background (for WARM START measure)")
// To have proper WARM START benchmarks, open the app once,
// wait until it is fully drawn and put it in the background
Runtime.getRuntime().exec(appStart)
waitForLog(fullyDrawm, scanner)
Thread.sleep(COOLDOWN_MILIS)
Runtime.getRuntime().exec(CMD_HOME)
} else {
println(" - Making sure that the app is closed (for COLD START measure)")
Runtime.getRuntime().exec(appStop)
}
}
fun runMeasureLoop(isWarmStart: Boolean, benchmarkTimes: Int, scanner: Scanner) {
var totalMillis: Long = 0
println("Step 2 - BENCHMARKING $benchmarkTimes TIMES")
for(i in 1..benchmarkTimes) {
println(" - Run $i")
val benchmark = benchmarkApp(isWarmStart, scanner)
totalMillis = totalMillis + benchmark
Thread.sleep(COOLDOWN_MILIS)
}
println("Step 3 - RESULTS")
println(" - Start type: ${if(isWarmStart) "WARM START" else "COLD START"}")
println(" - Times benchmarked: $benchmarkTimes")
println(" - Average start time: ${totalMillis/benchmarkTimes} milliseconds")
}
fun waitForLog(logToSearch: String, scanner: Scanner): String {
while (scanner.hasNext()) {
val str = scanner.next().trim()
if(str.contains(logToSearch)) {
return str
}
}
return ""
}
fun benchmarkApp(isWarmStart: Boolean, scanner: Scanner): Long {
val appStart = generateAppStartAdbCommand()
val appStop = generateAppStopAdbCommand()
val logToSearch = if(isWarmStart) generateFullyDrawnLog() else generateTimeToDisplayLog()
Runtime.getRuntime().exec(appStart)
val log = waitForLog(logToSearch, scanner)
val millis = extractMiliseconds(log)
Thread.sleep(COOLDOWN_MILIS)
if(isWarmStart) {
println(" * WARM START at $millis milliseconds")
Runtime.getRuntime().exec(CMD_HOME)
} else {
println(" * COLD START at $millis milliseconds")
Runtime.getRuntime().exec(appStop)
}
return millis
}
fun extractMiliseconds(logcat: String): Long {
//1s234ms -> 1234 millis
//345ms -> 345 millis
var milis = logcat.split(" ").last().filter { it.isDigit() }
return milis.toLong()
}
fun generateAppStartAdbCommand(): String {
return "adb shell am start -n ${PACKAGE_NAME}/.${ACTIVITY_NAME}"
}
fun generateAppStopAdbCommand(): String {
return "adb shell am force-stop ${PACKAGE_NAME}"
}
fun generateTimeToDisplayLog(): String {
return "Displayed ${PACKAGE_NAME}/.${ACTIVITY_NAME}"
}
fun generateFullyDrawnLog(): String {
return "Fully drawn ${PACKAGE_NAME}/.${ACTIVITY_NAME}"
}
ここで紹介したベストプラクティスを参考にして、Fire TV対応アプリの起動時間を改善しましょう。アプリを効率化し、最新のツールと手法を活用することで、確実にユーザーエクスペリエンスを向上させましょう。