preface

Recently, I have been studying how to do some simple memory monitoring in iOS applications, including memory leaks and memory usage. Before you start to record their own course on pit, recommend an article: from OOM to iOS memory management | creator training camp. This article introduces the basic knowledge of iOS memory in a comprehensive way. This article mainly describes how to debug memory leaks, code detection of memory leaks, memory usage acquisition and other basic content, the length is relatively long, can be read according to the title.

Memory leak tool detection

The detection of memory leaks is mainly in the debug phase. Xcode also provides tools for detecting memory usage and memory leaks.

There is no doubt that circular references are a major cause of memory leaks. Here is a simple demo simulating circular references.

class Server: NSObject {
    var clients: [Client] = []

    func add(client: Client) {
        self.clients.append(client)
    }
}

class Client: NSObject {
    var server: Server?

    override init(a) {
        super.init()}}class ViewController2: UIViewController {
    let client: Client
    let server: Server
    
    init(a) {
        self.client = Client.init(a)self.server = Server.init(a)super.init(nibName: nil, bundle: nil)
        self.client.server = server
        self.server.add(client: client)
    }
}
Copy the code

It can be inferred from the code that instances of the Server and Client objects will cause circular references, resulting in memory leaks. The following two memory leak detection tools provided by Xcode are introduced. (ps: using Xcode version 12.0)

  • Allocations and Leaks of Instruments detect memory usage during app run as well as memory Leaks, and this was the most common way to detect memory in my daily development. Here is a screenshot of a memory leak detected using Leaks:

Here we can clearly see that the client and server objects are not released, and form a circular reference. (The study found that there may be some Leaks that couldn’t be detected with Leaks, but those who know can discuss them in the comments below.)

  • The Memory Graph is a way of looking at the Memory usage of an app, and clearly looking at the reference chain of objects.

To open the Memory Graph, click the button in the diagram while the app is running. Now let’s see what happens if we look at the demo above.As you can see in the figure above, two objects that should be released, client and server, are leaking memory due to circular references. In general, if the object is leaking, it will be followed by a purple flag.

Ps: what’s embarrassing is that the author cannot open the company’s project when running on his iPhone7, and it will prompt as shown in the figure. No suitable solution has been found at present:

Another option is to see some memory usage on the command line by exporting.memGraph. Here’s a quick look at the leaks command, and some more advanced gameplay: Let’s debug the iOS memory-Memory Graph

Export the. Memgraph File: In the above mode, click File->Export Memory GraphThe Leaks command is used on the terminal to view the memory leak analysis, for example printing out references if a circular reference occurs:

You can also turn on the malloc log stack to get a traceback to the root node.

Memory leak code detection

The detection methods mentioned above are implemented with the help of Xcode tool, which is more dependent on PC. Is there a pure code test? The author’s goal is to detect leaks in a release environment. And if the code can be automatically detected, it can automatically detect some memory leaks during the daily development debugging phase. The answer is yes. Next, I will introduce the three open source libraries that THE author studied in the process of consulting materials. By reading the source code of the open source library and some articles, I got a formula, and the memory leak code detection can also be summarized by this formula.

Here I summarize memory leak detection in two aspects: trigger timing and verification. IOS memory leak code detection has certain limitations, compared with some open source projects, the implementation needs to find a reasonable trigger time (for example, MLeaksFinder will use the life cycle of hook ViewController as the starting point for detection, which will be discussed later). A simple scheme is to use Method Swizzling hook vc Method trigger check. IOS custom memory monitoring

Using the above example, apply the formula:

  • Trigger time: Replace VC’s dismiss and viewWillDisappear by Method Swizzling to indirectly judge vc’s destruction. To trigger detection.
  • Check method: by calling an extended instance method of VC itself after a delay of 2 seconds.
  • Detection result: under normal circumstances, the object will be released after vc destruction after 2s, and then the instance method of the vc cannot be called. If the invocation succeeds, the object is leaked.

The above simple ideas can be used throughout most code detection implementations of memory leaks, and the following open source libraries are similar.

RIBs-LeakDetector

RIBsIs an open source App architecture design framework of Uber. The author found a tool to detect memory leaks in the libraryLeakDetector.swift A trackingObjects object of NSMapTable<AnyObject, AnyObject>.StrongToWeakObjects () is used to store the object to be observed. The document explains that the key value is a strong reference and the value value is a weak reference.

On the less friendly side, the library needs to be triggered by the developer, such as vc deinit can monitor its viewModel for memory leaks:

deinit {
	LeakDetector.instance.expectDeallocate(object: viewModel)
}
Copy the code

Apply the formula:

  • Trigger timing: developers need to find the trigger timing, such as vc deinit
  • Verification method: Add the objects to be observed to the NSMapTable in advance, and obtain the value based on the corresponding key after the specified delay time
  • Detection result: If value is empty, memory is freed (weak reference); otherwise, a memory leak occurs.

Advantages: It is worth learning from is, 1, borrowed NSMapTable features; 2. The timing logic is implemented using RxSwift, which helps detect event cancellation and code decoupling.

Disadvantages: 1. It relies on a large third-party library such as RxSwift, which is not lightweight; 2. 2. The trigger time needs to be found by itself, which may be poor for logical compatibility.

LifetimeTracker

LifetimeTrackerThis is an interesting library that checks for leaks based on the maximum number of objects set by the developer. Here’s how to intercept the readME:

MaxCount will be configured by a LifetimeConfiguration object within the LifetimeTrackable protocol, and at appropriate times such as init method calls to trackLifetime will trigger leak checking. The principle is that there is an internal collection to store the identifier of each object (in this case, the information of the object is generated into a model), and then the number of checks are carried out every time the trackLifetime is triggered. Specific source implementation can be viewedLifetimeTracker.swift.

Ps: It’s worth noting that each time the number of trackLifetime is +1, there is also an onDealloc time opportunity -1. The associated property (as shown in the figure above) is used to pre-associate an object to the observed object. When the observed object calls deinit, the associated property object’s deinit is also called. This indirectly determines the release of the observed object. For an introduction to association properties, see the article objc_setAssociatedObject association in detail.

Apply the formula:

  • Trigger timing: Requires the developer to call trackLifetime when the object is init
  • Check method: trackLifetime will add a count of observed objects and associate an object, when the associated object deinit call will automatically count -1
  • Check result: If maxCount is greater than LifetimeConfiguration, it is a leak.

Ps: There is also a group design in the library. My understanding is that some types may be combined and groups can be better managed. I won’t go into it too much here.

Advantages: 1, than rely on VC and other views; 2. According to objc_setAssociatedObject, the passive detection object is released without delay active detection.

Disadvantages: 1, need to pre-set maxCount for the use of the scenario comparison limits; 2, leak check needs to be triggered by calling trackLifetime, the trigger time is relatively late.

MLeaksFinder

MLeaksFinderIs Tencent open source a memory leak detection library. The principle is roughly similar to the example provided at the beginning of this section: Replacing the VC lifecycle with Method Swizzling triggers the check.

Automatic replacement when the object is loaded, which makes it non-intrusiveMonitoring View LevelNext, let’s look at the soul of the entire library, which triggers the check:

So that’s the library extension to the ViewController, and the soul is the willDealloc method. In this case, the willDealloc method will be triggered when the VC is dismissed, and the method will add the VC’s subview, subVC, to the observation, so that you can see the maximum leakage of view objects. Let’s look at what the willReleaseChildren and willReleaseChild methods do.

So that’s the library extension to NSObject, which is essentially associating an object’s address set (parentPtrs) with its reference stack (viewStack) with objc_setAssociatedObject, The action here is to allow a child object to have the address information of its parent object as well as the full reference stack. Finally, it calls the willDealloc of the child object.

Ps: The willDealloc implementation is also the old method, delay 2 seconds to call the object’s methods, to determine whether there is a leak.

Unfortunately, if you want to observe a property of a custom type, you still need to manually trigger it. For example, if there is a leak in the VC’s viewModel:

    @objc dynamic public override func willDealloc(a) -> Bool {
        if !super.willDealloc() {
            return false
        }
        self.willReleaseChildren([
            self.viewModel
            ])
        return true
    }
Copy the code

Apply the formula:

  • Trigger timing: call willDealloc layer by layer, starting with vc’s destruction
  • Check method: The extension method assertNotDealloc of NSObject is called after a delay of 2 seconds.
  • Detection results: generally can be invoked successfully, large probability is the occurrence of leakage.

Advantages: 1. No intrusive viewing of view-level objects. 2. The compatibility logic of looking for trigger timing is relatively perfect; 3, the reference stack of the record is more perfect, convenient to locate the problem.

Disadvantages: 1. Manual processing is required for non-view objects. 2. NSObject dependency, not friendly to types on Swift that do not inherit NSObject; 3. It may be difficult to find the trigger time for observing non-global properties such as objects within methods.

Summary of memory leak code detection

In summary, the design analysis of the above open source libraries is in fact very consistent with the formula proposed by the author at the beginning of this section. Unfortunately, they are all more or less intrusive to the code and less friendly to the intrusiveness of objects on the process (within the method). Based on the advantages and disadvantages of the above analysis, the author still chooses MLeaksFinder and will spend a little time to introduce the author’s minor changes to the library.

MLeaksFinder Memory leak recording code analysis

As shown above, MLeaksFinder once detects a memory leak:

  • A MLeakedObjectProxy object is first associated with the leak object. The main function is to observe the lifecycle of the leaked object and trigger the release of the MLeakedObjectProxy object once it is released. This is similar to the LifetimeTracker mentioned above.
  • There will be a pop-up at leak time and release time to prompt the developer to respond.

In combination with the above analysis, if you want to do some custom logging (such as development logging, etc.) after the leak, you can add or modify your own logic in the popover area. This part of the implementation in DoraemonKit doraemonkit-MLeaksFinder is very good.

Memory usage

Application performance testing, contains the current access memory footprint, here are two articles to share: from OOM to iOS memory management | creator camp, iOS development – APP performance testing plan summary (a). Inside the introduction is more detailed, here is no longer too much tautology 😁. The following is a summary of some methods through the data, there is a need to take to use.

  • Gets the amount of memory currently occupied by the app
#include <mach/mach.h>

+ (NSInteger)useMemoryForApp {
    task_vm_info_data_t vmInfo;
    mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
    kern_return_t kernelReturn = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t) &vmInfo, &count);
    if(kernelReturn = = KERN_SUCCESS)
    {
        int64_t memoryUsageInByte = (int64_t) vmInfo.phys_footprint;
        return (NSInteger)(memoryUsageInByte/1024/1024);
    }
    else
    {
        return -1; }}Copy the code
  • Total device memory
#include <mach/mach.h>

+ (NSInteger)totalMemoryForDevice {
    return (NSInteger) ([NSProcessInfo processInfo].physicalMemory/1024/1024);
}
Copy the code
  • After iOS13, you can get the current memory available for your app
#import <os/proc.h>

+ (NSInteger)availableSizeOfMemory {
    if (@available(iOS 13.0.*)) {
        return (NSInteger)(os_proc_available_memory() / 1024.0 / 1024.0);
    }
    return 0;
}
Copy the code
  • Of course, you can also combine the available memory and used memory to calculate the total memory available to the app. Ps: The following code is for reference only.
#include <mach/mach.h>
#import <os/proc.h>

+ (NSInteger)limitSizeOfMemory {
    if (@available(iOS 13.0.*)) {
        task_vm_info_data_t taskInfo;
        mach_msg_type_number_t infoCount = TASK_VM_INFO_COUNT;
        kern_return_t kernReturn = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t)&taskInfo, &infoCount);

        if (kernReturn ! = KERN_SUCCESS) {
            return 0;
        }
        return (NSInteger)((taskInfo.phys_footprint + os_proc_available_memory()) / 1024.0 / 1024.0);
    } else {
        NSInteger totalMemory = [Utils totalMemoryForDevice];
        NSInteger limitMemory;
        if (totalMemory < = 1024) {
            limitMemory = totalMemory * 0.45;
        } else if (totalMemory > = 1024 && totalMemory < = 2048) {
            limitMemory = totalMemory * 0.45;
        } else if (totalMemory > = 2048 && totalMemory < = 3072) {
            limitMemory = totalMemory * 0.50;
        } else {
            limitMemory = totalMemory * 0.55;
        }
        returnlimitMemory; }}Copy the code

Finally, regarding the performance monitoring code, it is strongly recommended that you read the DoraemonKit source code. Doraemonhealthmanager. m contains many commonly used performance monitoring code.

MetricKit

By the way, in the course of my research, I found that iOS released a performance monitoring API after 13, which returns some performance data to developers on a regular basis. The official documentation is written to call back every 24 hours (bad design, development phase don’t know how to debug). We don’t know if unlisted apps can be used, but those interested in iOS performance optimization: MetricKit 2.0 is used to collect data

The last

This article focuses on the basics of debugging memory leaks, code detection of memory leaks, and memory usage acquisition. In general, there are limitations to the current practice of monitoring memory leaks. If there is a better solution, please comment below.