Celeste Engineer

Androidとか自転車とか

Again: AsyncLayoutInflater vs Litho #potatotips

前回 は実装が悪くて完全に Litho が負けていましたが、ある程度動かせるものができたので、potatotips で発表してきました。

speakerdeck.com

結論から言えば、これでもやはり AsyncLayoutInflater のほうが速いです。「Litho は既存の XML によるレイアウトを完全に置き換えるものではない」との言葉通り、適用箇所をきちんと見極めないと意味がないよね、ということを身にしみて感じました。

前回の計測で抱えた問題

Litho は RecyclerView を念頭に置いているため、今のところ単なる ScrollView に相当するコンポーネントを持っていません。LinearLayout 相当のコンポーネントに子 View を足しても、与えられた領域のなかに子 View がおさまるよう計算するだけで、これが前回の計測で問題となった点でした。子 View が多くなると、子同士がオーバーラップし始め、その分無駄に計算しまくるためです(ログにもそのようなメッセージが表示されます)。

どう改善したか

ScrollView を親にその下で Column を設定しました。これで画面内にびっしり Text が敷き詰められるようなことがなくなり、計算がシンプルになって高速化しました。

ただしまだ問題はある

スクロールすると、初回描画領域外にあった Text が見えません😂。

Robolectric の依存を事前に解決してテストにかかる時間を短縮する

起こったこと

Robolectric を使って JVM 上でテストを動かす場合、次のようなエラーログを目撃することが稀によくあります。

com.sample.SampleTest > sampleTestCase STANDARD_ERROR
    Downloading: org/robolectric/android-all/6.0.0_r1-robolectric-0/android-all-6.0.0_r1-robolectric-0.pom from repository sonatype at https://oss.sonatype.org/content/groups/public/

テストの実行時に依存を解決しに行くような挙動をしていることがわかります。CI の場合、最初に実行されるいくつかのテストでこのようなログが出てきます。 CI の特性上実行ごとに依存を解決することには問題がありませんが、テストの実行時に都度依存を解決するのではテストの実行時間に影響が出ます。昨今の CI as a Service を利用する場合は、タスクやステップの実行時間制限があるものもあり、気をつけないとテストの実行ステップがタイムアウトして CI が失敗してしまいます。 また、ダウンロードのたびに STANDARD_ERROR としてエラーログが吐き出されるのも精神衛生上あまりよろしくありません。

解決の方針

テストごとに必要な依存を解決するのではなく、テストステップの前にすべての依存をシュッと解決しておき、あとはテストを動かすだけの状態にできれば、純粋にテストステップはテストの実行時間だけになり、タイムアウトの可能性が減らせます。

解決策

ちょうど同じようなことを解決しようとしている issue が Robolectric のリポジトリに立っています。

github.com

この issue にあるコメントによると、プロジェクトルートにある build.gradle に次のコードを記述し、依存解決のタスクを追加します。

subprojects { project ->
    task downloadDependencies(type: Copy) {
        description "Downloads all dependencies."
        group "build"

        from {
            // Use of closure defers evaluation until execution time
            project.configurations
                .findAll { configuration -> configuration.canBeResolved }
                .collect { configuration -> configuration.resolvedConfiguration.lenientConfiguration.files }
        }
        into "$project.buildDir/dependencies"
    }
}

そして CI の設定ファイルに、追加した依存解決のタスクをテストステップの前にさしこみます。例えば WerckerCI であれば、次のように記述します。

build:
  steps:
    - script:
      name: "install dependencies"
      script: |
        ./gradlew downloadDependencies
    - script:
      name: "test"
      script: |
        ./gradlew testDebug

結果

CI のトータルの時間ではさほど変化がないか、微増となりますが、テストステップの実行時間は1分ほどの改善が見られました。

余談

最近 Wercker がかなり不安定になっていて世は大消耗時代です。

富士ヒルに参加してきた!

人生初の自転車イベントとして、富士ヒルクライムに参加してきました。普段から山を登りに行くのは好きなのと、完走者が多いということで、3月にシュッと申し込みをしていました。

知人が宿等のアテンドをしてくれたので、自分は移動手段と運転を担当しました。前日も当日もぐっすり睡眠、大事です。

前日は受付とブースをウロウロしました。

f:id:KeithYokoma:20170611215547j:plain

頑張るぞ!写真をツイートして補給食もGET!

f:id:KeithYokoma:20170611215559j:plain

このあとアクションカムにSDカードを入れ忘れたことを思い出して電気屋さんダッシュし、ご飯を食べて宿でぐっすり眠りました。

本番当日は、4時に起きて移動、車を止めて自転車を組み立て、荷物を預けて順番が来るのを待ちました。荷物を預けるまでに大行列が出来ていて、早起きは三文の得ということわざを思い出しました。 自分は20グループでのスタートということで、5合目まで登る最終組でした。スタートがきられて計測開始までにすでに何やら登っていて消耗が始まるな?という気持ちがありましたが、ゆるゆるとペースを維持しつつ上げてくぞ💪という作戦で臨みました。

f:id:KeithYokoma:20170611215530j:plain

富士スバルラインは富士山の5合目まで登る3つの道のうち一番ゆるやかな坂(その分距離は長い)ということで、最大斜度も10%いかない感じではありましたが、6~8%の坂はこらえて、ちょくちょくやってくる3~4%の坂で少しずつスッとペースをあげられるようにしたところ、特に目標タイムとかは設定しなかったものの、1時間52分ほどで登りきりました!

f:id:KeithYokoma:20170611215533j:plain

f:id:KeithYokoma:20170611215554j:plain

いくつか沿道で応援してくれる人たちのいる場所や、関門、カメラマンの人たちがいる場所があって、お祭りのようなイベントでもありました。山岳スプリット区間で「ここからもがけ!」という標識を見たときは「どこまでもがくのー!」という気持ちになるくらいそびえる坂とヘアピンカーブが見えましたが、坂をシュッと登るのは楽しいですね!速くはないけど、もうちょっと!と言う所まで来てニヤついてしまいました😁

最後はカレーと焼きそばをモリモリ食べて下山!

f:id:KeithYokoma:20170611215525j:plain

冬装備でも十分寒い5合目でしたが、下に降りると暖かみを感じました😉

行き帰りの運転は渋滞に捕まったり道を間違えたりとありましたが、無事に返ってくることが出来ました😃。また来年、次はブロンズを目指せるように頑張ります!

AsyncLayoutInflater vs Litho

先月の shibuya.apk で Litho の発表があり、非同期でレイアウトの展開(draw 以外の部分)をやってくれる機能をもっているということで、AsyncLayoutInflater と Litho で勝負したらどっちが速いかを確かめたくなったので、結果を書いておこうと思います。

内容

10000個の TextView をもつレイアウトを表示するまでの時間を競います。

結果

データを取るまでもないほどに Litho が遅くて勝負になりませんでしたorz

いや、きっとまだ 0.3.x と正式リリース前なのと、自分自身が API を完全には理解しきれていないせいだと思いたい😞

リポジトリ

とりあえずそれっぽくつくったものは GitHub に公開しています。Litho の状況からして今後改善の余地は大いにあるはずですので、ゆっくりと見守りたいと思います。

github.com

AsyncLayoutInflater について

サポートライブラリに入っている、名前の通りレイアウトを非同期に inflate するクラスです。コールバックでレイアウトまで完了した view が得られるので、あとはそれを親の ViewGroup に追加することで描画できます。内部は至極簡単な作りで、一つのスレッドが無限に inflate の要求を待っていて、要求が来るとそれをレイアウトまで計算してコールバックに渡す、というものです。

android:windowBackground に指定する BitmapDrawable の位置を指定するときに気をつけること

Android アプリで SplashScreen を作る場合、android:windowBackground をつかうことでレイアウトを読み込まなくてもスプラッシュ用の画像を表示できます。レイアウトを待たなくてもよいので、アプリの起動直後からスプラッシュ画像が見えてよい、というのが android:windowBackground を使う場合の利点ですが、このプロパティに指定できるのはレイアウトではなく Drawable Resource です。

多くの場合スプラッシュ用画像には png 等の画像ファイルを用意すると思いますが、android:windowBackground に直接その画像を設定すると、画面サイズにあわせて画像が引き伸ばされてしまいます。これを避けるには、別途 XML で Bitmap Drawable を用意し、それを android:windowBackground に設定します。XML を用意すると画像を複数重ねたりすることも柔軟にできるようになり便利ですが、いくつか注意が必要です。

背景が必要な場合は Layer Drawable を使う

背景に模様を入れたり、色をちょっと変えたいなどの場合には、Layer Drawable で背景と Bitmap を重ねます。

背景のレイヤでは、<item>android:gravity="fill"を設定しましょう。

<?xml version="1.0" encoding="utf-8"?>
<layer-list
    xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:gravity="fill">
        <shape android:shape="rectangle">
            <solid android:color="@color/splash_background"/>
        </shape>
    </item>
    <item>
        <bitmap
            android:src="@drawable/splash_image"/>
    </item>
</layer-list>

引き伸ばしを避けるために gravity を指定する

LayerDrawable 内で Bitmap を引き伸ばさず自分で位置を決めて表示する場合<item><bitmap>双方にandroid:gravityを設定します。 <bitmap>android:gravityがないと、端末によっては引き伸ばされてしまいます。

<?xml version="1.0" encoding="utf-8"?>
<layer-list
    xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:gravity="fill">
        <shape android:shape="rectangle">
            <solid android:color="@color/splash_background"/>
        </shape>
    </item>
    <item android:gravity="center">
        <bitmap
            android:src="@drawable/splash_image"
            android:gravity="center"/>
    </item>
</layer-list>

Context#startActivity からすぐに Activity が起動しないパターン

前提条件

Service から次のように Activity を起動しようとした時、直近 5 秒以内に他の Activity を Home キーで閉じていると、startActivity の呼び出しからすぐには Activity が起動しません。

// this は Service
Intent intent = new Intent(this, SomeActivity.class);
// Service から起動するときには FLAG_ACTIVITY_NEW_TASK が必要
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);

Affinity やその他 LaunchMode などの設定に関わらず、Activity を Home キーで閉じたあと 5 秒は Context#startActivity を呼んでもすぐに Activity は起動しないようになっています。

StackOverflow

同じようなことにハマっている人がいて、質問が上がっていました。

stackoverflow.com

Home キーをおした時の処理

PhoneWindowManager に、Home キーを押したときの処理があります。具体的にはPhoneWindowManager#launchHomeFromHotKey(boolean, boolean)がその処理を受け持つメソッドです。 このメソッドはまず、 KeyGuard が有効かどうかで処理を分岐し、有効でロックスクリーンが表示されている場合はなにもしません。 KeyGuard のアンロックに成功した時、または KeyGuard が関係ない場合にはホームに戻る処理を実行しますが、どちらも必ず次の処理が入ります。

try {
  ActivityManagerNative.getDefault().stopAppSwitches();
} catch (RemoteException e) {
}

名前からして何かを差し止めるための処理に見えますが、ActivityManagerNative.getDefault()で取得しているオブジェクトの実体はActivityManagerServiceです。

ActivityManagerService#stopAppSwitches()

そしてActivityManagerService#stopAppSwitches()を見ると、次にあげる処理を実行しています。

  1. メンバ変数のmAppSwitchesAllowedTimeに5000ミリ秒後(==5秒後)を代入
  2. メンバ変数のmDidAppSwitchにfalseを代入
  3. Handlerに送信した未実行のDO_PENDING_ACTIVITY_LAUNCHES_MSGを削除
  4. APP_SWITCH_DELAY_TIMEの遅延(5000ミリ秒==5秒)でDO_PENDING_ACTIVITY_LAUNCHES_MSGを再送信

これで"Home キー押下後 5 秒は startActivity がすぐに効かない"の 5 秒という時間の理由がはっきりしました。またDO_PENDING_ACTIVITY_LAUNCHES_MSGという定数名から分かるとおり、5 秒後にActivityManagerService#L1878で Activity の起動を再開します。

この 5 秒以内にContext#startActivity(Intent)を実行すると、ActivityManagerService#checkAppSwitchAllowedLocked()でfalseが返ります。このメソッドを読んでいるとandroid.Manifest.permission.STOP_APP_SWITCHESというパーミッションの存在に気が付きますが、このパーミッションの ProtectionLevel はsystemOrSignatureですから、通常のアプリは許可を得られません。

なぜ Home キー押下後の 5 秒間 Context#startActivity() を止められるのか

APP_SWITCH_DELAY_TIMEのコメントによると、Home キーを押したあとに信頼されない Activity の起動を防ぐためとあります。Home キーのイベントで Activity を起動するような、端末をハイジャックしたような挙動を防ぐためのもののようです。

Home キーのイベント処理からコードを追いかけて分かる通り、Activity#onUserLeaveHint()などでActivity#finish()をしていてもこの問題は回避できません。なぜなら問答無用で Home キーのイベントで強制的に 5 秒間の保留が決まるからです。

都民の森に登ってきた

今日も自転車でヒルクライムでした。今日は以前話をしていた人たちと都民の森へ!

昼に武蔵五日市駅にあつまって、そこから都民の森へ行って帰ってくる行程です。以前は武蔵五日市から檜原街道の途中の交差点を上野原にむかって登っていったことがあるのと、都民の森自体は風張峠から下ってきたのを通り過ぎただけだったので、実は目的地としては初めてでした。

残り8kmくらいのところでチェーンが脱落してしまい、直そうとしたところ脱落したチェーンを受けるやつが邪魔をしてうまく引っ掛けられなかったので、一旦それをゆるめてどかして引っかかったら戻す…ということをしました。お陰さまで手がドロドロになってしまいました😞

都民の森ではアイスクリームと甘辛醤油のだんごを食べてリフレッシュしました😁。

くだりは快調でしたが、ふもとのあたりでは大渋滞…ゴールデンウィーク感あるなぁと思って渋滞をノロノロ進んでいたところ、もう数キロで駅につくというところで、通りすがりのおばちゃんが「こっちの裏道抜けたら渋滞避けられるよ!!」ということで、住民じゃないと分から無さそうな住宅街の裏道を教えてもらいました。昨日に引き続き、親切なおばちゃんに助けられて大変ありがたい😉。 「そこを右に曲がってあとは道なりにいけばいいよ」と犬のお散歩をしつつもいい感じのきつい坂を登っていて、おばちゃんの足腰の強さを感じつつ、無事に駅にたどり着きました。