iOSエンジニアのつぶやき

毎朝8:30に iOS 関連の技術について1つぶやいています。まれに釣りについてつぶやく可能性があります。

Bridging Header vs Module Map

今回は昨日の import と Link の記事に引き続き、Bridging Header や Module Map について簡単にまとめていきたいと思います。

Bridging Header

Bridging Header は Objective-C で書かれたコードを Swift で利用する仕組みで、それぞれの Objective-C 製ライブラリのヘッダーファイル(**.h) を、ファイルに import することで Swift 側から公開されているシンボルにアクセスできるようになります。Bridging Header はアプリケーションターゲットとしてのみ作用するのでフレームワークの開発などでは使用することができません。また、これらはグローバルに作用するので、特定のファイルのみに Import するということは基本的にはできません。

developer.apple.com

Module Map とは?

Module Map は Bridging Header の上位互換で、Objective-C または C で書かれたライブラリの場合は、Modules ディレクトリの中に module.modulemap というファイルを設定することで、Swift 側からシンボルにアクセスできるようにします。Swift が登場して移行の XcodeObjective-C 製のライブラリなどをビルドした場合は自動的に module.modulemap ファイルが生成されます。

そもそもモジュールとは?

Swift が Framework および Library をインポートする際に必要なもので、Framework の Public な API の宣言です。Swift で書かれた Framework の場合は Framwork の中に下記のようなモジュールが存在し、これらはコンパイラによって自動的に生成されます。実態は .swiftmodule という拡張子のファイルが Module です。

- Hoge.swiftmodule
  - arm64.swiftdoc
  - arm64.swiftmodule ⭕️
  - x86_64.swiftdoc
  - x86_64.swiftmodule ⭕️

Objective-C/C 製の Framework などの場合は Module Map についての説明でも触れたように、module.modulemap で Framework の **.h ファイルを Module に変換することで、Swift から Framework をインポートできるようになります。

また、これらの Objective-C/C 製の Framework などの場合は、Modules(module.modulemap) を Framework バンドル外に配置することも可能で、下記がインポートする際に必要となる設定となります。

Framework バンドル内に Module がある場合

FRAMEWORK_SEARCH_PATHS に Framework があるディレクトリの親ディレクトリの Path を指定します。

Framework バンドル外に Module がある場合

SWIFT_INCLUDE_PATHSmodule.modulemap のあるディレクトリの親ディレクトリを指定します。

参考

その他の記事

yamato8010.hatenablog.com

yamato8010.hatenablog.com

yamato8010.hatenablog.com

ライブラリの import と link について理解する

今回は iOS 開発における Library および Framework のインポートおよび、リンクの仕組みについて学んだことをまとめていきたいと思います。

これらのことを学ぶことで、今まで Xcode が暗黙的に行っていた処理がどのようになっているのかが分かり、ライブラリを使用する際に Error が発生した際にも適切に対応することができるようになるかと思います✍️

Import とは?

Import とは、ライブラリをプロジェクトのコードベースで利用可能にするための言語機能で、それぞれのライブラリが公開しているシンボルを参照できるようにします。各ファイルはコンパイルする時点で解決され、rubyjs などのインポートとは違い、ビルド時点で全て解決されている必要があります。

Link とは?

Link とは、プロジェクトのソースコードコンパイルして生成されたオブジェクトファイルや外部ライブラリなどを全て連結(リンク)して実行ファイルまたは、ライブラリを生成する機能で、リンカ が行います。これらはソースコードが全てコンパイルされた後に実行され、シンボルが全て解決されます。

Xcode での設定

Framewoks, Libraries, and Embedded Content に、Framework を追加した際に Xcode 側では下記のようなことが行われます。

  • FRAME_WORK_SEARCH_PATHS に Framework があるディレクトリの親ディレクトリを追加する
  • Link Binary With Libraries に Framework を追加する
  • Build Phase の Embed Frameworks に Framework を追加して Framework を Application バンドルにコピーする

FRAME_WORK_SEARCH_PATHS を設定する

FRAME_WORK_SEARCH_PATHS はライブラリのインポートに必要な Module を見つけてソースコードで利用可能にするために必要な Path ですが、ここについてはまた別の記事でまとめようかと思います。

Link Binary With Libraries に Framework を追加する

Link Binary With Libraries に Framework が追加されることで、コンパイル後にリンカがリンクを正常に行うことができるようになります。このことから、Link Binary With Libraries から Framework の設定を削除した場合、リンクエラーになるかと考えられますが、Swift の Automatic Linking という機能によって、ライブラリを Import した時点で、コンパイルが自動でライブラリのリンクをはるので、Error にはならずに Build が完了します。また、これらのコンパイルオプションは Build Settings > Linking > Other Linker Flags から手動で渡すこともできます。

Embed Frameworks に Framework を追加する

Embed Frameworks に Framework が追加されることで、システムがアプリケーションバンドルからライブラリへのリンクをすることができるようになります。つまり、dynamic framework・library のような動的リンクが必要なものは、この設定がないとランタイムにライブラリへのリンクをすることができずに、一度は見たことがあるであろう下記のような Runtime Error を引き起こします。反対に、ビルド時にリンクが完了しているような static framework・library などの場合は、Embed Frameworks の設定がなくても正常に動作することが可能です。

dyld: Library not loaded: @rpath/RxSwift.framework/RxSwift
  Referenced from: /private/var/containers/Bundle/Application/C70206BC-1D89-4AFF-A02B-8423C521AA64/MyApp.app/MyApp
  Reason: image not found

これらビルド時にアプリケーションバンドル の Frameworks にコピーされた Framework を確認するには、プロジェクトの Products ディレクトリの XX.app を右クリックし Show in Finder を選択します。Finder で表示されたコンテンツを右クリックして パッケージの内容を表示 を選択します。その中の Frameworks ディレクトリの中身がバンドルされた Framework になります。

参考

Swiftにおけるインポートとリンクの仕組みを探る - Speaker Deck

www.youtube.com

その他の記事

yamato8010.hatenablog.com

yamato8010.hatenablog.com

yamato8010.hatenablog.com

知識ゼロからの Kotlin Android アプリリリースへの軌跡 / Day14【Complex lifecycle situations編】

学ぶこと

  • ライフサイクルコールバックでアプリの一部を設定、開始、停止する方法
  • Android ライフサイクルライブラリを使用してライフサイクルオブザーバーを作成し、activity と fragment のライフサイクルを管理しやすくする方法
  • Android プロセスのシャットダウンがアプリ内のデータに与える影響、および Android アプリを閉じた時にそのデータを自動的に保存および復元する方法
  • バイスの rotation やその他の configuration がライフサイクルの状態にどのような変化をもたらし、アプリの状態に影響を与えるのか

すること

  • DessertClicker アプリを変更してタイマー機能を含め、Activity ライフサイクルの様々な時点でそのタイマーを開始および停止します

  • Android ライフサイクルライブラリを使用するようにアプリを変更して、DessertTimer クラスをライフサイクルオブザーバーに変換します

  • Android Debug Bridge(abc)を設定して使用し、アプリのプロセスシャットダウンとその時に発生するライフサイクルコールバックをシュミレートします

  • onSaveInstanceState() メソッドを実装して、アプリが予期せず閉じられた場合に失われる可能性のあるアプリデータに保存します。アプリの再起動時にそのデータを復元するコードを追加します

アプリ概要

このコードラボでは、前のコードラボの DessertClicker アプリを拡張します。バックグランドタイマーを追加してから、Android ライフサイクルライブラリを使用するようにアプリを変換します。

ライフサイクルの間違いを避ける

前回のコードラボでは、様々なライフサイクルコールバックをオーバーライドし、システムがそれらのコールバックを呼び出す時にログに記録することで、activity と fragment のライフサイクルを監視する方法を学習しました。このタスクでは、DessertClicker アプリでライフサイクルタスクを管理するより複雑な例について説明します。実行されているカウントとともに、ログステートメントを毎秒出力するタイマーを使用します。

DessertTimer を設定する

  1. DessertClicker アプリを開きます。

  2. java > com.example.android.dessertclicker を展開し、DessertTimer.kt を開きます。現在、全てのコードがコメントアウトされているため、アプリの一部として実行されないことに注意してください

  3. Code > Comment with Line Comment を選択するか、Command + / を押します。このコマンドは、ファイル内の全てのコードのコメントを解除します。(AndroidStudio では、アプリを再構築するまで未解決の参照エラーが表示される場合があります。)

  4. DessertTimer クラスには、タイマーを開始および停止する startTimer() および stopTimer() が含まれていることに注意してください。startTimer() が実行されている場合、タイマーは1秒ごとにログメッセージを出力し、その時間の合計秒数を示します。次に、stopTimer() メソッドは、タイマーとログステートメントを停止します。

Note: DessertTimer クラスは、タイマーにバックグランドスレッドを使用し、Runable クラスと Handler クラスが関連づけられています。このコードラボでは、これらのことについて知る必要はありません(ただし、後のコードラボでスレッドについて詳しく学習します)。

  1. MainActivity.kt を開きます。クラスの最上部、dessertsSold 変数のすぐ下に、タイマーの変数を追加します。
private lateinit var dessertTimer : DessertTimer;
  1. setOnClickListener() を呼び出した直後に、onCreate() まで下にスクロールして、新しい DessertTimer オブジェクトを作成します。
dessertTimer = DessertTimer()

dessert timer object ができたので、activity が画面に表示されている時にのみタイマーを実行するために、タイマーを開始および停止する場所を検討します。次のステップでいくつかのオプションを見ていきます。

タイマーの開始と停止

onStart() メソッドは、activity が表示される直前に呼び出されます。onStop() メソッドは、activity が表示されなくなった後に呼び出されます。これらのコールバックは、タイマーをいつ開始および停止するかについての適切な候補のようです。

  1. MainActivity クラスで、onStart() コールバックでタイマーを開始します。
override fun onStart() {
   super.onStart()
   dessertTimer.startTimer()

   Timber.i("onStart called")
}
  1. onStop() でタイマーを停止します。
override fun onStop() {
   super.onStop()
   dessertTimer.stopTimer()

   Timber.i("onStop Called")
}
  1. アプリをコンパイルして実行します。Android Studio で、Logcat ペインををクリックします。Logcat 検索ボックスに dessertclicker と入力します。これにより、MainActivity クラスと DessertTimer クラスと DessertTimer クラスの両方でフィルタリングされます。アプリが起動すると、タイマーもすぐに実行を開始することに注意してください。

  1. Back ボタンをクリックして、タイマーが再び停止することを確認します。Activity とそれが制御するタイマーの両方が破棄されたため、タイマーは停止します。

  2. 最近の画面を使用してアプリに戻ります。Logcat で、タイマーが 0 から再開することに注意してください。

  3. Share ボタンをクリックします。Logcat で、タイマーが実行中であることに注目してください。

  1. ホームボタンをクリックします。Logcat で、タイマーの実行が停止していることに注目してください。

  2. 最近の画面を使用してアプリに戻ります。Logcat で、タイマーが中断したところから再開することに注意してください。

  3. MainActivityonStop() メソッドで、stopTimer() の呼び出しをコメントアウトします。

  4. アプリを実行し、タイマーが開始したらホームボタンをクリックします。アプリがバックグランドにある場合でも、タイマーは実行されており、システムリソースを継続的に使用しています。タイマーを実行し続けることは、アプリのメモリリークであり、恐らく希望する動作ではありません。

一般的なパターンは、コールバックで何かを設定または開始すると、対応するコールバックでそれらを停止または削除する必要があります。これにより、不要になった時に何かを実行する必要がなくなります。

Note: 音楽の再生など、場合によっては、実行を続けたいことがあります。何かを実行し続けるための適切で効率的な方法がありますが、このコードラボの範囲外です。

  1. タイマーを停止する onStop() の行のコメントを外します。

  2. startTimer() の呼び出しを onCreate() に変更します。

  3. アプリを実行します。

  4. Home をクリックしてアプリを停止します。予想通り、タイマーは実行を停止します。

  5. 最近の画面を使用してアプリに戻ります。この時に、タイマーが再び開始されないことに注意してください。

覚えておくべき重要なポイント:

  • ライフサイクルコールバックでリソースを設定する時は、リソースも破棄します。
  • 対応する方法でセットアップと分解を行います。
  • onStart() で何かを設定した場合は、onStop() で停止するか、再度破棄します。

Android ライフサイクルライブラリを使用する

DessertClicker アプリでは、onStart() でタイマーを開始した場合、onStop() でタイマーを停止する必要があることを簡単に確認できます。タイマーは1つしかないので、タイマーを停止することを覚えておくのは難しくありません。

より複雑な Android アプリでは、onStart() または onCreate() で多くの設定を行い、onStop() または onDestroy() でそれらを全て破棄する場合があります。例えば、アニメーション、音楽、センサー、またはタイマーがあり、セットアップと破棄、および開始と停止の両方が必要な場合があります。忘れているとバグの種になってしまいます。

Android Jetpack の一部であるライフサイクルライブラリは、このタスクを簡素化します。このライブラリは、ライフサイクルの状態が異なるものもある多くの可動部品を追跡する必要がある場合に特に役立ちます。ライブラリはライフサイクルの動作方法を反転させます。通常、Activity または Fragment は、ライフサイクルコールバックが発生した時にコンポーネントに何をするかを指示します。ただし、ライフサイクルライブラリを使用すると、コンポーネント自体がライフサイクルの変更を監視し、それらの変更が発生した時に必要な処理を実行します。

ライフサイクルライブラリには、次の3つの主要部分があります:

  • コンポーネントはライフサイクルを所有します。ActivityFragment はライフサイクルの所有者です。ライフサイクルの所有者は、LifecycleOwner インターフェースを実装します。
  • ライフサイクル所有者の実際の状態を保持し、ライフサイクルの変更が発生した時にイベントをトリガーします。
  • LifecycleObserverはライフサイクルの状態を監視し、ライフサイクルが変更された時にタスクを実行します。LifecycleObserver は、LifecycleObserver インターフェースを実装します。

このタスクでは、DessertClicker アプリを Android ライフサイクルライブラリを使用するように変換し、ライブラリによって Android activity と fragment ライフサイクルの操作がどのように管理しやすくなるかを学習します。

DessertTimer を LifecycleObserver に変える

ライフサイクルライブラリの重要な部分は、lifecycle observer の概念です。監視により、クラスは Activity または Fragment のライフサイクルについて認識し、それらのライフサイクル状態の変化に応じて開始および停止できます。lifecycle observer を使用すると、activity と fragment のメソッドからオブジェクトを開始および停止する責任を取り除くことができます。

  1. DessertTimer.kt クラスを開きます。
  2. DessertTimer クラスのクラスシグネチャを次のように変更します。
class DessertTimer(lifecycle: Lifecycle) : LifecycleObserver {...}

この新しいクラス定義は2つのことを行います:

  • コンストラクターは、タイマーが監視しているライフサイクルである Lifecycle オブジェクトを受け取ります。
  • クラス定義は、LifecycleObserver インターフェースを実装します。

  • runnable 変数の下で、クラス定義に init ブロックを追加します。init ブロックで、addObserver() メソッドを使用して、所有者(activity) からこのクラス(observer)に渡されたライフサイクルオブジェクトを接続します。

 init {
   lifecycle.addObserver(this)
}
  1. startTimer()@OnLifecycleEvent アノテーションを付け、ON_START ライフサイクルイベントを使用します。lifecycle observer が監視できる全てのライフサイクルイベントは、 Lifecycle.Event クラスにあります。
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun startTimer() {}
  1. ON_STOP イベントを使用して、stopTimer() に対して同じことを行います。
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun stopTimer()

MainActivity を変更する

FragmentActivity スーパークラスLifecycleOwner を実装しているため、MainActivity クラスは継承によってすでにライフサイクルの所有者になっています。したがって、Activity を lifecycle に対応させるために必要なことは何もありません。Activity のライフサイクルオブジェクトを DessertTimer コンストラクターに渡すだけです。

  1. MainActiviy を開きます。onCreate() メソッドで、DessertTimer の初期化を変更して this.lifecycle を含めます。
dessertTimer = DessertTimer(this.lifecycle)

activity のライフサイクルプロパティは、この Activity が所有するライフサイクルオブジェクトを保持します。

  1. onCreate()onStop でのそれぞれのタイマー呼び出しメソッドを削除します。DessertTimer はライフサイクル自体を監視しており、ライフサイクルの状態が変化すると自動的に通知されるため、Activity で何をするかを DessertTimer に指示する必要はありません。これらのコールバックを行うのは、メッセージをログに記録することだけです。

  2. アプリをコンパイルして実行します。Logcat で期待通りログが表示されていることを確認してください。

  1. ホームボタンをクリックして、アプリをバックグランドに配置します。予想通り、タイマーの実行が停止していることに注意してください。

onSaveInstanceState() を使用して、アプリのシャットダウンをシュミレートする

Android がバックグランドでアプリをシャットダウンすると、アプリとそのデータはどうなりますか?これを理解することが重要です。

アプリがバックグランドに移行しても、破棄されることはなく、停止され、ユーザがアプリに戻るのを待つだけです。しかし、Android OS の主な懸念事項は1つは、フォアグランドでの activity をスムーズに実行し続けることです。たとえば、ユーザーがGPSアプリを使用してバスに乗るのを支援している場合、そのGPSアプリを素早くレンダリングし、道順を表示し続けることが重要です。ユーザが数日間見ていなかった可能性のある DessertClicker アプリを、バックグランドでスムーズに実行し続けることはそれほど重要ではありません。

Android は、フォアグランドアプリを問題なく実行できるように、バックグランドアプリを規制しています。たとえば、Android は、バックグランドで実行されているアプリが実行できる処理の量を制限します。

Android は、アプリに関連する全ての Activity を含むアプリプロセス全体をシャットダウンすることもあります。Android は、システムにストレスがかかり、視覚的に遅れる危険がある場合にこの種のシャットダウンを実行するため、この時点で追加のコールバックやコードは実行されません。アプリのプロセスは、バックグランドでサイレントにシャットダウンされるだけです。しかし、ユーザには、アプリが閉じられているようには見えません。ユーザが AndroidOS にシャットダウンしたアプリに戻ると、Android はそのアプリを再起動します。

このタスクでは、Android プロセスのシャットダウンをシュミレートし、アプリが再起動した時にアプリがどうなるかを調べます。

Note: 開始する前に、API28以降をサポートするエミュレーターまたはデバイスを実行していることを確認してください。

adb を使用してプロセスのシャットダウンをシュミレートします

Android Debug Bridge(adb)は、コンピューターに接続されているエミュレーターやデバイスに指示を送信できるコマンドラインツールです。このステップでは、adb を使用してアプリのプロセスを閉じ、Android がアプリをシャットダウンした時に何が起こるかを確認します。

  1. アプリをコンパイルして実行します。カップケーキを数回クリックします。

  2. ホームボタンを押して、アプリをバックグランドに配置します。これでアプリは停止し、Android がアプリを使用しているリソースを必要とする場合、アプリは閉じられる可能性があります。

  3. Android Studio で、Terminal タブをクリックして、command-line terminal を開きます。

  1. adb と入力し、Return キーを押します。

Android Debug Bridge version X.XX.X で始まり、logcat で使用されるタグで終わる出力が多数表示される場合、全て問題ありません。adb: command not found が表示された場合は、実行パスで adb コマンドが使用可能であることを確認してください。手順については、Utilities chapter の "Add adb to your execution path" を参照してください。

  1. このコメントをコピーしてコマンドラインにに貼り付け、Return キーを押します。
adb shell am kill com.example.android.dessertclicker

このコマンドは、接続されているデバイスまたはエミュレータに、dessertclicker パッケージ名を使用してプロセスを停止するように指示しますが、アプリがバックグランドにある場合に限ります。アプリがバックグランドで実行されていたため、デバイスまたはエミュレーターの画面には、プロセスが停止したことを示すものは何も表示されません。Android Studio で、Run タブをクリックして、"Application terminated" というメッセージを表示します。Logcat タブをクリックして、onDestroy() コールバックが実行されなかったこと、つまり Activity が単に終了したことを確認します。

  1. 最近の画面を使用してアプリに戻ります。アプリは、バックグランドに配置されているか、完全に停止されているかに関係なく、最近の画面に表示されます。最近の画面を使用してアプリに戻ると、Activity が再開されます。Activity は、onCreate() を含むスタートアップライフサイクルコールバックのセット全体を追加します。

  2. アプリを再起動すると、"score"(販売されたデザートの数と合計金額の両方)がデフォルト値(0) にリセットされることに注目してください。Android がアプリをシャットダウンした場合、なぜそれが状態を保存しなかったのでしょう?

OS がアプリを再起動すると、Android はアプリを以前の状態にリセットしようとします。Android は、一部の View の状態を取得し、Activity から離れるたびにバンドルに保存します。自動的に保存されるデータの例としては、EditText のテキスト(レイアウトに ID が設定されている場合)や Activity の back stack があります。

ただし、Android OS が全てのデータを認識していない場合があります。たとえば、DessertClicker アプリに収益などのカスタム変数がある場合、Android Os はこのデータや Activity に対するその重要性を認識しません。このデータを自分でバンドルに追加する必要があります。

onSaveInstanceState() を使用して bundle data を保存します

onSaveInstance() メソッドは、Android OS がアプリを破棄した場合、必要になる可能性のあるデータを保存するために使用するコールバックです。lifecycle callback diagram では、Activity が停止した後に onSaveInstanceState() が呼び出されます。アプリがバックグランドに入るたびに呼び出されます。

onSaveInstanceState() 呼び出しを安全対策と考えてください。Activity がフォアグランドを終了する時に、少量の情報を bundle に保存する機会が与えられます。アプリをシャットダウンするまで待機した場合、OS がリソースの負荷にさらわれている可能性があるため、システムはこのデータを保存します。毎回データを保存することで、バンドルないの更新データを必要に応じて復元できるようになります。

  1. MainActivity で、onSaveInstanceState() コールバックをオーバーライドし、Timber ログステートメントを追加します。
override fun onSaveInstanceState(outState: Bundle) {
   super.onSaveInstanceState(outState)

   Timber.i("onSaveInstanceState Called")
}

Note: onSaveInstanceState() には2つのオーバーライドがあります。1つは outState パラメーターのみを使用し、もう1つは outState パラメーターと outPersistentState パラメーターを含みます。

  1. アプリをコンパイルして実行し、Home ボタンをクリックしてアプリをバックグランドに配置します。onSaveInstanceState() コールバックが onPause()onStop() の直後に発生することに注意してください。

  1. ファイルの先頭、クラス定義の直前に、次の定数を追加します:
const val KEY_REVENUE = "revenue_key"
const val KEY_DESSERT_SOLD = "dessert_sold_key"
const val KEY_TIMER_SECONDS = "timer_seconds_key"

これらのキーは、instance state bundle からのデータの保存と取得の両方に使用します。

  1. onSaveInstanceState() まで下にスクロールし、Bundle タイプの outState パラメータに注目してください。

バンドルは、キーと値のペアコレクションであり、キーは常に文字列です。intboolean などのプリミティブ値をバンドルに入れることができます。 システムはこのバンドルを RAM に保存するため、バンドル内のデータを小さく保つことをお勧めします。サイズはデバイスごとに異なりますが、このバンドルのサイズも制限されています。通常は、100k 未満で保存する必要があり、そうしないと TransactionTooLargeException エラーでアプリがクラッシュするリスクがあります。

  1. onSaceInstanceState() で、putInt() メソッドを使用して revenue 値(integer) をバンドルに入れます。
outState.putInt(KEY_REVENUE, revenue)

putInt() メソッド(および putFloat()) や putString() などの Bundle クラスの同様のメソッドは、キーの文字列(KEY_REVENUE 定数) と保存する実際の値の2つの引数をとります。

  1. 販売したデザートの数とタイマーのステートメントを使用して、同じプロセスを繰り返します。
outState.putInt(KEY_DESSERT_SOLD, dessertsSold)
outState.putInt(KEY_TIMER_SECONDS, dessertTimer.secondsCount)

onCreate() を使用して bundle data を復元します

  1. onCreate() までスクロールし、メソッドのシグネチャを調べます:
override fun onCreate(savedInstanceState: Bundle) {...}

onCreate() は、呼び出されるたびにバンドルを取得することに注意してください。プロセスのシャットダウンが原因で Activity が再開されると、保存したバンドルが onCreate() に渡されます。Activity が開始された場合、onCreate() のこのバンドルは null です。したがって、Bundle が null でない場合は、以前にわかっていたポイントから Activity を "re-creating" していることが分かります。

Note: Activity が re-creating されている場合、onRestoreInstanceState() コールバックは、Bundle とともに onStart() の後に呼び出されます。ほとんどの場合、onCreate() で Activity の状態を復元します。ただし、onRestoreInstanceState() は、onStart() の後に呼び出されるため、onCreate() の呼び出し後に何らかの状態を復元する必要がある場合は、onRestoreInstanceState() を使用できます。

  1. DessertTimer のセットアップ後、次のコードを onCreate() に追加します。
if (savedInstanceState != null) {
   revenue = savedInstanceState.getInt(KEY_REVENUE, 0)
}

null のテストでは、bundle にデータがあるかどうか、または bundle が null であるかどうかを判断します。これにより、アプリが新しく起動されたか、シャットダウン後に再作成されたかが分かります。このテストは、バンドルからデータを復元するための一般的なパターンです。

ここで使用したキー(KEY_REVENUE) は、putInt() に使用したものと同じであることに注意してください。毎回同じキーを使用するようにするには、それらのキーを定数として定義することお勧めします。putInt() を使用してデータをバンドルに入れるのと同じように、getInt() を使用してバンドルからデータを取得します。getInt() メソッドは2つの引数をとります。

  • キーとして機能する文字列。例えば、収益値の"key_revenue"
  • バンドル内のそのキーに値が存在しない場合のデフォルト値

バンドルから取得した整数が収益変数に割り当てられ、UI はその値を使用します。

  1. getInt() メソッドを追加して、販売されたデザートの数とタイマーの値を復元します。
if (savedInstanceState != null) {
   revenue = savedInstanceState.getInt(KEY_REVENUE, 0)
   dessertsSold = savedInstanceState.getInt(KEY_DESSERT_SOLD, 0)
   dessertTimer.secondsCount =
       savedInstanceState.getInt(KEY_TIMER_SECONDS, 0)
}
  1. アプリをコンパイルして実行します。ドーナッツに切り替わるまで、カップケーキを少なくとも5回押します。Home をクリックして、アプリをバックグランドに配置します。

  2. Android Studio Terminal タブで、adb を実行してアプリのプロセスをシャットダウンします。

adb shell am kill com.example.android.dessertclicker

最近の画面を使用してアプリに戻ります。今回は、アプリが正しい収益で返され、バンドルからのデザートの販売額に注目してください。しかし、デザートがカップケーキに戻ったことにも注目してください。アプリがシャットダウンから元の状態に戻るようにするために、もう1つやるべきことがあります。

  1. MainActivity で、showCurrentDessert() メソッドを調べます。このメソッドは、現在販売されているデザートの数と allDesserts 変数のデザートのリストに基づいて、Activity に表示するデザート画像を決定することに注意してください。
for (dessert in allDesserts) {
   if (dessertsSold >= dessert.startProductionAmount) {
       newDessert = dessert
   }
    else break
}

この方法は、販売されたデザートの数に依存して適切な画像を選択します。したがって、onSaveInstance() でバンドル内の画像への参照を保存するために何もする必要はありません。そのバンドルには、販売されたデザートの数がすでに保存されています。

  1. onCreate() で、バンドルから状態を復元するブロックで、showCurrentDessert() を呼び出します。
 if (savedInstanceState != null) {
   revenue = savedInstanceState.getInt(KEY_REVENUE, 0)
   dessertsSold = savedInstanceState.getInt(KEY_DESSERT_SOLD, 0)
   dessertTimer.secondsCount = 
      savedInstanceState.getInt(KEY_TIMER_SECONDS, 0)
   showCurrentDessert()                   
}
  1. アプリをコンパイルして実行し、バックグランドに配置します。adb を使用してプロセスをシャットダウンします。最近の画面を使用してアプリに戻ります。ここで、全てのデータが正常に復元されたことに注目してください。

Configuration の変更を調べる

Activity と Fragment のライフサイクルの管理には、理解するべき重要な特別なケースがあります。それは、configuration の変更が Activity と Fragment のライフサイクルにどのように影響するかです。

configuration の変更は、デバイスの状態が急激に変化した時に発生するため、システムが変更を解決する最も簡単な方法は、Activity を完全にシャットダウンして再構築することです。たとえば、ユーザがデバイスの言語を変更した場合、様々なテキストの方向に対応するためにレイアウト全体を変更する必要がある場合があります。ユーザがデバイスをドックに接続したり、物理キーボードを追加したりする場合、アプリのレイアウトで異なるディスプレイサイズやレイアウトを利用する必要がある場合があります。また、デバイスの向きが変わった場合、新しい向きに合わせてレイアウトを変更する必要がある場合があります。

Device のローテーションとライフサイクルコールバックを調べる

  1. アプリをコンパイルして実行し、Logcat を開きます。

  2. rotation button でエミュレータを左右に回転させることができます。

  1. Logcat の出力を調べます。MainActivity で出力をフィルタリングします。

バイスまたはエミュレータが画面を回転させると、システムが全てのライフサイクルコールバックを呼び出して、Activity をシャットダウンすることに注意してください。次に、Activity が再作成されると、システムは全てのライフサイクルコールバックを呼び出して Activity を開始します。

  1. MainActivity で、onSaveInstanceState() メソッド全体をコメントアウトします。

  2. アプリをコンパイルして再度実行します。カップケーキを数回クリックし、デバイスまたはエミュレータを回転させます。今回は、デバイスを回転させて Activity をシャットダウンして再作成すると、 Activity はデフォルト値で起動します。

configuration の変更が発生すると、Android は、前のタスクで学習したのと同じインスタンス State バンドルを使用ステ、アプリの状態を保存および復元します。プロセスのシャットダウンと同様に、onSaveInstanceState() を使用してアプリのデータをバンドルに入れます。次に、デバイスが回転した場合に Activity state data が失われ内容に、onCreate() でデータを復元します。

  1. MainActivity で、onSaveInstanceState() メソッドのコメントを解除し、アプリを実行し、カップケーキをクリックして、アプリまたはデバイスを回転させます。今回は、デザートデータが Activity rotation 全体で保持されることに注目してください。

まとめ

Lifecycle tips

  • ライフサイクルコールバックで何かを設定または開始した場合は、対応するコールバックでそれらを停止または削除します。停止することで、不要になった時に実行を継続しないようにします。たとえば、onStart() でタイマーを設定した場合、onStop() でタイマーを一時停止または停止する必要があります。

  • onCreate() は、アプリが最初に起動した時に一回実行されるアプリの部分を初期化する場合にのみ使用してください。onStart() を使用して、アプリの起動時とアプリがフォアグランドに戻るたびの両方で実行されるアプリの部分を起動します。

Lifecycle library

  • Android ライフサイクルライブラリを使用して、ライフサイクル制御を Activity または Fragment から、ライフサイクル対応である必要がある実際のコンポーネントにシフトします。
  • Lifecycle owners は、Activity や Fragment などのライフサイクルを持つコンポーネントです。Lifecycle owners は、LifecycleOwner インターフェースを実装します。
  • Lifecycle observers は、現在の lifecycle state に注意をはらい、ライフサイクルが変更された時にタスクを実行します。Lifecycle observers は、LifecycleObserver インターフェースを実装します。
  • Lifecycle オブジェクトには実際の lifecycle state が含まれており、ライフサイクルが変更されるとイベントがトリガーされます。

ライフサイクル対応クラスを作成するには:

  • ライフサイクルをオブザーブする必要があるクラスに LifecycleObserver インターフェースを実装します。
  • Activity または Fragment のライフサイクルオブジェクトを使用して、lifecycle observer クラスを初期化します。
  • lifecycle observer クラスで、ライフサイクル対応メソッドに、関心のあるライフサイクル状態の変化をアノテーションします。

例えば、@OnLifecycleEvent(Lifecycle.Event.ON_START) アノテーションは、メソッドが onStart ライフサイクルイベントを監視していることを示します。

プロセスのシャットダウンと Activity Sate の保存

  • Android は、フォアグランドアプリが問題なく実行できるように、バックグランドで実行されるアプリを規制します。この規制には、バックグランドでアプリが実行できる処理の量を制限することや、アプリプロセス全体をシャットダウンすることも含まれます。
  • ユーザは、システムがバックグランドでアプリをシャットダウンしたかどうかを知ることができません。アプリは引き続き最近の画面に表示され、ユーザがアプリを離れた時と同じ状態で再起動する必要があります。
  • Android Debug Bridge(adb) は、コンピューターに接続されているエミュレータやデバイスに指示を送信できるコマンドラインツールです。adb を使用して、アプリのプロセスシャットダウンをシュミレートできます。
  • Android がアプリプロセスをシャットダウンすると、onDestroy() ライフサイクルメソッドは呼ばれず、アプリは停止します。

Activity と Fragment の状態を保持

  • アプリがバックグランドに移行すると、onStop() が呼び出された直後に、アプリのデータがバンドルに保存されます。EditText のコンテンツなど、一部のアプリデータは自動的に保存されます。
  • bundle は、キーと値のコレクションである Bundleインスタンス です。キーは常に文字列です。
  • onSaveInstanceState() コールバックを使用して、アプリが自動的にシャットダウンされた場合でも、保持するバンドルに他のデータを保存します。データをバンドルに入れるには、putInt() などの put で始まるバンドルメソッドを使用します。
  • onRestoreInstanceState() メソッド、またはより一般的な onCreate() で、バンドルからデータを取り戻すことができます。onCreate() メソッドには、バンドルを保持する savedInstanceState パラメーターがあります。
  • savedInstanceState 変数には null が含まれている場合、Activity は state bundle なしでえ開始され、取得する状態データはありません。
  • キーを使用してバンドルからデータを取得するには、getInt() などの get で始まる Bundle メソッドを使用します。

Configuration changes

  • Configuration の変更は、デバイスの状態が急激に変化した時に発生するため、システムが変更を解決する最も簡単な方法は、Activity をシャットダウンして再構築することです。
  • configuration 変更の最も一般的な例は、ユーザがデバイスを縦向きから横向きモードに、または横向きモードから縦向きモードに回転させる場合です。configuration の変更は、デバイスの言語が変更された時、またはハードウェアキーボードが接続された時にも発生する可能性があります。
  • configuration の変更が発生すると、Android は全ての Activity lifecycle のシャットダウンコールバックを呼び出します。次に、Android は Activity を最初から再開し、ライフサイクルの全ての起動コールバックを実行します。
  • configuration の変更が原因で Android がアプリをシャットダウンすると、onCreate() で使用できる state bundle を使用して Activity が再開されます。
  • プロセスのシャットダウンと同様に、アプリの状態を onSaveInstanceState() のバンドルに保存します。

その他の記事

yamato8010.hatenablog.com

yamato8010.hatenablog.com

yamato8010.hatenablog.com

Static Library vs Dynamic Library

今回は、Swift で使用されるライブラリの形式について調べたことをまとめていきたいと思います。

ライブラリの種類

今回は、ライブラリという言葉で FrameworkLibrary をまとめていますが、両者には違いがあるのでご注意ください。それでは、それぞれの形式について簡単に見ていきましょう。

Static Library(*.a)

コンパイル時にライブラリがリンクされ、実行可能ファイル自体にライブラリがコピーされます(ビルド時にはシンボルの解決がされる)。アプリケーションのサイズは増えますが、アプリの起動時間などを短縮することができます。

Dynamic Library(*.dylib)

ランタイム時に動的にライブラリがリンクされ(ビルド時にはシンボルの解決がされない)、実行可能ファイルにはライブラリがコピーされません。メリットとしては、アプリサイズが小さくなることや、アプリの更新をせずとも Dynamic Library の更新が可能なことです。

Framework(*.frmework)

iOS における Framework とライブラリとの大きな違いは、バンドルを保持していることです。

Bundle とは、ディスク上の Bundle ディレクトリにあるコードとリソースの保存領域のことで、Apple はこれを使用して、App、Framework、Plugin など多くのコンテンツを表現します。また、Bundle は含まれているリソースを明確に定義されたサブディレクトへと編成します。(例えば、Xcodeproject のファイルなどは一見するとファイルですが、実際はディレクトリ構造を持つ Bundle です。)

また、Framework は StaticLibrary を保持する StaticFramework と、DynamicLibrary を保持する DynamicLibrary に分けることができます。

Static or Dynamic?

file コマンドでライブラリの実行ファイルを指定してあげることで、framework が static・dynamic にリンクされるのを判断することができます。

下記の例だと、Framework は Dynamic にリンクされます。

$ file Instabug.framework/Instabug
Instabug.framework/Instabug: Mach-O universal binary with 4 architectures: [i386:Mach-O dynamically linked shared library i386] [x86_64] [arm_v7] [arm64]
Instabug.framework/Instabug (for architecture i386):    Mach-O dynamically linked shared library i386
Instabug.framework/Instabug (for architecture x86_64):  Mach-O 64-bit dynamically linked shared library x86_64
Instabug.framework/Instabug (for architecture armv7):   Mach-O dynamically linked shared library arm_v7
Instabug.framework/Instabug (for architecture arm64):   Mach-O 64-bit dynamically linked shared library arm64

参考

その他の記事

yamato8010.hatenablog.com

yamato8010.hatenablog.com

yamato8010.hatenablog.com

知識ゼロからの Kotlin Android アプリリリースへの軌跡 / Day13【Lifecycles and logging編】

学ぶこと

  • Log 情報を Logcat(Android console または Android monitor と呼ばれることもあります)に出力する方法。
  • ActivityFragment のライフサイクルの基本、および activity が状態間を移動する時に呼び出されるコールバック
  • ライフサイクルコールバックメソッドをオーバーライドして、Activity lifecycle の様々な時点で操作を実行する方法。
  • Timber ライブラリを使用してアプリにログインする方法。

すること

  • DessertClicker というスターターアプリを変更して、Logcat に表示されるログ情報を追加します。
  • ライフサイクルコールバックメソッドをオーバーライドし、Activity の変更をログに記録します。
  • アプリを実行し、Activity の開始、停止、再開時に表示されるログ情報を確認します。
  • Timber ライブラリを使用するようにアプリを変更します。
  • AndroidTrivia アプリにロギングを追加し、フラグメントの状態の変化を監視します。

アプリの概要

このコードラボでは、DessertClicker というスターターアプリを使用します。このアプリでは、ユーザが画面上のデザートをタップするたびに、アプリがユーザのためにデザートを購入します。アプリは、購入したデザートの数とユーザが費やした合計金額のレイアウトの値を更新します。

このアプリには、Android ライフサイクルに関連するいくつかのバグが含まれています。たとえば 、特定の状況では、アプリはデザートの値を0にリセットし、アプリがバックグランドにある時でもシステムリソースを使用し続けます。

ライフサイクルメソッドを探索し、基本的なロギングを追加する

全ての Activity と全ての Fragment には、ライフサイクルと呼ばれるものがあります。これは、この蝶のライフサイクルと同様に、動物のライフサイクルに対する暗示です。蝶の様々な状態は、誕生から完全に形成された成虫、死に至るまでの成長を示しています。

同様に、Activity のライフサイクルは、Activity が最初に初期化されてから最終的に破棄され、システムによってメモリが解放されるまで、Activity が通過する様々な状態で構成されます。ユーザがアプリを起動し、Activity 間を移動し、アプリの内外をナビゲートし、アプリを離れると、Activity の状態が変化します。以下の図は、Activity のライフサイクルの全ての状態を示しています。名前が示すように、これらの状態は Activity のステータスを表します。

多くの場合、動作を変更したり、Activity のライフサイクルの状態が変化した時にコードを実行したりします。したがって、Activity クラス自体、および AppCompatActivity などの Activity のサブクラスは、一連のライフサイクルコールバックメソッドを実装します。Android は、Activity がある状態から別の状態に移行した時にこれらのコールバックを呼び出します。独自の Activities でこれらのメソッドをオーバーライドして、ライフサイクルの状態の変化に応じてタスクを実行できます。次の図は、使用・オーバーライド可能なコールバックとともにライフサイクルの状態を示しています。

これらのコールバックがいつ呼び出され、各コールバックメソッドで何を実行するかを知ることが重要です。ただし、これらの図はどちらも複雑で、混乱を招く可能性があります。このコードラボでは、各状態とコールバックの意味を読み取るだけでなく、何が起こっているかを理解します。

onCreate() メソッドを調べてロギングを追加する

Android ライフサイクルで何が起こっているのかを理解するには、様々なライフサイクルメソッドがいつ呼び出されたかを知ることが役立ちます。これは、DessertClicker で問題が発生している場所を特定するのに役立ちます。

これを行う簡単な方法は、Android ロギング API を使用することです。ロギングを使用すると、アプリの実行中に短いメッセージをコンソールに書き込むことができ、それを使用して、様々なコールバックがトリガーされた時にそれらを表示できます。

  1. DesserClicker stater app をダウンロードして、AndroidStudio で開きます。

  2. アプリをコンパイルして実行し、デザートの写真を数回タップします。Desserts Sold の値と合計金額がどのように変化するかに注意してください。

  3. MainActivity.kt を開き、この Activity の onCreate() メソッドを調べます。

override fun onCreate(savedInstanceState: Bundle?) {
...
}

onCreate() はすべての Activity が実装する必要がある1つのメソッドです。onCreate() メソッドは、Activity の一回限りの初期化を行う場所です。たとえば、onCreate() では、レイアウトを Inflate したり、Click Listeners を定義したり、データバインディングを設定したりします。

onCreate() ライフサイクルメソッドは、Activity が初期化された直後(メモリ内に新しい Activity オブジェクトが作成された時)に一回呼び出されます。onCreate() の実行後、Activity は作成されたとみなされます。

Note:

onCreate() メソッドはオーバーライドであるため、その中ですぐに super.onCreate() を呼び出す必要があります。同じことが他のライフサイクルメソッドにも当てはまります。

  1. onCreate() メソッドで、super.onCreate() の呼び出しの直後に、次の行を追加します。必要に応じて Log クラスをインポートします。
Log.i("MainActivity", "onCreate Called")

Log クラスは、Logcat にメッセージを書き込みます。このコマンドには3つの部分があります。

  • severity ログメッセージ、つまりメッセージの重要度。Log.i()) メソッドは infomation message を書き込みます。Log クラスの他のメソッドには、エラーの場合は Log.e())、警告の場合は [Log.w(https://developer.android.com/reference/kotlin/android/util/Log#w(kotlin.String,%20kotlin.String))] などがあります。

  • ログ tag は、今回の場合だと "MainActivity" です。タグは、LogCat でログメッセージをより簡単に検索できる文字列です。タグは通常、クラスの名前です。

  • 実際のログメッセージは、今回の場合 "onCreate called" です。

  • DessertClicker アプリをコンパイルして実行します。デザートをタップしても、アプリの動作に違いはありません。 Android Studio の画面下部にある Logcat タブをクリックします。

Logcat は、メッセージをログに記録するためのコンソールです、アプリに関する Android からのメッセージがここに表示されます。これには、Log.i() メソッドまたは、他の Log クラスメソッドを使用して明示的にログに送信したメッセージが含まれます。

  1. Logcat ペインで、検索フィールドに I/MainActivity と入力します。

Logcat には多くのメッセージが含まれる場合があり、そのほとんどは役立ちません。Logcat エントリは様々な方法でフィルタリングできますが、検索が最も簡単です。MainActivity をコードのログタグとして使用していたため、そのタグを使用してログをフィルター処理できます。先頭に I/ を追加すると、information message を意味し、これは Log.i() のよって作成されます。

ログメッセージには、日付、時刻、パッケージの名前(com.example.android.dessertclicker)、ログタグ、および実際のメッセージが含まれています。このメッセージはログに表示されるため、onCreate() が実行されたことが分かります。

onStart() メソッドを実装する

onStart() ライフサイクルメソッドは、onCreate() の直後に呼び出されます。onStart() の実行後、Activity が画面に表示されます。Activity を初期化するために一回だけ呼び出される onCreate() とは異なり、onStart() は Activity のライフサイクルで何度でも呼び出すことができます。

onStart() は、対応する onStop() ライフサイクルメソッドとペアになっていることに注意してください。ユーザがアプリを起動してからデバイスのホーム画面に戻ると、Activity は停止し、画面に表示されなくなります。

  1. Android StudioMainActivity.kt を開いた状態で、Code > Override Methods を選択するか、Control + o を押します。このクラスでオーバーライドできる全てのメソッドのリストを含むダイアログが表示されます。

  1. onStart と入力して、適切なメソッドを検索します。リストから onStart() を選択し、OK をクリックしてボイラープレートオーバーライドコードを挿入します。コードは次のようになります。
override fun onStart() {
   super.onStart()
}

Tip: Android Studio は、オーバーライドされたメソッドコードを、クラス内の次に利用可能な適切な場所に挿入します。ライフサイクルオーバーライドを特定の場所(クラスの最後など)に配置する場合は、オーバーライドメソッドを使用する前に挿入ポイントを設定します。

  1. onStart() メソッド内に、ログメッセージを追加します:
override fun onStart() {
   super.onStart()

   Log.i("MainActivity", "onStart Called")
}
  1. DessertClicker アプリをコンパイルして実行し、Logcat ペインを開きます。ログをフィルタリングするには、検索フィールドに I/MainActivity と入力します。onCreate() メソッドと onStart() メソッドの両方が順番に呼び出され、Activity が画面に表示されていることに注意してください。

  2. バイスのホームボタンを押してから、最近の画面を使用して Activity に戻ります。Activity は中断したところから再開され、全ての同じ値で、onStart() が再度 Logcat に記録されます。通常、onCreate() メソッドが再度呼び出されないことにも注意してください。

Note: ライフサイクルのコールバックをデバイスで観察していると、デバイスを回転させた時に異常な動作が発生する場合があります。この動作については、次のコードラボで学習します。

ログに Timber を使用する

このタスクでは、Timber と呼ばれる一般的な logging library を使用するようにアプリを変更します。Timber には、組み込みの Android Log クラスに比べていくつかの利点があります。特に、Timber ライブラリ:

  • クラス名に基づいて log tag を生成します。

  • Android アプリのリリースバージョンでログが表示されないようにします。

  • crash-reporting ライブラリとの統合を可能にします。

Gradle に Timber を追加する

  1. GithubTimber project にアクセスし、implementation という単語で始まる Download 見出しの下のコード行をコピーしてください。コードの行は次のようになりますが、バージョン番号は異なる場合があります。
implementation 'com.jakewharton.timber:timber:4.7.1'
  1. Gradle Scripts を開いて、build.gradle(Module:app) ファイルを開きます。

  2. dependencies セクション内に、コピーしたコード行を貼り付けます。

dependencies {
   ...
   implementation 'com.jakewharton.timber:timber:4.7.1'
}
  1. Android Studio の右上にある Sync Now をクリックして、Gradle を再構築します。ビルドはエラーなしで実行されます。

Application クラスを作成して Timber を初期化する

このステップでは、アプリケーションクラスを作成します。アプリケーションは、アプリケーション全体のグローバルアプリケーション状態を含む基本クラスです。また、オペレーティングシステムがアプリとの対話に使用する主要なオブジェクトでもあります。指定しない場合、Android が使用するデフォルトの Application クラスがあるため、特別なことを行わなくても、アプリケーション用に作成された Application オブジェクトが常に存在します。

TimberApplication クラスを使用します。これは、アプリ全体がこのロギングライブラリを使用するため、他の全てを設定する前に、ライブラリを一同初期化する必要があるためです。このような場合、Application クラスをサブクラス化して、独自のカスタム実装でデフォルトをオーバーライドできます。

Warning: クラスは全ての Activity の前に作成され、グローバルな状態を保持できるため、Application クラスに独自のコードを追加するのは魅力的です。しかし、グローバルに利用できる読み取りおよび書き込み可能な静的変数を作成するのがエラーが発生しやすいのと同様に、Application クラスを悪用するのは簡単です。コードが本当に必要でない限り、Application クラスに Activity コードを配置しないでください。

Application クラスを作成したら、Android マニフェストでクラスを指定する必要があります。

  1. dessertclicker パッケージで、ClickerApplication という新しい Kotlin クラスを作成します。これを行うには、app > java を展開し、com.example.android.dessertclicker を右クリックします。New > Kotlin File/Class を選択します。

  2. クラスに ClickerApplication という名前をつけ、Kind Class に設定します。OK をクリックします。

Android Studio は新しい ClickerApplication クラスを作成し、コードエディターで開きます。コードは次のようになります。

package com.example.android.dessertclicker

class ClickerApplication {
}
  1. クラス定義を Application のサブラクスに変更し、必要に応じて Application クラスをインポートします。
class ClickerApplication : Application() { }
  1. onCreate() メソッドをオーバーライドするには、Code > Override Methods を選択するか、Control + o を押します。
class ClickerApplication : Application() {
   override fun onCreate() {
       super.onCreate()
   }
}
  1. onCreate() メソッド内で、Timber ライブラリを初期化します。
override fun onCreate() {
    super.onCreate()

    Timber.plant(Timber.DebugTree())
}

このコード行は、アプリの Timber ライブラリを初期化して、Activity でライブラリを使用できるようにします。

  1. AndroidManifest.xml を開きます。

  2. <application> 要素の上に ClickerApplication クラスの新しい attributes を追加します。これにより、Android がデフォルトの Application クラスではなく、ClickerApplication クラスを使用することが認識されます。

<application
   android:name=".ClickerApplication"
...

Note: Application クラスを Android マニフェストに追加しない場合、アプリはエラーなしで実行されます。ただし、アプリは作成したクラスを使用しないため、Timber からのログ情報は表示されません。

Timber log ステートメントを追加する

このステップでは、Timber を使用するように Log.i() 呼び出しを変更してから、他の全てのライフサイクルメソッドのロギングを実装します。

  1. MainActivity を開き、onCreate() までスクロールします。Log.i()Timber.i() に置き換え、ログタグを削除します。
Timber.i("onCreate called")

Log クラスと同様に、Timber も informational messages に i() メソッドを使用します。Timber を使用すると、ログタグを追加する必要がないことに注意してください。Timber は自動的にクラスの名前をログタグとして使用します。

  1. 同様に、onStart()Log 呼び出しを変更します。
override fun onStart() {
   super.onStart()

   Timber.i("onStart Called")
}
  1. DessertClicker アプリをコンパイルして実行し、Logcat を開きます。onCreate()onStart() で同じログメッセージが表示されていることに注目してください。

  2. MainActivity の残りのライフサイクルメソッドをオーバーライドし、それぞれに Timber ログステートメントを追加します。下記がコードになります。

override fun onResume() {
   super.onResume()
   Timber.i("onResume Called")
}

override fun onPause() {
   super.onPause()
   Timber.i("onPause Called")
}

override fun onStop() {
   super.onStop()
   Timber.i("onStop Called")
}

override fun onDestroy() {
   super.onDestroy()
   Timber.i("onDestroy Called")
}

override fun onRestart() {
   super.onRestart()
   Timber.i("onRestart Called")
}
  1. DessertClicker をコンパイルして再度実行し、Logcat を調べます。今回は、onCreate()onStart() に加えて、onResume() ライフサイクコールバックのログメッセージがあることに注意してください。

Activity が最初から開始すると、次の3つのライフサイクルコールバック全てが順番に呼び出されることが分かります:

  • onCreate() アプリを作成
  • onStart() 起動して画面に表示
  • onResume() Activity がフォーカスを与え、ユーザが操作できるようになる

onResume() メソッドは、再開するものがない場合でも、起動時に呼び出されます。

ライフサイクルのユースケースを調べる

DessertClicker アプリがロギング用に設定されたので、さまざまな方法でアプリの使用を開始する準備が整い、これらの用途に応じてライフサイクルコールバックがどのようにトリガーされるかを調べる準備ができました。

Activity の開閉

最も基本的なユースケースから始めます。つまり、アプリを初めて起動し、アプリを完全に閉じます。

  1. DessertClicker アプリがまだ実行されていない場合は、コンパイルして実行します。ご覧のとおり、onCreate()onStart()onResume() コールバックは、Activity が初めて開始される時に呼び出されます。

  1. カップケーキを数回タップします。

  2. バイスの Back button をタップします。Logcat で、onPause()onStop()onDestroy() の順序で呼ばれることに注目してください。

この場合、Back button を使用すると、Activity が完全に閉じます。onDestroy() メソッドの実行は、Activity が完全にシャットダウンされ、ガベージコレクションできることを意味します。Garbage collection) とは、今後使用しないオブジェクトの自動クリーンアップを指します。onDestroy() が呼びだされた後、OS はそれらのリソースが破棄可能であることを認識し、そのメモリのクリーンアップを開始します。

コードが手動で Activity の finish()) メソッドを呼び出した場合、またはユーザーがアプリを強制終了した場合も、Activity が完全にシャットダウンされることがあります。また、アプリが画面に長時間表示されていない場合、Android システムが自動的に Activity をシャットダウンする場合もあります。Android はこれをすることで、バッテリーを節約し、アプリのリソースを他のアプリで使用できるようにします。

  1. 最近の画面を使用してアプリに戻ります。Logcat は次のとりおりです:

Activity は前のステップで破棄されたため、アプリに戻ると、Android は新しい Activity を開始し、onCreate()onStart()onResume() メソッドを呼び出します。前の Activity の DessertClicker の統計が保持されていないことに注意してください。

ここで重要なのは、onCreate()onDestroy() が単一の Activity インスタンスの有効期間中に一度だけ呼び出されるということです。onCreate() はアプリを初めて初期化し、onDestroy() はアプリが使用するリソースをクリーンアップします。

onCreate() メソッドは重要なステップです。これは、最初に全ての初期化が行われる場所・最初にレイアウトをインフレートして設定する場所・および変数を初期化する場所です。

Activity から離れて Activity に戻る

アプリを起動して完全に閉じたので、Activity が初めて作成された時のほとんどのライフサイクル状態を確認しました。また、Activity が完全にシャットダウンして破壊された時に、Activity が通過する全てのライフサイクル状態を確認しました。しかし、ユーザは Android 搭載デバイスを操作する時に、アプリを切り替えたり、Home に戻ったり、新しいアプリを起動したり、電話などの他の Activity による割り込みを処理したりします。

ユーザがその Activity から離れるたびに、Activity が完全に終了するわけではありません:

  • Activity が画面に表示されなくなった場合、これは Activity を background に配置すると呼ばれます。(この反対は、Activity が foreground または画面上にある場合です。)
  • ユーザがアプリに戻ると、同じ Activity が再開され、再び表示されます。ライフサイクルのこの部分は、visible lifecycle と呼ばれます。

アプリが background にある時は、システムリソースとバッテリー寿命を維持するために、アプリを Active に実行しないでください。Activity のライフサイクルとそのコールバックを使用して、アプリがバックグランドに移行するタイミングを把握し、進行中の操作を一時停止できるようにします。次にアプリが foreground になった時に操作を再開します。

例えば、物理シュミレーションを実行するアプリを考えてみましょう。シュミレーションないの全てのオブジェクトを配置する場所を決定し、それを表示するには、デバイスの CPU でクランチされた多くの計算が必要です。

これにはパフォーマンス上の理由もあります。ユーザが、CPU 集中型の物理シュミレーションを使用する20個のアプリを開いたとしましょう。それらのアプリの Activity が画面上にない場合でも、background で大量のレンダリング計算を実行していると、Device 全体のパフォーマンスが低下します。

このステップでは、アプリが background に入り、再び foreground に戻る時の Activity のライフサイクルを確認します。

  1. DessertClicker アプリが実行されている状態で、カップケーキを数回クリックします。

  2. バイスのホームボタンを押して、Android Studio の Logcat を観察します。ホーム画面に戻ると、アプリが完全にシャットダウンされるのではなく、アプリがバッググランドになります。onPause() メソッドと onStop() メソッドは呼び出されますが、onDestroy() は呼び出されないことに注意してください。

onPause() が呼び出されると、アプリはフォーカスを失います。onStop() の後、アプリは画面に表示されなくなります。Activity は停止していますが、Activity オブジェクトはまだバックグランドでメモリ内にあります。Activity は破棄されていません。ユーザがアプリに戻る可能性があるため、Android は Activitu Resources を保持します。

  1. 最近の画面を使用してアプリに戻ります。Logcat で、Activity が onRestart() および onStart() で再開され、onResume() で再開されることに注目してください。

Activity が Foreground に戻ると、onCreate() メソッドは呼び出されません。Activity オブジェクトは破棄されなかったため、再度作成する必要はありません。onCreate() の代わりに、onRestart() が呼び出されます。今回は、Activity が Foreground に戻った時に、デザートの販売数が保持されていることに注目してください。

  1. DessertClicker 以外のアプリを少なくとも1つ起動して、デバイスの最近の画面にいくつかのアプリがあるようにします。

  2. 最近の画面を表示して、別の最近の Activity を開きます。次に、最近のアプリに戻り、DessertClicker を Foreground に戻します。

ここでは、ホームボタンを押した時に同じコールバックが Logcat に表示されます。onPause()onStop() は、アプリがバックグランドになると呼び出され、onRestart()onStart()onResume() が呼び出されます。

ここでの重要な点は、ユーザが Activity に出入する時に onStart() および、onStop() が複数回呼び出されることです。これらのメソッドをオーバーライドして、アプリがバックグランドに移動した時に停止するか、Foreground に戻った時に再起動する必要があります。

では、onRestart() はどうでしょうか? onRestart() メソッドは onCreate() によく似ています。Activity が表示される前に onCreate() または onRestart() が呼び出されます。onCreate() は最初にのみ呼び出され、その後 onRestart() が呼び出されます。onRestart() メソッドは、Activity が初めて開始されていない場合にのみ呼び出すコードを配置する場所です。

Activity を部分的に隠す

アプリを起動して onStart() を呼び出すと、アプリが画面に表示され、onResume() が呼び出されるようになることを学びました。アプリが再開され、onResume() が呼び出されると、アプリはユーザフォーカスを取得します。アプリが完全に画面上にあり、ユーザフォーカスがあるライフサイクルの部分は、interactive lifecycle と呼ばれます。

アプリがバックグランドになると、onPause() の後で、フォーカスが失われ、onStop() の後でアプリが表示されなくなります。

フォーカスと可視性の違いは重要です。Activity が画面上に部分的に表示されていても、ユーザフォーカスがない場合があるためです。このステップでは、Activity が部分的に表示されているが、ユーザにフォーカスがない1つのケースを調べます。

  1. DessertClicker アプリが実行されている状態で、画面の右上にある Share ボタンをクリックします。

Share Activity は画面の下半分に表示されますが、Activity はまだ上半分に表示されています。

  1. Logcat を調べて、onPause() のみが呼び出されたことに注目してください。

このユースケースでは、Activity がまだ部分的に表示されているため、onStop() は呼び出されません。ただし、Activity にはユーザフォーカスがなく、ユーザは Activity を操作できません。Foreground にある "share" activity は、ユーザフォーカスを保持しています。

この違いが重要なのはなぜでしょう?物理アプリを考えてみましょう。アプリがバックグランドにある時にシュミレーションを停止し、アプリが部分的に隠されている時に実行を継続したい場合があります。この場合、onStop() でシュミレーションが停止します。Activity が部分的に隠されている時にシュミレーションを停止する場合は、シュミレーションを停止するコードを onPause() に配置します。

onPause() で実行されるコードは全て、他のものの表示をブロックするため、pnPause() のコードは軽量にする必要があります。たとえば、電話がかかってきた場合、onPause() のコードによって着信通知が遅れる可能性があります。

  1. share dialog の外側をクリックしてアプリに戻り、onResume() が呼び出されていることを確認します。

onResume()onPause() はどちらもフォーカスと関係があります。onResume() メソッドは、Activity にフォーカスがある時に呼び出され、onPause() は、Activity がフォーカスを失った時に呼び出されます。

Fragment ライフサイクルをを調べる

Android fragment のライフサイクルは、Activity のライフサイクルと似ていますが、Fragment 固有の方法がいくつかあります。

このタスクでは、以前のコードラボで構築した AndroidTrivia アプリを確認し、ログを追加して Fragment のライフサイクルを調査します。

AndroidTrivia アプリの各画面は Fragment です。

シンプルに調査するために、このタスクでは Timber ライブラリではなく Android ロギング API を使用します。

  1. AndroidTrivia アプリを開きます。

  2. TitleFragment.kt ファイルを開きます。AndroidStudio では、アプリを rebuild するまで、バインドエラーと未解決の参照エラーが表示される場合があることに注意してください。

  3. onCreateView() メソッドまでスクロールします。ここで Fragment のレイアウトが inflate され、data binding が発生することに注意してください。

  4. setHasOptionsMenu() の行と最後の呼び出しの間に、onCreateView() メソッドにロギングステートメントを追加します。

setHasOptionsMenu(true)

Log.i("TitleFragment", "onCreateView called")

return binding.root
  1. onCreateView() メソッドの下に、残りの Fragment ライフサイクルメソッドごとにロギングステートメントを追加します。下記がコードになります:
override fun onAttach(context: Context) {
   super.onAttach(context)
   Log.i("TitleFragment", "onAttach called")
}
override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   Log.i("TitleFragment", "onCreate called")
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    Log.i("TitleFragment", "onViewCreated called")
}

override fun onStart() {
   super.onStart()
   Log.i("TitleFragment", "onStart called")
}
override fun onResume() {
   super.onResume()
   Log.i("TitleFragment", "onResume called")
}
override fun onPause() {
   super.onPause()
   Log.i("TitleFragment", "onPause called")
}
override fun onStop() {
   super.onStop()
   Log.i("TitleFragment", "onStop called")
}
override fun onDestroyView() {
   super.onDestroyView()
   Log.i("TitleFragment", "onDestroyView called")
}
override fun onDetach() {
   super.onDetach()
   Log.i("TitleFragment", "onDetach called")
}
  1. アプリをコンパイルして実行し、Logcat を開きます。

  2. 検索フィールドに I/TitleFragment と入力して、ログをフィルタリングします。アプリが起動すると、Logcat は次のようになります。

21933-21933/com.example.android.navigation I/TitleFragment: onAttach called
21933-21933/com.example.android.navigation I/TitleFragment: onCreate called
21933-21933/com.example.android.navigation I/TitleFragment: onCreateView called
21933-21933/com.example.android.navigation I/TitleFragment: onViewCreated called
21933-21933/com.example.android.navigation I/TitleFragment: onStart called
21933-21933/com.example.android.navigation I/TitleFragment: onResume called

ここでは、これらのコールバックを含む、Fragment の起動ライフサイクル全体を確認できます。

  • onAttach(): Fragment がその所有者 Activity に関連付け羅れている時に呼び出されます。
  • onCreate(): Activity の onCreate() と同様に、(レイアウト以外の)最初の Fragment 作成を行うために呼び出されます。
  • onCreateView(): Fragment のレイアウトを Inflate するために呼び出されます。
  • onViewCreated(): onCreateView() が戻った直後に呼び出されます。ただし、保存された状態が view に復元される前に呼び出されます。
  • onStart(): Activity の onStart() と並行し、Fragment が表示された時に呼び出されます。
  • onResume(): Activity の onResume() と並行して、Fragment がユーザフォーカスを取得した時に呼び出されます。

  • Play ボタンをタップしてクイズゲームに進み、Logcat に注目してください。

21933-21933/com.example.android.navigation I/TitleFragment: onAttach called
21933-21933/com.example.android.navigation I/TitleFragment: onCreate called
21933-21933/com.example.android.navigation I/TitleFragment: onCreateView called
21933-21933/com.example.android.navigation I/TitleFragment: onViewCreated called
21933-21933/com.example.android.navigation I/TitleFragment: onStart called
21933-21933/com.example.android.navigation I/TitleFragment: onResume called
21933-21933/com.example.android.navigation I/TitleFragment: onPause called
21933-21933/com.example.android.navigation I/TitleFragment: onStop called
21933-21933/com.example.android.navigation I/TitleFragment: onDestroyView called

次の Fragment を開くと、タイトル Fragment が閉じ、次のライフサイクルメソッドが呼び出されます:

  • onPause(): フラグメントがユーザフォーカスを失った時に呼び出されます。Activity の onPause() と並行しています。

  • onStop(): Fragment が画面に表示されなくなった時に呼び出されます。Activity の onStop() と並行しています。

  • onDestroyView(): Fragment の View が不要になった時に呼び出され、その View に関連づけられているリソースをクリーンアップします。

  • アプリで、画面左上の Up button をタップして、Title Fragment に戻ります。

21933-21933/com.example.android.navigation I/TitleFragment: onPause called
21933-21933/com.example.android.navigation I/TitleFragment: onStop called
21933-21933/com.example.android.navigation I/TitleFragment: onDestroyView called
21933-21933/com.example.android.navigation I/TitleFragment: onCreateView called
21933-21933/com.example.android.navigation I/TitleFragment: onViewCreated called
21933-21933/com.example.android.navigation I/TitleFragment: onStart called
21933-21933/com.example.android.navigation I/TitleFragment: onResume called

今回は、Fragment を開始するための onAttach()onCreate() が呼び出されない可能性があります。Fragment オブジェクトはまだ存在し、その所有者 Activity にアタッチされているため、ライフサイクルは onCreateView() で再開されます。

  1. バイスのホームボタンを押します。Logcat で、onPause()onStop() のみが呼び出されることに注意してください。これは Activity の場合と同じ動作です。ホームに戻ると、Activity と Fragment が Background に配置されます。

  2. 最近の画面を使用してアプリに戻ります。Activity で発生したのと同じように、onStart()onResume() メソッドが呼び出され、Fragment が Foreground に返されます。

まとめ

Activity lifecycle

  • Activity lifecycle は、Activity が移行する一連の状態です。Activity のライフサイクルは、Activity が最初に作成された時に始まり、Activity が破棄された時に終了します。

  • ユーザが Activity 間やアプリの内部と外部を移動すると、各 Activity は Activity lifecycle の状態間を移動します。

  • Activity lifecycle の各状態には、 Activity のクラスでオーバーライドできるコールバックメソッドがあります。7つのライフサイクル methods があります。 onCreate()) onStart()) onPause()) onRestart()) onResume()) onStop()) onDestroy())

  • Activity が lifecycle state に移行する時に発生する動作を追加するには、state's のコールバックメソッドをオーバーライドします。

  • Android Studio のクラスにスケルトンオーバーライドメソッドを追加するには、Code > Override Methods を選択するか、Control + o を押します。

Log を使用してロギング

  • Android logging API、特に Log クラスを使用すると、AndroidStudio 内の Logcat に表示されるショートメッセージを作成できます。
  • Log.i() を使用して、information message を記述します。このメソッドは2つの引数をとります。ログタグとログメッセージです。
  • Android StudioLogcat ペインを使用して、書き込んだメッセージを含むシステムログを表示します。

Timber を使用してロギング

Timber は、Android ロギング API に比べていくつかの利点があるロギングライブラリです。特に、Timber ライブラリ:

  • クラス名に基づいてログタグを生成します。
  • Android アプリのリリースバージョンでログが表示されないようにするのに役立ちます。
  • クラッシュレポートライブラリとの統合を可能にします。

Timber を使用するには、その依存関係を Gradle ファイルに追加し、Application クラスを拡張して初期化します。

  • Application は、アプリ全体のグローバルアプリケーション状態を含む基本クラスです。指定しない場合に、Android で使用されるデフォルトの Application クラスがあります。独自の Application サブクラスを作成して、Timber などのアプリ全体のライブラリを初期化できます。

  • Android manifest の <application> 要素に android:name attribute を追加して、カスタム Application クラスをアプリに追加します。これを行うことを忘れないでください。

  • Timber.i() を使用して、Timber でログメッセージを書き込みます。このメソッドは、書き込むメッセージという1つの引数のみとります。ログタグは自動的にクラスの名前が割り当てられます。

その他の記事

yamato8010.hatenablog.com

yamato8010.hatenablog.com

yamato8010.hatenablog.com

Swift で文字列に含まれている URL を抽出しよう

今回は、Swift で文字列に含まれている URL を抽出する方法を簡単にまとめていこかと思います🙃

結論

下記のように NSDataDetector というクラスを使用して、テキストからリンクを抽出することができます。

func getLinkTextList(text: String) -> [String] {
    guard let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) else {
        return []
    }
    let enableLinkTuples = detector.matches(in: text, range: NSRange(location: 0, length: text.count))
    return enableLinkTuples.map { checkingResult -> String in
        return (text as NSString).substring(with: checkingResult.range)
    }
}

let linkString = getLinkTextList(text: "僕の記事はこちら👉 https://yamato8010.hatenablog.com/").first!

print(linkString) // 出力: https://yamato8010.hatenablog.com

NSDataDetector とは?

自然言語テキストに対して、事前定義されたデータパターンに一致する特殊な正規表現を検出するためのオブジェクトで、日付・住所・リンク・電話番号・通過情報などを照合することができます。

一致するコンテンツの結果は、NSTextCheckingResult として返されます。ただし、NSDataDetector によって返される NSTextCheckingResult オブジェクトは、NSDataDetector が継承している NSRegularExpression によって返されるオブジェクトとは異なるようで、NSDataDetector は、返される結果のタイプに対応するプロパティがあるそうです。

基本的には、NSRegularExpression の上位互換なので、パターンマッチングしたい時は、NSDataDetector を使用する。という感じで大丈夫そうです。

NSTextCheckingResult.CheckingType の種類

iOS13 現在では、下記のようなタイプを NSDataDetector の初期化時に(NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)) 使用することができます。

type 内容
orthography 言語を特定します。結果は、orthography でアクセスでき、NSOrthography の値が渡されます。
spelling スペルのチェックをします。
grammar 文法のチェックをします。
date 日付を見つけます。結果は、date プロパティでアクセスすることができます。
address アドレスを見つけます。
url リンクを見つけます。
quote 引用符をスマート符に置き換えます。
dash dash を em-dashes に置き換えます。
replacement (c) などの文字を適切な記号に置き換えます。(この場合は、©︎)
correction スペルミスのある単語に対して自動修正を実行します。
regularExpression 正規表現と照合します。
phoneNumber 電話番号と照合します。
transitInformation フライト情報など、トランジット情報と照合します。

その他の記事

yamato8010.hatenablog.com

yamato8010.hatenablog.com

yamato8010.hatenablog.com

Carthage Command リスト(v0.34.0)

ライブラリ依存管理ツールである Carthage のコマンド一覧をまとめていこうと思います。リストは v.0.34.0 のものなので、最新バージョンの方は公式の Repo を確認してみてください。

github.com

コマンド一覧

コマンド 内容
archive Framework の zip を生成します。ビルド済みバイナリとして提供したい場合などに利用します。
bootstrap 依存関係をチェックアウトしてビルドします。bootstrapCartfile.resolved を見てライブラリをチェックアウト・ビルドするので、ライブラリを更新したくない時などに使用する。
build 依存関係をチェックアウトからビルドします。update/bootstrap はこの build が自動で走ります。
checkout プロジェクトの依存関係をチェックアウトします。
copy-frameworks アプリバンドルへのコピーと、ipa から x86_64i386 の除去を行います(シュミレータ用のバイナリを省く)。
fetch Gihリポジトリから Clone・Fetch を行います。
outdated プロジェクトの依存関係にアップデートがあるかを確認します。
update プロジェクトの依存関係をアップデートし、ビルドし直します。
validate Cartfile.resolved のバージョンが Cartfile 要件と互換性があることを確認します。
version Carthage のバージョンを表示します。

参考

その他の記事

yamato8010.hatenablog.com

yamato8010.hatenablog.com

yamato8010.hatenablog.com