読者です 読者をやめる 読者になる 読者になる

Androidアプリを高速化しよう

はじめに

この投稿はAndroid Advent Calendar 2014の25日目の記事です。

Androidアプリの開発をしていたのがきっかけで彼女が出来たyuyakaidoです。昨日のkaneshinthさんの記事の冒頭にあるように僕はマルチスレッド初心者なので常にシングルスレッドで動作しています。勿論クリスマスイブも。

今回は既存のアプリケーションのボトルネックを探すための手法を紹介していこうと思います。

お品書き

  • StrictMode
    • パフォーマンスに影響を及ぼすコードの検出
  • Traceview
    • パフォーマンス計測ツール
  • その他
    • Viewのネストについて
    • Viewの塗り潰しについて

環境

この記事で紹介するソースコードの動作確認は以下の環境で行いました。

StrictMode

StrictModeはGingerbreadから追加された機能で、アプリケーションのパフォーマンスに影響を及ぼす可能性のあるコードを検出してくれるものです。StrictModeはポリシーという形で何を検出するかを自由に設定することが出来ます。

使用方法

実際にコードを交えつつStrictModeの使用方法を解説していきます。

まず、StrictModeを有効にするためにカスタムApplicationクラスのonCreate()、もしくはアプリケーションのエントリーポイントとなるActivityのonCreate()に以下のコードを追加します。

StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                .detectAll()
                .penaltyLog()
                .build());

上記で設定したポリシーに違反するコードが検出された場合はlogcatにその旨が出力されます。新規で作成したプロジェクトに上記のコードを追加して実行した場合には何も出力されないのが確認出来るかと思います。

では、上記で設定したポリシーに違反するコードを書いてみましょう。適当な新規プロジェクトを作成して以下のコードを任意の箇所に記述してください。

String fileName = "sample.txt";
String fileContent = "Hello Android Advent Calendar!!";

File file = new File(Environment.getExternalStorageDirectory(), fileName);

try {
    FileOutputStream fileOutputStream = new FileOutputStream(file);
    fileOutputStream.write(fileContent.getBytes());
    fileOutputStream.close();
} catch (FileNotFoundException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}

上記のポリシーに違反しているコードを追加してアプリを実行してください。するとlogcatに以下のようなログが表示されるはずです。

12-20 21:08:33.894    5064-5064/com.yuyakaido.androidadventcalendar2014 D/StrictMode﹕ StrictMode policy violation; ~duration=24 ms: android.os.StrictMode$StrictModeDiskReadViolation: policy=31 violation=2
            at android.os.StrictMode$AndroidBlockGuardPolicy.onReadFromDisk(StrictMode.java:1089)
            (省略)
12-20 21:08:33.894    5064-5064/com.yuyakaido.androidadventcalendar2014 D/StrictMode﹕ StrictMode policy violation; ~duration=8 ms: android.os.StrictMode$StrictModeDiskWriteViolation: policy=31 violation=1
            at android.os.StrictMode$AndroidBlockGuardPolicy.onWriteToDisk(StrictMode.java:1063)
            (省略)

上記のログからディスク読み込み・書き込みに関するポリシー違反が検出されたことが分かります。 上記の2つのポリシー違反は以下の2行で発生しています。

  • 読み込み: FileOutputStream fileOutputStream = new FileOutputStream(file);
  • 書き込み: fileOutputStream.write(fileContent.getBytes());

上記はかなり単純な例でしたが、もう少し実践的な例を見てみましょう。 ActiveAndroidでデータをDBに保存して、その保存したデータをDBから取り出す場合を考えてみます。 ActiveAndroidに関しては、20日目のandrohiさんの記事をご参照ください。

まず、保存対象となるモデルクラスを用意します。

@Table(name = "Items")
public class Item extends Model {

    @Column(name = "Name")
    public String name;

}

次に先ほど作成した新規プロジェクトの任意の箇所に以下のコードを記述します。

Item item = new Item();
item.name = "NAME";
item.save();
item = new Select().from(Item.class).executeSingle();

上記のコードを追加してアプリケーションを実行すると、logcatに以下のようなログが出力されるはずです。

12-20 21:18:56.758    5350-5350/com.yuyakaido.androidadventcalendar2014 D/StrictMode﹕ StrictMode policy violation; ~duration=32 ms: android.os.StrictMode$StrictModeDiskWriteViolation: policy=31 violation=1
            at android.os.StrictMode$AndroidBlockGuardPolicy.onWriteToDisk(StrictMode.java:1063)
            at android.database.sqlite.SQLiteStatement.acquireAndLock(SQLiteStatement.java:226)
            at android.database.sqlite.SQLiteStatement.executeUpdateDelete(SQLiteStatement.java:84)
            at android.database.sqlite.SQLiteDatabase.executeSql(SQLiteDatabase.java:2020)
            at android.database.sqlite.SQLiteDatabase.execSQL(SQLiteDatabase.java:1960)
            at android.database.sqlite.SQLiteDatabase.endTransaction(SQLiteDatabase.java:736)
            at android.database.sqlite.SQLiteStatement.releaseAndUnlock(SQLiteStatement.java:273)
            at android.database.sqlite.SQLiteStatement.executeInsert(SQLiteStatement.java:115)
            at android.database.sqlite.SQLiteDatabase.insertWithOnConflict(SQLiteDatabase.java:1839)
            at android.database.sqlite.SQLiteDatabase.insert(SQLiteDatabase.java:1712)
            at com.activeandroid.Model.save(Model.java:155)
            at com.yuyakaido.androidadventcalendar2014.MainActivity.ActiveAndroidTest(MainActivity.java:27)
            at com.yuyakaido.androidadventcalendar2014.MainActivity.onCreate(MainActivity.java:21)
            at android.app.Activity.performCreate(Activity.java:4470)
            at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1053)
            at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1934)
            at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:1995)
            at android.app.ActivityThread.access$600(ActivityThread.java:128)
            at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1161)
            at android.os.Handler.dispatchMessage(Handler.java:99)
            at android.os.Looper.loop(Looper.java:137)
            at android.app.ActivityThread.main(ActivityThread.java:4514)
            at java.lang.reflect.Method.invokeNative(Native Method)
            at java.lang.reflect.Method.invoke(Method.java:511)
            at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:980)
            at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:747)
            at dalvik.system.NativeStart.main(Native Method)
12-20 21:18:56.758    5350-5350/com.yuyakaido.androidadventcalendar2014 D/StrictMode﹕ StrictMode policy violation; ~duration=10 ms: android.os.StrictMode$StrictModeDiskReadViolation: policy=31 violation=2
            at android.os.StrictMode$AndroidBlockGuardPolicy.onReadFromDisk(StrictMode.java:1089)
            at android.database.sqlite.SQLiteDatabase.rawQueryWithFactory(SQLiteDatabase.java:1678)
            at android.database.sqlite.SQLiteDatabase.rawQuery(SQLiteDatabase.java:1659)
            at com.activeandroid.util.SQLiteUtils.rawQuery(SQLiteUtils.java:106)
            at com.activeandroid.util.SQLiteUtils.rawQuerySingle(SQLiteUtils.java:122)
            at com.activeandroid.query.From.executeSingle(From.java:311)
            at com.yuyakaido.androidadventcalendar2014.MainActivity.ActiveAndroidTest(MainActivity.java:28)
            at com.yuyakaido.androidadventcalendar2014.MainActivity.onCreate(MainActivity.java:21)
            at android.app.Activity.performCreate(Activity.java:4470)
            at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1053)
            at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1934)
            at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:1995)
            at android.app.ActivityThread.access$600(ActivityThread.java:128)
            at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1161)
            at android.os.Handler.dispatchMessage(Handler.java:99)
            at android.os.Looper.loop(Looper.java:137)
            at android.app.ActivityThread.main(ActivityThread.java:4514)
            at java.lang.reflect.Method.invokeNative(Native Method)
            at java.lang.reflect.Method.invoke(Method.java:511)
            at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:980)
            at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:747)
            at dalvik.system.NativeStart.main(Native Method)

上記のログ内のスタックトレースを見るとDBに書き込む時、DBから読み込む時にポリシー違反が発生していることが分かると思います。

Traceview

TraceviewはAndroid SDKに付属しているパフォーマンス計測のためのツールで、開発中のアプリケーション内で実行されるメソッドの実行時間の計測や時系列の表示、メソッドのコールスタックの可視化等高速化の役に立つであろう様々な計測を行うことが出来ます。 以前はソースコードに計測開始・計測終了を指定するメソッドを埋め込まなければならず、さらに計測結果のファイルが端末内に保存されるためadb pullでホストマシンに引っ張ってくる必要がある等かなり面倒でしたが、Android Studio 1.0.1ではIDE上で計測開始・計測終了時にボタンを押すだけで良くなり、簡単にパフォーマンス計測を行うことが可能になりました。

使用方法

今回は簡単のために画面上に配置されたボタンを押すと以下の処理が走るようにします。

try {
    Thread.sleep(3000);
} catch (InterruptedException e) {
    e.printStackTrace();
}

以下のように画面上に適当なボタンを配置してそのボタンを押すと上記のコードが実行されるようにします。

f:id:yuyakaido:20141221001514p:plain

そして、Android Studio上で以下の画像で示す操作を行うことで簡単にパフォーマンス計測を行うことが出来ます。

f:id:yuyakaido:20141221001350p:plain

  • ①: Android DDMSを開く
  • ②: パフォーマンス計測を行うプロセスを選択(今回はcom.yuyakaido.androidadventcalendar2014を選択)
  • ③: 計測開始
  • ④: 計測終了

上記の操作を行うと以下に計測結果が表示されるはずです。

f:id:yuyakaido:20141221003359p:plain

上半分にはUIスレッド上の処理が時系列順に並んでいます。下半分には全メソッドの実行時間や総実行時間に対する各メソッドの実行時間の割合等が表示されています。

次にTraceviewの実践的な使い方を見ていきましょう。 Traceviewはメソッドのコールスタックを追うことが出来ます。

画面にボタンを追加して、そのボタンを押すと以下の処理が走るようにします。

ActiveAndroid.beginTransaction();
try {
    for (int i = 0; i < 100; i++) {
        Item item = new Item();
        item.name = "NAME";
        item.save();
    }
    ActiveAndroid.setTransactionSuccessful();
} finally {
    ActiveAndroid.endTransaction();
}

上記のコードを追加した上でパフォーマンス計測を行った結果を以下に示します。

f:id:yuyakaido:20141221011747p:plain

処理が開始された時点を矢印で示しています。矢印の少し下を見ると上記の処理が書かれているMainActivity.performActiveAndroidTransaction()が実行されていることが分かるかと思います。そして、このメソッド内ではsaveメソッドが100回呼ばれているので、MainActivity.performActiveAndroidTransactionの下にはModel.save()が並んでいます。

このようにTraceviewを使えばメソッドのコールスタックを辿ることが出来ます。このコールスタックを下からみていき、自分のプロジェクトパッケージに行き当たればその部分が根本原因であると特定することが出来ます。

その他

  • Viewのネストについて

15日目でkonifarさんも触れていますが、Viewのネストはパフォーマンスを低下させる一因になります。一般にLinearLayoutよりもRelativeLayoutを使った方がViewのネストが浅くなります。とはいえ、既存のものを書き換えるのはコスパが悪いので、そこまで神経質に全てを書き換える必要はないと思います。新規で作るレイアウトに関してはネストの深さを意識してみると良いのではないでしょうか。

  • Viewの塗り潰しについて

こちらも同じ方がここで言及しているように無駄にViewの塗りつぶしをするのはパフォーマンスを低下させる一因になります。

おわりに

この記事で紹介したソースコードは以下のリポジトリにアップロードしています。

https://github.com/yuyakaido/AndroidAdventCalendar2014