Unyablog.

のにれんのブログ

Android Studioでのビルドをクラウドで行い開発を高速化する

こんにちは。学校も始まり最近は健康な時間に起きています*1

今回は 手元の Android Studio からビルドのみをクラウドで行う ことによって快適な生活を手に入れる話です。

概要

  • ビルドを移譲できる Android Sutudio のプラグインを書きました
  • これを使ってクラウドのリッチなマシンでビルドを行い高速化
  • ビルドのみが移譲されるので操作感は変わらない
  • 実装は割りと無理矢理

本題

動機

最近 Android のビルドが遅くてつらいなあという気分でいました。

Javaコンパイル (Make) ぐらいならいいのですが、デバッグ等をするときにライブラリが多いため Dex 処理にすごく時間がかかります。Dex 処理は一行変えただけでも行われるので非常につらい。

足りないのはメモリとCPUで、とにかくマシンパワーが必要なのですが、まだ自分の PC を変える時期ではない*2

結局 クラウドで強いインスタンス借りてビルドすればいいじゃん! という結論にたどり着き、いろいろ試しました。マシンパワーは大事。

そのうちの一つがこの記事です。(完全にネタ)

nonylene.hatenablog.jp

Android Studio で外部でビルドだけ行う方法をググったりしたのですがどこでも見つからず、Gradle プラグインでも作って無理矢理タスク実行するしかないかなあ…と思っていたのですが、よく見ると APK 作成するタスクが端末にインストールするタスクと分離されていました。今回はそこを弄ってみることにしました。

環境

  • Android Studio 1.5 / 2.0 / 2.1
    • リフレクションや内部API叩いてるので5年後も動いてるようなことは無さそう
    • 1.4 以前にはない API を用いています
    • 2.2 では既に動いてないみたいです(追記)*3

流れ

  1. Android Studio プラグインが実行すべきタスクとパラメーターを取得する

  2. 予め設定されたコマンド(今回の場合 Shell Script)を叩き、タスク等をオプションとして渡す

  3. Shell Script では受け取ったオプションを基に外部に ssh してソース等を rsyncgradlew を実行する。

  4. 外部から成果物を rsync で持ってきて、それを端末にインストールしデバッグ

プラグイン

Android Studioデバッグなどで Run を行う前に Gradle-aware Make を行い、そこで APK が生成されます。

f:id:nonylene:20160427030028p:plain

ここでは他のタスクを追加することができ、例えば External Tool を選択すると Shell などの適当なプログラムを実行することができます。

f:id:nonylene:20160427030322p:plain

ここで Gradle-aware Make 相当のことを外部で行えば、手元では APK を端末にインストールするのみとなります。もし外部ビルドをデバッグだけに限定するなら専用の Shell Script を書けば良いのですが、それでは Release や Instant Run などの時に使うことができません。

"今どんな Gradle タスクを実行すべきか" はこの機能では渡せないのでプラグインを作成しました。

github.com

このプラグインでは Android Studio の内部メソッドを直接叩くことによって 行うべきタスクとパラメーター を取得しています。

GradleInvokerOptions options = GradleInvokerOptions.create(myProject, context, configuration, env, null);

こうして得たオプションを基にコマンドを組み立て、先ほどの External Tool の機能を利用するために内部的に Tool を(リフレクションを一部用いて)作成し、コマンド付きの Shell Script を実行する、というものです。

このプラグインを入れると Gradle Make on External Tool という項目が追加されるので Gradle-aware Make をこれに置き換えます。

f:id:nonylene:20160428014116p:plain

するとインストールされる前に Shell Script を実行できます。

実行するコマンドはプラグインの設定画面(External Tool のダイアログを簡略化したもの)で設定するようにしました。

$GRADLE_TASKS$ と書くと :app:assembleDebug,:app:assemble,:app:assembleRelease, のように置き換わります。

f:id:nonylene:20160427032726p:plain

プラグインから SSHrsync を直接行うこともできたのですが、環境依存しやすいのと気軽に弄ることができないのでタスクを取り出して渡すだけにしました。

Shell Script で外部ビルドを実行

まずはサーバーを用意します。僕は下のような Ansible Playbook を用いています。

github.com

そしてオプションからタスクを受け取り rsync したり gradlew を打ったりするスクリプトを書きました。

#!/usr/bin/env bash

cd `dirname $0`

## set remote server ip address or alias name
server=""

set -e

while getopts a?t:m:p: OPT; do
    case $OPT in
        a) arguments=$OPTARG
            ;;
        t) tasks=$OPTARG
            ;;
        m) module_dir=$OPTARG
            ;;
        p) project_dir=$OPTARG
            ;;
    esac
done

# unescape
gradle_args=`echo $arguments | perl -pe "s/(?<!\\\\\\),/ /g; s/\,/,/g"`
gradle_tasks=`echo $tasks | perl -pe "s/(?<!\\\\\\),//g; s/\,/,/g"`

echo "Tasks: $gradle_tasks"
echo "Args: $gradle_args"
echo "Project Dir: $project_dir"
echo "Module Dir: $module_dir"

project_relative=`realpath --relative-to=$HOME $project_dir`
module_relative=`realpath --relative-to=$HOME $module_dir`

ctl_path="$HOME/.ssh/ctl/%L-%r@%h:%p"
rsync_ssh="ssh -S $ctl_path"
mkdir -p ~/.ssh/ctl/

# create session with Control Master
echo "Opening session..."
ssh -fNM -S "$ctl_path" "$server"

# sync source files
echo "Syncing source files..."
ssh -S "$ctl_path" "$server" mkdir -p "~/$project_relative"
rsync -Cavz --delete --filter=":- .gitignore" -e "$rsync_ssh" $project_dir/ "$server":$project_relative/
# also sync build directory
mkdir -p $project_dir/build/ $module_dir/build/
rsync -CavzS --delete -e "$rsync_ssh" $project_dir/build/ "$server":$project_relative/build/
rsync -CavzS --delete -e "$rsync_ssh" $module_dir/build/ "$server":$module_relative/build/

echo "Starting Build..."
ssh -S $ctl_path "$server" "
export ANDROID_HOME=~/android-sdk-linux
cd ~/$project_relative
./gradlew $gradle_tasks $gradle_args"

echo "Syncing output files..."
rsync -CavzS --delete -e "$rsync_ssh" --exclude='*unaligned.apk' "$server":$project_relative/build/ $project_dir/build/
rsync -CavzS --delete -e "$rsync_ssh" --exclude='*unaligned.apk' "$server":$module_relative/build/ $module_dir/build/

# close session with Control Master
echo "Closing session..."
ssh -O exit -S "$ctl_path" "$server"

echo "Remote build and sync finished."

exit 0

GradleExternalBuildPlugin/example.sh at master · nonylene/GradleExternalBuildPlugin · GitHub

コードは長いですがやってることは引数を受け取り unescape して、 rsync とビルドを行っているぐらいです。

ちなみに SSH 接続部は高速化のために ControlMaster を用いていて、これで毎回 SSH 接続をリセットされることがなくなります。

外部で実行しているのは

ssh -S $ctl_path "$server" "
export ANDROID_HOME=~/android-sdk-linux
cd ~/$project_relative
./gradlew $gradle_tasks $gradle_args"

の部分で、直接 SSH からコマンドを実行しています*4

この Shell Script が完了すると手元にビルド後の /build ディレクトリが同期されているので、後は普段同様 IDE が自動でインストール・デバッグ立ち上げなどを行ってくれます。

こうして、

  1. 柔軟なタスク実行
  2. プロジェクトで専用の設定を作らなくて良い*5
  3. タスクだけを渡す機能のみなので実行部分はそれぞれの環境に合った設定が可能

が達成できました。

External Tool は実行できさえすれば良いので Windows でも使える*6!!最高!!!

Google Compute Engine で 8コア、メモリ 8GB を借りてやってみたところ*7 速度が二倍ぐらいになりました。top で眺めてるとCPU使用率が 798% になってたりしてすごい。

ビルド待ちの間ブラウザ見てても重くなりませんしファンが回る音もしなくなって最高です。

f:id:nonylene:20160428004927p:plain

ただ、今の方法だとビルド毎に rsync で APK を転送しているのでネットワークの遅い場所ではここの時間がネックになってきます。

rsync のフェーズで 20秒ぐらいかかっていたら元も子もありません。GCEだとその点回線が太いので安心。

コードの話など

プラグインについて

書き方

Android Studio / IntelliJプラグインの情報を探しても、セットアップ程度であまり良いページはありません*8。ドキュメントも余り充実してるとは言えないです。

プラグインを作成するのに一番参考になるのは他人のコードです。GitHub で似たプラグインを検索してそのコードを眺めましょう。

Android Studio のソースを落としてくると Android Studio プラグイン作るときに使う内部のコードも閲覧できて便利。

$ git clone https://android.googlesource.com/platform/tools/adt/idea

また、IntelliJ-Idea Community のコードは GitHub で公開されているので、それも参考にすると良いです。

$ git clone git@github.com:JetBrains/intellij-community.git --depth 1

僕はプラグイン作るときにもう一つ IDE 立ち上げてそこで IntelliJ-Community のコードを見ていました。

また、 Android Studio では JDK 6 で開発する必要があるみたいですが、最新の IntelliJ Community では Java6 でのプラグイン開発が行えないみたいなので 2016年 1月版を入れて行っています。

Android Studio 関係のメソッドを叩くにはプロジェクトの依存関係に Android Studio 関係の library を入れる必要がありました。

コード

Kotlin で書いています。Kotlin 最高。

Android Studio の内部で叩いてるコードを叩いてる時点で割りとアレなのですが、Tool を作成しているところは闇。タイトルを設定するために private なメソッドをリフレクションを使って無理矢理叩いています。

parameters = parameters
        .replace("\$GRADLE_TASKS\$", tasks)
        .replace("\$GRADLE_ARGS\$", args)

with(javaClass.superclass.getDeclaredMethod("setName", String::class.java)) {
    isAccessible = true
    invoke(this@ExternalBeforeRunTaskTool, "Gralde External Build Task")
}
with(javaClass.superclass.getDeclaredMethod("setUseConsole", Boolean::class.java)) {
    isAccessible = true
    invoke(this@ExternalBeforeRunTaskTool, true)
}

まあ本来 Tool は専用の画面で設定されるものであるためこうなるのは仕方ないです… 他の方法で行おうとすると独自のコンソール画面を作り、独自にプロセスの管理をするなどする必要があり色々と面倒なので…

基本的に画面は JFrame で構成します。JFrame 書いたことなかったのでここが一番難しかった。

課題

  • Unit Test Instrumentation Test ならできるかもしれない?未確認です。

  • Make 今の方法では無理です。Make はまあ Javaコンパイルだけなので早いし良さそう。

また、今の Shell Script ではプロジェクトの build ディレクトリとモジュールの build ディレクトリを rsync しています。モジュールは現在選択しているファイルから取得しているので、プロジェクト全体の build.gradle を編集してたりするとうまく同期されなかったりします。


Android Studio のビルドのみを切り出す処理、全く見つからなかったので参考になれば嬉しいです。もっと簡単にできるよ!等あれば教えて下さい。

*1:ちなみに現在風邪を引いて寝込んでいます

*2:多分最新macでも重い

*3:自分は現在使っていないです

*4:それが一番楽なので…

*5:External Tool にはプロジェクトの情報を渡すことができる機能があり、それと組み合わせることで可能となります

*6:共通化を考えたら Python スクリプトとかのほうが楽だったかも

*7:ちゃんと消せばいいのですが放置してると月1万ぐらい行きます

*8:そもそもプラグインにも様々な種類があるので