Skip to content

pkh0225/DeinitChecker

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

22 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

๐Ÿšฅ Deinit Manager

SwiftPM compatible

๋ชฉํ‘œ

  • ๋ชจ๋“  ํ‘ธ์‹œ&ํŒ ์ด๋ฒคํŠธ์— ๋Œ€ํ•ด ์ง๊ด€์ ์œผ๋กœ ๋ฉ”๋ชจ๋ฆฌ ํ•ด์ œ๋ฅผ ํ™•์ธํ•˜์„ธ์š”!
  • ๐Ÿšง Check Memory Leak in every push & pop events!

๐Ÿš ์ž‘๋™ ๋ฐฉ์‹

  1. Navigation Push ํ›„ Pop์„ ํ•˜๋ฉด Pop ํ›„ ์•ฝ 1.5์ดˆ ๊ฐ„ ํ„ฐ์น˜๊ฐ€ ๋ง‰ํ˜€ ํด๋ฆญ์ด ๋ถˆ๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
  2. ๋งŒ์•ฝ ํ•ด์ œ ๋˜์ง€ ์•Š์€ view ์™€ controller๊ฐ€ ์žˆ๋‹ค๋ฉด ํ•ด๋‹น ์ด๋ฆ„์ด ํŒ์—…์— ๋ฆฌ์ŠคํŠธ๋ฉ๋‹ˆ๋‹ค.
  3. ๋งŒ์•ฝ ๋ชจ๋“  ์ธ์Šคํ„ด์Šค๊ฐ€ ์ •์ƒ ํ•ด์ œ ๋˜์—ˆ๋‹ค๋ฉด ๐Ÿ’ฏ์  ํ† ์ŠคํŠธ ํŒ์—…์ด ๋„์›Œ์ง‘๋‹ˆ๋‹ค. โ›ฑ

๐Ÿš How it works

  1. There is 1.5 sec UI leg following the pop action. You are not able to touch the screen for the time being.
  2. If memory leaked happens, leaked views and controllers will be listed on the popup.
  3. If all the instances deinited, then ok๐Ÿ’ฏ๐Ÿ†— popup will be toasted! ๐Ÿฅช

Test Cases

blogimg

โ˜น๏ธŽ deinit fail

blogimg

// self ๊ฐ€ weak ์ฒ˜๋ฆฌ ๋˜์ง€ ์•Š์•„ self deinit์ด ํ˜ธ์ถœ๋˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ
testClosure = {
            print(self)
        }

โ˜บ๏ธŽ deinit success

blogimg


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)
}


Core functions

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()
        }
    }
}