Ở sự kiện WWDC21 năm ngoái, Apple đã có sự cập nhật mới về các hàm bất đồng bộ trong swift 5.5. Đó là sự xuất hiện của async
và await
.
A/e dev C# hay JS nghe đến đây thì kiểu: 😏 😏 😏
Quả đúng là async/await
là những keyword chẳng xa lạ gì đối với anh em lập trình, nhưng cập nhật mới này của Apple, đối với dev iOS lại mang ý nghĩa khá to lớn. Nó đã cho thấy sự tập trung và ưu tiên phát triển của Concurency
, thu hẹp đáng kể về khoảng cách cũng như thân thiện hơn đối với các dev trong việc xử lý Concurency
.
Nói đến đây thì nhiều anh em lại tò mò muốn biết async/await
có gì hay ho, cơ mà khoan, trước khi tìm hiểu về async/await
thì t sẽ nhắc lại về Completion Handle
Completion Handle chắc hẳn đã quá là quen thuộc với mỗi dev iOS. Anh em dev iOS sử dụng nhiều hơn cả ăn cơm mỗi ngày. Tuy nhiên, dùng nhiều quen tay, không ít dev lại chẳng nhận ra những nhược điểm của nó. Cùng phân tích ví dụ đơn giản dưới đây để thấy rằng Completion Handle cũng không hoàn hảo như chúng ta tưởng :D
Bạn muốn lấy 1 image từ 1 Url, sau đó chuyển image đó về 1 ảnh thumbnail bé hơn và hiển thị hình ảnh thumbnail đó ra ngoài view, trường hợp bị lỗi sẽ update view để người dùng có thể biết dc. Ok, bài toán đã có, giờ chúng ta sẽ tiến hành triển khai bài toán theo cách mà trước giờ chúng ta đều làm. Let’s go!
Từ yêu cầu của bài toán, việc chúng ta cần làm đầu tiên là thực hiện việc lấy ảnh từ URL. Tất nhiên, việc lấy ảnh này sẽ không thể có kết quả ngay, và để tránh block luồng chính, thì việc download ảnh sẽ là 1 xử lý bất đồng bộ, và khi request hoàn thành, chúng ta sẽ sử dụng Completion Handle để pass dữ liệu về - như cách mà chúng ta vẫn thường làm. Func cơ bản sẽ như sau..
func fetchThumbnailImage(from urlString: String, completion: @escaping (UIImage?, Error?) -> Void){guard let url = URL(string: urlString) else {return}let task = URLSession.shared.dataTask(with: url) { data, response, error inif let error = error {completion(nil, error)} else {guard let data = data else {return}let image = UIImage(data: data)// xử lý lấy ảnh thumbnail....}}task.resume()}
OK, ở trên là một đoạn code xử lý việc lấy ảnh từ URL, tuy nhiên, đã có 1 chút vấn đề ở đoạn code trên. Hãy nhìn lại xem liệu b có nhận ra ngay lập tức vấn đề nằm ở đâu không? (trước khi kéo xuống dưới để xem kết quả ) 😆😆😆
… … …
Và vấn đề của đoạn code trên, nó nằm ở đây.
Đúng vậy, ở đây t đã return và thoát ra khỏi func luôn mà k gọi completion
.
T đoán là không chỉ mình t, mà kha khá các bạn dev iOS khác, đặc biệt là các bạn fresher, sau khi viết guard let ...
là lập tức return
ngay mà chẳng màng gì khác, nào biết đâu, User vẫn đang chờ đợi request hoàn thành mãi mà chẳng view cập nhật gì. 😅
=> Và đó là vấn đề đầu tiên khi sử dụng Completion Handle: Quên không gọi hoặc gọi quá nhiều completion
Được rồi, giờ t sẽ chỉnh sửa lại code một chút và thêm xử lý ảnh thumbnail. Thật may là swift cung cấp một func có sẵn cho việc này và chúng ta chỉ việc gọi nó ra. Và code hoàn chỉnh cho cả bài toán sẽ như thế này:
Ở đây, việc xử lý trả về ảnh thumbnail mà swift cung cấp, cũng là 1 xử lý bất đồng bộ, vì vậy ta sẽ phải sử dụng thêm 1 function với Completion Handle.
Đây chỉ là một bài toán đơn giản, nhưng đã phải sử dụng liên tiếp 2 function với Completion Handle, mục đích chỉ là chờ xử lý này xong rồi mới đến xử lý tiếp theo. Trong thực tế, nhiều khi không chỉ có 2 function lồng nhau như thế này. Việc gọi các function với Completion Handle liên tiếp sẽ có nhược điểm như là :
Ngoài ra còn một nhược điểm mà t và nhiều người nữa đã, đang và sẽ gặp trong việc sử dụng Completion Handle với các hàm bất đồng bộ là việc: quên từ khoá @escaping
, vì nhiều khi bản thân không biết lúc nào function là đồng bộ hay bất đồng bộ .
Việc thêm @escaping
là vô cùng quan trọng đối với các function bất đồng bộ sử dụng Completion Handle , vì nó sẽ giữ lại closure lại trong bộ nhớ kể cả khi function kết thúc, cho đến khi closure được sử dụng. Quên từ khoá escaping
, sẽ khiến closure không được gọi -> Bug bắt đầu từ đó đó 😆
Qua ví dụ trên cùng với các phân tích, ta có thể thấy Completion Handle không thực sự hoàn hảo như chúng ta vẫn nghĩ. Nó có những nhược điểm cơ bản sau:
@escaping
cho function bất đồng bộ -> Closure không được gọiNhưng đừng lo, Swift 5.5 đã cung cấp cho chúng ta giải pháp, đó là sử dụng 2 từ khoá async
và await
. Tiếp theo chúng ta sẽ tìm hiểu cách sử dụng async/await
.
Sẽ rất khó hiểu nếu chúng ta cố gắng định nghĩa thế nào là async
và await
, nên chúng ta có thể hiểu đơn giản: hai từ khoá async
và await
để khai báo và gọi các function bất động bộ.
async
: dùng trong khai báo một function, đánh dấu cho chúng ta biết đó là function bất động bộawait
: dùng khi gọi để thực thi các function bất đồng bộasync
func someFunction() async -> Void {// do something}
await
await
ngay trong một đoạn code đồng bộ, thì cần phải bọc trong Task{...}
func synchronousFunction {// do somethingTask {await someFunction()}}
func anotherFunction() async {await somefunction()}
Chúng ta sẽ triển khai lại với bài toán đã có ở trên.
fetchThumnailImage
, vẫn nhận vào là một urlString
nhưng không có cần phải sử dụng Completion Handle để pass dữ liệu nữa, mà sẽ sử dụng async
và trả về luôn data là UIImage
như sau:func fetchThumnailImage(from urlString: String) async throws -> UIImage {guard let url = URL(string: urlString) else {throw CustomError.badUrl}// call request..}
Vì không phải lúc nào cũng có thể may mắn trả về ngay một UIImage
, nên sẽ sử dụng throws
trong trường hợp không may xảy ra lỗi 😅
async
và Swift đã giúp ta làm việc đó với với function:
Việc chúng ta cần làm bây giờ, là sử dụng await
để gọi nó ra. Tuy nhiên vì function mà swift cung cấp có sử dụng throws
, nên chúng ta phải sử dụng cú pháp try await
như sau:func fetchThumnailImage(from urlString: String) async throws -> UIImage {guard let url = URL(string: urlString) else {throw CustomError.badUrl}let urlRequest = URLRequest(url: url)// call request..let (data, _) = try await URLSession.shared.data(for: urlRequest)let image = UIImage(data: data)// handle thumbnail image..}
byPreparingThumbnail(ofSize:)
đã được dánh dấu với await
và chúng ta chỉ việc sử dụng:func fetchThumnailImage(from urlString: String) async throws -> UIImage {guard let url = URL(string: urlString) else {throw CustomError.badUrl}let urlRequest = URLRequest(url: url)// call request..let (data, _) = try await URLSession.shared.data(for: urlRequest)let image = UIImage(data: data)// handle thumbnail imageguard let thumbnail = await image?.byPreparingThumbnail(ofSize: CGSize(width: 30, height: 30)) else {throw CustomError.badImage}return thumbnail}
fetchThumnailImage
đã được đánh dấu bất đồng bộ với async
, trong 1 luồng đang chạy đồng bộ như các xử lý trong viewDidLoad
, ta sẽ gọi như sau:override func viewDidLoad() {super.viewDidLoad()let urlString = "https://d38b044pevnwc9.cloudfront.net/cutout-nuxt/enhancer/2.jpg"Task {do {let image = try await fetchThumnailImage(from: urlString)self.imageView.image = image} catch {self.lbError.text = error.localizedDescription}}}
Rõ ràng, cùng một bài toán, xử lý với Async/await
sẽ ngắn gọn và dễ nhìn hơn rất nhiều so với Completion Handle
. Từ đó mà có thể dễ dàng quản lý lỗi, đọc code và không cần lo lắng bị block luồng hoạt động vì lỡ quên cái gì đó như @escaping
hay completion
=> Async/await
khắc phục được các nhược điểm mà chúng ta đã đề cập ở trên của Completion Handle. Tuy nhiên, nó được sinh ra không chỉ làm những nhiệm vụ đơn giản như vậy. Chúng ta sẽ đi sâu tiếp về async/await
ở blog tiếp theo.
Cảm ơn mọi người vì đã đọc đến đây. Hẹn gặp lại ở bài viết tiếp theo. 🥰 Thân ái và quyết thắng 🇻🇳🇻🇳🇻🇳🇻🇳🇻🇳