I am a sheep from the north, baa-baa-baa! Speaking of circular references, what can you think of? Maybe it’s why the delegate has to be weak, maybe it’s why blocks always need special treatment, or maybe you just think of a weakSelf because it does 99% of the things you do with circular references. In this article, I’ll talk about my thoughts on circular referencing.

First, the generation of circular references

1. Basic knowledge

First, let’s talk about the variable-related partitions in memory: heap, stack, and static. Among them, the stack and static area is managed by the operating system itself, relatively transparent to the programmer, so generally we only need to pay attention to the heap memory allocation, and the generation of circular reference, is closely related to it, that is, circular reference will cause the heap memory can not be recycled properly. When it comes to memory recycling, we must say the following cliche recycling mechanism:

  • Send a release message to an object in the heap to reduce its reference count by one;
  • Query the reference count table and dealloc with a reference count of 0. So how does circular referencing affect this process?

2. Sample analysis

In some situations you retrieve an object from another object, and then directly or indirectly release the parent object. If releasing the parent causes it to be deallocated, and the parent was the only owner of the child, then the child (heisenObject in the example) will be deallocated at the same time (assuming that it is sent a release rather than an autorelease message in the parent’s dealloc method).

B is A property of A. If you send A release message to A that causes the reference count of A to be 0, then A will be dealloc, and A will send A release message to B at the same time as A’s dealloc, and that’s the problem. Look at a normal memory reclamation, as shown in Figure 1:




Figure 1

Next, look at how a circular reference affects memory reclamation, as shown in Figure 2:




Figure 2

By extension, figure 2 looks a lot like a digraph, and the root cause of circular references is the presence of rings in digraphs. Make no mistake, however, that the following is not a ring, as shown in Figure 3:




Figure 3

3, conclusion

From the above, we can conclude that whenever a ring appears in the reference diagram in the heap, it will cause a circular reference. An observant child would surely have found the question whether only the relationship between objects A and B (where B is A property of A) would have A ring, as explored in part ii: the generation of rings.

Second, the generation of rings

1. How heap memory is held

If you think about it carefully, you can find that there are only two ways to hold heap memory: method A: an external declared null pointer to a segment of memory (e.g. stack to heap reference), as shown in Figure 4:




Figure 4.

Method B: Point a pointer in a segment of memory (an existing object) to a segment of memory (a heap-to-heap reference), as shown in Figure 5:




Figure 5

In addition to this relation, there are also several common relations belonging to mode B, such as: block holds variables intercepted by block, and container class NSDictionary, NSArray holds objects contained in it.

2. The influence of mode A on the generation of rings

As shown in figure 6:




Figure 6.

3. The influence of mode B on the generation of rings

As shown in figure 7:




Figure 7.

4, the conclusion

Mode B is the root cause of the ring, i.e. heap-to-heap reference is the root cause of the circular reference. Some kids might say, well, what’s the point of mode A? Of course it is. The reference to A and the reference to B together determine the reference count of an object, that is, when the object needs dealloc, as shown in Figure 8:




Figure 8.

Three, how to kill the ring

Delegate and ring

//ClassA: @protocol ClssADelegate - (void)fuck; @end @interface ClassA : UIViewController @property (nonatomic, strong) id delegate; @end //ClassB: @interface ClassB () @property (nonatomic, strong) ClassA *classA; @end@implementation ClassB - (void)viewDidLoad {[super viewDidLoad]; self.classA = [[ClassA alloc] init]; self.classA.delegate = self; }Copy the code

As shown in the code above, B strongly references A, and A’s delegate property points to B. Here the delegate property is modified with strong, so A strongly references B. This is A typical example of A circular reference. The solution to this problem is a familiar one: change the delegate to a weak reference.

2. Blocks and rings

@interface ClassA ()
@property (nonatomic, copy) dispatch_block_t block;
@property (nonatomic, assign) NSInteger tem;
@end
@implementation ClassA
- (void)viewDidLoad {
    [super viewDidLoad];
    self.block = ^{
        self.tem = 1;
    };  
}Copy the code

In the code above, self holds a block, and the block on the heap holds self, so this will cause a circular reference. This is a good example because Xcode can detect this and issue a warning: [capturing self strongly in this block is likely to lead to a retain cycle] xcode will not warn against cyclic references. A common way to resolve this type of circular reference is as follows (this solution resolves most block circular references, but has some drawbacks, see the next section) :

@interface ClassA ()
@property (nonatomic, copy) dispatch_block_t block;
@property (nonatomic, assign) NSInteger tem;
@end
@implementation ClassA
- (void)viewDidLoad {
    [super viewDidLoad];
    __weak typeof(self) weakSelf = self
    self.block = ^{
        weakSelf.tem = 1;
    };  
}Copy the code

3, conclusion

A common feature of the delegate and block handling of circular references above is the use of weak references to break the ring and make it disappear. Therefore, it can be concluded that we can solve the problem of circular references by replacing strong with weak.

Fourth, solve the block circular reference in-depth exploration

1. WeakSelf and its defects

//ClassB is a UIViewController, So let's say I'm showing ClassB from ClassA pushViewController @interface ClassB () @property (nonatomic, copy) dispatch_block_t block; @property (nonatomic, strong) NSString *str; @end@implementation ClassB - (void)dealloc {} - (void)viewDidLoad {[super viewDidLoad]; self.str = @"111"; __weak typeof(self) weakSelf = self; self.block = ^{ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSLog(@"%@", weakSelf.str); }); }; self.block(); }Copy the code

There are two scenarios:

  • If the push from A to B does not pop back to A within 10 seconds, the block in B will print 111.
  • If push from A to B and pop back to A within 10 seconds, B will execute dealloc immediately, causing the block in B to print (NULL). This situation is the defect of using weakSelf, which may lead to memory recovery in advance.

WeakSelf and strongSelf

@interface ClassB () @property (nonatomic, copy) dispatch_block_t block; @property (nonatomic, strong) NSString *str; @end@implementation ClassB - (void)dealloc {} - (void)viewDidLoad {[super viewDidLoad]; self.str = @"111"; __weak typeof(self) weakSelf = self; self.block = ^{ __strong typeof(self) strongSelf = weakSelf; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSLog(@"%@", strongSelf.str); }); }; self.block(); }Copy the code

We found that this did solve the problem, but there might be two points of confusion.

  • The external weakSelf is used to break the loop so that there is no circular reference, while the internal strongSelf is just a local variable that exists on the stack and will be recycled after the block execution ends, so there will be no circular reference.
  • What is the difference between doing this and using weakSelf: The only difference is that strongSelf will make the object reference count of ClassB +1, so when ClassB pops to A, it will not execute dealloc, because the reference count is not zero, strongSelf will still hold ClassB, At the end of the block, the local strongSelf is recycled, at which point ClassB dealloc.

In fact, this can solve all problems, but we can still find its flaws:

  • You have to use strongSelf inside the block, which is cumbersome, so it’s easier to just use self.
  • It’s easy to accidentally use self inside a block and still cause circular references, which are hard to detect.

@Weakify and @Strongify

The libextobjc library on Github has two macro definitions for weak and strong in extscope.h.

#define weakify(...) \ ext_keywordify \ metamacro_foreach_cxt(ext_weakify_,, __weak, __VA_ARGS__) #define strongify(...) \ ext_keywordify \ _Pragma("clang diagnostic push") \ _Pragma("clang diagnostic ignored \"-Wshadow\"") \ metamacro_foreach(ext_strongify_,, __VA_ARGS__) \ _Pragma("clang diagnostic POP ") @interface ClassB () @property (nonatomic, copy) dispatch_block_t block; @property (nonatomic, strong) NSString *str; @end@implementation ClassB - (void)dealloc {} - (void)viewDidLoad {[super viewDidLoad]; self.str = @"111"; @weakify(self) self.block = ^{ @strongify(self) dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSLog(@"%@", self.str); }); }; self.block(); }Copy the code

As you can see, this perfectly solves the defect in 3, and we can use self freely within the block.

Nei CUN: Memory; Xun Huan Yin Yong: Circular reference.