RY 's Blog

2021-03-19

# Background

Memory is important resource in iOS. If a application uses too much memory, exceeding the limit based on the device, the iOS system will kill this App by sending SIGKILL signal. Besides, minimizing memory usage not only decreases application’s memory footprint, but also reduces the amount of CPU time it consumes. These are mentioned in several WWDC sessions.

Obviously, it is important to keep memory under control. In our daily life, we usually use Xcode memory debugger tool and instruments to detect memory leaks. Basically, lots of manual works. Maybe, a better way is to integrate memory leak detection into internal test phase or regression phase. The earlier we detect the issue, the more time we got to fix it. The less efforts we put in checking memory leaks manually, the more likely we spend less time to optimal memory issues and keep our app away from memory leaks. So, using MLeaksFinder and FBRetainCycleDetector in dev or test phase sounds like a good idea.

# What is MLeaksFinder for?

As we know, MLeaksFinder is an light-weight tool from WeChat team, Tencent, which automatically finds leaks in some specific objects. When leaks happening, it will present an alert showing the leaked object and backtrace.

# How does MLeaksFinder work?

The basic idea for MLeaksFinder is to set a timer when the object is about to be released. When the timer is triggered, checked if the reference to the object is still valid. If it is, this object is leaked. Then, it uses this leaked object as seed object for FBRetainCycleDetector to figure out the retain cycle using DFS algorithm to traversal object graph. You may see this brief introduction in some Chinese tech articles. While, I found lots of articles introducing MLeakdsFinder are outdated. Since its source code is a bit easy to read, let’s just start to explore it.

MLeaksFinder has several categories for these classes:

• NSObject+MemoryLeak
• UIApplication+MemoryLeak
• UIPageViewController+MemoryLeak
• UISplitViewController+MemoryLeak
• UITabBarController+MemoryLeak
• UITouch+MemoryLeak
• UIView+MemoryLeak
• UIViewController+MemoryLeak

Take UIViewController as an example, it swizzles viewDidDisappear: method, then checks if current view controller has been popped by UINavigationController. Why need this check? Because the view controller isn’t necessarily popped from view controller stack when viewDidDisappear: called. Maybe, another view controller just has been pushed into the view controller stack, cover it and showing on the screen.

kHasBeenPoppedKey tag here is set by UINavigationController, code here.

As this pic demonstrates, if the view controller was released, the reference the block captured 2 seconds ago is nil. If this view controller isn’t released, strongSelf here would be a valid base address to it. Then MLeakFinder will show an alter to warn users.

We have talked about view controller, how about views? Well, in willDealloc method in UIViewController, MLeaksFinder will run self.view’s willDealloc; then check subviews Array. Basically, the view tree in this view controller will be traversed through and checked.

If you enable the FBRetainCycleDetector through macro, the current leaked object will be used as seed object for FBRetainCycleDetector, which will detect the retain cycle.

# What is FBRetainCycleDetector for?

Finding retain cycles in Objective-C is analogous to finding cycles in a directed acyclic graph in which nodes are objects and edges are references between objects (so if object A retains object B, there exists reference from A to B). Our Objective-C objects are already in our graph; all we have to do is traverse it with a depth-first search.

So, in order to traverse the directed graph, how to get neighbors of each node? How to get objects each node references? For each node in the graph, it could be either an object or block.

# References in object

## strong ivars

For objects, FBRetainCycleDetector get its ivar list from the object.

The first thing we can do is grab the layout of all an object’s instance variables (the “ivar layout”). For a given object, an ivar layout describes where we should look for other objects that it references.

FBClassStrongLayout here

1. Because class_copyIvarList won’t include instance variables declared by superclasses. This method has to get ivar list for current, its superclass, all the way up to its ancestoiicoder
2. get strong ivar by analyzing ivar layout
3. cache ivar list in a map, <Class, NSArray<FBObjectReference>>

Let’s understand it deeper by taking an example. For the following class, there are 4 strong references to others, 2 weak reference.

using class_copyIvarList, we can see its Ivar list. Each pointer is 8-byte in memory in 64-bit device, we an see the offset for first ivar to the class base address is 8 bytes; the second ivar is 16 bytes, the third one is 24bytes, etc.

Then, use class_getIvarLayout to get ivar layout

Basically, the value of fullLayout is

• In hexadecimal figure \x03, the high bits represents the number of non-strong ivar, the lower bits represents the number of strong ivar. \x03 indicates that there are zero non-strong ivar and 3 strong ivar, _object1, _object2, _object3 in this case.
• x11 claims that there comes 1 non-strong ivar, weak _object4 in above declaration; and then follows 1 strong ivar object5

The following method is to parse ivar layout according to the above rule and get a set of NSRange for index and length for strong ivars in this class. One range is 1 to 3 and the other is 5.

Idx Weak/strong
1 strong object1
2 strong object2
3 strong object3
4 weak object4
5 strong object5
6 weak object6

For the above case, the ivar layout is "\x03\x11"

upperNibble lowerNibble currentIndex NSRange
x03 0 3 1 {1, 3}
x11 1 1 5 {5, 1}

Parsing ivar layout to filter out the 4th and 6th ivar and get a set of index range for strong ivar. The result are two ranges, {1, 3} and {5, 1}

There are other interesting cases in the FBClassStrongLayoutTests.mm, the ivar type can be structure or block, and it can be weak as well.

## References to associated objects

FBRetainCycleDetector hooks the calls, objc_setAssociatedObject and objc_removeAssociatedObjects. Then it store objects and a set of pointers to strongly referred associated objects into a global map.

Using OBJC_ASSOCIATION_RETAIN and OBJC_ASSOCIATION_RETAIN_NONATOMIC to trace strong references only

## Block and captured objects

What attracts me most is the capability in FBRetainCycleDetector to detect leaked blocks and its reference. Amazing method to get references from the block and strong reference layout in block.

What we can use is application binary interface for blocks (ABI). It describes how the block will look in memory. If we know that the reference we are dealing with is a block, we can cast it on a fake structure that imitates a block. After casting the block to a C-struct we know where objects retained by the block are kept.

ABI for block

First of all, let’s take a look at the Block Literal.

For a block like this

It will be compiled into

and

This is the initialization of the block literal structure.

You can use clang -rewrite-objc to convert the Objective-C code into cpp implementation .

## What if the block has reference to others?

### Imported const copy variables

It will be compiled into

We can see the variable x is appended at the end of __block_literal_2 structure.

### Imported const copy of Block reference

In the following case, block existingBlock is captured by vv.

• a Block requires copy/dispose helpers in block descriptor if it imports any block variables

### Importing __block variables into Blocks

• Variables of __block storage class are imported as a pointer to an enclosing data structure. see more here)

would be compiled into:

and

• copy_helper and dispose_helper helper functions are added
• a structure _block_byref_i is generated to store __block variable; see captured_i in _block_byref_i

### import __block object

• structure __Block_byref_obj_0 holds reference to NSObject *obj pointer.
• need copy/dispose helper function

## Block_size

From the above cases, we can see in the descriptor structure __block_descriptor_2, the Block_size field is sizeof(struct __block_literal_2) . This is a very import field. FBRetainCycleDetector uses it to get the number of pointers inside

Let’s take a look at a test case here. Supposed a block captures an object from outside.

The block literal is like this

• The value of blockLiteral->descriptor->size is 40, indicating the block 40 bytes in memory;
• int is 32 bit, flags and reserved will be put together into one word, 8 bytes in 64bit processor device.
• In ARM64 device, the pointer size a 8 bytes.
• So it needs 5 pointers to fill out the fake object.

We create an object that pretends to be a block we want to investigate. Because we know the block’s interface, we know where to look for references this block holds. In place of those references our fake object will have “release detectors.” Release detectors are small objects that are observing release messages sent to them. These messages are sent to strong references when an owner wants to relinquish ownership. We can check which detectors received such a message when we deallocate our fake object. Knowing which indexes said detectors are in the fake object, we can find actual objects that are owned by our original block.

Create detector for each of the pointer in the faked object.

Now faked object obj contains 5 references to 5 FBBlockStrongRelationDetector instances. These 5 detectors are newly created to detect whether the pointer inside obj is strong or not. They are not the original block object in your code, but with same memory layout and reference retain policy.

Then, try to dispose the faked object.

The disposing of the fake object actually triggers releasing of those detectors if they are strongly referred by the fake object only. In FBBlockStrongRelationDetector, release message has been overridden and set _strong ivar to YES to mark the related strong reference in the blockLiteral

Finally get the index of the strong reference of current block by figuring out in which FBBlockStrongRelationDetector, _strong is YES.

## Detect cycle

To detect the cycle of objects, it is doing DFS over graph of objects. code here.

# Impact on memory footprint

• MLeaksFinder is light-weight. It has few impact on memory footprint
• FBRetainCycleDetector has impact on the memory footprint. The upside is that MLeaksFinder triggers DFS in FBRetainCycleDetector on when the user click Retain Cycle button in the alter. After the Alert is dismissed, most of the memory usage will be gone.

# Summary:

• FBRetainCycleDetector is quite powerful. It can even detect leaks related to Blocks. But it is a bit slow since it uses DFS algorithm to traverse the object tree. Besides, there is potential risks of data race in associated manager.
• MLeaksFinder is simple but tricky. So once it detects the leaked object, it use FBRetainCycleDetector to detect the retain cycle. Then it shows the alter.
• We can use MLeaksFinder to detect some seed objects. and provide FBRetainCycleDetector with these candidate objects from which it will start detection.