スキップしてメイン コンテンツに移動

[C#]Xamarin.FormsのApplication.Current.Propertiesより高性能な設定保存ライブラリ作った話

タイトルは半分釣りです(ごめんなさい)
(半分大真面目)

作った動機

もともと、こんなライブラリ作るつもりなんてなかったんですが、理想のMVVM Navigatorを製作中にXamarin.AndroidでのViewModelの状態保持がきついなと。

どういうことかというと、AndroidではView(Activity, Fragment)間の画面遷移時のデータのやり取りはIntentまたはBundleクラスを経由するんですが、そいつらに渡せるデータはJavaで言うところのプリミティブ型であったりJavaのSerializableインターフェースを実装したものまたはAndroidのParcelableインターフェースを実装したもののみなんですよ。

そんなインターフェースをMVVMのViewModelであったりModelに持ってこれるわけもなく、普段ならModelをJson化して文字列を画面遷移時に飛ばしていました。そういう標準的な仕組みを使わずにアプリケーションレイヤーでゴネゴネと頑張ってたわけですが、理想のMVVM Navigatorを作るとなると何らかの機構を用意しなければいけないと。

また、他にも画面遷移以外にも画面回転でViewが一度死ぬことにも対応しないといけません。これは前述したBundleクラスでデータを保持することで対応できるのですが、同じくMVVMではきつい。

.NET/Xamarin.Forms的にはそういうときはおとなしくViewModelが一度死んで、データは永続化して対応するという手法もあるようなので、それを参考にしようかなと。

AndroidのSharedPreferencesは実装がいいとは言えない

前述のわけで、データの永続化方面に着目していくわけですが、AndroidのSharedPreferencesクラス(Android標準の設定保存する奴)は実装がいいとは言えません。

なぜかというと理由は二つあって、

  1. コンストラクターで非同期的にファイルを読み込んで、読み込み終わるまでロックする
  2. メソッドはすべてロックする

という感じです。

1.のコンストラクターでの非同期ファイル読み込みはこのあたりのソースを読めばわかります。
async/awaitがないJavaならではの回避方法でしょうが、コンストラクターで非同期処理始めちゃうってのはいかがなモノかなと思いますね。

2.のメソッドはすべてロックするってのは1.のせいもありますが、マルチスレッドでアクセスしない場合では無駄なロックになってしまいます。ソースはこのあたりとか

また、実装によって良い悪いはいいがたいものですが、設定値をすべてObject型にしてBoxingを発生させてるのもあまりいただけない。(まぁ素直にBoxingしたほうが早いときもありますが)

Xamarin.AndroidならAndroid(Java)のクラスをわざわざ使わなくてもいいんじゃね?

前述の通りAndroid標準の設定を保存するクラスはいい実装ではないです。また、Xamarin.Androidの場合、MonoAndroidランタイムとAndroid-Java側のランタイムでのオブジェクトのマーシャリングコストがあるので、C#のみで扱う処理に関してはわざわざAndroidの標準のを使わないほうがパフォーマンスがいいはずです(ちゃんとした計測はしてないので理論的な話)

Xamarin.Formsはどうしてるのか?

タイトルからわかりますが、Xamarin.FormsならApplication.Current.Propertiesで設定の保存とかできます。
Applicationクラスのソースを見る限り、設定値はobjectとして持っています。
(アプリ側で取得したらキャストする必要がある)

そのあたりから追っていくとIDeserializerインターフェースに遭遇しますが、それの実装は当たり前ですが、各プラットフォームにあって、たとえばUWPならWindowsSerializerというクラスで実装しています。(おいまて、なぜIDeserializerインターフェースで名前がWindowsSerializerになる)

このあたりのコードを読むと各プラットフォーム独自にファイルを作って独自にシリアライズしてる感じということは読み取れます。

.NET Standard 2.0ならもっといいもの作れるんじゃね?

時代は.NET Standard 2.0ですが、IsolatedStorageFileを使えば各プラットフォームでファイルパスを気にせずにアプリケーション固有な領域でファイルを読み書きできます。

また、アプリケーション設定は暗号化とかの需要があります。一昔前はPCLCryptoでクロスプラットフォームな暗号化をしていたかと思いますが、今は.NET Standard 2.0でクロスプラットフォームな暗号化ができます。(サポート内容はこのあたりをみるとわかるかと)

よし、作ってみよう

下調べ的にはよさそうだったので、作ってみることにしました。
しかし、普通に作っていてはファイル書き込み時のシリアライズがつらいです。
また、それなりにはJson形式で保存したいところだったので(XMLを辞める)、.NET Standard 2.0で最速と言って良さそうなUtf8JsonをJsonシリアライザーとして使うことに。

ただ、.NET Core 2.1のパフォーマンスチューニングによって.NET Core 2.1向けの設計・ライブラリでUtf8Json越えをしたっぽいJsonシリアライザーがあったので、SpanJsonにも対応させることにしました。

Utf8JsonとSpanJsonは設計がよくて障壁があまりなかった

パフォーマンスチューニングしまくると設計が似たよったりになるのか、二つとも似たような設計になっていました。

Valueのシリアライズ・デシリアライズにIJsonFormatterインターフェースと各型向けの実装。名前解決や、どのIJsonFormatterを使うか解決するためのIJsonFormatterResolverインターフェースとその実装に分かれていました。

そのため、こちらとしては適切なIJsonFormatterやIJsonFormatterResolverと型を結び付けるだけでよかったのです。

ユーザー定義型にも対応させたい

まぁ、普通に作ってたらユーザー定義型を保存する際に不便になるので、ユーザー定義型を標準でサポートすることに。

ユーザー定義型をJsonで保存するには、Jsonから読み取るときにどういう型なのか判断できなければいけません。

アプローチはたぶん2通りあると思います。

  1. 型情報をJsonに載せる
  2. Keyから型情報を判断する

2.のほうは読み取る型の種類があらかじめ決まっていれば可能ですが、決まってないので現実的ではないです。そこで、型情報をJsonに載せることにしました。

そこで先日書いた記事([C#]Type.GetType(string)するときはアセンブリ情報がないほうが早い)が関わってきます。
記事の中では名前空間+型名が最速と書いていますが、アセンブリ情報がないとアセンブリを読み込んでない時に型を取得できなかったので、最低限のアセンブリ情報を載せるようにしました。

Jsonの形式どうしよう

型情報に基づいたシリアライズをするので、実際の設定値より先に型情報を読み込む必要があります。

アプローチとしては2通り考えれて

  1. 型情報のあとに設定値が来ないといけない制限を付ける
  2. 型情報をKey、設定値をValueで配置する

1.のほうはJsonっぽい形になっています。ただ、実装レベルの制限がついてしまう()
そして、無駄にKey情報もあるのでパフォーマンスもちょっと落ちます。
2.のほうは無駄なKey情報を省いて完全にチューニングした結果です。Jsonとしてもはや違法すれすれの形式なのであまり褒めたものではないです。

結果的に、二つの形式に対応したものとしました。(標準では2.の形式が使われる)
まぁ、そんな裏側のところ弄る人もいないだろうという判断です。

暗黙的な型変換どうしよう

作るものとしてはユーザー定義型にも対応させたいし、いちいち専用のメソッド叩くのも嫌なので、ジェネリクスを使ってます。(getとかsetのとこ)

で、問題になるのはget時の暗黙的な型変換です。
Xamarin.FormsのApplication.Current.Propertiesではアプリケーションレイヤー側が明示的なキャストをしてますが、AndroidのSharedPreferencesでは内部でいい感じにキャストしてます。

暗黙的に変換しても問題ないものはget時に型パラメーターの型へ暗黙的な型変換してもいいだろうとも考えれます。

そこで暗黙的な型変換はどういうのがあるのか、整理することにしました。

  • アップキャスト
  • 共変性・反変性
  • Nullable化
  • 数値変換
  • implicit operator

このうちimplicit operator以外は手ごろに何とかなりました。
implicit operatorに関してはどうも式ツリーで動的にコード生成しないといけなさそうだったので、Let's go 式ツリーということに;;
そのとき発見した黒魔術がこの記事です([C#][黒魔術] implicit operator: あいまいなユーザー定義の変換を回避する方法)

暗黙的型変換に関してはサポートしていくつもりですが、C# 8.0の変更でサポートし続けれるかわからない点と単にパフォーマンスもよくない(式ツリーとか使ってるからね)ので、お勧めはしない機能扱いです。
普通に正確な型を型パラメーターに設定しろ案件

成果物

https://github.com/MeilCli/SharedProperty

READMEをそれなりに頑張って書いたので日本語版のREADMEを読めばそれなりにわかると思います。

もちろんNuGetにも公開していて、Utf8Jsonを使う場合はこっち、SpanJsonを使う場合はこっちをインストールすればいいです。

今後の展望的なもの

最初のほうにロックするから遅いんだ!!!みたいなことを言いましたが一応スレッドセーフなものも用意しているんです。

ただ、一応()って感じに中身の実装は雑なので、そこはどうにかしたいところです。
あまりいい実装方法が思いつかないので、もしいい実装方法があったら教えてくださいm(__)m

PR/Issueももちろん受け付けてますm(__)m

コメント

このブログの人気の投稿

Stellaris 2.0.1 植民スパムプレイ

さっそくですが、友達との3日がかりのマルチプレイ終わりました。

銀河設定は中サイズ(600)、リング2、中盤の危機75年早く、終盤の危機100年早く、技術・伝統コスト0.5倍、難易度普通って感じでした。

そして、自分のプレイングは機械帝国植民スパム、友達は内向き牧歌でした。
最終的な国力としては機械帝国植民スパムのほうが強く、序盤から中盤は内向き牧歌のほうが強かったです。

機械帝国植民スパム はい、前回も言いましたが2.0から植民スパムゲーです。2.0.2からは前哨地維持費がかかるようになり更なる植民スパムゲーにもなりそうです。
植民スパムするからには、まぁロボットか機械帝国って感じなんですが、今回はゲーム進行が速い設定なので機械製造速度アップにガン振り帝国にしました。生産はエネルギー重視で、直轄地に鉱物惑星・研究惑星があるような感じにしました。
それでも、機械帝国は序盤の成長が遅く、鉱物を大量に消費するので、周りの帝国との不可侵条約はかかせません。直轄地6惑星(母星・エネルギー、エネルギー、研究*3、鉱物)の発展が終われば、今度はセクターの開発です。 まぁ、セクターの開発なんか物資さえ送り込めば勝手にやってくれるのでいいですが、エネルギー重視の生産なので鉱物の面倒は見てやらないとダメです。
帝国所有惑星が20ぐらいになれば、まぁ中銀河(サイズ600)なら敵なしですかね。あとは軍拡するだけです。
中盤の危機と終盤の危機 前述の通り危機はかなり早く発生するようにしたのですが、なんと中盤の危機がいっこうに発生しませんでした。発生したのは終盤の危機に立ち向かってるとき…おい()
終盤の危機は機械帝国プレイだったのでもちろんのようにコンティンジェンシー、中盤の危機は機械の反乱でした。
中盤の危機に関しては機械帝国プレイなので自分はどうでもよかったのですが、終盤の危機はがんばらないとまずかったです。コンティンジェンシーなので機械の生産-40%補正がつくので、スペシャルプロジェクトで解除(自分の研究力500ぐらいでは50か月ぐらいでした)しない限り国力が持ちません。
コンティンジェンシーは艦隊戦力が300k+100k*2の集団が4つほど湧いて100k艦隊は時間とともに増え続けるので急がないといけませんが、解除さえすればあまり余る国力で殴り続けるだけでしたね。終盤の危機の間に…

Stellaris ver2.0 Apocalypse プレイ感想

タイトルの通り大型アプデ後での友人とのマルチプレイでのプレイ感想です。

変更内容に関してはこちらのサイトで日本語訳された内容が公開されています(本当に助かりました)
Simulationian.com - 「Stellaris」開発日記#105――2.0「チェリイ」パッチノート

使用した帝国について 細かい設定は覚えていませんが(確認するのもめんどくさい)、有機生命体で狂信権威・平和主義で国是にアップデート内容の生まれた生命(Life-Seeded)を選択しています。 狂信権威について アップデートにより帝国の領域を増やすには星系ごとに影響力(1回限りに支払い)を使い前哨地を建てていく必要がありました。そこで狂信権威を選択したのですが、プレイスタイルが悪いのか、よく奴隷が不満を抱えストライキ?を起こしてしまいました。これに関しては防衛設備を地上に建て防衛軍を増やすことで対処できますが、タイルを無駄に消費している感じがすごいあります。 影響力が毎月4増えるのは領域を増やすのに好都合でしたが、Life-Seededとの相性が微妙でした(後述)。 Life-Seededについて 自分の種族がガイア型惑星にしか住めなくなるが、母星がガイア型25でスタートします。使用感想としては初期ブーストはいいかな?って感じです。ただ、アップデート後の環境ではプレイスタイルを頑張らなければ弱いかなと。その理由としては、まずアップデート後は研究コスト補正にPOP数が関係なくなり領域星系数が関係あるようになりました。つまり領域が大きければ大きいほどコストが高くなるということ。星系の資源はプレイヤーではどうしようもならない固定資源のようなものですから、それに頼って領域を大きくし過ぎると研究コストがすごいことになってしまいます。植民地を確保して賄おうにもガイア型にしか住めないのでかなり増やしにくいです。 ガイア型にしか住めないというのは、有機生命体であってロボットなら他のタイプの惑星に植民できるというのであれば、Life-Seededはロボットプレイをするしかないのかもしれないですが、これに関しては未検証です。
プレイスタイルについて 今回自分がとったプレイスタイルは要塞でガン守りキメるガチ芋戦略です。アップデートで要塞関連が変わり結構使えるものになったらしいので。 防衛プラットフォームを上限まで…

Xperia XZs SOV35をAndroid 8.0にアップデートしてみた

はい、タイトルの通りですが02/9の夕方ごろ8.0へのアップデートが降ってきたので、メイン機であるXperia XZsの人柱アップデートをしました。後悔はありません(今のところ) アップデート中は昼寝していたのでどれほど時間がかかったのかはわかりませんが電池の減りは20%ぐらいでした。


PiPを試そうとアレコレやってみたのですがどうもYouTubeはYouTube Redに入ってないとダメみたいです。
いちおうGoogle Play Musicの定額には入ってるんですけどね…日本でもYouTube Red始まらないといくらVPNでアメリカ装ってYouTube Red使おうにもGoogle Play Musicの定額連携はされないみたいです。 いちおうPiPはGoogle Mapの経路案内を開始で試しましたが、経路案内なんか今までしたことなく……使うこともなさそうでした。ChromeとかでもPiPできそうでしたがYouTube Webのほうを開いて動画流しても上手くできず…どのサイトが対応してるかはわかりませんでした。
設定画面からPiP対応のアプリを見ることはできますが、これ、信用できないですねぇ~~~