srus2017
srus2017

Reputation: 404

@escaping completionHandler - How does memory management work for them?

In my app 2 controller uses same data to display, and server call is made from each controller class to get data from server.

If user navigates back and forth another request that both fetch same data is being made before first one is completed .

I want to optimize this by using only single server call and prevent another if one is in progress.

To do this I am using a singleton class which takes care of getting data and preventing 2 simultaneous request from server for same data.

If controller's ViewWillDisappear gets call I don't want to notify that controller once data is received from server. Just like we remove observers in viewWillDisappear and add observer in viewWillAppear

Below is my code -

final class SingletonDataManager { 
        typealias MYCompletionHandler = (_ persons:[Person]?, _ error: NSError?) -> Void

        // MARK: Shared Instance
        static let sharedInstance = SingletonDataManager()

        // MARK: Concurrent queue f

        fileprivate let concurrentQ = DispatchQueue(label: "com.test.concurrentQueue",
                                                                      qos: .userInitiated,
                                                                      attributes: .concurrent)

        fileprivate var _handler: MYCompletionHandler?
        fileprivate var handler: MYCompletionHandler? {
            get {
                return concurrentQ.sync {
                    return _handler
                }
            }

            set {
                concurrentQ.async(flags: .barrier){ [weak self] in
                    self?._handler = newValue
                }
            }
        }

        fileprivate var result:(persons: [Person]?, error: NSError?) {
            didSet {
                if let hndlr = handler {
                    hndlr(result.persons, result.error)
                    self.isInProgress = false
                }
            }
        }

        fileprivate var _isInProgress = false
        fileprivate var isInProgress: Bool {
            get {
                return concurrentQ.sync {
                    return _isInProgress
                }
            }

            set {
                concurrentQ.async(flags: .barrier){ [weak self] in
                    self?._isInProgress = newValue
                }
            }
        }


        // MARK:- init()
        private init() {

        }

        deinit {
            print(" destroyed")
        }


        internal func getData(_ onCompletion: @escaping MYCompletionHandler) {

            if self.isInProgress == true { 
                self.handler = onCompletion
            } else {
                       NetworkManager.sharedInstance().fetchDataFromServer({ [weak self] (data, error) in 
                        DispatchQueue.main.async {
                            self?.result = (data, error)
                        }
                    })
                }
            } 
    }

    }

And Controller class 1 - ViewControllerA

class ViewControllerA {

func getPersons() {
SingletonDataManager.sharedInstance.getData(onCompletion: { [weak self] (persons, error) in

})

override func viewWillAppear(_ animated: Bool) {
   getPersons()
}
}

Controller class 2 - ViewControllerB

class ViewControllerB {

func getPersons() {
SingletonDataManager.sharedInstance.getData(onCompletion: { [weak self] (persons, error) in

})

override func viewWillAppear(_ animated: Bool) {
   getPersons()
}
}

User can navigates between ViewControllerA and ViewControllerB. Here there are two cases - User may navigates to ViewControllerB from ViewControllerA before network request initiated by controller ViewControllerA completed. OR After moved after request completion.

I want to solve case 1 by preventing multiple server request. To achieve this I am using one bool variable in my singleton class isInProgress and setting its value thread-safe way.

I have one more variable **handler** which keeps most recent completion handler that needs to be called. This is also thread-safe.

Everything is working as expected but I want to make sure that completionHanlder which are not called won't take extra memory.

Do they released when I assign new completion handler to my **handler variable?** or will eat up memory contineously?

Is it the right way to solve this problem?

Or Should I use NSNotification here. What will be the best approach?

I want to free memory taken by results array in case of low memory warning. How to handle this in ARC.

Hope I explain my problem properly now. I have asked this questions many times but didn't receive good response because of bad explanation of problem. Please let me know if more clarification required.

Thanks.

Upvotes: 1

Views: 313

Answers (1)

srus2017
srus2017

Reputation: 404

I finally figured out!

  1. @escaping closure gets call even after class deinitialized, But won't it will get nil instance variable if you properly managed memory by using weak self.

  2. If we don't call @escaping closure at all it doesn't occupy any memory.

Sample Code

final class DataSource {

    typealias RequestCompleted = (_ data:String?, _ error: NSError?) -> Void

    // MARK: Shared Instance
    static let sharedInstance = DataSource()

    // MARK: Concurrent queue for thread-safe array

    fileprivate let concurrentQ = DispatchQueue(label: "com.test.concurrentQueue",
                                                qos: .userInitiated,
                                                attributes: .concurrent)

    // MARK:- Local Variable
    fileprivate var _dataHandler: RequestCompleted?
    fileprivate var dataHandler: RequestCompleted? {
        get {
            return concurrentQ.sync {
                return _dataHandler
            }
        }

        set {
            concurrentQ.async(flags: .barrier){ [weak self] in
                self?._dataHandler = newValue
            }
        }
    }

    fileprivate var result:(data: String?, error: NSError?) {
        didSet {
            if let handlr = dataHandler {
                handlr(result.data, result.error)
                self.isRequestInProgress = false
            }
        }
    }

    fileprivate var _isRequestInProgress = false
    fileprivate var isRequestInProgress: Bool {
        get {
            return concurrentQ.sync {
                return _isRequestInProgress
            }
        }

        set {
            concurrentQ.async(flags: .barrier){ [weak self] in
                self?._isRequestInProgress = newValue
            }
        }
    }


    // MARK:- Private init()
    private init() {

    }

    deinit {
        print("Deinitialized")
    }


    internal func fetchData(_ onCompletion: @escaping RequestCompleted) {
        self.dataHandler = onCompletion
        if self.isRequestInProgress == true { print("TTT: In Progress")
            return
        } else {

            self.isRequestInProgress = true

            DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(20)) {
                // Code
                 self.result = ("Done", nil)
            }
        }
    }
}

ViewController

class ViewController: UIViewController {
    override func viewWillAppear(_ animated: Bool) {
        print("ViewController 1 Function")

        DataSource.sharedInstance.fetchData { (name, error) in
            print("ViewController 1 Handler")
        }
    }
}

ViewController2

class ViewController2: UIViewController {
    var str = "Test"
    var arr = [1, 2, 3]

    override func viewWillAppear(_ animated: Bool) {
        print("ViewController 2 Function")

        DataSource.sharedInstance.fetchData { [weak self] (name, error) in
            print("ViewController 2 Handler")
            print("CCCC\(self?.arr ?? [0])")
            print("SSSS\(self?.str ?? "Happy")")
        }
    }

    deinit {
        print("VC2.. deinit")
    }
}

ViewController3

class ViewController3: UIViewController {
    override func viewWillAppear(_ animated: Bool) {
        print("ViewController 3 Function")

        DataSource.sharedInstance.fetchData { (name, error) in
            print("ViewController 3 Handler")
        }
    }

    deinit {
        print("VC3.. deinit")
    }

}

And For Low memory warning-

Since in swift

collection types and tuple are value type,

I will either remove person objects or set tuple to nil in case of low memory warning. It won't impact data on my Controller view.

Upvotes: 1

Related Questions