Objective-C memory management – everything you need to know

Time:2022-1-15

preface

I believe you have read a lot of articles on IOS memory management, and so have I. However, most articles on the Internet do not solve some key doubts about memory management, which is not very friendly for beginners. This paper aims to introduce the key parts of IOS memory management from the perspective of beginners, hoping to be helpful to the majority of IOS learners.

This article starts from my initiativeArrangement of written interview knowledgeProject, here is the correspondingGitHub warehouse, edited by multiple people. If you find any mistakes, you are welcome to correct them at any time.

Memory allocation in Objective-C

In Objective-C, objects are usually usedallocMethod is created on the heap.[NSObject alloc]Method allocates a piece of memory on the heap, according toNSObjectThe internal structure of fills this memory area.

Once the object is created, it is no longer possible to move it. Because it is likely that many pointers point to this object, these pointers are not tracked. Therefore, there is no way to update all these pointers after moving the position of the object.

MRC and arc

Objective-C provides two memory management mechanisms: MRC (manulreference counting) and arc (automatic reference counting), which provide manual and automatic memory management to meet different needs. Now Apple recommends using arc for memory management.

MRC

Four categories of object operations

Object operation Corresponding method in OC The corresponding retaincount changes
Generate and hold objects Alloc / new / copy / mutablecopy, etc +1
Holding object retain +1
Release object release -1
Obsolete object dealloc

be careful:

  • The operation methods of these objects are not included in the OC, but in the foundation framework under the cocoa framework.

  • ObjectreatinCountAttribute has no actual reference value. Refer to Apple’s official documents《Practical Memory Management》.

Four rules

  • Self generated objects, self held.

  • Non self generated objects can be held by themselves.

  • When you don’t need your own object, release it.

  • Objects that are not owned by themselves cannot be released.

The following is a code example corresponding to the four golden rules:

/*
 *Generate and hold the object yourself
 */
 id obj0 = [[NSObeject alloc] init];
 id obj1 = [NSObeject new];
/*
 *Hold non self generated objects
 */
id obj = [NSArray array]; //  An object that is not self generated and exists but does not own
[obj retain]; //  Own object
/*
 *When you don't need your own object, release it
 */
id obj = [[NSObeject alloc] init]; //  Object held at this time
[obj release]; //  Release object
/*
 *The pointer to the object remains in the obj variable
 *But the object has been released and is inaccessible
 */
/*
 *Objects not held by themselves cannot be released
 */
id obj = [NSArray array]; //  An object that is not self generated and exists but does not own
[obj release]; //  At this time, the runtime crash or compiler will report error

amongAn object that is not self generated and exists but does not ownThis feature is usedautoreleaseThe example code is as follows:

- (id) getAObjNotRetain {
    id obj = [[NSObject alloc] init]; //  Own object
    [obj autorelease]; //  The obtained object exists, but it does not own it
    return obj;
}

autoreleaseSo that the object can be released correctly after exceeding the life cycle (by calling the release method). In callreleaseAfter, the object is immediately released and calledautoreleaseAfter, the object is not released immediately, but registered toautoreleasepoolAfter a period of timepoolAt this time, the release method is called and the object is released.

In the memory management mode of MRC, the methods related to variable management are: retain, release and autorelease. The retain and release methods operate on the reference count. When the reference count is zero, the memory will be released automatically. And you can use the nsautoreleasepool object to manage the variables added to the automatic release pool (autorelease call), and reclaim memory when drawing.

ARC

Arc is an automatic memory management mechanism introduced by apple. It will automatically monitor the life cycle of objects according to the reference count. The implementation method is to automatically insert appropriate memory management code into the existing code during compilation and make some optimization at runtime.

Variable identifier

Variable identifiers related to memory management in arc include the following:

  • __strong

  • __weak

  • __unsafe_unretained

  • __autoreleasing

__strongIs the default identifier used. Only if there is a strong pointer to an object, the object will live forever.

__weakDeclaring this reference will not keep the referenced object alive. If the object has no strong reference, the weak reference will be set to nil

__unsafe_unretainedDeclaring this reference will not keep the referenced object alive. If the object has no strong reference, it will not be set to nil. If the object it references is recycled, the pointer becomes a wild pointer.

__autoreleasingThe parameter (ID *) used to mark the value passed by reference will be automatically released when the function returns.

The usage of variable identifier is as follows:

Number* __strong num = [[Number alloc] init];

be careful__strongThe position should be*And variable names are placed in other places. Strictly speaking, it is incorrect, but the compiler will not report an error.

Attribute identifier

Attributes in the class can also be marked:

@property (assign/retain/strong/weak/unsafe_unretained/copy) Number* num

assign Indicates that setter is just a simple assignment operation, which is usually used for basic numeric types, such asCGFloatandNSInteger

strongIndicates that the attribute defines an owner relationship. When setting a new value for an attribute, the value is changed firstretain, old valuerelease, and then perform the assignment operation.

weakIndicates that the attribute defines a non owner relationship. When a new value is set for a property, the value is not changedretain, the old values will not be changedreleaseInstead, they do something similarassignOperation of. However, when the object pointed to by the property is destroyed, the property is set to nil.

unsafe_unretainedSemantic andassignSimilarly, it is only used for object types. It represents an unretained relationship that will not be set to nil when the object is destroyed.

copybe similar tostrong, however, it is performed at the time of assignmentcopyOperation instead ofretainOperation. It is usually used when you need to keep an immutable object (nsstring is the most common) and prevent it from being accidentally changed.

Consequences of incorrect use of attribute identifiers

If we set a primitive typestrong\weak\copy, the compiler will directly report an error:

Property with ‘retain (or strong)’ attribute must be of object type

Set tounsafe_unretainedIt can be compiled, but it can be used withassignIt makes no difference.

Conversely, if we set an nsobject property to assign, the compiler will alarm:

Assigning retained object to unsafe property; object will be released after assignment

As the warning says, the object is released immediately after assignment, and the corresponding attribute becomes a wild pointer. When running to the attribute, the relevant operation will directly crash. And set asunsafe_unretainedIs the same effect (set toweakWill not crash).

unsafe_unretainedUse of

unsafe_unretainedIt is almost the least used identifier. Its main uses in use are as follows:

  1. Compatibility considerations. IOS4 has not been introduced beforeweakIn this case, the only way to express the semantics of weak references is to useunsafe_unretained。 This situation is now very rare.

  2. Performance considerations. useweakIt has some impact on performance, so it can be used where performance requirements are highunsafe_unretainedreplaceweak。 One example isImplementation of yymodel, in order to pursue higher performance, it is widely usedunsafe_unretainedAs a variable identifier.

Reference loop

When two objects hold strong references to each other, and the reference count of these two objects is not 0, a reference cycle is caused.

To break the reference loop, you can start with the following points:

  • Note the variable scope, usingautoreleaseLet the compiler handle references

  • Use weak references

  • When the instance variable completes its work, set it to nil

Autorelease Pool

Autorelase pool provides a method that allows you to delay sending to an objectreleaseMessage mechanism. When you want to give up the ownership of an object and don’t want the object to be released immediately (for example, when an object is returned in a method), the role of autorelease pool appears.

The so-called delayed transmissionreleaseMessages refer to when we mark an object asautoreleaseWhen:

NSString* str = [[[NSString alloc] initWithString:@"hello"] autorelease];

The retaincount of this object will be + 1, but no release will occur. When the autoreleasepool in which this statement is located performs the draw operation, all the tags are markedautoreleaseThe retaincount of the object will be – 1. NamelyreleaseThe sending of the message is delayed until the pool is released.

In the arc environment, Apple introduced@autoreleasepoolSyntax, no need to call manuallyautoreleaseanddrainAnd other methods.

Use of autorelease pool

Under arc, we do not need to manually call the methods related to autorelease. We can manage the memory correctly without even knowing the existence of autorelease. Because in the runloop of cocoa touch, the system automatically adds the creation and release of autorelease pool in each runloop circle.

When we need to create and destroy a large number of objects, using the auto release pool created manually can effectively avoid the occurrence of memory peak. If it is not created manually, the pool created by the outer system will be drawn after the end of the whole runloop cycle. If it is created manually, it will be drawn after the end of the block. For details, please refer toApple official documents。 A commonly used example is as follows:

for (int i = 0; i < 100000000; i++)
{
    @autoreleasepool
    {
        NSString* string = @"ab c";
        NSArray* array = [string componentsSeparatedByString:string];
    }
}

If you do not use autoreleasepool, you need to release 100000000 strings after the end of the loop
If used, the release operation will be performed at the end of each cycle.

Autorelease pool timing for drain

As mentioned above, the autoreaspool created by the system in runloop will be released at the end of an event in runloop. The autoreleasepool we created manually will draw after the block is executed. It should be noted that:

  • When the block ends with an exception, the pool will not be drawn

  • The drain operation of the pool will reduce the reference count of all objects marked as autorelease by one, but it does not mean that this object will be released. We can manually retain the object in the autorelease pool to prolong its life cycle (in MRC).

main. Explanation of autorelease pool in M

As we all know, in IOS program main There are statements like this in the m file:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

In the interview, when asked about the knowledge about autorelease pool, most of them will also ask about the role of the pool here and whether it can be removed. Here we analyze.

according toApple official documents, uiapplicationmain function is the entry of the whole app, which is used to create application objects (singletons) and application delegates. Although this function has a return value, it will never return. When you press the home key, the app is only switched to the background state.

Also refer to Apple’s on lifecycleOfficial documents, uiapplication will create a main run loop. We can roughly draw the following conclusions:

  1. main. Uiapplicationmain in M will never return. Only when the system kills the whole app, the system will release all the memory occupied by the application.

  2. Because (1), uiapplicationmain will never return, and the autorelease pool here will never enter the release stage

  3. On the basis of (2), suppose that some variables really enter main The pool in M (not captured by the inner pool), then these variables are actually leaked. This autorelease pool hides the leak.

  4. Uiapplication will create its own main run loop. In fact, the runloop of cocoa automatically contains autorelease pool, so main The pool in M can be considered asNo,necessary.

In Mac OS development based on AppKit framework, main There is no autorelease pool in M, which further verifies our conclusion. However, because we can’t see the lower level code, and Apple’s documents don’t recommend modifying main m. Therefore, there is no reason for us to delete it directly (for personal testing, the deletion will not affect the operation of the app, and we can’t see the leakage with instruments).

Autorelease pool and function return value

If the return value of a function is a pointer to an object, the object must not be released before the function returns, so the caller gets a wild pointer when calling the function, and cannot release immediately after the function returns, because we don’t know whether the caller retains the object. If we release directly, It may cause this object to become nil when it is used later.

In order to solve this tangled problem, the return value of object pointer is distinguished in Objective-Cretained return valueThe other is calledunretained return value。 The former means that the caller owns the return value, and the latter means that the caller does not own the return value. According to the principle of “who owns who releases”, the caller is responsible for releasing the former, but not the latter.

According to the name convention of applealloc, copy, init, mutableCopyandnewThe methods that start with these methods return returned value, for example[[NSString alloc] initWithFormat:], while others are unretained return value, for example[NSString stringWithFormat:]。 We should also abide by this Convention when writing code.

Let’s analyze the differences between the two return value types in the case of MRC and arc respectively.

MRC

In MRC, we need to pay attention to the difference between the return types of these two functions, otherwise it may lead to memory leakage.

For returned return value, you need to be responsible for releasing

Suppose we have a property defined as follows:

@property (nonatomic, retain) NSObject *property;

When assigning values to them, we should use:

self.property = [[[NSObject alloc] init] autorelease];

Then add in dealloc method:

[_property release];
_property = nil;

The general situation of such memory is as follows:

  1. Init increases the retain count to 1

  2. Assign to self Property, increase the retain count to 2

  3. When the runloop cycle ends, the autorelease pool executes drain and reduces the retain count to 1

  4. When dealloc is executed for the whole object, release reduces the retain count to 0, and the object is released

You can see that no memory leaks have occurred.

If we just use:

self.property = [[NSObject alloc] init];

This statement will increase the retain count to 2. If we execute the release less than once, the retain count cannot be reduced to 0.

In addition, we can also use temporary variables:

NSObject * a = [[NSObject alloc] init];
self.property = a;
[a release];

In this case, because a release is performed on a, the above situation that the retain count cannot be reduced to 0 will not occur.

be careful: at present, most of us write in arc, and we will ignore this. However, according to the above content, we can see that self. Is directly written in MRC Proprty is assigned to a temporary variable and then to self Property is really different! I was asked about this in the interview.

When writing our own code, we should also abide by the above principles. We also use autorelease:

//Note the difference between function names
+ (MyCustomClass *) myCustomClass
{
    return [[[MyCustomClass alloc] init] autorelease]; //  Autorelease required
}
- (MyCustomClass *) initWithName:(NSString *) name
{
    return [[MyCustomClass alloc] init]; //  Autorelease is not required
}

Unretained return value does not need to be released

When we call non alloc and init methods to initialize objects (usually factory methods), we do not need to be responsible for the release of variables and can use them as ordinary temporary variables:

NSString *name = [NSString stringWithFormat:@"%@ %@", firstName, lastName];
self.name = name
//[name release] is not required
ARC

In arc, we don’t need to consider the difference between the two return value types at all. Arc will automatically add the necessary code, so we can safely and boldly write:

self.property = [[NSObject alloc] init];
self.name = [NSString stringWithFormat:@"%@ %@", firstName, lastName];

And in the function written by yourself:

+ (MyCustomClass *) myCustomClass
{
    return [[MyCustomClass alloc] init]; //  No autorelease
}

These writing methods are OK, and there will be no memory problems.

In order to further understand how arc does this, we can refer to clang’sfile

For returned return value, clang does this:

When returning from such a function or method, ARC retains the value at the point of evaluation of the return statement, before leaving all local scopes.

When receiving a return result from such a function or method, ARC releases the value at the end of the full-expression it is contained within, subject to the usual optimizations for local values.

You can see that arc basically helps us release at the end of the code block:

NSObject * a = [[NSObject alloc] init];
self.property = a;
//[a release];  We don't need to write this sentence because arc will help us add this sentence

For unretained return value:

When returning from such a function or method, ARC retains the value at the point of evaluation of the return statement, then leaves all local scopes, and then balances out the retain while ensuring that the value lives across the call boundary. In the worst case, this may involve an autorelease, but callers must not assume that the value is actually in the autorelease pool.

ARC performs no extra mandatory work on the caller side, although it may elect to do something to shorten the lifetime of the returned value.

This is not exactly the same as what we did in MRC before. Arc will extend the life cycle of the object to ensure that the caller can get and use the return value, but it does not necessarily use autorelease. The document is written in the worst case, so the caller cannot assume that the return value is really in the autorelease pool. This approach is understandable from a performance perspective. If we can know how long the longest life cycle of an object should be, there is no need to use autorelease. Just use release directly. If many objects use autorelease, the performance of the whole pool will be degraded when drawing.

Weak and autorelease

As we all know, break does not hold objects. When a break is assigned a self generated object (i.e. the returned return value mentioned above), the object will be released immediately.

A common warning is assigning retained object to weak variable, object will be released after assignment

However, as we mentioned earlier, you can hold non self generated objects, which is implemented through autorelease.

What if a break is assigned a non self generated object (i.e. the unretained return value mentioned above)? The code is as follows:

NSNumber __weak *number = [NSNumber numberWithInt:100];
NSLog(@"number = %@", number);

In this case, the value can be printed correctly.

Clang’s documentationIn this case, break will not be released immediately, but will passobjc_loadWeakThis method is registered in the autoreleasepool to extend the life cycle.

Is it necessary to set the attribute to nil in dealloc under arc?

In order to solve this problem, let’s first clarify what attribute exists. Property is actually a syntax sugar. Behind each property is an instance variable (Ivar). The compiler will help us automatically generate relevant setters and getters. For the following properties:

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

The generated getters and setters are similar to the following:

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

Property is applicable to both MRC and arc.

With this part of the foundation, let’s understand the step of setting the attribute to nil. First of all, it should be clear that under MRC, we do not really set the attribute to nil, but set Ivar to nil.

[_property release];
_property = nil;

If you use self Property will also call setter, which may contain some code that should not be run during dealloc.

For arc, the system will automatically release all Ivars during dealloc, so we don’t need to write release code in dealloc.

What is the effect of setting the variable to nil under arc? When do I need to set the variable to nil?

Based on the above content about property, we know that:

self.property = nil

In fact, the release was manually executed once. For temporary variables:

NSObject *object = [[NSObject alloc] init];
object = nil;

The sentence set to nil is actually useless (except that the object can no longer be used in the following code), because as we discussed above, the temporary variables under arc are managed by autorelease pool and will be released automatically.

Because we can no longer use the release function under arc, setting the variable to nil becomes a method to release the variable. We really need to set the variable to nil, which is usually used to break the circular reference when using block:

MyViewController * __block myController = [[MyViewController alloc] init…];
// ...
myController.completionHandler =  ^(NSInteger result) {
    [myController dismissViewControllerAnimated:YES completion:nil];
    myController = nil;
};

stayYTKNetworkSimilar codes can be seen in this project:

- (void)clearCompletionBlock {
    // nil out to break the retain cycle.
    self.successCompletionBlock = nil;
    self.failureCompletionBlock = nil;
}
Optimization of arc during operation

As mentioned above, for unretained return value, arc “does not necessarily use autorelease”, which is explained in detail below.

What arc does is not limited to finding the right place to help you insert the right data at compile timereleaseFor memory management methods such as, etc., some optimizations are also made during operation. The following are two optimization examples:

  1. Merge symmetric reference count operations. For example, set + 1 / – 1 / + 1 / – 1 directly to 0

  2. Skilfully skip some situationsautoreleaseCall of mechanism.

The second optimization is arcautoreleaseThe return value provides a set of optimization strategies. The general process is as follows:

When all methods are implemented based on arc, arc will call when the method returnsobjc_autoreleaseReturnValue()To replace under MRCautorelease。 Arc will call where retain is required under MRCobjc_retainAutoreleasedReturnValue()。 Therefore, the following arc Code:

+ (instancetype)createSark {
    return [self new];
}
// caller
Sark *sark = [Sark createSark];

It will actually be rewritten like this:

+ (instancetype)createSark {
    id tmp = [self new];
    return objc_ autoreleaseReturnValue(tmp); //  Instead of calling autorelease
}
// caller
id tmp = objc_ Retainautoreleasedreturnvalue ([Sark createpark]) // call retain instead of us
Sark *sark = tmp;
objc_ storeStrong(&sark, nil); //  It is equivalent to calling release instead of us

With this foundation, arc can use some optimization techniques. In callobjc_autoreleaseReturnValue()When, return address will be queried on the stack to determine whether return value will be passed directly toobjc_retainAutoreleasedReturnValue()。 If it is not transmitted, it means that the return value cannot be directly sent from the provider to the receiver. In this case, it will be calledautorelease。 Conversely, if the return value can be successfully passed from the provider to the receiver, it will be skipped directlyautoreleaseProcess, and modify return address to skipobjc_retainAutoreleasedReturnValue()Process, which skips the whole processautoreleaseandretainThe process of.

Core idea: when the returned value needs to be retained immediately after it is returned, there is no need to do autorelease + retain. Just don’t do anything directly.

In addition, when the caller of the function is in a non arc environment, arc will make more judgments, which will not be detailed here. SeeAutorelease behind the scenes

About how to write a tool to detect circular references

Instrument provides us with an easy-to-use tool such as allocations / leaks to detect memory leaks. There are two types of memory leaks:

  • Leaked memory: Memory unreferenced by your application that cannot be used again or freed (also detectable by using the Leaks instrument).

  • Abandoned memory: Memory still referenced by your application that has no useful purpose.

The leaks tool is mainly used to detect leaked memory. In the MRC era, programmers often forget to write the release method, resulting in memory leakage. This is not common in the arc era. (in the arc era, the main leaked memory comes from the underlying C language and some underlying libraries written in C, which often lead to leaks because they forget to manually free).

The allocations tool is mainly used to detect the disabled memory The main idea is to detect the declaration cycle of the object in a time slice to observe whether the memory will grow indefinitely. Record the life cycle of the object by hook, drop alloc, dealloc, retain, release and other methods.

reference material