Android Studioでアプリ起動中に生成されるオブジェクトのメモリサイズを調べる

DaggerのModuleとComponentの紐付けについて考えていると、DIする全てのクラスをSingletonComponentに紐付けておけば良いのではと考えたりすることがあります。
この案が現実的なのか、インスタンス化されたクラスがどの程度のサイズになるのか調べればわかるのではと思い調べてみました。

Memory Profilerでアプリのメモリ使用量を調べる

実行中のAndroidアプリのメモリ使用量を調査するときはAndroid StudioのMemory Profilerを使います。
詳しいことは公式ドキュメントに記載されているので端折ります。

こちらを使ってDaggerのSingletonComponentで生成されるクラスのメモリ使用量を調べると下記のようになりました。

Mempry Profilerからダンプしたメモリ情報1

なぜかTodoRepositoryのインスタンスが4つもできてますね…
インスタンスによって使用しているメモリサイズが異なるのが気になります。
試しにTodoRepositoryを提供しているメソッドに@Singletonを付けると下記のようになりました。

Mempry Profilerからダンプしたメモリ情報2

インスタンスが1つになりました。
シングルトンにできていない状態でも5KB程度かつ今回調査したアプリでDaggerのSingletonComponentで生成しているクラスは100未満、仮に100個のクラスをインスタンス化したとしてもメモリ使用量は500KBと考えると全く問題なさそうです。

所感

Memory Profilerで調査する限りは問題なさそうなのですが、もっと確信を持って問題ないと言えるようになるには、
オブジェクトをインスタンス化してメモリに展開する時にJVM上でどのような処理が行われているのか、コンパイルがどのようにjavaファイルをclassファイルに変換しているのか等、より低レイヤーな部分も理解する必要がありそうです。
学習は続く…

PCを自作したときにわからなかった単語メモ

最近、自作PCにチャレンジして無事にUbuntuの起動までできました。
自作PCを作る上で初めて聞いた単語や見たことはあるけどわかっていなかった単語を調べたのでメモとして残しておきます。

わからんかった単語たち

PCI Express

PCIはPeripheral Component Interconnectの略で、マザボと(CPUやグラボ)周辺機器の間の通信を行うための規格です。
バージョンによって転送速度等に違いがありますが、互換性がありバージョンが違っても通信することができます。
性能をフルに出したいならより最新バージョンに対応した機器を購入すると良さそうです。

大学のコンピュータハードウェアの授業でバスの話がなんとも理解し難かったのを思い出しました。
大学の頃にPC自作していれば授業も余裕だったかもしれない…

UEFI

Unified Extensible Firmware Interfaceの略でBIOSに上位互換的な存在です。
今回ASRockのマザボを購入したのですが、PCの起動オプションでUbuntuのインストーラー最優先にするときにUEFIを通して設定しましたが、BIOSに比べて直感的なUIで迷わず操作が行えました。インストーラーの設定以外にも色々できそうなので今度いじってみようと思います。

オーバークロック

CPUの定格を超えて性能を引き出すことを言うらしいです。
自作PCでそんなことができるとは知らず、知った時は厨二心をくすぐられました。(エヴァの暴走みたいで好き)
が、機器の寿命を短くするらしい&使用しているソフトウェア的にオーバークロックする必要がないので控えておこうと思います。

M.2

SSDの接続規格です。
マザボに接続端子が用意されておりそこにM.2対応のSSDを取り付けましたが、従来のSSDやHDDと比較するととても小さくPCケースの場所を取らないで済むのがとてもありがたいです。
さらに転送速度も早いということで言うことなしです。

PC自作した感想

人生初自作PCだったのですが、メモリを押し込む時に軋む音がしたり各種ケーブルを接続する時に正しく配線できてるかとても心配になりましたが、電源投入したら無事GUIが表示されて感動しました。
使い始めてまだあまり時間が経ってないですが苦労して作ったので既に愛着が湧いてます。
しばらくは開発兼ゲーム用PCとして使って、普段業務で使っているMacとどれくらい性能差があるのか肌で体感しようと思います。

参考サイト

https://ja.wikipedia.org/wiki/PCI_Express
https://wa3.i-3-i.info/word12796.html
https://ja.wikipedia.org/wiki/%E3%82%AA%E3%83%BC%E3%83%90%E3%83%BC%E3%82%AF%E3%83%AD%E3%83%83%E3%82%AF
https://www.dospara.co.jp/5info/cts_str_parts_m2

Raspberry PIとCO2センサーで部屋のCO2濃度を計測する

作業部屋を締め切って作業していると、息苦しくなったり頭がぼーっとしてくる時があるので二酸化炭素濃度は問題ないのかとても気になり調べてみることにしました。
電子工作でド定番の分野なので細かい作業工程とかはスキップします。
MH-Z19というCO2センサーを使って計測しました。
ちなみに今回購入したものはフェイク品だったらしく精度が悪いです…
精度は悪いですがやりたいこと(作業部屋の二酸化炭素濃度が適切かどうか判断する)に対しては十分な精度&購入ページを見直すと精度の詳細表記もなくケチもつけづらいので手元にあるセンサーを使いました。

取り付け後はこんな感じです。
最初、はんだ付けなしでいけないか試してみましたがピンヘッダーの接触部分が少しでもずれるとセンサーから値を取れなくなるので、おとなしくはんだ付けしました。
定期的に取得してグラフにした結果が下記になります。

Y軸の単位を書き忘れていますがppmです。
作業部屋に人がいる状態でドアと窓を締め切ると急激に二酸化炭素濃度が上昇しグラフ上のスパイクが発生します。
だいたい2時間程度で1000ppmに到達します。一般的には1000ppmを超えたら換気が必要らしいです。

感想

仕事の日でかつ真夏or真冬の日の場合、今までほとんど換気を行わなかったために1000ppmを超えていた可能性が高いです。
今回の実装でCO2センサーから値を取得する処理は作れたので、あとは取得した結果が1000ppmだったらSlackに通知するなどの仕組みを考えれば、常に快適な環境で作業ができそうです。

おまけ

二酸化炭素濃度のことを調べていたら、気象庁のデータを発見しました。
点々観測している二酸化炭素濃度が毎年増えていることを知って驚きました。昨今のCO2削減ブームはこのあたりの数値が要因だったりするのでしょうか。

参考サイト

https://www.kccs-iot.jp/20200817-technical/
http://kunsen.net/2018/08/13/post-841/

Hexoのヘッダーにプロフィールを追加する

結論からいうとHexoのドキュメントを読もうになります。
やり方はいくつかあるのですが今回は記事とは別に個別のページを作ってそこにプロフィールを書く形式にしました。

プロフィール画面を追加する

Hexo上で新しいページを作成する時は、CLI上で下記のようなコマンドを実行します。

1
hexo new page "profile"

コマンドを実行するとプロジェクト直下のsourceディレクトリ内にprofile/index.mdが生成されるので、
そこに対してマークダウン形式でプロフィールを書いていけばできあがりです。
今回私が作成したプロフィールはこちら

参考サイト

https://stackoverflow.com/questions/29167023/how-to-add-route-for-hexo
https://hexo.io/docs/writing

Assisted Injectを使って既存のViewModelのコンストラクタにIDを渡す

Dagger 2.31からAssisted InjectというInjectする一部のインスタンスをDaggerの外側から注入できる仕組みが登場しました。
これの何が便利かというと、ViewModelへIDを渡して通信を行うケースでコンストラクタでIDを渡せるため、IDをViewModelへ渡す際にセッターや通信を行うメソッドの引数に持たせる必要がなくなりました。
既にDaggerを導入済みのViewModelであれば比較的簡単に移行が行えます。
必要な実装は下記になります。

  1. ViewModelのコンストラクタにIDを定義
  2. ViewModel生成用のファクトリメソッド追加
  3. ViewModelProvider.Factory生成用のファクトリメソッド追加
  4. 2で作成したファクトリメソッドをActivity/Fragment側にInjectする
  5. Daggerの古いViewModel定義を削除する

実際にどのようなコードになるのか書いてみましょう。

実装

  1. ViewModelのコンストラクタにIDを定義
1
2
3
4
class HogeViewModel @AssistedInject constructor(
private val useCase: FugaUseCase,
@Assisted private val fugaId: Long
) : ViewModel()

ViewModelのコンストラクタにAssistedInjectアノテーションを指定します。
ViewModelにIDを追加するときは、Assistedアノテーション使います。

  1. ViewModel生成用のファクトリメソッド追加
1
2
3
4
@AssistedFactory
interface Factory {
fun create(fugaId: Long): HogeViewModel
}

AssistedFactoryアノテーションを使うとDaggerがいい感じに依存関係を解決してくれるらしい。

  1. ViewModelProvider.Factory生成用のファクトリメソッド追加
1
2
3
4
5
6
7
8
9
10
companion object {
fun provideFactory(
assistedFactory: Factory,
fugaId: Long
): ViewModelProvider.Factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return assistedFactory.create(fugaId) as T
}
}
}

HogeViewModelに必要なパラメータを含んだViewModelProvider.Factoryを作ります。

  1. 2で作成したファクトリメソッドをActivity/Fragment側にInjectする
1
2
3
4
5
@Inject
lateinit var viewModelFactory: HogeViewModel.Factory
private val viewModel: HogeViewModel by viewModels {
HogeViewModel.provideFactory(viewModelFactory, args.fugaId)
}

HogeViewModel用のファクトリをInjectし、それを元にViewModelをインスタンス化します。

  1. Daggerの古いViewModel定義を削除する
1
2
3
4
//@Binds
//@IntoMap
//@ViewModelKey(HogeViewModel::class)
fun bindHogeViewModel(viewModel: HogeViewModel): ViewModel

地味に嵌ったのがこの処理で、最初Daggerまわりでコンパイルが通らずAssisted Injectの書き方を疑っていましたが、エラーをよくよく見ると旧ViewModel定義が解決できてないようなエラーだったので削除することで解決しました。

所感

今まではDagger経由で頑張ってIDを渡すよりは、セッターを使ってIDを渡した方が簡単だったので後者を採用していましたが、Assisted Injectが導入されたことで形勢逆転した感じがあります。
日々使いやすくなっていくDaggerを今後も追いかけていきたい。
すごいぞDagger!頑張れDagger!

参考サイト

https://dagger.dev/dev-guide/assisted-injection.html
https://qiita.com/takahirom/items/23b0f05ed3cdd6872bcb

Robolectricで端末の言語設定を変更する

Androidで文字列リソースを正しく組み立てることができるか、みたいなテストが書きたい時とかありますよね?
Robolectricを使っているとそんな時、デフォルト設定が英語なのでアプリが複数言語に対応している場合、期待値を日本語で書けなかったりします。
Robolectricでテスト時の言語設定を変更できないか調べてみたら、下記のように記述することで実現できました。

1
2
3
4
@RunWith(RobolectricTestRunner::class)
@Config(qualifiers = "ja")
class HogeTest(
...

qualifiersでは言語以外にも画面サイズやナイトモードなどいくつか変更可能なパラメータがあるようです。

参考サイト

http://robolectric.org/device-configuration/

画面回転時のEditTextの入力内容の保持はどこで行われているのか

EditTextに文字列が入力がされた状態で画面回転を行うと何もしないでも文字列が回転後も保持されていて不思議だったのでどうやって保持しているの調査してみました。

結論からいうと、EditTextのonSaveInstanceStateで保存されていました(より正確にいうと、EditTextはTextViewを継承しており、継承元のTextViewのonSaveInstanceStateで保存処理が行われています)。
実際の処理を抜粋したのが以下のコード。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SavedState ss = new SavedState(superState);

if (mText instanceof Spanned) {
final Spannable sp = new SpannableStringBuilder(mText);

if (mEditor != null) {
removeMisspelledSpans(sp);
sp.removeSpan(mEditor.mSuggestionRangeSpan);
}

ss.text = sp;
} else {
ss.text = mText.toString();
}

保存していた文字列を画面回転後に再度EditTextにセットするのはEditTextonRestoreInstanceState内の下記コード。

1
2
3
if (ss.text != null) {
setText(ss.text);
}

画面回転後にEditTextに入力されている文字列を元に再度通信クエリを実行する等の処理を行う場合は、onRestoreInstanceState以降の文字列がセットされたタイミングで行わないと空文字しか取得できずハマります。

Picassoで取得した画像をDarwableとして扱う

Androidで画像のURLをImageViewにセットする場合、PicassoやGlide等のライブラリを使うのが一般的だと思います。
しかし、何らかの理由でImageViewを参照できなかった場合どうすれば良いでしょうか。
今回は、ImageViewの参照が限定的な時にPicassoのライブラリを使って画像をセットするやり方について書きます。

問題

任意の画像URLから画像をダウンロードしてToolbarのロゴにセットしたかったのですが、
Toolbarのロゴ(ImageView)は外部に公開されておらずDrawableをセットするメソッドがあるだけした。
ImageViewが参照できないと詰みでは?と思ってたのですがドキュメントを眺めたらTargetというクラスがこの問題を解決してくれそうだったので試してみました。

解決策

コードは下記になります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
val target = object : Target {
override fun onBitmapLoaded(bitmap: Bitmap?, from: Picasso.LoadedFrom?) {
binding.toobar.logo = bitmap?.toDrawable(resources)
}

override fun onBitmapFailed(errorDrawable: Drawable?) {
binding.toobar.logo = errorDrawable
}

override fun onPrepareLoad(placeHolderDrawable: Drawable?) {
binding.toobar.logo = placeHolderDrawable
}
}

Picasso.with(this)
.load(url)
.into(target)

Targetの使い方はImageView同様、into()メソッドのパラメータにセットするだけです。
そうすることで、プレースホルダー画像・エラー画像・URLから取得した画像のDrawableをコールバックで受け取ることができます。

感想

普段自分は問題にぶつかった時、自分が思いつく解決策を実現してくれるコードがライブラリにあるかという観点でコードを読んでいます。
が、square製ライブラリの場合は、そもそも自分が思いもしなかったスマートな解決が提供されてる場合がありドキュメントをしっかり読むことの重要性を再確認しました。

minSdkVersion 24未満のプロジェクトでSimpleDateFormatを使ってISO-8601に対応してはいけない

問題

1
SimpleDateFormat("yyyy-MM-dd'T'00:00:00XXX", Locale.getDefault())

WEB APIに送る日付書式をISO-8601の拡張形式に準拠した形式にするため、上記のようなコードを書いていたらAndroid 6未満でIllegalArgumentExceptionが発生してました。
メッセージは下記のようなものでXが認識されてないようでした。

1
Unknown pattern character 'X'

ソースコードを読んだところ、Android 6に含まれるSimpleDateFormatの実装ではStringで渡される日付フォーマットにXが入ることが考慮されていないようでした。
Android 7以降では問題なく動作しているので、Android 6ではJava6ベースのSimpleDateFormatが、Android 7ではJava7ベースのSimpleDateFormatが使われてるのではないかと推測します。(調べ方がわからない。。。)

対策

ThreeTenABPを使いました。
ThreeTenABPはAndroidでJava8のDate and Time APIの一部をバックポートできるライブラリです。
Java8のDate and Time API使えば間違いないやろ!と考えてましたが、AndroidでJava8を使うにはAPIレベル 26が必要らしくもう数年待つ必要がありました。

まとめ

DroidKaigiアプリでThreeTenABPが使われてて、何か便利なのだろうけど何が便利なのかわからんという状態でしたが今回の件でありがたみを理解できました。

参考サイト

Android Studioで署名鍵を作るときに表示されるエラーはスルーできる

Android StudioでApp Bundleの挙動を確かめるために、Google Playアップロード用の署名鍵を作成したものの、
ちょっと心配になる挙動があったのでメモとして残します。

作業環境

  • macOS Catalina Ver.10.15.6
  • Android Studio 4.1

署名鍵を作る

Android Studioで新しく署名鍵を作る時は下記の手順になります。

  1. Build -> Generate Signed Bundle/APK を選択
  2. Android App Bundle を選択
  3. Create new… を選択
  4. 必要事項を記入
  5. OKボタンを押す

今回、新しく鍵を作成したところ、手順5の後に下記のようなダイアログが表示されました。
Android Studioのエラー

エラー内容的には、JKSで署名鍵作ってるけど業界標準はPKCS12だからそっち使ってねという内容です。(素直にこの指示に従いCLIで署名鍵の書き換えを行ったらエラーが出て置き換えできませんでした。)
個人的には、このエラーと共に生成された鍵を信用して良いのかだいぶ心配だったので調査してみました。

調査結果

結論として、主に下記3点を確認しましたが、生成された鍵自体は何も問題ありませんでした。

  • 生成された署名鍵でアプリに署名できる
  • 署名されたAABファイルをストアにアップロードできる
  • クローズドベータテストでアプリを配布して任意のAndroidデバイスから起動できる

また、調査する中で下記のような情報を発見しました。

上記情報を踏まえての推測になりますが、Android Studioの署名鍵作成時は警告などのログを表示する場所がないために、生成後に表示するエラーに雑にまとめて表示されてるのかなと思いました。