Bo2SS

Bo2SS

1 Basic Part (Continued)

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

image

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

First, let's look at some common crash reasons:

  • Array issues: Array out of bounds, or adding nil elements to the array.
  • Multithreading issues: UI updates in a background thread may cause crashes, for example, if one thread is nullifying data while another thread is reading that data.
  • Main thread unresponsive: 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 crash scenario.

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.

Crash information can be divided into two categories based on whether it can be captured by signals:

  1. Capturable by signals: Crash information such as array out of bounds, wild pointers, KVO issues, NSNotification thread issues, etc.
  2. Not capturable by signals: Information such as background task timeouts, memory overflow, main thread stutters 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 the 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 first, so it can be uploaded as crash logs upon the next launch.

Collecting Crash Information Not Capturable by Signals#

Background knowledge: Due to system limitations, signals thrown by system force kills 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 scenarios 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 relatively rare.
  3. Silent Push: Silent push will wake the app in the background for 30 seconds. Its priority is lower and will call the application:didReceiveRemoteNotification:fetchCompletionHandler: delegate, similar to the delegate called by regular remote push notifications.
  4. PushKit: Will wake 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 after 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 the 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, and after the task is completed, the app is suspended. However, if the task does not complete within 3 minutes, the system will forcibly kill the process, leading to a crash. This is the reason why apps are prone to crashes when going 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 launch or when the app is awakened in the background.

(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. As it approaches 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 scenarios cannot be captured by signals?

Mainly memory overflow and main thread stutters that exceed the timeout and are killed by the Watchdog.

(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.

How to Analyze and Solve Crash Problems After Collecting Crash Information?#

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 the unique identifier of the crash report, unique key value, device identifier;
  4. Basic information: Date of the crash, iOS version;

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 the watchdog due to being unresponsive for a certain period, see the next lesson), 0xdeadfa11 (App was forcefully exited by the user), 0xc00010ff (App was killed due to causing the device to overheat, see lesson 18 on battery optimization).

⚠️: Some issues cannot be analyzed solely through the stack; at this point, you can use logs related to user behavior and system environment conditions before the crash for further analysis, see lesson 15 on log monitoring.

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 Stutters?#

Stutter issues refer to the inability to respond to user interactions on the main thread, caused by factors such as: excessive UI rendering; performing synchronous network requests on the main thread, performing a large number of 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 were 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 stutter situations.

Let's first introduce the principles of RunLoop.

RunLoop Principles#

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

Task: Monitor 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: Place heavy, non-urgent tasks that consume a lot of CPU (like image loading) into a free RunLoop mode for execution, avoiding execution when the RunLoop mode is UITrackingRunLoopMode. UITrackingRunLoopMode mode:

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

Working Principle:

In iOS, the RunLoop object is implemented by CFRunLoop, and the entire process can be summarized in the following diagram. For specifics, see 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 of the loop
}

RunLoop's thread blocking situations:

  1. Execution time of methods before entering sleep is too long (leading to inability to enter sleep);
  2. Time taken to receive messages after the thread wakes up is too long (leading to inability to proceed to the next step).

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

Therefore, to use RunLoop principles to monitor stutters, we need to focus on these two loop states:

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

How to Check for Stutters?#

Three steps:

  1. Create a CFRunLoopObserverContext observer;
  2. Add the observer to observe the main thread RunLoop in common mode;
  3. Create a persistent child thread to monitor the main thread's RunLoop state. Stutter determination: 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 the stutter.

⚠️: The time threshold for triggering stutters can be set based on 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 stutters must be less than the WatchDog's limit time.

How to Obtain Stutter Method Stack Information?#

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

  2. Directly use the PLCrashReporter open-source library. Its characteristic is that it can locate the specific location of the problematic code, and it has been optimized for performance consumption.

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

Related materials: 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 an app is forcibly 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 adopted 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 your phone (Settings > Privacy > Analytics & Improvements > Analytics Data) to understand the memory limits for apps on different devices and system versions.

Focus on the crash reason in the system logs corresponding to per-process-limit and rpages:

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

⚠️:

  • The value of the memory page size is the pageSize value in the logs.
  • Apps that are forcibly killed cannot obtain 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 situation, maintaining a stack of all app processes. Additionally, it 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 will send a notification, and the app under memory pressure will execute the corresponding didReceiveMemoryWarning delegate (this is an opportunity to release memory and can avoid the app being forcibly killed by the system).

Priority Judgment Basis (Before forcibly killing an app, the system will first make a priority judgment):

  • 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 permissions, which are insufficient within the app, 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 task_info function provided by the iOS system to obtain 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 memory occupied by the app.

Locating Memory Problem Information Collection#

Obtaining memory usage is not enough; you also need to know who allocated the memory to accurately pinpoint the problem. 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 small memory allocations below 256B, while larger memory allocations 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: In addition to being forcibly killed due to excessive memory, there are three other memory issues:

  • Accessing unallocated memory: XNU will report EXC_BAD_ACCESS error, with signal SIGSEGV Signal #11.
  • Accessing memory without adhering to permission: 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 physical memory allocation, freezing the thread when it attempts to allocate 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 in the App?#

Background: Previous lessons shared monitoring of crashes, stutters, and memory issues. Once a problem is detected, it is necessary to record detailed information about the issue in logs 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 for recording user behavior and key operations.

The function of full logs: To facilitate developers in quickly and accurately locating 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 self-developed or using third-party logging libraries. So, how can we obtain all logs in the app in a non-intrusive way?

Below are the methods for obtaining 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 the API of ASL (Apple System Logger, a logging system implemented by Apple) to store log messages directly 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 send a notification.
  • By registering this notification com.apple.system.logger.message (kNotifyASLDBUpdate macro) through 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 invalid notifications for other processes 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 is easier to retrieve them later, see the next section for details. The log levels include two categories: the first category is Verbose and Debug, which are for debugging; the second category is Info, Warn, Error, which are for formal logging and need to be persistently stored for more important information. The default is Verbose level.
  • Using NSLog for debugging can lead to I/O disk operations, so frequent use of NSLog is not performance-friendly.
  • There are many cross-process notifications, such as when the system disk space is insufficient, which will send 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 (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 access all levels of message passing in the system;
  • However, it does not have an ASL-like interface 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 to invoke the original NSLog method, or use the original method call method provided by fishhook.
  1. Use the dup2 function to redirect the STDERR handle.

The final file handle for NSLog 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 output.

Now, logs from various system versions of NSLog can be obtained. How 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 mostly wrap 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 analyzing and solving problems, so give it a try!

16 | Performance Monitoring: The Measure of App Quality#

Purpose: To proactively and efficiently discover performance issues, preventing the app's quality from entering an uncontrolled state without supervision.

Monitoring methods: Offline and 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 includes two components: Standard UI and Analysis Core. All its tools are developed based on these two components. Based on these 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 task being to complete the configuration of Standard UI and Analysis Core;
  3. Refer to the numerous 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 be synthesized 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 for recording, 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), FMDB (third-party database framework) are very cautious when using multithreading technology; especially since UIKit does not use multithreading technology, it is made thread-unsafe and can only be operated on the main thread.

Why does this phenomenon occur? Let's take a look at two common pitfalls of multithreading technology: Persistent Threads and Concurrency Issues.

Persistent Threads#

Definition: Threads that do not stop and exist in memory indefinitely.

Where do they come from?

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

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 an NSOperationQueue and then specifically creates the above persistent thread to receive callbacks from NSOperationQueue.

The reason for not avoiding the persistent thread pitfall is that the network request using NSURLConnection 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. Creating a separate thread instead of using the main thread is because the main thread still needs to handle a lot of UI and interaction work.

🎉: However, in AFNetworking 3.0, it replaced NSURLConnection with NSURLSession introduced by Apple, 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?

Having too many persistent threads not only fails to improve CPU utilization but also reduces the execution efficiency of the program.

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

  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 certain period.

⚠️: The methods for adding a runloop through NSRunLoop are run, runUntilDate:, and runMode:beforeDate:. Among them, the run method adds a runloop that will repeatedly call the runMode:beforeDate: method to ensure it does not stop.

Concurrency#

Where do they come from? Creating multiple threads simultaneously.

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

  • Advantages: Simple and easy-to-use interface, convenient for managing complex threads (creation, release timing, etc.).
  • Disadvantages: There are risks in resource usage. For example, in database read-write scenarios:
    • When read-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 and more new threads, leading to tightening memory resources.
    • When the disk starts responding, reading data will occupy even more memory, ultimately resulting in uncontrolled memory management.

How to avoid?

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

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 consumption.

Tips: Lock issues in multithreading technology are the easiest to identify; you should pay more attention to those hidden problems 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; too short intervals for scheduled tasks...

To find the specific location, use the elimination method: comment out each function one by one and observe the changes in power consumption.

However, it must be said that you must first obtain the battery level to 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 level change notifications, calling 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 reveal the problem, then this power consumption must be caused by other threads, and the thread that causes this power consumption may be in third-party libraries or second-party 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 stacks, refer to section 13 | Obtaining Stutter Method Stack Information.

Optimizing Power Consumption#

CPU Aspect#

Avoid unnecessary tasks for the CPU.

  1. For complex calculations involving large amounts of data, delegate to the server for processing.
  2. For complex calculations that must be handled within the app, you can use GCD's dispatch_block_create_with_qos_class method to specify the queue's Qos as QOS_CLASS_UTILITY, placing the computation work into the block of this queue. This is because, in this Qos mode, the system has made power optimizations for 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's built-in NSCache to complete data aggregation in memory:
    • It is thread-safe.
    • It will clean 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, networking, graphics, animations, video, location, accelerometers, gyroscopes, magnetometers, and Bluetooth.
  • Writing Energy Efficient Apps” - A session shared by Apple at WWDC 2017 on how to write energy-efficient apps.

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

Learning Order for RunLoop Principles#

  1. Sun Yuan's Offline Sharing | RunLoop: Get a general understanding of RunLoop.
  2. RunLoop Official Documentation: A 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 removed 718 apps, it mentioned that using methods like dlopen(), dlsym(), respondsToSelector:, performSelector:, and method_exchangeImplementations() to execute remote scripts is not allowed. Because:

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

matrix-iOS#

An open-source stutter 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 normally 1 second. In the case of stutters, the interval will be influenced by the annealing algorithm, increasing according to the Fibonacci sequence until it returns to 1 second when there are no stutters.
  2. The annealing algorithm for the child thread monitoring: Avoids repeatedly obtaining the main thread stack for the same stutter.
  3. RunLoop stutter time threshold is set to 2 seconds.
  4. CPU usage threshold is set to determine high CPU usage when a single-core CPU usage exceeds 80%.

References:

Teaching is learning, reviewing helps to understand new knowledge~

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