Swiftで遊ぼう! on Hatena

あしたさぬきblogでやってた初心者オヤジのiOSプログラミング奮闘記がHatenaに来ました

Swiftで遊ぼう! - 302 - マルチスレッド(まとめ)

2017年3月23日:メモリーサイクルの問題にも言及*1

Swift 3になって、マルチスレッドの取り扱いが大きく変わりました。マルチスレッドにはGCD(Grand Central Dispatch)OOPスタイルのNSOperationクラスがありますが、GCDに大幅な改訂が加わり生まれ変わりました。

スタンフォード大学、ポール先生の講義、Lecture9の最後のトピックは、マルチスレッドです。彼に言わせれば、マルチスレッドの実装はシンプルで基本を押さえれば楽勝... とはいかないでしょう(^_^;)

キュー(queue)という考え方でプログラムを実行

キューとは、プログラムの一連の処理の流れのことで以下の2種類があります。

  1. Serial:キュー内の処理を連続的に実行するので、前の処理が完了してから次の処理に入る。
  2. Concurrent:キュー内の処理は並列的に実行されるので、バラバラな順番で処理が終了して行く。

Main Queue(メイン・キュー)は、特殊なSerialキューで、アプリ起動時にシステムによって自動的に作られます。でUI(ユーザー・インターフェイス)関連の作業をさせます。

あえて取得するのなら次のように書きます。

let mainQueue = DispatchQueue.main

同期と非同期

キューを生成したら、スレッドを同期(sync)させるか、非同期(async)させるか割り振って行くわけです。同期とは、登録した処理が完了するまで「待つ」、そして非同期は、登録した処理が完了するまで「待たない」処理です。時間のかかる作業はキューを非同期に割りふればいいのです。

この非同期の処理ですが、iOSの開発の歴史的な理由から色々と書き方があり混沌としていました。Swift 2まで、dispatch_asyncというアンラーラインの入った古い「C」スタイルなコーディングが使用されていて、これがGCD(Grand Central Dispatch)でした。OOP的なマルチスレッド、NSOperationQueueとNSOperationもあるんですが、どうも一般的ではなく、Swift 3になってGCDに大幅な改訂が加わりモダンなコーディングスタイルに変更され。より理解しやすくSwiftらしい記述法に生まれ変わりました*2

基本的なコーディング

自分でキューを生成して任意の処理をサブスレッドで実行、終わったらメインスレッドで処理を実行する例を示します。

// Swift 3スタイル
// まずキューを生成して、それを非同期スレッドで動かします。
DispatchQueue(label: "someTask").async {
    self.doSomething()
 
    // メインスレッドに戻ってUIに絡む
    DispatchQueue.main.async {
        self.doAnotherthing()
    }
}


// Swift 2までは以下のようにdispatch_asyncを使っていました。
dispatch_async(queue: dispatch_queue_t dispatch_queue_t,
               block: dispatch_block_t dispatch_block_t() -> Void)
// 非常に理解し難い記述法でした。
// このメソッドは2つのパラメーターを受け取ります。
// 最初のパラメーターが「queue」で、2番目が「クロージャ」です。
// そして、クロージャーはトレーリングクロージャーにできるので、
// 次のように「()」の外に「{}]ブロックで記述します。

Swift 3になってラベル名を指定してキューを生成することができるんです。上記の例で示しているキューは「Serial」です。「Concurrent」キューを生成させるのであれば次のように記述します。

// Swift 3スタイル
DispatchQueue(label: "someTask", attributes: .concurrent)

ここまでの説明は自分でキューを生成させる方法ですが、ほとんどの場合、システムが用意しているキューを利用する方が一般的だと思います。

// Swift 3スタイル
let globalQueue = 
    DispatchQueue.global(qos: DispatchQoS.QoSClass.default)

// Swift 2スタイル
let queue: dispatch_queue_t = < /* 色々なqueueの記述 */ >
dispatch_async(queue) { /* ここで何かさせる */ }
// 

パラメータで指定されている「qos」とは、「Quority Of Service」の頭文字で、優先度を意味します。このパラメータの扱いもSwift 3で変更されました*3

// Swift 3スタイル
// enum型でパラメータが用意されました。
enum QoSClass {
    case userInteractive
    case userInitiated
    case default
    case utility
    case background
    case unspecified
}

// Swift 2スタイル
// 理解しにくかったですね。
- QOS_CLASS_USER_INTERACTIVE
- QOS_CLASS_USER_INITIATED
- QOS_CLASS_UTILITY
- QOS_CLASS_BACKGROUND

ということで、ここまでの内容をまとめると、一般的なマルチスレッドなコーディングは以下のようになります。

// Swift 3スタイル
 DispatchQueue.global(qos: .userInitiated).async {
    // 時間のかかる仕事
    DispatchQueue.main.async {
        // 上でさせた作業でUI機能が呼ばれた時
    }
}

// Swift 2スタイル
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED,0)) {
    // 時間のかかる仕事
    dispatch_async(dispatch_get_main_queue()) {
        // 上でさせた作業でUI機能が呼ばれた時
    }
}

他にもキューを作ったり、マルチスレッドを使用しなければならないiOS APIがあったりします*4

ここの内容はiOS8向けのスタンフォード大学の講義の内容でした。その後iOS9に合わせて改訂しました。しかし、今やiOS10の開発がメインになっているため、iOS8向けの内容は削除して、ここの記事だけ読んでいる人向けに完全に改訂します。

マルチスレッドデモ

新規のプロジェクトを作成します。Single View Applicationを選択してプロジェクト名を「BigImageView」にします。オブジェクト・ライブラリからUIImageViewを探して、Main.storyboardにドラッグ&ドロップ、UIButtonも設置するんで、下端にスーペースを残して画面一杯に広げ、コンストレイントを設定します。UIImageViewオブジェクトをViewController.swiftに「Ctrl + ドラッグ」して名前を「imageView」にして@IBOutletインスタンスを作ります。

@IBOutlet weak var imageView: UIImageView!

オブジェクト・ライブラからUIButtonを選択して、同様に下端のスペースに設置してダブルクリックして名前を「ダウンロード」に変えてから、コンストレイントを設定します*5。そしてViewController.swiftに「Ctrl + ドラッグ」します。今度は@IBActionメソッドにして、名前を「getImage」にします。

@IBAction func getImage(_ sender: UIButton) {
}

コンストレイントを設定したここまでの状態で以下のようにします。

f:id:yataiblue:20161118223651j:plain

次に、2つプロパティを用意します。

// MARK: Properties

// 1
var image: UIImage? = UIImage()
// 2 - Swift 3スタイル
let imageURL = URL(string: "http://www.jpl.nasa.gov/
                          images/cassini/20090202/
                              pia03883-full.jpg")

// 2 - Swift 2スタイル
let imageURL = NSURL(string: "http://www.jpl.nasa.gov/
                          images/cassini/20090202/
                              pia03883-full.jpg")
  1. ネットからダウンロードしたイメージを保持するための空のimageを用意します。
  2. URLアドレスデータをURLクラス(Swift 3から利用)として保持します。

ATSの設定をしないと画像をダウンロードできません。Swiftで遊ぼう! - 455 - Swift 2 NSURLSessionまだ全然わかっていない...ちょっと分かったかな(ATS) - Swiftで遊ぼう! on Hatenaで説明しているinfo.plistに編集を加えます。

次にネット上にあるイメージをダウンロードするカスタムメソッド、fetchImage()を用意します。

private func fetchImage() {
    guard let url = imageURL else {
        return
    }
    var imageData: Data?
    do {
        imageData = try Data(contentsOf: url)
    } catch {
        print("fail to download")
        image = nil
    }
    image = UIImage(data: imageData!)
    imageView.image = image
}

Swift 3から使える新しいクラスがDataで、NSDataクラスから変更のあった部分です。このクラスのイニシャライザに次のものがあります。

Data(contentsOf: URL) throws

throws」の付いたイニシャライザやメソッドを使用する場合、必ず「try」して、エラーをキャッチする仕組みを組み込まないといけません。この説明は、Swiftで遊ぼう! - 451 - Swift 2 : Error Handling(エラー・ハンドリング) - Swiftで遊ぼう! on Hatenaにあるので確認しましょう。

そして、このメソッドを@IBActionメソッド、getImage()メソッド内に加えます。

    @IBAction func getImage(_ sender: UIButton) {
        fetchImage()
    }

これをランすると真っ白な画面が現れます。「ダウンロード」ボタンを押すと、ボタンはグレーアウトして、しばらく白い画面が続いて*6からイメージが出現します。イメージが出現するとグレーアウトしたボタンは元に戻ります。これは処理がSerialにメインスレッドで動作しているからです。imageViewの設定がデフォルトのままなんでアスペクト比は崩れています。

f:id:yataiblue:20161118231516j:plain

マルチスレッド

QualityOfServiceクラスの「DispatchQoS.QoSClass.userInitiated)」を使えば、非同期スレッドでダウンロードできます。サイズの大きな画像はダウンロードに時間がかかります。メインスレッドでダウンロードをさせると、ダウンロード中、他の処理を受け付けることができないため、「ダウンロード」ボタンはグレーアウトしたままになります。バックグラウンドのスレッドでイメージのダウンロード処理をさせる必要があるので、fetchImage()メソッドを次のように変更します。バックグラウンドのキューはクロージャーで処理させるため、メモリーリークが生じる危険性がありキャプチャーリストを作ります*7

private func fetchImage() {
        guard let url = imageURL else {
            return
        }
        let globalQueue =
            DispatchQueue.global(
                qos: DispatchQoS.QoSClass.userInitiated)
        
        globalQueue.async { [ weak self] in
            var imageData: Data?
            do {
                imageData = try Data(contentsOf: url)
            } catch {
                print("fail to download")
                self?.image = nil
            }
            DispatchQueue.main.async {
                self?.image = UIImage(data: imageData!)
                self?.imageView.image = self?.image
            }
        }
    }

システムの用意している「.userInitiated」のキューを使って、非同期スレッドでネットからイメージのデータをダウンロードできます*8。処理が済んだらメインスレッドに戻って画像を表示します。

f:id:yataiblue:20161118231516j:plain

結果は先ほどのメインスレッドで動かした時と同じです。しかし、違いに気がつきましたか? 「ダウンロード」ボタンの挙動です。メインスレッドで画像のダウンロードをさせると、ダウンロードが完了するまでボタンはグレーアウトしたまま反応はしません。しかし、バックグラウンドで画像をダウンロードさせると、グレーアウトしたボタンはダウンロードが完了する前に元に戻っているんです。

考えて見れば、マルチスレッドというのは、時間のかかるコードブロックを別スレッド割りふるという考え方でいいようです。

UI(ユーザー・インターフェイス)に関する注意点

マルチスレッドを組み込むことで、待たされる処理でも無反応に陥ること無く他の処理を受け付けることができます。しかし、処理を待たされている間、真っ白な画面が表示されているだけだとユーザーは不安感を覚えるでしょう。

f:id:yataiblue:20161119002611j:plain

こういう場合、「処理はしてますよ」という意味あいで、画面の中央でスピニング・ウィールがぐるぐる回るのが一般的です。

そう、このスピニング・ウィールもオブジェクトとして用意されています。

UIActivityIndicatorViewというUIViewのサブクラスになります。

オブジェクト・ライブラリからActivitey Indicator Viewを選択してUIImageViewの真ん中に設置します。コンストレイントを設定してViewControllerに「Ctrl + ドラッグ」をして「spinner」と名前をつけた@IBOutletインスタンスを作ります。

spinnerの開始と終了はすべてfetchImage関数内で制御できます。

 private func fetchImage() {
        guard let url = imageURL else {
            return
        }

        spinner.startAnimating()

        let globalQueue =
            DispatchQueue.global(qos: DispatchQoS.QoSClass.userInitiated)
        globalQueue.async { [weak self] in
            var imageData: Data?
            do {
                imageData = try Data(contentsOf: url)
            } catch {
                print("fail to download")
                self?.image = nil
            }
            DispatchQueue.main.async {
                self?.image = UIImage(data: imageData!)

                self?.spinner.stopAnimating()

                self?.imageView.image = self?.image
            }
        }
    }

これでランさせるとスピナーがぐるぐる回ります。

f:id:yataiblue:20161119004447j:plain

これでマルチスレッドの基本事項は終わります。

*1:2017年3月22日:大きな間違いを修正(^_^;)、2016年11月17日:Swift 3対応に向けて改訂中、2016年6月7日改訂:スタンフォード大学のiOS9開発の講義Developing iOS 9 Apps with Swift - Free Course by Stanford on iTunes Uで復習、2015年12月2日改訂:以下の記事はすべてiOS9, Swift2.1向けに書き換え

*2:いつもコメントをくださるNaoさんによればGCDのほうがNSOperationQueueより新しいということで少し改訂しました。

*3:優先度にdefaultとunspecifiedが加わりましたが、基本は他の4種類なのでこの4種類の使い分けに慣れる必要があります

*4:実はこのブログエントリーを書いた後に勉強したNSURSessionクラスの「task」も含まれます。

*5:コンストレイントに関する情報は、Swiftで遊ぼう! - 204 - フィリングを使ってレイアウト調整(Auto Layoutのまとめ) - Swiftで遊ぼう! on Hatenaで、確認します。

*6:画像サイズが大きいのでダウンロードに少し時間がかかります。

*7:モリーマネージメントのまとめはSwiftで遊ぼう! - 869 - ARCとメモリーマネージメント - Swiftで遊ぼう! on Hatenaを見てください。

*8:1番時間のかかる処理です。