Androidアプリを高速化しよう

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

はじめに

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

今回はAndroidアプリのボトルネックを探すための手法を紹介していこうと思います。

目次

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

環境

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

StrictMode

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

基礎編

まず、StrictModeを有効にするためにApplicationのonCreateメソッド、もしくはActivityのonCreateメソッドに以下コードを追加します。

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

では、上記ポリシーに違反するコードを書いてみましょう。

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();
}

上記コードを実行すると以下ログが表示されるはずです。

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)
            (省略)

上記ログからディスクI/Oに関するポリシー違反が検出されたことが分かります。

実践編

ActiveAndroidでデータをDBに保存して、その保存したデータをDBから取り出す場合を考えてみます。

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

@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();

上記コードを実行すると以下ログが出力されるはずです。

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に付属しているパフォーマンス計測のためのツールで、開発中のアプリ内で実行されるメソッドの実行時間の計測や時系列の表示、メソッドのコールスタックの可視化といったアプリの高速化の役に立つであろう様々な計測を行うことが出来ます。

以前はソースコードに計測開始・終了を指定するメソッドを埋め込まなければならず、さらに計測結果のファイルが端末内に保存されるので取り出しが面倒でしたが、Android Studioでは計測開始・終了時にボタンを押すだけで良くなり、簡単にパフォーマンス計測を行うことが可能になりました。

基礎編

適当な箇所に以下コードを記述します。

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

Android Studioで以下操作を行うことで簡単にパフォーマンス計測を行うことが出来ます。

f:id:yuyakaido:20141221001350p:plain

  1. Android DDMSを開く
  2. パフォーマンス計測を行うプロセスを選択
  3. 計測開始
  4. 計測終了

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

f:id:yuyakaido:20141221003359p:plain

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

実践編

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

処理が開始された時点を矢印で示しています。今回の検証コードではModelのsaveメソッドが100回呼ばれているので、Modelのsaveメソッドがいくつも並んでいます。

このようにTraceviewを使えばメソッドのコールスタックを辿ることが出来ます。このコールスタックで自身のコードに行き当たればその部分が根本原因であると特定することが出来ます。

その他

Viewのネストについて

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

Viewの塗り潰しについて

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