アプリのパフォーマンスを向上させるためのiOSパフォーマンスの秘訣
公開: 2022-03-10最新のiOSハードウェアは、多くの集中的で複雑なタスクを処理するのに十分強力ですが、アプリのパフォーマンスに注意しないと、デバイスが応答しなくなる可能性があります。 この記事では、アプリの応答性を高めるための5つの最適化の秘訣について説明します。
1.再利用可能なセルをデキューします
以前にtableView(_:cellForRowAt:)
:)内でtableView.dequeueReusableCell tableView.dequeueReusableCell(withIdentifier:for:)
を使用したことがあるでしょう。 セルの配列を渡すだけでなく、なぜこの厄介なAPIに従わなければならないのか疑問に思ったことはありませんか? これの推論を見てみましょう。
1000行のテーブルビューがあるとします。 再利用可能なセルを使用しない場合は、次のように、行ごとに新しいセルを作成する必要があります。
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { // Create a new cell whenever cellForRowAt is called. let cell = UITableViewCell() cell.textLabel?.text = "Cell \(indexPath.row)" return cell }
ご想像のとおり、これにより、一番下までスクロールすると、デバイスのメモリに1,000個のセルが追加されます。 各セルにUIImageView
と大量のテキストが含まれているとどうなるか想像してみてください。一度にすべてを読み込むと、アプリのメモリが不足する可能性があります。 それとは別に、すべてのセルには、スクロール中に新しいメモリを割り当てる必要があります。 テーブルビューをすばやくスクロールすると、メモリの小さなチャンクがその場で割り当てられ、このプロセスによってUIがぎこちなくなります。
これを解決するために、AppleはdequeueReusableCell(withIdentifier:for:)
メソッドを提供しています。 セルの再利用は、画面に表示されなくなったセルをキューに入れることで機能します。新しいセルが画面に表示されようとすると(たとえば、ユーザーが下にスクロールすると、下の次のセル)、テーブルビューが表示されます。このキューからセルを取得し、 cellForRowAt indexPath:
メソッドで変更します。
キューを使用してセルを格納することにより、テーブルビューで1,000個のセルを作成する必要がなくなります。 代わりに、テーブルビューの領域をカバーするのに十分なセルが必要です。
dequeueReusableCell
を使用することで、アプリが使用するメモリを削減し、メモリが不足しにくくすることができます。
2.初期画面のように見える起動画面を使用する
Appleのヒューマンインターフェイスガイドライン(HIG)で述べられているように、起動画面を使用して、アプリの応答性の認識を高めることができます。
「これは、アプリをすばやく起動してすぐに使用できるようにすることだけを目的としています。 すべてのアプリは起動画面を提供する必要があります。」
起動画面をスプラッシュ画面として使用してブランドを表示したり、読み込み中のアニメーションを追加したりするのはよくある間違いです。 Appleが述べているように、起動画面をアプリの最初の画面と同じになるように設計します。
「アプリの最初の画面とほぼ同じ起動画面を設計します。 アプリの起動が終了したときに外観が異なる要素を含めると、起動画面とアプリの最初の画面の間で不快なフラッシュが発生する可能性があります。
「起動画面はブランディングの機会ではありません。 スプラッシュ画面や「バージョン情報」ウィンドウのようなエントリエクスペリエンスを設計しないでください。 アプリの最初の画面の静的な部分でない限り、ロゴやその他のブランド要素を含めないでください。」
読み込みやブランディングの目的で起動画面を使用すると、最初の使用時間が遅くなり、ユーザーにアプリの動作が遅いと感じさせる可能性があります。
新しいiOSプロジェクトを開始すると、空白のLaunchScreen.storyboard
が作成されます。 この画面は、アプリがViewControllerとレイアウトを読み込んでいる間ユーザーに表示されます。
アプリの動作を高速化するために、ユーザーに表示される最初の画面(ビューコントローラー)と同様の起動画面を設計できます。
たとえば、Safariアプリの起動画面は最初のビューに似ています。
起動画面のストーリーボードは、UIViewController、UITabBarController、UINavigationControllerなどの標準のUIKitクラスのみを使用できることを除いて、他のストーリーボードファイルと同じです。 他のカスタムサブクラス(UserViewControllerなど)を使用しようとすると、Xcodeはカスタムクラス名の使用が禁止されていることを通知します。
もう1つの注意点は、iOSが起動画面のストーリーボードから静止画像を生成してユーザーに表示するため、起動画面に配置されたときにUIActivityIndicatorView
がアニメーション化されないことです。 (これは、 01:21:56
のプレゼンテーション「PlatformsStateof the Union」、01:21:56頃に簡単に言及されています。)
AppleのHIGは、起動画面が静的であり、異なる言語に対応するためにテキストをローカライズできないため、起動画面にテキストを含めないようにアドバイスしています。
おすすめの読み物:顔認識機能を備えたモバイルアプリ:それを現実のものにする方法
3.ビューコントローラの状態の復元
状態の保存と復元により、ユーザーはアプリを終了する直前からまったく同じUI状態に戻ることができます。 場合によっては、メモリ不足のために、アプリがバックグラウンドにあるときにオペレーティングシステムがアプリをメモリから削除する必要があり、アプリが保存されていない場合、アプリは最後のUI状態を追跡できなくなり、ユーザーが作業を失う可能性があります。進行中!
マルチタスク画面では、バックグラウンドに置かれているアプリのリストを見ることができます。 これらのアプリはまだバックグラウンドで実行されていると思われるかもしれません。 実際には、これらのアプリの一部は、メモリの需要のためにシステムによって強制終了され、再起動される可能性があります。 マルチタスクビューに表示されるアプリのスナップショットは、実際には、アプリを終了したとき(つまり、ホーム画面またはマルチタスク画面に移動するとき)にシステムによって取得されたスクリーンショットです。
iOSはこれらのスクリーンショットを使用して、アプリがまだ実行中またはこの特定のビューを表示しているように見せかけますが、アプリは同じスクリーンショットを表示している間にバックグラウンドですでに終了または再起動されている可能性があります。
マルチタスク画面からアプリを再開したときに、アプリがマルチタスクビューに表示されるスナップショットとは異なるユーザーインターフェイスを表示することを経験したことがありますか? これは、アプリが状態復元メカニズムを実装しておらず、アプリがバックグラウンドで強制終了されたときに表示されたデータが失われたためです。 ユーザーはアプリがアプリを離れたときと同じ状態になることを期待しているため、これは悪いエクスペリエンスにつながる可能性があります。
Appleの記事から:
「彼らはあなたのアプリが彼らがそれを去ったときと同じ状態にあることを期待しています。 状態の保存と復元により、アプリが再び起動したときにアプリが以前の状態に戻ることが保証されます。」
UIKitは、状態の保存と復元を簡素化するために多くの作業を行います。アプリの状態の保存と読み込みを適切なタイミングで自動的に処理します。 必要なのは、状態の保存と復元をサポートするようにアプリに指示し、どのデータを保存する必要があるかをアプリに指示するための構成を追加することだけです。
状態の保存と復元を有効にするために、 AppDelegate.swift
に次の2つのメソッドを実装できます。
func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool { return true }
func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool { return true }
これにより、アプリケーションの状態を自動的に保存および復元するようにアプリに指示されます。
次に、どのビューコントローラーを保持する必要があるかをアプリに通知します。 これを行うには、ストーリーボードで「復元ID」を指定します。
「ストーリーボードIDを使用する」にチェックを入れると、ストーリーボードIDを復元IDとして使用することもできます。
コードで復元IDを設定するには、ViewControllerのrestorationIdentifier
プロパティを使用できます。
// ViewController.swift self.restorationIdentifier = "MainVC"
状態の保存中、復元IDが割り当てられたビューコントローラーまたはビューは、その状態がディスクに保存されます。
復元識別子をグループ化して、復元パスを形成できます。 識別子は、ルートビューコントローラから現在アクティブなビューコントローラまで、ビュー階層を使用してグループ化されます。 MyViewControllerが別のタブバーコントローラーに埋め込まれているナビゲーションコントローラーに埋め込まれているとします。 復元識別子として独自のクラス名を使用しているとすると、復元パスは次のようになります。
TabBarController/NavigationController/MyViewController
MyViewControllerがアクティブなViewControllerである状態でユーザーがアプリを離れると、このパスはアプリによって保存されます。 次に、アプリは表示された前のビュー階層を記憶します(タブバーコントローラー→ナビゲーションコントローラー→マイビューコントローラー)。
復元識別子を割り当てた後、保存されている各ビューコントローラーにencodeRestorableState(コーダー:)メソッドとdecodeRestorableState(コーダー:)メソッドを実装する必要があります。 これらの2つの方法では、保存またはロードする必要のあるデータと、それらをエンコードまたはデコードする方法を指定できます。
ビューコントローラを見てみましょう。
// MyViewController.swift // MARK: State restoration // UIViewController already conforms to UIStateRestoring protocol by default extension MyViewController { // will be called during state preservation override func encodeRestorableState(with coder: NSCoder) { // encode the data you want to save during state preservation coder.encode(self.username, forKey: "username") super.encodeRestorableState(with: coder) } // will be called during state restoration override func decodeRestorableState(with coder: NSCoder) { // decode the data saved and load it during state restoration if let restoredUsername = coder.decodeObject(forKey: "username") as? String { self.username = restoredUsername } super.decodeRestorableState(with: coder) } }
独自のメソッドの下部にあるスーパークラスの実装を呼び出すことを忘れないでください。 これにより、親クラスが状態を保存および復元できるようになります。
オブジェクトのデコードが完了すると、 applicationFinishedRestoringState()
が呼び出され、状態が復元されたことをViewControllerに通知します。 このメソッドでViewControllerのUIを更新できます。
// MyViewController.swift // MARK: State restoration // UIViewController already conforms to UIStateRestoring protocol by default extension MyViewController { ... override func applicationFinishedRestoringState() { // update the UI here self.usernameLabel.text = self.username } }
あります! これらは、アプリの状態の保存と復元を実装するために不可欠な方法です。 状態の保存と復元で問題が発生した場合に壊れた状態でスタックしないように、ユーザーがアプリを強制的に閉じているときに、オペレーティングシステムが保存された状態を削除することに注意してください。
また、便利に思えるかもしれませんが、モデルデータ(つまり、UserDefaultsまたはCore Dataに保存されているはずのデータ)を状態に保存しないでください。 ユーザーがアプリを強制終了すると、状態データは削除されます。この方法でモデルデータを失いたくないことは確かです。
状態の保存と復元が正常に機能しているかどうかをテストするには、次の手順に従います。
- Xcodeを使用してアプリをビルドして起動します。
- テストする状態の保存と復元が表示された画面に移動します。
- ホーム画面に戻り(ホームボタンを上にスワイプまたはダブルクリックするか、シミュレーターでShift⇧ + Cmd⌘ + Hを押す)、アプリをバックグラウンドに送信します。
- ボタンを押して、Xcodeでアプリを停止します。
- アプリを再度起動し、状態が正常に復元されたかどうかを確認します。
このセクションでは状態の保存と復元の基本のみを取り上げているため、状態の復元に関するより深い知識については、AppleInc。による次の記事をお勧めします。
- 状態の保存と復元
- UI保存プロセス
- UI復元プロセス
4.不透明でないビューの使用を可能な限り減らします
不透明なビューとは、透明度のないビューです。つまり、背後に配置されたUI要素はまったく表示されません。 Interface Builderで、ビューを不透明に設定できます。
または、UIViewのisOpaque
プロパティを使用してプログラムで実行できます。
view.isOpaque = true
ビューを不透明に設定すると、描画システムは画面のレンダリング中に描画パフォーマンスを最適化します。
ビューに透明度がある場合(つまり、アルファが1.0未満の場合)、iOSは、ビュー階層内のビューのさまざまなレイヤーをブレンドすることによって表示される内容を計算するために追加の作業を行う必要があります。 一方、ビューが不透明に設定されている場合、描画システムはこのビューを前面に配置し、背後にある複数のビューレイヤーをブレンドするという余分な作業を回避します。
デバッグ→カラーブレンドレイヤーをチェックすることで、iOSシミュレーターでブレンドされている(不透明でない)レイヤーをチェックできます。
Color Blended Layersオプションをチェックすると、一部のビューが赤で、一部が緑であることがわかります。 赤は、ビューが不透明ではなく、その出力表示がその背後にブレンドされたレイヤーの結果であることを示します。 緑は、ビューが不透明で、ブレンドが行われていないことを示します。
ラベルをストーリーボードにドラッグすると、その背景色がデフォルトで透明に設定されているため、上記のラベル(「友達を表示」など)は赤で強調表示されます。 描画システムがラベル領域の近くでディスプレイを合成しているとき、ラベルの後ろのレイヤーを要求し、計算を行います。
アプリのパフォーマンスを最適化する1つの方法は、赤で強調表示されるビューの数をできるだけ減らすことです。
label.backgroundColor = UIColor.clear
をlabel.backgroundColor = UIColor.white
UIColor.whiteに変更することで、ラベルとその背後のビューレイヤーの間のレイヤーブレンディングを減らすことができます。
UIImageViewを不透明に設定し、それに背景色を割り当てた場合でも、シミュレーターは画像ビューに赤で表示されることに気付いたかもしれません。 これは、画像ビューに使用した画像にアルファチャネルがあるためと考えられます。
画像のアルファチャンネルを削除するには、プレビューアプリを使用して画像の複製を作成し( Shift⇧ + Cmd⌘ + S )、保存時に[アルファ]チェックボックスをオフにします。
5.重い処理関数をバックグラウンドスレッド(GCD)に渡す
UIKitはメインスレッドでのみ機能するため、メインスレッドで重い処理を実行するとUIの速度が低下します。 メインスレッドは、UIKitがユーザー入力を処理して応答するだけでなく、画面を描画するためにも使用されます。
アプリをレスポンシブにするための鍵は、できるだけ多くの重い処理タスクをバックグラウンドスレッドに移動することです。 メインスレッドで複雑な計算、ネットワーキング、および大量のIO操作(ディスクの読み取りと書き込みなど)を実行することは避けてください。
タッチ入力に突然反応しなくなったアプリを使用したことがあり、アプリがハングしたように感じます。 これはおそらく、アプリがメインスレッドで重い計算タスクを実行していることが原因です。
メインスレッドは通常、UIKitタスク(ユーザー入力の処理など)といくつかの軽いタスクを短い間隔で交互に実行します。 重いタスクがメインスレッドで実行されている場合、UIKitは、重いタスクが終了するまで待機してから、タッチ入力を処理できるようにする必要があります。
デフォルトでは、View Controllerライフサイクルメソッド(viewDidLoadなど)およびIBOutlet関数内のコードはメインスレッドで実行されます。 重い処理タスクをバックグラウンドスレッドに移動するために、Appleが提供するGrand CentralDispatchキューを使用できます。
キューを切り替えるためのテンプレートは次のとおりです。
// Switch to background thread to perform heavy task. DispatchQueue.global(qos: .default).async { // Perform heavy task here. // Switch back to main thread to perform UI-related task. DispatchQueue.main.async { // Update UI. } }
qos
は「サービス品質」の略です。 異なるサービス品質値は、指定されたタスクの異なる優先順位を示します。 オペレーティングシステムは、より高いQoS値を持つキューに割り当てられたタスクにより多くのCPU時間とCPUパワーI / Oスループットを割り当てます。つまり、タスクは、より高いQoS値を持つキューでより速く終了します。 QoS値が高いほど、より多くのリソースを使用するため、より多くのエネルギーを消費します。
優先度の高いものから低いものへのQoS値のリストは次のとおりです。
Appleは、さまざまなタスクに使用するQoS値の例を示す便利な表を提供しています。
覚えておくべきことの1つは、すべてのUIKitコードは常にメインスレッドで実行する必要があるということです。 バックグラウンドスレッドでUIKitオブジェクト( UILabel
やUIImageView
など)を変更すると、UIが実際に更新されない、クラッシュが発生するなど、意図しない結果が生じる可能性があります。
Appleの記事から:
「メインスレッド以外のスレッドでUIを更新することはよくある間違いであり、UIの更新を見逃したり、視覚的な欠陥、データの破損、クラッシュを引き起こしたりする可能性があります。」
レスポンシブアプリの構築方法をよりよく理解するために、UIの同時実行性に関するAppleのWWDC2012ビデオを視聴することをお勧めします。
ノート
パフォーマンスの最適化のトレードオフは、アプリの機能に加えて、より多くのコードを記述したり、追加の設定を構成したりする必要があることです。 これにより、アプリが予想よりも遅れて配信される可能性があり、将来的に維持するコードが増えることになります。コードが増えると、バグが増える可能性があります。
アプリの最適化に時間を費やす前に、アプリがすでにスムーズであるかどうか、または本当に最適化する必要のある応答しない部分があるかどうかを自問してください。 すでにスムーズなアプリを最適化して0.01秒を短縮するために多くの時間を費やすのは、価値がないかもしれません。より良い機能やその他の優先順位の開発に時間を費やすほうがよいからです。
その他のリソース
- 「おいしいiOSアイキャンディーのスイート」、Tim Oliver、東京iOS Meetup 2018(ビデオ)
- 「iOSでの同時ユーザーインターフェイスの構築」、Andy Matuschak、WWDC 2012(ビデオ)
- 「リリース間でアプリのUIを維持する」Apple
- 「同時実行プログラミングガイド:ディスパッチキュー」、ドキュメントアーカイブ、Apple
- 「メインスレッドチェッカー」、Apple