One, the introduction

During iOS application development, it is inevitable that the application will crash due to various exceptions.

For crashes encountered during development, we can quickly locate the problem based on local crash information. However, for some crashes in the online version, we can only analyze the specific cause by collecting crash information. Although Apple does provide crash reporting, not all users have turned it on. Therefore, collecting crash information and reporting it is an essential feature of the data acquisition SDK.

The following is for the analysis of iOS SDK crash acquisition module, hoping to provide you with some reference.

Two, crash type

Application crash information is collected in the following scenarios:

  • NSException exception;
  • The Unix signal is abnormal.

Before designing a crash capture scheme, it’s worth taking a look at NSException and Unix signals.

2.1 NSException NSException[1] is a class provided by the Foundation framework. Encapsulate some exception information and throw it when needed. The encapsulated exception information includes the exception name, the exception cause, and the call stack.


@interface NSException : NSObject <NSCopying, NSSecureCoding>
 
@property (readonly, copy) NSExceptionName name;
@property (nullable, readonly, copy) NSString *reason;
@property (readonly, copy) NSArray<NSString *> *callStackSymbols;
 
@end
Copy the code

In iOS applications, the most common exception is thrown by @throw, as shown in Figure 2-1:

Figure 2-1 Exception handling process (the picture is from Apple developer documentation [2])

For example, the common out-of-bounds array access exception:


@throw [NSException exceptionWithName:@"NSRangeException" reason:@"index 2 beyond boun
Copy the code

The following exception information is displayed when the program is run:


Terminating app due to uncaught exception 'NSRangeException'.reason: 'index 2 beyond bounds [0 .. 1]'
terminating with uncaught exception of type NSException
Copy the code

2.2 Unix signal

In the crash logs automatically collected by the iOS system, logs similar to the following are often displayed:


Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERNINVALIDADDRESS at 0x0000000001000010
VM Region Info: 0x1000010is not in any region. Bytes before following region: 4283498480
REGION TYPE START - END [ VSIZE] PRT/MAX SHRMOD REGION DETAIL UNUSED SPACE AT START TEXT 0000000100510000-0000000100514000 [16K] r-x/r-x SM=COW
.app/Ekuaibao
Termination Signal: Segmentation fault: 11
Termination Reason: Namespace SIGNAl, Code 0xb Terminating Process: exc handler [21776]
Triggered by Thread: 9
Copy the code

The two fields EXC_BAD_ACCESS and SIGSEGV in Exception Type refer to Mach exceptions and Unix signals, respectively.

So what are Mach exceptions and Unix signals?

Mach is the microkernel of macOS and iOS operating systems, and Mach exceptions are the lowest kernel-level exceptions [3]. Mach exceptions are converted to the appropriate Unix signal and passed to the thread that failed. EXC_BAD_ACCESS in the above Exception Type is a Mach layer Exception that is converted to the Unix signal SIGSEGV and then passed to the error thread. The reason Mach exceptions are converted to Unix signals is to be compatible with the POSIX standard (SUS specification) [4], so that developers without knowledge of the Mach kernel can use Unix signals for compatible development.

There are many types of Unix signals. In iOS applications, common Unix signals [5] are as follows:

  • SIGILL: Signal of an invalid program instruction, usually due to an error in the executable itself or an attempt to execute a data segment. This signal may also be generated when the stack overflows;
  • SIGABRT: the abort signal generated when abort is called.
  • SIGBUS: program memory byte address unaligned abort signal, such as accessing a 4-byte long integer whose address is not a multiple of 4;
  • SIGFPE: a program floating point exception signal, usually generated when floating point arithmetic errors, overflow, and arithmetic errors such as a divisor of 0 occur;
  • SIGKILL: termination of a program Receives an abort signal that terminates the program immediately and cannot be processed, blocked, or ignored;
  • SIGSEGV: Program invalid memory abort signal, that is, attempt to access unallocated memory, or write data to a memory address that does not have write permission;
  • SIGPIPE: signal of a program pipe break, usually produced during interprocess communication;
  • SIGSTOP: Program process termination signal, like SIGKILL, cannot be processed, blocked, or ignored.

IOS SDK designed and implemented a crash acquisition scheme for data analysis for NSException and Unix signal anomaly.

Third, NSException abnormal collection

3.1 Solution Overview

NSException class defined in the NSSetUncaughtExceptionHandler can set the global exception handler. As a result, we can through setting the function to handle exceptions NSSetUncaughtExceptionHandler first, and then collect the exception stack information and trigger the corresponding event ($AppCrashed), to implement NSException abnormal points.

NSSetUncaughtExceptionHandler function receives a C function pointer, the function are defined as follows:


typedef void NSUncaughtExceptionHandler(NSException *exception);
 
FOUNDATION_EXPORT void NSSetUncaughtExceptionHandler(NSUncaughtExceptionHandl
Copy the code

3.2 Specific Implementation

  1. Design a method to collect the $AppCrashed event and log the stack information to the event attribute app_crashed_Reason:

- (void)sa_handleUncaughtException:(NSException *)exception {
    // Collect the $AppCrashed event
    SensorsAnalyticsSDK *sdk = [SensorsAnalyticsSDK sharedInstance];
    if (sdk.configOptions.enableTrackAppCrash) {
        NSMutableDictionary *properties = [[NSMutableDictionary alloc] init];
        if ([exception callStackSymbols]) {
            // If there is an exception stack information, obtain the exception stack information
            NSString *exceptionStack = [[exception callStackSymbols] componentsJoinedByString:@"\n"];
            // Collect the application crash cause
            [properties setValue:[NSString stringWithFormat:@"Exception Reason:%@\nException Stack:%@", [exception reason], exceptionStack] forKey:@"app_crashed_reason"];
        } else {
            // If there is no abnormal stack information, the thread stack information is obtained
            NSString *exceptionStack = [[NSThread callStackSymbols] componentsJoinedByString:@"\n"];
            // Collect the application crash cause
            [properties setValue:[NSString stringWithFormat:@"% @ % @", [exception reason], exceptionStack] forKey:@"app_crashed_reason"];
        }
        // Trigger $AppCrashed event
        [sdk trackPresetEvent:SA_EVENT_NAME_APP_CRASHED properties:properties];
    }
    NSSetUncaughtExceptionHandler(NULL);
}
Copy the code
  1. Create and add + SensorsAnalyticsExceptionHandler sharedHandler method:

+ (instancetype)sharedHandler {
    static SensorsAnalyticsExceptionHandler *gSharedHandler = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        gSharedHandler = [[SensorsAnalyticsExceptionHandler alloc] init];
    });
    return gSharedHandler;
}
Copy the code
  1. Implement SensorsAnalyticsExceptionHandler initialization method of a class – init, set the global exception handler and trigger $AppCrashed events:

- (instancetype)init {
    self = [super init];
    if (self) {
        [self setupHandlers];
    }
    return self;
}
 
- (void)setupHandlers {
    // Set the global exception handler
    NSSetUncaughtExceptionHandler(&SAHandleException);
}
 
static void SAHandleException(NSException *exception) {
    SensorsAnalyticsExceptionHandler *handler = [SensorsAnalyticsExceptionHandler sharedHandler];
    // Handle the caught NSException, which triggers the $AppCrashed event
    [handler sa_handleUncaughtException:exception];
}
Copy the code
  1. In SensorsAnalyticsSDK type – initWithConfigOptions: debugMode: method of initialization SensorsAnalyticsExceptionHandler singleton class:

- (instancetype)initWithConfigOptions:(nonnull SAConfigOptions *)configOptions debugMode:(SensorsAnalyticsDebugMode)debugMode {
    self = [super init];
    if (self) {
        // Enable crash collection
        if (_configOptions.enableTrackAppCrash) {
            [[SensorsAnalyticsExceptionHandler sharedHandler];
        }
    }
    return self;
}
Copy the code

3.3 Scheme Optimization

In the actual development process, multiple SDKS may be integrated. If all SDKS collect exception information according to the method described above, some SDKS may fail to collect exception information. This is because by NSSetUncaughtExceptionHandler function Settings is a global exception handler, set behind the exception handler will automatically overwrite the previous set of exception handling function. So how to solve this problem? Common practices are: In setting global exception handler NSSetUncaughtExceptionHandler function called before, first by NSGetUncaughtExceptionHandler function has been set before obtaining the exception handler and save, after in dealing with the abnormal information, The override problem mentioned above can be solved by actively calling the saved handler.

  1. The properties of a new NSUncaughtExceptionHandler type defaultExceptionHandler, used to store already set before the exception handler:

@property (nonatomic) NSUncaughtExceptionHandler *defaultExceptionHandler;
 
- (void)setupHandlers {
    // Back up the previously set exception handler
    _defaultExceptionHandler = NSGetUncaughtExceptionHandler();
    // Set the global exception handler
    NSSetUncaughtExceptionHandler(&SAHandleException);
}
Copy the code
  1. Invoking the $AppCrashed event calls the previously set exception handler, passing UncaughtExceptionHandler:

static void SAHandleException(NSException *exception) {
    // Handle the caught NSException, which triggers the $AppCrashed event
     
    / / pass UncaughtExceptionHandler
    if(handler.defaultExceptionHandler) { handler.defaultExceptionHandler(exception); }}Copy the code

By doing this, you can chain all the exception handlers to ensure that the exception handlers you set earlier can also collect exception information.

4. Abnormal collection of Unix signals

4.1 Solution Overview

In iOS applications, common signals such as SIGILL, SIGABRT, SIGBUS, SIGFPE, and SIGSEGV are collected to meet the requirements of routine application exception information collection. We can first add a new signal handler, then register the signal handler, and use the Unix signal information to construct an NSException object to reuse the method that collected the $AppCrashed event from the previous section.

4.2 Implementation

  1. New handlers to capture Unix signals:

static NSString * const UncaughtExceptionHandlerSignalExceptionName = @"UncaughtExceptionHandlerSignalExceptionName";
static NSString * const UncaughtExceptionHandlerSignalKey = @"UncaughtExceptionHandlerSignalKey";
 
static void SASignalHandler(int crashSignal, struct __siginfo *info, void *context) {
    SensorsAnalyticsExceptionHandler *handler = [SensorsAnalyticsExceptionHandler sharedHandler];
    // Construct the Unix signal exception as NSException exception
    NSDictionary *userInfo = @{UncaughtExceptionHandlerSignalKey: @(crashSignal)};
    NSString *reason = [NSString stringWithFormat:@"Signal %d was raised.", crashSignal];
    NSException *exception = [NSException exceptionWithName:UncaughtExceptionHandlerSignalExceptionName reason:reason userInfo:userInfo];
    // Handle the caught Unix signal exception and trigger the $AppCrashed event
    [handler sa_handleUncaughtException:exception];
}
Copy the code
  1. Registers signal handlers

- (void)setupHandlers {
    // Back up and set NSException global exception handler
     
    // Define the signal set structure
    struct sigaction action;
    // Initialize the signal set to null
    sigemptyset(&action.sa_mask);
    // Pass the __siginfo argument to the handler
    action.sa_flags = SA_SIGINFO;
    // Set the signal handler function
    action.sa_sigaction = &SASignalHandler;
    // Define the signal type to be collected
    int signals[] = {SIGABRT, SIGILL, SIGSEGV, SIGFPE, SIGBUS};
    for (int i = 0; i < sizeof(signals) / sizeof(int); i++) {
        struct sigaction prev_action;
        int err = sigaction(signals[i], &action, &prev_action);
        if (err) {
            SALogError(@"Errored while trying to set up sigaction for signal %d", signals[i]); }}}Copy the code

Note: Since the Unix signal exception object was built ourselves, there is no stack information, which is obtained by default for the current thread. The previous section – sa_handleUncaughtException: This logic is already handled in the method.

4.3 Scheme Optimization

Also, to avoid affecting other SDKS from catching Unix signals, we should save the Unix signal exception handlers we have set up before processing Unix signals. Then, after processing the exception information, the saved Unix signal exception handler is actively called. The logic for passing Unix signals is similar to that for passing UncaughtExceptionHandler in the previous section.

  1. New prev_signal_handlers property to hold previously set Unix signal exception handlers:

@property (nonatomic, unsafe_unretained) struct sigaction *prev_signal_handlers;
 
- (void)setupHandlers {
    // Back up and set NSException global exception handler
     
    // Register the signal set
    struct sigaction action;
    sigemptyset(&action.sa_mask);
    action.sa_flags = SA_SIGINFO;
    action.sa_sigaction = &SASignalHandler;
    int signals[] = {SIGABRT, SIGILL, SIGSEGV, SIGFPE, SIGBUS};
    for (int i = 0; i < sizeof(signals) / sizeof(int); i++) {
        struct sigaction prev_action;
        int err = sigaction(signals[i], &action, &prev_action);
        if (err == 0) {
            char *address_action = (char *)&prev_action;
            // Save the Unix signal exception handler
            char *address_signal = (char *)(_prev_signal_handlers + signals[i]);
            strlcpy(address_signal, address_action, sizeof(prev_action));
        } else {
            SALogError(@"Errored while trying to set up sigaction for signal %d", signals[i]); }}}Copy the code
  1. Raise the $AppCrashed event and pass a Unix signal to the previously saved exception handler and call:

static void SASignalHandler(int crashSignal, struct __siginfo *info, void *context) {
    // Handle the caught Unix signal exception and trigger the $AppCrashed event
     
    // Gets an exception handler that passes Unix signals
    struct sigaction prev_action = handler.prev_signal_handlers[crashSignal];
    if (prev_action.sa_flags & SA_SIGINFO) {
        if(prev_action.sa_sigaction) { prev_action.sa_sigaction(crashSignal, info, context); }}else if(prev_action.sa_handler && prev_action.sa_handler ! = SIG_IGN) {// SIG_IGN indicates that signals are ignoredprev_action.sa_handler(crashSignal); }}Copy the code

Note: If other SDKS ignore a signal while processing Unix signals, you should avoid passing the ignored Unix signal to them after the $AppCrashed event is triggered. We made a judgment call to the sa_handler function to handle this logic.

5. Reissue withdrawal event

Once the application is abnormal, we will not collect the App exit event (AppEnd). This causes an App launch event (AppEnd) to appear in the user’s sequence of actions. This causes an App launch event (AppEnd) to appear in the user’s sequence of actions. This will result in a mismatch between App start events (AppStart) and App exit events (AppEnd) in the user’s behavior sequence. Therefore, when the application crashes, we need to reissue the case that the AppEnd is not matched. Therefore, when the application crashes, we need to reissue the case that the AppEnd is not matched. Therefore, when the application crashes, we need to reissue the AppEnd event:


- (void)sa_handleUncaughtException:(NSException *)exception {
    // Collect the $AppCrashed event
     
    // Reissue the $AppEnd event
    if(! [sdk isAutoTrackEventTypeIgnored:SensorsAnalyticsEventTypeAppEnd]) { [SACommonUtility performBlockOnMainThread:^{if(UIApplication.sharedApplication.applicationState == UIApplicationStateActive) { [sdk trackAutoEvent:SA_EVENT_NAME_APP_END properties:nil]; }}]; }// Block the current thread to complete data related tasks in serialQueue
    sensorsdata_dispatch_safe_sync(sdk.serialQueue, ^{});
}
Copy the code

After doing this, when an exception occurs in our application, we can not only collect the AppCrashed event, but we can also collect the AppEnd event.

Six, summarized

This paper mainly introduces the specific implementation of iOS SDK crash acquisition module of Strategic analysis. SDK crash collection covers NSException exception and Unix signal exception, and detailed implementation can refer to the iOS SDK source code [6].

Finally, we hope that through this article, we can have a systematic understanding of the iOS SDK crash module analysis.

References:

[1] developer.apple.com/documentati…

[2] developer.apple.com/library/arc…

[3] mp.weixin.qq.com/s/hOOzVzJ-n…

[4] zh.wikipedia.org/wiki/%E5%96…

[5] blog.51cto.com/arthurchen/…

[6] github.com/sensorsdata…