Listen to this story
Die-Hard fan of Nikola Tesla.
With the exponential growth in smartphone usage and the wide range of apps developed for different purposes, effective debugging strategies are crucial for developers to identify and resolve mobile app bugs efficiently. In this article, we will explore expert debugging strategies for common to complex issues that can assist developers in effectively tackling mobile app errors and ensuring their applications' seamless performance.
I am providing examples of solutions for mobile app vulnerabilities without referencing specific apps. This is because I need to ensure that I have explicit written consent from the app owner or relevant stakeholders before using any specific app for debugging.
While intentionally vulnerable apps are available for educational purposes, they may not cover all aspects of mobile app vulnerabilities comprehensively.
If you are developing apps for popular OS like iOS and Android, it is essential to understand the common app and the complex errors, and their solutions.
Please note that codes provided here are just for the illustration purpose, verify before implementing them into your program with a secure coding practice. However, all codes are thoroughly tested to make sure they are working. If you encounter any issues, write in the comment section.
Here are a few of them.
Null Pointer Exception (NPE)
This is the most common runtime error in Java/Kotlin (Android) that occurs when you mistakenly use an object that has not been properly initialized or when you try to access a property or method of an object that is null. When developing an Android app using either Java or Kotlin, there are so many common scenarios that can result in Null Pointer Exceptions (NPEs).
For example:
Kotlin Example:
class Person(val name: String)
class Address(val person: Person?)
class Mobile(val address: Address?)
fun main() {
val mobile = Mobile(null)
val personName = mobile.address?.person?.name // Throws NPE at multiple levels
}
The solution in Kotlin:
fun main() {
val mobile = Mobile(null)
val personName = mobile.address?.person?.name // Safely access with null checks at multiple levels
// Handle the null case appropriately
}
In both Java and Kotlin, Null Pointer Exceptions (NPEs) can arise in complex scenarios when there are multiple levels of object references involved, and any of them can be null.
To effectively handle such situations, you can implement multiple null checks at each level to ensure that all objects are not null before accessing their properties or calling methods on them.
This approach helps prevent NPEs and ensures cool error handling in your code.
Memory Leaks Error in Swift (iOS):
Memory leaks in Swift (iOS) occur when objects are not properly deallocated from memory which leads to potential memory consumption issues. This can happen when there are strong reference cycles where objects hold strong references to each other by preventing them from being deallocated even when they are no longer needed.
One common scenario is when using closures or blocks that capture self
strongly, such as with Timer
or NotificationCenter
callbacks. To fix memory leaks, weak references to self
can be used in closures or blocks, and objects should be properly invalidated or removed when they are no longer needed.
Example:
class ViewController: UIViewController {
var timer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
startTimer()
}
func startTimer() {
// Create a strong reference cycle
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { [weak self] (_) in
// Update UI
self?.updateUI()
})
}
func updateUI() {
// Update UI
}
deinit {
// Timer is not invalidated, leading to a memory leak
// Uncomment the following line to fix the memory leak
// timer?.invalidate()
}
}
Explanation:
In this example, a Timer
is created using the method, Timer.scheduledTimer(withTimeInterval:repeats:block:)
and the Timer
has a strong reference to the ViewController
object because of the captured self
in the closure. If the ViewController
is dismissed or deallocated before the timer is invalidated, it will cause a memory leak because the ViewController
will not be deallocated from memory which eventually leads to potential memory consumption issues.
The solution in Swift:
To fix the memory leak, you can use a weak reference to self in the closure to break the strong reference cycle. Here's an updated version of the code:
class ViewController: UIViewController {
var timer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
startTimer()
}
func startTimer() {
// Use a weak reference to self to avoid strong reference cycle
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { [weak self] (_) in
// Update UI
self?.updateUI()
})
}
func updateUI() {
// Update UI
}
deinit {
// Invalidate the timer to avoid memory leak
timer?.invalidate()
}
}
By using [weak self]
in the closure, we create a weak reference to self
, preventing a strong reference cycle and potential memory leaks. Additionally, in the deinit
method of the ViewController
, we call timer?. Invalidate()
to ensure the timer is invalidated and released properly when the view controller is deallocated, preventing memory leaks.
Input Validation Error in JavaScript (React Native):
Input validation errors in JavaScript (React Native) happen when the user's input is not checked properly before it is applied in the app which can cause security problems or unexpected errors or issues in the app. It is important to validate or check the user's input thoroughly to protect user data and prevent problems like data corruption, attacks, or crashes, and it should be done for all input from users (such as form fields, API requests, and user interactions) to make sure the app is secure and works well.
Example of Error in Complex Email Input Validation:
Suppose your system is validating an email address entered by a user in a sign-up form, the user enters the email address "example@example.com". Your email validation code matches his input against the regular expression pattern /^[A-Z0-9._%+-]+@[A-Z0-9.-]+.[A-Z]{2,}$/i, which correctly identifies the input as a valid email address by your system.
But suppose there is a bug in your code that does not properly handle Unicode characters in the email address then the user enters an email address with a non-Latin character (such as "משלוח@דואר.ישראל"). This input also matches the regular expression pattern but your code may not recognize it as a valid email address due to the Unicode characters.
const isEmailValid = (email) => {
const regex = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i; // Email format regex
const blacklistedDomains = ['example.com', 'test.com']; // Blacklisted domains
if (!email.match(regex)) return false;
const domain = email.split('@')[1];
if (blacklistedDomains.includes(domain)) return false;
return true;
};
// Usage
const email = 'test@example.com'; // Valid email
const isValid = isEmailValid(email);
console.log(isValid); // Output: true
Solution:
To fix this sort of error, you could modify the regular expression pattern to allow for Unicode characters in the local and domain parts of the email address. You could also use a library or a service that assists with the processes of email validation and handles complex inputs like this (mentioned above) more precisely.
Network Connection Errors:
Explanation:
A network connection error occurs when an app fails to establish or maintain a connection with a remote server or API which eventually results in network-related failures or errors.
Example (Java - Android):
URL url = new URL("https://api.example.com/data");
URLConnection connection = url.openConnection();
InputStream inputStream = connection.getInputStream();
// Network connection error - handle IOException
Solution: To fix network connection errors, ensure that you handle network-related failures properly in your app during the testing phases. Implement proper error-handling mechanisms (such as retrying failed requests, displaying error messages, and handling timeouts). You can use network-monitoring libraries or built-in APIs to detect network connectivity changes and provide appropriate feedback to users.
These are a few examples of common Mobile App errors but sometimes, you may notice very complex errors. Here are some examples:
Multithreading issues:
The term Multithreading refers to the concurrent execution of multiple tasks without altering the performances of specific apps. Sometimes, issues like race conditions, deadlocks, etc. associated with this can be problematic. Here is an example:
Issue:
Problems with concurrent access to shared resources (such as race conditions or deadlocks) lead to unexpected app behavior or crashes.
Solution:
Carefully check the app's code for any places where different parts of the app might be trying to use the same things simultaneously then make sure they are working together properly. You can use proper tools like locks, semaphores, or special operations to make sure everything runs smoothly and nothing gets stuck or mixed up. Use safe ways of handling shared things, like special data structures or libraries that are made to work with multiple parts of the app at once. For example, in iOS, you can use something called Grand Central Dispatch (GCD) to help with this.
Here's an example of how you can use GCD to do things at the same time without any problems:
let concurrentQueue = DispatchQueue(label: "com.example.concurrentQueue", attributes: .concurrent)
func performConcurrentOperations() {
concurrentQueue.async {
// Perform concurrent operation 1
}
concurrentQueue.async {
// Perform concurrent operation 2
}
concurrentQueue.async(flags: .barrier) {
// Perform concurrent operation 3 with a barrier to synchronize
// access to shared resources
}
concurrentQueue.async {
// Perform concurrent operation 4
}
}
Vulnerabilities in the app's security
Mobile app security vulnerabilities can allow attackers to gain unauthorized access to the app's data. These vulnerabilities may go unnoticed for a significant duration and can arise from complex issues such as improper handling of sensitive data, insecure communication, or lack of proper authentication and authorization during app development.
Issue:
Improper handling of sensitive data in Swift(iOS).
Solution:
Implement proper encryption and secure storage of sensitive data (such as user credentials, API keys, and personal information). Here's an example in Swift for iOS to use the Keychain for secure storage of sensitive data:
// Implement secure storage of sensitive data using Keychain in iOS
import Security
let password = "my_password"
let service = "com.example.myapp"
let account = "user@example.com"
let passwordData = password.data(using: .utf8)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecValueData as String: passwordData!
]
let status = SecItemAdd(query as CFDictionary, nil)
if status == errSecSuccess {
print("Password added to Keychain successfully")
} else {
print("Failed to add password to Keychain")
}
Note: To test the code, prefer the following steps:
Copy and paste the above code into a new Xcode playground or a new Swift file in your Xcode project.
Add the necessary import statement for the Security framework at the top of the file:
import Security
Try modifying the values of the password, account variables, and service to match your own app's needs.
Then try Running the code and check the console output to see if the password was successfully added to the Keychain. If yes, done!
If a password is successfully added, you see the message "Password added to Keychain successfully" in the console otherwise, you will see the message "Failed to add password to Keychain". It is better to run code on physical iOS devices instead of the virtual simulator.
Issue:
Insecure communication
Solution:
Use secure communication protocols (such as HTTPS) for transmitting sensitive data over the network. Here's an example in Java for Android to use HTTPS for secure communication:
// Implement HTTPS for secure communication in Android
URL url = new URL("https://example.com/api");
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(5000);
int responseCode = connection.getResponseCode();
if (responseCode == HttpsURLConnection.HTTP_OK) {
// Read response from server
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String line;
StringBuilder response = new StringBuilder();
while ((line = reader.readLine()) != null) {
response.append(line);
}
reader.close();
// Process response
String jsonResponse = response.toString();
// ...
} else {
// Handle error
}
Note: Follow the following steps carefully to test this code:
P.S. During the testing phase, you should use your own test servers to protect sensitive app data.
Fixing the bugs in mobile apps requires expert debugging strategies which involve using suitable tools, different techniques, and experience to identify and fix issues in the code so that the app functions correctly. It's essential for developers to regularly update and maintain their apps, following best practices such as input validation, exception handling, memory management, performance optimization, network connectivity, and security measures. Staying updated with the latest developments and proactively handling errors are essential for ensuring a smooth user experience. With a systematic approach and attention to detail, app developers can overcome challenges and create high-quality mobile apps for various purposes. Debugging mobile apps can be an enjoyable experience. Happy debugging!