PEEE802.11

モバイルソフトウェアエンジニアの備忘録

WebView.saveState()のJavaDocの解釈

WebView.saveState()は、WebViewの状態をActivity破棄前に保存するためのメソッドである。が、どうもJavaDocがもやもやする。

WebView  |  Android Developers

Saves the state of this WebView used in Activity.onSaveInstanceState(Bundle).

これはわかる。

Please note that this method no longer stores the display data for this WebView.

the display dataってなに。唐突にtheとか言われても。それに、no longerって前はthe display dataとやらを保存してたのか。

The previous behavior could potentially leak files if restoreState(Bundle) was never called.

The previous behaviorってなに。ここでも唐突にtheとか言われる。しかもleakするfilesってなに。

対となるrestoreState()を見ると、

WebView  |  Android Developers

Restores the state of this WebView from the given Bundle. This method is intended for use in Activity.onRestoreInstanceState(Bundle) and should be called to restore the state of this WebView.

わかる。

If it is called after this WebView has had a chance to build state (load pages, create a back/forward list, etc.) there may be undesirable side-effects.

要は、保存してた状態から別の状態をbuildした後に復元しようとすると思わぬ副作用が発生する、ということだろうか。まあわかる。

Please note that this method no longer restores the display data for this WebView.

またもやthe display dataとno longer。わからん。

このthe display dataとやらは、WebViewの他のメソッド含む上記ページJavaDoc内では他に一切出てこない。それとも自分が無知なだけで、WebViewというかブラウザの文脈でthe display dataと言えばみんな知っているものなのだろうか。

なんだか悔しいのでソースコードを見てみると、WebView.savePicture()restorePicture()にthe display dataへの言及がある。これらのメソッドは@deprecatedかつ@hideなので公式JavaDocには含まれていない。

core/java/android/webkit/WebView.java - platform/frameworks/base - Git at Google

    /**
     * Saves the current display data to the Bundle given. Used in conjunction
     * with {@link #saveState}.
     * @param b a Bundle to store the display data
     * @param dest the file to store the serialized picture data. Will be
     *             overwritten with this WebView's picture data.
     * @return {@code true} if the picture was successfully saved
     * @deprecated This method is now obsolete.
     * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1}
     */
    @Deprecated
    @UnsupportedAppUsage
    public boolean savePicture(Bundle b, final File dest) {
        checkThread();
        return mProvider.savePicture(b, dest);
    }

    /**
     * Restores the display data that was saved in {@link #savePicture}. Used in
     * conjunction with {@link #restoreState}. Note that this will not work if
     * this WebView is hardware accelerated.
     *
     * @param b a Bundle containing the saved display data
     * @param src the file where the picture data was stored
     * @return {@code true} if the picture was successfully restored
     * @deprecated This method is now obsolete.
     * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1}
     */
    @Deprecated
    @UnsupportedAppUsage
    public boolean restorePicture(Bundle b, File src) {
        checkThread();
        return mProvider.restorePicture(b, src);
    }

それぞれBundleFileをパラメータに取り、saveState()restoreState()と一緒に使われて、Bundleにはthe display dataを、Fileにはシリアライズした画像を保存・復元するらしい。the display dataの詳細は書いていないが、具体的な処理はmProviderWebViewProviderインターフェース)に委譲されているので、それぞれのWebView提供具象クラス内で処理されるそれぞれのthe display dataがあるのだろう。また、saveState()でleakすると言っていたfilesはおそらくこの画像ファイルのことだろう。Activityが破棄される前にFileとしてシリアライズした画像を保存して、そのままアプリ自体が終了してしまった場合、復元の機会がなくFileはストレージに残ったままになる。no longer save/restore the display dataと言っているのは、savePicture()/restorePicture()がdeprecatedになる前と比べているのだろう。

以下のコミットが@hideJavaDocに追加したコミットで、ついでにsaveState()/restoreState()のコメントも更新されているのだが、必要最低限中途半端に編集したためメソッドの説明としてこのようなわかりにくい文章になってしまった、というように見える。

Diff - f4912580e6adc90ab37b07b8108c7334f359e317^! - platform/frameworks/base - Git at Google

ドキュメンテーションは大事。

ちなみに、WebViewProviderインターフェースの実装の一つであるchromiumのWebViewのソースコードを追っていくと、AwContents.saveState()でネイティブコードからバイト配列を取得してWEBVIEW_CHROMIUM_STATEというキーでBundleに保存している。savePicture()/restorePicture()はdeprecatedのためfalseを返すのみ。

android_webview/java/src/org/chromium/android_webview/AwContents.java - chromium/src - Git at Google

ネイティブの方は読むのが辛くなってきたので気が向いたら。

Android Studio 3.4/Gradle Plugin 5.1.1にアップデートしたらRobolectricのテストが失敗する

TL;DR

Robolectric 3.x(AndroidXにアップデート前のバージョン)を使っている状態でAndroid Studio 3.4にアップデートすると、Android Gradle Plugin 5.1.1+が強制されて、gradle.propertiesにandroid.enableUnitTestBinaryResources=falseを書かないとRobolectricを利用しているテストが初期化で失敗します。

問題発生条件

Robolectric 4.0+を使っている方はおそらく問題ないはず。

経緯

Robolectricは4.0+でAndroidXに対応している。が、Robolectric 4.0未満 + 従来のテストサポートライブラリでのテストコードがそれなりの量存在する場合、まだ移行できていないプロジェクトも多いのではないかと思う。 そんな状態でAndroid Studio 3.4にアップデートして、例えば以下のようなテストコードを実行すると、

@RunWith(RobolectricTestRunner::class)
@Config(
    constants = BuildConfig::class,
    sdk = [(Build.VERSION_CODES.LOLLIPOP)]
)
class ExampleUnitTest {
    @Test
    fun addition_isCorrect() {
        assertEquals(4, 2 + 2)
    }
}

テストランナー初期化時に NullPointerExceptionが発生してしまう。

java.lang.NullPointerException at org.robolectric.internal.DefaultManifestFactory.getFsFileFromPath(DefaultManifestFactory.java:65) at org.robolectric.internal.DefaultManifestFactory.identify(DefaultManifestFactory.java:24) at org.robolectric.RobolectricTestRunner.getAppManifest(RobolectricTestRunner.java:431) at org.robolectric.RobolectricTestRunner.getChildren(RobolectricTestRunner.java:255) at org.junit.runners.ParentRunner.getFilteredChildren(ParentRunner.java:426) at org.junit.runners.ParentRunner.getDescription(ParentRunner.java:351) at com.intellij.junit4.JUnit4IdeaTestRunner.getDescription(JUnit4IdeaTestRunner.java:78) at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:50) at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47) at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242) at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)

例外が発生しているのはテストコードそのものではない。どうもマニフェストファイルを開こうとして、マニフェストファイルのパスの設定値自体が存在しないということが起こっている様子。 そんなこと言われても、もともとマニフェストのパスなんて自分ではどこにも指定していないのだが…

で、stackoverflowとか色々調べてみたが、@Configconstantsはdeprecatedだの、android { testOptions { unitTests { includeAndroidResources = true } } }が必要だの(設定済だった)、4.0ではandroid.enableUnitTestBinaryResources=trueが必要だの、なんとなく関係ありそうでなさそうな情報しか引っかからない。その中で気になったのが、android.enableUnitTestBinaryResourcesである。公式ページにはRobolectric 4.0+をAndroid Studio 3.3未満で使う場合はgradle.propertiesandroid.enableUnitTestBinaryResources=trueを書けよ!と書いてある。

robolectric.org

Add this line to your gradle.properties (no longer necessary with Android Studio 3.3+):

android.enableUnitTestBinaryResources=true

Android Studioでプロジェクトを作った時に生成されるgradle.propertiesにこのフラグは存在しないが、試しにこのフラグを明示的にfalseにする行を追加してみると、テストが通るようになった。

以下のパスにテスト用のコンフィグが生成されているのだが、このフラグのtrue/falseでコンフィグが変わることを確認した。何も設定しなければデフォルトでtrueの設定値が生成された。

your/module/path/build/intermediates/unit_test_config_directory/debugUnitTest/generateDebugUnitTestConfig/out/com/android/tools/test_config.properties

android.enableUnitTestBinaryResources=false

## Generated by the Android Gradle Plugin
#Wed May 01 19:01:33 JST 2019
android_sdk_home=~/Library/Android/sdk/platforms/android-28
android_custom_package=com.example.your.app.id
android_merged_assets=your/module/path/build/intermediates/merged_assets/debug/out
android_merged_manifest=your/module/path/build/intermediates/merged_manifests/debug/AndroidManifest.xml
android_merged_resources=your/module/path/build/intermediates/merged-not-compiled-resources/debug

android.enableUnitTestBinaryResources=true or 設定を書かない

## Generated by the Android Gradle Plugin
#Wed May 01 18:46:52 JST 2019
android_sdk_home=~/Library/Android/sdk/platforms/android-28
android_custom_package=com.example.your.app.id
android_merged_assets=your/module/path/build/intermediates/merged_assets/debug/out
android_merged_manifest=your/module/path/build/intermediates/merged_manifests/debug/AndroidManifest.xml
android_resource_apk=your/module/path/build/intermediates/apk_for_local_test/debugUnitTest/packageDebugUnitTestForUnitTest/apk-for-local-test.ap_

最後の行だけが違う。フラグの名前の通り、APKにパッケージされる前の中間生成物であるmerged resourceを利用するか、APKをまるっと利用するか(binary resources)を切り替えるようだ。

色々な現場で割とぶち当たりそうな問題に思えるのだが、あまりみんな騒いでないので、みんなAndroid Studio 3.4にまだ上げてないのか、Robolectric使ってないのか、すでにAndroidXに移行済みなのか…

Androidアプリでstaticフィールドは絶対ではない

Androidアプリで、Contextをどこからでも参照する方法として以下のようなコードがよく紹介されている。

  • staticでApplication Contextを保持するクラス
public class ContextHolder {
    private static Context mContext;

    public static void setContext(Context context) {
        mContext = context;
    }

    public static Context getContext() {
        return mContext;
    }
}
  • アプリのプロセス起動時に実行されるApplicationクラスのonCreate()でContextをセット
public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        ContextHolder.setContext(this);
    }
}

あとはContextHolder.getContext()で任意のクラスからContextを取得できる、というわけだ。

しかし、実はこのコードは問題を孕んでいる。

Android StudioでContextをstaticフィールドに突っ込むと「メモリリークするよ!」と警告が出るが、もっと深刻な問題は知らぬ間にnullになることである。

ご存知の通り、Androidはメモリが逼迫するとバックグラウンドにあるActivityやらServiceやらを殺していく。殺されるのはそういったAndroid特有のオブジェクトだけではなく、アプリのライフサイクルとは無縁の上記ContextHolderのようなオブジェクトも例外では無い…ように感じる。というのも、実際に上記のようにstaticで参照を保持しているフィールドにアクセスするアプリで、ぬるぽでクラッシュしたレポートがコンスタントに上がってきているので。クラスがUnloadされると、次回必要になった際に再Loadされる。その時に初期値を設定していないstaticフィールドはnullで初期化される。そこで本来はApplicationクラスのonCreate()でContextを設定したいところが、プロセスまでは殺されていないためにApplicationクラスのonCreate()が再度呼ばれず、staticフィールドはnullのままになってしまうのである。

以下にちらっと書いてあるが、アプリをロードしたClassLoaderごとGCされることがあると示唆されている。

developer.android.com

The class references, field IDs, and method IDs are guaranteed valid until the class is unloaded. Classes are only unloaded if all classes associated with a ClassLoader can be garbage collected, which is rare but will not be impossible in Android.

別の例として、以下のようにApplicationクラス内にstaticで持つコードも見かける。さすがにApplicationクラスはUnloadされないと思うし、されたとしても再Load時にonCreate()が呼ばれるはずなので、この場合はおそらく問題ないと思う。

public class MyApplication extends Application {
    private static Context mContext;

    @Override
    public void onCreate() {
        super.onCreate();
        mContext = this;
    }

    public static Context getContext() {
        return mContext;
    }
}

ClassLoaderごとUnloadされる状況をあえて作り出すのは容易ではなさそうなので、実際に動作確認したわけではなくあくまで予測ですが…

IPv6をIPoEで使う + IPv4をDS-Liteで使う

自宅のブロードバンド環境は、auとの契約の関係もあり長らくJCOMであった。東京在住で、遅い時で2Mbpsほど、速い時で15Mbpsほど出ていたのでそこまで不満に感じていなかった。しかし、そろそろ契約更新月なのと、Wi-Fiルータを新調して11acにしようか考えていたところで、一新してしまうことにした。

さて、他の多くのISPもそうだが、So-netも例に漏れず回線が混雑する夜間は1Mbpsほどしか速度が出ない。空いてる時間帯でも20Mbpsすらなかなかいかない。この原因はウェブ上各所で説明されているが、IPv4 PPPoE認証がボトルネックになっているためである。以下のページが詳しい。

techlog.iij.ad.jp

VDSL環境だと、IPoEを使うと理論上は電話線の速度上限である100Mbpsまでは出るはずである。 早速、混んでる時間帯のPPPoEでの速度を覚えておき、So-netに連絡してIPoEを使いたい旨申し出る。 So-netとしては大々的に宣伝しているオプションではないし、IPoEに変えて改善するかはわからないため(それ以外の原因で遅いユーザもいるらしい)、現在の速度と状況について簡単にヒアリングされる。言うだけで機械的に処理してくれるわけではない。

IPoEが解放されたら、ONUWi-Fiルータを再起動する。Wi-FiルータはAUTOモードに設定して、インターネット@スタートが設定されていればただ再起動するだけでよい。ただし、IPv6パススルーはOFFにする(デフォルトOFF)。ここで、PPPoE接続の設定を無効化すると(Internet -> PPPoE -> PPPoE接続先リスト に有効な接続先があれば、「接続先の編集」ボタンを押して無効にするか削除する)、DS-Lite (IPv4 over IPv6)が設定される。

再起動後以下のようにルータのステータスが表示されればOK。

  • 動作モード
    • ルータモード
  • Internet
  • IPv6
    • IPv6接続方法: インターネット@スタートを行う
    • IPv6接続状態: NDプロキシ
    • グローバルアドレス: (IPv6アドレスが表示)

以下のサイトで、IPv6の接続性があればIPv6 IPoEで繋がっている。ISPSo-netではなくINTERNET MULTIFEEDと表示されれば、DS-Liteが有効になっている。

test-ipv6.com

IPv4 DS-Liteでの速度測定結果。VDSLのポテンシャルを限界近くまで引き出している。

www.speedtest.net

IPoEはともかく、DS-LiteはSo-netが案内していないオプションであり、現状はルータの設定次第で勝手に繋がってしまった、というものである。いつ使えなくなるかわからないのでご利用は自己責任で。

転職して1ヶ月経ったので振り返ってみる

基本的に前職同様ソフトウェア開発なので大きく戸惑うことはないのだが、ところどころ違う点が見えてきた。

プロジェクトの進め方

前職だとリーダーがスケジュールを決めて、各マイルストーンまでにやるべきことを洗い出し、時間かけて準備することや不確定要素が多くリスクが見えにくいタスクから先に取り組んで進捗を定期的に確認したり、そういった作業がプロジェクトの初期にあって、それらがミーティングを通じてテキパキと決定されていった。現職は、プロジェクト規模や期間が相対的に小さいこともあり、リーダーやPMがテキパキ決めるというよりはみんなで話し合いながら柔軟に決めていくという感じ。ミーティングもややゆるい感じで、前職でははっきりしたミーティングの目的がなければ時間節約のためにキャンセルとか、議論が停滞したら宿題にしてサクサク先に進めてその後メールで議論を進めるとかしていたが、それもない。そもそもミーティングの数が少なかったり、時間が切迫しているわけでもないというのが大きいのだろうか。

コミュニケーション

Slackでチーム内でゆるい感じのコミュニケーション。前職はプロジェクト関連の状況報告や運営方針など諸々のメールが一日100通は来ていたが、今は一日10通来るかどうか。その分開発に集中できて最高である。

チーム体制

前職はアーキテクト的な人だけがコードを承認できたが、今はみんな対等な立場でレビューし合う感じ。本を読んで勉強している人が多いし、チーム全員がちゃんとコードが書けるエンジニアという印象を受けている。

休みを気軽に取れる雰囲気で、土日に繋げてそれなりの連休も問題ない感じ。若干のドライさは感じるものの、少なくとも心理的安全性は確保されていそう。

あまり変わらないところ

服装や勤務制度の自由さ、役職を振りかざす人がいないことなど。

結論

現状満足。

前職の感想

よかった点

  • 旧態依然とした日本企業の悪い風習や文化がなく自由な雰囲気であるため居心地は良い
    • 服装は自由。社内ではみんな「さん」付けで呼び、役職を振りかざす人はいない。ウェブ上でハンコリレーだの冗長な会議だの飲み会強制参加やアルハラパワハラだの劣悪な労働環境の内情が散見されるが、都市伝説なのでは?と思うほど無縁だった。また、退職にあたって今まで一緒に仕事したたくさんの人に挨拶に行った時、誰もが温かい言葉をかけてくれたのには本当に救われた。
  • 英語を業務で使う機会が多く良い経験になる
    • "Janglish"だからとバカにされることはない。むしろ何も話さない方が問題視される。いろいろな国の英語に触れられるので(むしろネイティヴがほぼいない)、発音はネイティヴ並みじゃなくても通じるし聞き手も理解しようという雰囲気が良い。
  • コンシューマ製品を作っていたためダイレクトにユーザや世間の反応がわかる
    • 自分が開発した製品を街で見かけたりネットで評判になったりする。良くも悪くも。

よくなかった点

  • 協力会社に開発を委託することが多い
    • 個人の志向の問題とも思うが、社員は他チームとの連絡役とタスクとリソースの管理に時間をとられる。もっと開発現場で働きたい自分にはちょっと辛かった。
  • リソースとタスク量のアンバランス
    • タスクは増えどもリソースは増えず。効率的に業務を回す工夫は現場に委ねられるも現状の業務を回すので精一杯。
    • 開発スケジュールはバッファが少なく、リソースもバッファがないため、トラブルは残業でカバー。トラブルはそれなりの規模のプロジェクトには付き物なので、残業前提でプロジェクトを進めていることになる。
  • 分業化によってコミュニケーションコストが高い
    • 誰に聞いていいかわからないことがよくあり、隙間領域のバグ票などは押し付け合いになりがち。
    • 分業化に伴って階層化が進み、中間層は仕事やメッセージのバケツリレーをするだけになる。

こうやって書き出してみると、人月ビジネスとその管理業務が自分の方向性に合わないことと、 人材をコストとみなしてコスト削減の号令を聞き続けるのに疲れたのと、 常に負荷オーバーで楽になる未来が見えなかったいうところだろうか。 技術的バックグラウンドは必要とは言え、開発する余裕がなく調整作業ばかりの毎日で、日に日に違和感ばかりを溜め込んでいた。 もちろん目的やビジネス的成功なき開発は会社では意味をなさないし、できるだけ固定費を削減して利鞘を確保しようというのは正しい。 しかしそれを達成しようとした結果、従業員のモチベーションや心身の健康といった数値化しにくいものを下げてしまうリスクは認識されるべきだと思う。

転職活動をするにあたってコーディング面接対策をしていて、改めて自分の技量の未熟さを痛感したけど、 それでもやはりエンジニアとして調整だけではなく開発にもっともっと時間を割きたいという思いは変わらない。

晴れて転職した先の職場はチームみんながちゃんとコーディングしてるエンジニアという様相で、組織がよりフラットで、以前よりも開発に時間を割けそうである。 初めての技術領域なので慣れないことも多いが、指導を受けながら勉強して一刻も早くチームに貢献できるようになりたい。

USBバルク転送の挙動(ZLP: Zero Length Packet)

USB機器のデバイスドライバ書いてる人なら当たり前の話だとは思うんだけど、専門外の自分が調べる機会があったのでメモ。

USBの転送モードはいくつかあって、そのうちの一つにバルク転送というものがある。大容量のデータをやりとりするのに適した転送モードで、記憶装置とのデータのやりとりに使われたりする。このバルク転送を使ってPC(ホスト側)から携帯端末(デバイス側)にデータを送っていたのだけど、あるサイズのデータを送ると、デバイスが応答を停止してしまう現象が起きた。あるサイズというのは、512バイト、1024バイト、1536バイト、…つまり512バイトの倍数である。

最初は、受け取ったデータを処理をするためにデバイス側に追加した自分のコードが悪さしてるのでは?と思ったのだが、いろいろ検索してみるとどうも違うらしい。

まず、USB2.0 High Speedバルク転送のデバイスのエンドポイントのバッファサイズは512バイトである。そして、USB 2.0 の仕様の5.8.3でバルク転送について以下のように書かれている。

endpoint must always transmit data payloads with a data field less than or equal to the endpoint’s reported wMaxPacketSize value. When a bulk IRP involves more data than can fit in one maximum-sized data payload, all data payloads are required to be maximum size except for the last data payload, which will contain the remaining data. A bulk transfer is complete when the endpoint does one of the following:

  • Has transferred exactly the amount of data expected.
  • Transfers a packet with a payload size less than wMaxPacketSize or transfers a zero-length packet

When a bulk transfer is complete, the Host Controller retires the current IRP and advances to the next IRP.

つまり、バッファサイズより大きなデータを送る際はバッファサイズいっぱいまで詰めるだけ詰めて分割して送り、期待するデータ量を送り終わったら、もしくはバッファサイズより小さいパケットが来たら転送が終了する、ということらしい。

ここで曲者なのが期待するデータ量をどうやって知るかということ。USBバルク転送の仕様では、これから送るデータ量をやりとりする方法は記述されていない。ネット上を調べた感じ、どうもデバイスやドライバの実装依存のようなのだ。自分が調べていたドライバやデバイスには、特にそういう実装がないように見える。つまり、送る/受け取るデータ量を事前に知ることができない。ということで、データ転送を終了する条件はただ一つ、バッファサイズより小さいパケットが来ること、になる。自分が直面した問題は、バッファサイズ = 512バイトと同じパケットを送ると、デバイスが転送の途中だと判断し、いつまでも次のパケットを待っている状態になっていた、ということだった。

この問題を解決するための一般的な方法が、上記引用にも書いてあるzero-length packet (ZLP)である。ホスト側が、全部のデータを送り終わり、かつ送ったデータがデバイス側のバッファサイズの倍数だったら、データペイロードが空のパケットを送り、デバイスに転送が終了したことを知らせる。この実装だと、デバイス側がどれだけデータが送られてくるかを知らなくてもよい。

自分が担当しているのはデバイス側の実装だけなので、ホスト側のドライバを書いているチームに実装してもらわなければならない… デバイス側はAndroid Linuxのコードを流用してるから、そのままシンプルにキープしておきたいなあ。

ちょっと古くてUSB2.0までしか載ってないけど、以下の書籍で調べた。訳書だけど自然な日本語でわかりやすかった。

USB 2.0とUSB On-The-Goを含むカスタムUSBデバイス開発のすべて USBコンプリート[第3版]

USB 2.0とUSB On-The-Goを含むカスタムUSBデバイス開発のすべて USBコンプリート[第3版]