In our daily development, we more or less encounter the problem of circular reference. In fact, the essence of the problem is to create a mutual holding relationship, when the object is released, it is like a deadlock, the system can not release any one of the objects, resulting in memory leakage. We all know that NSTimer is typical of this. But why doesn’t an object that inherits from the UIControl class also call the addTarget method cause a memory leak? Let’s start exploring this article now.
1. The Target – the Action mode
This is a design pattern that Apple does, where once you set a target object, that object can perform the corresponding Selector. We can see in our project, often using UIButton, UISegmentedControl such as inherited from the UIControl class called when
– (void)addTarget:(nullableid)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents; This method, however, is not particularly good for code readability, and we often write extensions for these classes to complete block calls. Why does this exist? It’s not designed as a block callback. In my opinion, there are two reasons for this.
1. In a storyboard, that’s the pattern that’s used to connect the selectors, which I think is quite powerful in this case.
2. In fact, this mode is accompanied by the entire OC version, and the block was only released in iOS4. So the target-action mode looks really powerful at the beginning. And I noticed that in iOS10, apple has already added a block to the NSTimer class, so we can actually use block for loop references, but only in iOS10.
Other thoughts on this model are no longer extended. There are many articles on the web, many on Google, but the core of this article is to delve a little deeper.
2. Why is the addTarget method different between UIControl and NSTimer
This is the method that we call when we call it, but UIButton doesn’t cause circular references, but why does NSTimer cause circular references? I looked at the official documentation for UIControl and NSTimer, and there’s really not much to explain here, and I don’t have any strong evidence for why this is happening, but let’s think about it: the underlying mechanism of UIControl must be weak referencing self, breaking the loop chain, So there’s no such operation under UIControl. From this point of view, I googled and read some articles about it, and found that I could see something fishy in the stack information. So let’s see what we can find in our stack information.
First let’s take a look at the LLDB scheme we get the information can be used for us? I set breakpoints on both addTarget methods. Then type dis on the console and print the call information for the current stack as follows.
When I saw the stack information, I found that the way to reference the same memory was exactly the same, which further increased my curiosity. The stack information here could not solve the existing question completely. Is there any other way? When we thought about calling the method stack, it might be clearer to see what the method actually did. We could clearly see what the method used, so we added the following two symbolic breakpoint breakpoint practices to the project for testing.
At this point, run the program again, at each breakpoint execution, we can see the corresponding stack information as follows.
As can be seen from the two pieces of stack information in the figure above, the holding mode of Target under UIControl is indeed weakRetained and weak holding, which enables the reference loop to be solved, so there is no reference loop problem in use. But in NSTimer, when I saw this line of code in the stack, I started to see how it worked, and in NSTimer the way that Target holds is autorelease, This means that target checks to see if the area is freed the next time the runloop executes, which explains why memory is freed if we set the repeats property to NO, and why memory is not freed even after self is set to nil. Then I printed the stack information for the invalidate method, but I found that there was no stack information for the corresponding method. Instead, I called the addtarget method again. This is explained in the official documentation of NSTimer, which reminds me that once the invalidate method is called, the timer can no longer be used. I think the bottom line is that the current timer does a target redirection, just does a Runloop timerObserver listener, frees up the memory, and undoes the reference loop. Now that we understand the principle, let’s start with the principle. See if existing solutions make sense.
3. Start at the root and look at existing solutions
I googled the NSTimer circular reference problem and summarized the solution
1) Call the invalidate method in time
2) Write an extension class to NSTimer and use block callbacks
3) Create the mid-tier proxy while adding the proxy to self.
So now when we look at the three methods, first of all, we know how method one redirects and we know why it solves the problem up here, so let’s see if method two and method three solve the problem.
First of all, the core code of method two is roughly as follows
After looking at the above code, we find that the target is an NSTimer class object, in fact, it is a singleton, so it will be with the whole life cycle of the program, so it does not matter whether the program keeps its cyclic reference to it, so it will not cause a memory leak problem, but we need to think about one thing, Our application will continue to execute repeats events where we don’t see them. If we have a lot of NSTimer events in our application, I don’t know much about the underlying implementation, but I think it will have some impact on the performance of the application. But for the memory free considerations I think the problem has been solved. My advice is to call the invalidate method in a timely manner, otherwise the performance of the program will suffer. Of course, we use this method a lot, because I think for the sake of code readability, so do not use this method as if the memory problem is solved.
Having looked at the problem in method 2, let’s now look at how method 3 undoes circular references. I downloaded a relevant demo on Github, and the core source code is roughly as follows.
We see that the author has rewritten a class and used it as the target to unlock the looped references, so that the delloc method does not have looped references. It looks like creating a timer class solved the looped references problem. However, I tested and verified my idea, and the weakTimer object created by the author would be resident in the memory and could not be released. In fact, if the author in the middle layer will target point to a class object, I think this way is able to solve many problems, but the key lies in above, or may cause performance problems, but also need to write the corresponding invalidate method, etc., I think this time, actually this way itself has little meaning. Therefore, as for the way of intermediate proxy, I think it is not really available, which increases the complexity of the program and cannot solve the problem in essence.
So my final personal recommendation for using NSTimer is to create extensions, which I think is the most readable way to code. However, notice that the invalidate method is called in time as usual. After all, it is not visible that the problem is solved, and there is no problem with our program.
I hope this article can help you in the development. I have been doing some project optimization recently, and I will share my thoughts and experience on how to make the program more power-saving when I have time recently. If there is any problem with the ideas in this article, please point it out in the comment area and I will correct it immediately. Thank you.