1, What’s the closure?

As an iOS developer you will be familiar with blocks in Objective-C, and in other development languages closure is also called lambdas, etc. In short, a closure is a stand-alone function that typically captures and stores references to any constants and variables defined in its context.

Closure syntax:

{ (parameters) -> return type in
    statements
}
Copy the code

Closure can use constant-form arguments, variable-form arguments, and input-output arguments, but cannot set default values. Variable-form arguments can also be used, but only at the end of the row argument list. Tuples can also be used as formal parameters and return types.

In fact, global and embedded Functions are also a special kind of closure (see The Swift Programming Language: Functions for more information on Functions). Closures can take one of three forms depending on how they capture values:

  • A global function is a closure that has a name but does not capture any values
  • An embedded function is a closure that has a name and can capture values from its upper function
  • A closure expression is an unnamed closure written by lightweight syntax that captures the value of a constant or variable in its context

2. Various types of closures

If you need to pass a long closure expression to a function as its last actual argument, using a trailing closure enhances the readability of the function. Trailing closures are typically used as function line arguments. Functions such as sorted and map provided by the system are a trailing closure.

2.1 Trailing Closure

Trailing closures are arguments to the function, even though they are written after the parentheses of the function call. When using trailing closures, do not include the closure’s argument labels as part of the function call.

let strList = ["1"."2"."3"."4"."5"]
let numList: [Int] = strList.map { (num) in
    return Int(num) ?? 0
}
Copy the code
2.2 Escaping Closures

Closure escaping is said to occur when a closure is passed to a function as an actual argument, and it is called after the function returns. Escaping closure is generally identified with the @escaping function formal argument.

  • Escape closures are typically used for callbacks to asynchronous tasks
  • The escape closure is called after the function returns
  • Let the closure @escapingMeans that self must be explicitly referenced in the closure
// Escape closure
func requestServer(with URL: String.parameter: @escaping(AnyObject? .Error?). ->Void){}// Trailing closure
func requestServerTrailing(losure: () - >Void){}class EscapingTest {
    var x = 10
    func request(a) {
        // Trailing closure
        requestServerTrailing {
            x = x + 1
        }
        // Escape closure
        requestServer(with: "") { (obj, error) in
            x = x + 1}}}Copy the code
Reference to property 'x' in closure requires explicit use of 'self' to make capture semantics explicit
Copy the code

Modify code:

requestServer(with: "") {[weak self] (obj, error) in
     guard let self = `self` else {
          return
     }
     self.x = self.x + 1
}
Copy the code
  • Practical use of escape closures

For example, IF I want to design a download picture management class, asynchronous download picture after downloading and then return to the main interface display, here can use escape closure to achieve, the core code is as follows:

struct DownLoadImageManager {
    / / the singleton
    static let sharedInstance = DownLoadImageManager(a)let queue = DispatchQueue(label: "com.tsn.demo.escapingClosure", attributes: .concurrent)
    // Escape closure
    // path: the URL of the image
    func downLoadImageWithEscapingClosure(path: String.completionHandler: @escaping(UIImage? .Error?). ->Void) {
        queue.async {
            URLSession.shared.dataTask(with: URL(string: path)!) { (data, response, error) in
                if let error = error {
                    print("error===============\(error)")
                    DispatchQueue.main.async {
                        completionHandler(nil, error)
                    }
                } else {
                    guard let responseData = data, let image = UIImage(data: responseData) else {
                        return
                    }
                    DispatchQueue.main.async {
                        completionHandler(image, nil)
                    }
                }
            }.resume()
        }
    }
    // Ensure that the init method is not called externally
    private init(a){}}Copy the code

Download the image and display:

 let path = "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Finews.gtimg.com%2Fnewsapp_match%2F0%2F12056372662%2F0.jpg&refer=h Ttp%3A%2F%2Finews.gtimg.com & app = 2002 & size = f9999, 10000 & q = a80 & n = 0 & g = 0 n & FMT = jpeg? The SEC = 1618456758 & df7a5cf69ad424954badda9b t = 3 c7fc55f"
DownLoadImageManager.sharedInstance.downLoadImageWithEscapingClosure(path: path) { (image: UIImage? , error:Error?).in
    if let error = error {
        print("error===============\(error)")}else {
        guard let image = image else { return }
        print("Picture download completed, display picture:\(image)")
        let imageView = UIImageView(image: image)
        imageView.layer.cornerRadius = 5}}Copy the code

Although the above code can complete the image download management, in fact, in the project to download and display an image processing is much more complex, here do not do more details, can refer to the official demo: Asynchronously Loading Images into Table and Collection Views

2.3. Auto Closure
  • An automatic closure is an expression package that is automatically created to pass the actual seat argument to a function
  • Automatic closures take no arguments and, when called, return the value of the internally packaged expression
  • Automatic closures can omit the closing braces and replace an explicit closure with a normal expression
  • Automatic closures allow delayed processing, so the code inside the closure does not run until it is called. Great for code that has side effects or hogs resources

If I have a homework management class, the teacher needs to count the homework handed in by students and process the corrected homework at the same time, in order to demonstrate the automatic closure, I use the following code to achieve:

enum Course {
    case spacePhysics // Space physics
    case nuclearPhysics Nuclear physics
    case calculus / / calculus
    case quantumMechanics // Quantum mechanics
    case geology / / geology
}
struct StudentModel {
    var name: String = String(a)var course: Course!
    
    init(name: String.course: Course) {
        self.name = name
        self.course = course
    }
}
// MARK: - Automatic closure
class StudentManager {
    var studentInfoArray: [StudentModel] = [StudentModel] ()// A student handed in his homework
    func autoAddWith(_ student: @autoclosure() - >StudentModel) {
        studentInfoArray.append(student())
    }
    // The teacher finished correcting a student's homework
    func autoDeleteWith(_ index: @autoclosure() - >Int) {
        studentInfoArray.remove(at: index())
    }
}
Copy the code

Where, autoAddWith indicates that a student handed in his homework, and autoDeleteWith indicates that the teacher finished correcting a student’s homework. The general call method is as follows:

let studentManager: StudentManager = StudentManager(a)// Kate Bell handed in her homework
studentManager.autoAddWith(StudentModel(name: "Kate Bell", course: .spacePhysics))
// Kate Bell handed in her homework
studentManager.autoAddWith(StudentModel(name: "Kate Bell", course: .nuclearPhysics))
// Anna Haro handed in her homework
studentManager.autoAddWith(StudentModel(name: "Anna Haro", course: .calculus))

// The teacher finished correcting the first homework
studentManager.autoDeleteWith(0)
Copy the code
2.4, Automatic + Closure + Escaping

If you want automatic closure escape, you can use both @Autoclosure and @escaping.

func autoAddWith(_ student: @autoclosure @escaping() - >StudentModel) {
    studentInfoArray.append(student())
}
Copy the code

3. Closures capture values

We briefly introduced the concepts and basic uses of trailing closures, escape closures, and automatic closures. Here’s how closures capture values. In Swift, value type variables are typically stored in the Stack, while reference types such as func Class Closure are stored in the Heap. The closure capture value essentially stores the value in the Stack to the Heap.

To verify what types of values closure can capture, use the following code as a test:

class Demo: NSObject {
    var test = String()}/ / constant
let index = 10086
/ / variable
var number = 1008611
// Reference type
let demo = Demo(a)var capturel = {
    number = 1008611 - 998525
    demo.test = "block test"
    print("index==========\(index)")
    print("number==========\(number)")
    print("demo.test==========\(demo.test)")
}
number = number + 1
demo.test = "test"
capturel()

// Print the result
// index==========10086
// number==========10086
// demo.test==========block test
Copy the code

In the code above, capturel() is called for constants, variables, and reference types. Closure captures the value of any constant, variable, value type, or reference type. In fact, as an optimization in Swift, Swift may use a copy of the value instead of capturing it when the Closure does not modify or is outside the Closure. Swift also handles memory management operations for variables, which are released when they are no longer needed.

Let’s look at an example of incrementing:

func makeIncrementer(_ amount: Int)- > () - >Int {
    var total = 0
    // Inline functions are a special kind of Closure
    func incrementerClosure(a) -> Int {
        total = total + amount
        return total
    }
    return incrementerClosure
}
Copy the code

In the code above, the total value is captured in incrementerClosure, and when I return incrementerClosure, the function that wraps total is theoretically gone, but incrementerClosure can still capture total. You can conclude that closure captures the value even if the original scope that defined the variable or constant no longer exists.

let incrementerTen = makeIncrementer(10) // () -> Int
incrementerTen() / / 10
incrementerTen() / / 20
incrementerTen() / / 30
let incrementerSix = makeIncrementer(6) // () -> Int
incrementerSix() / / 6
incrementerSix() / / 12
incrementerTen() / / 40
let alsoIncrementerTen = incrementerTen // () -> Int
alsoIncrementerTen() / / 50
Copy the code

In the code above, I call the incrementerTen closure each time +10, which becomes +6 incrementing when I create a new incrementerSix closure, creating a new variable reference.

When a call to alsoIncrementerTen returns a value of 50, you can be sure that Closure is a reference type because alsoIncrementerTen references incrementerTen and they share the same memory. If it is a value type, alsoIncrementerTen returns 10, not 50;

To summarize the closure capture values, use the code above:

  • closureCapturing values is essentially storing values in the Stack area into the Heap area
  • whenClosureWhen a value is not modified or outside the closure, Swift may use a copy of the value instead of capturing it
  • ClosureCapture values even if the original scope in which these variables or constants were defined no longer existsclosureThe value can still be captured
  • If a new closure call is created, a reference to a new independent variable is created
  • Whenever you assign a function or closure to a constant or variable, you’re actually setting the constants and variables to be references to the function and closure, right

4. Closure loop reference

Closure in Swift is a reference type, and we know that reference types in Swift manage their memory through the ARC mechanism. In Swift, strong reference loops, often referred to as circular references, occur when two reference objects hold each other. By default, Swift handles all of the management of captured memory, but that doesn’t stop developers from worrying about memory issues, because circular references are more complex than circular references generated by objects, so they should be used with caution. What are the common situations that cause circular reference problems when using Closure?

4.1,ClosureCapture the circular reference generated by the object

When a Closure is assigned to an instance’s properties and the Closure captures the instance by referring to the instance or its members, a circular reference is generated between the Closure and the instance.

Here I use the Student class to demonstrate. Suppose now the Student needs to do a multiple-choice question, and the teacher decides whether the answer is correct or not according to the answer returned. I’ll do a comparison with Blocks in Objective-C by writing the following code in Xcode:

typedef NS_ENUM(NSInteger, AnswerEnum) {
    A,
    B,
    C,
    D,
};
@interface Student : NSObject
@property (copy, nonatomic) NSString *name;
@property (copy, nonatomic) void (^replyClosure)(AnswerEnum answer);
@end
@implementation Student
- (instancetype)init {
    self = [super init];
    if (self) {
        if (self.replyClosure) {
            self.replyClosure(B);
        }
    }
    return self;
}
@end
Copy the code
@interface Teacher : NSObject
@property (assign, nonatomic) BOOL isRight;
@property (strong, nonatomic) Student *student;
@end

@implementation Teacher
- (instancetype)init {
    self = [super init];
    if (self) {
        self.student.replyClosure = ^(AnswerEnum answer) {
             // Capturing 'self' strongly in this block is likely to lead to a retain cycle
             NSLog(@"%@",self.student.name);
        };
    }
    return self;
}
@end
Copy the code

Capturing ‘self’ strongly in this block is likely to lead to a retain cycle.

Does using closure in Swift also create circular references? I convert objective-C code to Swift:

enum Answer {
    case A
    case B
    case C
    case D
}
class Student: CustomStringConvertible {
    var name: String = String(a)var replyClosure: (Answer) - >Void = { _ in }
    
    var description: String {
        return "<Student: \(name)>"
    }
    
    init(name: String) {
        self.name = name
        print("==========Student init==========\(name)")
        replyClosure(.B)}deinit {
        print("==========Student deinit==========\ [self.name)")}}class Teacher {
    var isRight: Bool = false
    init(a) {
        print("==========Teacher init==========")
        let student = Student(name: "Kate Bell")
        let judgeClosure = { (answer: Answer) in
            print("\(student.name) is \(answer)")
        }
        student.replyClosure = judgeClosure
    }
    
    deinit {
        print("==========Teacher deinit==========")}}Copy the code

The Student class has two properties: Name for the name of the Student and replyClosure for the action of the Student to answer the question and return the answer.

// Call and run the code
Teacher(a)// Print the result
= = = = = = = = = =Teacher init= = = = = = = = = =
= = = = = = = = = =Student init= = = = = = = = = =Kate Bell
= = = = = = = = = =Teacher deinit= = = = = = = = = =
Copy the code

The Student class does not call deinit (). The Student class does not free memory after initialization. Actually inside the judgeClosure, as soon as I call (capture) the student, no matter what the action is, that part of memory is not freed effectively. So why does this happen? Step by step analysis:

  • After I call the closure, the closure catches the value and executesstudent.replyClosure = judgeClosureThen, in memory, their relationship looks like this:

In Swift, class, func, closure are all reference types, so student and judgeClosure point to strong references for various objects in the code above.

Also because the student was captured in the closure, the closure has a strong reference to the student. Finally, when student.replyClosure = judgeClosure is executed, make replyClosure also strong reference to judgeClosure. Student’s reference count is 1 and judgeClosure’s is 2.

  • When out of scope,studentandjudgeClosureThe relationship between the references is as follows:

At this point, only the Closure object’s reference count becomes 1. The Closure then goes on to refer to student, who goes on to refer to his object replyClosure, which goes on to refer to judgeClosure. This creates a reference loop, so memory can’t be freed properly.

#####4.2. The internal implementation of the closure property captures the circular reference generated by self

Class Student = objective-C; class Student = objective-C;

@implementation Student
- (instancetype)init {
    self = [super init];
    if (self) {
       if (self.replyClosure) {
          self.replyClosure = ^(AnswerEnum answer) {
             // Capturing 'self' strongly in this block is likely to lead to a retain cycle
             NSLog(@"%@",self);
          };
       }
    }
    return self;
}
@end
Copy the code

Capturing ‘self’ strongly in this block is likely to lead to a retain cycle.

In Swift, the compiler does not warn, but it also generates circular references. Modify the replyClosure code defined in Student as follows:

lazy var replyClosure: (Answer) - >Void = { _ in
     print("replyClosure self=============\ [self)")}Copy the code

Lazy loading is used to ensure that the replyClosure closure is properly initialized when self is called inside the replyClosure. Modify the code for the call to:

Student(name: "Tom").replyClosure(.B)
Copy the code

Run the code and print the result:

= = = = = = = = = =Student init= = = = = = = = = =Kate Bell
replyClosure self= = = = = = = = = = = = ="Student: Tom>
Copy the code

Since the Student instance and replyClosure are strongly held to each other, there are references between them even when they go out of scope, so memory cannot be freed efficiently. The quote relationship between them is:

In Objective-C, a weak reference is usually used to solve the problem of Block and instance circular reference. The reasons for the circular reference between Block and class instance are not described here. For more details about the use of blocks in Objective-C, see Advanced Programming in Objective-C. IOS and OS X multithreading and memory management and Getting Started with Blocks. So how do you handle circular references in Swift? Depending on the closure and class object lifecycles, different solutions are needed to solve the circular reference problem in Swift.

5. Unreferenced references (unowned)

5.1. Use unowned to handle closures and reference loops for class objects

To make it easier to understand, I modify the Teacher class code:

class Teacher {
    var isRight: Bool = false
    init(a) {
        print("==========Teacher init==========")
        let student = Student(name: "Kate Bell")
        let judgeClosure = { [student] (answer: Answer) in
            print("student===========\(student)")
        }
        student.replyClosure = judgeClosure
    }
    deinit {
        print("==========Teacher deinit==========")}}Copy the code

Closure ‘= student; closure’ = student; closure ‘= student; closure’ = student; closure ‘= student; closure’ = student; closure ‘= student; closure’ = student; closure ‘= student; closure’ = student; closure ‘= student;

Student and Clousre cannot be destroyed because the reference count of student and Clousre is not zero.

For this reference relationship, as mentioned earlier in ARC, it is good to change either side of the loop to unowned or weak. I set student as an unreferenced reference with the following code:

let judgeClosure ={[unowned student] (answer: Answer) in
    print("\(student.name) is \(answer)")}Copy the code

With an ownerless reference, the reference relationship between them looks like this:

Run the code and print:

/ / run
Teacher(a)// Print the result
= = = = = = = = = =Teacher init= = = = = = = = = =
= = = = = = = = = =Student init= = = = = = = = = =Kate Bell
= = = = = = = = = =Student deinit= = = = = = = = = =Kate Bell
= = = = = = = = = =Teacher deinit= = = = = = = = = =
Copy the code

Closure’s memory has been reclaimed. Closure’s memory has been reclaimed. When closure is nil, the student object is reclaimed by ARC, and when student is nil, the teacher loses its usefulness and ARC retrieves its memory.

5.2,unownedIt doesn’t solve all circular reference problems

While unowned can solve the circular reference problem, it does not mean that all of the closure circular reference problems encountered can be solved with unclaimed reference (unowned) :

5.2.1. Example Code 1

Again using the Student object, after the “Kate Bell” Student has answered the question, another Tom has answered the question, and he has chosen C as the code:

var student = Student(name: "Kate Bell")
let judgeClosure ={[unowned student] (answer: Answer) in
     print("student===========\(student)")
}
student = Student(name: "Tom")
student.replyClosure = judgeClosure
student.replyClosure(.C)

// Print the result
// ==========Student init==========Kate Bell
// ==========Student init==========Tom
// ==========Student deinit==========Kate Bell
Copy the code

After running the code, the program crashes with an error: Execution was interrupted, reason: signal SIGABRT. Here’s why:

  • The code first creates a file namedKate BellOf the students,judgeClosureCaptured thisstudentobject
  • whenstudent = Student(name: "Tom")And then, because ofjudgeClosureIs in accordance with theunownedThe way captured at this timejudgeClosureWithin thestudentThe object actually doesn’t exist anymore
  • calledTomthestudentObject refers toreplyClosureclosure
  • callstudent.replyClosure(.C)The time,replyClosurePreviously capturedstudentThe object no longer exists
5.2.1. Example Code 2

So what if I move student.replyclosure = judgeClosure to the front? Modify the code as follows:

var student = Student(name: "Kate Bell")
let judgeClosure ={[unowned student] (answer: Answer) in
    print("student===========\(student)")
}
student.replyClosure = judgeClosure
student = Student(name: "Tom")
student.replyClosure(.C)
// Print the result
// ==========Student init==========Kate Bell
// ==========Student init==========Tom
// ==========Student deinit==========Kate Bell
Copy the code

As you can see, the Student object named “Kate Bell” is destroyed normally, but the Tom student object is not, because the replyClosure closure captures a circular reference to self within it. The quote relationship between them is as follows:

In this case using unowned does not solve the problem of circular references, so an alternative solution for circular references is weak, which tells the closure that it is no longer needed to access the object it captured once it has been freed.

6. Weak references

6.1. Use weak to handle reference loops between closures and class objects

To solve the circular reference problem above, I changed the replyClosure code to:

lazy var replyClosure: (Answer) - >Void ={[weak self] _ in
     print("replyClosure self=============\ [self)")}Copy the code

Re-execute the code and you can see that the Tom student object can be freed normally:

// ==========Student init==========Kate Bell
// ==========Student init==========Tom
// ==========Student deinit==========Kate Bell
// ==========Student deinit==========Tom
Copy the code

After making self a weak reference, the reference relationship between students is:

When I use weak, it means that the object is nil, and capturing and using an Optional value in a closure can cause some unexpected problems, so I need to unwrap:

lazy var replyClosure: (Answer) - >Void ={[weak self] _ in
     guard let value = self else { return }
     print("replyClosure self=============\(value)")}Copy the code

When out of scope, student and closure both have a reference count of 0, and their memory should be reasonably freed. The reference relationship between student and closure is as follows:

In terms of loops between a closure and a class object, how to determine whether a circular reference will occur between the two depends on whether a class object actually has a closure in use. If the class object doesn’t hold the closure, then you don’t have to worry about circular references.

6.2 weak Does not solve all circular reference problems

While unowned and weak can solve the problem of circular references between a Closure and a class instance, this does not mean that you can use this solution in any Closure. On the other hand, abusing weak references can sometimes cause weird headaches and memory problems.

6.2.1 Abusing weak references may cause some unnecessary trouble

Here I also use the Student class as an example to add an assignment for students:

func doHomeWork(a) {
   // Global queue
   let queue = DispatchQueue.global()
   queue.async { [weak self] in
          print("\ [self?.name): Start your homework.")
          sleep(2)
          print("\ [self?.name): Finish your homework.")}}// Simulate doing homework
Student(name: "Kate Bell").doHomeWork()
Copy the code

Print result:

= = = = = = = = = =Student init= = = = = = = = = =
= = = = = = = = = =Student deinit= = = = = = = = = =
Optional("Kate Bell"): Start your homeworknil: Finish your homeworkCopy the code

Why is the job done nil? There is no need to use a weak reference because the closure used in the Async method is not owned by the Student object. Although the closure captures the Student object, there is no circular reference between the two. Instead, the weak reference is released prematurely. But if I use forced unpack here, it may cause the program to Crash again. Therefore, understanding the closure and class object reference relationship correctly and using weak and unowned properly can solve the problem in essence.

6.2.2 use withExtendedLifetime to fix this problem

Is there a way to avoid the problem caused by using weak incorrectly? Here you can use the withExtendedLifetime function provided by Swift, which takes two arguments: The first argument is the object to prolong life, and the second object is clousre. The first argument will live in memory until the closure returns.

let queue = DispatchQueue.global()
queue.async { [weak self] in
   withExtendedLifetime(self) {
      print("\ [self?.name): Start your homework.")
      sleep(2)
      print("\ [self?.name): Finish your homework.")}}Copy the code

Recompile the code and print the result:

= = = = = = = = = =Student init= = = = = = = = = =
Optional("Kate Bell"): Start your homeworkOptional("Kate Bell"): Finish your homework= = = = = = = = = =Student deinit= = = = = = = = = =
Copy the code
6.2.3 improve withExtendedLifetime syntax

Although withExtendedLifetime solves the problem of weak references, it can be cumbersome if there are many places to access objects in this way. One solution is to wrap withExtendedLifetime and extend Optional:

extension Optional {
    func withExtendedLifetime(_ body: (Wrapped) - >Void) {
        guard let value = self else { return }
        body(value)
    }
}
Copy the code

The code to call:

func doHomeWork(a) {
    // Global queue
    let queue = DispatchQueue.global()
    queue.async { [weak self] in
        self.withExtendedLifetime { _ in
            print("\ [self?.name): Start your homework.")
            sleep(2)
            print("\ [self?.name): Finish your homework.")}}}Copy the code

The final print is the same as before, and I can call it elsewhere:

= = = = = = = = = =Student init= = = = = = = = = =
Optional("Kate Bell"): Start your homeworkOptional("Kate Bell"): Finish your homework= = = = = = = = = =Student deinit= = = = = = = = = =
Copy the code

This article mainly introduces the basic concepts of closure, the types of closure, the memory problems between closure and class objects and their solutions. If you find any mistakes in my understanding, please point out.