@MainActor είναι ένας καθολικός ηθοποιός (global actor) στη γλώσσα Swift, ο οποίος εγγυάται την εκτέλεση κώδικα στο κύριο νήμα. Σύμφωνα με την Apple Developer, 2024, το @MainActor αυτοματοποιεί την εναλλαγή στο κύριο νήμα κατά την εργασία με UI, απαλλάσσοντας τον προγραμματιστή από τη μη αυτόματη κλήση του DispatchQueue.main.async. Η σημείωση εμφανίστηκε στο Swift 5.5 μαζί με το σύστημα async/await.
Κύρια σημεία
@MainActor είναι ένας καθολικός ηθοποιός (global actor) στη Swift, ο οποίος συνδυάζει τις ιδιότητες των ηθοποιών και την εγγύηση εκτέλεσης στο κύριο νήμα της εφαρμογής. Αποτελεί μέρος του συστήματος παραλληλισμού Swift, που παρουσιάστηκε στο Swift 5.5 μαζί με τα async/await και τη δομημένη ταυτοχρονία. Η σημείωση επιτρέπει στον προγραμματιστή να μην ανησυχεί για τη μη αυτόματη εναλλαγή νημάτων και μειώνει τον αριθμό σφαλμάτων UI.
Ένας ηθοποιός (actor) στη Swift είναι ένας τύπος αναφοράς που απομονώνει την κατάστασή του και εγγυάται ότι μόνο ένα νήμα μπορεί να τον τροποποιήσει. Το @MainActor είναι ένας ειδικός καθολικός ηθοποιός, του οποίου εκτελεστής είναι το κύριο νήμα. Οποιοσδήποτε κώδικας επισημασμένος με @MainActor εκτελείται στο κύριο νήμα — ακόμα κι αν κληθεί από μια εργασία παρασκηνίου.
Πριν από την εμφάνιση του @MainActor, οι προγραμματιστές άλλαζαν μη αυτόματα στο κύριο νήμα μέσω του DispatchQueue.main.async. Αυτό ήταν πηγή συχνών σφαλμάτων: οι προγραμματιστές ξεχνούσαν να κάνουν εναλλαγή, οδηγώντας σε crashes λόγω ενημέρωσης του UI όχι στο κύριο νήμα. Το @MainActor λύνει αυτό το πρόβλημα σε επίπεδο συστήματος τύπων.
Η πηγή των περισσότερων σφαλμάτων σε εφαρμογές iOS είναι η UI-ανασφάλεια — η ενημέρωση της διεπαφής από ένα νήμα παρασκηνίου. Η Apple ενσωμάτωσε το @MainActor στο Swift Concurrency για να κάνει την εναλλαγή στο κύριο νήμα αυτόματη και ελέγξιμη από τον μεταγλωττιστή, εξαλείφοντας μια ολόκληρη κατηγορία σφαλμάτων χρόνου εκτέλεσης.
Αρχή λειτουργίας του @MainActor βασίζεται στο σύστημα εκτέλεσης του Swift Concurrency. Όταν ένα νήμα καλεί μια συνάρτηση επισημασμένη με @MainActor, ο χρονοπρογραμματιστής την αναστέλλει στον τρέχοντα εκτελεστή και την επαναλαμβάνει στο κύριο νήμα. Ο μεταγλωττιστής παρακολουθεί τα όρια κλήσης και εγγυάται την ασφάλεια.
Για την εκτέλεση του @MainActor είναι υπεύθυνος ο MainActor.shared — ο εκτελεστής που σχετίζεται με το κύριο νήμα της εφαρμογής. Όταν μια ασύγχρονη συνάρτηση επισημαίνεται με @MainActor, πάντα επαναλαμβάνεται σε αυτόν τον εκτελεστή, ανεξάρτητα από το νήμα στο οποίο ξεκίνησε η αρχική εργασία.
import SwiftUI
class ViewModel: ObservableObject {
@Published var items: [String] = []
@MainActor
func loadData() async {
let result = await fetchRemoteData()
items = result // ασφαλές, το MainActor εγγυάται το κύριο νήμα
}
}
Εάν μια συνάρτηση επισημαίνεται με @MainActor και καλεί μια άλλη ασύγχρονη συνάρτηση, από προεπιλογή κληρονομεί το περιβάλλον του ηθοποιού. Αυτό σημαίνει ότι όλες οι ένθετες κλήσεις εκτελούνται επίσης στο κύριο νήμα, εκτός εάν ορίζεται διαφορετικά. Ο μεταγλωττιστής το παρακολουθεί και εκδίδει σφάλμα κατά την προσπάθεια μεταβίβασης ενός ασύμφωνου closure.
Η σύγκριση του @MainActor με το DispatchQueue.main βοηθά στην κατανόηση του γιατί ο νέος μηχανισμός θεωρείται πιο ασφαλής και βολικός, αν και και οι δύο λύνουν το ίδιο πρόβλημα — την εκτέλεση κώδικα στο κύριο νήμα.
Το @MainActor είναι ένας έλεγχος σε επίπεδο μεταγλωττιστή. Εάν προσπαθήσετε να καλέσετε μια συνάρτηση @MainActor από ένα μη ασφαλές περιβάλλον, ο μεταγλωττιστής θα εκδώσει προειδοποίηση ή σφάλμα. Το DispatchQueue.main.async είναι μια κλήση χρόνου εκτέλεσης: ο κώδικας μεταγλωττίζεται αλλά μπορεί να αποτύχει κατά την εκτέλεση όταν προσπαθεί να ενημερώσει το UI από ένα νήμα παρασκηνίου.
Το DispatchQueue.main.async προσθέτει στην ουρά ένα μπλοκ που μπορεί να εκτελεστεί με καθυστέρηση. Το @MainActor με async/await εκτελεί άμεση εναλλαγή του εκτελεστή χωρίς δημιουργία περιττών closures. Αυτό μειώνει την επιβάρυνση και κάνει τον κώδικα πιο προβλέψιμο ως προς τον χρόνο εκτέλεσης.
// Παλιά προσέγγιση
DispatchQueue.main.async {
self.updateUI()
}
// Νέα προσέγγιση με @MainActor
@MainActor
func updateUI() {
// εκτελείται στο κύριο νήμα
self.label.text = "Ενημερώθηκε"
}
| Κριτήριο | @MainActor | DispatchQueue.main |
|---|---|---|
| Έλεγχος | μεταγλωττιστής | χρόνος εκτέλεσης |
| Σύνταξη | σημείωση (δηλωτική) | κλήση (προστακτική) |
| Επιβάρυνση | χαμηλή (εναλλαγή εκτελεστή) | μεσαία (closure + ουρά) |
| Δυνατότητα δοκιμής | υψηλή (το MainActor.shared μπορεί να αντικατασταθεί) | χαμηλή (δύσκολο να γίνει mock) |
Σε πραγματικά έργα iOS, το @MainActor εφαρμόζεται σε επίπεδα ViewModel, SwiftUI views και UIKit controllers. Η σημείωση μπορεί να εφαρμοστεί τόσο σε μεμονωμένες μεθόδους όσο και σε ολόκληρο τον τύπο.
Επισημαίνοντας μια κλάση με @MainActor, εγγυάστε ότι όλες οι μέθοδοι και οι ιδιότητές της είναι προσβάσιμες μόνο στο κύριο νήμα. Αυτό είναι ιδιαίτερα βολικό για SwiftUI views και κλάσεις ObservableObject: απλώς προσθέτετε @MainActor πριν από το class, και όλες οι ιδιότητες @Published ενημερώνονται με ασφάλεια.
@MainActor
final class UserListViewModel: ObservableObject {
@Published var users: [User] = []
@Published var isLoading = false
func fetchUsers() async {
isLoading = true
users = await api.getUsers()
isLoading = false
}
}
Κατά την εργασία με παλιό κώδικα UIKit, όπου η εναλλαγή νημάτων ήταν χειροκίνητη, μπορείτε να χρησιμοποιήσετε το MainActor.run για ρητή εναλλαγή. Αυτό είναι βολικό για σταδιακή μετάβαση στο Swift Concurrency χωρίς επανεγγραφή ολόκληρης της βάσης κώδικα.
await MainActor.run {
self.tableView.reloadData()
}
Παρά όλα τα πλεονεκτήματα, το @MainActor έχει ορισμένους περιορισμούς που είναι σημαντικό να ληφθούν υπόψη κατά τον σχεδιασμό της αρχιτεκτονικής της εφαρμογής. Η κατανόηση των ορίων εφαρμογής βοηθά στην αποφυγή εσφαλμένης χρήσης.
Εάν ολόκληρη η αλυσίδα κλήσεων είναι επισημασμένη με @MainActor, τότε οποιαδήποτε βαριά εργασία θα εκτελείται στο κύριο νήμα, προκαλώντας καθυστερήσεις στο UI. Συνιστάται να επισημαίνετε με @MainActor μόνο το επίπεδο UI, αφήνοντας την επιχειρηματική λογική και τα αιτήματα δικτύου σε ηθοποιούς παρασκηνίου ή στον καθολικό εκτελεστή.
Παλαιότερα API βασισμένα σε callback (για παράδειγμα, URLSession χωρίς async/await) δεν υποστηρίζουν το περιβάλλον ηθοποιού. Για ενσωμάτωση απαιτείται ένα περιτύλιγμα με CheckedContinuation. Επίσης, το @MainActor δεν είναι συμβατό με performSelector, target-action και άλλα μη ασύγχρονα μοτίβα UIKit.
Κατά την αποσφαλμάτωση εφαρμογών με @MainActor, είναι πιο δύσκολο να αναπαραχθούν συνθήκες ανταγωνισμού, καθώς ο μεταγλωττιστής αποτρέπει πολλές από αυτές στο στάδιο της μεταγλώττισης και όχι κατά την εκτέλεση. Ωστόσο, αυτό μπορεί να δημιουργήσει μια ψευδή αίσθηση ασφάλειας: η λανθασμένη εργασία με κοινόχρηστα mutable αντικείμενα (όπως NSCache ή κοινές καθολικές μεταβλητές) εξακολουθεί να είναι δυνατή εάν δεν είναι επισημασμένα με @MainActor και χρησιμοποιούνται χωρίς ρητό συγχρονισμό.
Το @MainActor απλοποιεί σημαντικά τη δοκιμή της λογικής UI, καθώς απαλλάσσει από την ανάγκη μη αυτόματης εναλλαγής νημάτων στα τεστ. Ωστόσο, υπάρχουν ιδιαιτερότητες που πρέπει να ληφθούν υπόψη κατά τη σύνταξη unit tests και UI tests.
Στο XCTest, το περιβάλλον εκτέλεσης ρυθμίζει αυτόματα τον εκτελεστή του κύριου νήματος. Όταν μια μέθοδος δοκιμής εκτελείται στο κύριο νήμα, η κλήση συναρτήσεων @MainActor δεν απαιτεί πρόσθετες ρυθμίσεις — εκτελούνται στο ίδιο περιβάλλον. Για τη δοκιμή σεναρίων παρασκηνίου, χρησιμοποιήστε το MainActor.run μέσα σε ένα Task με ρητό καθορισμό προτεραιότητας και εκτελεστή, ελέγχοντας ξεχωριστά ότι ο κώδικας λειτουργεί σωστά όταν καλείται από το παρασκήνιο.
Μία από τις κοινές προσεγγίσεις είναι η δοκιμή ViewModel με @MainActor, όπου ελέγχεται ότι οι ιδιότητες @Published ενημερώνονται σωστά μετά από ασύγχρονες λειτουργίες. Χάρη στην κληρονομικότητα του περιβάλλοντος ηθοποιού, η κλήση await μέσα στο τεστ εγγυάται την εκτέλεση στο κύριο νήμα χωρίς πρόσθετες εγγυήσεις DispatchQueue και μη αυτόματη εναλλαγή περιβάλλοντος, απλοποιώντας τη σύνταξη των τεστ.
Κατά τον ανασχηματισμό υπάρχοντος κώδικα σε Swift Concurrency, ελέγξτε την απομόνωση @MainActor μέσω του μεταγλωττιστή: οποιεσδήποτε κλήσεις σύγχρονων μεθόδων χωρίς @MainActor από το περιβάλλον @MainActor επισημαίνονται ως σφάλμα. Αυτή η ιδιότητα χρησιμοποιείται για σταδιακή μετάφραση του έργου σε async/await: επισημαίνετε το επίπεδο ViewModel ως @MainActor και ο μεταγλωττιστής φωτίζει όλες τις μη ασφαλείς κλήσεις που πρέπει να μεταφερθούν σε ηθοποιούς παρασκηνίου.
Κατά τη δημιουργία mocks για εξαρτήσεις @MainActor, χρησιμοποιήστε πρωτόκολλα με async-μεθόδους που δηλώνουν ασύγχρονες συναρτήσεις με τύπους επιστροφής. Αυτό επιτρέπει την αντικατάσταση υπηρεσιών δικτύου, βάσεων δεδομένων και άλλων εξωτερικών εξαρτήσεων χωρίς παραβίαση της απομόνωσης ηθοποιού. Ο μεταγλωττιστής θα ελέγξει ότι το mock ικανοποιεί όλες τις απαιτήσεις απομόνωσης, αποτρέποντας τυχαία πρόσβαση σε κώδικα @MainActor από νήματα δοκιμών παρασκηνίου.
Κατά τη σύγχρονη δοκιμή κώδικα @MainActor, χρησιμοποιήστε το XCTestExpectation για αναμονή ολοκλήρωσης ασύγχρονων λειτουργιών. Ορίστε την αναμονή στο τεστ και εκτελέστε το fulfillment μέσα σε ένα closure που εκτελείται στο κύριο νήμα. Εάν το τεστ παραμένει σε αναμονή επ' αόριστον, πιθανότατα η κλήση στο κύριο νήμα δεν πραγματοποιείται και πρέπει να ελέγξετε την απομόνωση του ηθοποιού. Για αποσφαλμάτωση του περιβάλλοντος εκτέλεσης, είναι χρήσιμο να προσθέσετε έλεγχο του Thread.isMainThread μέσα στον κώδικα δοκιμής.
Συχνές ερωτήσεις
Όχι, αρκεί να επισημάνετε μόνο τις μεθόδους που ενημερώνουν το UI. Ωστόσο, εάν η κλάση έχει πολλές τέτοιες μεθόδους, είναι πιο εύκολο να προσθέσετε @MainActor σε ολόκληρη την κλάση. Αυτό εγγυάται ότι όλα τα μέλη της εκτελούνται στο κύριο νήμα και απλοποιεί τη συντήρηση του κώδικα.
@MainActor είναι ένα συγκεκριμένο στιγμιότυπο καθολικού ηθοποιού, συνδεδεμένο με το κύριο νήμα. Το @globalActor είναι ένα πρωτόκολλο για τη δημιουργία δικών σας καθολικών ηθοποιών. Για παράδειγμα, μπορείτε να δημιουργήσετε ένα @BackgroundActor για εκτέλεση κώδικα σε νήμα παρασκηνίου, εάν απαιτείται από την αρχιτεκτονική του έργου.
Ναι, οι σύγχρονες συναρτήσεις με @MainActor εκτελούνται επίσης στο κύριο νήμα. Ωστόσο, η κύρια αξία του @MainActor αποκαλύπτεται με το async/await, όταν μια ασύγχρονη συνάρτηση επαναλαμβάνεται αυτόματα στο κύριο νήμα χωρίς μη αυτόματη εναλλαγή μέσω DispatchQueue.main.
Το Task.cancel() λειτουργεί με εργασίες @MainActor όπως και με τις κανονικές. Μια εργασία @MainActor μπορεί να ελέγξει το Task.isCancelled ή να πετάξει CancellationError. Κατά την ακύρωση, το κύριο νήμα δεν μπλοκάρεται — η εργασία απλώς σταματά την εκτέλεση στο πλησιέστερο σημείο αναστολής.
Ο μεταγλωττιστής εγγυάται την ασφάλεια: εάν καλέσετε μια συνάρτηση @MainActor από περιβάλλον παρασκηνίου, ο μεταγλωττιστής θα υποδείξει το σφάλμα. Για ασύγχρονες κλήσεις, αρκεί να επισημάνετε τον καλούντα κώδικα με await και ο εκτελεστής θα μεταβεί αυτόματα στο κύριο νήμα. Για σύγχρονες κλήσεις, απαιτείται ρητή εναλλαγή μέσω MainActor.run.
Σύνοψη
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης