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

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

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とかに貼っておいてほしい…

TabItemをxml layout fileに記述してもview hierarchyからfindViewById()できない理由

Tweetにリンクを貼っている通り、公式ドキュメントによると

TabItem is a special 'view' which allows you to declare tab items for a TabLayout within a layout. This view is not actually added to TabLayout, it is just a dummy which allows setting of a tab items's text, icon and custom layout.

This view (Layout fileに定義されたTabItem)はただのダミーで、text, icon, それからcustom layoutの設定だけができるとある。

どうやって実装しているのか気になったので、内部実装を調べたのでメモ。

知りたいこと

こんな感じでTabItemをTabLayoutのchild viewとして記述しても、

<android.support.design.widget.TabLayout
    android:id="@+id/tab"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent">
    
    <android.support.design.widget.TabItem
        android:id="@+id/tabItem"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</android.support.design.widget.TabLayout>

コードからアクセスしようとしたら(ちなみにIDEはR.idを認識してくれる) f:id:muumuumuumuu:20190211155020p:plain

NPEが発生する。

java.lang.NullPointerException: Attempt to read from field 'android.graphics.drawable.Drawable android.support.design.widget.TabItem.icon' on a null object reference

ViewHierarchyに存在する(?)のにidからviewが引けない仕組みを調べる。

TabLayoutの実装

例によってAndroidXではなくsupport libraryのコードを読んだ結果ですごめんなさい 😇

まず前提としてTabLayoutHorizontalScrollViewをextendsしたviewである。でHorizontalScrollViewViewGroupなので、addView()メソッドをコールされることでchild viewを持つことができる。(LayoutInflatorがxmlを読み込んでview hierarchyを構成するときにparent viewに対してこのaddView()が呼ばれる。)

というわけでTabLayout#addView()を見て行く。

    public void addView(View child) {
        this.addViewInternal(child);
    }

    public void addView(View child, int index) {
        this.addViewInternal(child);
    }

    public void addView(View child, android.view.ViewGroup.LayoutParams params) {
        this.addViewInternal(child);
    }

    public void addView(View child, int index, android.view.ViewGroup.LayoutParams params) {
        this.addViewInternal(child);
    }

こんな感じでただaddViewInternal() をコールしているだけ。このaddViewInternal()は何をしているかというと、

    private void addViewInternal(View child) {
        if (child instanceof TabItem) {
            this.addTabFromItemView((TabItem)child);
        } else {
            throw new IllegalArgumentException("Only TabItem instances can be added to TabLayout");
        }
    }

childの型を調べてTabItemだったらaddTabFromItemView()を呼んでいる。

    private void addTabFromItemView(@NonNull TabItem item) {
        TabLayout.Tab tab = this.newTab();
        if (item.text != null) {
            tab.setText(item.text);
        }

        if (item.icon != null) {
            tab.setIcon(item.icon);
        }

        if (item.customLayout != 0) {
            tab.setCustomView(item.customLayout);
        }

        if (!TextUtils.isEmpty(item.getContentDescription())) {
            tab.setContentDescription(item.getContentDescription());
        }

        this.addTab(tab);
    }

ここで面白いのはTabItemはViewをextendsしたclassなのに、ただのdataを保持するだけのクラスのように扱われている。xmlからattributeSetを引回す必要があるのでViewである必要があるのだろうけど。

public class TabItem extends View {
    public final CharSequence text;
    public final Drawable icon;
    public final int customLayout;

    public TabItem(Context context) {
        this(context, (AttributeSet)null);
    }

    public TabItem(Context context, AttributeSet attrs) {
        super(context, attrs);
        TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs, styleable.TabItem);
        this.text = a.getText(styleable.TabItem_android_text);
        this.icon = a.getDrawable(styleable.TabItem_android_icon);
        this.customLayout = a.getResourceId(styleable.TabItem_android_layout, 0);
        a.recycle();
    }
}

生成したTabLayout.Tabに対して保持していたtext, icon, customLayout, contentDescriptionの4種類の情報のみをセットしてaddTab()している。 ここでidが捨てられているので、実際にview hierarchyに追加されたtabをidからfindできない仕組みになっていたのだ。

以下は余談になるが、この辺りは読んでいて色々面白い。

TabLayout.Tabを生成するnewTab()`メソッドはpoolを利用してrecycleしている。

    private static final Pool<TabLayout.Tab> tabPool = new SynchronizedPool(16);

    private final Pool<TabLayout.TabView> tabViewPool;

    public TabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
...
        this.tabViewPool = new SimplePool(12);
...
    }

    @NonNull
    public TabLayout.Tab newTab() {
        TabLayout.Tab tab = this.createTabFromPool();
        tab.parent = this;
        tab.view = this.createTabView(tab);
        return tab;
    }

    protected TabLayout.Tab createTabFromPool() {
        TabLayout.Tab tab = (TabLayout.Tab)tabPool.acquire();
        if (tab == null) {
            tab = new TabLayout.Tab();
        }

        return tab;
    }

tabPooltabViewPoolのmaxPoolSizeが16と12で異なっているのはなんでなんだろう?