techium

このブログは何かに追われないと頑張れない人たちが週一更新をノルマに技術情報を発信するブログです。もし何か調査して欲しい内容がありましたら、@kobashinG or @muchiki0226 までいただけますと気が向いたら調査するかもしれません。

Stethoのdumpappを使ってアプリの情報を取り出す

Androidアプリを開発する際にアプリ内のデータにアクセスして何が保存されているかを取得してデバッグを効率的にできるようにすることは多々あると思います。
そこで今回紹介するのはStethoというFacebookが開発したライブラリのコマンドラインツールの作り方についてご説明します。

Stethoとは

StethoとはFacebookが開発したライブラリでGoogle ChromeのDevelopser Toolsの一部を利用してDBの中身を確認したり、ネットワークの通信速度を計測したりができるものとなります。
また、CLIのdumpappを用いるとAndroidと接続できる状態ならPC側のターミナルを操作することでデータ収集などを行える機能も備えています。
今回はCLIのdumpappの使い方をご説明します。
Developer Toolsを使ったやり方については次回にでもご説明したいと思います。

dumpappを使ってみる

まずdumpappを使うにはStethoのリポジトリ内にあるスクリプトを実行することで使えるようになります。
ダウンロードはこちら

ダウンロードできたら適当な場所に配置しリポジトリ内のscriptフォルダにパスを通しましょう。
ただし、dumpappを実行するにはpython3が必要となりますのでそちらが入っていない人はインストールをお願いします。
これでCLIの準備が整いました。

それでは次にアプリの方の準備を行います。
アプリ内でStethoの初期化を実行するとアプリ起動中はdumpappからの接続ができるようになります。

まずはAndroidアプリのapp/build.gradleに下記を記載してみましょう。

dependencies {
    <省略>
    compile 'com.facebook.stetho:stetho:1.3.1'
}

次にApplicationクラスに下記のコードを埋め込んでみましょう。

public class StethoApplicaiton extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        Stetho.initializeWithDefaults(this);
    }
}

5行目にStethoのinitializeWithDefaultsというのを行っているのがわかると思います。
initializeWithDefaultsdumpappのデフォルト機能を有効にし、初期化を実行するAPIになっています。
dumpappのデフォルト機能は次の4つとなります。

  • SharedPreferencesDumperPlugin : SharedPreferencesの中身を表示する/編集することができます
  • CrashDumperPlugin : クラッシュを意図的に発生させることができます
  • HprofDumperPlugin : Heap dumpをファイルにはきだしたりできます
  • FilesDumperPlugin : /data/dataの下に作られるアプリフォルダ内のファイルリストを表示することができます

それでは試しにFilesDumperPluginを動かしてみましょう。

ターミナルで下記のコマンドを打ってみましょう。

$ dumpapp -l
crash
files
hprof
prefs

-lオプションでdumpappが持つ機能をすべて表示できます。
デフォルトではこの4つとなります。
filesがFilesDumperPluginとなりますので呼び出すには下記の様になります。

$ dumpapp files ls
cache/slice-slice_9-classes.dex
cache/reload0x0000.dex
cache/slice-support-annotations-23.3.0_3c736939f093d4dfef80f60459901cb7e9c54daa-classes.dex
cache/slice-stetho-1.3.1_e2de68779bee6c38f74429700219f154e541e30a-classes.dex
cache/slice-slice_8-classes.dex
cache/slice-slice_7-classes.dex
cache/slice-slice_6-classes.dex
cache/slice-slice_5-classes.dex
cache/slice-slice_4-classes.dex
cache/slice-slice_3-classes.dex
cache/slice-slice_2-classes.dex
cache/slice-slice_1-classes.dex
cache/slice-slice_0-classes.dex
cache/slice-internal_impl-23.3.0_cf4289545c85756f91ab07b60972a469cefe78fb-classes.dex
cache/slice-jsr305-2.0.1_9963f6b53b672668cd8fe15451ded7dac85ee72e-classes.dex
cache/slice-commons-cli-1.2_8bc49dc92fbff628dbbf7e9e9b0c0b1b13f081e2-classes.dex
cache/slice-com.android.support-support-vector-drawable-23.3.0_0e5e62837fd2b65873d89694903de29ffce158d3-classes.dex
cache/slice-com.android.support-support-v4-23.3.0_d968a97500d0f14df93b1670d7e8a1cff931d05e-classes.dex
cache/slice-com.android.support-appcompat-v7-23.3.0_8a8f6ae71aeb6a701b5ad2c23ff9f6bba1659c0b-classes.dex
cache/slice-com.android.support-animated-vector-drawable-23.3.0_12971c849f0345ef66aa641cf2f1cee8fe54914c-classes.dex
files/instant-run/dex/slice-com.android.support-animated-vector-drawable-23.3.0_12971c849f0345ef66aa641cf2f1cee8fe54914c-classes.dex
files/instant-run/dex/slice-com.android.support-appcompat-v7-23.3.0_8a8f6ae71aeb6a701b5ad2c23ff9f6bba1659c0b-classes.dex
files/instant-run/dex/slice-com.android.support-support-v4-23.3.0_d968a97500d0f14df93b1670d7e8a1cff931d05e-classes.dex
files/instant-run/dex/slice-com.android.support-support-vector-drawable-23.3.0_0e5e62837fd2b65873d89694903de29ffce158d3-classes.dex
files/instant-run/dex/slice-commons-cli-1.2_8bc49dc92fbff628dbbf7e9e9b0c0b1b13f081e2-classes.dex
files/instant-run/dex/slice-internal_impl-23.3.0_cf4289545c85756f91ab07b60972a469cefe78fb-classes.dex
files/instant-run/dex/slice-jsr305-2.0.1_9963f6b53b672668cd8fe15451ded7dac85ee72e-classes.dex
files/instant-run/dex/slice-stetho-1.3.1_e2de68779bee6c38f74429700219f154e541e30a-classes.dex
files/instant-run/dex/slice-support-annotations-23.3.0_3c736939f093d4dfef80f60459901cb7e9c54daa-classes.dex
files/instant-run/dex/slice-slice_0-classes.dex
files/instant-run/dex/slice-slice_1-classes.dex
files/instant-run/dex/slice-slice_2-classes.dex
files/instant-run/dex/slice-slice_3-classes.dex
files/instant-run/dex/slice-slice_4-classes.dex
files/instant-run/dex/slice-slice_5-classes.dex
files/instant-run/dex/slice-slice_6-classes.dex
files/instant-run/dex/slice-slice_7-classes.dex
files/instant-run/dex/slice-slice_8-classes.dex
files/instant-run/dex/slice-slice_9-classes.dex
files/instant-run/dex-temp/reload0x0000.dex
files/instant-run/right/resources.ap_
files/instant-run/active
files/instant-run/left/resources.ap_

こんな感じで表示されます。
cacheとfilesのフォルダの中身がすべて羅列されているのがわかります。
今回はlsを実行しましたが他にもコマンドがあります。
下記のようにするとusageに何が設定できるかが確認できます。

$ dumpapp files
Usage: dumpapp files <command> [command-options]
       dumpapp files ls
       dumpapp files tree
       dumpapp files download <output.zip> [<path>...]

dumpapp files ls: List files similar to the ls command

dumpapp files tree: List files similar to the tree command

dumpapp files download: Fetch internal application storage
    <output.zip>: Output location or '-' for stdout
    <path>: Fetch only those paths named (directories fetch recursively)

これで動かし方がわかったので、今度はアプリ独自のコマンドを作ってみましょう。

独自のコマンドを作る

独自コマンドを作るには2つクラスを作る必要があります。
1つ目はDumperPluginsProviderを継承したクラス。
2つ目はDumperPluginを継承したクラスです。

DumperPluginsProviderはどのDumperPluginを有効にするかを記述するクラスになっており、前節で出てきたinitializeWithDefaultsdumpappは4つの機能を有効にするDumperPluginsProviderが内部で作成され利用されています。

DumperPluginはdumpappで実行時の動作を記述するクラスになります。
それでは作ってみましょう。

今回はAndroidのボタンをクリックした時の時刻をダンプし、その時刻を取得するdumpappのコマンドを作成しましょう。

まずはDumperPluginを作成します。
[TimeMeasurementDumperPlugin.java]

public class TimeMeasurementDumperPlugin implements DumperPlugin {

    private static final String NAME = "time";

    private static final String USAGE = "Usage: dumpapp " + NAME + " <length>";

    @Override
    public String getName() {
        return NAME;
    }

    @Override
    public void dump(DumperContext dumpContext) throws DumpException {
        PrintStream writer = dumpContext.getStdout();
        Iterator<String> args = dumpContext.getArgsAsList().iterator();

        String length = args.hasNext() ? args.next() : null;
        try {
            int len = Integer.valueOf(length);
            if (len >= 0) {
                doHistory(writer, len);
            } else {
                throw new DumpUsageException(USAGE);
            }
        } catch (NumberFormatException e) {
            throw new DumpUsageException(USAGE);
        }
    }

    private void doHistory(PrintStream writer, int length) throws DumpUsageException {
        ArrayList<String> list = ClickManager.getInstance().getHistory();

        for (int i = 0; i < list.size(); i++) {
            if (length < i) {
                break;
            }
            writer.println(list.get(i));
        }
    }
}

このコードのgetNameで返している文字列がdumpapp -lで表示されるコマンド名になります。
そのためNAMEでtimeと指定しているため下記のコマンドがこれで使えるようになります。

$ dumpapp time

そしてdumpメソッドは実際にコマンドが実行された時に呼び出されます。 ここにログとして出したいものを記述します。
引数のDumperContextのgetStdoutで取得できたPrintStreamに対して文字列を書き込むとターミナル上にその書き込んだ文字列が表示されるようになります。
ここではdoHistoryの中でClickManagerと呼ばれるクリック時刻履歴を取得してwriter.println(list.get(i));にて時刻を表示するようにしています。
また、DumperContextのgetArgsAsList().iterator()で取り出されたものはコマンドの引数の値となります。
そのため下記のように記載した場合はlengthに10という文字列が格納されます。

$ dumpapp time 10

これでDumperPluginの準備が整いました。
次にDumperPluginsProviderを作ります。
次のように記載します。

[StethoApplication.java]

    private static class CustomDebugDumperPluginsProvider implements DumperPluginsProvider {
        private final Context mContext;

        public CustomDebugDumperPluginsProvider(Context context) {
            mContext = context;
        }

        @Override
        public Iterable<DumperPlugin> get() {
            return new Stetho.DefaultDumperPluginsBuilder(mContext)
                    .provide(new TimeMeasurementDumperPlugin())
                    .finish();
        }
    }

DumperPluginsProviderのgetで返り値がそのままdumpapp -lで表示される内容となります。
そのためここでreternで返している内容はDefaultDumperPluginsBuilderを作成し、それに対してTimeMeasurementDumperPluginをprovideで追加指定ます。
最後にfinishを行うと先の4つの機能を付与を行った結果を得ることができます。

これで準備が整ったのでDumperPluginsProviderを設定し利用してみましょう。
ApplicationクラスのinitializeWithDefaultsdumpappを変更すると独自のコマンドを呼び出せるようになります。

public class StethoApplicaiton extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        Stetho.initialize(Stetho.newInitializerBuilder(this).enableDumpapp(new CustomDebugDumperPluginsProvider(this)).build());
    }

    <省略>
}

enableDumpappの引数にDumperPluginsProviderのインスタンスを渡すことで独自コマンドが使えるようになります。

この状態でコマンドを打つと下記のようになります。

$ dumpapp -l
crash
files
hprof
prefs
time

アプリにボタンを押して、押した時にログを貯めるようにすると下記の用に表示されれます。

$ dumpapp time 5
Thu May 05 11:29:31 GMT+09:00 2016
Thu May 05 11:29:31 GMT+09:00 2016
Thu May 05 11:29:31 GMT+09:00 2016
Thu May 05 11:29:32 GMT+09:00 2016
Thu May 05 11:29:33 GMT+09:00 2016

ボタンを押した時のコードは今回あまり重要でないのでどのようになった上記の用になった確認したい人はサンプルコードを参考にしてください。

サンプルコード

github.com