Stories from ancient times Those who have experienced the era of manual memory management (MRC) must have a fresh memory of memory management in iOS development. It was around 2010, when domestic iOS development had just started. Uncle Tinyfool was already well-known, but I was still an unknown fresh graduate. The iOS development process at that time was like this: We first wrote a piece of iOS code, then held our breath and started running it. As expected, it crashed. In the MRC era, even the most powerful iOS developers could not guarantee to write perfect memory management code in one go. So, we started debugging step by step, trying to print out the reference count (Retain Count) of each suspected object, and then we carefully inserted reasonable retain and release codes. After repeated application crashes and debugging, finally one time, the application was able to run normally! So we breathed a sigh of relief and smiled for the first time in a long time. Yes, this was what iOS developers were like in that era. Usually, after developing a feature, we would need to spend several hours to manage the reference count. Apple proposed Automatic Reference Counting (ARC) at the WWDC conference in 2011. The principle behind ARC is to rely on the static analysis capability of the compiler, and to completely liberate programmers by finding reasonable insertion of reference count management code during compilation. When ARC was first introduced, the industry was full of doubts and wait-and-see attitude towards this black technology. In addition, the migration of existing MRC code would have required additional costs, so ARC was not quickly accepted. It was not until around 2013 that Apple believed that ARC technology was mature enough and directly abandoned the garbage collection mechanism on macOS (then called OS X), which led to the rapid acceptance of ARC. At the WWDC conference in 2014, Apple launched the Swift language, which still uses ARC technology as its memory management method. Why do I mention this history? It is because today's iOS developers are so comfortable that most of the time, they don't have to care about the memory management behavior of the program. However, although ARC helps us solve most of the reference counting problems, some young iOS developers still can't do a good job of memory management. They can't even understand the common circular reference problems, which can lead to memory leaks, and eventually make the application run slowly or be terminated by the system. Therefore, every iOS developer needs to understand reference counting as a memory management method. Only in this way can we handle issues related to memory management. What is reference counting Reference counting is a simple and effective way to manage the life cycle of an object. When we create a new object, its reference count is 1. When a new pointer points to this object, we add 1 to its reference count. When a pointer no longer points to this object, we subtract 1 from its reference count. When the object's reference count becomes 0, it means that this object is no longer pointed to by any pointer. At this time, we can destroy the object and reclaim the memory. Because reference counting is simple and effective, in addition to Objective-C and Swift, Microsoft's COM (Component Object Model) and C++11 (C++11 provides a smart pointer based on reference counting, share_prt) and other languages also provide memory management methods based on reference counting. To make it more vivid, let's look at another Objective-C code. Create a new project. Since the default project now has automatic reference counting ARC (Automatic Reference Count) enabled, we first modify the project settings and add the -fno-objc-arc compilation parameter to AppDelegate.m (as shown in the figure below). This parameter can enable the manual management of reference counts. Then, we enter the following code in and can see the corresponding reference count changes through Log.
Running results:
Students who are familiar with the Linux file system may find that this management method of reference counts is similar to hard links in the file system. In the Linux file system, we can use the ln command to create a hard link (equivalent to retain here). When deleting a file (equivalent to release here), the system call will check the link count value of the file. If it is greater than 1, the disk area occupied by the file will not be reclaimed. Until the last deletion, the system finds that the link count value is 1, then the system will perform a direct deletion operation and mark the disk area occupied by the file as unused. Why do we need reference counting? From the simple example above, we still can't see the real use of reference counting. Because the life cycle of the object is only within a function, in real application scenarios, when we use a temporary object in a function, we usually don't need to modify its reference count, but only need to destroy the object before the function returns. Reference counting is really useful in object-oriented programming architecture, where it is used to transfer and share data between objects. Let's take a specific example: If object A generates an object M, it needs to call a method of object B and pass object M as a parameter. In the absence of reference counting, the general principle of memory management is "whoever requests it releases it", so object A needs to destroy object M when object B no longer needs it. However, object B may only use object M temporarily, or may think that object M is very important and set it as one of its member variables. In this case, when to destroy object M becomes a difficult problem. For this situation, there is a violent approach, which is that after object A calls object B, it immediately destroys the parameter object M, and then object B needs to copy the parameter to generate another object M2, and then manage the life cycle of object M2 itself. However, there is a big problem with this approach, which is that it brings more work of memory application, copying, and release. A reusable object is simply destroyed because it is inconvenient to manage its life cycle, and then a new copy is constructed, which really affects performance. As shown in the following figure: We have another way, that is, after object A constructs object M, it never destroys object M, and object B completes the destruction of object M. If object B needs to use object M for a long time, it will not destroy it. If it is only used temporarily, it can be destroyed immediately after use. This approach seems to solve the problem of object copying very well, but it strongly depends on the cooperation of two objects AB. Code maintainers need to remember this programming convention clearly. Moreover, since object M is applied in object A and released in object B, its memory management code is scattered in different objects, and it is also very difficult to manage. If the situation is more complicated at this time, for example, object B needs to pass object M to object C, then this object in object C cannot be managed by object C. Therefore, this method brings greater complexity and is even more undesirable. Therefore, reference counting solves this problem very well. During the transmission of parameter M, if an object needs to use the object for a long time, its reference count is increased by 1, and after use, the reference count is reduced by 1. If all objects follow this rule, the object life cycle management can be completely handed over to reference counting. We can also easily enjoy the benefits brought by shared objects. Do not send messages to deallocated objects Some students want to test whether the retainCount becomes 0 when the object is released. Their test code is as follows:
However, if you actually experiment like this, the output you get might look like this:
We noticed that the reference count did not become 0 in the last output. Why is that? Because the memory of the object has been recycled, and we sent a retainCount message to an object that has been recycled, so its output result should be uncertain. If the memory occupied by the object is reused, it may cause the program to crash abnormally. Why is this uncertain value 1 instead of 0 after the object is recycled? This is because when the release is executed for the last time, the system knows that the memory will be recycled soon, so there is no need to reduce retainCount by 1, because the object will definitely be recycled regardless of whether it is reduced by 1 or not, and after the object is recycled, all its memory areas, including the retainCount value, become meaningless. Not changing this value from 1 to 0 can reduce one memory write operation and speed up the recycling of the object. Take the Linux file system we mentioned earlier as an example. When a file is deleted in the Linux file system, the disk area of the file is not actually erased, but only the inode number of the file is deleted. This is similar to the memory recycling method of reference counting, that is, only marking is done during recycling, and the related data is not erased. Memory management issues under ARC ARC can solve 90% of memory management problems in iOS development, but there is another 10% of memory management that needs to be handled by developers themselves. This is mainly the part that interacts with the underlying Core Foundation objects. Since the underlying Core Foundation objects are not under the management of ARC, you need to maintain the reference counts of these objects yourself. For iOS newcomers who blindly rely on ARC, because they don’t know reference counting, their problems are mainly reflected in:
Reference Cycle Problem Although the reference counting method of managing memory is simple, it has a big flaw, that is, it cannot solve the circular reference problem well. As shown in the figure below: Object A and Object B reference each other as their member variables. Only when they are destroyed will the reference count of the member variable be reduced by 1. Because the destruction of object A depends on the destruction of object B, and the destruction of object B depends on the destruction of object A, this creates a problem we call a circular reference (Reference Cycle). These two objects cannot be released even if there is no pointer in the outside world that can access them. Circular references can occur when more than two objects have a circular reference problem. Multiple objects can hold each other in turn, forming a ring, which can also cause a circular reference problem. In a real programming environment, the larger the ring, the harder it is to detect. The following figure shows a circular reference problem formed by 4 objects. There are two main ways to solve the circular reference problem. The first way is that I know clearly that there will be a circular reference here, and I actively break a reference in the loop at a reasonable position so that the object can be recycled. As shown in the following figure: Actively breaking circular references is common in various block-related code logic. For example, in our company's open source YTKNetwork network library, the callback block of the network request is held, but if there is a reference to the View Controller in this block, it is easy to generate a circular reference because:
The solution is to release the block after the network request is completed and the network request object has executed the block, so as to break the circular reference. See the relevant code:
However, actively breaking circular references relies on the programmer's own manual and explicit control, which is equivalent to returning to the old era of "whoever applies releases" memory management. It relies on the programmer's ability to discover circular references and know when to break circular references and reclaim memory (this is usually related to specific business logic). Therefore, this solution is not commonly used. A more common method is to use weak references. Although weak references hold objects, they do not increase the reference count, thus avoiding the generation of circular references. In iOS development, weak references are usually used in the delegate mode. For example, there are two ViewControllers A and B. ViewController A needs to pop up ViewController B to let the user enter some content. When the user completes the input, ViewController B needs to return the content to ViewController A. At this time, the delegate member variable of the View Controller is usually a weak reference to avoid the two ViewControllers referencing each other and causing circular reference problems, as shown below: Detecting Circular References with Xcode Xcode's Instruments tool set can easily detect circular references. To test the effect, we fill in the following code in a test ViewController. In the code, firstArray and secondArray reference each other, forming a circular reference.
In the Xcode menu bar, select Product -> Profile, then select "Leaks", and then click the "Profile" button in the lower right corner to start detection. As shown below At this time, the iOS simulator will start running. We can switch some interfaces in the simulator. After a few seconds, you can see that Instruments has detected our circular reference. A red bar will be used in Instruments to indicate the occurrence of a memory leak. As shown in the following figure: We can switch to the Leaks column and click "Cycles & Roots" to see the circular reference displayed in a graphical way. In this way, we can easily find the objects with circular references. Memory Management of Core Foundation Objects Next, we will briefly introduce the memory management of the underlying Core Foundation objects. The underlying Core Foundation objects are mostly created in the way of XxxCreateWithXxx, for example:
To modify the reference counts of these objects, use the CFRetain and CFRelease methods accordingly, as shown below:
For the CFRetain and CFRelease methods, readers can intuitively think that this is equivalent to the retain and release methods of Objective-C objects. So for the underlying Core Foundation objects, we only need to continue the previous method of manually managing reference counts. In addition, there is another problem that needs to be solved. Under ARC, we sometimes need to convert a Core Foundation object into an Objective-C object. At this time, we need to tell the compiler how to adjust the reference count during the conversion process. This introduces bridge-related keywords. The following is a description of these keywords:
According to the specific business logic, we can solve the problem of relative conversion between Core Foundation objects and Objective-C objects by using the above three conversion keywords reasonably. Summarize With the help of ARC, the memory management work of iOS developers has been greatly reduced, but we still need to understand the advantages and common problems of reference counting as a memory management method, especially to solve the problem of circular references. There are two main solutions to the problem of circular references, one is to actively break the circular reference, and the other is to use weak references to avoid circular references. For Core Foundation objects, since they are not managed by ARC, we still need to continue the previous method of manually managing reference counts. When debugging memory problems, the Instruments tool can assist us very well. Making good use of Instruments can save us a lot of debugging time. I hope every iOS developer can master iOS memory management skills. |
<<: iOS Symbol Table Recovery & Reverse Alipay
>>: Reward Collection | The second issue of Aiti Tribe Stories is officially launched
In April 2022, General Secretary Xi Jinping visit...
Zhixingke, "The Underlying Logic of Making M...
The original intention of the author to write thi...
Yin Chen's four new courses in 2022, 2022 alg...
If we count the new consumer brands that have bec...
In 2019, the author participated in the cold star...
After the Double Eleven shopping festival in 2015...
Do you know which fruit is named after both a mon...
Nowadays, short videos have become a new trend th...
"After the launch of 'Xihe', it has ...
On October 1, the number of railway passengers re...
[[121165]] Introduction In March this year, Googl...
Recently, when you browse the updates of your fri...
Jazz dance, also known as American modern dance, ...
PDCA stands for Plan, Do, Check, and Action, whic...