読者です 読者をやめる 読者になる 読者になる

visible true

技術的なメモを書く

Data BindingとMultidexの兼ね合いの問題を大体倒したので実用段階待ったなし

Data Bindingを導入したかったけどmultidexの兼ね合いで躓いたので問題をまとめます。 - visible trueで死んでましたが、設定でなんとか頑張れる事がわかったのでメモします。

結論

これらを加えるだけ。

build.gradle

android {
  defaultConfig {
    multiDexEnabled = true
    multiDexKeepProguard file('multi-dex-keep.txt') //<- new!
  }
}

multi-dex-keep.txt

-keep public class * extends android.databinding.ViewDataBinding {
 *;
}

大体解決するまで

大体解決するまでこういう事しましたというメモです。長いので読まなくてもいいです。

1. gradle pluginのソースを落とす

まずはgradle pluginでmultidexの処理している所を探す必要があるなーと思ってgradle pluginのソースを落としました。

Build Overview - Android Tools Project Site

でbranchはstudio-master-devを選択しました。ビルドする気でいたのでcase-sensitiveなdisk image作ってそこに持ってきました。

2. multidexを処理してるタスクを探す

multidexというか、maindexlist.txtを作ってる所を探すわけですが、よくわからないのでとりあえずmultidexとかでgrepすると以下の場所にドンピシャっぽいタスクがあるのを見つけました。 studio-master-dev/tools/base/build-syste/gradle-core/src/main/groovy/com/android/build/gradle/internal/tasks/multidex/CreateMainDexList.groovy

中身を見るとすげーそれっぽい感じ。

@TaskAction
void output() {
  if (getAllClassesJarFile() == null) {
    throw new NullPointerException("No input file")
  }
  
  File _allClassesJarFile = getAllClassesJarFile()
  Set<String> mainDexClasses = callDx(_allClassesJarFile, getComponentsJarFile())

  File _includeInMainDexJarFile = getIncludeInMainDexJarFile()
  if (_includeInMainDexJarFile != null) {
    mainDexClasses.addAll(callDx(_allClassesJarFile, _includeInMainDexJarFile))
  }

  if (mainDexListFile != null) {
    Set<String> mainDexList = new HashSet<String>(Files.readLines(mainDexListFile, Charsets.UTF_8))
    mainDexClasses.addAll(mainDexList)
  }

  String fileContent = Joiner.on(System.getProperty("line.separator")).join(mainDexClasses)

  Files.write(fileContent, getOutputFile(), Charsets.UTF_8)
}

でもあんまり具体的な事はしてなさそうですね。mainDexClasses.addAll()に値を渡しているcallDx()が怪しそうです。

private Set<String> callDx(File allClassesJarFile, File jarOfRoots) {
  return getBuilder().createMainDexList(allClassesJarFile, jarOfRoots)
}

callDx()の中身もあんまりない...。次はgetBuilder()が返す値を探す旅に出かけます。

3. 正体はAndroidBuilder

とりあえずcreateMainDexList全文検索すると以下のクラスが引っかかりました。

studio-master-dev/tools/base/build-system/builder/src/main/java/com/android/builder/core/AndroidBuilder.java

中身はこんなの。

public Set<String> createMainDexList(
    @NonNull File allClassesJarFile,
    @NonNull File jarOfRoots) throws ProcessException {

  BuildToolInfo buildToolInfo = mTargetInfo.getBuildTools();
  ProcessInfoBuilder builder = new ProcessInfoBuilder();

  String dx = buildToolInfo.getPath(BuildToolInfo.PathId.DX_JAR);
  if (dx == null || !new File(dx).isFile()) {
    throw new IllegalStateException("dx.jar is missing");
  }

  builder.setClasspath(dx);
  builder.setMain("com.android.multidex.ClassReferenceListBuilder");

  builder.addArgs(jarOfRoots.getAbsolutePath());
  builder.addArgs(allClassesJarFile.getAbsolutePath());

  CachedProcessOutputHandler processOutputHandler = new CachedProcessOutputHandler();

  mJavaProcessExecutor.execute(builder.createJavaProcess(), processOutputHandler)
      .rethrowFailure()
      .assertNormalExitValue();

  String content = processOutputHandler.getProcessOutput().getStandardOutputAsString();

  return Sets.newHashSet(Splitter.on('\n').split(content));
}

おや〜〜?builder.setMain("com.android.multidex.ClassReferenceListBuilder");dx.jarの中のクラスを呼び出してますね。という事でこんな事つぶやいてます。

4. AOSPのソース持ってくる & ClassReferenceListBuilderのソース読む

という事でdxのソース読む為に持ってきます。Downloading the Source | Android Open Source Projectとかに書いてあります。とりあえずmasterにしました。

com.android.multidex.ClassReferenceListBuilderの実装を見ると引数で渡されたjarファイルを展開して.classを取り出してるだけっぽい事がわかります。AndroidBuilder#createMainDexList()ではjarOfRoots.getAbsolutePath()allClassesJarFile.getAbsolutePath()を渡しています。

実装を辿っていくと、これらはアプリケーションのビルド時にproject_root/module/build/intermediates/multi-dex/[flavor]/[variant]/に生成されるcomponentClasses.jarallclasses.jarである事がわかりました。

5. componentClasses.jarはどこで生成されている?

とりあえず./gradlew --debug assembleでビルドプロセスを見てどのタスクで何が生成されているのかを見ます。

[INFO] [org.gradle.api.internal.tasks.execution.SkipUpToDateTaskExecuter] Executing task ':app:shrinkMultiDexComponents' (up-to-date check took 0.002 secs) due to:
  Output file //*略*//build/intermediates/multi-dex/product/debug/componentClasses.jar has changed.

するとshrinkMultiDexComponentsタスクでcomponentClasses.jarが生成されてる事がわかりました。studio-master-devを検索すると/studio-master-dev/tools/base/build-system/gradle-core/src/main/groovy/com/android/build/gradle/tasks/factory/ProGuardTaskConfigAction.javaで実行されている事がわかりました。実装を見ると以下の箇所が。

proguardComponentsTask.configuration(scope.getManifestKeepListFile());

これはcomponentClasses.jarが生成される場所にあるmanifest_keep.txtっぽい!このファイルの中身を見ると...

-keep class com.sys1yagi.android.hoge.Application {
    <init>();
    void attachBaseContext(android.content.Context);
}
//略

という感じでproguardの設定ぽいものが書かれています。どうやらこの設定に沿ってallclasses.jarからcomponentClasses.jarを抽出してるっぽい事がわかりました。

6. manifest_keep.txtはどこで生成されている?

manifest_keep.txtに任意のkeep設定を差し込めれば勝てる気配がしてきました。ちなみにタスク名は以下の様に定義されてるみたいです。

ProGuardTaskConfigAction.java

@Override
public String getName() {
  return scope.getTaskName("shrink", "MultiDexComponents");
}

"shrink"と"MultiDexComponents"の間にflavorとvariantsが入ります。という事でMultiDexComponents全文検索すると何か引っかかるかも?という事で検索すると/studio-master-dev/tools/base/build-system/gradle-core/src/main/groovy/com/android/build/gradle/internal/tasks/multidex/CreateManifestKeepList.groovyが引っかかりました。こっちのタスク名はこう

@Override
String getName() {
  return scope.getTaskName("collect", "MultiDexComponents");
}

CreateManifestKeepList.groovyでは、AndroidManifest.xmlに定義されたコンポーネントmanifest_keep.txtに書き出してました。その他決め打ちでいくらかの設定を吐いてます。で、こんな実装が。

@Override
void execute(CreateManifestKeepList manifestKeepListTask) {
  // since all the output have the same manifest, besides the versionCode,
  // we can take any of the output and use that.
  final BaseVariantOutputData output = scope.variantData.outputs.get(0)
  ConventionMappingHelper.map(manifestKeepListTask, "manifest") {
    output.getScope().getManifestOutputFile()
  }
  manifestKeepListTask.proguardFile = scope.variantConfiguration.getMultiDexKeepProguard()
  manifestKeepListTask.outputFile = scope.getManifestKeepListFile();
  //variant.ext.collectMultiDexComponents = manifestKeepListTask
}

おや〜〜? manifestKeepListTask.proguardFile = scope.variantConfiguration.getMultiDexKeepProguard() という事でbuild.gradleに書けるのでは〜!?

build.gradle

android {
  defaultConfig {
    multiDexEnabled = true
    multiDexKeepProguard file('multi-dex-keep.txt') //<- new!
  }
}

こうして

multi-dex-keep.txt

-keep public class * extends android.databinding.ViewDataBinding {
 *;
}

こうじゃ

これでmaindexlist.txtにData Bindingによって生成されたXXXBindingクラスが出力されるようになりました!!!

おわりに

正式な設定項目なので他のケースでも何とかできそうでいいですね。

ところでC88で@mhidaka氏のAndroid本に参加します。Data Bindingについて書きまくるので買って下さい。

新たなる敵

(ヽ´ω`)…