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.
- WWDC: performance and power optimization
- Advanced Memory Management Programming Guide
- https://developer.apple.com/videos/play/wwdc2018/219
- https://developer.apple.com/videos/play/wwdc2018/416/
- https://developer.apple.com/videos/play/wwdc2020/10078/?time=256
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
- UINavigationController+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.
1 | - (void)swizzled_viewDidDisappear:(BOOL)animated { |
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.
1 | @implementation UIView (MemoryLeak) |
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?
Facebook has a dedicated article about the FBRetainCycleDetector
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.
1 | const uint8_t *fullLayout = class_getIvarLayout(aCls); |
- 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 - get strong ivar by analyzing ivar layout
- 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.
1 | @interface _RCDTestClassWithMixedWeakAndStrongProperties : NSObject |
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
1 | const uint8_t *fullLayout = class_getIvarLayout(aCls); |
Basically, the value of fullLayout
is
1 | "\x03\x11" |
- In hexadecimal figure
\x03
, the high bits represents the number ofnon-strong
ivar, the lower bits represents the number ofstrong 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 ivarobject5
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
.
1 | static NSIndexSet *FBGetLayoutAsIndexesForDescription(NSUInteger minimumIndex, const uint8_t *layoutDescription) { |
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}
1 | <NSMutableIndexSet: 0x7fb7aea8ef40>[number of indexes: 4 (in 2 ranges), indexes: (1-3 5)] |
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.
1 | using ObjectAssociationSet = std::unordered_set<void *>; |
Using OBJC_ASSOCIATION_RETAIN
and OBJC_ASSOCIATION_RETAIN_NONATOMIC
to trace strong references only
1 | if (policy == OBJC_ASSOCIATION_RETAIN || |
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
1 | ^ { printf("hello world\n"); } |
It will be compiled into
1 | struct __block_literal_1 { |
and
1 | struct __block_literal_1 _block_literal = { |
This is the initialization of the block literal structure.
You can use clang -rewrite-objc
to convert the Objective-C code into cpp implementation .1
clang -rewrite-objc xxxxx.m
What if the block has reference to others?
Imported const copy variables
1 | int x = 10; |
It will be compiled into
1 | struct __block_literal_2 { |
1 | struct __block_literal_2 __block_literal_2 = { |
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
1 | void (^existingBlock)(void) = ...; |
1 | struct __block_literal_4 _block_literal = { |
Importing __block
variables into Blocks
- Variables of
__block
storage class are imported as a pointer to an enclosing data structure. see more here)
1 | int __block i = 2; |
would be compiled into:
1 | struct _block_byref_i { |
and
1 | struct _block_byref_i i = {( .isa=NULL, .forwarding=&i, .flags=0, .size=sizeof(struct _block_byref_i), .captured_i=2 )}; |
copy_helper
anddispose_helper
helper functions are added- a structure
_block_byref_i
is generated to store__block
variable; seecaptured_i
in_block_byref_i
import __block
object
1 | void func() { |
- structure
__Block_byref_obj_0
holds reference toNSObject *obj
pointer. - need copy/dispose helper function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26struct __Block_byref_obj_0 {
void *__isa;
__Block_byref_obj_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*)
NSObject *obj; // capture __block NSObject *obj
};
static void __func_block_func_0(struct __func_block_impl_0 *__cself) {
__Block_byref_obj_0 *obj = __cself->obj; // bound by ref
(obj->__forwarding->obj) = __null;
}
static void __func_block_copy_0(struct __func_block_impl_0*dst, struct __func_block_impl_0*src) {_Block_object_assign((void*)&dst->obj, (void*)src->obj, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __func_block_dispose_0(struct __func_block_impl_0*src) {_Block_object_dispose((void*)src->obj, 8/*BLOCK_FIELD_IS_BYREF*/);}
static struct __func_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __func_block_impl_0*, struct __func_block_impl_0*);
void (*dispose)(struct __func_block_impl_0*);
}
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
1 | void (*dispose_helper)(void *src) = blockLiteral->descriptor->dispose_helper; |
Let’s take a look at a test case here. Supposed a block captures an object from outside.
1 | NSObject *object = [NSObject new]; |
The block literal is like this
1 | struct BlockLiteral { |
1
2(lldb) p blockLiteral->descriptor->size
(unsigned long) $0 = 40
- The value of
blockLiteral->descriptor->size
is 40, indicating the block 40 bytes in memory; int
is 32 bit,flags
andreserved
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.
1 | // Create a fake object of the appropriate length. |
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.
1 | (void *[]) obj = ([0] = 0x00007fc07b41c370, [1] = 0x00007fc07b41f4b0, [2] = 0x00007fc07b42c080, [3] = 0x00007fc07b422820, [4] = 0x00007fc07b42e420) |
Then, try to dispose the faked object.
1 | @autoreleasepool { |
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
1 | FBBlockStrongRelationDetector |
Finally get the index of the strong reference of current block by figuring out in which FBBlockStrongRelationDetector
, _strong
is YES
.
1 | <NSMutableIndexSet: 0x7fc07b41f560>[number of indexes: 1 (in 1 ranges), indexes: (4)] |
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.
scan qr code and share this article