Κολλάει — είναι η περιγραφή από τον χρήστη της κατάστασης όταν η κινητή εφαρμογή λειτουργεί αργά και ασταθώς: άλλοτε ανταποκρίνεται κανονικά, άλλοτε ξαφνικά παγώνει για μερικά δευτερόλεπτα. Σε τεχνικό πλαίσιο, το «κολλάει» σημαίνει έναν συνδυασμό lag και μικροπαγώματος που προκαλείται από συχνές παύσεις GC, μπλοκάρισμα του κύριου νήματος από σύγχρονες λειτουργίες και μη βέλτιστες δομές δεδομένων. Σύμφωνα με τον Android Performance Benchmarking Guide, η μείωση του χρόνου απόκρισης από 300 ms σε 100 ms αυξάνει τη διατήρηση χρηστών κατά 25%. Η διάγνωση του κολλήματος απαιτεί συνδυασμό CPU και Memory προφίλ με ανάλυση της συχνότητας συλλογής απορριμμάτων.
Κύρια σημεία
Κολλάει — ένας ανεπίσημος όρος με τον οποίο οι χρήστες περιγράφουν την υποκειμενικά αργή λειτουργία της εφαρμογής. Σε αντίθεση με το lag, το οποίο εκδηλώνεται ως σταθερή καθυστέρηση, το κόλλημα είναι ακανόνιστο πάγωμα: η εφαρμογή μπορεί να λειτουργεί τέλεια για μερικά δευτερόλεπτα και στη συνέχεια να «σκέφτεται» για 1–3 δευτερόλεπτα.
Από την άποψη της προφίλ, το κολλάει εκδηλώνεται ως μια σειρά από χαμένα καρέ (jank) με μέγιστες καθυστερήσεις άνω των 100 ms. Στο γράφημα FPS αυτό μοιάζει με απότομες πτώσεις: 60 → 20 → 55 → 10 καρέ ανά δευτερόλεπτο. Σε αντίθεση με το lag με ομοιόμορφα χαμηλό FPS, το κόλλημα έχει σαφή μεταβλητότητα.
Όταν η εφαρμογή κολλάει, ο χρήστης δεν κατανοεί τη λογική των επιβραδύνσεων: η οθόνη μπορεί να κυλά ομαλά και στη συνέχεια να σταματήσει ξαφνικά για ένα δευτερόλεπτο. Αυτό προκαλεί απογοήτευση και μειώνει την εμπιστοσύνη στην εφαρμογή. Σύμφωνα με την Google, το 53% των χρηστών εγκαταλείπει έναν ιστότοπο ή εφαρμογή εάν η φόρτωση διαρκεί περισσότερο από 3 δευτερόλεπτα.
Η ακανόνιστη φύση του κολλήματος υποδηλώνει ότι το πρόβλημα προκαλείται από παράγοντες συμβάντων, όχι από σταθερή υπερφόρτωση. Ας εξετάσουμε τυπικά σενάρια.
Στο Android στο περιβάλλον ART, η συλλογή απορριμμάτων σταματά όλα τα νήματα της εφαρμογής. Εάν στον κώδικα δημιουργούνται πολλά προσωρινά αντικείμενα — για παράδειγμα, σε κάθε κλήση onBindViewHolder δημιουργείται ένα νέο String μέσω συνένωσης — το GC εκκινείται συχνότερα. Η παύση μπορεί να διαρκέσει 5–50 ms ανάλογα με το μέγεθος του σωρού και τη γενιά των αντικειμένων. Ο χρήστης το αισθάνεται ως ξαφνική «σκέψη».
Room στο Android και Core Data στο iOS υποστηρίζουν ασύγχρονα ερωτήματα, αλλά οι προγραμματιστές συχνά καλούν την getValue() ή εκτελούν ερώτημα μέσω runBlocking για απλότητα. Ένα βαρύ SELECT με joins σε πίνακα 10.000 γραμμών μπορεί να διαρκέσει 200–500 ms, μπλοκάροντας πλήρως το UI για αυτό το διάστημα.
Η φόρτωση εικόνας από κάμερα (12 Mp, 4000x3000 px) χωρίς κλιμάκωση διαρκεί έως 200 ms για αποκωδικοποίηση σε Bitmap. Εάν οι εικόνες φορτώνονται ασύγχρονα αλλά χωρίς pool νημάτων με περιορισμό, η ταυτόχρονη εκκίνηση 5–6 αποκωδικοποιήσεων μπορεί να υπερφορτώσει την CPU, προκαλώντας μεταναστεύουσες επιβραδύνσεις.
Η διάγνωση ακανόνιστων επιβραδύνσεων είναι δυσκολότερη από τη διάγνωση σταθερών lag, επειδή το πρόβλημα μπορεί να μην αναπαράγεται σε κάθε εκκίνηση. Απαιτείται συλλογή στατιστικών για μεγαλύτερο χρονικό διάστημα.
Android Studio Memory Profiler δείχνει όχι μόνο τη χρήση μνήμης, αλλά και τα συμβάντα GC: συχνότητα, τύπο (Concurrent, Full), διάρκεια. Εάν το GC συμβαίνει συχνότερα από 1 φορά σε 5 δευτερόλεπτα σε κατάσταση ηρεμίας — αυτό είναι σημάδι υπερβολικής δέσμευσης. Η καταγραφή heap dump τη στιγμή του κολλήματος επιτρέπει να δείτε ποια αντικείμενα καταλαμβάνουν μνήμη.
Στο iOS χρησιμοποιήστε το πρότυπο Allocations στο Instruments για την παρακολούθηση δημιουργίας και απελευθέρωσης αντικειμένων. Ενεργοποιήστε τις γενιές (Generations) — σας επιτρέπουν να τραβάτε στιγμιότυπα σωρού μεταξύ ενεργειών και να βλέπετε ποια αντικείμενα παραμένουν στη μνήμη. Τα επίμονα αντικείμενα που δεν απελευθερώνονται — πηγή συσσώρευσης μνήμης και επακόλουθων παύσεων.
JankStats — βιβλιοθήκη Android που συλλέγει μετρικές χαμένων καρέ σε πραγματικό χρόνο. Συνδέει κάθε jank με το τρέχον σενάριο (π.χ. «κύλιση λίστας», «άνοιγμα οθόνης»), επιτρέποντας να κατανοήσετε σε ποια ακριβώς ενέργεια εμφανίζεται το κόλλημα.
Παράδειγμα ενσωμάτωσης JankStats για παρακολούθηση παγωμάτων στο Android:
class MainActivity : AppCompatActivity() {
private lateinit var jankStats: JankStats
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
jankStats = JankStats.create(this.window.decorView) { frameData ->
if (frameData.isJank()) {
Log.w("Jank", "Duration=${frameData.durationMs}ms")
}
}
}
}
Η εξάλειψη του κολλήματος απαιτεί στοχευμένη εργασία με κάθε αιτία. Δεν υπάρχει καθολική λύση — απαιτείται ανάλυση συγκεκριμένων προφίλ απόδοσης.
Εάν η λίστα περιέχει 1000+ στοιχεία και όλα φορτώνονται ταυτόχρονα — αυτό είναι εγγυημένο κόλλημα. Το Paging 3 στο Android και το NSFetchedResultsController στο iOS φορτώνουν δεδομένα σε δόσεις κατά την κύλιση. Ο χρήστης βλέπει μόνο τα πρώτα 10–20 στοιχεία, τα υπόλοιπα φορτώνονται στο παρασκήνιο.
Room επιτρέπει την προφίλ ερωτημάτων μέσω του Inspection Tool στο Android Studio: φαίνεται ο χρόνος εκτέλεσης, ο αριθμός επιστρεφόμενων γραμμών και το σχέδιο ερωτήματος. Η προσθήκη δεικτών στις στήλες WHERE και ORDER BY μπορεί να μειώσει τον χρόνο ερωτήματος από 300 ms σε 5 ms. Στο iOS παρόμοιο έλεγχο εκτελεί το Core Data Profiler στο Instruments.
Συγχρονισμοί παρασκηνίου, μεταφορτώσεις αρχείων, επεξεργασία δεδομένων — όλα αυτά πρέπει να εκτελούνται μέσω WorkManager (Android) ή Background Tasks (iOS). Εάν ο συγχρονισμός ξεκινά στο νήμα UI, η εφαρμογή θα κολλάει κατά τη διάρκεια εκτέλεσης. Το WorkManager εγγυάται εκτέλεση στο νήμα παρασκηνίου λαμβάνοντας υπόψη την κατάσταση μπαταρίας και δικτύου.
Παράδειγμα συγχρονισμού παρασκηνίου μέσω WorkManager στο Android:
class SyncWorker(context: Context, params: WorkerParameters)
: CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
Log.d("Sync", "Συγχρονισμός δεδομένων σε νήμα παρασκηνίου")
syncData()
Result.success()
} catch (e: Exception) {
Result.retry()
}
}
}
Το κόλλημα μπορεί να προληφθεί στο στάδιο της γραφής κώδικα, ακολουθώντας τις αρχές αποτελεσματικής εργασίας με μνήμη και νήματα.
Baseline Profiles — λίστα κλάσεων και μεθόδων που το Android μεταγλωττίζει εκ των προτέρων (AOT), όχι JIT. Χωρίς προφίλ, κάθε νέα οθόνη μεταγλωττίζεται κατά το πρώτο άνοιγμα, προκαλώντας καθυστέρηση 100–500 ms. Προετοιμάστε Baseline Profile για βασικές οθόνες και ενεργοποιήστε τη δημιουργία στο Gradle μέσω του baseline-profile-gradle-plugin.
Hot path — κώδικας που εκτελείται σε κάθε καρέ: onBindViewHolder, draw, layoutSubviews. Αποφύγετε τη δημιουργία αντικειμένων σε αυτές τις μεθόδους: χρησιμοποιήστε pool αντικειμένων, StringBuilder αντί συνένωσης, αποθηκεύστε προσωρινά μορφοποιημένες συμβολοσειρές και μορφοποιητές. Κάθε επιπλέον δέσμευση φέρνει πιο κοντά το επόμενο GC.
Προσθέστε στο CI pipeline την εκτέλεση Macrobenchmark με σενάριο κύλισης λίστας και ανοίγματος οθόνης. Ορίστε όριο: το 99ο εκατοστημόριο του χρόνου καρέ δεν πρέπει να υπερβαίνει τα 16 ms. Εάν το όριο ξεπεραστεί — η build απορρίπτεται μέχρι τη βελτιστοποίηση.
Συχνές ερωτήσεις
Lag — σταθερή καθυστέρηση (π.χ. 200 ms ανά πάτημα). Κολλάει — ακανόνιστα: η εφαρμογή λειτουργεί κανονικά, στη συνέχεια ξαφνικά επιβραδύνεται για 1–3 δευτερόλεπτα, στη συνέχεια πάλι κανονικά. Αιτία — παράγοντες συμβάντων όπως παύσεις GC ή σύγχρονα ερωτήματα στη βάση δεδομένων.
Χρησιμοποιήστε Memory Profiler στο Android Studio: η καρτέλα Memory εμφανίζει συμβάντα GC με διάρκεια. Για παρακολούθηση παραγωγής, συνδέστε το Firebase Performance Monitoring με προσαρμοσμένες ιχνηλατήσεις. Στο iOS ενεργοποιήστε το Malloc Debug και σημειώστε τις γενιές δέσμευσης στο Instruments.
Έμμεσα — ναι. Εάν η απόκριση του διακομιστή έρχεται με καθυστέρηση και το UI την περιμένει σύγχρονα, η εφαρμογή παγώνει. Εάν το αίτημα είναι ασύγχρονο αλλά η επεξεργασία απόκρισης γίνεται στο νήμα UI — αυτό θα προκαλέσει επίσης κόλλημα. Λύση — ασύγχρονη επεξεργασία με coroutines και δείκτες προόδου.
Σε λανθασμένη χρήση το KMP μπορεί να δημιουργήσει περιττά αντικείμενα-περιτυλίγματα για διαλειτουργικότητα. Στο iOS αυτό αυξάνει τη συχνότητα δέσμευσης και κατά συνέπεια τις παύσεις ARC. Χρησιμοποιήστε @ObjCName, βελτιστοποιήστε το expect/actual και αποφύγετε συχνές κλήσεις κοινόχρηστου κώδικα από hot paths του UI.
Η αύξηση του σωρού μέσω android:largeHeap="true" καθυστερεί το GC αλλά δεν εξαλείφει την αιτία των δεσμεύσεων. Όταν το GC τελικά εκκινηθεί, η παύση θα είναι μεγαλύτερη επειδή πρέπει να διασχίσει περισσότερα αντικείμενα. Λύση — μειώστε τον αριθμό δεσμεύσεων, μην επεκτείνετε τον σωρό.
Σύνοψη
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης