This article has participated in the call for good writing activity, click to view:Back end, big front end double track submission, 20,000 yuan prize pool waiting for you to challenge!

The WanAndroid client written by RxSwift is now open source

Previously, I did not participate in the Nuggets’ July good writing activities immediately after finishing the daily writing activities in June. We mainly did the following things:

  • Self-rehabilitation, daily writing, so THAT I have not read a lot of nuggets articles, I need to read and learn.

  • Wanandroid client code CodeReview, written in a hurry before, many details are not implemented.

  • Gather your thoughts and figure out how to start your Thoughts in July.

Currently RxSwift written wanAndroid client has been open source –Project linkMake sure to switch to the play_Android branch.

Attached is an effect picture:

This article benefits from the wanAndroid client code CodeReview, which makes heavy use of closures with RxSwift, leading to circular references.

So much nonsense, let’s get to the point.

The Timer causes circular references

Why can’t timers be destroyed

While the vast majority of circular references are caused by strong references between objects, timers are not:

The main thread runloop is not destroyed during program execution. The runloop refers to the timer, and the timer is not destroyed automatically. The timer references the target, and the target is not destroyed.

Timers in Swift lead to circular references, which a novice would probably get stuck in.

Apple itself is largely to blame for the timer-induced circular references.

Fortunately, Apple introduced a new API for Timer in iOS 10 to address this issue. I strongly recommend using the new API for Timer tasks if your App engineering profile supports them after iOS 10!

After iOS 10

Let’s take a look at the source of Timer:

Public /* Not inherited*/ Init (timeInterval ti: timeInterval, Invocation: NSInvocation, repeats yesOrNo: Bool) /// Open class func scheduledTimer(timeInterval ti: TimeInterval, invocation: NSInvocation, repeats yesOrNo: Bool) -> Timer /// Public /*not inherited*/ init(timeInterval ti: timeInterval, target aTarget: inherited Any, selector aSelector: Selector, userInfo: Any? , repeats yesOrNo: Bool) /// It is not recommended to use open Class func scheduledTimer(timeInterval ti: timeInterval, target aTarget: Any, selector aSelector: Selector, userInfo: Any? , repeats yesOrNo: /// Creates and returns a new NSTimer object initialized with the specified block object. This Creates and returns a new NSTimer object initialized with the specified block object timer needs to be scheduled on a run loop (via -[NSRunLoop addTimer:]) before it will fire. /// - parameter: TimeInterval The number of seconds between firings of The timer. If seconds is less than or equal to 0.0, This method puts the nonnegative value of 0.1 milliseconds instead /// -parameter: repeats If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires. /// - parameter: block The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references @available(iOS 10.0, *) public /* Not inherited*/ init(timeInterval interval: timeInterval, repeats: Bool, block: @escaping (Timer) -> Void) /// / Creates and returns a new NSTimer object initialized with the Specified block object and schedules it on the current run loop in the default mode. /// - parameter: Ti The number of seconds between firings of The timer. If seconds is less than or equal to 0.0, This method puts the nonnegative value of 0.1 milliseconds instead /// -parameter: repeats If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires. /// - parameter: block The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references @ Available (iOS 10.0, *) Open class func scheduledTimer(withTimeInterval interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Void) -> Timer }Copy the code

I explicitly discourage the use of the above four methods, because you might write them in a step-by-step fashion and refer to them in a circular fashion.

Finally, the next two methods, new to the iOS 10 API, create a scheduled task and use a block to call back, so that circular references don’t occur. Note that weak references are requested in blocks.

IOS 10 prior to processing

Although it is already 14 in iOS system, many apps may be compatible with many historical versions, so the above API cannot be used. At this time, we can write a Timer classification to solve the problem of circular reference:

// -parameters: /// -timeInterval: > > < span style = "max-width: 100%; clear: no; TimeInterval, repeats: Bool, with callback: @escaping () -> Void) -> Timer { return scheduledTimer(timeInterval: timeInterval, target: self, selector: #selector(callbackInvoke(_:)), userInfo: callback, repeats: Repeats)} / / / private timer implementation method / / / / / / - Parameter timer: timer @ objc private static func callbackInvoke (_ the timer: Timer) { guard let callback = timer.userInfo as? () -> Void else { return } callback() } }Copy the code

The system API called here is actually number 4 in the source code above:

open class func scheduledTimer(timeInterval ti: TimeInterval, target aTarget: Any, selector aSelector: Selector, userInfo: Any? , repeats yesOrNo: Bool) -> TimerCopy the code

In fact, the Timer task is implemented in the Timer, not in the target. Presumably, the system API after iOS 10 is similar to this implementation.

There’s actually a little trick here: userInfo: Any? The argument passed is of type Any, and we pass callback in the encapsulated input: @escaping () -> Void, is a closure. Closures are of type Any, so we implement the Timer task with guard let callback = timer.userinfo as? () -> Void else {return}

init(timeInterval…) With scheduledTimer (timeInterval…). Differences in methods

  • init(timeInterval…) The created timer cannot be used immediately and needs to be added to NSRunloop for it to work properly

“After creating it, you must add the timer to a run loop manually by calling the addTimer:forMode: Method of the corresponding NSRunLoop object.”

  • scheduledTimer(timeInterval…) The created runloop is added to the current thread’s currentRunloop.

“Schedules it on the current run loop in the default mode.”

WKScriptMessageHandler causes circular references

In the previous article, I talked about Swift intercalling with JS methods, and I talked about doing JS calls to Swift methods in WebView by listening to method handles, and I don’t know if YOU noticed that when you add handles, you don’t use self, But by WeakScriptMessageDelegate middle class to add.

let config = WKWebViewConfiguration()
config.userContentController.add(WeakScriptMessageDelegate(scriptDelegate: self), name: JSCallback)
let preferences = WKPreferences()
preferences.javaScriptCanOpenWindowsAutomatically = true
config.preferences = preferences

let webView = WKWebView(frame: CGRect.zero, configuration: config)
Copy the code

Because if I go straight like thisconfig.userContentController.add(self, name: JSCallback)This will lead to circular references.

The WebViewController does not exit the destructor at all, so it cannot remove the handle to the corresponding listener method:

deinit {
    webView.configuration.userContentController.removeScriptMessageHandler(forName: JSCallback)
}
Copy the code

There is nothing special and WeakScriptMessageDelegate support, just let strong become weak holds:

import Foundation import WebKit class WeakScriptMessageDelegate: NSObject {//MARK:- Private weak var scriptDelegate: WKScriptMessageHandler! //MARK:- Init (scriptDelegate: WKScriptMessageHandler) { self.init() self.scriptDelegate = scriptDelegate } } extension WeakScriptMessageDelegate: WKScriptMessageHandler { func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { scriptDelegate.userContentController(userContentController, didReceive: message) } }Copy the code

There is another way to fix WKScriptMessageHandler’s loop reference problem on the web. That is, during the UIViewController’s declaration cycle, Timer also does this:

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        webView.configuration.userContentController.add(self, name: JSCallback)
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        webView.configuration.userContentController.removeScriptMessageHandler(forName: JSCallback)
    }
Copy the code

But this is not a good way to bind the Controller declaration cycle, because it relies on other classes to trigger the condition, and there are uncontrollable factors in it!


It can be said that the loop reference of Timer and WKScriptMessageHandler is partly caused by the trap designed by Apple’s own API. Then how can we find whether a Controller, a View or a ViewModel has a loop reference?

How do I find out if THE code I’m writing is circular

Cococa framework classes

  1. For classes in the Cocoa framework, since they all inherit from NSObject, there’s a little bit of consistency. Let’s write a class on NSObject so we can get the name of a class:
Extension NSObject {/// / public var className: Public static var className: String {return runtimeType.className} public static var className: String {return String(describing: Public var runtimeType: nsobject. Type {return Type (of: self)}}Copy the code
  1. Write BaseViewController and BaseView, rewritedeinitMethod, using BaseViewController as an example:
Deinit {print("\(className) destroyed ")}}Copy the code
  1. All child Controllers and child views inherit from the base class by pushing to a page and then pop to see if the destructor is printed on the console.

In this way, we can see if the page is not destroyed and then troubleshoot the problem.

Non-cococa class

For classes outside the Cococa framework, we can write a base class. I’ll use BaseViewModel as an example:

Var className: String {set () {set () : set () {set () : set () : set (); Self)} deinit {print("\(className) destroyed ")}} class HotKeyViewModel: BaseViewModel {Copy the code

I also re-deinit in BaseViewModel, and then all the other viewModels inherit BaseViewModel, and I can also print a log to see if the object is destroyed.

Here is the printed log:

Struct to pay attention to

Note that struct objects are created on the stack, not on the heap, so there is no need to manage their destruction, and you can’t even call deinit in a struct at all.

Kingfisher

If you read the Kingfisher5 source code, you’ll see that the meow god problem with strong and weak references to objects is solved using a Delegate class:

public class Delegate<Input, Output> { public init() {} private var block: ((Input) -> Output?) ? public func delegate<T: AnyObject>(on target: T, block: ((T, Input) -> Output)?) { self.block = { [weak target] input in guard let target = target else { return nil } return block? (target, input) } } public func call(_ input: Input) -> Output? { return block? (input) } public func callAsFunction(_ input: Input) -> Output? { return call(input) } } extension Delegate where Input == Void { public func call() -> Output? { return call(()) } public func callAsFunction() -> Output? { return call() } }Copy the code

The Delegate comment in Kingfisher illustrates this nicely:

/// You can create a `Delegate` and observe on `self`. Now, there is no retain cycle inside: /// /// ```swift /// // MyClass.swift /// let onDone = Delegate<(), Void>() /// func done() { /// onDone.call() /// } /// /// // ViewController.swift /// var obj: MyClass? /// /// func doSomething() { /// obj = MyClass() /// obj! .onDone.delegate(on: self) { (self, _) /// // `self` here is shadowed and does not keep a strong ref. /// // So you can release both `MyClass` instance and `ViewController` instance. /// self.reportDone() /// } /// } /// ```Copy the code

It’s a little more cumbersome, but in closures, it’s safe to use self without the [weak self] modifier, and self’s robustness allows for less writing.

Weak or unowned

We might use the object itself in the closure of an object, usually with the [weak target] modifier. In fact, another modifier is [unowned target]. What’s the difference?

Here’s a quote from the Internet:

In addition to weak, another Swift identifier that screams a similar “don’t quote me” identifier to the compiler is unowned. What’s the difference? If you write objective-C all the time, unowned is more like unsafe_unretained, and weak is the same as weak. In plain English, the unowned setting remains an “invalid” reference to the freed object even if the original reference has been freed. It cannot be Optional and will not be referred to nil. If you try to call the referenced method or access a member attribute, the program will crash. Weak is friendlier, and members marked as weak will automatically become nil after the reference is released (so the variable marked as @weak must be Optional). As for the choice of using the two, Apple advises us to use unowned if we can be sure that the user will not be released during the visit. If there is a possibility that the user will be released, we choose weak.

I’m just asking if you’re tired of reading it, okay?

Let me explain my understanding:

  • unowned use, need a clear understanding of the target object life cycle, in case of bad call, it will roll over and cause crash problems. In short, it’s hair scratching and hair losing.

  • Weak: You may need to use guard to protect target. In short, you need to write more code but not lose your hair.

So, I choose to write more code without losing the weak part of my hair. If I use the Meow-god Delegate class, I can also write [weak self] without adding other code.

WeakProxy

Cocoa framework for strong and weak reference loop application, we can write a relatively general middle layer to deal with:

class WeakProxy: NSObject { weak var target: NSObjectProtocol? init(target: NSObjectProtocol) { self.target = target super.init() } override func responds(to aSelector: Selector!) -> Bool { return (target? .responds(to: aSelector) ?? false) || super.responds(to: aSelector) } override func forwardingTarget(for aSelector: Selector!) -> Any? { return target } }Copy the code

This utilizes the message forwarding mechanism in NSObject to ensure that methods are delivered correctly, while eliminating circular references caused by strong references with weak references.

If to add a bit of extension WeakProxy (implementation) is actually deal, is can be replaced WeakScriptMessageDelegate:

extension WeakProxy: WKScriptMessageHandler { func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { (target as? WKScriptMessageHandler)? .userContentController(userContentController, didReceive: message) } }Copy the code

Use of WeakProxy in WebView:

Let config = WKWebViewConfiguration() // config.userContentController.add(WeakScriptMessageDelegate(scriptDelegate: self), name: JSCallback) config.userContentController.add(WeakProxy(target: self), name: JSCallback) let preferences = WKPreferences() preferences.javaScriptCanOpenWindowsAutomatically = true config.preferences = preferences let webView = WKWebView(frame: CGRect.zero, configuration: config)Copy the code

conclusion

  • Most of the circular references are caused by the strong references between objects, and the solution of the strong references between objects is the fundamental solution to the problem.

  • The loop reference is special because the main thread’s runloop refers to the Timer and the Timer is not destroyed automatically. The timer references the target, and the target is not destroyed.

  • Starting from Timer and WKScriptMessageHandler, this paper introduces an intermediate layer to reduce the circular reference problems caused by strong references. Delelgate class in Kingfisher and general WeakProxy are good experiences and examples to share.

  • By rewriting the deinit method of the class base class, through inheritance log way to check the destruction of the object, which is helpful to analyze the problem of circular reference.

Reference article:

Understand why NSTimer loop references

Memory management, weak and unowned

Summary of iOS caton monitoring scheme