nashcft's blog

時々何か書く。

Android Studio 4.2 から組み込まれる JDK のバージョンが 11 になるっぽい

現在最新の 4.2 Canary 13 で以下のようになっている。

f:id:nashcft:20201017203309p:plain
Android Studio 4.2 Canary 13 の "About Android Studio" のキャプチャ

前回書いた記事では当時の最新バージョンに組み込まれていたのが JDK 8 だったっぽいのと、手元にある記録からすると Canary 8 で上がったらしい。

f:id:nashcft:20201017205744p:plain
Android Studio 4.2 Canary 8 の "About Android Studio" のキャプチャ

Release Update には特に言及がない。

androidstudio.googleblog.com

CircleCI の Android Docker Image でJDKが11にアップデートされた件のまとめ

私は今回の件より前から CircleCI を使っておらず、最近は主に GitHub Actions でCIを構築していてこの件で被害は被っていないのだけど、軽く調べてみたところ Android project を JDK 9+ でビルドすることに強い興味をを持っている自分には結構興味深いことがわかったのでもう少し詳細に調べてまとめることにした。

何があったの

日本時間で 2020-08-18 のお話

  • CircleCI の Android Docker Image が更新され、JDK 8 ベースから JDK 11 ベースになった*1
  • これらの image を使ってCIを行っている Android project でビルドが失敗するものが発生した
  • Twitter が少し賑やかになった

原因1: なぜビルドが失敗するようになったの

この blog を書き始めてから調べて知った付け焼き刃な部分もあり正確な説明になっていない部分もあると思うが (詳しい方指摘ください...)、大雑把には JDK 9 で導入された Project Jigsaw*2 と呼ばれる module system に関する変更によって標準の package に破壊的な変更が加わり、それによってこれまでデフォルトでアクセスできた package にアクセスできなくなったことが原因である。

具体的には

  • DataBinding -> JAXB
    • JDK 9 でJAXBの package は deprecated となり、デフォルトで解決可能な classpath から外された*3
    • JDK 11 でJAXBを含む Java EEAPIJDKから削除された*4
  • JaCoCo -> jdk.internal.reflect
    • JDK 9 でデフォルトでアクセス不可能になった*5

という感じ。

原因2: なぜJDKのアップデートが行われたの

この変更は予告なく突然行われたわけではなく、実際には7月から変更のアナウンスが forum で発表されていた。

discuss.circleci.com

この投稿から辿っていくと、以下の issue がきっかけのように思われる。

github.com

この issue は targetSdkVersion を29にしているため Robolectric を使って実行するテストが Java 8 では動かないので、Java のバージョンを上げてくれというものだ。これは Robolectric 4.3 から適用される制約で、 release note で簡単に触れられているが、SDK 29 以降で Robolectric を使用する場合には runtime に Java 9 以上が要求され、Java 8 で実行するとエラーで止まるようになった。

https://github.com/robolectric/robolectric/releases/tag/robolectric-4.3

Running tests on Android Q requires Java 9.

https://github.com/robolectric/robolectric/releases/tag/robolectric-4.3.1

Running tests on Android API 29 now strictly requires a Java9 runtime or newer.

ところでこの問題自体は JDK のバージョンを上げなくても回避可能で、テスト実行時のSDKのバージョンを28以下に指定すればよく、 robolectric.properties に以下の設定を追加すれば良い:

# 28未満でも良い
sdk=28

これによって robolectric.properties が配置されている module での Robolectric を使ったテストでは指定した SDK version に基づいてテストを実行してくれる (この場合でも警告は出力される)。この指定をした場合でも、個別に @Config annotation で指定した SDK version が優先されるので影響は出ない*6

大半の場合はこれで解決するのでJDKのバージョンを上げる必要はないのだが、一つだけ上記の回避方法では解決できずに Java のバージョンを上げなければ解決しないケースがあり、それは minSdkVersion が29以上のときである。このケースでは robolectric.propertiessdk のバージョンを下げてしまうと targetSdkVersionminSdkVersion を下回る状態になってしまい下記のようなエラーになる。

java.lang.RuntimeException: android.content.pm.PackageParser$PackageParserException: <path to apk for local test>: Requires newer sdk version #29 (current version is #28)

解決方法

動かせた頃の image を使う

CircleCI Japan のアカウントで紹介されているように*7JDK 8 ベースだった時の docker image の digest を、使用する image の指定に追記することで使う image の固定ができる。今回の件で知ったが、これは document でも best practice として言及されている*8

docker:
  - image: circleci/android:api-29@sha256:<digest>

JDK 11 でもビルドできるように project を修正する

DataBinding

公式のアナウンスはまだなかった気がするが、 Android Gradle Plugin 4.1.0 で修正されているらしく、そちらでは JDK 11 を使っても DataBinding 関連のビルドが成功するようになっている*9*10。手元で動かしてみた感じでは 4.0.0, 4.0.1 でも同様にビルドできたので、4.0.0 にも backport されているらしい。

また、何らかの理由で AGP を 4.0.0 以上に上げられない場合は、JAXB関連の依存を自分の project に追加することでも解決できる:

dependencies {
  annotationProcessor 'javax.xml.bind:jaxb-api:2.3.1'
  annotationProcessor 'com.sun.xml.bind:jaxb-core:2.3.0.1'
  annotationProcessor 'com.sun.xml.bind:jaxb-impl:2.3.2'
  // kapt を使っている場合以下も追加する
  kapt 'javax.xml.bind:jaxb-api:2.3.1'
  kapt 'com.sun.xml.bind:jaxb-core:2.3.0.1'
  kapt 'com.sun.xml.bind:jaxb-impl:2.3.2'
}

JaCoCo

JaCoCo の設定に excludes という coverage 対象から除外するパターンを追加できるものがあるが、それに以下のように jdk.internal. から始まる package を追加することで解決可能*11*12

tasks.withType(Test) {
  jacoco {
    includeNoLocationClasses = true
    // excludes の一覧に 'jdk.internal.*' を追加
    excludes = ['jdk.internal.*']
  }
}

余談: JDK 11 になって嬉しいこともある

CircleCI で Java project といえばOOMだったりCPUの core を container に割り当てられた数よりも多く認識してしまったりという問題をどう対処するかという話があり、公式でも幾らかの言及がある*13*14。これはJVMの挙動の都合で cgroup による割り当て設定を見ないことが原因の一つなのだが、Java 10 で UseContainerSupport という VM option がデフォルト有効で導入され、cgroup で割り当てられた設定が反映されるようになり、また同じく追加された MaxRAMPercentage で割り当てられたRAMの量に対するパーセンテージで heap size の上限を定められるようになった。

この辺の話は本当に詳しくないので適当に記事のリンク貼ってお茶を濁させて。

www.eclipse.org

developers.redhat.com

merikan.com

medium.com

というわけで、もともと JDK 11 でもビルドできたり、先に述べたような対応をして JDK 11 でもビルドできるようにしたりした project は、上記の VM option の恩恵を得られるようになる。とはいえ昨今のCIを構築している Android project は大体が対策として Gradle の worker の数を絞ったり Xmx を設定したりしていると思うので、今なおRAMやCPUの問題で苦しんでいる人はそれほど多くないような気がするし、それらの対策のためにわざわざ JDK 11 対応をするにしてもモチベーションとしては弱いかもしれない。

雑感

この blog を書き始めた当初は AGP 4.0.0 以上でも JDK 11 で DataBinding がビルドできないものと思っていたので、こんな影響評価のお粗末なサービスは使わないで他のCIサービスに移行した方が良いですよみたいな締めくくりにして、ついでに移行先候補についても幾らか書こうと思っていた程度には個人的には印象が悪かった。その後調べた結果として DataBinding を捨てるなど大規模な修正なしに Android project 側でも対応可能なのがわかって、そこまで言う程ではないという評価に変わってその部分は削ったものの、実際はどうあれ CircleCI (の Android まわりに関わっている人) には JavaAndroid の情勢に詳しい人はいないんだろうなあ、という不信感は残った。

発端がユーザからのリクエストで、それに素早く対応する姿勢はとても好ましいものだと思う。しかし解決したいこと (Robolectric 4.3 以上を動かせるようにすること) に対してとった手段 (JDK 11 へのアップデート) によって発生した影響が釣り合っていないので、ちゃんと JDK のアップデート内容やそれによって起こりうる影響の調査をしたのだろうかという疑問がある。彼らは docker image の更新をリリースした当時 JDK 9 でJAXBのAPIにアクセスできなくなることが DataBinding のビルドに影響を与えることを知らなかったのだろうし*15、もしかしたら実行時の Android SDK のバージョンが29以上であることが必要でない限り Java 8 でも Robolectric 4.3 以上を使ったテストを実行できるようにする方法があることを今でも知らないかもしれない。そもそもまだ Android Studio に組み込まれている JDK は4.2 Canary でも JDK 8 で、Android アプリ開発の現場の大半では開発環境に JDK 8 を想定していると見て良いはずなのに、1つのライブラリに対応するために自分たちのメインストリームをそこから外していく判断はちょっとよくわからなかった。CircleCI としては過去のバージョンを digest 使って固定できるし、それを将来の変更に備えるための best practice として紹介しているという言い分はあると思うけど。

それはそうと Java 8 のリリースは2014年と6年も前のことであり、現在の Java の最新は14, 来月には Java 15 がリリースされ、来年2021年には Java 11 の次のLTSである Java 17 がリリースされる予定である*16Java 8 までと Java 9 以降でリリースサイクルが変わったとはいえ、様々なアップデートが行われた、また行われている Java において8はもはやレガシーな環境だと言って良いと思われる。Android OS それ自体は Java そのものを使っているわけではないのでAPIの追従に時間がかかるのは仕方ないことだが、ビルドや local test の実行に関しては Java後方互換性のおかげで結構なんとかなるし*17、現時点で最も困難な (と自分は思っている) Java 9 の壁を越えられるようになったことがわかったので、今後は Android に対して新しいJDKへのサポートを積極的に要求していっても良いのかもしれない。

www.oreilly.com

www.manning.com


Android Gradle Plugin 4.1.0 から library module の BuildConfig に VERSION_CODE と VERSION_NAME が生えなくなりそう (なった)

Android Gradle Plugin 4.0.0 までは library module にも生えていた BuildConfig.VERSION_CODEBuildConfig.VERSION_NAME が 4.1.0-alpha05 から生えなくなってた

issuetracker: https://issuetracker.google.com/issues/154275579

Library module にとっては不要でしょ、とのこと

Version Code mean nothing for Libraries. It's actually potentially confusing as one might expect that the BuildConfig.VERSION_NAME of a library is set to the version name of the app but this was never the case. For these reasons, it's better to not include it in library modules.

AGP 4.1.0-beta01 で試しても同様だった

現在 library module の build.gradleversionCodeversionName を設定して、その module の BuildConfig からバージョン情報にアクセスしているようなアプリはDIでバージョン情報を使うもの/取得できるものを app から配るようにしないとダメそう

追記 (2020-10-13)

4.1.0 stable 出ました

https://developer.android.com/studio/releases/gradle-plugin?buildsystem=ndk-build#4-1-0

記述はないけど上記のままっぽいです 書いてあったわ
アプリの構造や設計によってやりようは色々ですが、大雑把には

  • DI で app module の BuildConfig へのプロキシを配る
  • 自分のアプリの PackageInfo から取得する
  • buildConfigField でバージョン情報を設定する

辺りになるんじゃないでしょうか

CircleCI to GitHub Actions 移行日記

追記 (2020-06-13)

この件その後ログを精査してみたら、メモリの問題じゃなくて実行しているテストの1つにあった非同期処理の実装がまずくて処理が迷子になったまま帰ってこないのが原因だった。

追記終わり

職場の Android アプリのCIに CircleCI を使っていたのだけど、古いプランのままでコンテナのRAMが4GBの medium しか使えないのと、アプリの規模が大きくなりつつあるために色々対策をしてもOOMで落ちることがままあり、幾らかお話をした結果として現状簡素なCIしか無いし欲しいのは十分なRAMくらいなので CircleCI のプランを現行の performance plan にするくらいなら GitHub Actions の方がコスパが良いということで移行をしている。

先にビルドがより不安定な project から移行をして、そちらは問題なく移行できたのだけど、最も安定していた project を移行した際に gradle task の実行中に特に中断されるでもなく応答が返ってこなくなるという挙動に遭遇した。よくあるCI用の実行オプションを足しても特に改善される様子がなかったので debug log を出力させてみたところ、 task の実行途中で空き容量を超えた量のメモリ確保の要求がきて、解放しようとしたが殆どもしくは全く空けられず、延々と request が飛んでくるだけといった状況になってることがわかった。

関連する log は以下のような感じ:

[DEBUG] [org.gradle.process.internal.health.memory.DefaultMemoryManager] 728290099 memory requested, 429182976 free
[DEBUG] [org.gradle.workers.internal.WorkerDaemonExpiration] Will attempt to release 694 of memory
[DEBUG] [org.gradle.process.internal.health.memory.DefaultMemoryManager] 728290099 memory requested, 0 released, 429182976 free
[DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Waiting to acquire shared lock on daemon addresses registry.
[DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Lock acquired on daemon addresses registry.
[DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Releasing lock on daemon addresses registry.
[DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Waiting to acquire shared lock on daemon addresses registry.
[DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Lock acquired on daemon addresses registry.
[DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Releasing lock on daemon addresses registry.
[DEBUG] [org.gradle.process.internal.health.memory.DefaultMemoryManager] 728290099 memory requested, 429182976 free
... (以下繰り返し)

Gradle task 実行時のオプションは以下の通り:

 -Dorg.gradle.jvmargs="-Xmx5120m -XX:+HeapDumpOnOutOfMemoryError" -Dorg.gradle.workers.max=2 -Dorg.gradle.daemon=false -Dkotlin.incremental=false

要求されるメモリの量は常に同じなのでずっと同じ要求が繰り返されているんだろうなと思うのだけど、何のために700MB程度確保しようとしているのか、何故できてもほんの少ししか解放できないのかなどわからないことばかりだし、じゃあアプリの規模に対して -Xmx が小さすぎるのかというと見ての通り少なくない、というかむしろかなり多い方だし、大きくなりつつあるとはいえ特別大規模なアプリではないのでカツカツということはないと思っているので、今の自分の gradle に対する知識ではお手上げだなあとなっている。あとこの gradle task が進まなくなる挙動は必ず発生するわけではなく時々完走するので、何か変更を入れたときにたまたまビルドが通るとそこに原因を求めてしまいがちで調査がなかなか進まずもどかしい。

Groupie に View Binding サポートが追加された

先月出してた groupie-viewbinding を追加する pull request が今日 merge された。

github.com

早速これの入った v2.8.0 のリリースもされた模様。

Release v2.8.0 · lisawray/groupie · GitHub

ものとしては groupie-databinding ほとんどそのままという感じで、型パラメータが ViewBinding になっているのと、View Binding には DataBindingUtil#bind みたいな任意の binding class のインスタンスを生成する方法がないのでそれについては BindableItem#initializeViewBinding を override して自分で書いてね、というところがAPI上の差異になる。

簡単な例としては以下のような感じ:

class MyItem(private val data: MyData) : BindableItem<MyItemBinding>() {

  override fun getLayout(): Int = R.layout.my_item

  override fun initializeViewBinding(view: View): MyItemBinding =
      MyItemBinding.bind(view)

  override fun bind(viewBinding: MyItemBinding, position: Int) {
    // ...
  }
}

また、プロジェクトのAGPが3.6.0以上だと、 以前ブログで書いた通り ViewDataBindingViewBinding を implement するようになったので Data Binding も扱うことができ、 Data Binding と View Binding 両方使う場合は groupie-viewbinding だけ依存を追加すればよくなっている。

ただこれまでに groupie-databinding を使ったことがある人には groupie-viewbinding で Data Binding を扱う時にちょっと注意点があって、groupie-databindingBindableItem は ViewHolder が bind される時に内部で ViewDataBiding#executePendingBindings を実行してくれていたのだけど、 groupie-viewbinding のはそれがなくなっている。なので binding class に対応する layout が持つ変数や observable object を変更することで view の更新を行う場合には、 override する bind の実装の中で自分で executePendingBindings を実行するのを忘れないようにしなければならない。 groupie-databinding から groupie-viewbinding に移行する場合は既存の BindableItem を継承した class 達にはすべて initializeViewBinding の override をして回る必要があるので、その時に確認を忘れないようにしてほしい。

developer.android.com

stackoverflow.com

あと、今回の groupie-viewbinding の追加で groupie-databinding が deprecated になった。近い内にメンテも止める予定だそうだ。まあ確かに groupie-viewbinding が Data Binding も扱えるのでそっちに寄せてしまおうというのはわかるのだけど、新規はともかく groupie-databinding からの移行となるとユーザにはやや不便を強いることになり、その点については心残りがある。

まとめ

  • View Binding サポートの groupie-viewbinding が追加された。Data Binding とも使える
  • groupie-databinding が deprecated になった。メンテも止まる予定
  • (groupie-databinding から移行するユーザ向け) groupie-viewbinding で Data Binding を扱う場合の変更ポイント:
    • Binding class のインスタンス生成のために initializeViewBinding が必要
    • bind の実装の中で layout に持たせた変数や observable objects を経由して view の更新を行っている場合は、最後に executePendingBindings を実行する

******

From: CyberAgent (2019/03~2020/03)
To: U-NEXT (2020/03~)

出戻りです。

CyberAgent では Android アプリの開発をしたり Bitrise で CI を良い感じにしたり GitHub Actions で CI を良い感じにしたりしていました。U-NEXTでも引き続き Android アプリの開発をしたり CI を良い感じにしたりすると思われます。

Android: Data Binding 3.6.0 から ViewDataBinding の定義が変わった

今日 Android Studio 3.6.0 stable がリリースされた。

android-developers.googleblog.com

3.6 から導入された機能の一つに view binding という、data binding の layout 中の要素にアクセスする機能だけを抜き出したようなものがある。

developer.android.com

機能の詳細については公式ドキュメントを見てもらうとして、この機能で data binding の時のように生成される Binding クラスは ViewBinding という interface を実装している。

/*
 * Copyright (C) 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package androidx.viewbinding;

import android.view.View;
import androidx.annotation.NonNull;

/** A type which binds the views in a layout XML to fields. */
public interface ViewBinding {
    /**
     * Returns the outermost {@link View} in the associated layout file. If this binding is for a
     * {@code <merge>} layout, this will return the first view inside of the merge tag.
     */
    @NonNull
    View getRoot();
}

(from: AndroidX Tech: Source Code for ViewBinding.java)

この ViewBinding だが、3.6.0 から data binding で生成される Binding クラスの親クラスである ViewDataBinding もこれを実装するようになっている。

3.5 まで (3.5.3): AndroidX Tech: Source Code for ViewDataBinding.java

public abstract class ViewDataBinding extends BaseObservable

3.6 から (3.6.0): AndroidX Tech: Source Code for ViewDataBinding.java

public abstract class ViewDataBinding extends BaseObservable implements ViewBinding

つまり、 ViewBindingViewDataBinding, それと data binding や view binding で生成された Binding クラスの関係は以下のようになる。

ViewBinding
 |
 + - < View binding の機能で生成された Binding クラス >
 |
 + - ViewDataBinding
        |
        + - < Data binding の機能で生成された Binding クラス >

それが何か、というと、 <T extends ViewBinding> (Kotlin だと <T: ViewBinding>) みたいな型パラメータを定義すると view binding で生成されたクラスだけではなく data binding で生成されたクラスも適用可能になるということで、view binding と data binding は1つのプロジェクトに混在させることができるので、ViewBinding に関する実装を書いた時はそれに data binding の生成クラスを放り込んでも問題ないか考慮しておいた方が良いよ、という話。

追記