Practical tips for iOS MRC memory management

The basic principles of iOS MRC memory management were mentioned in the last article.

Memory management strategies based on reference counting are fairly simple, so there are a lot of points for developers to be aware of. It is almost impossible for many developers to master the knowledge of memory management and apply it properly in daily development.

As a result, Apple’s official documentation should provide some reasonable specifications and techniques to make it easier for developers to handle memory management. The official document can refer to Practical Memory Management. I have translated a copy for reference to Practical Memory Management.

Official documents are always accurate and authoritative, and it is often more efficient to read official documents than to read articles that have been copied and pasted on CSDN for many times.

Based on the official documents, the arrangement and summary are as follows.

Using accessors makes memory management easier

Accessors are just plain old getters and setters. The official recommendation is to put the code for memory management, that is, reference count management, into the accessor and then use the accessor in your business logic.

Officials say this will make memory management easier and reduce the chance of errors. This is inevitable. I think there are two other important points:

  1. Simplify and unify memory management code for easy debugging and maintenance.
  2. Separate memory management code from business code for cleaner design.

Let’s say I have a variable, count.

@interface Counter : NSObject
@property (nonatomic, retain) NSNumber *count;
@end;
Copy the code

In the getter, you just return the synthesized instance variable, no need for retain or release.

- (NSNumber *)count {
    return _count;
}
Copy the code

In setter methods, if everyone else is following the same rules, it is likely that someone else will subtract the reference count of newCount by one at any time, resulting in newCount being released, so you must first increments its reference count by retain. Then, you have to subtract the reference count of _count by one.

In OC, it’s allowed to message a nil object, so it’s okay to call release on it even if _count hasn’t been set.

If newCount and _count are the same object, release may cause the object to be released immediately.

- (void)setCount:(NSNumber *)newCount {
    [newCount retain];
    [_count release];
    // Make the new assignment.
    _count = newCount;
}
Copy the code

Once you have defined accessors, it is recommended that you use accessors to set variables in your code.

Imagine implementing a method to reset a counter. You have a couple of options, if you’re not using accessors you can write this

- (void)reset {
    NSNumber *zero = [[NSNumber alloc] initWithInteger:0];
    [_count release];
    _count = zero;
}
Copy the code

However, by bypassing accessors and assigning values directly, Apple loses face.

Face is trivial, it’s hard to maintain and error prone because you have to think about when to retain and when to release.

Also note that changing variables in this way is not KVO compatible.

Using accessors, it is more concise:

- (void)reset {
    NSNumber *zero = [[NSNumber alloc] initWithInteger:0];
    [self setCount:zero];
    [zero release];
}
Copy the code

Or you can use autoRelease, and two lines of code will do it:

- (void)reset {
    NSNumber *zero = [NSNumber numberWithInteger:0];
    [self setCount:zero];
}
Copy the code

Do not use accessors in the init and dealloc methods

There is an exception to the above recommendation to use accessor simplification and aggregate memory management logic. Do not use accessors in the init and dealloc methods.

To initialize a variable in init, you need to implement:

- init {
    self = [super init];
    if (self) {
        _count = [[NSNumber alloc] initWithInteger:0];
    }
    return self;
}
Copy the code

If you want to initialize a variable to a non-zero value, you can do this:

- initWithCount:(NSNumber *)startingCount {
    self = [super init];
    if (self) {
        _count = [startingCount copy];
    }
    return self;
}
Copy the code

Because the Counter class has an object instance variable, you must also implement the dealloc method. Decrement the reference count for all instance variables by one and finally call the super method:

- (void)dealloc {
    [_count release];
    [super dealloc];
}
Copy the code

As for why accessors can’t be used, it’s not stated in the official documentation, and stackOverflow has an answer for several reasons: Why-shoudnt-i-use-accessor-methods-in-init-methods why shouldn’t I use Objective C 2.0 accessors in init/dealloc?

The accepted explanation is that using accessors in init and dealloc can cause potential problems.

Init is responsible for the basic initialization logic, the logic should be light, do not introduce unnecessary logic. If you call the accessor, you’re probably doing lazy loading in the accessor. Lazy loading is invalidated if the accessor is called directly. There is even some heavy logic in the accessor, which causes init to take longer.

- (NSMutableDictionary *) myMutableDict { if (! myMutableDict) { myMutableDict = [[NSMutableDictionary alloc] init]; } return myMutableDict; }Copy the code

Dealloc is responsible for freeing resources to clean up the scene, and if the accessor is called, it may perform a lot of unnecessary and even buggy logic:

-(void) setFoo:(Foo*)foo
{
   _foo = foo;
  [_observer onPropertyChange:self object:foo];
}
Copy the code

-(void) dealloc { ... self.foo = nil; }

Use weak references to avoid circular references

Retain an object increments the object’s reference count by one. The object cannot be released until the reference count reaches zero. Therefore, if two objects may have circular references (that is, they have a strong reference to each other), then a problem called a retain cycle (which could be two objects referring to each other, or multiple objects referring to each other, eventually forming a ring) occurs.

The object relationship shown in Figure 1 illustrates a potential retain cycle. The Document object has a Page object for each Page in the Document. Each Page object has a property that tracks the document in which it is located. If the Document object has a strong reference to the Page object and the Page object has a strong reference to the Document object, then neither object can be freed. The Document reference count cannot go to zero until the Page object is freed, and the Page object is not freed until the Document object is freed.


Using weak references solves the retain cycle problem. A weak reference is a non-owning relationship in which the source object does not retain the object to which it has a reference.

However, to keep the object graph intact, there must be strong references somewhere (if there are only weak references, then the Page and Document may not have any owners and therefore need to be freed). Thus, Cocoa established a convention that “parent” objects should maintain strong references to their “children” and that children should have weak references to their parents.

Thus, in Figure 1, the Document object has a strong reference (retain) to its Page object, and the Page object has a weak reference (unretain) to the Document object.

Prevents the object in use from being released

Cocoa’s reference policy specifies that an object passed in through a function generally remains valid for the scope of the calling method. You can also return an object passed as a function parameter without worrying about it being freed. It does not matter to the application whether the getter method of an object returns cached instance variables or computed values. The important thing is that the object remains valid for as long as you need it.

There are occasional exceptions to this rule, which fall into two main categories.

  • When an object is deleted from one of the base collection classes
heisenObject = [array objectAtIndex:n];
[array removeObjectAtIndex:n];
// heisenObject could now be invalid.
Copy the code

When an object is removed from a base collection class, it is sent a Release (instead of an AutoRelease) message. If the collection is the sole owner of the deleted object, the deleted object (heisenObject in this case) is released immediately.

  • When the parent object is released
id parent = <#create a parent object#>;
// ...
heisenObject = [parent child] ;
[parent release]; // Or, for example: self.parent = nil;
// heisenObject could now be invalid.
Copy the code

In some cases, you get an object from another object and then directly or indirectly release the parent object. If releasing the parent causes it to be freed, and the parent is the sole owner of the child, then the child object (heisenObject in this case) will be freed at the same time (assuming it was sent as a release rather than an AutoRelease message in the parent object’s dealloc method).

To prevent these situations, you reference a heisenObject when you receive it and release it when you’re done with it. Such as:

heisenObject = [[array objectAtIndex:n] retain];
[array removeObjectAtIndex:n];
// Use heisenObject...
[heisenObject release];
Copy the code

Collections own the objects they contain

When you add an object to a collection (such as an array, dictionary, or collection), the collection takes ownership of the object. When an object is removed from the collection or the collection itself is released, the collection relinquishes ownership. So, for example, if you wanted to create an array of numbers, you could do either of the following:

NSMutableArray *array = <#Get a mutable array#>;
NSUInteger i;
// ...
for (i = 0; i < 10; i++) {
    NSNumber *convenienceNumber = [NSNumber numberWithInteger:i];
    [array addObject:convenienceNumber];
}
Copy the code

In this case, you didn’t call alloc, so you didn’t call Release. On addObject, the array will retain Convenienumber.

NSMutableArray *array = <#Get a mutable array#>;
NSUInteger i;
// ...
for (i = 0; i < 10; i++) {
    NSNumber *allocedNumber = [[NSNumber alloc] initWithInteger:i];
    [array addObject:allocedNumber];
    [allocedNumber release];
}
Copy the code

In this case, you need to send a release message to allocedNumber within the scope of the for loop to offset the alloc’s increased reference count. Since the array retains the number on addObject:, it will not be freed in the array.

To understand this, step into the shoes of a person of a real set class. You want to make sure that none of the objects in the collection are released, so you send a retain message to them when they are passed in. If they are removed, you must send a release message, and any objects that are not removed should be sent a Release message during your dealloc method.

conclusion

Based on the basic principles of MRC memory management, this article lists some common memory management techniques to help developers better deal with memory problems.

Such basic and tedious memory management logic should not be left to the developers, otherwise it will add a lot of costs to the developers and will inevitably lead to various memory management problems.

Fortunately, Apple introduced ARC, and the retain release was finally removed. However, ARC only automatically adds MRC memory management code in the compiler, so understanding MRC can better understand the operating rules of ARC, improve the depth of iOS memory management understanding.

This article is formatted using MDNICE