I. Preface
Crash problems may arise in our applications during daily development or testing. We should adopt a zero tolerance attitude towards such problems, because if such problems occur online, it will seriously affect the user's experience.
If Crash happens to be in the process of development, developers can locate the cause of the problem based on the Xcode call stack or console output information. However, if it is in the process of testing, it will be more troublesome. Two common solutions are:
- Connect the test phone directly to Xcode to view the logs in the device information.
- We need to test students to give Crash's replication path, and then the developer will replicate it in the debugging process.
However, the above two ways are not very convenient. So the question arises. Is there a better way to view Crash logs? The answer, of course, is yes. The Crash view function in DoraemonKit's common toolkit solves this problem. You can view Crash log directly on the APP side. Let's introduce the implementation of Crash view function.
II. Technological Realization
In the development of iOS, there will be a variety of Crash, so how can we capture these different Crash? In fact, there are two kinds of common Crash anomalies, one is Objective-C anomaly and the other is Mach anomaly. Some common anomalies are shown in the following figure:
Next, let's look at how these two types of exceptions should be captured.
2.1 Objective-C anomaly
As the name implies, Objective-C exceptions refer to exceptions that occur at the OC level (iOS libraries, third-party libraries when errors occur). Before describing how to capture Objective-C exceptions, let's look at what common Objective-C exceptions include.
2.1.1 Common Objective-C anomalies
Generally speaking, common Objective-C anomalies include the following:
-
NSInvalid ArgumentException
The main reason for this kind of anomaly is that the validity of the parameter is not checked, and the most common one is to pass in nil as the parameter. For example, NSMutableDictionary adds an object whose key is nil, and the test code is as follows:
NSString *key = nil; NSString *value = @"Hello"; NSMutableDictionary *mDic = [[NSMutableDictionary alloc] init]; [mDic setObject:value forKey:key];
After running, the console output logs:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[__NSDictionaryM setObject:forKey:]: key cannot be nil'
-
NSRangeException
The main reason for this kind of exception is that the index is not checked for its legitimacy, which results in the index falling outside the legitimate scope of the set data. For example, the index goes beyond the range of the array and causes the problem of array crossing the boundary. The test code is as follows:
NSArray *array = @[@0, @1, @2]; NSUInteger index = 3; NSNumber *value = [array objectAtIndex:index];
After running, the console output logs:
*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayI objectAtIndex:]: index 3 beyond bounds [0 .. 2]'
-
NSGenericException
Such exceptions are most likely to occur in foreach operations, mainly because elements are modified during traversal. For example, modifying the traversed array in a for in loop can cause this problem. The test code is as follows:
NSMutableArray *mArray = [NSMutableArray arrayWithArray:@[@0, @1, @2]]; for (NSNumber *num in mArray) { [mArray addObject:@3]; }
After running, the console output logs:
*** Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <__NSArrayM: 0x600000c08660> was mutated while being enumerated.'
-
NSMallocException
The main reason for such exceptions is that insufficient memory space cannot be allocated. For example, allocating a large memory space can lead to such exceptions. The test code is as follows:
NSMutableData *mData = [[NSMutableData alloc] initWithCapacity:1]; NSUInteger len = 1844674407370955161; [mData increaseLengthBy:len];
After running, the console output logs:
*** Terminating app due to uncaught exception 'NSMallocException', reason: 'Failed to grow buffer'
-
NSFileHandleOperationException
The main reason for this kind of anomaly is that there are some anomalies in the operation of files, such as the mobile phone does not have enough storage space, the permission of reading and writing files, and so on. For example, for a file with read permission only, the test code is as follows:
NSString *cacheDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; NSString *filePath = [cacheDir stringByAppendingPathComponent:@"1.txt"]; if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) { NSString *str1 = @"Hello1"; NSData *data1 = [str1 dataUsingEncoding:NSUTF8StringEncoding]; [[NSFileManager defaultManager] createFileAtPath:filePath contents:data1 attributes:nil]; } NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingAtPath:filePath]; [fileHandle seekToEndOfFile]; NSString *str2 = @"Hello2"; NSData *data2 = [str2 dataUsingEncoding:NSUTF8StringEncoding]; [fileHandle writeData:data2]; [fileHandle closeFile];
After running, the console output logs:
*** Terminating app due to uncaught exception 'NSFileHandleOperationException', reason: '*** -[NSConcreteFileHandle writeData:]: Bad file descriptor'
This article introduces several common Objective-C anomalies. Next, let's look at how to capture Objective-C anomalies.
2.1.2 Capture Objective-C Anomalies
If in the development process, Crash caused by Objective-C exceptions will output the type, cause and call stack of the exceptions in the Xcode console. Based on this information, we can quickly locate the cause of the exceptions and repair them.
So if it's not in the development process, how should we capture the information of these exceptions?
In fact, Apple has provided us with an API for capturing Objective-C exceptions, namely NSSet Uncaught Exception Handler. Let's first look at how official documents describe:
Sets the top-level error-handling function where you can perform last-minute logging before the program terminates.
This means that after the exception handling function is set through this API, the log can be recorded at the last minute before the program terminates. This function is exactly what we want, and it is relatively simple to use. The code is as follows:
+ (void)registerHandler { NSSetUncaughtExceptionHandler(&DoraemonUncaughtExceptionHandler); }
The parameter Doraemon UncaughtException Handler here is the exception handler, which is defined as follows:
// Callback function in collapse static void DoraemonUncaughtExceptionHandler(NSException * exception) { // Exceptional stack information NSArray * stackArray = [exception callStackSymbols]; // Causes of abnormalities NSString * reason = [exception reason]; // Exception name NSString * name = [exception name]; NSString * exceptionInfo = [NSString stringWithFormat:@"========uncaughtException Exception error reporting========\nname:%@\nreason:\n%@\ncallStackSymbols:\n%@", name, reason, [stackArray componentsJoinedByString:@"\n"]]; // Save the crash log to the sandbox cache directory [DoraemonCrashTool saveCrashLog:exceptionInfo fileName:@"Crash(Uncaught)"]; }
From the above code, we can see that when an exception occurs, the name of the exception, the cause of the exception, and the stack information of the exception can be obtained. After you get this information, save it in the cache directory of the sandbox and then you can view it directly.
It is important to note that for an APP, there may be multiple Crash collection tools. If everyone calls NSSet Uncaught Exception Handler to register exception handlers, then the registered exception handlers will override the registered exception handlers, which will cause the previously registered exception handlers to not work properly.
How can we solve this problem of coverage? The idea is simple. Before we call NSSetUncaughtException Handler to register the exception handler, we first get the existing exception handler and save it. Then, after the execution of our handler, we can call the handler saved before. In this way, the later registration will not affect the previous registration.
With ideas, how to achieve it? As you can see from Apple's documentation, there is an API to get the previous exception handling function, namely NSGetUncaught Exception Handler, through which we can get the previous exception handling function. The code is as follows:
// Collapse callback function before recording static NSUncaughtExceptionHandler *previousUncaughtExceptionHandler = NULL; + (void)registerHandler { // Backup original handler previousUncaughtExceptionHandler = NSGetUncaughtExceptionHandler(); NSSetUncaughtExceptionHandler(&DoraemonUncaughtExceptionHandler); }
Before we set up our own exception handler, we save the existing exception handler. When dealing with exceptions, we need to throw exceptions to the previously saved exception handlers after our own exception handlers are finished. The code is as follows:
// Callback function in collapse static void DoraemonUncaughtExceptionHandler(NSException * exception) { // Exceptional stack information NSArray * stackArray = [exception callStackSymbols]; // Causes of abnormalities NSString * reason = [exception reason]; // Exception name NSString * name = [exception name]; NSString * exceptionInfo = [NSString stringWithFormat:@"========uncaughtException Exception error reporting========\nname:%@\nreason:\n%@\ncallStackSymbols:\n%@", name, reason, [stackArray componentsJoinedByString:@"\n"]]; // Save the crash log to the sandbox cache directory [DoraemonCrashTool saveCrashLog:exceptionInfo fileName:@"Crash(Uncaught)"]; // Callback function that crashed before calling if (previousUncaughtExceptionHandler) { previousUncaughtExceptionHandler(exception); } }
At this point, we have basically completed the capture of Objective-C exceptions.
2.2 Mach anomaly
The Objective-C anomaly is introduced in the previous section, and the Mach anomaly is introduced in this section. What exactly is a Mach anomaly? Before answering this question, let's take a look at some relevant knowledge.
2.2.1 Mach-related concepts
The above figure is from Apple's Mac Technology Overview, and OS X and iOS architectures are generally consistent for the Kernel and Device Drivers layers. Among them, the core part is XNU, and Mach is the core of XNU's microkernel.
Mach's main responsibilities are process and thread abstraction, virtual memory management, task scheduling, inter-process communication and message passing mechanism.
There are several basic concepts in Mach microkernels:
- task: Objects that have a set of system resources in which thread s are allowed to execute.
- thread: The basic unit of execution that has the context of task and shares its resources.
- Port: A group of protected message queues for communication between task s that can send/receive data to any port.
- message: A collection of typed data objects that can only be sent to the port.
The BSD layer, on top of Mach, provides a reliable and more modern API and POSIX compatibility.
2.2.2 Mach anomaly and Unix signal
After understanding some of Mach's related concepts, let's look at what Mach anomalies are. Here Quotes Talking about iOS Crash Collection Framework The explanation of Mach anomaly.
Apple's Crash Reporter, which comes with iOS system, records Crash logs on devices. Exception Type items usually contain two elements: Mach exceptions and Unix signals.
Mach exception: Allows processing in or out of process, and the handler is called through Mach RPC.
Unix signal: Processing only in the process, the handler always calls on the thread where the error occurs.
Mach exceptions refer to the lowest level of kernel-level exceptions defined under <mach/exception_types.h>. Every thread, task and host has an array of abnormal ports. Some APIs of Mach are exposed to user mode. User mode developers can directly set abnormal ports of thread, task and host through Mach API to capture Mach exceptions and Crash events.
All Mach exceptions are converted to corresponding Unix signals by ux_exception s in the host layer, and the signals are delivered to the wrong thread through threadsignal s. The POSIX API in iOS is implemented through the BSD layer above Mach. As shown in the following figure:
For example, Exception Type: EXC_BAD_ACCESS (SIGSEGV) means that EXC_BAD_ACCESS exceptions at the Mach layer are converted into SIGSEGV signals at the host layer and delivered to the error thread. The following figure shows the conversion from Mach exception to Unix signal:
Since the signal is eventually delivered to the wrong thread in the form of a signal, the signal can be captured by registering the signal handler:
signal(SIGSEGV,signalHandler);
Crash events can be caught by capturing Mach anomalies or Unix signals. Here we use Unix signal to capture, the main reasons are as follows:
- Mach exceptions don't have a convenient way to capture them. Since they will eventually be converted into signals, we can also capture Crash events by capturing signals.
- Unix signals are converted to be compatible with the more popular POSIX standard (SUS specification), so that the Mach kernel can be developed compatibly through Unix signals without knowing about it.
For the above reasons, we choose Unix-based signal to capture anomalies.
2.2.3 Signal Interpretation
There are many kinds of Unix signals. Detailed definitions can be found in <sys/signal.h>. The following are the common signals we monitor and their meanings:
- SIGABRT: Signal generated by calling abort function.
- SIGBUS: Illegal addresses, including memory address alignment errors. For example, access a four-word integer, but its address is not a multiple of 4. It differs from SIGSEGV in that the latter is triggered by illegal access to legitimate storage addresses (such as access that does not belong to its own storage space or read-only storage space).
- SIGFPE: Issued when a fatal arithmetic error occurs. It includes not only floating-point arithmetic errors, but also all other arithmetic errors such as overflow and divisor 0.
- SIGILL: Illegal instructions were executed. Usually because of an error in the executable itself or an attempt to execute a data segment. This signal may also be generated when the stack overflows.
- SIGPIPE: Pipeline rupture. This signal is usually generated by inter-process communication, such as two processes using FIFO (pipeline) communication. The read pipeline is written to the pipeline without opening or terminating unexpectedly, and the write process receives SIGPIPE signal. In addition, the two processes communicating with Socket, when the writing process writes the Socket, the reading process has terminated.
- SIGSEGV: Attempts to access memory that is not allocated to you, or to write data to memory addresses that do not have write permissions.
- SIGSYS: Illegal system call.
- SIGTRAP: Generated by breakpoint instructions or other trap instructions and used by debugger.
More explanations of signals are available for reference. iOS Exception Capture.
2.2.4 Capture Unix Signal
Similar to the idea of capturing Objective-C exceptions in the previous section, an exception handling function is registered to monitor the signal. The code is as follows:
+ (void)signalRegister { DoraemonSignalRegister(SIGABRT); DoraemonSignalRegister(SIGBUS); DoraemonSignalRegister(SIGFPE); DoraemonSignalRegister(SIGILL); DoraemonSignalRegister(SIGPIPE); DoraemonSignalRegister(SIGSEGV); DoraemonSignalRegister(SIGSYS); DoraemonSignalRegister(SIGTRAP); } static void DoraemonSignalRegister(int signal) { // Register Signal struct sigaction action; action.sa_sigaction = DoraemonSignalHandler; action.sa_flags = SA_NODEFER | SA_SIGINFO; sigemptyset(&action.sa_mask); sigaction(signal, &action, 0); }
Doraemon SignalHandler here is an exception handling function for monitoring signals, which is defined as follows:
static void DoraemonSignalHandler(int signal, siginfo_t* info, void* context) { NSMutableString *mstr = [[NSMutableString alloc] init]; [mstr appendString:@"Signal Exception:\n"]; [mstr appendString:[NSString stringWithFormat:@"Signal %@ was raised.\n", signalName(signal)]]; [mstr appendString:@"Call Stack:\n"]; // Here filter out the first line of logs // Because the signal crash callback method is registered, the system calls it and records it on the call stack, so the line log needs to be filtered out. for (NSUInteger index = 1; index < NSThread.callStackSymbols.count; index++) { NSString *str = [NSThread.callStackSymbols objectAtIndex:index]; [mstr appendString:[str stringByAppendingString:@"\n"]]; } [mstr appendString:@"threadInfo:\n"]; [mstr appendString:[[NSThread currentThread] description]]; // Save the crash log to the sandbox cache directory [DoraemonCrashTool saveCrashLog:[NSString stringWithString:mstr] fileName:@"Crash(Signal)"]; DoraemonClearSignalRigister(); }
One thing to note here is that the first line of logs has been filtered out. This is because the callback method of signal crash is registered, and the system calls it and records it on the call stack, so the line log is filtered out to avoid troubles.
From the above code, we can see that when an exception occurs, the signal name, call stack, thread information and so on can be obtained. After you get this information, save it in the cache directory of the sandbox and then you can view it directly.
Similar to the problems that may arise in capturing Objective-C exceptions, when integrating multiple Crash collection tools, if everyone registers exception handlers for the same signal, then the registered exception handlers will override the previously registered ones, which will cause the previously registered exception handlers to fail to work properly.
Referring to the idea of dealing with coverage when capturing Objective-C exceptions, we can also save the existing exception handling functions, and then call the previously saved exception handling functions after the execution of our exception handling functions. The code for implementation is as follows:
static SignalHandler previousABRTSignalHandler = NULL; static SignalHandler previousBUSSignalHandler = NULL; static SignalHandler previousFPESignalHandler = NULL; static SignalHandler previousILLSignalHandler = NULL; static SignalHandler previousPIPESignalHandler = NULL; static SignalHandler previousSEGVSignalHandler = NULL; static SignalHandler previousSYSSignalHandler = NULL; static SignalHandler previousTRAPSignalHandler = NULL; + (void)backupOriginalHandler { struct sigaction old_action_abrt; sigaction(SIGABRT, NULL, &old_action_abrt); if (old_action_abrt.sa_sigaction) { previousABRTSignalHandler = old_action_abrt.sa_sigaction; } struct sigaction old_action_bus; sigaction(SIGBUS, NULL, &old_action_bus); if (old_action_bus.sa_sigaction) { previousBUSSignalHandler = old_action_bus.sa_sigaction; } struct sigaction old_action_fpe; sigaction(SIGFPE, NULL, &old_action_fpe); if (old_action_fpe.sa_sigaction) { previousFPESignalHandler = old_action_fpe.sa_sigaction; } struct sigaction old_action_ill; sigaction(SIGILL, NULL, &old_action_ill); if (old_action_ill.sa_sigaction) { previousILLSignalHandler = old_action_ill.sa_sigaction; } struct sigaction old_action_pipe; sigaction(SIGPIPE, NULL, &old_action_pipe); if (old_action_pipe.sa_sigaction) { previousPIPESignalHandler = old_action_pipe.sa_sigaction; } struct sigaction old_action_segv; sigaction(SIGSEGV, NULL, &old_action_segv); if (old_action_segv.sa_sigaction) { previousSEGVSignalHandler = old_action_segv.sa_sigaction; } struct sigaction old_action_sys; sigaction(SIGSYS, NULL, &old_action_sys); if (old_action_sys.sa_sigaction) { previousSYSSignalHandler = old_action_sys.sa_sigaction; } struct sigaction old_action_trap; sigaction(SIGTRAP, NULL, &old_action_trap); if (old_action_trap.sa_sigaction) { previousTRAPSignalHandler = old_action_trap.sa_sigaction; } }
One thing to note here is that for the signals we listen to, we need to save the previous exception handling function.
When dealing with exceptions, we need to throw exceptions to the previously saved exception handlers after our own exception handlers are finished. The code is as follows:
static void DoraemonSignalHandler(int signal, siginfo_t* info, void* context) { NSMutableString *mstr = [[NSMutableString alloc] init]; [mstr appendString:@"Signal Exception:\n"]; [mstr appendString:[NSString stringWithFormat:@"Signal %@ was raised.\n", signalName(signal)]]; [mstr appendString:@"Call Stack:\n"]; // Here filter out the first line of logs // Because the signal crash callback method is registered, the system calls it and records it on the call stack, so the line log needs to be filtered out. for (NSUInteger index = 1; index < NSThread.callStackSymbols.count; index++) { NSString *str = [NSThread.callStackSymbols objectAtIndex:index]; [mstr appendString:[str stringByAppendingString:@"\n"]]; } [mstr appendString:@"threadInfo:\n"]; [mstr appendString:[[NSThread currentThread] description]]; // Save the crash log to the sandbox cache directory [DoraemonCrashTool saveCrashLog:[NSString stringWithString:mstr] fileName:@"Crash(Signal)"]; DoraemonClearSignalRigister(); // Callback function that crashed before calling previousSignalHandler(signal, info, context); }
At this point, the acquisition of Unix signal is basically completed.
2.3 Summary
Through the previous introduction, I believe you have a certain understanding of how to capture Crash, the following Quotes Mach Abnormality One of the diagrams summarizes the previous contents as follows:
3. Trampled pits
The above two sections introduce how to capture Object-C and Mach anomalies respectively. This section mainly summarizes some problems encountered in the process of implementation.
3.1 Problem of Capturing Objective-C Anomalies via Unix Signals
You might think that since Unix signals can capture the underlying Mach anomaly, why can't we capture Objective-C anomaly? In fact, it can be captured, but for such application-level exceptions, you will find that there is no code in the call stack, can not locate the problem. For example, the code for Object-C exceptions such as array crossing is as follows:
NSArray *array = @[@0, @1, @2]; NSUInteger index = 3; NSNumber *value = [array objectAtIndex:index];
If we use Unix signals to capture, the Crash logs are as follows:
Signal Exception: Signal SIGABRT was raised. Call Stack: 1 libsystem_platform.dylib 0x00000001a6df0a20 <redacted> + 56 2 libsystem_pthread.dylib 0x00000001a6df6070 <redacted> + 380 3 libsystem_c.dylib 0x00000001a6cd2d78 abort + 140 4 libc++abi.dylib 0x00000001a639cf78 __cxa_bad_cast + 0 5 libc++abi.dylib 0x00000001a639d120 <redacted> + 0 6 libobjc.A.dylib 0x00000001a63b5e48 <redacted> + 124 7 libc++abi.dylib 0x00000001a63a90fc <redacted> + 16 8 libc++abi.dylib 0x00000001a63a8cec __cxa_rethrow + 144 9 libobjc.A.dylib 0x00000001a63b5c10 objc_exception_rethrow + 44 10 CoreFoundation 0x00000001a716e238 CFRunLoopRunSpecific + 544 11 GraphicsServices 0x00000001a93e5584 GSEventRunModal + 100 12 UIKitCore 0x00000001d4269054 UIApplicationMain + 212 13 DoraemonKitDemo 0x00000001024babf0 main + 124 14 libdyld.dylib 0x00000001a6c2ebb4 <redacted> + 4 threadInfo: <NSThread: 0x280f01400>{number = 1, name = main}
As you can see, we can't locate the problem through the call stack mentioned above. Therefore, we need to get the NSSException that causes Crash, get the name of the exception, the cause, and the call stack from it, so that we can locate the problem accurately.
So in Doraemon Kit, we used NSSet Uncaught Exception Handler to capture Objective-C exceptions.
3.2 The problem of two kinds of abnormal coexistence
Since we have captured both Objective-C and Mach anomalies, two Crash logs will appear when an Objective-C exception occurs.
One is the log generated by setting exception handling functions through NSSet Uncaught Exception Handler, the other is the log generated by capturing Unix signals. In these two logs, the logs captured by Unix signals cannot locate the problem, so we only need the logs generated by the exception handler in NSSet Uncaught Exception Handler.
So what can be done to prevent the generation of logs that capture Unix signals? In DoraemonKit, the method is to call exit(0) or kill(getpid(), SIGKILL) actively after the Objective-C exception captures Crash.
3.3 Debugging Problems
When capturing Objective-C exceptions, debugging with Xcode clearly shows the calling process. The test code that led to Crash was invoked first, and then the exception handling function was used to capture the Crash log.
However, when debugging the acquisition of Unix signals, it is found that no exception handling function is entered. What's the matter? Didn't our capture of Unix signal work? This is not the case. The main reason is that the priority of the Xcode debugger will be higher than that of the Unix signal acquisition. The signal thrown by the system will be captured by the Xcode debugger, and the exception handling function will not be thrown up to us.
Therefore, if we want to debug the capture of Unix signal, we can not debug it directly in the Xcode debugger. The general debugging method is:
- Look at Device Logs of the device through Xcode and get the logs we print.
- Save Crash directly into a sandbox and check it out.
In Doraemon Kit, we save Crash directly into the cache directory of the sandbox, and then look at it.
3.4 Problems of Coexistence of Multiple Crash Collection Tools
As mentioned earlier, integrating multiple Crash collection tools in the same APP may have the problem of mandatory coverage, i.e. post-registered exception handlers will override previously registered exception handlers.
In order to keep DoraemonKit from affecting other Crash collection tools, the exception handlers that were previously registered are saved here before registering the exception handlers. Then, after the execution of our handler, we call the previously saved handler. In this way, Doraemon Kit will not have an impact on the previously registered Crash collection tools.
3.5 Some special Crash
Even if the process of capturing Crash is not a problem, there will still be some cases that can not be captured. For example, in a short period of time, the memory will rise sharply, at which time the APP will be kill ed by the system. However, the Unix signal at this time is SIGKILL, which is used to terminate the program immediately and can not be blocked, processed and ignored. Therefore, the signal can not be captured.
For memory leak detection, an iOS memory leak detection tool MLeaksFinder is recommended: MLeaksFinder
Some Crash can be collected, but there is no code in the log, so it is very difficult to locate. This is exactly the case with wild pointers. In view of this situation, it is recommended to refer to the series "How to Locate Obj-C Wild Pointer Random Crash":
How to Locate Obj-C Wild Pointer Random Crash(1): First Increase the Crash Rate of Wild Pointer)
How to Locate Obj-C Wild Pointer Random Crash
How to Locate Obj-C Wild Pointer Random Crash(3): How to Make Crash Self-report Home
IV. SUMMARY
The main purpose of this article is to give you a quick understanding of Crash viewing tools in Doraemon Kit. Due to the short time and limited personal level, if there are any mistakes, you are welcome to criticize and correct them.
Currently, Crash View only implements the most basic functions, which need to be improved in the future. If you have any good ideas, or find that our project has bug s, you are welcome to go to github to mention Issues or direct Pull requests. We will deal with them as soon as possible. We can also join our qq communication group to communicate. We hope that our tool set can do better with your efforts. Perfect.
If you think our project is OK, click on a star.
DoraemonKit project address: https://github.com/didi/DoraemonKit
A screenshot of the DoraemonKit project:
5. References
Talking about iOS Crash Collection Framework
iOS Exception Capture
iOS Internal Work: A Brief Talk on Crash
iOS Mach Anomalies and signal Signals
Talking about Mach Exceptions
"Crash Monitoring of iOS Monitoring Programming"
Mach Abnormality