「ThreeTenABP」で捨てれないのか android.text.format.DateUtils

Androidで日付表記をお手軽に国際化する « LINE Engineers' Blog

読んでみると, なんかいろいろややこしそう.

イギリス式: Fri, 18 Nov 2016
アメリカ式: Fri, Nov 18, 2016

イギリス英語: Fri, 18 Nov 2016
アメリカ英語: Fri, Nov 18, 2016
日本語: 2016年11月18日(金)

イギリス英語: 18/11/2016
アメリカ英語: 11/18/2016
日本語: 2016/11/18

Android 6.0: 2016年11月18日(金)
Android 4.1: 2016/11/18 (金)

Android 6.0: 2016年11月18日金曜日
Android 4.1: 2016年11月18日 (金)

日本語: 3 日前
英語: 3 days ago
フランス語: Il y a 3 jours

日本語: 11月18日
英語: on Nov 18
フランス語: le 18 nov.

挙動にぶれがあります(エミュレータにて動作確認)。小さいTextViewに表示する際にはOS versionによって見切れてしまっていないかなどの検証をしましょう。

OS version間での挙動差分や、ビジネス上アプリで必要な言語をAPIが対応してくれるかの検証は必要

Android 7.0でのICU4Jの導入と共に下記クラスがICU4JでのDateUtilsに対応するクラスとして案内されています。

うーむ...「日付表示」て, 書いときゃ世界のどこでもまあまあいけるやつはねえのかなあ, と.

ThreeTenABP で DateTimeFormatter を使う

「ThreeTenABP」て何なのかは, 作者であるぼくらの Jake Wharton神 の書いていることを読めば分かります.

JakeWharton/ThreeTenABP: An adaptation of the JSR-310 backport for Android.

なぜ JSR-310 なのか?

JSR-310 は, java.time.* パッケージとして Java8 に含まれています. これは, Java と Android の両方で Date/Calendar API群をすべて置き換えるものです.
Java6 の開発者であるStepahen Colebourne によって調整され, それにバックポートされました.

なぜ ThreeTenBP を使わないのか?

Android 上で Joda-Time を使う問題のように ThreeTenBP はタイムゾーン情報の読込みにjar リソースを利用しています. これが Android 上では非常に非効率です.
このライブラリでは, タイムゾーン情報を Android標準で使えるようして, 効率的にパースできるローダーを提供しています.

なぜ Joda-Time を使わないのか?

Joda-Time は非常に大きいバイナリサイズと多くのメソッド数をもつ非常に大きいAPIです. また, JSR-310 と Joda-Time の作者は「壊れてはいないが, 設計に欠陥がある」と言っています.
すでに Joda-Time を使っているなら, サイズとメソッド数を除けば, 置き換える理由はほとんどありません. 新しいプロジェクトのために, このライブラリはバイナリサイズとメソッド数だけでなくAPIの数をかなり削減したJava8の標準APIの形 で提供しています.

Joda Time's Memory Issue in Android

Date/Calendar API + JodaTime の機能をJava8 で合成した java.time.* (JSR-310) を 現Android開発向けに適正化し利用できるようにしたものが「TheeTenABP」ということになります.

コードの記述は Java8 の記述と同じなので Java8 Date and Time APIドキュメントや記事が参考にできます.

続・今日から始めるJava8 - JSR-310 Date and Time API - Taste of Tech Topics

13 章 : Date and Time API · Java Study

Java 8 の DateTimeFormatter の曜日等のフォーマットについて - tokuhirom's blog

Javaで日時を扱う(Java8) - Qiita

同梱されている国際化可能そうな日時フォーマットを試してみる.

...
DateTimeFormatter formatter =
    DateTimeFormatter
        .ofLocalizedDateTime(formatStyle, formatStyle)
        .withLocale(Locale.getDefault());
Timber.d(zonedDateTime.format(formatter) + " : " + formatStyle.name());
...

日付と時刻, それぞれ FormatStyle.SHORTからFULLまでの4種類ある.


2016年12月14日水曜日 10時13分36秒 日本標準時 : FULL
2016年12月14日 10:13:36 JST : LONG
2016/12/14 10:13:36 : MEDIUM
2016/12/14 10:13 : SHORT

Wednesday, 14 December 2016 01:13:36 Greenwich Mean Time : FULL
14 December 2016 01:13:36 GMT+00:00 : LONG
14 Dec 2016 01:13:36 : MEDIUM
14/12/2016 01:13 : SHORT

Tuesday, December 13, 2016 8:13:36 PM Eastern Standard Time : FULL
December 13, 2016 8:13:36 PM EST : LONG
Dec 13, 2016 20:13:36 : MEDIUM
12/13/16 20:13 : SHORT

2016年12月14日星期三 香港標準時間上午9時13分36秒 : FULL
2016年12月14日 GMT+08:00上午9時13分36秒 : LONG
2016年12月14日 09:13:36 : MEDIUM
2016/12/14 09:13 : SHORT

2016년 12월 14일 수요일 오전 10시 13분 36초 대한민국 표준시 : FULL
2016년 12월 14일 오전 10시 13분 36초 GMT+09:00 : LONG
2016. 12. 14. 10시 13분 36초 : MEDIUM
16. 12. 14. 10:13 : SHORT

なんか微妙...

FormatStyle.MEDIUM くらいが良さげだが, 曜日が含まれてないし.

別個, 曜日の文字列としては別に


localDateTime.getDayOfWeek()
    .getDisplayName(TextStyle.FULL, Locale.getDefault());

のようにして文字列として取れるっちゃあとれるが並び順やセパレータの件がある.

カスタムで言語別にフォーマットつくるのもなあ.

まとめ

android.text.format.DateUtils で.


DateUtils.formatDateTime(
       context,
       timestamp, 
       FORMAT_SHOW_YEAR | FORMAT_SHOW_DATE | FORMAT_SHOW_WEEKDAY | FORMAT_SHOW_TIME | FORMAT_ABBREV_ALL);


2016年12月14日(水) 10:13
Wed, 14 Dec 2016 00:13
Tue, Dec 13, 2016, 20:13
2016年12月14日週三 09:13
2016년 12월 14일 (수) 10:13

結局, これが簡単で自然,

表示言語やタイムゾーンの設定は context から取得してくれるし.

モバイル画面上の表示としてその他海外で自然かどうかは実際知らんけども.

日時を多言語化するというのは思ったより複雑.

モバイル端末というのは, いろいろな制限の中で動いているのだなあと実感.

Date/Calendar のことはもう忘れていいのだよな?

(つづく)


どうやってアプリ起動時間を6秒から1.6秒に短縮したか

どこの開発チームでもやっているように, アプリの起動と読込み時間を改善することは わしら Android開発チームでもひとつの優先事項でした.

デバイスメーカーはさらに速く流れるような操作を提供し続けており, ユーザーは一層のスピード向上を期待しています.

最近, 新アプリを「Dependency Injection」や 「Reactive Programming」のようなモダンなパターンを利用して書き直しました. これによって メンテナンス性が向上し, よりモダン & モジュール化 されましたが, いくつかの調整が必要となりました.

リリース時, 起動時間は Nexus5 上で 5, 6 秒かかっていました. これはわしらの目標である「2秒以下」を超えていました. それでパフォーマンス改善を行うことになりました.

主な低速化の原因が 「リフレクション」であることが分かりました. これとその他の細かいことを改善することで, 起動時間を 1.6 秒に短縮することができました.

どのようなことを行ったか

まず, アプリの起動時間を Android のトレース機能を使って起動時間の計測です. Application クラスのコンストラクタから画面に表示されるプログレスインジケータの終了までを計測しました.

traceview_profile

Profiling with Traceview and dmtracedump | Android Developers

最大のパフォーマンス低下の原因を見つけるために出力された分析用トレースファイルたちを集め DDMS に読み込ませていましたが, 最終的には 簡単な方法でボトルネックを特定できトレースしながらパフォーマンスを比較できる「NimbleDroid」に切り替えました.

Home_-_NimbleDroid___Automated__Comprehensive_Performance_Analysis_for_Every_Build_of_Your_App

Home - NimbleDroid | Automated, Comprehensive Performance Analysis for Every Build of Your App

簡単な改善

最初に見つけた主な速度低下の原因は, 多量のクラス, メモリを大量に消費する jar リソースの読み込み JodaTime (特定済みの問題有り) でした.

Joda Time's Memory Issue in Android

Groovy のクロージャを使っていましたが 他にこれを使っていなかったので, 取り除くことにしました. 昔ながらの Java7 の構文に戻って. Groovy で作成されたコードを取り除きました. 他の選択肢を探していますが Java の 匿名クラスを見るための IDE補助機能は優先ではありません.

次に, RxJava が1秒程度 遅延させていることを見つけました. これについては, 次のリリースでの修正されます. ありがとうございます.

NewThreadWorker.tryEnableCancelPolicy doing costly reflection on Android · Issue #3119 · ReactiveX/RxJava

次に, アプリに組み込まれているいくつかの サードパーティの 統計クライアントが アプリの起動時にブロッキングしているコールがありました. どのようにインスタンス化され動作しているか ベンダーと協力をいただきながら起動時の機能改善を行いました.

また, 我々のコード自体にも技術的問題を見つけました.
md5の計算処理や 一般的な多すぎる処理で起動オブジェクトをブロックをしていました.

これらを改善したところで, 起動時間は以前の半分の 3.2 秒に短縮できました.

次は データフローの最適化です. これは, わしらのアプリのような高データ集約型のアプリには特に重要な意味を持ちます.

データストア処理

データ処理の全てが非同期であるため、我々は最初のプレゼンテーションとデータ層との間の抽象化として機能するように、単一のコンテンツマネージャとしていました。これによりカプセル化することはできましたが、徐々にデータが永続ストレージから必要とされる場合に必ずゴッドクラスをインスタンス化することになる、というスローダウンを引き起こしていました。

God object - Wikipedia, the free encyclopedia

この場合、Analytics の設定値は、アプリの起動時にディスクからの取得を必要としているのですが, SSL対応ネットワーククライアントのインスタンス化をを待たなければなりませんでした.

アプリに機能追加されるにつれ, さらに依存関係が加わり, コンテントマネージャーの起動時間は増加し, 初期インストール時と同じくらい時間がかかっていました.

単一のコンテントマネージャーとするよりも, 独立したデータストアとする方針にすることで, 素早くキャッシュされたデータを読み込むことが可能となりました.

これは, ディスクからUIへのデータの処理フローを最適化する FacebookのAndroidのチームによって行われた最近の研究と似ています.

Improving Facebook's performance on Android with FlatBuffers | Engineering Blog | Facebook Code

まず, 最初は, ディスクとネットワークDAOを利用して個々のシングルトンのデータ・ストアとしているコンテンツマネージャを壊すことにしました。こんなかんじで、コンテンツマネージャから ConfigNetworkDAO と ConfigDiskDAO を利用して ConfigStore を立ち上げていました.


@Singleton
public class ConfigStore extends Store {
   @Inject
   public ConfigStore(AppConfigParser parser,
                      final ConfigDiskDAO loader,
                      final Lazy<ConfigNetworkDAO> fetcher) {
       super(loader,fetcher,parser);
}

重要なオフライン時または最初のロード後に - 時間のかかるネットワーククライアントを利用し、私たちが実際にネットワークの操作を行うまでそれをインスタンス化しないために許可されている Dagger の時間のかかるインスタンス化を改善することにしました。このアプリでは, バックグラウンドサービスを使用してデータをダウンロードすることがメインで ここで処理するデータはほとんどなく、ネットワークを利用した通信よりもディスクストレージからUIにロードされます. 結果, UIに ディスクからのデータ処理経路を最適化することができ、大きくスピードを取り戻すことができました。

リフレクションの削除

トップストーリーのデータをパースする際に 永続ストレージとネットワークのどちらから取得しているかに関係なく 700ミリ秒以上かかっていることを発見しました.
このアプリのような大量のデータドリブンなアプリでのAndroid 標準の Gson 処理パフォーマンスがどれだけ貧弱であるかを知った時驚きました. 起動時のトレースにより「リフレクションタイプアダプター」のコールが原因であることが分かりました.

NYTimes - NimbleDroid | Automated, Comprehensive Performance Analysis for Every Build of Your App

Gsonからのリフレクションコールを最小限にしようとしましたが, 実行可能な方法は 苦労して独自でタイプアダプタを書くことだけでした. リフレクションを使わずかつ起動時間をかけずにシリアライズする技術をみつけることは長く無駄でした. モデルにコードを追加することしか選択肢が残っていなかったので, 簡単な「Gson with custon type adapter」に戻ることにしました.

TypeAdapter (Gson 2.3.1 API)

タイプアダプターを書くことで10倍のパースパフォーマンスの改善をすることができました.

開発者の負荷を最小限にするために以下の「Immutable library」を利用することにしました.

JSON serialization

これはコンパイル時にデータモデル用のカスタムタイプアダプターを提供してくれると同時に AutoValue 同様の不変性も提供してくれます.


@Value.Immutable
@Gson.TypeAdapters
public abstract class AppConfig {
  public abstract Optional<DeviceGroups> deviceGroups();
  public abstract Marketing marketing();
  public abstract List<OverrideCondition> overrides();
  public abstract LinkedHashMap<String, List<Integer>> imageCropMappings();
}

public AppConfigParser() {
  gson = new GsonBuilder()
    .registerTypeAdapterFactory(new GsonAdaptersAppConfig()) //auto generated adapter
    .create();
}

public void parse() {
  reader = new InputStreamReader(source.inputStream(), UTF_8);
  AppConfig appConfig = gson.fromJson(reader, AppConfig.class);
}

現在では, データフローは以下のようになっています:

バックグランドサービスが 起動時間のかかるネットワーククライアントである RxStore をコールして新しいデータをJSONでダウンロードします. これは, スケージュル, またはプッシュアラートから実行されます. その後, ディスクにJSONデータを流しこみます. 単一のオブジェクトとしてJSONを保存するのではなく, ストリーミングAPIを使います. なぜなら メモリー上での1M以上のインスタンス化を避けることができるからです.

UIがデータを必要とすれば、メモリ内のキャッシュ(guava)とディスクにバックアップされたデータストアから不変のデータを取得します。

CachesExplained - guava-libraries - Explanation for how to use Guava caches. - Guava: Google Core Libraries for Java 1.6+ - Google Project Hosting

ディスクにデータが存在しないとき, または最終保存したデータのフォーマットが変更したときのみネットワーククライアントを起動します. データは永続ストレージからUIに一方向で流れ, ネットワークから UI には決して流れません.
この一方向のデータフローは、データストアへの要求の99パーセントは、ディスク以外にアクセスする必要がないことを意味します。

私たちは、このようなディスクストレージのためのデータをシリアライズするFlatBuffersのような他の方法を模索していきますが、現在の結果に満足しています。リニューアル後は、ユーザーに 2秒未満でページの完全なビューを提供しています。

結論

Androidでは, リフレクションは 重大なパフォーマンス問題を引き起こすことがある (とくに大きいデータ・ドリブンなアプリ) . このことから, アプリ起動時に できるだけそれはさけるべきです.


こういう記事が日本でももっと増えるといいですね.

Improving Startup Time in the NYTimes Android App - The New York Times