このエントリーは、KLab Advent Calendar 2016 の12/22の記事です。
22番手も緊張しますね。KLabGamesの基盤エンジニアのkenseiです。よろしくお願いします。
ある日突然事は起こります。それまで順調にビルドされていたjenkinsのandroidビルドが突然のエラー。ログを見るとstderrの項目に
trouble writing output:
Too many field references: 131000; max is 65536.
You may try using --multi-dex option.
の文字が。そう、Androidはファイル内でコードによって呼び出すことができる参照の総数で65535を超える事ができないのです。
いつかは戦わないといけないのですが、いざその日が来ると憂鬱ですね。ちなみに宗教上の理由でmulti dexはご法度です。
dexのメソッド数を計測してみます。ありがたいことにツールが公開されているので、それを使用します。
$ ./gradlew assemble
$ ./dex-method-counts path/to/target.apk > method-count.txt
吐かれたログを見ると
.
.
android: 16833
com: 19806
facebook: 3263
google: 14115
android: 13911
gms: 13911 <= gpgs
.
.
androidはしょうがないとして、うむ、、GPGSでかいな。。てことで、ここを重点的に潰す事にします。
最近のandroid libraryはAAR(Android Archive)で提供されています。ただのzipですが、jarは中に入ってしまってるので解凍しないといけません。という事でみんな大好き、shellの出番です。
本当はgradleでスマートにやりたかったのですが、aarをzipTreeするとpermissionエラーを吐いてgradleが死にました。googleさん、中をゴニョゴニョしたい人の事考えてassembleして下さい。。
unzipするので、Assetsと同じフォルダにディレクトリを作ります。そこにshellを作っていきます。
$ mkdir -p Target/shrink-jar-task && touch shrink-jar.sh && chmod u+x shrink-jar.sh
まずはおまじない
#!/bin/bash
# 変数の準備
WORKSPACE=$(pwd)
WORK_DIR="${WORKSPACE}/tmp"
GRADLE_WORK_DIR="${WORKSPACE}/lib"
PLUGIN_DIR="${WORKSPACE}/../Assets/Plugins/Android"
# 優しさ
while getopts v OPT
do
case $OPT in
v ) DEBUG="true"
;;
esac
done
# おしおき
if [ "${JAVA_HOME-undefined}" = "undefined" ]; then
echo "JAVA_HOME not set"
echo "alias emacs='vim'" >> .bash_profile
exit 1
fi
if [ "${ANDROID_HOME-undefined}" = "undefined" ]; then
echo "ANDROID_HOME not set"
echo "alias emacs='vim'" >> .bash_profile
exit 1
fi
# ワーク作る
if [ -d $WORK_DIR ]; then
rm -rf $WORK_DIR
fi
mkdir $WORK_DIR
if [ -d $GRADLE_WORK_DIR ]; then
rm -rf $GRADLE_WORK_DIR
fi
mkdir $GRADLE_WORK_DIR
だいたいコメントの通りです。途中でJAVA_HOMEとANDROID_HOMEを設定せずにビルドをしようとする不届き者にemacsを使えなくするお仕置きをしています。vimを使ってればお仕置きされずに済んだのに。。
shellでやっていますが、一部の処理はgradleを使用するのでhomebrew等でインストールを済ませておいて下さい。あ、言うの遅くなりましたが、動作はmacでしか確認していません。
上記で用意したフォルダにbuild.gradleファイルを作成し、下記の内容を設定します。
$ vim build.gradle
task wrapper(type: Wrapper) {
gradleVersion = '2.14.1'
}
設定後にbuild.gradleファイルのある場所で
$ gradle wrapper
を実行します。
Assets/Plugins/Android
の下にあるaarを解凍していきます。
echo "unziping..."
find $PLUGIN_DIR -type f -name "*.aar" -exec basename {} \; | sed -e "s/.aar//" | xargs -I{} unzip ${PLUGIN_DIR}/{}.aar -d ${WORK_DIR}/{} >/dev/null 2>&1
find $WORK_DIR -type d -maxdepth 1 -mindepth 1 -exec basename {} \; | xargs -I{} rm -rf ${PLUGIN_DIR}/{}
ちなみにmaxdepthオプション付けてディレクトリをfindすると、rootが入って来てしまうのですが、mindepthを重ねがけするとrootを除外できるのを今回発見しました。他のスマートなやり方あるかもしれないので、もっと綺麗なshellを書けるようになりたい。。
もう使用しないので、aarを消していきます。
echo "remove plugin aar..."
if [ "$DEBUG" == "true" ] ; then
find $PLUGIN_DIR -type f -name "*.aar*" -not -name 'appcompat-v7*.aar' -not -name 'support-v4-*.aar' -exec echo " "{} \;
fi
find $PLUGIN_DIR -type f -name "*.aar*" -not -name 'appcompat-v7*.aar' -not -name 'support-v4-*.aar' -exec rm {} \;
この時にappcompatとsupportはいったん除きます。理由は後で面倒くさいからです!
解凍したaarの中からjarを取り出していきます。
echo "move classes.jar"
find $WORK_DIR -type d -maxdepth 1 -mindepth 1 -not -name 'appcompat-v7*' -not -name 'support-v4-*' -exec basename {} \; | xargs -I{} mv ${WORK_DIR}/{}/classes.jar ${GRADLE_WORK_DIR}/{}.jar
find $PLUGIN_DIR -type d -name "firebase*" -exec basename {} \; | xargs -I{} mv ${PLUGIN_DIR}/{}/libs/classes.jar ${GRADLE_WORK_DIR}/{}.jar
if [ "$DEBUG" == "true" ] ; then
find $GRADLE_WORK_DIR -type f -name "*.jar" -exec echo " "{} \;
fi
ファイルが同名なので、元のフォルダ名+"jar"
に名前を変更しています。また、AARファイルになっていないAndroidLibraryのjarも持ってきています。今回はfirebaseを指定しています。
renameしたlibフォルダのjarファイル達をunzipして一つのjarファイルにまとめてしまいます。build.gradleを編集します。
apply plugin: 'java'
dependencies {
compile fileTree(dir: 'lib', include: ['*.jar'])
}
jar {
from configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
archiveName = 'google.jar'
}
task wrapper(type: Wrapper) {
gradleVersion = '2.14.1'
}
shellにgradleのjarタスクを実行させます。
echo -n "make aggregation jar"
./gradlew -b build.gradle jar
-b でgradle 設定ファイルを指定しています。
jd-guiで生成されたjarを確認した所 一個のjarファイルにまとまりました。
使用していないメソッドをstripするためにproguardをかけていきます。一からproguardのruleファイルを書いていくのは面倒なので、aarの中に入ってるproguard.txtを再利用します。また、アプリ独自の設定を書いていくようにproguard-rules.pro
ファイルを用意します。
$ touch proguard.base
shellにproguard ruleファイルを生成させます。
echo "generate proguard rule"
cat $ANDROID_HOME/tools/proguard/proguard-android.txt > proguard-rules.pro
find $WORK_DIR -type f -name "proguard.txt" | grep -v appcompat-v7 | grep -v support-v4 | xargs -I{} cat {} >> proguard-rules.pro
cat proguard.base >> proguard-rules.pro
ちなみにデフォルトのproguard-android.txtもまとめて1ファイルにしてしまってます。gradleのproguardタスクでgetDefaultProguardFile関数が使えなかったので。。
proguardを使用して、未使用のメソッドをjarから抹殺します。androidのpluginとjavaのpuluginでタスクがかち合うので、別名のgradle設定ファイルにします。今回はproguard.gradleとします。
まずはandroidプラグインを使ってビルドを通すためにダミーのAndroidManifest.xmlを作成します。
$ mkdir -p src/main && vim src/main/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.klab.tekitou" >
<uses-sdk android:minSdkVersion="7" />
</manifest>
次にgradle設定ファイルを作成します。
$ vim proguard.gradle
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.0'
}
}
allprojects {
repositories {
jcenter()
}
}
apply plugin: 'com.android.library'
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
minSdkVersion 14
targetSdkVersion 23
}
}
task clearJar(type: Delete) {
delete '../Assets/Plugins/Android/google.jar'
}
task proguard(type: proguard.gradle.ProGuardTask) {
configuration 'proguard-rules.pro'
injars 'build/libs/google.jar'
outjars '../Assets/Plugins/Android/google.jar'
libraryjars System.getenv("JAVA_HOME") + '/lib/rt.jar'
libraryjars System.getenv("JAVA_HOME") + '/lib/jce.jar'
libraryjars System.getenv("ANDROID_HOME") + '/platforms/android-23/android.jar'
libraryjars System.getenv("ANDROID_HOME") + '/extras/android/m2repository/com/android/support/support-annotations/23.4.0/support-annotations-23.4.0.jar'
libraryjars 'tmp/appcompat-v7-23.1.1/classes.jar'
libraryjars 'tmp/support-v4-24.0.0/classes.jar'
libraryjars '../Assets/Plugins/Android/firebase-common-9.8.0/libs/classes.jar'
}
proguard.dependsOn(clearJar)
injarsにclasses.jarをまとめたgoogle.jar、libraryjarsにjarが依存している物を指定しています。
※適当に必要そうなのを足していったので、結局どれが本当に必要なのか分かってない
appcompatとsupportは最初に作業フォルダに持ってきたけど、excludeしていたjarを使用します。support-annotationsはversionがpathに入っていますが、target sdk versionの中で一番新しいのを適当に設定しています。android.jarはproguard.gradleで指定したcompileSdkVersionを指定しています。
shellにgradleのjarタスクを実行させます。
echo "shurink jar"
./gradlew -b proguard.gradle proguard
ここはビルドに必要なlibraryjars不足によるエラーとの戦いになるので、何回もgradlewコマンドを叩いて通るまで頑張ります。
できあがったミニマムなjarファイルです。
解凍したaarの中のリソースを戻していきます。Assets/Plugins/Android
フォルダ直下に置いて、packagingをUnityに任せます。
echo "copy unziped android resources"
if [ "$DEBUG" == "true" ] ; then
find $WORK_DIR -type d -maxdepth 1 -mindepth 1 -exec basename {} \; | grep -v appcompat-v7 | grep -v support-v4 | xargs -I{} echo " "${WORK_DIR}/{}
fi
cat <<EOS > project.properties
target=android-14
android.library=true
generated=true
EOS
find $WORK_DIR -type d -maxdepth 1 -mindepth 1 -exec basename {} \; | grep -v appcompat-v7 | grep -v support-v4 | xargs -I{} cp project.properties ${WORK_DIR}/{}/
find $WORK_DIR -type d -maxdepth 1 -mindepth 1 -exec basename {} \; | grep -v appcompat-v7 | grep -v support-v4 | xargs -I{} cp -r ${WORK_DIR}/{} ${PLUGIN_DIR}/{}
if [ "$DEBUG" == "true" ] ; then
find $WORK_DIR -type d -maxdepth 1 -mindepth 1 -exec basename {} \; | grep -v appcompat-v7 | grep -v support-v4 | xargs -I{} echo " "${PLUGIN_DIR}/{}
fi
project.propertiesを追加しないとUnityにAndroidResourceとして認識されないので、追加しています。targetはPlayer SettingsのMinimum API Levelを設定すれば大丈夫だと思います。generated=true
としてるのは、aarのバージョンアップ時にフォルダを消すためのマーキングです。
きちんとAndroidResourceとして認識されているかはUnityAppPath/Temp/StagingArea/android-libraries
を見ると確認できます。
上記で生成したjarファイルはミニマム過ぎて動きません。試しにfirebaseを全部stripから覗いてみます。
$ vim proguard.base
-dontwarn android.support.v4.app.**
-dontwarn com.google.firebase.**
-dontwarn com.google.android.gms.**
-keep public class com.google.firebase.**
ついでにwarningがうるさいので切っています。
実行してみると
firebaseが復活しているのが分かります。
ここからはUnityでAndroidビルド&&実機で動かしながら動作確認 => 落ちた所でログを確認して、必要なクラスをproguard.baseに足してshellを流す の作業の繰り返しになります。
vimを使いましょう
明日はTech Blogなのに、泣かせるので評判な@hhattoです。
KLabのゲーム開発・運用で培われた技術や挑戦とそのノウハウを発信します。
合わせて読みたい
KLabのゲーム開発・運用で培われた技術や挑戦とそのノウハウを発信します。