- ๋ชจ๋ ํธ์&ํ ์ด๋ฒคํธ์ ๋ํด ์ง๊ด์ ์ผ๋ก ๋ฉ๋ชจ๋ฆฌ ํด์ ๋ฅผ ํ์ธํ์ธ์!
- ๐ง Check Memory Leak in every push & pop events!
- Navigation Push ํ Pop์ ํ๋ฉด Pop ํ ์ฝ 1.5์ด ๊ฐ ํฐ์น๊ฐ ๋งํ ํด๋ฆญ์ด ๋ถ๊ฐํฉ๋๋ค.
- ๋ง์ฝ ํด์ ๋์ง ์์ view ์ controller๊ฐ ์๋ค๋ฉด ํด๋น ์ด๋ฆ์ด ํ์ ์ ๋ฆฌ์คํธ๋ฉ๋๋ค.
- ๋ง์ฝ ๋ชจ๋ ์ธ์คํด์ค๊ฐ ์ ์ ํด์ ๋์๋ค๋ฉด ๐ฏ์ ํ ์คํธ ํ์ ์ด ๋์์ง๋๋ค. โฑ
- There is 1.5 sec UI leg following the pop action. You are not able to touch the screen for the time being.
- If memory leaked happens, leaked views and controllers will be listed on the popup.
- If all the instances deinited, then ok๐ฏ๐ popup will be toasted! ๐ฅช
// self ๊ฐ weak ์ฒ๋ฆฌ ๋์ง ์์ self deinit์ด ํธ์ถ๋์ง ์๋ ๊ฒฝ์ฐ
testClosure = {
print(self)
}
public class BaseView: UIView, DeinitChecker {
public var deinitNotifier: DeinitNotifier?
override init(frame: CGRect) {
super.init(frame: frame)
setDeinitNotifier()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setDeinitNotifier()
}
}
public class BaseViewController: UIViewController, DeinitChecker {
public var deinitNotifier: DeinitNotifier?
override public func viewDidLoad() {
super.viewDidLoad()
setDeinitNotifier()
}
}
DeinitChecker Protocol ์ฑํ ํ ๊ฐ์ฒด ์์ฑ์์์ setDeinitNotifier() ํจ ์ ํธ์ถํด ์ฃผ๋ฉด ๋จ
- ๊ผญ base์ฒ๋ผ ์์ ๊ตฌ์กฐ ์๋์ฌ๋ ์๊ด ์์ ๊ทธ๋ฅ ํ๋กํ ์ฝ๋ง ์ฒดํํ๋ฉด ๊ทธ ๊ฐ์ฒด๋ ์ฒดํฌ๊ฐ ๊ฐ๋ฅํด ์ง
- ๋จ ํธ์, ํ์ ํ๋ ๊ธฐ์ค์ด ๋๋ ViewController๋ ํ๋๋ ๊ผญ ์์ด์ผ ํจ
DeinitManager.shared.isRun = true ํด์ค ํ ๋์ํจ ๋๊ณ ์ถ์๋ false ์ฒ๋ฆฌ ํ๋ฉด ๋จ
์๋์ฒ๋ผ weak ์ฒ๋ฆฌ ์๋์ด ์์ ์ ๋ฉ๋ชจ๋ฆฌ ํด์ ํด์ง ์์์ ์ค๋ฅ ํ์
์์ฑ๋จ
testClosure = {
guard let `self` = self else { return }
print(self)
}
public final class DeinitManager {
final class VCInfoClass: Equatable {
static func == (lhs: VCInfoClass, rhs: VCInfoClass) -> Bool {
lhs === rhs
}
final class ObjectInfo: Equatable {
static func == (lhs: ObjectInfo, rhs: ObjectInfo) -> Bool {
lhs === rhs
}
var name: String
var count: Int = 1
init(name: String) {
self.name = name
}
}
var address: Int
var vcName: String
var objects = [ObjectInfo]()
init(_ vc: String, address: Int) {
self.vcName = vc
self.address = address
}
}
static let shared: DeinitManager = { return DeinitManager() }()
private init() {}
public var isRun: Bool = false {
didSet {
if isRun {
UIViewController.enableSwizzleMethodForViewWillDisappear()
startMemoryReport()
}
else {
removeAll()
UIViewController.disableSwizzleMethodForViewWillDisappear()
}
}
}
private var workItem: DispatchWorkItem? // ์์
์ ๊ด๋ฆฌํ ๋ณ์
private var vcInfos = [VCInfoClass]()
private(set) var isMemoryRepory: Bool = false
private var memoryLabel: UILabel?
private func removeAll() {
self.vcInfos.removeAll()
}
public func checkPopViewController(_ name: String, address: Int) {
guard isRun else { return }
guard self.vcInfos.last?.vcName == name, self.vcInfos.last?.address == address else { return }
// print("checkPopViewController name: \(name), address: \(address)")
// ์ด์ ์์
์ทจ์ (์๋ค๋ฉด)
workItem?.cancel()
// ์ ์์
์์ฑ
workItem = DispatchWorkItem { [weak self] in
guard let self else { return }
if self.vcInfos.contains(where: { $0.vcName == name && $0.address == address }) {
let string = """
------ Warning -------
๐๐ป \(name) ๐๐ป
๐ฃ deinit Check Fail -----
โฌ๏ธ ํด์ ๋์ง ์์ ๋ฉ๋ชจ๋ฆฌ๋ฅผ ๋นผ์ฃผ์ธ์ -----
--------------------------------
\(name)
-------------------------------
"""
print(string)
self.makeView(value: string)
}
}
// ์์
๋์คํจ์น
if let workItem = workItem {
DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: workItem)
}
}
public func pushViewController(_ name: String, address: Int) {
guard isRun else { return }
print()
print(" ๐งฒ pushViewController \(name) ๐งฒ address: \(address)")
self.vcInfos.append(VCInfoClass(name, address: address))
}
public func popViewController(_ name: String, address: Int) {
guard isRun else { return }
print()
print(" โด๏ธ popViewController \(name) โด๏ธ ")
checkDeinit(name, address: address)
}
public func initObject(_ name: String) {
guard isRun else { return }
guard let vcInfo = vcInfos.last else { return }
if let viewInfo = vcInfo.objects.first(where: { $0.name == name }) {
viewInfo.count += 1
print("add Object \(name) count: \(viewInfo.count)")
}
else {
vcInfo.objects.append(.init(name: name))
print("add Object \(name) count: 1")
}
}
public func deinitObject(_ name: String) {
guard isRun else { return }
guard let vcInfo = vcInfos.last else { return }
if let viewInfo = vcInfo.objects.first(where: { $0.name == name }) {
viewInfo.count -= 1
print("deinit Object \(name) count: \(viewInfo.count)")
}
}
private func checkDeinit(_ name: String, address: Int) {
guard isRun else { return }
workItem?.cancel()
workItem = nil
var objects = [VCInfoClass.ObjectInfo]()
var removeVi = [VCInfoClass]()
for vi in self.vcInfos.reversed() {
objects.append(contentsOf: vi.objects)
removeVi.append(vi)
if vi.vcName == name, vi.address == address { break }
}
let deadline = Double(objects.count) * 0.3
DispatchQueue.main.asyncAfter(deadline: .now() + deadline) {
print()
print(" โ ๏ธ deinit checker start โ ๏ธ")
var list: [String] = [String]()
list.reserveCapacity(objects.count)
for vi in objects {
if vi.count > 0 {
list.append("\t\(vi.name) count: \(vi.count)")
}
}
self.vcInfos.removeAll(where: { removeVi.contains($0) })
if list.count > 0 {
let string = """
------ Warning -------
๐๐ป \(name) ๐๐ป
๐ฃ deinit Check Fail -----
โฌ๏ธ ํด์ ๋์ง ์์ ๋ฉ๋ชจ๋ฆฌ๋ฅผ ๋นผ์ฃผ์ธ์ -----
--------------------------------
\(list.joined(separator: "\n"))
-------------------------------
"""
print(string)
self.makeView(value: string)
}
else {
self.checkOK(name)
}
print(" โ ๏ธ deinit checker end โ ๏ธ")
print()
}
}
}