Understanding iOS memory management

Understanding iOS memory management

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.

  1. - (BOOL)application:(UIApplication *)application
  2. didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
  3. {
  4. NSObject *object = [[NSObject alloc] init];
  5. NSLog(@ "Reference Count = %u" , [object retainCount]);
  6. NSObject *another = [object retain];
  7. NSLog(@ "Reference Count = %u" , [object retainCount]);
  8. [another release];
  9. NSLog(@ "Reference Count = %u" , [object retainCount]);
  10. [object release]; // At this point, the object's memory is released
  11. return YES;
  12. }

Running results:

  1. Reference Count = 1
  2. Reference Count = 2
  3. Reference Count = 1

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:

  1. - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
  2. { NSObject *object = [[NSObject alloc] init]; NSLog(@ "Reference Count = %u" , [object retainCount]);
  3. [object release]; NSLog(@ "Reference Count = %u" , [object retainCount]); return YES;
  4. }

However, if you actually experiment like this, the output you get might look like this:

  1. Reference Count = 1
  2. Reference Count = 1

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:

  1. Excessive use of blocks will not solve the circular reference problem.
  2. When you encounter the underlying Core Foundation objects and need to manually manage their reference counts, you seem to be at a loss.

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:

  • Controller holds the network request object
  • The network request object holds the callback block
  • The callback block uses self, so it holds the Controller

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:

  1. // https://github.com/yuantiku/YTKNetwork/blob/master/YTKNetwork/YTKBaseRequest.m
  2. // Line 147:
  3. - (void)clearCompletionBlock {
  4. // Actively release the reference to the block
  5. self.successCompletionBlock = nil;
  6. self.failureCompletionBlock = nil;
  7. }

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.

  1. - (void)viewDidLoad
  2. {
  3. [super viewDidLoad];
  4. NSMutableArray *firstArray = [NSMutableArray array];
  5. NSMutableArray *secondArray = [NSMutableArray array];
  6. [firstArray addObject:secondArray];
  7. [secondArray addObject:firstArray];
  8. }

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:

  1. // Create a CFStringRef object
  2. CFStringRef str= CFStringCreateWithCString(kCFAllocatorDefault, "hello world", kCFStringEncodingUTF8);
  3.  
  4. // Create a CTFontRef object
  5. CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@ "ArialMT" , fontSize, NULL );

To modify the reference counts of these objects, use the CFRetain and CFRelease methods accordingly, as shown below:

  1. // Create a CTFontRef object
  2. CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@ "ArialMT" , fontSize, NULL );
  3.  
  4. //Increase the reference count by 1
  5. CFRetain(fontRef);
  6. //Decrement the reference count by 1
  7. CFRelease(fontRef);

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:

  • __bridge: Only performs type conversion and does not modify the reference count of related objects. The original Core Foundation object needs to call the CFRelease method when it is not in use.
  • __bridge_retained: After type conversion, the reference count of the related object is increased by 1. The original Core Foundation object needs to call the CFRelease method when it is not in use.
  • __bridge_transfer: After the type conversion, the reference count of the object is handed over to ARC management. The Core Foundation object no longer needs to call the CFRelease method when it is not in use.

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

Recommend

How a good rice variety is cultivated

In April 2022, General Secretary Xi Jinping visit...

50 Tips, Tricks, and Resources for Android Developers

The original intention of the author to write thi...

Brand marketing innovation methodology!

If we count the new consumer brands that have bec...

Profitable customer acquisition methods for K12 online education!

In 2019, the author participated in the cold star...

Business secrets in digital mobile Internet operations in the era of big data!

After the Double Eleven shopping festival in 2015...

Why is kiwi fruit called kiwi? The truth is that it is a bird!

Do you know which fruit is named after both a mon...

Android Wear in-depth analysis: making Android Wear development easier

[[121165]] Introduction In March this year, Googl...

Closed-loop thinking for B2B theme promotion

PDCA stands for Plan, Do, Check, and Action, whic...