Recently, I encountered various problems when I was looking at variables intercepted by Swift closure. After summarizing, I found that it was mainly caused by thinking about Swift problems in the OC era. Take this opportunity to first review the details of blocks in OC and sort out the issues related to closures in Swift. Whether you are currently using OC or Swift, or switching from OC to Swift, you can read this article and communicate with me.

# block of OC

OC blocks have been introduced in many related articles, the main difficulty lies in the __block modifier and principle, and circular reference problems. Let’s start with a few examples of __block modifiers, and finally look at circular references. The discussion here is based on ARC.

Basic type of interception

int value = 10;
void(^block)() = ^{
NSLog(@"value = %d", value);
};
value = 20;
block();

// 打印结果是:"value = 10"
Copy the code

An OC block intercepts external variables, and an internal copy of the block is made for basic data types such as int. In simple terms, this implementation looks like this:

struct block_impl {
// Other content
int value;
};
Copy the code

Because a block contains a copy of the intercepted variable, modifying the variable after the block is generated does not affect the intercepted variable. This variable cannot be modified inside the block.

Modifying base Types

If we want to modify a intercepted primitive variable in a block, we need to mark it as __block:

__block int value = 10;
void(^block)() = ^{
NSLog(@"value = %d", value);
};
value = 20;
block();

// 打印结果是:"value = 20"
Copy the code

This is because block holds a pointer to a variable marked __block when it intercepts it. In a nutshell, the implementation looks something like this:

struct block_impl {
// Other content
block_ref_value *value;
};

struct block_ref_value {
int value; // This is where the intercepted value is stored.
};
Copy the code

Since there is always a pointer to value in a block, changes to it inside the block can affect variables outside the block. Because the block modifies that external variable and not a copy of it.

The above example of a block implementation is a simplified model, which is not the case, but is similar in nature. In general, only variables that are modified by a __block modifier are mutable when intercepted by a block. For a detailed explanation of this, please refer to these three articles:

  • IOS OC language: Underlying implementation principle of BlockB: This is covered in great detail__blockImplementation principle of
  • Block reference loop (ARC & non-Arc) : This describes the underlying implementation of Block and circular reference problems.
  • Do you really understand __block modifiers?: This is an introduction I wrote earlier__blockPrinciple of the article, the content will be more detailed.

Intercepted a pointer

Block intercepting Pointers is similar to intercepting primitive types, but slightly more complex. Let’s start with the simplest example.

Person *p = [[Person alloc] initWithName:@"zxy"];
void(^block)() = ^{
NSLog(@"person name = %@", p.name);
};

p.name = @"new name";
block();

// 打印结果是:"person name = new name"
Copy the code

Int capturedValue = value; This code, analogous to Pointers, will also have code inside a block like this: Person *capturedP = p; . In ARC, this is actually a strong reference to retain p outside the block.

Since p inside the block and p outside the block refer to the same memory address. Therefore, modifying p attributes outside the block will still affect p intercepted inside the block.

It’s important to note that p here is still not variable. Changing the name of p does not change p, but only changes the properties inside p:

Person *p = [[Person alloc] initWithName:@"zxy"];
void(^block)() = ^{
p.name = @"new name"; //OK, no change to p
p = [[Person alloc] initWithName:@"new name"]; // Error compiling
NSLog(@"person name = %@", p.name);
};

block();
Copy the code

Change the pointer

In a similar way to the __block modifier for primitive types, a block intercepts a pointer that it modifies as a pointer to that pointer. For example, let’s modify the previous example:

__block Person *p = [[Person alloc] initWithName:@"zxy"];
void(^block)() = ^{
NSLog(@"person name = %@", p.name);
};

p = nil;
block();

"Person name = (null)"
Copy the code

Now, inside the block there’s a pointer to external p, and once P is set to nil, that pointer inside points to nil. So the print result is null.

__block and strong references

Remember when I was asked in an interview if __block would retain variables? The answer is: yes. In principle, __block-modified variables are encapsulated within a structure that holds a strong reference to that structure. This is true for both primitive types and Pointers. From practical examples:

Block block;
if (true) {
__block Person *p = [[Person alloc] initWithName:@"zxy"];
block = ^{
NSLog(@"person name = %@", p.name);
};
}
block();

// "person name = zxy"
Copy the code

If there is no retain pointer P marked __block, then the override should get nil.

Avoid circular references

Regardless of whether an object is marked __block, once the block intercepts it, it is strongly referenced. So, to determine whether a circular reference has occurred, you just need to determine whether the object that the block intercepts also holds the block. If the object does need to hold the block directly or indirectly, then we need to avoid the block strong-referencing the object. The solution is to use the __weak modifier.

// Block is an attribute of self

id __weak weakSelf = self;
block = ^{
// Use weakSelf instead of self
};
Copy the code

Blocks do not strongly reference objects marked __weak, only weak references to them. To prevent operations within a block from freeing wself, it can be strong-referenced first. This method has a nice name called weak-strong dacne and can be implemented by referring to @Strongify and @Weakify in RAC.

OC block summary

Simply put, unless marked __weak, a block will always strongly reference any captured object. __block, on the other hand, captures the pointer itself, not another pointer to the object. That is, changes to objects decorated with a __block inside and outside the block affect each other.

If you want to avoid the circular reference problem, first determine which objects the block refers to, then determine whether those objects directly or indirectly hold the block, and if so, mark those objects as __weak to avoid the block strong-referencing them.

Swift closures

__block in OC is a nasty modifier. Not only is it not easy to understand, but it behaves very differently in ARC and non-ARC. __block modifiers essentially modify intercepted variables within closures by intercepting Pointers to them.

In Swift, this is called the interception variable reference. Closures intercept references to variables by default, which means that all variables have a __block modifier by default.

var x = 42
let f = {
// [x] in // If uncommented, the result is 42
print(x)
}
x = 43
f() // The result is 43
Copy the code

If the intercepted variable is a reference, as with OC, then there is a reference to a reference inside the closure:

var block2: (() -> ())?
if true {
var a: A? = A()
block2 = {
print(a? .name) } a =A(name: "new name") } block2? (a)// "Optional("new name")"
Copy the code

If you write variables in the intercept list, there will be a strong reference to the object inside the block, which is the same effect as writing nothing in OC:

var block2: (() -> ())?
if true {
var a: A? = A()
block2 = {
[a] in
print(a? .name) } a =A(name: "new name") } block2? (a)// "Optional("old name")"
Copy the code

Swift automatically holds references to intercepted variables so that they can be modified directly within the block. However, in some special cases, Swift will make some optimizations. As seen in the previous OC analysis of __block, holding a reference to a variable is definitely more expensive than holding it directly. So Swift will automatically determine if you’ve changed a variable inside or outside of a closure. If not, the closure holds the variable directly, even if you don’t explicitly unload it from the capture list. Here’s a quote from Swift’s official document:

As an optimization, Swift may instead capture and store a copy of a value if that value is not mutated by or outside a closure.

Swift loop reference

Closures have strong references to objects regardless of whether variables are explicitly written to the capture list. If the closure is a property of an object, and the object itself, or a property of the object, is captured in the closure, this results in a circular reference. This is exactly the same as OC. The solution is to mark captured variables as weak or unowned in the capture list.

Here’s an example to note about Swift’s circular reference:

class A {
var name: String = "A"
var block: (() -> ())?

// Other methods
}

var a: A? = A(a)var block = {
print(a? .name) } a?.block = block a =nil
block()
Copy the code

We first create variable A of optional type, then create a closure variable and assign it to the block property of A. The closure will intercept a inside, but will this result in circular references?

The answer is no. On the surface, though, the object’s closure properties capture the object itself. But if you run the above code, you’ll see that the object’s deinit method is actually called and instead of printing “A” it prints “nil.”

This is because we have ignored the optional type factor. Here a is not an object of type A, but an optional type variable that encapsulates an instance object of A inside. Closure intercepts optional type variable A, and when you say a = nil, you don’t release variable A, you release instance objects of type A contained in A. So the deinit method of A is going to execute, and when you call A block, because you’re using an optional chain, you’re going to get nil, and if you use forced decapsulation, it’s going to crash.

If you want to create an artificial circular reference, the code would look like this:

var block: (() -> ())?
if true {
var a = A()
block = {
print(a.name)
}
a.name = "New Name"} block! (a)Copy the code

Weak-Strong Dance

To avoid the weak variable being released early in the closure, we need to strongly reference it at the beginning of the block. This is explained in the OC section. There are three ways to implement weak-strong Dance in Swift. These are the simplest optional if let binding, the library withExtendedLifetime method, and the custom withExtendedLifetime method.

conclusion

  1. OC intercepts variables by default, Swift intercepts variables by default. They all make strong references to intercepted variables.
  2. Swift is no__blockModifier, but with an intercept list. By marking the intercepted variables asweakAvoid reference loops
  3. Both have weak-strong Dance, but OC is easier to write at this point.
  4. When using optional types, make it clear whether the closure intercepts an optional type or an instance variable. This is the correct way to determine whether a circular reference has occurred.