はじめに
こんにちはsuzukiです「SwiftUI」が発表されました。レイアウト作成がとてもしやすそうでした。かなり触りやすそうに思えたのですが、業務ではデザインの指示書で細かい指示が出る場合が多いのです。細かいレイアウトも記述した時のコード全体の見易さが気になるところです。
それでは本題です。
リリース済みのアプリが特定の画面でたまに落ちると言われコードを確認すると、通信後のクロージャーの中に[unowned self]と記述していました。
[unowned self]とは、非所有参照でselfをキャプチャする事です。
メモリリークを防ぐため、クロージャー内で新しい非所有参照のselfを使い循環参照を起こらなくします。
[unowned self]はキャプチャされたselfが削除されないことが前提です。
しかしながら通信中にナビゲーションの閉じるボタンが有効だったため、自画面が削除されてアプリが落ちてました。
通信環境の再現
通信環境が良い時はレスポンスがすぐ返却されるため自画面の削除前にクロージャが呼ばれるのですが、不安定な回線でレスポンスが遅れた時に自画面が閉じられクラッシュしていました。
iOSの設定アプリからデベロッパを選択し通信環境を変更して再現をしました。
その上で、通信中に自画面を閉じたら、見事にクラッシュしました。
普段の開発環境で通信環境が悪いときの挙動をテストするなどはこちらの機能がとても便利です。
是非使っていきましょう。
unowned self
[unowned self] 使うサンプルのプログラムを作成しました。
Notificationを利用して非同期の処理を通信から通知に置き換えております。実際はNSNotificationでは循環参照が起きないため[unowned self]は記述不要ですが、動作確認のため記述しています。
StoryBoard
- NavigationControllerの追加
- ViewControllerにButtonを追加
- NextViewControllerを追加
- NextViewControllerにLabelを追加
- Buttonに遷移を追加
続いてコードです。[unowned self]を使用しているコードです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | class NextViewController: UIViewController { @IBOutlet weak var label: UILabel! override func viewDidLoad() { super.viewDidLoad() //通知の設定 NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: "endAsyncMethod"),object: nil,queue: nil) { [unowned self] (_) in //ラベルの更新 self.updateLabel() } asyncMethod() } //通知を受け取ったら行う関数 func updateLabel(){ self.label.text = "end" } func asyncMethod(){ //通信が時間がかかったった場合を再現するために2秒後に通知 DispatchQueue.main.asyncAfter(deadline: .now() + 2) { NotificationCenter.default.post(name:NSNotification.Name(rawValue:"endAsyncMethod"), object: nil) } } deinit { NotificationCenter.default.removeObserver(self) } } |
画面表示時に下記を行っています。
- 通知の受信設定”endAsyncMethod”の通知を受信したら、self.updateLabel()を行う
- 2秒後に通知”endAsyncMethod”を行う
2秒以内にナビゲーションの戻るをタップするとアプリがクラッシュします。
Fatal error: Attempted to read an unowned reference but the object was already deallocated
修正方法
今回の場合アプリでは[unownde self]を[weak self]に置き換えました。
クロージャの記述を下記のように切り替えています。
1 2 3 4 5 6 7 8 | //通知の設定 _ = NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: "endAsyncMethod"),object: nil,queue: nil) { [weak self] (_) in guard let self = self else{ return } //ラベルの更新 self.updateLabel() } |
[weak self]ですが、クロージャーに自分の参照を渡す際にキャプチャされるselfがオプショナルで渡せます。
オプショナルなので実際に破棄された場合のselfはnilになるので上記のコードのようにguard let self = self else{return}
でselfがnilの時にUIの更新を行わせないように記述をすることが可能です。
さいごに
[unowned self]そのものが悪いのではなく、途中で設計の周知ができなかった、引き継ぎで情報共有が不十分であった、等でバグを発生させてしまったように思えます。なぜなら他の箇所では[unowned self]を利用するため、途中で閉じれないように制御されていたためです。
今回はある程度レイヤーごとに役割が分割されていたのでViewControllerのunowned selfを躊躇なく[weak selfに]置き換えましたが、保存処理やフラグ管理が入り乱れてたら簡単には判断できませんでした。初期設計とドキュメントのいいところと悪いところ両方を感じたバグでした。