💡

【翻訳】《Swift and Cocoa Essentials》What Is the Main Thread

2023/07/25に公開

スレッディングと並行処理はより高度な概念ですが、経験レベルに関係なく基本を理解する必要があります。私たちがアプリケーションを開発するデバイスはマルチコア・プロセッサーを搭載しており、そのパワーを活用することが重要です。

前回のエピソードでは、スレッド、キュー、並行処理について説明しました。今日のエピソードでは、メイン・スレッドについて詳しく見ていきたい。メイン・スレッドについて聞いたことがあると思いますし、アプリケーションがメイン・スレッドで実行する作業を最小限にすることで、メイン・スレッドの応答性を保つことの重要性についてもご存知かもしれません。なぜでしょうか?メイン・スレッドで重い処理を行うとどうなるでしょうか?

私がインタビューのためにコードをレビューしたり、学生のプロジェクトを見ているときはいつも、 Swift と Cocoa 開発の開発者の経験について考えを形成するのに時間はかかりません。開発者がメインスレッドをどのように扱うかは、しばしば良い兆候です。

このエピソードを最大限に活用するために、私はあなたについていき、私が持っている質問に答えることをお勧めします。就職の面接や、フリーランスとしてプロジェクトに応募するときに聞かれるかもしれない質問です。

最初のステップは簡単です。このエピソードのスターター・プロジェクトをダウンロードして、 Xcode で開いてください。プロジェクトは複雑ではありません。このアプリケーションは、画像を集めた table view を示しています。各 table view の cell には、左に画像、右にタイトルがあります。デバイス上でアプリケーションを実行してください。何に気づきますか?あなたの第一印象は?

なぜアプリケーションが白いビューを表示するのか不思議に思うかもしれません。しかし、起動から数秒後には、 table view に画像とタイトルが表示されています。これはあまり良いユーザーエクスペリエンスではありません。ビデオを一時停止して、プロジェクトを点検してください。画像やタイトルが表示されるまでに何秒もかかる理由を探ってみてください。プロジェクトは複雑ではありません。試してみてください。

メインスレッドのブロック

何が起こっているのか説明しましょう。このプロジェクトには ImagesViewController という UIViewController のサブクラスが1つあります。 ImagesViewController クラスは [Image] 型 の dataSource というプロパティを定義しています。 Image は private struct で、 title と url プロパティを定義しています。 Image インスタンスの配列は、 table view の入力に使用されます。

private lazy var dataSource: [Image] = [
    Image(title: "Misery Ridge",                    url: URL(string: "https://cdn.cocoacasts.com/7ba5c3e7df669703cd7f0f0d4cefa5e5947126a8/1.jpg")),
    Image(title: "Stonehenge Storm",                url: URL(string: "https://cdn.cocoacasts.com/7ba5c3e7df669703cd7f0f0d4cefa5e5947126a8/7.jpg")),
    ...
    Image(title: "Mountain Sunrise",                url: URL(string: "https://cdn.cocoacasts.com/7ba5c3e7df669703cd7f0f0d4cefa5e5947126a8/8.jpg")),
    Image(title: "Colours of Middle Earth",         url: URL(string: "https://cdn.cocoacasts.com/7ba5c3e7df669703cd7f0f0d4cefa5e5947126a8/9.jpg"))
]

tableView(_:cellForRowAt:) では、 Image インスタンス に格納されたデータでImageTableViewCell インスタンス が構成されます。

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCell(withIdentifier: ImageTableViewCell.reuseIdentifier, for: indexPath) as? ImageTableViewCell else {
        fatalError("Unable to Dequeue Image Table View Cell")
    }

    // Fetch Image
    let image = dataSource[indexPath.row]

    // Configure Cell
    cell.configure(with: image.title, url: image.url)

    return cell
}

ImageTableViewCell クラス の configure(with:url:) メソッド の実装に興味があります。このメソッドでは、アプリケーションは titleLabel の text プロパティを title パラメータに格納された値で更新し、 url パラメータに格納された URL を使用して Data インスタンスを初期化します。 URL はリモートサーバを含む任意の場所を指すことができるため、 Data インスタンス の初期化に時間がかかる場合があります。 URL がディスク上のファイルを指す場合でも、ファイルのサイズによっては Data インスタンスの初期化にかなりの時間がかかることがあります。

func configure(with title: String, url: URL?) {
    // Configure Title Label
    titleLabel.text = title

    // Load Data
    if let url = url, let data = try? Data(contentsOf: url) {
        // Configure Thumbnail Image View
        thumbnailImageView.image = UIImage(data: data)
    }
}

アプリケーションは Data インスタンス を使用して UIImage インスタンス を作成し、 UIImageView インスタンス である thumbnailImageView の image プロパティ に割り当てます。 Data インスタンス と UIImage インスタンス の初期化はメインスレッドで行われます。これを確認するには、 Thread クラス にクラスの計算プロパティである isMainThread の値を問い合わせて、呼び出しがメインスレッドで行われるかどうかを示すブール値を返します。 configure(with:url:) メソッド に以下の print 文 を追加し、アプリケーションを実行します。

func configure(with title: String, url: URL?) {
    // Configure Title Label
    titleLabel.text = title

    print(Date())
    print(Thread.isMainThread)

    // Load Data
    if let url = url, let data = try? Data(contentsOf: url) {
        // Configure Thumbnail Image View
        thumbnailImageView.image = UIImage(data: data)
    }
}

タイムスタンプは、データをロードして UIImage インスタンス をインスタンス化するのに数秒かかることを示唆しています。コンソールの出力は、現在メインスレッドで画像データをロードしていることも示しています。

**2018-08-01 15:47:32 +0000**
**true**
**2018-08-01 15:47:33 +0000**
**true**
**2018-08-01 15:47:33 +0000**
**true**
**2018-08-01 15:47:33 +0000**
**true**
**2018-08-01 15:47:33 +0000**
**true**
**2018-08-01 15:47:34 +0000**
**true**
**2018-08-01 15:47:34 +0000**
**true**
**2018-08-01 15:47:34 +0000**
**true**
**2018-08-01 15:47:35 +0000**
**true**

残念ながら、 table view にデータが入力されると、問題は解決しません。 table view をスクロールしてみましたか?それもうまくいきません。何が起こっているのか、そしてもっと重要なことは、これらの問題を解決するにはどうすればいいのか、ということです。

何が問題なのか?

答えは簡単です。アプリケーションのパフォーマンスが悪いのは、メインスレッドをブロックしているからです。メインスレッドをブロックしてはいけないと聞いたり読んだりしたことがあるかもしれません。メインスレッドをブロックするとはどういうことでしょうか?そして、なぜそれがアプリケーションのパフォーマンスに劇的な影響を与えるのでしょうか?

アプリケーションのユーザー・インターフェースはメイン・スレッドで画面に描画されます。ユーザーインターフェイスを画面に描画するのは安い処理ではありません。時間もリソースもかかります。最近のデバイスは、この処理を1秒間に最大60回実行します。つまり、1秒の何分の1が重要なのです。

先ほどの print 文 を覚えているでしょうか?データをロードし、 UIImage インスタンス をインスタンス化するのに数秒かかりました。その間、アプリケーションはユーザーインターフェースを画面に描画することができず、白い画面しか見えなかったわけです。 table view のスクロールも同様です。 table view の cell がユーザーに表示されようとするたびに、 UIImage インスタンス のデータがリモートからロードされ、メイン・スレッドがブロックされます。

スムーズなスクロールのためには、アプリケーションはユーザー・インターフェースを1秒間に数回画面に描画するリソースが必要です。メインスレッドがブロックされていると、それは不可能です。

バックグラウンドでの作業

アプリケーションは非常に基本的なものですが、重大な問題がいくつもあります。アプリケーションの起動体験を改善するよう求められたら、あなたはどうしますか?ビデオを一時停止して、選択肢を考えてみてください。

いくつかの解決策が考えられますが、際立っている解決策が1つあります。どのような解決策であっても、メインスレッドがブロックされないようにすることが目標です。どうやってそれを達成するつもりですか?私が考えているソリューションは簡単に実装できます。 Grand Central Dispatch を活用するのです。前回のエピソードで、 Grand Central Dispatch を使えば、同時並行、つまりパラレルに作業を実行することが簡単にできることを思い出してほしい。これは、1つ以上のマルチコアプロセッサを搭載したデバイスのパワーを活用したい場合に重要なことです。

このソリューションがどのようなものかをお見せしましょう。 ImageTableViewCell.swift を開き、 configure(with:url:) メソッド に移動します。 DispatchQueue クラスにグローバル・キューへの参照を求めます。グローバル・キューやバックグラウンド・キューについては、前回のエピソードで説明しました。グローバル・キューとは、オペレーティング・システムがアプリケーションに作業を行わせるためのキューのことです。オプションとして、グローバル・キューのサービス品質を指定することができます。サービス品質とは、これから実行しようとしている作業の重要度を指します。 DispatchQueue クラス にバックグラウンド・キューを要求します。オペレーティング・システムは、キューのサービス品質を使って、そのキューで実行される作業がいつ実行されるかを決定します。

ディスパッチ・キューで async(group:qos:flags:execute:) メソッド を呼び出し、そのメソッドにクロージャを渡します。 async(group:qos:flags:execute:) メソッド はすぐに戻り、クロージャはディスパッチ・キュー上で非同期に実行されマス。これは必須です。 sync(group:qos:flags:execute:) メソッド を呼び出すこともできます。 sync(group:qos:flags:execute:) メソッド は、クロージャで実行した作業が終わった後に制御を返します。メインスレッドがブロックされたままになってしまうからです。

url に格納された値で Data インスタンス を初期化し、そのデータで UIImage インスタンス を作成します。

func configure(with name: String, url: URL?) {
    // Configure Name Label
    nameLabel.text = name

    // Load Image
    if let url = url {
        DispatchQueue.global(qos: .background).async {
            if let data = try? Data(contentsOf: url) {
                let image = UIImage(data: data)
            }
        }
    }
}

最後のステップは、 thumbnailImageView の image プロパティ を UIImage インスタンス で更新することです。以前に学んだことを思い出してください。ユーザー・インターフェースは常にメイン・スレッドで更新されるべきです。つまり、 thumbnailImageView の imageプロパティ はメインスレッドで代入する必要があるということです。 Grand Central Dispatch はこれを簡単にします。メインスレッドに関連付けられたキューへの参照を DispatchQueue クラス に要求し、 async(group:qos:flags:execute:) メソッド を呼び出すことで、そのキューでの作業ブロックを非同期にスケジューリングします。

func configure(with name: String, url: URL?) {
    // Configure Name Label
    nameLabel.text = name

    // Load Image
    if let url = url {
        DispatchQueue.global(qos: .background).async {
            if let data = try? Data(contentsOf: url) {
                let image = UIImage(data: data)

                DispatchQueue.main.async {
                    self.thumbnailImageView.image = image
                }
            }
        }
    }
}

ここまでの成果を見てみましょう。アプリケーションを起動して、結果を見てみましょう。すぐに名前入りの table view が表示され、画像の読み込み中であることをユーザーに示すアクティビティ・インジケーター・ビューまで表示されています。見た目はかなり良くなりました。スクロールもかなり改善されましたが、古くて遅いデバイスでは完璧ではありません。私たちが取得している画像はかなり大きく、この目的には大きすぎます。必要なのはサムネイルだけです。これはクライアント側で簡単に修正できるものではありません。

次は?

table view をスクロールすると、 table view の cell が移動するたびに同じ画像が取得されることに気づきます。これはパフォーマンスを低下させ、また無駄でもあります。次回は、現在の実装をさらに改良します。

【翻訳元の記事】

Swift and Cocoa Essentials
What Is the Main Thread
https://cocoacasts.com/swift-and-cocoa-fundamentals-what-is-the-main-thread

Discussion