Bo2SS

Bo2SS

1 Basic Part (Continued)

Today I want to share: Basic Part - During the launch phase, mainly focusing on monitoring crashes, stuttering, memory, logs, performance, threads 🧵, and battery 🔋.

image

12 | iOS Crashes Are Diverse, How to Monitor Them Comprehensively?#

First, let's look at some common causes of crashes:

  • Array issues: Array out of bounds, or adding nil elements to the array.
  • Multithreading issues: UI updates in a background thread can cause crashes, for example, if one thread is nullifying data while another thread is reading that data.
  • Main thread unresponsiveness: If the main thread is unresponsive for longer than the system's specified duration, it will be killed by the Watchdog, with the corresponding exception code being 0x8badf00d.
  • Accessing wild pointers: Wild pointers point to a deleted object, which is the most common yet hardest to locate type of crash.

Crashes cause the most harm to users, so the crash rate (the ratio of crash occurrences to launch occurrences over a period) has become the highest priority technical metric.

Based on whether crash information can be captured through signals, crash information can be divided into two categories:

  1. Can be captured through signals: Crash information such as array out of bounds, wild pointers, KVO issues, NSNotification thread issues, etc.
  2. Cannot be captured through signals: Information such as background task timeouts, memory overflow, main thread stuttering exceeding thresholds, etc.

Collecting Signal-Capturable Crash Logs#

A simple and straightforward method: Xcode > Product > Archive, check "Upload your app’s symbols to receive symbolicated reports from Apple," and you will be able to see symbolicated crash logs in Xcode's Archive later.

Third-party open-source libraries: PLCrashReporter, Fabric, Bugly. The first one requires your own server, while the latter two are suitable for companies without server development capabilities or those that are not sensitive to data.

  • Monitoring principle: Register for various signals, and after capturing an exception signal, use the backtrace_symbols method in the handleSignalException method to obtain the current stack information, saving the stack information locally so that it can be uploaded as crash logs upon the next startup.

Collecting Crash Information That Cannot Be Captured by Signals#

Background knowledge: Due to system limitations, signals thrown by the system's force kill cannot be captured.

With five questions: What are the reasons for crashes in the background? How to avoid crashes in the background? How to collect crash information that cannot be captured by signals in the background? What other crash situations cannot be captured by signals? How to monitor other crash information that cannot be captured by signals?

(1) What are the reasons for crashes in the background?

First, let's introduce the five ways to keep iOS apps alive in the background:

  1. Background Mode: Usually, only map, music, and VoIP apps can pass the review.
  2. Background Fetch: Wake-up time is unstable, and users can disable it in system settings, so the usage scenario is limited.
  3. Silent Push: Silent push can wake up the app in the background for 30 seconds. Its priority is lower and it calls the application:didReceiveRemoteNotifiacation:fetchCompletionHandler: delegate, similar to the delegate called by a regular remote push notification.
  4. PushKit: Can wake up the app in the background for 30 seconds, mainly used to enhance the experience of VoIP applications.
  5. Background Task: This method is used by default when the app goes to the background, so it is used more frequently.

For the Background Task method, the system provides the beginBackgroundTaskWithExpirationHandler method to extend background execution time, used as follows:

- (void)applicationDidEnterBackground:(UIApplication *)application {
    self.backgroundTaskIdentifier = [application beginBackgroundTaskWithExpirationHandler:^(void) {
        [self yourTask];
    }];
}

In this code, the yourTask task can execute for a maximum of 3 minutes. After the task is completed, the app is suspended, but if the task does not finish within 3 minutes, the system will forcibly kill the process, leading to a crash. This is the reason why crashes are likely to occur when the app goes to the background.

(2) How to avoid crashes in the background?

Strictly control data read and write operations in the background. For example, first check the size of the data to be processed. If the data is too large and cannot be processed within the background time limit, consider processing it upon the next startup or when the background program is awakened.

(3) How to collect crash information that cannot be captured by signals in the background?

When using the Background Task method, first set a timer. When approaching 3 minutes (beginBackgroundTaskWithExpirationHandler keeps the background alive for 3 minutes), check if the background program is still executing. If it is still executing, determine that the program is about to crash in the background, and immediately report and record it.

(4) What other crash situations cannot be captured by signals?

Mainly memory overflow and the main thread being killed by the Watchdog due to timeout.

(5) How to monitor other crash information that cannot be captured by signals?

Similar to monitoring background crashes, handle it when approaching the threshold. See the next two lessons for details.

After Collecting Crash Information, How to Analyze and Solve Crash Issues?#

The crash log mainly contains the following information:

  1. Exception information: Exception type, exception code, thread of the exception;
  2. Thread backtrace: Method call stack at the time of the crash.
  3. Process information: Such as unique identifier for the crash report, unique key value, device identifier;
  4. Basic information: Date of the crash, iOS version;

The typical analysis process:

  1. Analyze the exception thread in the "exception information," and analyze the method call stack of the exception thread in the "thread backtrace." From the symbolicated method call stack, you can see the complete process of method calls, with the top of the stack being the last method call that led to the crash.
  2. Refer to the exception code. Here are 44 types of exception codes, with three common ones being: 0x8badf00d (App was killed by watchdog due to unresponsiveness for a certain period, see the next lesson), 0xdeadfa11 (App was forcefully exited by the user), 0xc00010ff (App was killed due to high device temperature caused by its operation, see lesson 18 on battery optimization).

⚠️: Some issues cannot be analyzed solely through the stack trace. In such cases, you can further analyze using logs related to user behavior and system environment conditions prior to the crash. See lesson 15 on log monitoring for details.

Consideration: How can we improve the efficiency of collecting crash information and reduce the loss rate? How can we collect more crash information, especially those caused by system force kills?

13 | How to Use RunLoop Principles to Monitor Stuttering?#

The stuttering issue refers to the inability of the main thread to respond to user interactions, caused by factors such as: excessive UI rendering; synchronous network requests on the main thread, performing large I/O operations; excessive computation leading to sustained high CPU usage; deadlocks and contention for locks between main and child threads.

Starting from NSRunLoop (the message events of threads depend on NSRunLoop), we can know which methods are called on the main thread; by monitoring the state of NSRunLoop, we can discover whether the execution time of called methods is too long, thus monitoring stuttering situations.

First, let's introduce the principles of RunLoop.

RunLoop Principles#

Purpose: Keep the thread busy when there are events to handle, and let the thread sleep when there are no events to handle.

Task: Listen to input sources and perform scheduling.

It receives two types of input sources (input devices, network, periodic time, delay time, asynchronous callbacks):

  1. Asynchronous messages from another thread or different applications;
  2. Synchronous events at scheduled times or repeated intervals.

Application example: Offload heavy, non-urgent tasks that consume a lot of CPU (like image loading) to an idle RunLoop mode, avoiding execution when the RunLoop mode is UITrackingRunLoopMode. UITrackingRunLoopMode mode:

  • The RunLoop mode that switches when the user performs scrolling operations;
  • Avoid executing heavy CPU tasks in this mode to enhance user experience.

Working Principle:

In iOS, the RunLoop object is implemented by CFRunLoop. The entire process can be summarized in the following diagram; for specifics, see the CFRunLoop source code.

RunLoop Process - "Geek Time"

Six States of Loop#

The code is defined as follows:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry ,         // Entering loop
    kCFRunLoopBeforeTimers ,  // Trigger Timer callback
    kCFRunLoopBeforeSources , // Trigger Source0 callback
    kCFRunLoopBeforeWaiting , // Waiting for mach_port messages
    kCFRunLoopAfterWaiting ,  // Receiving mach_port messages
    kCFRunLoopExit ,          // Exiting loop
    kCFRunLoopAllActivities   // All state changes in loop
}

RunLoop's thread blocking situations:

  1. Execution time of methods before entering sleep is too long (preventing it from entering sleep);
  2. Time taken to receive messages after the thread wakes up is too long (preventing it from proceeding to the next step).

If this thread is the main thread, it manifests as stuttering.

Therefore, to utilize RunLoop principles to monitor stuttering, we need to focus on these two loop states:

  1. kCFRunLoopBeforeSources: Triggered before entering sleep, before Source0 callback;
  2. kCFRunLoopAfterWaiting: Receiving mach_port messages after waking up.

How to Check for Stuttering?#

Three steps:

  1. Create a CFRunLoopObserverContext observer;
  2. Add the observer to the main thread RunLoop in common mode for observation;
  3. Create a persistent child thread to monitor the main thread's RunLoop state. Stuttering Judgment: In the kCFRunLoopBeforeSources state before entering sleep, or in the kCFRunLoopAfterWaiting state after waking up, if there is no change within the set time threshold.

Next, dump the stack information to further analyze the cause of stuttering.

⚠️: The time threshold for triggering stuttering can be set according to the WatchDog mechanism.

  • Launch: 20s;
  • Resume: 10s;
  • Suspend: 10s;
  • Quit: 6s;
  • Background: 3min (before iOS 7, each request was for 10min; after that, it changed to 3min per request, with a maximum of 10min).

PS: The time threshold for triggering stuttering should be less than the WatchDog's limit time.

How to Obtain the Method Stack Information for Stuttering?#

  1. Directly call system functions (use signal to get error information). The advantage is low performance overhead; the downside is that it can only obtain simple information and cannot use dSYM (symbol file) to locate the problematic code. Because of its good performance, it is suitable for observing overall stuttering statistics but not for finding specific causes of stuttering.

  2. Directly use the PLCrashReporter open-source library. Its feature is that it can locate the exact position of the problematic code, and it has been optimized for performance overhead.

Consideration: Why should stuttering monitoring be done online? Mainly to collect problems on a larger scale. Some stuttering issues are caused by abnormal data from a small number of users.

Related Information: In-depth Understanding of RunLoop - ibireme

14 | Approaching OOM, How to Obtain Detailed Memory Allocation Information and Analyze Memory Issues?#

OOM (Out of Memory): The phenomenon where the app is forcefully killed by the system after occupying memory that reaches the system's limit for a single app.

  • It is a "special" crash caused by iOS's Jetsam mechanism (a resource management mechanism used by the operating system to control excessive memory resource usage);
  • Logs cannot capture it through signals.

Calculating Memory Limit Values Through JetsamEvent Logs#

Check the system logs starting with JetsamEvent on the phone (Settings > Privacy > Analytics & Improvements > Analytics Data) to understand the memory limits for apps on different devices and system versions.

Focus on the crash reasons in the system logs where per-process-limit corresponds to rpages:

  • per-process-limit: The app's memory usage exceeds the system's limit for a single app;
  • rpages: The number of memory pages occupied by the app.

⚠️:

  • The size of memory pages is the value of pageSize in the logs.
  • Apps that are forcefully killed cannot access system-level logs and can only be obtained through offline devices.

iOS System Monitoring Jetsam:

  1. The system starts the highest priority thread vm_pressure_monitor to monitor the system's memory pressure, maintaining a stack of all app processes. It also maintains a memory snapshot table to save the memory page consumption of each process.
  2. When the vm_pressure_monitor thread detects that a certain app is under memory pressure, it sends a notification, and the app under memory pressure executes the corresponding didReceiveMemoryWarning delegate (this is an opportunity to release memory and can prevent the app from being forcefully killed by the system).

Priority Judgment Basis (the system will first make a priority judgment before forcefully killing an app):

  • Kernel > Operating System > App;
  • Foreground App > Background Running App;
  • When using thread priority, the priority of threads with high CPU usage will be lowered.

Obtaining Memory Limit Values Through XNU#

Using XNU macros to obtain the memorystatus_priority_entry structure can provide the process's priority and memory limit values.

⚠️: Obtaining memory limits through XNU macros requires root privileges, which are insufficient for apps, so normally, app developers cannot see this information...

Obtaining Memory Limit Values Through Memory Warnings#

Utilize the didReceiveMemoryWarning memory pressure delegate event to dynamically obtain memory limit values. In the delegate event:

  • First, use the iOS system-provided task_info function to get the current task information (task_info_t structure);
  • Then, through the resident_size field in the task_info_t structure, you can obtain the current app's memory usage.

Locating Memory Issue Information Collection#

Obtaining the memory usage is not enough; you also need to know who allocated the memory to accurately pinpoint the key issues. All large memory allocations, regardless of how the external functions are wrapped, ultimately call the malloc_logger function.

  • Memory allocation functions like malloc and calloc use nano_zone by default;
  • nano_zone is for allocating small memory below 256B, while memory allocations above 256B will use scalable_zone;
  • Functions that allocate memory using scalable_zone will call the malloc_logger function, which the system uses to track and manage memory allocation.

Thus, you can use fishhook to hook this function and add your own statistics to grasp the memory allocation situation.

PS: Besides being forcefully killed due to excessive memory, there are three other memory issues:

  • Accessing unallocated memory: XNU will report an EXC_BAD_ACCESS error, with the signal being SIGSEGV Signal #11.
  • Accessing memory without adhering to permissions: The memory page's permission standards are similar to UNIX file permissions. Writing to a read-only memory page will result in an error, and XNU will issue a SIGBUS Signal #7 signal.
  • Accessing allocated but uncommitted memory: XNU will intercept the allocation of physical memory, freezing the thread that encounters issues when allocating memory pages.

The first two issues can be obtained through crash logs, refer to lesson 12 on crashes.

15 | Log Monitoring: How to Obtain Full Logs from the App?#

Background: The previous sections shared monitoring for crashes, stuttering, and memory issues. Once a problem is detected, it is necessary to record detailed information about the issue to inform developers, allowing them to pinpoint the problem from these logs.

The definition of full logs: All logs recorded in the app, such as logs used to record user behavior and key operations.

The function of full logs: It facilitates developers to quickly and accurately locate various complex issues, improving the efficiency of problem resolution.

However, an app is likely developed and maintained by multiple teams, and the logging libraries used by different teams may vary due to historical reasons, either being self-developed or using third-party logging libraries. So, how can we obtain all logs from the app in a non-intrusive manner?

Below are the methods to obtain NSLog and CocoaLumberjack logs, which cover most scenarios.

Obtaining NSLog Logs#

NSLog is actually a C function void NSLog(NSString *format, ...); that outputs information to the standard Error console and system logs.

How to obtain NSLog logs? There are three methods:

  1. Use the interface provided by ASL.

Before iOS 10, NSLog internally used ASL (Apple System Logger, a logging system implemented by Apple) API to directly store log messages on disk.

By leveraging the third-party library CocoaLumberjack with the command [DDASLLogCapture start], you can capture all NSLog logs and record them as CocoaLumberjack logs.

Capture Principle:

  • When logs are saved to the ASL database, syslogd (the logging daemon in the system that receives and distributes log messages) will issue a notification.
  • By registering this notification com.apple.system.logger.message (kNotifyASLDBUpdate macro) using notify_register_dispatch, you can handle all new logs iteratively when the notification is received, ultimately recording them as CocoaLumberjack logs.

The main method's code implementation is as follows:

+ (void)captureAslLogs {
    @autoreleasepool {
        ...
        notify_register_dispatch(kNotifyASLDBUpdate, &notifyToken, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0),^(int token) {
            @autoreleasepool {
                ...
                // Use process identifiers to ensure compatibility with other processes' logs in the simulator
                [self configureAslQuery:query];

                // Iteratively process all new logs (this notification may contain multiple logs)
                aslmsg msg;
                aslresponse response = asl_search(NULL, query);

                while ((msg = asl_next(response))) {
                    // Record logs (recorded as CocoaLumberjack logs, default is Verbose level)
                    [self aslMessageReceived:msg];

                    lastSeenID = (unsigned long long)atoll(asl_get(msg, ASL_KEY_MSG_ID));
                }
                asl_release(response);
                asl_free(query);

                if (_cancel) {
                    notify_cancel(token);
                    return;
                }
            }
        });

PS:

  • After being recorded as CocoaLumberjack logs, it facilitates further retrieval, see the next section. The log levels include two categories: the first category is Verbose and Debug, which belong to the debugging level; the second category is Info, Warn, Error, which belong to the formal level and need to be persistently stored, suitable for recording more important information. The default is Verbose level.
  • Using NSLog for debugging incurs I/O disk operations, so frequent use of NSLog is detrimental to performance.
  • There are many inter-process notifications, such as when the system disk space is insufficient, it will issue the com.apple.system.lowdiskspace notification (kNotifyVFSLowDiskSpace macro).
  1. Hook the NSLog method using fishhook.

To make logging more efficient and organized, after iOS 10, a new unified logging system was introduced to record logs, fully replacing the ASL method.

Unified Logging System:

  • Centralizes logs in memory and databases, providing a single, efficient, and high-performance interface to obtain messages at all levels;
  • However, it does not have an interface like ASL to extract all logs.

Therefore, to be compatible with the new unified logging system, it is necessary to redirect the output of NSLog logs. Since NSLog itself is a C function and not an Objective-C method, fishhook is used to complete the redirection:

  • Define the original method and the redirected method using struct rebinding.
  • In the redirected method:
    • You can first perform your own processing, such as redirecting the log output to a persistent storage system;
    • Then call the NSLogv method that will also be called by NSLog for the original NSLog method call, or use the original method call method provided by fishhook.
  1. Use the dup2 function to redirect the STDERR handle.

The handle that NSLog writes to is STDERR (standard error, where system error logs are recorded), and Apple's definition of NSLog is to record error information.

The dup2 function is specifically for file redirection, such as redirecting the STDERR handle, with the key code as follows:

int fd = open(path_to_file, (O_RDWR | O_CREAT), 0644);
dup2(fd, STDERR_FILENO);

Where path_to_file is the custom file path for redirection.

Now, logs from various system versions of NSLog can be obtained. What about logs generated by other methods?

Next, let's discuss how to obtain logs from the mainstream third-party logging library CocoaLumberjack, as other third-party libraries are mostly wrappers around CocoaLumberjack, so the approach is similar.

Obtaining CocoaLumberjack Logs#

CocoaLumberjack consists of the following components:

image

  • DDLogFormatter: Used to format the log format.
  • DDLogMessage: Encapsulates log messages.
  • DDLog: A global singleton class that saves loggers that comply with the DDLogger protocol.
  • DDLogger protocol: Implemented by DDAbstractLogger. There are four types of loggers that inherit from DDAbstractLogger:
    • DDTTYLogger: Outputs logs to the console.
    • DDASLLogger: Captures NSLog logs recorded in the ASL database.
    • DDFileLogger: Saves logs to files. You can obtain the saved file path through [fileLogger.logFileManager logsDirectory], thus obtaining all CocoaLumberjack logs.
    • DDAbstractDatabaseLogger: An abstract interface for database operations.

Collecting full logs can improve the efficiency of analysis and problem resolution, so go ahead and give it a try!

16 | Performance Monitoring: The Measure of App Quality#

Purpose: Actively and efficiently discover performance issues to prevent the app's quality from entering an uncontrolled state without supervision.

Monitoring methods: Offline, online.

Offline Performance Monitoring: The Official Ace Instruments#

Instruments is integrated into Xcode, as shown below. It includes various performance detection tools, such as power consumption, memory leaks, network conditions, etc.:

Various Performance Detection Tools Provided by Instruments - "Geek Time"

From an overall architecture perspective, Instruments consists of two components: Standard UI and Analysis Core. All its tools are developed based on these two components. Based on these two components, you can also develop custom Instruments tools (Instruments 10+):

  1. Xcode > File > New > Project > macOS > Instruments Package, generating a .instrpkg file;
  2. Configure this file, with the main focus on completing the configuration of Standard UI and Analysis Core;
  3. Refer to the extensive code snippets provided by Apple, see Instruments Developer Help.

Working Principle of Analysis Core:

It mainly involves the process of collecting and processing data, divided into three steps:

  1. Process the XML data table we configured (for visual display) and request storage space store.

  2. The store finds the corresponding data provider. If it cannot be found directly, it will synthesize through input signals from other stores.

⚠️: Use the os_signpost API to obtain data, refer to the example in WWDC 2018 Session 410: Creating Custom Instruments.

  1. After the store obtains the data source, it will perform Binding Solution work to optimize the data processing process.

PS: Instruments decouples the display and analysis of data through the XML standard data interface, which is worth learning.

Online Performance Monitoring#

Two principles:

  • Do not intrude on business code;
  • Minimize performance consumption.

Main indicators:

CPU Usage#

The current CPU usage of the app, which is the sum of the CPU usage of each thread in the app. Therefore, periodically traverse each thread and accumulate the cpu_usage values of each thread.

⚠️:

  • The task_threads(mach_task_self(), &threads, &threadCount) method can obtain the array of all threads in the current process and the total number of threads threadCount.
  • The thread_info((thread_act_t)threads[i], THREAD_BASIC_INFO, (thread_info_t)threadInfo, &threadInfoCount) method can obtain the basic information threadInfo of the thread threads[i].
  • cpu_usage is defined in the iOS system > usr/include/mach/thread_info.h > thread_basic_info structure.

Memory#

Similar to CPU usage, memory information also has a dedicated structure to record it, defined in the iOS system > usr/include/mach/task.info.h > task_vm_info structure, where phys_footprint represents physical memory usage.

⚠️:

  • The task_info(mach_task_self(), TASK_VM_INFO, (task_info_t) &vmInfo, &vmInfoCount method can obtain the memory information vmInfo of the current process.
  • Physical memory is represented by phys_footprint, not resident_size (resident memory: physical memory mapped to the process's virtual memory space).

FPS#

Low FPS indicates that the app is not smooth.

Simple implementation: In the method registered with CADisplayLink, record the refresh time and refresh count, thus obtaining the number of screen refreshes per second, i.e., FPS.

⚠️: Each screen refresh will call the method registered with CADisplayLink once.

Tips:

17 | The Many Pitfalls of Multithreading Beyond Your Imagination#

Phenomenon: Common foundational libraries like AFNetworking 2.0 (network framework) and FMDB (third-party database framework) are very cautious when using multithreading technology; especially UIKit has not used multithreading technology and has been made thread-unsafe, only allowing operations on the main thread.

Why does this phenomenon occur? Let's look at two common pitfalls of multithreading technology: persistent threads and concurrency issues.

Persistent Threads#

Definition: Threads that do not stop and remain in memory.

How do they arise?

Using the run method of NSRunLoop adds a runloop to that thread, causing it to persist in memory.

Example: The code for creating a persistent thread in AFNetworking 2.0 is as follows:

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        // First, create a thread using NSThread
        [[NSThread currentThread] setName:@"AFNetworking"];
        // Use the run method to add a runloop
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

AFNetworking 2.0 encapsulates each request into NSOperationQueue and then creates the above persistent thread to receive callbacks from NSOperationQueue.

The reason for not avoiding the persistent thread pitfall is that the network request uses NSURLConnection, which has design flaws: After initiating a request, the thread where NSURLConnection is located needs to remain alive to wait for the NSURLConnectionDelegate callback. However, the time for network returns is uncertain, so a persistent thread is needed to handle it. A separate thread is created instead of using the main thread because the main thread still needs to handle a lot of UI and interaction work.

🎉: However, in AFNetworking 3.0, it replaced NSURLConnection with Apple's newly introduced NSURLSession, which allows specifying the callback to be an NSOperationQueue, thus eliminating the need for a persistent thread to wait for request callbacks.

How to avoid?

Too many persistent threads not only fail to improve CPU utilization but also reduce the execution efficiency of the program.

Not creating persistent threads is certainly the best, but if you really need to keep a thread alive for a while, you can choose:

  1. Use the other two methods of NSRunLoop, runUntilDate: and runMode:beforeDate:, to specify the duration for which the thread should remain alive, making the thread's lifespan predictable.
  2. Use CFRunLoopRef's CFRunLoopRun and CFRunLoopStop methods to start and stop the runloop, achieving the goal of keeping the thread alive for a while.

⚠️: The methods to add a runloop through NSRunLoop are run, runUntilDate:, and runMode:beforeDate:. Among them, the run method adds a runloop that continuously calls the runMode:beforeDate: method to ensure it does not stop.

Concurrency#

How does it arise? By simultaneously creating multiple threads.

In iOS concurrent programming technology, GCD (Grand Central Dispatch) is the most widely used, developed by Apple as a multi-core programming solution.

  • Advantages: Simple and easy-to-use interfaces, convenient for managing complex threads (creation, release timing, etc.).
  • Disadvantages: Risks in resource usage. For example, in database read and write scenarios:
    • When read and write operations wait for disk responses, a task is initiated through GCD;
    • Following the principle of maximizing CPU utilization, GCD will create a new thread during the wait for disk responses to fully utilize the CPU.
    • If the new tasks initiated by GCD also require waiting for disk responses, as the number of tasks increases, GCD will create more new threads, leading to increasing memory resource pressure.
    • When the disk starts responding, reading data will occupy even more memory, ultimately leading to memory management going out of control.

How to avoid?

For tasks that frequently read and write to disk, such as databases, it is advisable to use serial queues for management to avoid memory issues caused by multithreading concurrency.

Recommendation: The open-source third-party database framework FMDB, whose core class FMDatabaseQueue executes all disk operations related to reading and writing databases in a serial queue.

⚠️: When there are too many threads, both memory and CPU will consume a lot of resources.

  • The system needs to allocate a certain amount of memory for the thread stack. In iOS development, the main thread stack size is 1MB, while the stack size for newly created child threads is 512KB (the stack size is a multiple of 4KB).
  • The CPU needs to update registers through addressing when switching thread contexts, and the addressing process incurs significant CPU overhead.

Tips: Lock issues in multithreading technology are the easiest to identify; you should pay more attention to those hidden issues that will slowly consume system resources.

18 | How to Reduce App Power Consumption?#

Possible reasons for excessive power consumption: Location services are enabled; frequent network requests; the time interval for scheduled tasks is too short...

Use the elimination method to find the specific location: Comment out each feature one by one and observe the changes in power consumption.

However, it must be said that only by obtaining the battery level can we discover power issues.

How to Obtain Battery Level?#

Use the system-provided batteryLevel property, as shown in the code below:

- (float)getBatteryLevel {
    // To monitor battery level, it must be enabled
    [[UIDevice currentDevice] setBatteryMonitoringEnabled:YES];
    // 0.0 (no power), 1.0 (full power), -1.0 (battery monitoring not enabled)
    float batteryLevel = [[UIDevice currentDevice] batteryLevel];
    
    NSLog(@"Remaining battery percentage: %@", [NSString stringWithFormat:@"%f", batteryLevel * 100]);
    return batteryLevel;
}

Refer to batteryLevel - Apple official documentation.

PS: You can also add observers for battery change notifications and call custom methods when the battery level changes to monitor it. Refer to UIDeviceBatteryLevelDidChangeNotification - Apple official documentation.

How to Diagnose Power Issues?#

If the above elimination method still does not identify the problem, then this power consumption must be caused by other threads, and the thread causing the power consumption may be in third-party libraries or internal libraries (libraries developed by other teams within the company).

In such cases, we can directly observe which thread is problematic, for example, if a certain thread's CPU usage remains high for an extended period, exceeding 90%, we can infer that it is problematic. At this point, record its method stack to trace back to the source.

  • To observe CPU usage, refer to section 16 | Online Performance Monitoring.
  • To record method stack, refer to section 13 | Obtaining Method Stack Information for Stuttering.

Optimizing Power Consumption#

CPU Aspect#

Avoid letting the CPU do unnecessary tasks.

  1. For complex calculations involving large amounts of data, delegate the processing to the server.
  2. For complex calculations that must be handled within the app, use GCD's dispatch_block_create_with_qos_class method to specify the queue's Qos as QOS_CLASS_UTILITY, placing the computation work in a block within this queue. This is because, under this Qos mode, the system has specifically optimized for power consumption in complex calculations involving large amounts of data.

I/O Aspect#

Any I/O operation will disrupt low-power states.

  • Delay disk storage operations for fragmented data, first aggregate in memory, and then perform disk storage.
  • You can use the system-provided NSCache to complete data aggregation in memory:
    • It is thread-safe.
    • It will clean up the cache when it reaches the preset cache space value and trigger the cache:willEvictObject: callback method, where I/O operations can be performed on the data.

Related case: The image loading framework SDWebImage reads cached images.

- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key {
    return [self.memCache objectForKey:key];
}

- (UIImage *)imageFromDiskCacheForKey:(NSString *)key {
    // Check if the image data exists in NSCache
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    if (image) {
        // If it exists
        return image;
    }
    // If not, read from disk
    UIImage *diskImage = [self diskImageForKey:key];
    if (diskImage && self.shouldCacheImagesInMemory) {
        NSUInteger cost = SDCacheCostForImage(diskImage);
        // And store it in NSCache
        [self.memCache setObject:diskImage forKey:key cost:cost];
    }
    return diskImage;
}
  • Each time an image is read, it checks whether the image data already exists in NSCache.
    • If it does, it reads directly from NSCache;
    • If not, it will read the cached image from disk through I/O and store the retrieved image data in NSCache.

Apple Company References#

  • Energy Efficiency Guide for iOS Apps” - A power optimization guide maintained by Apple, providing suggestions for power optimization across various factors such as CPU, device wake-up, network, graphics, animations, video, location, accelerometer, gyroscope, magnetometer, Bluetooth, etc.
  • Writing Energy Efficient Apps” - A theme shared by Apple at WWDC 2017 Session 238 on how to write energy-efficient apps.

19 | Hot Topic Q&A (Part 2): Basic Module Q&A#

Learning Sequence for RunLoop Principles#

  1. Sun Yuan's Offline Sharing | RunLoop: Get a general understanding of RunLoop.
  2. RunLoop Official Documentation: Comprehensive and detailed understanding of the RunLoop mechanism designed by Apple and how to use RunLoop to solve problems.
  3. ibireme | In-depth Understanding of RunLoop: In-depth analysis of the RunLoop mechanism combined with the underlying CFRunLoop source code.

Can Using dlopen() Pass Review?#

Using dlopen() to read remote dynamic libraries cannot pass Apple's review.

In November 2018, when Apple concentrated on taking down 718 apps, it mentioned that using dlopen(), dlsym(), respondsToSelector:, performSelector:, method_exchangeImplementations() to execute remote scripts is not allowed. Because:

  • These methods combined with remote resources may load private frameworks and private methods, causing significant changes in app behavior, which would differ from the situation during review.
  • Even if the remote resources used are not malicious, they can easily be hijacked, leading to security vulnerabilities in the application and bringing unpredictable harm to users.

matrix-iOS#

An open-source stuttering monitoring system from WeChat.

matrix-iOS reduces its performance impact on the app through four details:

  1. The monitoring detection interval for the child thread is 1 second under normal conditions, and in case of stuttering, the interval will be influenced by the annealing algorithm, increasing according to the Fibonacci sequence until it returns to 1 second when there is no stuttering.
  2. The annealing algorithm for child thread monitoring: Avoids repeatedly obtaining the main thread stack for the same stutter.
  3. RunLoop stuttering time threshold is set to 2 seconds.
  4. CPU usage threshold is set: When single-core CPU usage exceeds 80%, it is deemed excessively high.

References:

Teaching is learning, and reviewing helps to reinforce knowledge.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.