言いたいことはそれだけか

KotlinとかAndroidとかが好きです。調べたことをメモします。٩( 'ω' )و

androidx.benchmarkの1.0.0がリリースされたので試してみる

Android Dev Summit 2019の下記のセッションで軽くふれられていた androidx.benchmark の1.0.0がリリースされたようなのでさわってみます。このセッションはbenchmarkの使い方を説明したものではないけど、色々と面白いのでおすすめです。

www.youtube.com

それではやっていきましょう。

0. Reference

Jetpack Benchmarkの公式referenceはこの辺り。

developer.android.com

API referenceはこの辺り。

developer.android.com

で、これらをどうやって使っていくかはこの辺りにまとまっています。

developer.android.com

1. Benchmark用のmoduleを作る

最初に紹介した動画でも言及されていたのですが、benchmarkを測る時はdebug modeをOFFにすることが奨励されています。 こういったconfigurationを他のアプリのモジュールから分離するために専用のmoduleを作ります。

Android StudioはBenchmark moduleを作るためのtemplateが用意されているのでこれを利用します。ただし、Android Studio 3.5系を使っている場合はこのtempleteを使うためには手動で下記の設定が必要です。

  1. Help > Edit Custom Properties をクリック。(「idea.propertiesのファイルが今ないから作る?」って聞かれたら Create を選択して下さい。)
  2. 下記を1行追加してAndroid Studioを再起動する
npw.benchmark.template.module=true

で、ここからはAndroid Studio 3.6からと共通。Templateを使ってbenchmark moduleを用意します。

  1. Projectを右クリックして New > Module を選択
  2. Moduleの選択肢が出てくるので Benchmark Module を選んで Next をクリック
  3. Module名とか色々変えたかったら変更して Finishをクリック

これでProject rootの下に benchmark moduleが作成されます。

このmoduleの build.gradle をみると androidx.benchmark:benchmark にすでに依存が付いています。ただしtemplateで指定されたversionが古いままだったりする😇1.0.0 だとpackage nameも変わっているので注意です。下記に変えてやりましょう。

androidTestImplementation 'androidx.benchmark:benchmark-junit4:1.0.0'

また、gradleのsyncが下記のエラーで失敗したりする。

androidx.benchmark.AndroidBenchmarkRunner, in project benchmark, which is no longer valid as it has been moved to androidx.benchmark.junit4.AndroidBenchmarkRunner.

というわけで正しい依存をつけてエラーをとってやりましょう。

    defaultConfig {

-        testInstrumentationRunner 'androidx.benchmark.AndroidBenchmarkRunner'
+        testInstrumentationRunner 'androidx.benchmark.junit4.AndroidBenchmarkRunner'
    }

また、このmoduleの androidTest 下に作られたAndroidStudioにはdebug modeをOFFにする設定がすでに記述されています。便利!

project_root/benchmark/src/androidTest/AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example.muumuu.benchmark">

    <!--
      Important: disable debugging for accurate performance results

      In a com.android.library project, this flag must be disabled from this
      manifest, as it is not possible to override this flag from Gradle.
    -->
    <application
        android:debuggable="false"
        tools:ignore="HardcodedDebugMode"
        tools:replace="android:debuggable" />
</manifest>

2. benchmarkを書いていく

BenchmarkはInstrumentation testになります。benchmarkを作るには BenchmarkRule クラスを使っていきます。余談ですが、このクラスにはいくつかサブクラスが存在していて、Activity用のbenchmarkを測りたい時は ActivityTestRulectivityScenarioRuleを使うようです。 UIのbenchmarkを測る時は @UiThreadTestアノテーションを使います。

今回はActivityを作るのが面倒だったのでdata processingでbenchmarkを測っていきます。最近同僚に教えてもらったちょうどいい記事があるのでこれを試します。

blog.kotlin-academy.com

この記事の内容を軽く紹介すると、データサイズが大きい場合に、Kotlinの IterableSequence だとdata processのstepが2以上ある場合はSequence使ったほうが早いよーという話です。

こんな感じで 1から1000のlistを作ってSequenceIterableで全く同じ操作をしてそれぞれのbenchmarkを測ります。

@RunWith(AndroidJUnit4::class)
class SequenceBenchmark {

    @get:Rule
    val benchmarkRule = BenchmarkRule()

    private val dataSet = (1..1000).toList()

    @Test
    fun logSequence() {
        benchmarkRule.measureRepeated {
            dataSet.asSequence()
                .filter {
                    it.rem(2) == 0
                }
                .map {
                    it * 100
                }
                .average()
        }
    }

    @Test
    fun logIteration() {
        benchmarkRule.measureRepeated {
            dataSet
                .filter {
                    it.rem(2) == 0
                }
                .map {
                    it * 100
                }
                .average()
        }
    }
}

で、普通のtestを実行するようにtestを実行します。以下結果。確かにSequenceの方が早い。

Started running tests
benchmark:        63,038 ns SequenceBenchmark.logSequence
benchmark:        87,656 ns SequenceBenchmark.logIteration

感想

意外と簡単にシュッとできるので便利でした。Templeteがupdateされれば言うことない。
言いたいことは以上です。

ConstraintLayout 2.0でFlexboxLayoutとGridLayoutを代用する

はじめに

ConstraintLayout 2.0から導入された Flow を使ってFlexboxLayoutやGridLayoutと同じようなことができるようになる。

androidstudio.googleblog.com

developer.android.com

こんな感じでできた。
(上がFlexboxLayoutっぽいやつ、下がGridLayoutっぽいやつ)

f:id:muumuumuumuu:20190928193852p:plain:w200

ConstraintLayoutは 2.0.0-beta2 を使っている。 コードサンプルはこちら

github.com

FlexboxLayoutっぽいのを作ってみる

<androidx.constraintlayout.helper.widget.Flow
    android:id="@+id/flexbox_flow"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:layout_margin="16dp"
    android:orientation="horizontal"
    app:constraint_referenced_ids="text_1,text_2,text_3,text_4,text_5"
    app:flow_wrapMode="chain"
    app:flow_horizontalStyle="packed"
    app:flow_verticalStyle="packed"
    app:flow_horizontalGap="8dp"
    app:flow_verticalGap="8dp"
    app:flow_firstHorizontalBias="0"
    app:flow_horizontalBias="0"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintBottom_toTopOf="@id/grid_flow"
    />

配置したい要素となるviewとは別に Flow を定義してやる。以下各attributeの説明

  • 要素は app:constraint_referenced_ids でカンマ区切りで指定
  • FlexboxLayoutっぽくするポイントは app:flow_wrapModechain を指定すること
  • app:flow_horizontalStyleapp:flow_verticalStylepacked を指定しているので詰めて表示される
  • 各要素のmerginは app:flow_horizontalGapapp:flow_verticalGap で指定
  • layout_constraintHorizontal_bias などが効かないので、代わりに app:flow_horizontalBias を使う

GridLayoutっぽいのを作ってみる

    <androidx.constraintlayout.helper.widget.Flow
        android:id="@+id/grid_flow"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_margin="16dp"
        android:orientation="horizontal"
        app:constraint_referenced_ids="number_1,number_2,number_3,number_4,number_5,number_6,number_7,number_8,number_9"
        app:flow_wrapMode="aligned"
        app:flow_maxElementsWrap="3"
        app:flow_horizontalGap="4dp"
        app:flow_verticalGap="4dp"
        app:layout_constraintTop_toBottomOf="@id/flexbox_flow"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        />

FlexboxLayoutっぽい方とほとんど一緒だが、違いは app:flow_wrapModealigned を指定すること。あと、1行(orientationがverticalの場合は1列)にいくつ要素を配置するかを app:flow_maxElementsWrapで指定する。

終わりに

かなり使いやすい印象なので、早くbetaが取れて欲しい。FlexboxLayoutとGridLayoutではなくてConstraintLayoutを使う理由は何かあるのかと思う人がいるかもしれないが、個人的にはこの辺りの理由でConstraintLayoutを推す。

  • デザイン変更が容易にできる(各要素は変更せず、Flowだけの変更で対応できる。運がよければだけど)
  • Layoutがnestしないのでパフォーマンス観点で期待できる(計測して無いですごめんなさい)
  • 同じくConstraintLayout 2.0 から追加された LayerCircular Reveal などのdecoratorが使いたくなるかも?

Androidのdark theme

Androidのdark themeをさわってみたのでメモを残す。

Dark themeとは?

公式documentはこちら

developer.android.com

電池の節約だったり、low visionなuser向けにより良いUIを提供することを目的に、黒背景のthemeがAndroid Q からサポートされた。

アプリでDark themeに対応する

App Theme

まずはApplicationのThemeのparentをDayNightにする

<style name="AppTheme" parent="Theme.AppCompat.DayNight">

もしくはMaterialComponentsの方を使ってもいいらしい。

<style name="AppTheme" parent="Theme.MaterialComponents.DayNight">

MaterialComponentsの方がいい感じに色をそれっぽく自動で変えてくれる気がする。 (ActionBarのcolorPrimaryの色とかbuttonとか。)

Resource

dark themeがONになっているときは基本的にvalues-night などnightのqualifierがついたresource directoryに置いているものが読まれる。 (values-notnight も作ることができる。defaultをdark themeにしたいときはこれを使えば良さそう。)

dark themeで変えなきゃいけないのはcolorとアイコンくらいかなぁ。

f:id:muumuumuumuu:20190608155245p:plain

ちなみにdark themeは端末のSettings > Display > Dark Theme からONにすることができる。 Qより前のOS(Pieとか)でも同じメニューが存在するが、ONにしても values-night 配下のリソースが使われないので注意。

Dark themeを動的に切り替える

端末のDark Themeの設定ではなく、アプリ固有でDark Themeを設定することもできる。

例えば端末の設定に関わらず、アプリ全体でDark ThemeをONにしたいときは下記のような1行を呼んでやれば良い。

AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_YES)

アプリ全体ではなく、Activity単位でDark ThemeをONにしたいときは下記のような1行を呼んでやれば良い。

// Call this from AppCompatActivity
delegate.localNightMode = MODE_NIGHT_YES

引数に使える定数は以下(deprecatedなものは除く)

const values description
MODE_NIGHT_AUTO_BATTERY 3 システムの Battery Saver 機能が有効のときはdark modeを使って、それ以外はlight modeになる。
MODE_NIGHT_FOLLOW_SYSTEM 0 システムの night modeに従う。
MODE_NIGHT_NO 1 常にlight modeを使う。また、notnight qualified resourcesが時間に関わらず(夜中でも)使われる。
MODE_NIGHT_UNSPECIFIED 100 night modeをunspecifiedしたい時に使う。主にsetLocalNightMode() でdefaultのnight modeに戻したい時に使うっぽい。
MODE_NIGHT_YES 2 常にdark modeを使う。また、 night qualified resourcesが時間に関わらず(昼間でも)使われる。

おまけ

実装だけではなく、material designとしてのdark themeの考え方も読んでみると面白い。 背景(elevation = 0)は黒なんだけど、Surface (elevation = 1)は黒みがかった灰色。dark modeはelevationを影の強さで表現することができないので、高いelevationになるほど白みがかって行くことで表現しているらしい。なるほど。 material.io

Activity Shared Element TransitionでActivityが多重起動されるとAnimationがおかしくなる

はじめに

Activity Shared Element TransitionでActivityが多重起動されるとAnimationがおかしくなる現象を見つけて面白かったので調べてみた。

具体的に

まずは下記のgifを見てほしい。リストアイテム押下時にわざと1秒のdelayを入れてstartActivity()している。 この時、Activity Shared Element Transitionするように bundlestartActivity()の第2引数に渡している。

f:id:muumuumuumuu:20190602161732g:plain

gifでやっていること手順をまとめると、

  1. 左上のアイテムをタップ、1秒のdelayが走る(A)
  2. 1のdelayが終わる前に右上のアイテムをタップ(B)
  3. Aのdelayが終わり、左上のタップイベントが発火、startActivity()が走る
  4. Bのdelayが終わり、右上のタップイベントが発火、startActivity()が走る
  5. Aのタップイベント契機のActivityが起動される
  6. Bのタップイベント契機のActivityが起動される
  7. Back keyをタップしてBのactivityがfinishされる
  8. Back keyをタップしてAのactivityがfinishされる

step 8のあと、A, B両方のshared itemはなぜかBのpositionに戻っていき、Aのitemがいなくなってしまうのである。

結論だけ言うと

一つのActivityに登録できるActivity Shared Element Transition用のbundleは一つだけで、しかも上書き。このようなケースを防ぎたかったらActivityの多重起動はできないようにするしかない。

何が起こったのか

何が起こったかを理解するためには、Activity Shared Element Transitionの仕組みを知らないといけない。そのためにFrameworkの内部実装を読んでいく。

このシーケンス図を見てもらえればだいたいわかるんだけど、 f:id:muumuumuumuu:20190602175004p:plain

まずActivity起動時にstartActivity()の第2引数として渡すbundle (ActivityOptions) は色々あって最終的にActivityStarterActivityRecordを作るときに使われる。
別のactivityから戻って来たときのアニメーション用にこのoptionを取得する必要があるが、このときActivityRecordのstatic methodであるisInStackLocked()が使われる。

    static ActivityRecord isInStackLocked(IBinder token) {
        final ActivityRecord r = ActivityRecord.forTokenLocked(token);
        return (r != null) ? r.getStack().isInStackLocked(r) : null;
    }

このmethodはactivityのtokenからActivityRecordを返してくれる。そしてActivityRecord内に格納されているActivityOptionsを使ってActivityTransitionState#setEnterActivityOptions()がコールされる。

と言うわけでActivityOptionsとactivityのtokenは一対一で対応される。Activityが多重で起動されると起動された順番とfinishされる順番がおかしくなるとアニメーションもおかしくなる。この仕組みはアプリからは避けられないので、アプリとしてはActivityの多重起動を避けると言うアプローチがいいと思う。

おまけ

今回貼ったシーケンス図は下記のサイトで作りました。めっちゃ便利。 シーケンス図って最近エンジニアになった人には(特にweb系)馴染みのない古のツールかもしれないけど、私は好きです。

sequencediagram.org

title How to handle enter transition
note over User, Activity: Search Result Screen
User->User:click item
User->Activity:startActivityForResult()
Activity->Instrumentation:execStartActivity()
Instrumentation->ActivityManagerService:startActivity()
ActivityManagerService->ActivityManagerService:startActivityMayWait()
ActivityManagerService->ActivityStarter:setActivityOptions()

note over ActivityStarter:mRequest.activityOptions = options;
ActivityManagerService->ActivityStarter:execute()
ActivityStarter->ActivityStarter:startActivityMayWait()
ActivityStarter->ActivityStarter:startActivity()

note over ActivityStarter: create ActivityRecord using activity option as a one of parameter


note over User,ActivityStarter: New Activity is displayed

User->User:click back key
User->Activity:performStart()
Activity->Activity:getActivityOptions()
Activity->ActivityManagerService:getActivityOptions(mToken)
note over Activity,ActivityManagerService: get acvitivy options from ActivityRecord from static method
Activity<--ActivityManagerService:options
Activity->ActivityTransitionState:setEnterActivityOptions()

Javaのmethod referenceは初回しか評価されない

はじめに

言いたいことは掲題の通り。method referenceを渡しちゃうと実行時に毎度評価されるわけではない。 毎度評価されたければlambdaを渡した方がいい。

Reference

公式のドキュメントには以下のように記述がある。

Chapter 15. Expressions

The timing of method reference expression evaluation is more complex than that of lambda expressions (§15.27.4). When a method reference expression has an expression (rather than a type) preceding the :: separator, that subexpression is evaluated immediately. The result of evaluation is stored until the method of the corresponding functional interface type is invoked; at that point, the result is used as the target reference for the invocation. This means the expression preceding the :: separator is evaluated only when the program encounters the method reference expression, and is not re-evaluated on subsequent invocations on the functional interface type.

上記の様に最後の一文に「merhod referenceのプログラムに出会った時だけ評価されて、呼び出しのタイミングでは再評価されない」と明言されている。

具体的に

どう言う時に困るかと言うと、例えばクラス生成時に何かをsubscribeしていてviewを更新したいんだけど、viewだけが作り変えられてしまう時とか困る。viewが作り変えられるのでviewのinstanceは新しくなってるんだけど、method referenceでviewを更新しようとしてもこちらは再評価されず古いinstanceを更新しようとしてUIは何も変わらないということが起こりうる。

実際にコード例を示す。

例えばFragmentでBetterKnifeを使っていて、onCreateView() でviewをbindし、

@BindView(R.id.button)
Button button;

@BindView(R.id.image)
MyImageView imageView;

@Nullable
@Override
public View onCreateView(
        @NonNull LayoutInflater inflater,
        @Nullable ViewGroup container,
        @Nullable Bundle savedInstanceState
) {
    View view = inflater.inflate(R.layout.fragment_main, container, false);
    ButterKnife.bind(this, view);
    return view;
}

onViewCreated()本当に初回だけ以下のようなsubscribeを開始するとする。 以下は何かbuttonがあって、押されるたびに別のimage viewのvisibilityがVISIBLE <-> GONEでtoggleになる処理だ。 (何かしらの事情があってviewが作り変えられるたびにsubscribeしたくないものと仮定して読んでほしい :pray: )

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    if (!isInitialized) {
        disposable.add(
                RxView.clicks(button)
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribe(imageView::toggleVisibility)
        );
    }
    isInitialized = true;
}

一見良さそうに見えるがfragmentが一度detachされ、再度attachされた後、buttonを押してもimage viewのvisibilityは変わらない。この時点でsubscribe()の引数に渡されたmethod referenceは古いview imageのinstanceに対する参照を持っているからだ。

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    if (!isInitialized) {
        disposable.add(
                RxView.clicks(button)
                        .observeOn(AndroidSchedulers.mainThread())
                .subscribe((signal) -> {
                    imageView.toggleVisibility(signal);
                })
        );
    }
    isInitialized = true;
}

こんな感じでlambdaを渡してやることで回避できる。

AppBarLayoutを使う時にlayout_behaviorをFragmentに持たせるとviewが重ねて表示される

困った問題に出会ったのでメモ。

何が起こったか

Activityが CoordinatorLayout , その中に AppBarLayout を持っていて、(諸々省略するとこんな感じ)

<androidx.coordinatorlayout.widget.CoordinatorLayout>

    <com.google.android.material.appbar.AppBarLayout>

        <TextView
            app:layout_scrollFlags="scroll|enterAlways" />

    </com.google.android.material.appbar.AppBarLayout>

    <RelativeLayout /> <!-- Fragmentをhostingするcontainerのview group -->
</androidx.coordinatorlayout.widget.CoordinatorLayout>

上記のcontainerに app:layout_behavior をセットしたRecyclerViewを持ったFragmentをaddします。

<androidx.constraintlayout.widget.ConstraintLayout>
    <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <androidx.recyclerview.widget.RecyclerView />

    </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

するとこんな感じでRecyclerViewとAppBarLayoutが重ねて表示されてしまう。

f:id:muumuumuumuu:20190224180029p:plain:w200

Fragmentにわけない、もしくはActivityのcontainerに app:layout_behavior を設定する場合は問題なく表示されます。

f:id:muumuumuumuu:20190224180320p:plain:w200

なんでこんなことになるのか?

せっかくなのでAppBarLayoutCoordinatorLayoutの中身を見ていく。

そもそも前提として、app:layout_behaviorをつけると何が起きるのか?
CoordinatorLayout.LayoutParamsのコンストラクタでattribute setからapp:layout_behaviorで取得したbehaviorを取得しメンバに保持している。

android.googlesource.com

    public static class LayoutParams extends MarginLayoutParams {

        LayoutParams(@NonNull Context context, @Nullable AttributeSet attrs) {

                    R.styleable.CoordinatorLayout_Layout_layout_behavior);
            if (mBehaviorResolved) {
                mBehavior = parseBehavior(context, attrs, a.getString(
                        R.styleable.CoordinatorLayout_Layout_layout_behavior));
            }

app:layout_behaviorに指定するのは@string/appbar_scrolling_view_behaviorを使うように公式referenceにも書いているが、この参照値の実態はcom.google.android.material.appbar.AppBarLayout$ScrollingViewBehaviorというクラスパスである。上記のparseBehavior()でreflectionを使ってBehaviorのインスタンスを生成し、メンバのmBehaviorに格納する。

CoordinatorLayoutのlayoutがonPreDraw()で走る時に、onChildViewsChanged()が呼ばれるが、このメソッドの中で先ほど格納したmBehaviorをgetしてonDependentViewChanged()をコールする。

    final void onChildViewsChanged(@DispatchChangeEvent final int type) {

            // Update any behavior-dependent views for the change
            for (int j = i + 1; j < childCount; j++) {
                final View checkChild = mDependencySortedChildren.get(j);
                final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
                final Behavior b = checkLp.getBehavior();

                    switch (type) {
                        case EVENT_VIEW_REMOVED:

                            break;
                        default: // typeはEVENT_PRE_DRAWなのでdefaultに入る
                            // Otherwise we dispatch onDependentViewChanged()
                            handled = b.onDependentViewChanged(this, checkChild, child);
                            break;
                    }

onDependentViewChanged() が呼ばれた後はいくつかのメソッドを経由してAppBarLayout.ScrollingViewBehavior#offsetChildAsNeeded()に辿り着く。この中でdependencyであるAppBarLayoutのbottom位置を取得して重ならないように良い感じに調整してくれる。

github.com

  public static class ScrollingViewBehavior extends HeaderScrollingViewBehavior {

    private void offsetChildAsNeeded(View child, View dependency) {
      final CoordinatorLayout.Behavior behavior =
          ((CoordinatorLayout.LayoutParams) dependency.getLayoutParams()).getBehavior();
      if (behavior instanceof BaseBehavior) {
        // Offset the child, pinning it to the bottom the header-dependency, maintaining
        // any vertical gap and overlap
        final BaseBehavior ablBehavior = (BaseBehavior) behavior;
        ViewCompat.offsetTopAndBottom(
            child,
            (dependency.getBottom() - child.getTop())
                + ablBehavior.offsetDelta
                + getVerticalLayoutGap()
                - getOverlapPixelsForOffset(dependency));
      }
    }

というわけで一連の処理の起点にはmBehaviorが必要だ。しかし、最初に見た通りmBehaviorCoordinatorLayout.LayoutParamsのコンストラクタで取得される。fragmentで独立したxmlを持ってそこからinflateしたfragmentをコードから動的にattachする場合、fragmentのLayoutParamsはCoordinatorLayout.LayoutParamsにならないので1mBehaviorが取得できない。 そのためapp:layout_behaviorを記述したにも関わらずoffsetが正しく設定されずviewが重なって表示されていた。

で、どうすれば良い?

CoordinatorLayout.LayoutParamsからbehaviorが取得できれば良いので、CoordinatorLayoutのchildにapp:layout_behaviorをつけるしかないと思う。以下のようなStack Overflowの記事を見つけたけれどそもそもLayoutParamsがCoordinatorLayout.LayoutParamsにcastできないのでハイハイ解散という感じだ。

stackoverflow.com

そもそもなんでこんなことやりたかったかというと、containerにattachするfragmentによってAppBarLayoutがscrollしたりしなかったりという挙動を実装したくて、fragmentの中にapp:layout_behaviorが書けるのであればそっちでscroll制御できるので楽そうという理由だった。

楽はできなさそうなのでattachするfragmentによってscrollFragを変更してやるしかなさそう。

// fragmentをattach/replaceするタイミング
val text = findViewById<TextView>(R.id.text) // `app:layout_scrollFlags` を設定していたview
val layoutParam = text.layoutParams as? AppBarLayout.LayoutParams ?: return
val scrollFrag = layoutParam.scrollFlags
if ((scrollFrag and AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL) == 1) {
    // もしもscrollのオプションが付いていたらとる(scrollしないようにする)
    layoutParam.scrollFlags = scrollFrag - AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL
}

  1. 最初に示したxmlの例の場合はViewGroup#LayoutParamsになる。ここを見ると、LayoutParamsはroot viewのgetNameに$LayoutParamsをつけてreflectionしたclazzからinstance生成したもののようだ

Kotlin Bytecodeを読みたいけど、何から勉強したらいいかわからないあなたへ。ASMのドキュメントを読みましょう。

はじめに

このblog postは半年以上下書き放置していて自分の中では旬がすぎた話題ではありますが、最近Android系日本語技術podcastであるdex.fmのep.66でbytecodeが話題になったので、勿体無い精神で公開します :pray: それではどうぞ。

Kotlin書いているとたまに「これ、JavaのBytecodeだとどう変換されて動いているんだろう?」と思う時がしばしばある。

例えばこれとか

muumuutech.hatenablog.com

で、Android Studioの "Show Kotlin Bytecode" 機能を使ってbytecodeをのぞいてみるんだけど、完全に雰囲気で読んでいたので、わかるようなわからないようなゆるふわな感じだった😇これの意味は?とか聞かれても説明するなんて無理〜〜と言う程度の理解レベル。

何がつらいかっていうと、ググってもいい感じのリファレンスがないというか、まず何を読むべきかが見つけられないという状況😩😩😩 しかし軽い気持ちで呟いた結果、神々が降臨した。圧倒的感謝 :pray: 1

というわけで、Bytecodeに対してゆるふわな理解しかない人が勉強して行った記録を残していく。間違ったこと書いてたら教えてくださいmm

手取り早くbytecodeを理解した人へ

ASM User GuideのAppendixにBytecode instructionsがまとまっているので、ここを読みましょう。

ASMとは?

まずは公式の定義から。

ASM

ASM is an all purpose Java bytecode manipulation and analysis framework. It can be used to modify existing classes or to dynamically generate classes, directly in binary form. ASM provides some common bytecode transformations and analysis algorithms from which custom complex transformations and code analysis tools can be built. ASM offers similar functionality as other Java bytecode frameworks, but is focused on performance. Because it was designed and implemented to be as small and as fast as possible, it is well suited for use in dynamic systems (but can of course be used in a static way too, e.g. in compilers).

KotlinのコンバイラはASMライブラリを使ってbytecodeを生成している。なのでKotlinで生成されたBytecodeを読みたいだけだったら、ASMが何をしてどういう生成物を吐き出すか知っていれば良いっぽい。

余談だけどASMはOSSで、しかもGithubではなくGitlabでhostingされている。

gitlab.ow2.org

User Guide

まずは使う側としてUser Guide読むことにする。めちゃめちゃ長いPDFになっている。

https://asm.ow2.io/asm4-guide.pdf

読んでて面白かった部分を書いていく。なんかたまにルー大柴っぽくなった気がする。気になる人は原文読んでください…

1. Introduction

  • ASMはできるだけ早くて小さくて堅牢というのを目指している
  • ASMライブラリの目的はbyte arrayで表現されるコンパイルずみのJava classesを生成、変換、分析すること
    • class loading processは対象外
  • ASMという名前に意味はない。Cの__asm__ keyword からとっただけ
  • core APIはevent basedなクラス表現
  • tree APIはobject basedなクラス表現
    • この辺 見ると、Kotlin compilerはtreeの方使ってるのかな?という感じがする

Tree API

2. Classes 3. Methods 4. Metadata 5. Backward compatibility の章はCore APIについてだったのでスキップ)

6. Classes

  • tree APIを使ってclassを生成、変換する場合、core APIに比べて約3割多くの時間がかかり、メモリも多く使う
  • そのかわりどんな順序でもclass elementsが生成できるので便利

7. Method

  • methodの中身はInsnListでinstructionが記述されていく
    • こいつがgetOpCode()とかを持っている
    • getNext() があるからjumpが簡単にできる
  • Label, frame, line numberもinstructionではないけどAbstractInsnNodeのサブクラスとして表現される
    • これにより実instructionsの前とかにLabelとかをinsertすることができる

8. Method Analysis

  • ASMで使われるcode analysis techniqueにはdata flow analysiscontrol flow analysisがある
  • data flow analysis
    • methodのexecution frameのstateを計算する。
    • forward analysisとbackward analysisがある
    • stackから値をpopして計算して結果をstackにpush
    • interpreterJVMのように見えるが、違いは可能性がある全てのpath, argumentについてsimulateする
      • manipulated valueは可能性がある値の集合になるのでめっちゃ大きくなる。
      • 例)intergerをP="positive or null", N="negative or null, A="all integers" として表すとIADD instructionはoperandがどちらもPだったらPを返し、どちらもNだったらNを返し、そのほかのケースは全てAを返す
  • control flow analysis
    • methodのcontrol data flow graphを計算し、このgraphを解析する
    • graphはbasic blockにdecompileされる。それぞれのbasic blockは(最初のblockを除き)jumpの対象となりうる。
  • Interface and components
    • stackからのpopとかpushはframeworkで、valueをcombineしたり集合の計算とかはInterpreterとかValueと行ったuserが定義したsubclassで行われる
  • Basic Data Flow Analysis
definition means
UNINITIALIZED_VALUE all possible values
INT_VALUE all int, short, byte, boolean or char values
FLOAT_VALUE all float values
LONG_VALUE “all long values”
DOUBLE_VALUE “all double values”
REFERENCE_VALUE “all object and array values”
RETURNADDRESS_VALUE is used for subroutines (see Appendix A.2)
  • Appendix
    • 上記にも書いたが、ここにinstructionsとその操作例がまとまっているのでここを見ればだいたいbytecodeが読めるようになっている

Developer Guide読んでいく

こっちの方が短いけど難しい。3割くらいしか理解できていない気がするのでさらっとメモだけ。 ASM - Developer Guide

Main Algorithms

  • class loader
    • constant poolと(constructorの中の)bootstrap methodをparseする
    • classをparseする
    • class attributeをparseする
    • class attributeに対応したvisit methodをコールする
    • fieldについて上記と同じようなことをする
    • methodについて同上
      • ただしattributeはparseした後local variableにstore
      • labelを探してstore
      • instructionsをparse
  • 3.4.3 An example
    • 要約するのがむずいのでこの実例を見てくれたらなんとなくわかる





以上、わかるようなわからないような状態から一歩踏み出すためのリファレンス集でした。


  1. こう言う時本当に投げ銭で感謝のお気持ちを表明したいのでみんなKyashとかのリンクをTwitter bioとかに貼っておいてほしい…