Intelligent Technology's Technical Blog

株式会社インテリジェントテクノロジーの技術情報ブログです。

Android StudioのBuild Type/Flavor

こんにちは、間藤です。

久しぶりとなってしまいましたが、今回はAndroid StudioのBuild TypeとかFlavorについて書いてみます。何を今更な話題かもしれませんが、自分のためにもちょっと整理しておきます。
なお、今回利用しているのは、以下のバージョンです。

f:id:IntelligentTechnology:20150611091214j:plain

Android Plugin

何はともあれ、GradleのAndroid PluginのUser Guideに目を通しておくのがよいでしょう。

Gradle Plugin User Guide - Android Tools Project Site

Build Typesのところを読むと、デフォルトでdebugとreleaseのtype(ガイドにはversionと書かれてます)があるということです。
特に説明は不要かと思いますが、debugは開発時用のビルド設定を、releaseは本番リリース用のビルド設定を行う目的で用意されたものだと理解できます。具体的には、

  • apkファイルへの署名
  • Proguardの適用

といった設定を切り替えるためのものと考えればよいのではないかと。

一方のFlavorも、ビルド設定を切り替える目的に利用するものです。これをどう活用するか(Build Typesとどう使い分けるか)は利用者次第ということなんだと思います。例えば、テスト環境や本番環境によって接続するサーバを変えたいなんていう場合、Flavorを環境別に用意するといった使い方が思いつきます。

そして、Build TypeとFlavorを掛け合わせたものが、Build Variantです。例えば、Build Typeはdebugとrelease、Flavorにdevelop/staging/productionの3つを用意したとすると、Build Variantは以下の6つということになります。

  • developDebug
  • stagingDebug
  • productionDebug
  • developRelease
  • stagingRelease
  • productionRelease

では、実際にFlavorを追加してみましょう。

Flavorの追加

Gradleのスクリプトを直接編集してFlavorを追加することもできますが、[Project Structure]の設定画面から行えば、スクリプトのほうに反映されます。

f:id:IntelligentTechnology:20150611090403j:plain

developとstagingについては、Version CodeとVersion Nameにそれぞれ異なる値を設定しておきます。そうすると、ビルド時にその設定がマニフェストに反映されるので、各環境にリリースするバージョンをここで管理できるようになります。(後で結果も確認してみます)
なお、productionについては指定をしませんが、その場合はdefaultConfig(これもFlavorです)に設定されている値が使われるようです。

結果、Gradleのスクリプトは以下のようになりました。(抜粋です)

    productFlavors {
        develop {
            versionName '1.2'
            versionCode 3
        }
        production {
        }
        staging {
            versionCode 2
            versionName '1.1'
        }
    }

前述のようにBuild Variantが増えているか確認してみましょう。[Build Variants]のViewで確認できます。

f:id:IntelligentTechnology:20150611091424j:plain

これらをビルドするのは後回しにして、もう少し設定します。

Build Typeの設定

releaseの場合は、署名付きでapkファイルが作成されるようにします。
また、Proguardを適用するようにします。

まず、署名に利用するキーストアを作成しましょう。作成方法はいろいろありますが、Android Studioで作成するなら[Build]-[Generate Signed APK]を選び、途中の画面で[Create New...]を選択すると作成できます。

f:id:IntelligentTechnology:20150611094838j:plain

なお、この画面で生成すると、キーストアの拡張子はkeystoreではなくjksになるようです。
次に[Project Structure]でsigningConfigsブロックの設定を行います。

f:id:IntelligentTechnology:20150611095443j:plain

設定は以下のようにスクリプトに反映されます。

    signingConfigs {
        sampleConfig {
            keyAlias 'sample'
            keyPassword 'fugahoge'
            storeFile file('/Users/matoh/sample.jks')
            storePassword 'hogefuga'
        }
    }

次にreleaseのSigning Configに作成したコンフィグレーションを指定します。

f:id:IntelligentTechnology:20150611105858j:plain

これでrelease版のBuild Variantでは署名付きでAPKが作成されるようになります。(後で確認をします)
ただ、上の書き方ですと、キーストアやキーのパスワードをスクリプトにベタ書きすることになります。実際の運用では、リリース用ビルドを行うマシンにあれば良い情報ですので、設定を外だしにします。環境変数にしたり、gradle.propertiesに書いたりといった方法もありますが、以下のサイトに紹介されているように別Gradleスクリプトに外出しするという方法も悪くないなと思います。

qiita.com

ですが、外出ししたスクリプトをソースコード管理対象にしてしまうと意味がありませんので、もう少し工夫が必要かと思います。例えば、release.gradleはホームディレクトリ配下に置くことし、debug版のビルドではそのファイルが存在しなくてもビルドがエラーにならないようにします。

String signingConfigFilePath = "${System.properties['user.home']}/signingConfigs/release.gradle"
File signingConfigFile = new File(signingConfigFilePath)

android {

    signingConfigs {
        sampleConfig {
            keyAlias 'sample'
            keyPassword 'xxxxx'
            storeFile file("${System.properties['user.home']}/sample.jks")
            storePassword 'xxxxx'
        }
    }

    if(signingConfigFile.exists()) apply from: signingConfigFilePath, to: android
・・・

このスクリプト上に記載するパスワードは出鱈目なものにしてしまいます。そして、ホームディレクトリ配下のrelease.gradleには正しい設定を施し、このファイルが存在する場合のみapply()メソッドで正しい設定を読み込むようにします。(このファイルはリリースビルド用マシンにだけ配置するという想定です)

次にProguardの設定です。Android Studioでプロジェクトを生成した直後では、proguardFilesが以下のように設定されていました。

    release {
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }

ただ、これだけではProguadは適用されませんので、minifyEnabledプロパティにtrueを設定します。この設定も[Project Structure]画面で行えます。

    release {
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        signingConfig signingConfigs.sampleConfig
        minifyEnabled true
    }

debugのほうにも少しだけ設定を加えます。

    debug {
        applicationIdSuffix ".debug"
    }

これでアプリケーションパッケージに".debug"が付与されるので、release版とdebug版が別アプリとしてインストールされるようになります。

ソースセットの追加

Build TypeやFlavorを追加すると、自動的にソースセットも追加されます。ただ、対応するディレクトリは自分で作成する必要があります。今回はそれぞれにJavaのソースコードを配置してみます。debugであれば、src/debug/javaディレクトリ配下にjavaのソースコードを配置します。
以下のように配置しました。

f:id:IntelligentTechnology:20150611140720j:plain

debugとdevelop配下が、それ以外と表示が違っていますが、これはBuild Variantに"developDebug"を選択しているからです。つまり、これらがビルド対象になっているということです。よって、debug(Build Type)とdevelop(Flavor)配下に同じクラスを配置することは出来ません。debugとreleaseであれば、同じクラス(この例だとBuildType)を配置できます。
BuildTypeクラスとFlavorクラスは、以下のように文字列定数を定義してあります。

public class BuildType {
    public static final String TYPE_NAME = "DEBUG";
}
public class Flavor {
    public static final String FLAVOR_NAME = "DEVELOP";
}

これら定数に個別の値を与えることで、例えば環境別に接続先サーバを変えるといったことが可能になるでしょう。

なお、ActivityにTextViewを3つ用意しておいて、これら定数の値を画面表示するようにしておきます。
AndroidAnnotationsを利用しています。

    @ViewById
    TextView buildType;
    @ViewById
    TextView flavor;
    @ViewById
    TextView version;

    @AfterViews
    protected void init() {
        String versionStr = "";
        try{
            PackageInfo packageInfo = pm.getPackageInfo(getPackageName(), 0);
            versionStr = String.format("%s(%d)", packageInfo.versionName, packageInfo.versionCode);
        }catch(PackageManager.NameNotFoundException e){
            e.printStackTrace();
        }

        version.setText(versionStr);
        buildType.setText(BuildType.TYPE_NAME);
        flavor.setText(Flavor.FLAVOR_NAME);
    }

確認

ここまで仕込んだことを確認していきます。
まずはビルドしていきます。全てのBuild Variantを一括でビルドするには以下のコマンドをプロジェクト直下で実行します。

$ ./gradlew assemble

assembleが全てのBuild Variantをビルドするタスクになっています。
debug版すべてをビルドするなら、

$ ./gradlew assembleDebug

staging版すべてをビルドするなら、

$ ./gradlew assembleStaging

といったようなタスクも用意されています。
ビルドが成功すれば、app/build/outputs/apk配下に各Build Variantに対応したapkファイルが作成されています。

$ ls
./					app-production-debug.apk
../					app-production-release-unaligned.apk
app-develop-debug-unaligned.apk		app-production-release.apk
app-develop-debug.apk			app-staging-debug-unaligned.apk
app-develop-release-unaligned.apk	app-staging-debug.apk
app-develop-release.apk			app-staging-release-unaligned.apk
app-production-debug-unaligned.apk	app-staging-release.apk

AndroidManifest

build版ではパッケージに".debug"を付けるように設定しました。また、FlavorごとにVersion CodeとVersion Nameを変えるようにしました。ちゃんと結果に反映されているか確認してみます。

$ aapt dump xmltree app-develop-debug.apk AndroidManifest.xml
N: android=http://schemas.android.com/apk/res/android
  E: manifest (line=2)
    A: android:versionCode(0x0101021b)=(type 0x10)0x3
    A: android:versionName(0x0101021c)="1.2" (Raw: "1.2")
    A: package="iti.co.jp.gradlesample.debug" (Raw: "iti.co.jp.gradlesample.debug")
    A: platformBuildVersionCode=(type 0x10)0x16 (Raw: "22")
    A: platformBuildVersionName="5.1.1-1819727" (Raw: "5.1.1-1819727")
    E: uses-sdk (line=7)
      A: android:minSdkVersion(0x0101020c)=(type 0x10)0xf
      A: android:targetSdkVersion(0x01010270)=(type 0x10)0x16
    E: application (line=11)
      A: android:theme(0x01010000)=@0x7f08006f
      A: android:label(0x01010001)=@0x7f060012
      A: android:icon(0x01010002)=@0x7f030000
      A: android:debuggable(0x0101000f)=(type 0x12)0xffffffff
      A: android:allowBackup(0x01010280)=(type 0x12)0xffffffff
      E: activity (line=16)
        A: android:label(0x01010001)=@0x7f060012
        A: android:name(0x01010003)="iti.co.jp.gradlesample.MainActivity_" (Raw: "iti.co.jp.gradlesample.MainActivity_")
        E: intent-filter (line=19)
          E: action (line=20)
            A: android:name(0x01010003)="android.intent.action.MAIN" (Raw: "android.intent.action.MAIN")
          E: category (line=22)
            A: android:name(0x01010003)="android.intent.category.LAUNCHER" (Raw: "android.intent.category.LAUNCHER")

debug版なので、packageが"iti.co.jp.gradlesample.debug"となっています。
develop版なので、Version Codeが3、Version Nameが1.2となっています。

念のため、もう1つ確認してみます。

$ aapt dump xmltree app-production-release.apk AndroidManifest.xml
N: android=http://schemas.android.com/apk/res/android
  E: manifest (line=2)
    A: android:versionCode(0x0101021b)=(type 0x10)0x1
    A: android:versionName(0x0101021c)="1.0" (Raw: "1.0")
    A: package="iti.co.jp.gradlesample" (Raw: "iti.co.jp.gradlesample")
    A: platformBuildVersionCode=(type 0x10)0x16 (Raw: "22")
    A: platformBuildVersionName="5.1.1-1819727" (Raw: "5.1.1-1819727")
    E: uses-sdk (line=7)
      A: android:minSdkVersion(0x0101020c)=(type 0x10)0xf
      A: android:targetSdkVersion(0x01010270)=(type 0x10)0x16
    E: application (line=11)
      A: android:theme(0x01010000)=@0x7f08006f
      A: android:label(0x01010001)=@0x7f060012
      A: android:icon(0x01010002)=@0x7f030000
      A: android:allowBackup(0x01010280)=(type 0x12)0xffffffff
      E: activity (line=16)
        A: android:label(0x01010001)=@0x7f060012
        A: android:name(0x01010003)="iti.co.jp.gradlesample.MainActivity_" (Raw: "iti.co.jp.gradlesample.MainActivity_")
        E: intent-filter (line=19)
          E: action (line=20)
            A: android:name(0x01010003)="android.intent.action.MAIN" (Raw: "android.intent.action.MAIN")
          E: category (line=22)
            A: android:name(0x01010003)="android.intent.category.LAUNCHER" (Raw: "android.intent.category.LAUNCHER")

想定通りになっています。

署名

debug版は、debug.keystoreで署名されています。

$ jarsigner -verify -verbose -certs app-staging-debug.apk

sm      1904 Thu Jun 11 14:25:52 JST 2015 AndroidManifest.xml

      X.509, CN=Android Debug, O=Android, C=US
      [証明書は15/03/31 16:41から45/03/23 16:41まで有効です]
      [CertPathが検証されていません: Path does not chain with any of the trust anchors]
・・・(以下省略)

release版は、用意したキーストアで署名されています。

$ jarsigner -verify -verbose -certs app-staging-release.apk

sm      1840 Thu Jun 11 14:26:30 JST 2015 AndroidManifest.xml

      X.509, O=iti
      [証明書は15/06/08 19:23から40/06/01 19:23まで有効です]
      [CertPathが検証されていません: Path does not chain with any of the trust anchors]
・・・(以下省略)

Proguard

少々面倒くさいですが、apkを解凍して、dex2jarでdexをjarに変換、JD-GUIでデコンパイルします。

$ cp app-production-release.apk app-production-release.zip
$ open app-production-release.zip
$ cd app-production-release/
$ d2j-dex2jar.sh classes.dex    ※パスが通っているとして
dex2jar classes.dex -> ./classes-dex2jar.jar

f:id:IntelligentTechnology:20150611150451j:plain

ざっくりな確認ですが、ちゃんと難読化されていそうです。

画面表示

想定通りに画面表示されているかをサンプリング確認してみます。

$ adb install -r app-staging-debug.apk

f:id:IntelligentTechnology:20150611152641j:plain

大丈夫そうです。
念のためもう1つ確認します。

$ adb install -r app-production-release.apk

f:id:IntelligentTechnology:20150611152944j:plain

よさそうです。
なお、debug版とrelease版は、別アプリとしてインストールされています。

f:id:IntelligentTechnology:20150611153212j:plain

以上で目論見通りを確認できたと思います。