Global Exception Handler — ένας κεντρικός μηχανισμός εντοπισμού μη διαχειρισμένων εξαιρέσεων που αποτρέπει την απότομη τερματισμό της εφαρμογής για κινητά. Σύμφωνα με τα δεδομένα Apple Developer, 2024, η σωστή διαχείριση εξαιρέσεων μειώνει τον αριθμό των crash κατά 40–60% και βελτιώνει την εμπειρία χρήστη. Χωρίς έναν τέτοιο χειριστή, οποιαδήποτε μη διαχειρισμένη εξαίρεση σε ένα νήμα παρασκηνίου οδηγεί σε άμεσο τερματισμό της εφαρμογής.
Κύρια σημεία
Global Exception Handler — είναι ένας κεντρικός μηχανισμός εντοπισμού εξαιρέσεων που δεν διαχειρίστηκαν σε επίπεδο μεμονωμένων συναρτήσεων ή μονάδων της εφαρμογής. Στο πλαίσιο της ανάπτυξης εφαρμογών για κινητά, ένας τέτοιος χειριστής λειτουργεί ως η τελευταία γραμμή άμυνας πριν τον αναγκαστικό τερματισμό της διεργασίας.
Τα iOS και Android παρέχουν ενσωματωμένα API για την εγκατάσταση ενός καθολικού χειριστή. Η Apple χρησιμοποιεί το NSSetUncaughtExceptionHandler για το περιβάλλον Objective-C και η Google προσφέρει το Thread.setDefaultUncaughtExceptionHandler σε Java/Kotlin. Και οι δύο μηχανισμοί εντοπίζουν εξαιρέσεις που δεν πιάστηκαν από κατασκευές try-catch σε όλα τα νήματα της εφαρμογής.
Σύμφωνα με δεδομένα Crashlytics (Google, 2024), περίπου 25% των crash συμβαίνουν λόγω μη διαχειρισμένων εξαιρέσεων σε νήματα παρασκηνίου — ένας τομέας όπου το Global Exception Handler είναι ιδιαίτερα κρίσιμο. Οι προγραμματιστές συχνά επικεντρώνονται στο νήμα UI, ξεχνώντας τις ασύγχρονες λειτουργίες.
Η χρήση ενός καθολικού χειριστή δεν αντικαθιστά την τοπική διαχείριση σφαλμάτων, αλλά την συμπληρώνει. Η κύρια εργασία είναι η αποθήκευση μέγιστων πληροφοριών σχετικά με την κατάσταση της εφαρμογής τη στιγμή της εξαίρεσης και ο σωστός τερματισμός.
Μηχανισμός λειτουργίας — το Global Exception Handler βασίζεται στον εντοπισμό σημάτων λειτουργικού συστήματος ή εξαιρέσεων χρόνου εκτέλεσης. Όταν ο κώδικας εκτοξεύει μια εξαίρεση που δεν πιάστηκε από κανένα μπλοκ try-catch, ο έλεγχος μεταφέρεται στον προκαθορισμένο χειριστή.
Στην πλατφόρμα iOS, ο χειριστής καταχωρείται μέσω του NSSetUncaughtExceptionHandler και λαμβάνει ένα αντικείμενο NSException με πλήρες stack trace. Στο Android, χρησιμοποιείται το Thread.setDefaultUncaughtExceptionHandler, το οποίο λαμβάνει Thread και Throwable — παρέχοντας πρόσβαση στον τύπο εξαίρεσης, μήνυμα και στοίβα κλήσεων.
Μετά τη λήψη δεδομένων crash, ο χειριστής εκτελεί τρεις υποχρεωτικές ενέργειες: εγγραφή καταγραφής σε τοπική αποθήκευση, αποστολή αναφοράς στο Crashlytics ή Sentry και σωστό τερματισμό της εφαρμογής. Σύμφωνα με το Apple WWDC 2023, ο χρόνος λειτουργίας του χειριστή περιορίζεται σε 5 δευτερόλεπτα — μετά από αυτό, το σύστημα τερματίζει αναγκαστικά τη διεργασία.
Για εφαρμογές Swift από το iOS 13, εμφανίστηκε το Signals API, το οποίο επεξεργάζεται όχι μόνο εξαιρέσεις αλλά και σήματα λειτουργικού συστήματος — SIGABRT, SIGSEGV και SIGBUS, επεκτείνοντας την κάλυψη του χειριστή σε σφάλματα μνήμης χαμηλού επιπέδου.
Υλοποίηση — ο καθολικός χειριστής σε iOS απαιτεί ρύθμιση μιας συνάρτησης C μέσω του NSSetUncaughtExceptionHandler. Ο χειριστής καλείται σύγχρονα τη στιγμή μιας μη διαχειρισμένης εξαίρεσης και λαμβάνει το πλήρες περιβάλλον σφάλματος.
void handleUncaughtException(NSException exception) {
NSDictionary userInfo = [exception userInfo];
NSArray stackTrace = [exception callStackSymbols];
NSString reason = [exception reason];
// Αποθήκευση crash log σε τοπικό αρχείο
NSString logPath = [NSSearchPathForDirectoriesInDomains(
NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
[exceptionLog writeToFile:logPath atomically:YES];
}
int main(int argc, char argv[]) {
NSSetUncaughtExceptionHandler(&handleUncaughtException);
return UIApplicationMain(argc, argv, nil, nil);
}
Σημαντικό χαρακτηριστικό της υλοποίησης iOS: ο χειριστής εντοπίζει μόνο εξαιρέσεις Objective-C. Τα σφάλματα Swift που χρησιμοποιούν τον μηχανισμό throw-catch δεν φτάνουν σε αυτόν τον χειριστή — απαιτούν ξεχωριστή επεξεργασία μέσω του Swift Error Handling. Από το iOS 14, η Apple συνιστά τον συνδυασμό του NSSetUncaughtExceptionHandler με το Signals API για μέγιστη κάλυψη.
Σύμφωνα με το Apple Technical Note TN2151, μετά την κλήση του χειριστή, η εφαρμογή πρέπει να τερματιστεί εντός 5 δευτερολέπτων. Οποιαδήποτε προσπάθεια συνέχισης της εκτέλεσης μετά την επιστροφή από τον χειριστή οδηγεί σε απροσδιόριστη συμπεριφορά και επαναλαμβανόμενο crash.
Android προσφέρει έναν πιο ευέλικτο μηχανισμό καθολικής διαχείρισης εξαιρέσεων μέσω του Thread.setDefaultUncaughtExceptionHandler. Ο χειριστής λαμβάνει μια αναφορά στο νήμα όπου συνέβη η εξαίρεση και το ίδιο το αντικείμενο Throwable.
class GlobalExceptionHandler : Thread.UncaughtExceptionHandler {
override fun uncaughtException(thread: Thread, throwable: Throwable) {
// Αποθήκευση crash log σε αρχείο
val stackTrace = throwable.stackTraceToString()
val crashLog = "CRASH: ${thread.name}\n${stackTrace}"
val file = File(context.cacheDir, "crash_log.txt")
file.writeText(crashLog)
// Αποστολή στο Crashlytics
FirebaseCrashlytics.getInstance()
.recordException(throwable)
// Τερματισμός διεργασίας
android.os.Process.killProcess(
android.os.Process.myPid()
)
}
}
// Εγκατάσταση στο Application.onCreate
class App : Application() {
override fun onCreate() {
super.onCreate()
Thread.setDefaultUncaughtExceptionHandler(
GlobalExceptionHandler()
)
}
}
Βασική διαφορά της υλοποίησης Android: κάθε νήμα έχει τον δικό του χειριστή και το setDefaultUncaughtExceptionHandler ορίζει έναν χειριστή για όλα τα νήματα που δεν έχουν μεμονωμένο χειριστή. Αυτό εξασφαλίζει καθολική κάλυψη — από το νήμα UI έως τα AsyncTask παρασκηνίου και τα coroutine.
Στο Android 12+ εμφανίστηκε ένας περιορισμός: μετά την κλήση του uncaughtException, η εφαρμογή πρέπει να τερματιστεί εντός 100 χιλιοστών του δευτερολέπτου. Εάν ο χειριστής εκτελεί μακροχρόνιες λειτουργίες, το σύστημα μπορεί να σκοτώσει τη διεργασία πριν ολοκληρωθεί η εγγραφή καταγραφής. Συνιστάται η χρήση μιας υπηρεσίας παρασκηνίου για την αποστολή αναφοράς crash.
Πρώτος κανόνας — μην επιχειρείτε να επαναφέρετε την εφαρμογή μετά από μια μη διαχειρισμένη εξαίρεση. Η κατάσταση της εφαρμογής μετά από crash είναι απροσδιόριστη και η συνέχιση της λειτουργίας μπορεί να οδηγήσει σε καταστροφή δεδομένων χρήστη.
Χρονικός περιορισμός — ο κύριος τεχνικός περιορισμός του Global Exception Handler. Στο iOS είναι 5 δευτερόλεπτα, στο Android — 100 χιλιοστά του δευτερολέπτου. Εντός του χειριστή επιτρέπεται μόνο η αποθήκευση ενός ελάχιστου συνόλου δεδομένων: τύπος εξαίρεσης, στοίβα κλήσεων και κατάσταση μερικών βασικών μεταβλητών.
Η αποστολή αιτημάτων δικτύου, η εγγραφή σε βάση δεδομένων και η πολύπλοκη σειριοποίηση πρέπει να μεταφερθούν σε έναν καθυστερημένο μηχανισμό — για παράδειγμα, αποθήκευση καταγραφής σε αρχείο και αποστολή κατά την επόμενη εκκίνηση της εφαρμογής.
Υπηρεσίες αναφοράς crash — Firebase Crashlytics, Sentry, Bugsnag — εγκαθιστούν μόνες τους τον καθολικό χειριστή. Εάν ο προγραμματιστής εγκαθιστά τον δικό του χειριστή πάνω από αυτά, πρέπει να μεταβιβάσει τον έλεγχο στο σύστημα αναφοράς crash μετά τις ενέργειές του. Στο Android, χρησιμοποιείται σύνθεση χειριστών: εκτέλεση της δικής σας λογικής, στη συνέχεια κλήση του προηγούμενου χειριστή.
Για το Firebase Crashlytics, συνιστάται να μην εγκατασταθεί καθόλου προσαρμοσμένο Thread.setDefaultUncaughtExceptionHandler — το Crashlytics SDK το κάνει αυτόματα κατά την αρχικοποίηση.
Πλαίσιο χρήστη — εκτός από την τυπική στοίβα κλήσεων, είναι χρήσιμο να καταγράφεται η έκδοση της εφαρμογής, η έκδοση του λειτουργικού συστήματος, η ποσότητα ελεύθερης μνήμης και ο χρόνος λειτουργίας μέχρι το crash. Αυτά τα δεδομένα είναι κρίσιμα για την αναπαραγωγή και επίλυση του προβλήματος.
Στο iOS, το NSSetUncaughtExceptionHandler μπορεί να χρησιμοποιηθεί όχι μόνο για εγγραφή, αλλά και για προσωρινή αποθήκευση δεδομένων στο NSUserDefaults με τη σημαία synchronize — αυτό εγγυάται τη διατήρηση των δεδομένων ακόμη και με άμεσο τερματισμό της διεργασίας.
Υποχρεωτική δοκιμή — το Global Exception Handler πρέπει να δοκιμάζεται σε κάθε στάδιο του CI/CD. Στο iOS, μια δοκιμαστική εξαίρεση μπορεί να ξεκινήσει μέσω @throw NSException, στο Android — μέσω throw RuntimeException(). Ελέγχεται ότι ο χειριστής καλείται, η καταγραφή αποθηκεύεται και η εφαρμογή τερματίζεται σωστά.
Σύμφωνα με το Google I/O 2023, πάνω από 30% των crash στην παραγωγή συμβαίνουν σε συσκευές που ο προγραμματιστής δεν δοκίμασε — διαφορετικές εκδόσεις Android, προσαρμοσμένο υλικολογισμικό, περιορισμένη μνήμη.
Πρώτο και πιο συνηθισμένο λάθος — προσπάθεια συνέχισης της εκτέλεσης της εφαρμογής μετά την επεξεργασία της εξαίρεσης. Μετά την κλήση του uncaughtException, η εφαρμογή βρίσκεται σε ασταθή κατάσταση και οποιεσδήποτε περαιτέρω λειτουργίες μπορούν να προκαλέσουν καταρράκτη σφαλμάτων και καταστροφή δεδομένων.
Δεύτερο λάθος — εκτέλεση μακροχρόνιων λειτουργιών εντός του χειριστή. Αιτήματα δικτύου, εγγραφή μεγάλων αρχείων ή πολύπλοκοι υπολογισμοί δεν προλαβαίνουν να ολοκληρωθούν πριν από τον αναγκαστικό τερματισμό της διεργασίας. Σύμφωνα με το Apple Technical Q&A QA1468, η προσπάθεια αποστολής HTTP αιτήματος εντός του χειριστή είναι η κύρια αιτία χαμένων αναφορών crash.
Τρίτο λάθος — αγνόηση των νημάτων παρασκηνίου. Ένα Global Exception Handler που έχει οριστεί μόνο για το κύριο νήμα δεν προστατεύει από crash σε coroutine, DispatchQueue, AsyncTask ή RxJava. Στο Android, κάθε νήμα πρέπει να έχει τον δικό του χειριστή — και το setDefaultUncaughtExceptionHandler λύνει αυτό το πρόβλημα μόνο για νήματα χωρίς μεμονωμένο χειριστή.
Τέταρτο λάθος — έλλειψη εφεδρικής λύσης για σήματα λειτουργικού συστήματος. Το NSSetUncaughtExceptionHandler στο iOS δεν εντοπίζει SIGABRT, SIGSEGV και SIGBUS. Για αυτά τα σήματα, απαιτείται ξεχωριστή εγκατάσταση χειριστών μέσω του sigaction API. Οι προγραμματιστές το ανακαλύπτουν μόνο όταν η εφαρμογή καταρρέει χωρίς καμία αναφορά crash.
Πέμπτο λάθος — καταγραφή εμπιστευτικών δεδομένων. Στο αρχείο καταγραφής crash καταλήγουν email, διακριτικά εξουσιοδότησης ή προσωπικά δεδομένα χρηστών. Αυτό παραβιάζει τον GDPR και τις Apple App Store Review Guidelines. Να φιλτράρετε πάντα τα μεταδιδόμενα δεδομένα μέσω regex ή λίστας επιτρεπόμενων πεδίων.
Συχνές Ερωτήσεις
Όχι — μετά την κλήση του Global Exception Handler, η κατάσταση της εφαρμογής είναι απροσδιόριστη. Οποιαδήποτε προσπάθεια συνέχισης της λειτουργίας μπορεί να οδηγήσει σε καταστροφή δεδομένων. Η μόνη σωστή ενέργεια — αποθηκεύστε το αρχείο καταγραφής crash και τερματίστε τη διεργασία.
Όχι όλους — στο iOS, το NSSetUncaughtExceptionHandler πιάνει μόνο εξαιρέσεις Objective-C. Τα σφάλματα Swift και τα σήματα λειτουργικού συστήματος (SIGSEGV, SIGABRT) απαιτούν ξεχωριστούς χειριστές. Στο Android, το Thread.setDefaultUncaughtExceptionHandler εντοπίζει όλα τα RuntimeException, αλλά όχι σφάλματα native κώδικα μέσω JNI.
Αποθηκεύστε την αναφορά στον προηγούμενο χειριστή μέσω του Thread.getDefaultUncaughtExceptionHandler() πριν εγκαταστήσετε τον δικό σας χειριστή. Στο τέλος του χειριστή σας, καλέστε το previousHandler.uncaughtException(thread, throwable) — αυτό εγγυάται ότι το Crashlytics ή το Sentry θα λάβουν τα δεδομένα τους.
Για native κώδικα, απαιτείται διαχείριση σημάτων μέσω sigaction() — SIGSEGV, SIGABRT, SIGBUS. Στο Android, μπορεί να χρησιμοποιηθεί το Google Breakpad ή το Crashpad. Στο iOS από την έκδοση 13, είναι διαθέσιμο το Signals API για διαχείριση εξαιρέσεων mach.
Όχι — η εγκατάσταση του χειριστή επηρεάζει μόνο τη στιγμή της εξαίρεσης. Στην κανονική λειτουργία της εφαρμογής, δεν υπάρχει επιβάρυνση. Ο μόνος κίνδυνος είναι διαρροή μνήμης εάν ο χειριστής διατηρεί αναφορά σε Activity ή Context, εμποδίζοντας τη συλλογή απορριμμάτων.
Σύνοψη
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.