Nominalista
Nominalista

Reputation: 4840

Object is deinited before flatMap is called

For an example, there is simple task which makes network call and then maps the result.

class Task {

    func execute() -> Single<String> {
        return doSomeRequest().flatMap { [weak self] data in
            if let string = self?.map(data: data) {
                return .just(string)
            }
            return .error(Error())
        }
    }

    func doSomeRequest() -> Single<Data> {
        // makes network call and returns Data 
    }

    func map(data: Data) -> String? {
        // maps Data to String 
    }
}

And there is function that uses this Task:

func fetchSomeString() {
    Task().execute().subscribe(onSuccess: { string in
        print(string)
    }).disposed(by: disposeBag)
}

The problem is that Task is deinitialized before even get response from doSomeRequest(), due to weak self in flatMap. One of the solution would be to store Task instance as a property, but let's say there is no place in my case for that.

Is there any solution that doesn't lead to memory leaks in flatMap, but executes Task properly?

Upvotes: 2

Views: 196

Answers (4)

iWheelBuy
iWheelBuy

Reputation: 5679

One of the solution would be to store Task instance as a property, but let's say there is no place in my case for that.

There is one great operator which can store Task instance as a property and it doesn't need a place to store it. The operator is called Using.

Here is a little example:

flatMapLatest({ (room: String?) -> Observable<WebSocketServiceValue> in
    switch room {
    case .some(let room):
        return Observable
            .using({ WebSocketServiceInstance(room: room) }, observableFactory: { $0.observable })
    case .none:
        return Observable<WebSocketServiceValue>
            .just(WebSocketServiceValue.connected(false))
    }
})

I observer a room which is an optional String. If there is a valid room - I create a WebSocketServiceInstance and observe its signals. As soon as the room is changed - WebSocketServiceInstance is deinited because of flatMapLatest operator. So, the instance lives as long as you have a connection.

In your example your Task class might live as long as the operation executes if you use Using operator. The only thing is required - Task class should conform to Disposable. But it is very easy:

extension WebSocketServiceInstance: Disposable {

    func dispose() {
        socket?.delegate = nil
        socket?.close()
        socket = nil
    }
}

Upvotes: 1

zneak
zneak

Reputation: 138051

It seems to me that you simply have to use a strong reference to self instead of a weak reference.

I assume that your closure is evaluated once and then discarded. If that is the case, then you will temporarily have a strong reference cycle that will self-break at the end of the operation.

Upvotes: 1

Cristik
Cristik

Reputation: 32806

You could make map(data:) be a static method. This way you don't need the task instance, and it might make more sense than an instance method if the map one doesn't make use of any instance members.

class Task {

    func execute() -> Single<String> {
        return doSomeRequest().map { [weak self] data in
            guard let string = Task.map(data: data) else {
                throw Error() 
            }
            return string
        }
    }

    func doSomeRequest() -> Single<Data> {
        // makes network call and returns Data 
    }

    static func map(data: Data) -> String? {
        // maps Data to String 
    }
}

I also made some changes to you flow by calling Single.map and throwing the error, this is a little more readable.

Upvotes: 1

Lukas
Lukas

Reputation: 3433

Yes 'one solution is to store Task instance as a property' or you can pass/essentially store the instance in your execute function.

static func execute(_ task: Task) -> Single<String> {
    return task.doSomeRequest().flatMap { data in
        if let string = task.map(data: data) {
            return .just(string)
        }
        return .error(Error())
    }
}

Then call it like:

Task.execute(Task()).subscri...

Upvotes: 1

Related Questions