nashcft's blog

時々何か書く。

週末までにやったこと

週の頭に今週これやろうとメモってたことに対する振り返り

TODO に書いてあったこと

Binder transaction buffer のサイズについて再調査 & 記事の加筆修正

やってない 😇
来週末までに終わらせる。

TDD_kotlin: 14章を終わらせる

第I部の終盤だし1章1章が結構かかるかなーという予想に反して分量が少なかったので勢いで進めてたら第I部終わった。

Kotlin で『テスト駆動開発』をやった過程についてブログを書く (1~4章?)

7章まで書いた:

"Reactive Android Programming" の写経レポジトリを作成して chapter 1 まで終わらせる

レポジトリを作ってプロジェクトのセットアップをするところまでは終わらせた。Chapter 1 の内容に対する進捗としては半分。
欲を出してこっちも Kotlin でやろうとして色々調べていたら時間がかかってしまった。

書いてないことでやったこと

Kotlin で『テスト駆動開発』を進める (第5章 - 第7章)

前回 nashcft.hatenablog.com

今回は第5章から第7章まで進める。
これまでの過程で書いたコードは以下の通り。前回の脚注で触れているが実際に私が取り組んだ時のものに対して順序の再構成やコードのブラッシュアップなどを行っているため、レポジトリのコードやコミットの流れとは異なっている部分がある。

MoneyTest.kt

class MoneyTest {

    @Test
    fun testMultiplication() {
        val five = Dollar(5)
        assertEquals(Dollar(10), five.times(2))
        assertEquals(Dollar(15), five.times(3))
    }

    @Test
    fun testEquality() {
        assertTrue(Dollar(5) == Dollar(5))
        assertFalse(Dollar(5) == Dollar(6))
    }
}

Dollar.kt

data class Dollar(private val amount: Int) {

    fun times(multiplier: Int) = Dollar(amount * multiplier)
}

第5章 あえて原則を破るとき

これまでは1種類の通貨に関して注目していたが、この章から本題となる「多国通貨の足し算」を行えるようにするための準備が始まる。
まずはもう1つの通貨としてフラン (Franc) を追加し、これまで扱ってきたUSドルと同じように動作させたい:

  • [ ] $5 + 10CHF = $10 (レートが 2:1 の場合)
  • [x] $5 * 2 = $10
  • [x] amount を private に
  • [x] Dollar の副作用をどうする?
  • [ ] Money の丸め処理をどうする?
  • [x] equals()
  • [x] hashCode()
  • [x] null との等値性比較
  • [x] 他のオブジェクトとの等値性比較
  • [ ] 5CHF * 2 = 10CHF

この章で行うことは Franc クラスに対して Dollar に対して書いたものと同様のテストを追加し、 Dollar をコピペして Franc クラスを作るというもの。短時間で確実に前進できる手段を選ぶためならばこんな行いも許容される。ただしその後きっちりリファクタリングをするならば、だが。

class MoneyTest {

    // ...

    @Test
    fun testFrancMultiplication() {
        val five = Franc(5)
        assertEquals(Franc(10), five.times(2))
        assertEquals(Franc(15), five.times(3))
    }
}
data class Franc(private val amount: Int) {

    fun times(multiplier: Int) = Franc(amount * multiplier)
}

さて、これまでは殆どの場合テストを書くところからコードをリファクタリングするところまでをセットに進めてきたが、この点に関してはこれから第11章にかけてのより大きな流れでリファクタリングをしていくことになるので、とにかく先に進もう。

犯した罪を追加した現時点での TODO リスト:

  • [ ] $5 + 10CHF = $10 (レートが 2:1 の場合)
  • [x] $5 * 2 = $10
  • [x] amount を private に
  • [x] Dollar の副作用をどうする?
  • [ ] Money の丸め処理をどうする?
  • [x] equals()
  • [x] hashCode()
  • [x] null との等値性比較
  • [x] 他のオブジェクトとの等値性比較
  • [x] 5CHF * 2 = 10CHF
  • [ ] DollarFranc の重複
  • [ ] equals の一般化
  • [ ] times の一般化

第6章 テスト不足に気づいたら

この章では前章で作ってしまった重複を消していくために、まず共通の親クラスを作りそれを継承して equals() の一般化を行う、という進め方をする。
ところで私は Dollar, Franc とも data class として実装したので、equals() を自分で実装しておらず共通化するものがないのだった。

  • [ ] $5 + 10CHF = $10 (レートが 2:1 の場合)
  • [x] $5 * 2 = $10
  • [x] amount を private に
  • [x] Dollar の副作用をどうする?
  • [ ] Money の丸め処理をどうする?
  • [x] equals()
  • [x] hashCode()
  • [x] null との等値性比較
  • [x] 他のオブジェクトとの等値性比較
  • [x] 5CHF * 2 = 10CHF
  • [ ] DollarFranc の重複
  • [x] equals の一般化
  • [ ] times の一般化

とはいえ後のことも考えて、書籍の内容に則り Money クラスを作成し DollarFranc に継承させ、ついでに本章で追加される assertion も足しておく。

// Dollar.kt
data class Dollar(amount: Int) : Money(amount) {

    fun times(multiplier: Int) = Dollar(amount * multiplier)
}

// Franc.kt
data class Franc(amount: Int) : Money(amount) {

    fun times(multiplier: Int) = Franc(amount * multiplier)
}

// Money.kt
open class Money(private val amount: Int)

Kotlin のクラスはデフォルトで final なので継承できるようにするためには open を足しておく必要があることに注意。

ところで実際に写経をやっていた時は書籍と Elm 版記事の両方を開きながら進めていて、この章に取り組んでいた時に Elm 版の同じ章のところを見返してあー Union Type いいよなーパターンマッチいいよなー使いたいなーってなって探したら sealed class というのがあってそのものズバリな感じの使用感だったのでそれを使った。これによって times() の宣言自体は Money の中で行えるため、 Elm 版と同じく共通化を一歩先に進められるようになる。

// Money.kt
package money

data class Dollar(private val amount: Int) : Money(amount)
data class Franc(private val amount: Int) : Money(amount)

sealed class Money(private val amount: Int) {

    fun times(multiplier: Int) = when (this) {
        is Dollar -> Dollar(amount * multiplier)
        is Franc -> Franc(amount * multiplier)
    }
}

Sealed class の主な特徴は以下の通り:

  1. 宣言されたファイルの中でしか sealed class を継承することができない
  2. Constructor がデフォルトで private で、private でない constructor を持つことができない
  3. when 式と合わせて Union Type っぽくパターンマッチすることができる
    • when 式で検査対象に sealed class を渡して is <class> で分岐させると、その sealed class を継承したクラスだけ並べれば網羅されたことになり、 else を書く必要がない

上のコードでは times() は 3. によって関数内で when 式を用いて自身をパターンマッチにかけ、マッチしたクラスに計算を適用したものを返すように書くことで1つの式にまとめ上げることができ、 Money の関数として持ち上げることに成功している。パターンマッチのケースも Money を継承した DollarFranc だけでよい。このような方針で2017年現在の*1 Java でやろうとすると times() メソッド内では instanceof を用いた分岐フローが積み重なる嫌なコードになってしまうのでそんなアプローチを採用するわけにはいかず、まだ Money に持ってくることはできない。
また、上記以外にも 2. によって Money クラスを直接インスタンス化して使用することを防いでおり、ユーザが不用意な使用をできないようにもなった。これはこの一連の記事の最後で小話に使うかもしれない。

  • [ ] $5 + 10CHF = $10 (レートが 2:1 の場合)
  • [x] $5 * 2 = $10
  • [x] amount を private に
  • [x] Dollar の副作用をどうする?
  • [ ] Money の丸め処理をどうする?
  • [x] equals()
  • [x] hashCode()
  • [x] null との等値性比較
  • [x] 他のオブジェクトとの等値性比較
  • [x] 5CHF * 2 = 10CHF
  • [ ] DollarFranc の重複
  • [x] equals の一般化
  • [ ] times の一般化 -> WIP

第7章 疑念をテストに翻訳する

DollarFranc を比較するという内容。ここも data class としてそれぞれを実装したため異なるクラスで比較しても失敗するようになっている。
ところでここでテストを追加する時、 Dollar(5) == Franc(5) と記述すると以下のメッセージと共にコンパイルエラーが発生することに気がつく。

Operator '==' cannot be applied to 'Dollar' and 'Franc'

これを Dollar(5).equals(Franc(5)) とするとコンパイルが通るし結果も本章で期待するものになる。== は前回書いたように a?.equals(b) ?: (b === null) の糖衣構文のようなものだがどうやら完全に同じ挙動をするわけではなく、直系の親子クラスでないとコンパイルレベルで受け付けてくれないようだ。気持ち悪いけど今回はここだけ as Money をつけるなり equals() を使うなりしておくことにする。

ここまでのまとめ

現在のコードの様子:

MoneyTest.kt

class MoneyTest {

    @Test
    fun testMultiplication() {
        val five = Dollar(5)
        assertEquals(Dollar(10), five.times(2))
        assertEquals(Dollar(15), five.times(3))
    }

    @Test
    fun testEquality() {
        assertTrue(Dollar(5) == Dollar(5))
        assertFalse(Dollar(5) == Dollar(6))

        assertTrue(Franc(5) == Franc(5))
        assertFalse(Franc(5) == Franc(6))

        assertFalse(Dollar(5).equals(Franc(5)))
    }

    @Test
    fun testFrancMultiplication() {
        val five = Franc(5)
        assertEquals(Franc(10), five.times(2))
        assertEquals(Franc(15), five.times(3))
    }
}

Money.kt

package money

data class Dollar(private val amount: Int) : Money(amount)
data class Franc(private val amount: Int) : Money(amount)

sealed class Money(private val amount: Int) {

    fun times(multiplier: Int) = when (this) {
        is Dollar -> Dollar(amount * multiplier)
        is Franc -> Franc(amount * multiplier)
    }
}

TODO リスト:

  • [ ] $5 + 10CHF = $10 (レートが 2:1 の場合)
  • [x] $5 * 2 = $10
  • [x] amount を private に
  • [x] Dollar の副作用をどうする?
  • [ ] Money の丸め処理をどうする?
  • [x] equals()
  • [x] hashCode()
  • [x] null との等値性比較
  • [x] 他のオブジェクトとの等値性比較
  • [x] 5CHF * 2 = 10CHF
  • [ ] DollarFranc の重複
  • [x] equals の一般化
  • [ ] times の一般化 -> WIP
  • [x] FrancDollar を比較する
  • [ ] 通過の概念

今回は通貨のクラスを1つ増やし、それらに共通の親クラスを作って継承させるところまで進めた。第6章では書籍に載ってるコードの構成から離れて sealed class を用いた実装を行ってみた。今回は主な特徴の紹介程度の使い方ではあるが、その便利さから今後も使われることになるので覚えておいてほしい。

*1:Java もパターンマッチを実装する提案 (http://openjdk.java.net/jeps/305) が上がっていて、早くて 10 から実装されるかもしれない

穏やかに週末をむかえる

直近の日記としてのブログで忙しいとかつらいとかニャーンとか言ってた原因の切羽詰まった状況が木曜で終わり、その開放感に任せてTDD写経のブログを書き上げた後、とても穏やかな気持ちでむかえた今週最後の平日はそれなりな進捗と共にとても穏やかに過ぎ、帰る前にちょっとバタバタしたのをとても穏やかな気持ちで対応して帰りしなに寄り道してラーメンを食べて帰宅して諸々の後に今がありこれから寝ます。Rxの勉強はやり損ねました。土曜日は幕張までガルパンを観に行きます。おわり。

Kotlin で『テスト駆動開発』を進める (第1章 - 第4章)

せっかく『テスト駆動開発』を読むのだから写経をしながら、それにただ写経するだけというのもなんだし他の言語でやろうということで学習がてら Kotlin で取り組んでいたのだが、これが中々面白かったので取り組んだ過程を、ある程度整理した上で*1書いてまとめる事にした。

大雑把にいうと以下の記事に影響を受けており、章の区切りなどもこちらに沿うようにしている。

qiita.com

レポジトリは↓

github.com

なぜ過程を書くのか?

もともと『テスト駆動開発』の内容は、新訳版の翻訳者である t_wada さんのブログ記事にもある通り、著者 Kent Beckテスト駆動開発の手法を用いながらインクリメンタルにプログラムを書き進めていく時の思考の推移を辿っていくような構成になっている。

本書の第I部、第II部は、ペアプログラミングのような語り口で、そのときどきの思考の言語化を挟みながら、コードがだんだん育っていくという構成を採っています。

この「思考」が、使用した言語によって変化させられる様であるとか、その結果として書き上げられるコードの形にも表れるところがとても興味深く感じられたので、私自身が辿った思考の流れも残しておけば誰かにとって参考になったり興味深い読み物になったりするのではないかなあと思ったからである。

進め方

Qiita の記事の方針に沿って本の内容と変わらないところは TODO リストとコードだけで進め、Kotlin特有の何かや思考が本の内容と異なった部分に関してはそれについて書いていく。

事前準備

プロジェクトのセットアップは各自適当にやってください。私はこれを参考に IntelliJ IDEA で build.gradle を書いていたら色々サジェストしてくれたので結果的に このようになった。

第1章 仮実装

最初の TODO リストは以下の通り:

  • [ ] $5 + 10CHF = $10 (レートが 2:1 の場合)
  • [ ] $5 * 2 = $10

早速テストクラスを作る。

// MoneyTest.kt
// package, import は省略

class MoneyTest {

    @Test
    fun testMultiplication() {
        val five: Dollar = Dollar(5)
        five.times(2)
        assertEquals(10, five.amount)
    }
}

このレベルだとJavaと殆ど差異がない。ここでの Kotlin 的要素といえば以下のようなものがある:

  • デフォルトの公開レベルは public (classfun の前に修飾子をつけていない)
  • 変数宣言時に val をつける
    • val/var があり、前者は再代入不可、後者は再代入可
  • 型は後置表記
    • 型推論が効くので省略が可能。以後明示する必要がなければ全部省略する

以後進行にあまり関係のない Kotlin 要素はスルーしていくので各自適当にググってください。

最初のテストを書いたところで TODO リスト更新:

  • [ ] $5 + 10CHF = $10 (レートが 2:1 の場合)
  • [ ] $5 * 2 = $10
  • [ ] amount を private に
  • [ ] Dollar の副作用をどうする?
  • [ ] Money の丸め処理をどうする?

まずはコンパイルエラーを直す。 Dollar クラスに amounttimes(Int) があれば良い。

// Dollar.kt
class Dollar(var amount: Int) {

    fun times(multiplier: Int) {}
}

Kotlin はコンストラクタで引数に受けた値をそのままフィールドに代入するだけ、という場合はクラス名の後の() に修飾子つきの引数を記述するとそのまま値を入れてくれる。今回 amount はそれに当てはまるので Dollar(var amount: Int) となった。

とりあえずコンパイルが通って、次はテストが red になる。
テストを通すには amount10 を返してくれればいいのでとにかく10を返すようにする。
テストが green になったらリファクタリングtimes() のなかで amount を変更するようにする。

class Dollar(var amount: Int) {

    fun times(multiplier: Int) {
        amount = 5 * 2
    }
}

数字のハードコーディングになっているところをメンバや引数に置き換えて重複を排除していく。この辺は本の内容と一緒。

class Dollar(var amount: Int) {

    fun times(multiplier: Int) {
        amount *= multiplier
    }
}

この間コードを修正するたびにテストを実行して green であることを確認し続ける。ここまでくれば TODO リストの項目を1つ完了にできる。

  • [ ] $5 + 10CHF = $10 (レートが 2:1 の場合)
  • [x] $5 * 2 = $10
  • [ ] amount を private に
  • [ ] Dollar の副作用をどうする?
  • [ ] Money の丸め処理をどうする?

第2章 明白な実装

本の内容と変わりがないのでスキップ。

class Dollar(val amount: Int) {

    fun times(multiplier: Int) = Dollar(amount * multiplier)
}

amount に再代入しなくなったので var から val に、また times() は値を返却する1行の関数になったのでブロック表記から式表記に変更できた。

TODO リスト更新:

  • [ ] $5 + 10CHF = $10 (レートが 2:1 の場合)
  • [x] $5 * 2 = $10
  • [ ] amount を private に
  • [x] Dollar の副作用をどうする?
  • [ ] Money の丸め処理をどうする?

3章 三角測量

Value Object パターンのお話から始まり、DollarValue Object として扱うための TODO (equals(), hashCode()) がリストに項目が追加される。
これらの事情は Kotlin も一緒なので本のコードとほぼ同様に equals() 実装して、最後にまた TODO リストを更新。

  • [ ] $5 + 10CHF = $10 (レートが 2:1 の場合)
  • [x] $5 * 2 = $10
  • [ ] amount を private に
  • [x] Dollar の副作用をどうする?
  • [ ] Money の丸め処理をどうする?
  • [x] equals()
  • [ ] hashCode()
  • [x] null との等値性比較
  • [ ] 他のオブジェクトとの等値生比較
class Dollar(val amount: Int) {

    fun times(multiplier: Int) = Dollar(amount * multiplier)

    override fun equals(other: Any?) = amount == (other as? Dollar)?.amount
}
class MoneyTest {

    // ...

    @Test
    fun testEquality() {
        assertTrue(Dollar(5) == Dollar(5))
        assertFalse(Dollar(5) == Dollar(6))
    }
}

上記のコードで Dollar 同士を equals() ではなく == で比較するように実装しているが、Kotlin の ==Java== とは異なり equals() を用いた比較の糖衣構文でだからある。ちなみに equals()== は全く同じ機能ではなく、リファレンスによると a == ba?.equals(b) ?: (b === null) と同等の判定を行うとのこと。== に関しては後でちょっと話題になる。
あと Kotlin は基本的に null を扱う場合は ? が最後についた型や演算子を使ってNPEを吐かないように実装しないとコンパイラに怒られ、結果的に equals() の実装は null との等値性比較をクリアしている。

ところで Value Object を作るたびに equals() とか hashCode() とかいちいち実装しなきゃいけないとか等値性比較で色々考慮しなきゃいけないの大変面倒で、Kotlin に来てまでジャバ界のつらみを背負いたくないとなるが、そこはさすが最近の言語ということで、 data class というものがあり、これは以下の特徴を持っている:

  • Data class として定義するとそのクラスに適した equals(), hashCode() が自動的に付与される
  • toString()"<Class name>(<field>=<value>[, <field>=<value>...])" という出力のものが実装される

Dollar をただの class から data class に変更すると、テストをそのままにさっき実装した equals() を捨てることができ、さらにおまけでこの章の最後に追加された TODO の項目も潰せるようになる:

data class Dollar(val amount: Int) {

    fun times(multiplier: Int) = Dollar(amount * multiplier)
}
  • [ ] $5 + 10CHF = $10 (レートが 2:1 の場合)
  • [x] $5 * 2 = $10
  • [ ] amount を private に
  • [x] Dollar の副作用をどうする?
  • [ ] Money の丸め処理をどうする?
  • [x] equals()
  • [x] hashCode()
  • [x] null との等値性比較
  • [x] 他のオブジェクトとの等値生比較

Data class, Value Object を作る手間がだいぶ省けるので地味に嬉しい機能だ。

第4章 意図を語るテスト

ここはやることに変わりなし。 Dollar(val amount: Int)Dollar(private val amount: Int) とすると amount の公開レベルを private にできる。

  • [ ] $5 + 10CHF = $10 (レートが 2:1 の場合)
  • [x] $5 * 2 = $10
  • [x] amount を private に
  • [x] Dollar の副作用をどうする?
  • [ ] Money の丸め処理をどうする?
  • [x] equals()
  • [x] hashCode()
  • [x] null との等値性比較
  • [x] 他のオブジェクトとの等値生比較

この時点でのコード:

class MoneyTest {

    @Test
    fun testMultiplication() {
        val five = Dollar(5)
        assertEquals(Dollar(10), five.times(2))
        assertEquals(Dollar(15), five.times(3))
    }

    @Test
    fun testEquality() {
        assertTrue(Dollar(5) == Dollar(5))
        assertFalse(Dollar(5) == Dollar(6))
    }
}
data class Dollar(private val amount: Int) {

    fun times(multiplier: Int) = Dollar(amount * multiplier)
}

ここまでのまとめ

テスト駆動開発』の第4章までを Kotlin で進めてみた。ここまでは基本的に Java と変わりなく、違いといえば言語間の記述方法の違いと data class くらいなので、基本的な流れが変わらない分この記事単体では面白さに欠けるものかも知れない。これはクラス単体の表現の仕方に Java と Kotlin で変わりがないことの表れだとも言える。
次回以降は複数のオブジェクトの関係や抽象化などの話が加わり、Kotlin でできる表現の方法と Java のそれとの違いが見えるようになって、環境の違いによる思考や実装の変化というこの一連の記事で示したいことを見せることができる、はず。

続き:

nashcft.hatenablog.com

*1:実際に取り組んだ時は通り過ぎて暫くしてから、またこれを書いている最中に気づいたりより Kotlin っぽい書き方を知ったりしてコードを書き直したり記事の内容に盛り込んだりということがままあったので順番通りに書こうとすると脱線が激しく記事としてまとまりがなくなるため、それらを必要な地点で記述できるように並び順を直している。そのためレポジトリのコミットログとこのまとめの内容に乖離が発生しており、大雑把にいうと GitHub のレポジトリは1周目、この記事は2周目の世界として見ていただきたい。

+メモ書き

前回のブログで余裕があるとか言ったな、あれは嘘だ。嘘だったんだ... Binder transaction buffer の記事修正は今週末にやろう...

先週も結局ヒイヒイ言いながら開発タスクをやっつけてて、今週も厳しそうな雰囲気がある。そんなこんなもあり昨日今日とバス係数についてふわふわと考えているような状態になっていて、それは色々な事があるからです。

ところで先月頭に始めた『テスト駆動開発』の写経会、最近は参加者各位の多忙により全く進められておらず、とはいえ皆の予定が合うまで延期というのではつまらなくなってしまい先週末から1人で進め始めてしまう事にした。

github.com

レポジトリ名の通り Kotlin で進めているのだが割と面白いことになっていると感じているのでElmで進めている方がやっているようにブログに過程をまとめようと思う。章の区切りも合わせようかな。第一弾は今週末までに書く。

今回ブログを書いたのは日記的なものを書くというよりもTODOを書き残しておこうという理由で、今週末までにやることを以下にまとめておく

  • Binder transaction buffer のサイズについて再調査 & 記事の加筆修正
  • Kotlin で『テスト駆動開発』をやった過程についてブログを書く (1~4章?)
  • TDD_kotlin: 14章を終わらせる
  • "Reactive Android Programming" の写経レポジトリを作成して chapter 1 まで終わらせる

オッ、なんか増えましたね...

Remove all ads

久々の日記的な

色々あり3週間弱もの間日記的なものを書かないでいた...

先々週の土曜日までは JJUG CCC の発表資料を作っていたとか、先週は業務でタスクが突然押し寄せてきたので残業しまくっていたとかそういう言い訳をすることもできるが実際それを言い訳に忙しいから落ち着いてから再開すればいいやとか考えてた。
とまあそんな感じで最近は細かいバグ取りタスクを捌くことに気を取られていたので他のことはあまりやっておらず、でも休日には怪獣惑星を観るなどはしていた。ところでバグ捌きをしているとQAチームとのコミュニケーションが増えるのだけどここもまたニャーン

ニャーン

先週で怒涛のようにバグを潰したので今週は結構落ち着いていて割と余裕がある雰囲気。今日は息抜きに社内wikiを眺めていたら若者がいつの間にか日報を始めていたのに気づいて最新の分までさらりと読んでいた。日報って1人でやってると虚無になってしまうので私も日報仲間になろうかなーと思っている。

前回の記事のことを思い出したけど、あれは後から見直して正しくなさを感じたので調べ直して修正する予定。業務の方では Intent に保存するデータ量を雑に記録して適当な上限を設けるなどしてなんとなく解決した。感覚的には端末間で binder transaction buffer のサイズにばらつきがあるという方がしっくりくるのだけど、とりあえずAndroid One S2は600KBくらい放り込むとTransactionTooLargeExceptionを吐いて死ぬがXperia系は問題なく処理する。別件でXperia系に苦しめられていてどちらかというとXperia系の方がいい加減なのではないかという気持ちになりつつある。

とりとめがないけどおしまい

Binder transaction buffer は1MBとは限らないかもしれない話

今開発しているアプリで特定の端末・条件でとある画面への遷移時に画面が真っ暗になって操作を受け付けなくなったり、該当する中で古い端末だとアプリが強制終了したりしてしまうという現象に出くわして、ログを眺めてみたら以下の例外が出続けていた:

Exception thrown launching activities in ProcessRecord{(略)}
    android.os.TransactionTooLargeException: data parcel size 645548 bytes
        at android.os.BinderProxy.transactNative(Native Method)
        at android.os.BinderProxy.transact(Binder.java:615)
        at android.app.ApplicationThreadProxy.scheduleLaunchActivity(ApplicationThreadNative.java:893)
        at com.android.server.am.ActivityStackSupervisor.realStartActivityLocked(ActivityStackSupervisor.java:1330)
        at com.android.server.am.ActivityStackSupervisor.attachApplicationLocked(ActivityStackSupervisor.java:892)
        at com.android.server.am.ActivityManagerService.attachApplicationLocked(ActivityManagerService.java:7116)
        at com.android.server.am.ActivityManagerService.attachApplication(ActivityManagerService.java:7194)
        at android.app.ActivityManagerNative.onTransact(ActivityManagerNative.java:542)
        at com.android.server.am.ActivityManagerService.onTransact(ActivityManagerService.java:3032)
        at com.android.server.am.ActivityManagerServiceEx.onTransact(ActivityManagerServiceEx.java:594)
        at android.os.Binder.execTransact(Binder.java:565)

それでリファレンスを読んだりQiitaの記事を漁ったりして、上記の例外は Binder transaction buffer に対して上限を超えたサイズのデータをやりとりしようとした時に発生するっぽいことがわかった。

読んだ記事: qiita.com

qiita.com

確かに問題の画面遷移では Intent にそれなりなサイズになるデータを1つ保持させるのでそれかなーと納得しかけたが、ちょっと変なことに気がついた。
読んだページの中で Binder transaction buffer の上限は1MBと書かれているが、上の例外には data parcel size 645548 bytes とある。つまり読み方が間違ってなければだいたい650KB弱のデータで音を上げていることになる。さっきの「それなりなサイズのデータ」以外に保持させている大きなデータは無いし、そうなると transaction buffer の上限が1MBならばこれで too large と判断されるのはおかしいはず。

それで気になってググってみたら StackOverFlow の以下の記事がすぐに引っかかった:

stackoverflow.com

The limit is supposed to be 1MB but it varies by device from little less than 512KB up to almost a full 1MB.

質問は2013年、上で一部引用したベストアンサーの回答も2015年とやや古いが、端末によって transaction buffer のサイズが異なるとある。それで "binder transaction buffer size vary" とかでさらにググってみたら他にもいくらか引っかかる様子。

https://www.neotechsoftware.com/blog/android-intent-size-limit

リファレンスはまるで一律1MBだと定めているように読めるのでなんだかなーという気持ちになった。リファレンスの記述は以下の通り:

The Binder transaction buffer has a limited fixed size, currently 1Mb, which is shared by all transactions in progress for the process.

さてどうやって対応したものかね...