Contents
Chào mừng bạn đến với Fx Studio. Chúng ta vẫn tiếp tục với hành trình Concurrency trong Swift. Và ta sẽ khám phát thêm việc cập nhật dữ liệu trên Main Thread. Ngoài ra, ta học thêm các cách sử dụng khác của MainActor.
Đầu tiên, bạn cần biết được 2 khái niệm mới trong Swift 5.5. Đó là:
Còn nếu mọi thứ đã ổn rồi, thì …
Bắt đầu thôi!
Chuẩn bị
Sự kiện lớn nhất của Apple vào đầu tháng 6/2021 vừa qua là WWDC 21. Đã có rất nhiều thứ mới mẻ được giới thiệu trong Swift 5.5. Và bây giờ, chúng ta sẽ tiếp tục tìm hiểu khái niệm thêm về MainActor.
Do đó, bạn cần phải chuẩn bị như sau:
-
- Swift 5.5
- Xcode 13 (beta)
Thật là khó khi Xcode 13 khá là nặng, nên có thể Macbook của bạn sẽ chạy không nỗi. Nên bạn cũng phải cân nhắc khi cập nhật macOS và Xcode 13 nhoé.
Về mặt lý thuyết, những gì mình trình bày ở dưới đây sẽ liên quan tới 2 khái niệm trình bày 2 vấn đề gặp phải khi lập trình bất đồng bộ.
Về mặt demo, ta sẽ demo với một Project iOS nào đó cũng được. Giao diện cực kì đơn giản thôi. Bạn không cần lo lắng về nó quá. (Bạn có thể checkout code tại đây.)
Data trên Main Thread
Ở bài viết trước, bạn cũng đã biết là giao diện cũng không thoát khỏi Data Race trên chính Main Thread. Còn về mặt dữ liệu thì còn dễ bị rơi vào Data Race hơn nữa. Nhất là các dữ liệu mà bạn khai báo ra với mục đích phục vụ cho chính giao diện của bạn.
Mình sẽ lấy cho bạn một ví dụ rất là cơ bản cho mối quan hệ giữa giao diện và dữ liệu. Bạn muốn hiển thị 1 ảnh lên ứng dụng của bạn. Thì bạn sẽ dùng tới UIImageView. Class này sẽ có 2 ý nghĩa trong đó:
UIImageView = Image + View
Trong đó,
- Image chính là dữ liệu của đối tượng ảnh mà ta muốn hiển thị. Trong iOS, dữ liệu ảnh sẽ đại diện bằng UIImage.
- View chính là đối tượng UIImageView. Do có chữ View đó. Nó mang đầy đủ tính chất của một View và dùng để hiển thị dữ liệu ảnh cho người dùng thấy được.
Hầu như, các bạn sẽ không để ý tới imageView.image = motImageNaoDo
. Đó là cách truyền thống để cập nhật dữ liệu cho một đối tượng UIImageView trên iOS.
Tất nhiên, bạn cần phải lưu trữ dữ liệu cho UIImageView trước đó đã. Biến mà để lưu trữ dữ liệu kia, nó chính là dữ liệu mà bạn cần phải đảm báo an toàn trên Main Thread.
Do đó, trong quán trình phát triển project của bạn, việc tách biệt ý nghĩa của các loại dữ liệu trong project với các mục đích dùng khác nhau. Hiểu được dữ liệu của mình sẽ biến đổi trên Thread nào và có an toàn khi thay đổi hay không … Là cực kỳ quan trong đối với việc tồn vong của một chương trình.
Completion Handler
Ngoài ra, bạn còn các dữ liệu khác như là:
- Singleton
- Biến Static
- Các biến toàn cục
- Các khai báo define, cấu hình, config ….
Bạn sẽ thao tác với tụi nó khá là nhiều. Việc truy cập và cập nhật giá trị cho chúng khá thường xuyên. Nhất là tương tác bất đồng bộ (như gọi một API). Và lúc đó, việc sử dụng một Completion Handler là điều hiển nhiên.
Bênh cạnh, việc phổ biến mô hình MVVM trong iOS cũng là tác nhân lớn gây ra việc sử dụng không an toàn cho các biến đó. Nhất là các dữ liệu trên Main Thread.
Ta hãy lấy ví dụ demo của chúng ta tại bài viết trước. Và thêm một ViewModel như sau:
class HomeViewModel { // MARK: Properties let imageURL = "https://photo-cms-viettimes.zadn.vn/w1280/Uploaded/2021/aohuooh/2019_03_16/chum_anh_chung_to_meo_bi_ngao_da_la_co_that_158278544_1632019.png" // MARK: Actions func loadImage(completion: @escaping (UIImage?) -> Void ) { fetchImage(url: URL(string: imageURL)!) { image in DispatchQueue.main.async { completion(image) } } } // MARK: API private func fetchImage(url: URL, completion: @escaping (UIImage?) -> ()) { URLSession.shared.dataTask(with: url) { data, response, error in if error != nil { completion(nil) return } guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200, let data = data else { completion(nil) return } let image = UIImage(data: data) completion(image) } .resume() } }
Đây là một ViewModel điển hình trong việc tương tác với API để lấy dữ liệu về. ViewModel không lưu trữ dữ liệu, nhưng nó sẽ chuyển cho ViewController dữ liệu. Từ đó, dữ liệu sẽ dùng để cập nhật lên giao diện.
Bạn sẽ sử dụng nó như sau.
@IBAction func start(_ sender: Any) { viewmodel.loadImage { image in self.imageView.image = image } }
Tuy nhiên, bạn sẽ để ý tới DispatchQueue.main
đã xuất hiện trở lại. Và nó là nguyên nhân dẫn tới niềm đau. Chỉ cần bạn comment lại nó ở ViewModel. Sau đó, build project và cảm nhận kết quả nhoé.
(Mình đã đề cập về vấn đề này ở bài viết trước rồi.)
Như đã ví dụ ở trên, điểm yếu của chương trình nằm ở việc chuyển dữ liệu đi từ ViewModel sang ViewController. Tuy nhiên, cái cần chú ý là ta đang chuyển dữ liệu từ một Thread khác về lại Main Thread.
Publisher
Ta có thể có một giải pháp khác, nó cũng tiệm cận hoàn hảo. Khi nó có thể khắc phục được khâu truyền dữ liệu qua lại giữa các đối tượng. Đó là Reactive Programming, với framework sử dụng ở đây là Combine.
(Fx Studio có rất nhiều bài viết về Combine. Bạn có thể tham khảo thêm nha.)
Ta sẽ ví dụ bằng cách thêm Combine vào ViewModel như sau:
import Combine class HomeViewModel { // ... @Published var image: UIImage? func loadImage2() { fetchImage(url: URL(string: imageURL)!) { image in DispatchQueue.main.async { self.image = image } } } // ... }
Trong đó:
- Import thêm framework Combine
- Thêm một thuộc tính với Wrapper là
@Published
. Có tác dụng vừa trữ và vừa phát dữ liệu - Function
loadImage2()
không có bất cứ closure nào.
Bạn chỉ cần gọi fetchImage
, khi dữ liệu trả về. Bạn chỉ cần cập nhật lại dữ liệu cho thuộc tính @Published
. Nó sẽ tự động phát dữ liệu sang ViewController. Bên ViewController, ta chỉ cần lắng nghe dữ liệu như sau:
var subscriptions = Set<AnyCancellable>() // .... override func viewDidLoad() { super.viewDidLoad() viewmodel.$image .assign(to: \.image, on: imageView) .store(in: &subscriptions) }
Dữ liệu sẽ đưa thẳng tới .image
của đối tượng imageView
. Không có gì sai sót trong quá trình truyền dữ liệu đi. Tuy nhiên, bạn cũng sẽ nhận ra ở ViewModel có DispatchQueue.main
.
Bạn vẫn không thoát được định mệnh với
DispatchQueue.main
này.
Async/Await
Để có được trải nhiệm tốt hơn với Combine cho việc truyền tải dữ liệu trên Main Thread, thì Swift đã ra mắt thêm async/await. Async/Await sẽ cung cấp thêm sức mạnh cho Combine rất nhiều. (Mình sẽ có các bài viết crossover giữa 2 thế lực mới đó)
Giải pháp mới
Nhiệm vụ của 2 thực thể mới như sau:
- Async/Await sẽ lo phần bất đồng bộ với API
- Combine sẽ lo phần truyền dữ liệu trên Main Thread
Nhưng để đảm bảo mọi thứ phối hợp được với nhau, thì ta cần cải tiến các function của mình lại trước đã. Bạn sẽ cần một hàm fetchImage
mới với async
nha. Xem code tham khảo sau:
func fetchImage(url: URL) async throws -> UIImage? { let (data, _) = try await URLSession.shared.data(from: url) return UIImage(data: data) }
Quá đơn giản (nếu bạn đã theo dõi các bài về async/await rồi). Chúng ta sẽ dừng chờ data
từ việc triệu hồi 1 link url, bằng try await
(có thể có lỗi trong quá trình tương tác, nên dùng try
). Sau đó, return
về một UIImage.
Cũng tại ViewModel, ta sẽ viết mới một function tương tác với ViewController cho việc cập nhật ảnh. Tham khảo code nha.
func loadImage3() async { do { image = try await fetchImage(url: URL(string: imageURL)!) } catch { print("ViewModel : lỗi nè") } }
Cuối cùng, chính là việc triệu hồi tại ViewController.
@IBAction func start(_ sender: Any) { // Async/Await async { await viewmodel.loadImage3() } }
Bạn để ý là chúng ta không sử dụng tới DispatchQueue.main
, nên hạn chế đi nhiều lỗi gây ra khi cập nhật dữ liệu tại một thread khác Main.
Nhìn qua cũng ổn đó!
Vấn đề gặp phải
Tuy cách trên khá ổn, nhưng về bản chất là vẫn dừng chờ tại Thread nào đó. Nếu ta đang sử dụng Main Thread .thì sẽ dừng chờ tại Main Thread.
Mọi thứ sẽ không bị crash do bị cập nhật dữ liệu từ thread khác Main.
Và nếu bạn lạm dụng async
quá. Thì rất có thể bạn rơi vào trường hợp sau:
func loadImage2() { fetchImage(url: URL(string: imageURL)!) { image in async { self.image = image } } }
Trong đó:
- Bạn đang kết hợp giữa Completion Handler và Async
- Nhưng
async { ... }
sẽ dùng chờ tại thread lúc này đang là khác Main
Kết quả vẫn là crash như thường thôi. Và đôi khi, bạn cũng phải chấp nhận một điều là …
DispatchQueue.main
như là một kim chỉ nam giúp bạn đang biết mình ở đâu và đang làm gì.
Cuối cùng, bạn cũng thể ngăn chặn Data Race cho dữ liệu trên Main Thread được. Vì chúng ta chỉ mới giải quyết vấn đề truyền dữ liệu mà thôi.
MainActor
Qua các phần trên, mình chỉ muốn cho bạn thấy sử ảnh hưởng của các Thread khác nhau sẽ tác động như thế nào đến dữ liệu. Mô hình mình sử dụng là MVVM cơ bản. Bên cạnh đó, cho dù bạn sử dụng kĩ thuật nào (Completion, Combine, Async …) thì hầu như không thoát khỏi Data Race. Các cách kết hợp chỉ giúp bạn đảm bảo việc cập nhật dữ liệu trên Main Thread mà thôi.
Mục đích thứ hai, chúng ta cần đảm bảo việc cập nhật dữ liệu được an toàn. Tránh các Data Race cho các dữ liệu dùng chung trên Main Thread. Vì tất cả mọi thứ vẫn đang là bất đồng bộ. Không có ai đảm bảo rằng việc cập nhật cùng một dữ liệu đồng thời từ nhiều nơi khác nhau.
Và từ bài trước, cách hiệu quả nhất chính là …
MainActor
MainActor sẽ giúp bạn đảm bảo việc cập nhật dữ liệu chỉ ở Main Thread mà thôi. Việc cập nhật dữ liệu sẽ diễn ra tuần tự và tránh đi Data Race không đáng có.
Ta sẽ khám thêm các cách dùng khác của MainActor trong các trường hợp lỗi ở trên nha.
On Type
On Type chính là cách khai báo một class/struct mà kèm theo từ khoá @MainActor
trước đó. Nó sẽ đảm bảo rằng toàn bộ class/struct là một tác nhân toàn cục. Khi đó, các thuộc tính & phương thức của thể hiện class/struct đó là isolated
(cách ly).
Ví dụ, với ViewModel của chúng ta như sau:
@MainActor class HomeViewModel { // .... }
Quá đơn giản cho đội Concurrency phải không nào. bây giờ ta sẽ sử dụng các function lỗi khi ta không sử dụng tới DispatchQueue.main
.
func loadImage(completion: @escaping (UIImage?) -> Void ) { fetchImage(url: URL(string: imageURL)!) { image in //DispatchQueue.main.async { //completion(image) //} async { completion(image) } } } func loadImage2() { fetchImage(url: URL(string: imageURL)!) { image in //DispatchQueue.main.async { //self.image = image //} async { self.image = image } } }
Lúc này, đối tượng viewmodel
là một tác nhân toàn cục. Trạng thái của nó (thuộc tính image
) chỉ được cập nhật tại Main Thread. Triệt tiêu đi việc Data Race cho các dữ liệu sử dụng ở Main Thread.
Việc sử dụng MainActor tại On Type cũng giúp bạn tránh đi việc khai báo 2 wrapper properties cùng nhau. Ví dụ như sau:
@MainActor @Published var image: UIImage?
Closure
Bạn còn có một cách dùng khá đặc biệt nữa. Đó chính là khai báo một call back
với @MainActor
. Nghe qua hơi vô lý, nhưng mà cũng thực hiện được à.
Đầu tiên, bạn xoá đi @MainActor
ở khai báo ViewModel. Bạn sẽ sử dụng khai báo closure với @MainActor
, ví dụ như:
var callback : @MainActor (UIImage?) -> Void
Khá là ngầu đó!
Với khai báo đó, callback của bạn sẽ được thực thi tại Main Thread mà thôi. Nên bạn hoàn toàn yên tâm với việc dữ liệu mà nó mà đi. Còn áp dụng vào chương trình của chúng ta thì như sau:
func loadImage4(completion: @MainActor @escaping (UIImage?) -> Void ) { fetchImage(url: URL(string: imageURL)!) { image in detach { await completion(image) } } }
Trong đó:
- Vẫn dùng cách Completion Handler để tương tác giữa ViewController & ViewModel
- Khai báo thêm
@MainActor
cho closure của function detach
để triệu hồi thread khác (cho vui thôi), hoặc bạn có thể dùngasync
để dừng chờ thực thi bất đồng bộ
Code của bạn lúc này sẽ không cần phải thay đổi hết các Completion Handler. Nhưng vẫn đảm bảo việc hoạt động bất đồng bộ và tránh được Data Race rồi đó.
Hãy build project và cảm nhận kết quả nhoé!
Combine
Mình sẽ bổ sung thêm các dùng MainActor với Combine một cách đơn giản và xịn sò hơn nhiều. Bạn tham khảo cách dùng sau cho function loadImage
bằng Combine.
func loadImage() { URLSession.shared .dataTaskPublisher(for: URL(string: imageURL)!) //.receive(on: DispatchQueue.main) .map { data, _ in UIImage(data: data) } .replaceError(with: nil) .assign(to: \.image, on: self) .store(in: &subscriptions) }
Chú ý, ta đã comment dòng code nhận về Main Thread .receive(on: DispatchQueue.main)
, vì với khai báo MainActor rồi thì sẽ không cần dùng nó nữa.
Bạn sẽ không cần quá lo lắng nhiều về truyền dữ liệu qua lại giữa các Thread khác nhau. Tất cả đều được MainActor lo rồi. EZ Game!
Tạm kết
- Các loại dữ liệu trên Main Thread
- Truyền tải và cập nhật dữ liệu trên Main Thread
- Vấn đề Data Race cho dữ liệu
- Các cách sử dụng MainActor để giải quyết vấn đề Data Race trên Main Thread
- Sự kết hợp cách dùng giữa Completion Handler, Async, Combine và MainActor
Okay! Tới đây, mình xin kết thúc bài viết giới thiệu về MainActor trong Swift 5.5 . Nếu có gì thắc mắc hay góp ý cho mình thì bạn có thể để lại bình luận hoặc gởi email theo trang Contact.
Cảm ơn bạn đã đọc bài viết này!
Related Posts:
Written by chuotfx
Hãy ngồi xuống, uống miếng bánh và ăn miếng trà. Chúng ta cùng nhau đàm đạo về đời, về code nhóe!
Leave a Reply Cancel reply
Fan page
Tags
Recent Posts
- Charles Proxy – Phần 1 : Giới thiệu, cài đặt và cấu hình
- Complete Concurrency với Swift 6
- 300 Bài code thiếu nhi bằng Python – Ebook
- Builder Pattern trong 10 phút
- Observer Pattern trong 10 phút
- Memento Pattern trong 10 phút
- Strategy Pattern trong 10 phút
- Automatic Reference Counting (ARC) trong 10 phút
- Autoresizing Masks trong 10 phút
- Regular Expression (Regex) trong Swift
You may also like:
Archives
- September 2024 (1)
- July 2024 (1)
- June 2024 (1)
- May 2024 (4)
- April 2024 (2)
- March 2024 (5)
- January 2024 (4)
- February 2023 (1)
- January 2023 (2)
- November 2022 (2)
- October 2022 (1)
- September 2022 (5)
- August 2022 (6)
- July 2022 (7)
- June 2022 (8)
- May 2022 (5)
- April 2022 (1)
- March 2022 (3)
- February 2022 (5)
- January 2022 (4)
- December 2021 (6)
- November 2021 (8)
- October 2021 (8)
- September 2021 (8)
- August 2021 (8)
- July 2021 (9)
- June 2021 (8)
- May 2021 (7)
- April 2021 (11)
- March 2021 (12)
- February 2021 (3)
- January 2021 (3)
- December 2020 (3)
- November 2020 (9)
- October 2020 (7)
- September 2020 (17)
- August 2020 (1)
- July 2020 (3)
- June 2020 (1)
- May 2020 (2)
- April 2020 (3)
- March 2020 (20)
- February 2020 (5)
- January 2020 (2)
- December 2019 (12)
- November 2019 (12)
- October 2019 (19)
- September 2019 (17)
- August 2019 (10)