みやたくワークスの徒然ブログ

みやたくワークスの徒然ブログ

PR

×

Profile

たくぷれっさ2号機

たくぷれっさ2号機

Calendar

Archives

2026.05
2026.04
2026.03
2026.02
2026.01
2020.10.24
XML
カテゴリ: swift

ビューコントローラー内のオブジェクトを明示的に廃棄しないと、ゴミのようなコントローラーが残っちゃいますよ、というお話です。


先日作成した簡易的なメッセージ送受信画面(ビュー)。

テキストフィールドにメッセージを書き込むか、ビュー下段にあるボタン(イメージビュー)をタップすると、クラウド上のデータベースにメッセージが登録され、受信者&送信者は一定間隔でデータベースからメッセージを受信するという仕組みです。

受信したメッセージは、画面の上段の受信用イメージビューとメッセージラベルにそれぞれ表示されます。




テストをしていたところ、1回目のビュー表示時は正しくメッセージが送受信されるのですが、2回目以降のビュー表示時は、メッセージを送信してもその結果が反映されない事象が発生しました。

先に結論を言ってしまうと、前ビューに戻る際にコントローラー内で使用しているTimerオブジェクトが動きっぱなしになっており、そのせいでコントローラーが廃棄されていないことが原因でした。

表示中のビューに紐づいているコントローラーと、イベントを受けるコントローラーが別々になってしまっていたせいでラベルなどの更新がうまくいっていませんでした。


今後同じツボにはまらないように、調査の中で得た知見等をまとめておきます。

調査の過程

事象を発見した直後に、ビューの動作とデバッガーからわかったことは以下の3点。

・2回目以降も初期表示時は正しくイメージとラベルは表示される
・送信したメッセージはデータベースに格納されている
・受信処理も正常終了していて、メッセージを格納する配列にも正しく登録されている

以上のことから、メッセージ送受信の仕組みは正しく動いており、ビューの再描画ができていないことがは結構早い段階でわかりました。
ビューに対して、setNeedsDisplay() 発行してみても変わらず。

Debug用にUILabelを継承した以下のようなクラスを作ってみました。

import UIKit

// UILabelのデバッグ用。

class TMLabel : UILabel {

fileprivate let color = UIColor.white


// プロパティの初期化

fileprivate func configure () {

backgroundColor = color

translatesAutoresizingMaskIntoConstraints = false

}


/// イニシャライザ

init () {

print(NSStringFromClass(type(of: self )).components(separatedBy: "." )[ 1 ] + ":" + #function )

super .init(frame: CGRect.zero)

configure()

}

/// イニシャライザ

override init (frame: CGRect ) {

print(NSStringFromClass(type(of: self )).components(separatedBy: "." )[ 1 ] + ":" + #function )

super .init(frame: frame)

configure()

}


/// イニシャライザ(StoryBoardやIntefaceBuilderでの生成)

required init ?(coder aDecoder: NSCoder ) {

print(NSStringFromClass(type(of: self )).components(separatedBy: "." )[ 1 ] + ":" + #function )

super .init(coder: aDecoder)

configure()

}

/// 制約の更新

override func updateConstraints () {

print(NSStringFromClass(type(of: self )).components(separatedBy: "." )[ 1 ] + ":" + #function )

super .updateConstraints()

}

/// 制約の更新要否のフラグを立てる

override func setNeedsUpdateConstraints () {

print(NSStringFromClass(type(of: self )).components(separatedBy: "." )[ 1 ] + ":" + #function )

super .setNeedsUpdateConstraints()

}


/// 必要に応じて制約の更新を即時実行する

override func updateConstraintsIfNeeded () {

print(NSStringFromClass(type(of: self )).components(separatedBy: "." )[ 1 ] + ":" + #function )

super .updateConstraintsIfNeeded()

}

/// レイアウトの更新

override func layoutSubviews () {

print(NSStringFromClass(type(of: self )).components(separatedBy: "." )[ 1 ] + ":" + #function )

super .layoutSubviews()

}

/// レイアウトの更新要否のフラグを立てる

override func setNeedsLayout () {

print(NSStringFromClass(type(of: self )).components(separatedBy: "." )[ 1 ] + ":" + #function )

super .setNeedsLayout()

}

/// 必要に応じてレイアウトの更新を即時実行する

override func layoutIfNeeded () {

print(NSStringFromClass(type(of: self )).components(separatedBy: "." )[ 1 ] + ":" + #function )

super .layoutIfNeeded()

}

/// 描画の更新

override func draw ( _ rect: CGRect ) {

print(NSStringFromClass(type(of: self )).components(separatedBy: "." )[ 1 ] + ":" + #function )

super .draw(rect)

}

/// 描画の更新要否のフラグを立てる

override func setNeedsDisplay () {

print(NSStringFromClass(type(of: self )).components(separatedBy: "." )[ 1 ] + ":" + #function )

super .setNeedsDisplay()

}

/// 描画の更新要否のフラグを立てる

override func setNeedsDisplay ( _ rect: CGRect ) {

print(NSStringFromClass(type(of: self )).components(separatedBy: "." )[ 1 ] + ":" + #function )

super .setNeedsDisplay(rect)

}

deinit {

print(NSStringFromClass(type(of: self )).components(separatedBy: "." )[ 1 ] + ":" + #function )

}

}


実行結果はこんな感じ。(ラベルが3つあるので三重にダンプされてます)

1回目・ビュー初期化時

TMLabel:init(coder:)

TMLabel:setNeedsDisplay()

TMLabel:setNeedsDisplay()

TMLabel:setNeedsLayout()

TMLabel:setNeedsDisplay()

TMLabel:init(coder:)

TMLabel:setNeedsDisplay()

TMLabel:setNeedsDisplay()

TMLabel:setNeedsLayout()

TMLabel:setNeedsDisplay()

TMLabel:init(coder:)

TMLabel:setNeedsDisplay()

TMLabel:setNeedsDisplay()

TMLabel:setNeedsLayout()

TMLabel:setNeedsDisplay()

TMLabel:setNeedsLayout()

TMLabel:setNeedsDisplay()

TMLabel:setNeedsLayout()

TMLabel:setNeedsDisplay()

TMLabel:setNeedsLayout()

TMLabel:setNeedsDisplay()

TMLabel:updateConstraints()

TMLabel:updateConstraints()

TMLabel:updateConstraints()

TMLabel:updateConstraints()

TMLabel:updateConstraints()

TMLabel:updateConstraints()

TMLabel:setNeedsLayout()

TMLabel:setNeedsLayout()

TMLabel:setNeedsLayout()

TMLabel:layoutSubviews()

TMLabel:layoutSubviews()

TMLabel:layoutSubviews()

TMLabel:updateConstraints()

TMLabel:updateConstraints()

TMLabel:updateConstraints()

TMLabel:updateConstraints()

TMLabel:updateConstraints()

TMLabel:updateConstraints()

TMLabel:layoutSubviews()

TMLabel:layoutSubviews()

TMLabel:layoutSubviews()

TMLabel:draw(_:)

TMLabel:draw(_:)

TMLabel:draw(_:)


1回目・メッセージ送信から受信時後

TMLabel:setNeedsLayout()

TMLabel:setNeedsDisplay()

TMLabel:setNeedsDisplay()

TMLabel:setNeedsDisplay()

TMLabel:updateConstraints()

TMLabel:updateConstraints()

TMLabel:layoutSubviews()

TMLabel:updateConstraints()

TMLabel:updateConstraints()

TMLabel:layoutSubviews()

TMLabel:draw(_:)

TMLabel:draw(_:)

TMLabel:draw(_:)


2回目・ビュー初期化時

TMLabel:init(coder:)

TMLabel:setNeedsDisplay()

TMLabel:setNeedsDisplay()

TMLabel:setNeedsLayout()

TMLabel:setNeedsDisplay()

TMLabel:init(coder:)

TMLabel:setNeedsDisplay()

TMLabel:setNeedsDisplay()

TMLabel:setNeedsLayout()

TMLabel:setNeedsDisplay()

TMLabel:init(coder:)

TMLabel:setNeedsDisplay()

TMLabel:setNeedsDisplay()

TMLabel:setNeedsLayout()

TMLabel:setNeedsDisplay()

TMLabel:setNeedsLayout()

TMLabel:setNeedsDisplay()

TMLabel:setNeedsLayout()

TMLabel:setNeedsDisplay()

TMLabel:setNeedsLayout()

TMLabel:setNeedsDisplay()

TMLabel:layoutSubviews()

TMLabel:layoutSubviews()

TMLabel:layoutSubviews()

TMLabel:draw(_:)

TMLabel:draw(_:)

TMLabel:draw(_:)


2回目・ メッセージ送信から受信時後

TMLabel:setNeedsLayout()

TMLabel:setNeedsDisplay()

TMLabel:setNeedsLayout()

TMLabel:setNeedsDisplay()

TMLabel:setNeedsDisplay()

TMLabel:setNeedsLayout()

TMLabel:setNeedsDisplay()

TMLabel:setNeedsLayout()

TMLabel:setNeedsDisplay()

TMLabel:setNeedsLayout()

TMLabel:setNeedsDisplay()

2回目のビュー初期化時まではdraw()が呼び出されているが、メッセージ送受信後はsetNeedsDisplayは呼び出されているものの、drawが呼び出されていないことがわかりました。

ラベルのメモリ上の番地のダンプ結果はこんな感じでした。

1回目・ビュー初期化時


1回目・メッセージ送信から受信時後


2回目・ビュー初期化時


2回目・メッセージ送信から受信時後


これでやっと原因が分かりました。
2回目初期化時は番地が変わっているものの2回目受信時には1回目と同じ番地になってます。
つまり、2回目のイベントを受け取っているコントローラーは1回目と同じコントローラーでした。

概念的にはこんな感じ。


※iPhoneのメモ帳で手書きしたので全体的に汚くてすみません。


XCodeでメモリの状態を表示してみたところ、本来1つしか生成されないはずのビューコントローラーが2つあるのが分かります。



コントローラーが廃棄されない原因は冒頭で書いた通り、コントローラー内で使用しているTimerが生きて続けていたことでした。

前ビューに戻る操作を自分で実装し、アクション内でTimerを廃棄する処理を生成したら、無事コントローラーが破棄されるようになった。

// MARK: - IBAction

// 戻るボタンタップ時の操作

@IBAction func action_BackButton () {
// メッセージ受信タイマーを初期化
if ( self .msgRcvTimer != nil ) {
self .msgRcvTimer.invalidate()
self .msgRcvTimer = nil
}
// ビューのClose用のタイマーを初期化
if ( self .closeTimer != nil ) {
self .closeTimer.invalidate()
self .closeTimer = nil
}
// 前の画面に戻る
self .navigationController?.popViewController(animated: true )
return
}

わかってみたら、当たり前のような話ですが、だいぶはまりました。

解決時間ほぼ3日。






お気に入りの記事を「いいね!」で応援しよう

Last updated  2020.10.25 23:40:19
コメント(0) | コメントを書く


【毎日開催】
15記事にいいね!で1ポイント
10秒滞在
いいね! -- / --
おめでとうございます!
ミッションを達成しました。
※「ポイントを獲得する」ボタンを押すと広告が表示されます。
x
X

© Rakuten Group, Inc.
X
Design a Mobile Site
スマートフォン版を閲覧 | PC版を閲覧
Share by: