01. Common Crash Scenarios

It will take about 5 minutes to finish reading this article.

1. “Unrecognized selector sent to instance”

Problem analysis:
“unrecognized selector sent to instance” is an Objective-C exception that indicates an attempt to call a method or message that does not exist. This exception usually occurs when:

  • When using the performSelector: method, the specified method does not exist;
  • When using KVO (key Value observation), the observed property does not exist;
  • When using NSNotificationCenter, the monitored event does not exist;
  • The specified method is not present when using NSInvocation (message invocation);
  • When extending a class with a Category, methods are implemented directly in the implementation file instead of being declared in the header file;
    And so on.

Sample code:

Objective-C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@interface MyClass : NSObject
- (void)doSomething;
@end

@implementation MyClass
@end

@interface MyOtherClass : NSObject
@end

@implementation MyOtherClass
- (void)doSomethingElse {
NSLog(@"Doing something else...");
}
@end

int main(int argc, const char * argv[]) {
@autoreleasepool {
MyClass *object = [[MyClass alloc] init];
// unrecognized selector sent to instance
[object performSelector:@selector(doSomethingElse)];
}
return 0;
}

Recommended solution::

  • Use the “respondsToSelector” judgment before calling, or use ProtocolKit in Release mode to add a default implementation to the protocol to prevent crashes, and turn off the default implementation in Debug mode.
  • Take advantage of the dynamic nature of OC and use several methods of message forwarding to underwrite the process.

2. “EXC_BAD_ACCESS”

Problem analysis:
There are many reasons for the problem:

  • Occurrence of hanging pointers (null pointers, wild pointers)
  • Objects are not initialized
  • The accessed object is freed
  • Access to out-of-bounds collection elements
    etc.

Sample code:
Omitted.
Recommended solution::

  1. turn on zombie mode in Debug phase and turn off zombie mode in Release.
  2. use Xcode’s Address Sanitizer to check address access out of bounds.
  3. remember to initialize when creating objects.
  4. use the correct modifier for the object’s attributes (should use strong/weak, misused assign).
  5. call block and other objects before the time to make a judgment.

Problem analysis:
There are also many scenarios that lead to Crash.

  1. Array out-of-bounds, where the access subscript is greater than the number of arrays.
  2. Adding empty data to the array.
  3. Multi-threaded environment, a thread is reading, a thread is removing.
  4. Multi-threaded operation of variable arrays (expansion of arrays, access to zombie objects).

Sample code:

1
2
3
// Accessing an array out of bounds will cause a crash
let array = [1, 2, 3]
let item = array[3]
1
2
3
// Forcibly unwrapping an optional type and it's value is nil will cause a crash
var dict: [String: String?] = ["key1": "value1", "key2": "value2"]
let value = dict["key1"]!
1
2
3
// Forcibly unwrapping an optional type and it's value is nil will cause a crash
var set: Set<String?> = ["value1", "value2", nil]
let value = set.first!

Recommended solution::

  1. When using dictionaries and sets, check whether the keys and values are nil (use guard, if let, and other syntax to avoid crashes caused by forced unwrapping);
  2. Use extensions to override original methods and perform checks internally;
  3. In Objective-C, use Runtime mechanism to replace original methods with custom secure methods;
  4. When performing multi-threaded operations on arrays, ensure the atomicity of read and write operations, such as locking or other protective measures.
1
2
3
4
5
6
7
8
9
//Use the safe subscript extension provided by Swift to avoid accessing an array out of bounds.
extension Collection {
subscript(safe index: Index) -> Element? {
return indices.contains(index) ? self[index] : nil
}
}

let array = [1, 2, 3]
let item = array[safe: 3] //It will not cause a crash, and the resulting value will be nil.

4. “Out of Memory”

Problem analysis:
In iOS applications, if the application allocates too much memory and causes the system to run out of memory, an OOM error will occur. Each application on an iOS device has its own memory limit. When an application needs to allocate more memory, if there is not enough available memory, the system will automatically trigger an OOM error, terminate the application, and release it from memory.

The causes of OOM errors may include:

  1. The application tries to allocate a large amount of memory when there is not enough memory available.
  2. Memory leaks in the application that cause high memory usage.
  3. The application’s memory usage is not compatible with system resources, resulting in high memory usage, etc.

Sample code:

1
2
3
4
var array = [Int]()
while true {
array.append(1)
}

Recommended solution::

To avoid OOM errors, the following measures can be taken:

  1. Use appropriate data structures to avoid unnecessary memory usage.
  2. Debug memory leaks.
  3. Release unnecessary memory in a timely manner, such as calling autoreleasepool to release it after using a large memory object.
  4. Reduce object creation. Object pool and other technologies can be used to reuse existing objects rather than frequently creating and destroying objects.
  5. For large memory objects, lazy-loading and other technologies can be used to load them only when needed to reduce memory usage.

5. “Type Cast Exception”or “Type Mismatch”

Problem analysis:
Type Cast Exception or Type Mismatch refers to the exception that occurs during type conversion, leading to application crash. In iOS development, common scenarios include:

  1. Type mismatch occurs when converting an instance of one type to an instance of another type.
  2. Type mismatch occurs when converting from AnyObject to a specific type.
  3. Crash occurs when forcing the unwrapping of an optional type to a non-optional type with a nil value.

Sample code:

1
2
3
let array: [Any] = ["A", "B", "C"]
let str = array[0] as! Int
//Error: Could not cast value of type 'Swift.String' (0x7ff8553bc178) to 'Swift.Int' (0x7ff8553be0e0).

Recommended solution::

  1. Before performing type conversion, check if the object is an instance of the target type using the is keyword.
  2. Use optional binding to avoid exceptions when typecasting.

For example, in the above example, the following code can be used for improvement:

1
2
3
4
let array: [Any] = ["A", "B", "C"]
if let obj = array[0] as? Int {

}

6. Caused by Deadlock

Problem analysis:
Deadlock refers to a situation where two or more threads are waiting for each other to complete their operations, causing the program to become unresponsive. In iOS, the most common cause of a deadlock is when a synchronous operation is executed on the main thread that waits for another thread to complete, while the other thread is also waiting for the main thread to complete, resulting in a deadlock.

Sample code:
In the following code, when the main thread calls the queue.sync method, it waits for the completion of Block 1. However, Block 1 calls queue.sync again, causing the thread to enter a waiting state. Since Block 2 depends on the thread to release the lock before it can execute, the entire program is in a deadlock state and cannot continue executing.

1
2
3
4
5
6
7
8
let queue = DispatchQueue(label: "com.example.queue")
queue.sync {
print("Block 1")
queue.sync {
print("Block 2")
}
}
print("Done")

Recommended solution::

  1. Avoid executing long synchronous operations on the main thread, and instead, perform them on a background thread.
  2. Avoid using synchronous operations that wait for each other on the same queue and use asynchronous operations instead.
  3. Avoid using synchronous operations on multiple queues that cause deadlocks. Use asynchronous operations instead or use techniques such as dispatch_group to solve the issue.

In the above example, you can replace the synchronous operations with asynchronous operations, as shown below:

1
2
3
4
5
6
7
8
9
let queue = DispatchQueue(label: "com.example.queue")
queue.async {
print("Block 1")
queue.async {
print("Block 2")
}
}
print("Done")

7. Caused by Stack Overflow

Problem analysis:
Stack Overflow usually occurs in recursive calls. If the recursion does not have a termination condition or the termination condition is incorrect, the recursion depth will continue to increase until the stack space is exhausted, causing a stack overflow.

In addition, if one method calls another method, a deep call stack can also cause a stack overflow.

Sample code:

1
2
3
4
func recursiveFunction() {
recursiveFunction()
}
recursiveFunction()

Recommended solution::

  1. Optimize algorithms: Optimize recursive algorithms to reduce the depth of the call stack. For example, use iterative instead of recursive algorithms.
  2. Increase stack space: Increase available stack space by changing the thread stack size or using the dispatch_set_concurrency function of GCD.
  3. Reduce stack space usage: Reduce stack space usage by reducing the local variables allocated during function calls, reducing nested calls, or using tail recursion and other techniques.
  4. Use tail recursion: Tail recursion refers to the last operation in a recursive function being a recursive call to itself. In Swift, you can use the @_optimize(speed) and @_optimize(safety) attributes to mark functions so that the compiler can optimize tail recursion.
  5. Avoid infinite recursion: Ensure that the recursive algorithm has the correct termination condition; otherwise, the recursion depth will increase infinitely, eventually leading to a stack overflow.
  6. Use data structures with smaller stack space: For large-scale recursive algorithms, data structures with smaller stack space, such as linked lists or queues, can be used.

8. Caused by KVO

Problem analysis:
KVO (Key-Value Observing) is an observer pattern in the Cocoa framework that allows objects to be notified when the value of another object’s property changes. When using KVO, if observers are not removed in a timely manner or the observer object has already been released, it can cause a crash.
Sample code:
Omitted.(Objective-C)
Recommended solution::

  1. Remove the observer in a timely manner and ensure the observer object exists.
  2. Best practices for using KVO:
    • Use KVO only when necessary to avoid overuse;
    • Use Swift’s Property Observers instead of KVO;
    • Use closures or Notification Center instead of KVO;
    • Use the correct method when removing observers to avoid omissions;
    • Avoid using strings as keyPaths. Instead, use static variables or constants, or use the #keyPath() method introduced in Swift 4 to obtain the keyPath.

9. Caused by Multi-threaded

Problem analysis:
Crashes caused by multi-threading are a common issue in iOS, which may occur in different scenarios. Here are some possible scenarios, sample code, and recommended solutions that may cause multi-threading crashes:

  1. Multiple threads accessing the same shared data structure or variable without synchronization or locking.
  2. Using unsafe data structures or APIs in a multi-threaded environment, such as using non-thread-safe mutable collection class Array.
  3. Calling a long-running operation (such as network requests or I/O operations) in a thread, causing the UI thread to be blocked.
  4. Updating the UI in a sub-thread.

In general, the crash caused by multi-threading will receive a SIGSEGV signal, indicating that an attempt was made to access memory that was not allocated to oneself or to write data to a memory address without write permission.

Sample code:
The following is a simple sample code that demonstrates the scenarios and issues caused by multi-threading crashes:

1
2
3
4
5
6
7
8
9
10
11
12
var array = [Int]()
DispatchQueue.global().async {
for i in 0..<100 {
array.append(i)
}
}

DispatchQueue.global().async {
for i in 100..<200 {
array.append(i)
}
}

After running it, the crash occurs:

Recommended solution::

Here are three possible recommended solutions that can help you avoid multi-threading crashes:

  1. Use thread-safe data structures or APIs, such as using NSLock or dispatch_semaphore_t to synchronize.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    let lock = NSLock()
    DispatchQueue.global().async {
    lock.lock()
    for i in 0..<100 {
    array.append(i)
    }
    lock.unlock()
    }
    DispatchQueue.global().async {
    lock.lock()
    for i in 100..<200 {
    array.append(i)
    }
    lock.unlock()
    }
  2. Use GCD for inter-thread communication and avoid long-running operations on the main thread.

    1
    2
    3
    4
    5
    6
    DispatchQueue.global().async {
    let data = getData() // long-running operation
    DispatchQueue.main.async {
    self.updateUI(with: data) // update UI on the main thread
    }
    }

    The above code performs a long-running operation on the background thread and uses GCD to send the result to the main thread to update the UI.

  3. Use a serial queue to ensure that operations on the same object are executed in order.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    let serialQueue = DispatchQueue(label: "com.example.serialQueue")

    serialQueue.async {
    self.updateUI(with: data1) // operation 1
    }

    serialQueue.async {
    self.updateUI(with: data2) // operation 2
    }

10. Caused by Long connection of Socket

Problem analysis:
When the server closes a connection, if the client continues to send data, according to the TCP protocol, it will receive an RST response. When the client sends data to this server again, the system will send a SIGPIPE signal to the process, telling the process that the connection has been disconnected and not to write anymore. According to the default signal handling rules, the default action of the SIGPIPE signal is to terminate or exit. Therefore, the client will exit.

Sample code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class SocketManager {

private var inputStream: InputStream?
private var outputStream: OutputStream?
private let host: String = "localhost"
private let port: Int = 12345

func connect() {
Stream.getStreamsToHost(withName: host, port: port, inputStream: &inputStream, outputStream: &outputStream)

inputStream?.open()
outputStream?.open()
}

func disconnect() {
inputStream?.close()
outputStream?.close()
}
}

Recommended solution::
Here are three possible recommended solutions to avoid the crash caused by long connections of sockets:

First, when the application enters the background, immediately close the socket connection:

1
2
3
func applicationWillResignActive(_ application: UIApplication) {
socketManager.disconnect()
}

Second, when using the background running mode in the application, handle the socket connection correctly.

1
2
3
4
5
6
7
8
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
if application.backgroundTimeRemaining < 60 {
socketManager.disconnect()
}

// Execute background task here
}

The above code shows that when using Background Fetch, check the remaining background running time of the application, and close the socket connection if it is less than 1 minute.

Third, use signal(SIGPIPE,SIG_IGN) to hand over SIGPIPE to the system, which sets SIGPIPE to SIG_IGN, making the client not execute the default operation, that is, not to exit.

11. Caused by Watch Dog Timeout

Problem analysis:
Watchdog Timeout is a monitoring mechanism built into the iOS system that checks whether the code executed by the application in the main thread exceeds the specified time. If it times out, the system automatically terminates the execution of the application to avoid a poor user experience caused by application freezes.

Watchdog Timeout typically occurs during time-consuming operations such as network requests, IO operations, and large data processing. If these time-consuming operations are not handled correctly, it is easy to trigger Watchdog Timeout, leading to application crashes.

Sample code:
For example, executing the following code in the main thread may trigger a Watchdog timeout:

1
2
3
4
5
func doHeavyWork() {
for i in 1...1000000000 {
// execute a large number of loop operations
}
}

Recommended solution::
First, put time-consuming operations in a separate thread to avoid occupying the main thread for too long, thereby avoiding the occurrence of Watchdog Timeout. You can use GCD or NSOperationQueue to implement this.

Second, use an asynchronous method to execute time-consuming operations and use an appropriate queue to manage the execution. For example, use DispatchQueue.global() to create a global queue, and then use the async method to asynchronously execute tasks. In addition, you can also use NSOperationQueue to manage tasks.

Third, use a timer or RunLoop to periodically execute time-consuming operations, and check whether the time exceeds the Watchdog Timeout setting before execution. If it times out, stop executing and put the task on hold until the next execution cycle.

Fourth, if time-consuming operations must be performed in the main thread, you can use NSRunLoop to control the execution time and periodically call the run method to ensure that the Watchdog Timeout setting is not exceeded.

Reference

[1] https://juejin.cn/post/6844903775203753997
[2] https://juejin.cn/post/6978014329333350430