Trang chủ > Phát triển di động > Nội dung chính

Xử lý bất đồng bộ trong phát triển Android và iOS (hai) —— Quay lại tác vụ bất đồng bộ


Bài viết này là phần tiếp theo của loạt bài viết về: Xử lý bất đồng bộ trong phát triển Android và iOS. Phần thứ hai của “>”. Trong bài viết nàytỉ lệ cược, chúng ta sẽ chủ yếu thảo luận về nhiều vấn đề liên quan đến callback của các tác vụ bất đồng bộ.

Trong iOS789 Club, callback thường được thể hiện dưới dạng delegate; còn trong Android, callback lại thường xuất hiện dưới hình thứ Tuy nhiên, bất kể chúng được biểu đạt như thế nào, callback luôn là một phần không thể tách rời trong thiết kế giao diện. Chất lượng của việc thiết kế callback có vai trò quyết định đến thành công hay thất bại của toàn bộ quá trình thiết kế giao diện. Ngoài ra, việc lựa chọn cách thức triển khai callback phù hợp sẽ giúp tăng cường tính linh hoạt và khả năng mở rộng cho ứng dụng. Nếu callback được thiết kế tốt, nó sẽ tạo điều kiện cho các nhà phát triển dễ dàng tích hợp các chức năng mới mà không cần phải thay đổi quá nhiều mã nguồn hiện tại. Ngược lại, một thiết kế kém sẽ dẫn đến sự phức tạp không cần thiết, gây khó khăn trong việc bảo trì và mở rộng hệ thống sau này. Vì vậy, việc đầu tư thời gian và công sức để nghiên cứu và tối ưu hóa callback là rất quan trọng trong quy trình phát triển ứng dụng di động.

Trong quá trình thiết kế và triển khai giao diện callback789 Club, chúng ta cần cân nhắc những khía cạnh nào? Trước tiên, hãy cùng liệt kê các chủ đề con mà bài viết này sẽ đề cập đến, sau đó sẽ lần lượt phân tích từng vấn đề một: 1. Các yêu cầu cơ bản đối với giao diệ 2. Cách xử lý dữ liệu khi nhận được phản hồi từ callback. 3. Làm thế nào để tối ưu hóa hiệu suất củ 4. Quản lý lỗi và xử lý ngoại lệ 5. Bảo mật thông tin khi sử dụ Bây giờ, hãy bắt đầu khám phá từng yếu tố quan trọng này!

  • Cần tạo ra callback kết quả bắt buộc
  • Tập trung vào callback thất bại & mã lỗi nên được mô tả chi tiết nhất có thể
  • Giao diện gọi và giao diện callback cần có mối quan hệ tương ứng rõ ràng
  • Callback kết quả thành công và callback kết quả thất bại nên loại trừ lẫn nhau
  • Mô hình luồng làm việc của callback
  • Tham số ngữ cảnh callback (thông số truyền thẳng)
  • Thứ tự callback
  • Callback dưới dạng closure và Callback Hell

Lưu ý: Mã nguồn trong loạt bài viết này đã được tổ chức trên GitHub (liên tục cập nhật)tỉ lệ cược, đường dẫn kho lưu trữ mã là:

Trong bài viết này789 Club, mã nguồn Java được đề cập nằm trong package có tên là com. demos.async. callback. Đây là một phần quan trọng trong cấu trúc chương trình, nơi các phương thức xử lý dữ liệu theo cách bất đồng bộ được triển khai để tối ưu hóa hiệu suất. Package này đóng vai trò như một nền tảng giúp quản lý các callback một cách hiệu quả, từ đó tạo ra một hệ thống ứng dụng linh hoạt và mạnh mẽ.


Cần tạo ra callback kết quả bắt buộc

Khi giao diện được thiết kế dưới dạng bất đồng bộbầu cua, kết quả cuối cùng của giao diện sẽ được trả về cho người gọi thô

Tuy nhiên789 Club, giao diện callback không phải lúc nào cũng truyền kết quả cuối cùng. Thực tế, chúng ta có thể chia callback thành hai loại:

  • Callback giữa chừng
  • Callback kết quả

Và callback kết quả bao gồm callback kết quả thành công và callback kết quả thất bại.

Gọi lại giữa chừng có thể được kích hoạt khi nhiệm vụ bất đồng bộ bắt đầu thực hiện789 Club, khi có sự cập nhật về tiến độ thực thi, hoặc khi một sự kiện quan trọng khác xảy ra trong quá trình thực hiện. Trong khi đó, hàm gọi lại kết quả chỉ sẽ được kích hoạt khi nhiệm vụ bất đồng bộ hoàn tất và có một kết quả rõ ràng (thành công hoặc thất bại). Sự xuất hiện của hàm gọi lại kết quả đánh dấu sự kết thúc của lần thực thi giao diện bất đồng bộ này.

Phải có phản hồi kết quả789 Club,

Điểm khó ở đây chính là việc triển khai giao diện cần phải xử lý cẩn trọng mọi tình huống lỗi có thể xảy ra789 Club, và bất kể trường hợp nào xuất hiện, cũng phải đảm bảo trả về callback kết quả. Nếu không, rất có thể sẽ gây gián đoạn toàn bộ quy trình thực thi của bên gọi (caller). Thêm vào đó, việc bỏ qua các trường hợp lỗi không chỉ ảnh hưởng đến hiệu suất mà còn làm suy giảm tính ổn định của hệ thống.

Tập trung vào callback thất bại & mã lỗi nên được mô tả chi tiết nhất có thể

Trước tiên789 Club, hãy xem một đoạn mã ví dụ:

								
									
										public
									 interface
									 Downloader
									 {
									
    /** * Cài đặt trình lắng nghe. * @param listener Trình lắng nghe cần được thiết lập. */
    void
									 setListener
									(
									DownloadListener
									 listener
									);
									
    /** * Khởi động quá trình tải xuống tài nguyên. * @param url Địa chỉ nguồn tài nguyên cần tải xuống. * @param localPath Vị trí lưu trữ trên máy tính sau khi tải xuống. * Ngoài ratỉ lệ cược, bạn có thể tùy chỉnh tên tệp và cấu trúc thư mục để đảm bảo rằng tài nguyên được lưu ở đúng nơi. */
    void
									 startDownload
									(
									String
									 url
									,
									 String
									 localPath
									);
									
}
									

public
									 interface
									 DownloadListener
									 {
									
    /** * Hàm callback khi quá trình tải xuống hoàn tất. * @param result Kết quả của việc tải xuống. True nếu tải xuống thành công789 Club, false nếu thất bại. * @param url Địa chỉ nguồn tài nguyên được yêu cầu tải. * @param localPath Vị trí lưu trữ tài nguyên sau khi tải về. Chỉ có giá trị khi result=true. * Ngoài ra, trong trường hợp tải thành công, bạn có thể thêm các xử lý bổ sung như kiểm tra kích thước file hoặc thời gian tải để cải thiện hiệu suất ứng dụng. */
    void
									 downloadFinished
									(
									boolean
									 result
									,
									 String
									 url
									,
									 String
									 localPath
									);
									

    /** * Hàm callback để theo dõi tiến độ tải xuống. * @param url Địa chỉ nguồn tài nguyên. * @param downloadedSize Kích thước dữ liệu đã tải. * @param totalSize Kích thước tổng của tài nguyên. * Ngoài rabầu cua, bạn có thể thêm các thông tin khác như thời gian dự kiến hoàn thành hoặc trạng thái kết nối hiện tại. */
    void
									 downloadProgress
									(
									String
									 url
									,
									 long
									 downloadedSize
									,
									 long
									 totalSize
									);
									
}
									

								

Giao diện tải xuống này được thiết kế để thực hiện việc tải tài nguyên từ một URL cụ thể. Đây là một giao diện bất đồng bộ789 Club, trong đó người sử dụng sẽ khởi động nhiệm vụ tải xuống bằng cách gọi phương thức startDownload và sau đó chờ đợi sự kiện trả về thô Khi callback downloadFinished được kích hoạt, có nghĩa là nhiệm vụ tải xuống đã kết thúc. Nếu giá trị result trả về là true, điều đó cho thấy việc tải xuống đã thành công; ngược lại, nếu result là false, nhiệm vụ tải xuống đã thất bại. Ngoài ra, giao diện này cũng có thể cung cấp thêm các thông tin chi tiết về tiến trình tải xuống như tốc độ tải, dung lượng đã tải, hoặc thậm chí là lỗi cụ thể xảy ra trong quá trình tải. Điều này giúp người dùng có cái nhìn tổng quan hơn về trạng thái của nhiệm vụ tải xuống và dễ dàng khắc phục vấn đề nếu cần thiết.

Giao diện này về cơ bản đã khá hoàn chỉnhbầu cua, đủ để thực hiện quy trình tải xuống tài nguyên: chúng ta có thể khởi động một nhiệm vụ tải xuống thông qua giao diện này, trong quá trình tải xuống sẽ nhận được tiến độ tải (thông qua callback trung gian), khi tải xuống thành công sẽ nhận được kết quả, và khi tải xuống thất bại cũng sẽ nhận được thông báo (cả thành công lẫn thất bại đều thuộc dạng callback kết quả). Tuy nhiên, nếu chúng ta muốn biết thêm chi tiết về lý do thất bại trong trường hợp tải xuống không thành công, thì giao diện hiện tại không thể làm được điều đó.

không đủ dung lượng lưu trữ

tiết kiệm thời gian

Về ví dụ mã cho giao diện tải xuống phía trêntỉ lệ cược, để có thể trả về mã lỗi chi tiết hơn, mã code của DownloadListener đã được sửa đổi như sau:

								
									
										public
									 interface
									 DownloadListener
									 {
									
    /**
    public
									 static
									 final
									 int
									 SUCCESS
									 =
									 0
									;
									// Thành công
    public
									 static
									 final
									 int
									 INVALID_PARAMS
									 =
									 1
									;
									// Sai sót tham số đầu vào
    public
									 static
									 final
									 int
									 NETWORK_UNAVAILABLE
									 =
									 2
									;
									// Mạng không khả dụng
    public
									 static
									 final
									 int
									 UNKNOWN_HOST
									 =
									 3
									;
									// Tên miền không thể phân giải
    public
									 static
									 final
									 int
									 CONNECT_TIMEOUT
									 =
									 4
									;
									// Kết nối quá hạn
    public
									 static
									 final
									 int
									 HTTP_STATUS_NOT_OK
									 =
									 5
									;
									// Yêu cầu tải xuống trả về khác 200
    public
									 static
									 final
									 int
									 SDCARD_NOT_EXISTS
									 =
									 6
									;
									// Thẻ SD không tồn tại (không có nơi lưu trữ tài nguyên tải xuống)
    public
									 static
									 final
									 int
									 SD_CARD_NO_SPACE_LEFT
									 =
									 7
									;
									// Không đủ không gian trên thẻ SD (không có nơi lưu trữ tài nguyên tải xuống)
    public
									 static
									 final
									 int
									 READ_ONLY_FILE_SYSTEM
									 =
									 8
									;
									// Hệ thống tệp chỉ đọc (không có nơi lưu trữ tài nguyên tải xuống)
    public
									 static
									 final
									 int
									 LOCAL_IO_ERROR
									 =
									 9
									;
									// Lỗi liên quan đến việc lưu trữ thẻ SD cục bộ
    public
									 static
									 final
									 int
									 UNKNOWN_FAILED
									 =
									 10
									;
									// Lỗi không xác định khác

    /** * Khi tải xuống thành công sẽ thực hiệ * @param url Địa chỉ nguồn tài nguyên * @param localPath Vị trí lưu trữ tài nguyên sau khi tải xuống * Ngoài rabầu cua, bạn có thể thêm tùy chọn để kiểm tra kích thước file và so sánh với dữ liệu ban đầu để đảm bảo tính toàn vẹn của tệp tin tải về. */
    void
									 downloadSuccess
									(
									String
									 url
									,
									 String
									 localPath
									);
									
    /** * Khi tải xuống thất bạibầu cua, phương thức này sẽ được gọi. * @param url Địa chỉ tài nguyên mà bạn đang cố gắng tải xuống. * @param errorCode Mã lỗi giúp xác định cụ thể vấn đề xảy ra trong quá trình tải xuống. * @param errorMessage Mô tả ngắn gọn về lỗi để người dùng hoặc nhà phát triển hiểu rõ hơn về lý do gây ra sự cố. */
    void
									 downloadFailed
									(
									String
									 url
									,
									 int
									 errorCode
									,
									 String
									 errorMessage
									);
									

    /** * Hàm callback để theo dõi tiến độ tải xuống. * @param url Địa chỉ nguồn tài nguyên. * @param downloadedSize Kích thước dữ liệu đã tải. * @param totalSize Kích thước tổng của tài nguyên. * Ngoài ratỉ lệ cược, bạn có thể thêm các thông tin khác như thời gian dự kiến hoàn thành hoặc trạng thái kết nối hiện tại. */
    void
									 downloadProgress
									(
									String
									 url
									,
									 long
									 downloadedSize
									,
									 long
									 totalSize
									);
									
}
									

								

Trong iOSbầu cua, Foundation Framework cung cấp một cách tiếp cận có hệ thống để xử lý lỗi thông qua lớp NSError. Lớp này cho phép gói các mã lỗi theo cách rất linh hoạt và có thể phân chia chúng thành các domain khác nhau, giúp việc quản lý lỗi trở nên rõ ràng hơn. NSError là lựa chọn hoàn hảo khi định nghĩa các giao diện trả về lỗi, đặc biệt trong những trường hợp mà ứng dụng cần phải xử lý nhiều loại lỗi phức tạp hoặc đa dạng. Hơn nữa, NSError không chỉ đơn thuần là một công cụ báo cáo lỗi, mà còn hỗ trợ người lập trình thêm thông tin chi tiết như thông điệp mô tả lỗi hoặc hướng dẫn khắc phục sự cố. Điều này góp phần nâng cao trải nghiệm sử dụng API và giúp người dùng dễ dàng theo dõi vấn đề xảy ra trong quá trình vận hành ứng dụng.

Giao diện gọi và giao diện callback cần có mối quan hệ tương ứng rõ ràng

Chúng ta sẽ phân tích vấn đề này thông qua một ví dụ định nghĩa giao diện thực tế.

Bạn có thể tham khảo giao diện API của bảng điểm quảng cáo video từ một nền tảng quảng cáo trong nước (để dễ quan sáttỉ lệ cược, một số đoạn mã không liên quan đã được lược bỏ).

								
									
										@class
									 IndependentVideoManager
									;
									

@protocol
									 IndependentVideoManagerDelegate
									 <
									NSObject
									>
									
@optional
									
callback hiển thị quảng cáo video riêng lẻ
...
									

#pragma mark - Quản lý điểm callback Quản lý điểm là một phần quan trọng trong việc điều hành hệ thốngtỉ lệ cược, giúp duy trì tính minh bạch và hiệu quả trong việc phân phối cũng như ghi nhận điểm tích lũy của từng thành viên. Callback quản lý điểm sẽ đảm bảo rằng mọi hoạt động liên quan đến điểm số đều được theo dõi chặt chẽ và không có sai sót. Chức năng này không chỉ đơn giản là ghi nhận điểm mà còn giúp xác định chiến lược cải thiện hiệu suất cá nhân hay tập thể. Điều này đặc biệt hữu ích trong các tổ chức cần thường xuyên đánh giá và cập nhật kết quả làm việc của nhân viên hoặc các thành viên khác. Thông qua callback quản lý điểm, bạn có thể dễ dàng kiểm tra, sửa đổi hoặc thêm mới các thông tin liên quan đến điểm tích lũy, từ đó đưa ra quyết định chính xác hơn về việc thưởng hoặc phê bình dựa trên hiệu quả công việc thực tế.
...
									

Trạng thái gọi lại cho video độc lập (independent video status callback) trong hệ thống tích điểm (integral wall) có thể bao gồm nhiều tình huống khác nhau. Đầu tiênbầu cua, khi người dùng tương tác với quảng cáo video, hệ thống sẽ tự động cập nhật trạng thái để ghi nhận sự kiện này. Nếu quảng cáo được phát thành công và người dùng hoàn thành hành động như yêu cầu, hệ thống sẽ ghi nhận và cộng điểm tích lũy vào tài khoản của họ. Ngoài ra, nếu quảng cáo không thể tải hoặc phát do lỗi kỹ thuật, hệ thống cũng sẽ báo cáo trạng thái thất bại (failure status). Trong trường hợp này, bạn cần kiểm tra lại kết nối mạng hoặc liên hệ với nhà cung cấp dịch vụ để khắc phục sự cố. Để đảm bảo hoạt động trơn tru, hãy luôn theo dõi chặt chẽ các trạng thái từ callback. Điều này giúp bạn nhanh chóng phát hiện và giải quyết vấn đề, đồng thời tối ưu hóa trải nghiệm người dùng. Hãy nhớ rằng việc quản lý trạng thái một cách hiệu quả sẽ trực tiếp ảnh hưởng đến mức độ hài lòng của người dùng đối với nền tảng của bạn./** * Tường quảng cáo video có khả dụng hay không. * Được gọi sau khi nhận trạng thái bật/tắt video độc lập. * @param QuanLyVideoDoc lap * @param coTheSuDung */
-
									 (
									void
									)
									ivManager
									:(
									IndependentVideoManager
									 *
									)
									manager
									
didCheckEnableStatus
									:(
									BOOL
									)
									enable
									;
									

/** * Quảng cáo video độc lập có sẵn để phát không? * Được gọi sau khi kiểm tra xong quảng cáo video độc lập có sẵn hay không. * * @param QuanLyQuangCaoDocLap Quản lý quảng cáo độc lập * @param coSan Có sẵn (true/false) */
-
									 (
									void
									)
									ivManager
									:(
									IndependentVideoManager
									 *
									)
									manager
									
isIndependentVideoAvailable
									:(
									BOOL
									)
									available
									;
									


@end
									

@interface
									 IndependentVideoManager
									 :
									 NSObject
									 {
									
    
}
									

@property
									(
									nonatomic
									,
									assign
									)
									id
									<
									IndependentVideoManagerDelegate
									>
									delegate
									;
									

...
									

#pragma mark - Khởi tạo phương pháp liên quan đến khởi động
...
									

#pragma mark - phương pháp liên quan đến việc hiển thị tích điểm Bạn có thể dễ dàng sử dụng các phương pháp sau đây để quản lý và hiển thị tích điểm (wall of) một cách độc lập. Những kỹ thuật này sẽ giúp bạn tối ưu hóa trải nghiệm người dùng trong việc hiện thị và tương tác với hệ thống tích điểm. // Phương pháp khởi tạo hệ thống tích điểm: - Hàm thiết lập cấu hình ban đầu: ```func setupPointSystem(config: [String: Any]) -> Void``` - Thiết lập sự kiện tích điểm: ```func addEventForPointTrigger(eventID: Stringtỉ lệ cược, callback: @escaping () -> Void) -> Void``` // Phương thức cập nhật và hiển thị tích điểm: - Cập nhật số điểm: ```func updatePointCount(newPoints: Int) -> Void``` - Hiển thị thông báo tích điểm: ```func displayNotificationForEarnedPoints(pointsEarned: Int) -> Void``` // Tương tác với người dùng: - Xử lý hành động nhận thưởng: ```func handleUserActionToClaimReward() -> Void``` - Kiểm tra điều kiện mở khóa phần thưởng: ```func checkUnlockConditionForReward(rewardID: String) -> Bool``` Hãy chắc chắn rằng bạn đã kiểm tra toàn diện tất cả các phương thức trên để đảm bảo tính ổn định của hệ thống tích điểm khi triển khai vào ứng dụng./** * Dùng rootViewController của ứng dụng để hiển thị và bật lên bảng điểm tích lũy. * Hiển thị video độc lập theo phương thức mô hình hóa (ModelView) bằng cách sử dụng rootViewController của ứng dụng. * @param type Loại bảng điểm tích lũy */
-
									 (
									void
									)
									presentIndependentVideo
									;
									

...
									

Bạn có thể kiểm tra xem tường điểm video độc lập đã sẵn sàng sử dụng hay chưa bằng cách thực hiện các bước sau: Trước tiênbầu cua, hãy đảm bảo rằng hệ thống tường điểm video đang hoạt động ổn định và không gặp bất kỳ sự cố kỹ thuật nào. Tiếp theo, kiểm tra xem tất cả các kết nối mạng đều được thiết lập chính xác và có tốc độ tải xuống phù hợp để hỗ trợ việc phát video mượt mà. Sau đó, hãy chạy thử một đoạn video ngắn để đánh giá chất lượng hiển thị cũng như khả năng tích lũy điểm khi người dùng tương tác. Hãy ghi lại thời gian tải và hiệu suất của video để có cái nhìn tổng quan về hiệu quả hoạt động của hệ thống. Nếu mọi thứ đều diễn ra suôn sẻ, bạn có thể tự tin rằng tường điểm video độc lập đang ở trạng thái sẵn sàng để sử dụng. Nếu có bất kỳ vấn đề nào phát sinh trong quá trình kiểm tra, hãy nhanh chóng khắc phục chúng trước khi triển khai chính thức./** * Kiểm tra xem có quảng cáo video độc lập nào có thể phát không */
-
									 (
									void
									)
									checkVideoAvailable
									;
									

#pragma mark - Quản lý điểm tích lũy liên quan đến quảng cáo/** * Kiểm tra số điểm đã đạt đượcbầu cua, dù thành công hay thất bại đều sẽ gọi lại phương thức tương ứng trong đại lý (delegate). * */
-
									 (
									void
									)
									checkOwnedPoint
									;
									
/** * Sử dụng một số điểm cụ thể789 Club, sau khi thực hiện thành công hoặc thất bại, phương thức tương ứng trong delegate sẽ được gọi lại (hãy đặc biệt chú ý rằng kiểu tham số là unsigned int, điểm cần tiêu dùng phải là giá trị không âm). * * @param diem Số điểm muốn tiêu dùng */
-
									 (
									void
									)
									consumeWithPointNumber
									:(
									NSUInteger
									)
									point
									;
									

@end
									

								

Hãy cùng phân tích mối quan hệ tương ứng giữa giao diện gọi và giao diện callback trong phần định nghĩa giao diện này.

Với IndependentVideoManagerbầu cua, ngoài giao diện khởi tạo, còn có một số giao diện chính có thể được gọi như sau: 1. **Giao diện thiết lập cấu hình**: Đây là nơi bạn có thể cài đặt các thông số cơ bản như chất lượng video, độ phân giải, và tốc độ khung hình để tối ưu hóa hiệu suất. 2. **Giao diện điều khiển phát lại**: Cho phép bạn quản lý việc phát video, bao gồm việc bắt đầu, tạm dừng, tiếp tục hoặc dừng hoàn toàn quá trình phát. 3. **Giao diện xử lý sự cố**: Cung cấp các phương thức để theo dõi và khắc phục lỗi trong quá trình sử dụng, giúp đảm bảo trải nghiệm người dùng mượt mà hơn. 4. **Giao diện tương tác người dùng**: Cho phép tích hợp các tính năng nâng cao như điều chỉnh âm lượng, chọn phụ đề hoặc chuyển đổi giữa các nguồn video khác nhau. 5. **Giao diện cập nhật trạng thái**: Thường xuyên cập nhật tình trạng hiện tại của hệ thống, từ đó bạn có thể đưa ra phản hồi kịp thời nếu cần thiết. Tất cả những giao diện này kết hợp lại tạo nên một hệ thống mạnh mẽ cho việc quản lý và phát video một cách hiệu quả.

  • Hiển thị video độc lập (presentIndependentVideo)
  • Kiểm tra xem có video quảng cáo nào có thể phát không (checkVideoAvailable)
  • Quản lý điểm tích lũy (kiểm tra điểm đã sở hữu và sử dụng điểm với số lượng cụ thể: checkOwnedPoint và consumeWithPointNumber). Thêm một chút sáng tạo789 Club, bạn có thể hình dung đây như hệ thống phần thưởng trong một cửa hàng trực tuyến. Ví dụ, khi khách hàng muốn đổi điểm lấy sản phẩm, họ sẽ gọi hàm consumeWithPointNumber để xác nhận đủ điều kiện và trừ điểm khỏi tài khoản của mình. Còn checkOwnedPoint giúp họ kiểm tra xem mình còn bao nhiêu điểm tích lũy sẵn sàng để sử dụng. Hai hàm này đóng vai trò quan trọng trong việc duy trì sự minh bạch và thuận tiện cho người dùng trong quá trình giao dịch bằng điểm thưởng.

Giao diện (IndependentVideoManagerDelegate) có thể được phân loại thành các nhóm sau đây: 1. **Loại xử lý sự kiện cơ bản**: Bao gồm các phương thức để thông báo về trạng thái hiện tại của video như phátbầu cua, dừng, hoặc lỗi xảy ra trong quá trình tải. 2. **Xử lý tương tác người dùng nâng cao**: Gồm các phương thức cho phép phản hồi khi người dùng thực hiện thao tác như điều chỉnh âm lượng, di chuyển thanh tiến độ hoặc thay đổi chất lượng video. 3. **Phân tích số liệu và thống kê**: Cung cấp dữ liệu về thời gian phát, số lần xem và các hành vi liên quan đến nội dung video, giúp người quản trị dễ dàng theo dõi hiệu suất. 4. **Tùy chỉnh và cấu hình**: Cho phép người dùng tùy chỉnh các cài đặt riêng biệt trước hoặc trong quá trình chạy ứng dụng, chẳng hạn như chọn nguồn video hoặc định dạng phát trực tuyến. Các loại này đảm bảo rằng mọi chức năng đều hoạt động trơn tru và đáp ứng nhu cầu đa dạng của người dùng.

  • Lớp callback hiển thị video quảng cáo
  • Tình trạng của bức tường điểm tích lũy (bao gồm các hàm ivManager:didCheckEnableStatus: và ivManager:isIndependentVideoAvailable:) có thể được xem là những yếu tố quan trọng trong việc kiểm soát hoạt động của hệ thống. Những hàm này không chỉ giúp xác định xem tính năng đã được kích hoạt hay chưa mà còn đánh giá liệu có sẵn sàng để phát video độc lập hay không. Điều này đóng vai trò như một nền tảng kiểm soát chất lượngtỉ lệ cược, đảm bảo rằng mọi hoạt động đều diễn ra suôn sẻ và ổn định trong môi trường ứng dụng. Đồng thời, nó cũng cung cấp cho nhà phát triển cái nhìn sâu sắc về hiệu suất hoạt động, từ đó có thể tối ưu hóa hệ thống một cách tốt nhất.
  • Lớp quản lý điểm tích lũy

Tóm lạitỉ lệ cược, mối liên hệ giữa các phần ở đây khá rõ ràng, và ba loại giao diện callback này cơ bản đều có thể tương ứng một-một với ba phần giao diện gọi trước đó. Ngoài ra, việc sắp xếp này cũng giúp người dùng dễ dàng hơn trong việc xác định chức năng và mục đích của từng loại giao diện, từ đó tối ưu hóa quy trình làm việc hiệu quả hơn.

Tuy nhiêntỉ lệ cược, có một số chi tiết khá gây nhầm lẫn trong các callback của loại interface tường điểm tích lũy (reward wall) liên quan đến trạng thái. Khi người gọi thực hiện phương thức checkVideoAvailable, họ sẽ nhận được hai callback từ lớp quản lý tường điểm tích lũy (ivManager:didCheckEnableStatus: và ivManager:isIndependentVideoAvailable:). Về mặt lý thuyết, tên của phương thức checkVideoAvailable dường như chỉ để kiểm tra xem liệu có quảng cáo video nào sẵn sàng phát hay không. Do đó, việc chỉ cần có callback ivManager:isIndependentVideoAvailable: cũng có thể cung cấp kết quả mong muốn, mà không nhất thiết phải cần đến ivManager:didCheckEnableStatus:. Mặt khác, nếu nhìn vào ý nghĩa của callback ivManager:didCheckEnableStatus:, nó dường như nhằm xác định xem tường điểm tích lũy có khả dụng hay không, điều này khiến nó có vẻ như sẽ được kích hoạt bất kỳ khi nào một phương thức nào đó của lớp quản lý được gọi, thay vì chỉ liên quan đến việc kiểm tra trạng thái sau khi gọ Chính sự không rõ ràng trong mối tương quan giữa các callback và các phương thức gọi đã làm cho thiết kế của interface trở nên khó hiểu đối với người sử dụng.

Ngoài rabầu cua, giao diện IndependentVideoManager cũng gặp một số vấn đề trong việc thiết kế tham số ngữ cảnh, và vấn đề này sẽ được đề cập lại ở phần sau của bài viết.

Callback kết quả thành công và callback kết quả thất bại nên loại trừ lẫn nhau

Khi một tác vụ bất đồng bộ kết thúcbầu cua, nó sẽ hoặc gọi đến hàm xử lý kết quả thành công, hoặc gọi đến hàm xử lý kết quả thất bại. Chỉ một trong hai hàm này được thực thi, không thể nào cùng lúc xảy ra cả hai. Đây là yêu cầu hiển nhiên, nhưng nếu không cẩn thận trong quá trình triển khai, có thể sẽ dẫn đến vi phạm quy tắc này. Thực tế, việc đảm bảo sự loại trừ lẫn nhau giữa hai hàm này đôi khi không dễ dàng như tưởng tượng, đặc biệt khi các tác vụ phức tạp cần được quản lý trong môi trường đa luồng.

Giả sử giao diện Downloader mà chúng tôi đã đề cập trước đó trong phần cuối cùng tạo ra callback kết quả có mã như sau:

								
									int
									 errorCode
									 =
									 parseDownloadResult
									(
									result
									);
									
    if
									 (
									errorCode
									 ==
									 SUCCESS
									)
									 {
									
        listener
									.
									downloadSuccess
									(
									url
									,
									 localPath
									)
									
    }
									
    else
									 {
									
        listener
									.
									downloadFailed
									(
									url
									,
									 errorCode
									,
									 getErrorMessage
									(
									errorCode
									));
									
    }
									

								

phải có kết quả trả về

								
									try
									 {
									
        int
									 errorCode
									 =
									 parseDownloadResult
									(
									result
									);
									
        if
									 (
									errorCode
									 ==
									 SUCCESS
									)
									 {
									
            listener
									.
									downloadSuccess
									(
									url
									,
									 localPath
									)
									
        }
									
        else
									 {
									
            listener
									.
									downloadFailed
									(
									url
									,
									 errorCode
									,
									 getErrorMessage
									(
									errorCode
									));
									
        }
									
    }
									
    catch
									 (
									Exception
									 e
									)
									 {
									
        listener
									.
									downloadFailed
									(
									url
									,
									 UNKNOWN_FAILED
									,
									 getErrorMessage
									(
									UNKNOWN_FAILED
									));
									
    }
									

								

Mã đã được sửa đổi như vậy789 Club, đã đảm bảo rằng ngay cả khi xảy ra tình huống không ngờ tới, cũng có thể tạo ra một callback thất bại cho người gọi.

Tuy nhiên789 Club, điều này cũng dẫn đến một vấn đề khác: nếu khi thực hiện các phương thứ downloadSuccess hoặ downloadFailed, mã thực thi của giao diện callback phát sinh lỗi? Điều đó có thể dẫn đến việc gọi thêm một lầ Do đó, việc kích hoạt callback thành công và callback thất bại sẽ không còn được thực hiện một cách độc lập nữa: hoặc cả hai callback (thành công và thất bại) đều xảy ra, hoặc có hai lần callback thất bại liên tiếp. Điều này tạo ra một tình huống không mong muốn trong logic xử lý sự kiện, nơi mà kết quả cuối cùng có thể trở nên mâu thuẫn hoặc khó kiểm soát. Điều cần thiết là phải có cơ chế kiểm soát để đảm bảo rằng chỉ một trong hai callback (thành công hoặc thất bại) được thực thi đúng cách, tránh trường hợp chồng chéo hoặc lặp lại không đáng có.

Việc thực hiện giao diện callback là trách nhiệm của người gọi789 Club, nhưng liệu lỗi mà người gọi gây ra có phải là điều chúng ta cần quan tâm? Trước hết, đây vẫn nên là vấn đề mà người gọi ở tầng trên chịu trách nhiệm xử lý. Đơn vị thực hiện giao diện callback (người gọi) không nên ném lại lỗi cho người khác khi xảy ra sự cố. Tuy nhiên, nhà thiết kế giao diện phía dưới cũng cần cố gắng tối đa. Với vai trò là người thiết kế giao diện, thường thì chúng ta không thể đoán trước cách người gọi sẽ hành xử như thế nào. Nếu trong trường hợp xảy ra lỗi, chúng ta có thể đảm bảo rằng sai sót này không làm gián đoạn toàn bộ quy trình hoặc khiến chương trình bị treo, điều đó chẳng phải tốt hơn sao? Vì vậy, chúng ta có thể thử thay đổi mã nguồn theo cách sau: ```vietnamese try { // Thực thi các lệnh chính } catch (Exception e) { // Xử lý lỗi tại chỗ để tránh ném lên trê err.println("Lỗi đã xảy ra: " + e.getMessage()); } ``` Cách làm này giúp đảm bảo rằng ứng dụng vẫn hoạt động ổn định ngay cả khi một phần nào đó không hoàn hảo. Điều này cũng cho phép người gọi tập trung vào việc xử lý nghiệp vụ chính mà không lo lắng quá nhiều về các lỗi nhỏ lẻ.

								
									int
									 errorCode
									;
									
    try
									 {
									
        errorCode
									 =
									 parseDownloadResult
									(
									result
									);
									
    }
									
    catch
									 (
									Exception
									 e
									)
									 {
									
        errorCode
									 =
									 UNKNOWN_FAILED
									;
									
    }
									
    if
									 (
									errorCode
									 ==
									 SUCCESS
									)
									 {
									
        try
									 {
									
            listener
									.
									downloadSuccess
									(
									url
									,
									 localPath
									)
									
        }
									
        catch
									 (
									Throwable
									 e
									)
									 {
									
            e
									.
									printStackTrace
									();
									
        }
									
    }
									
    else
									 {
									
        try
									 {
									
            listener
									.
									downloadFailed
									(
									url
									,
									 errorCode
									,
									 getErrorMessage
									(
									errorCode
									));
									
        }
									
        catch
									 (
									Throwable
									 e
									)
									 {
									
            e
									.
									printStackTrace
									();
									
        }
									
    }
									

								

Mã callback phức tạp hơn một chúttỉ lệ cược, nhưng cũng an toàn hơn.

Mô hình luồng làm việc của callback

Cơ sở kỹ thuật để thực hiện giao diện bất đồng bộ chủ yếu có hai điều:

  • Đa luồng (mã thực hiện giao diện ở một luồng bất đồng bộ khác với luồng gọi)
  • Bạn có thể sử dụng I/O bất đồng bộ (như yêu cầu mạng bất đồng bộ). Trong trường hợp nàybầu cua, ngay cả khi toàn bộ chương trình chỉ có một luồng duy nhất, bạn vẫn có thể tạo ra giao diện bất đồng bộ. Điều này giúp cải thiện hiệu suất tổng thể của ứng dụng bằng cách cho phép các tác vụ chờ đợi (như truy vấn mạng) không bị chặn, từ đó giải phóng tài nguyên để thực hiện các tác vụ khác trong khi chờ đợi kết quả.

Dù là trường hợp nàobầu cua, chúng ta đều cần có định nghĩa rõ ràng về môi trường luồng khi callback xảy ra.

Về cơ bảnbầu cua, có ba chế độ chính để xác định môi trường luồng thực hiện callback kết quả:

  1. Gọi giao diện trên luồng nào thì callback kết quả cũng xảy ra trên luồng đó.
  2. Dù bạn gọi giao diện từ luồng nàobầu cua, kết quả trả về sẽ luôn được xử lý trên luồng chính (chẳng hạn như trong Android khi sử dụng AsyncTask). Điều này giúp đảm bảo rằng các tác vụ liên quan đến giao diện người dùng luôn được thực hiện trên luồng chính để tránh lỗi và đảm bảo tính ổn định của ứng dụng.
  3. Người gọi có thể tùy chỉnh luồng mà giao diện sẽ được thực hiện (ví dụ như trong iOS789 Club, với NSURLConnection, bạn có thể thiết lập luồng chạy của callback thông qua phương thức scheduleInRunLoop:forMode:, cho phép xác định chính xác nơi và thời điểm callback sẽ xảy ra). Điều này mang lại sự linh hoạt cao khi xử lý các tác vụ nền, giúp ứng dụng hoạt động mượt mà hơn trong nhiều tình huống đa nhiệm.

Rõ ràng chế độ thứ ba linh hoạt nhất vì nó bao gồm cả hai chế độ trước.

Để có thể điều phối mã thực thi sang các luồng khácbầu cua, chúng ta cần sử dụng phần tiếp theo trong loạt bài viết trước Xử lý bất đồng bộ trong Android và iOS (phần 1) – Tổng quan Cuối cùngtỉ lệ cược, một số công nghệ được đề cập, chẳng hạn như GCD và NSOperationQueue trong iOS, cũng như phương thức performSelectorXXX, hay các lớp ExecutorService, AsyncTask và Handler trong Android (lưu ý rằng ExecutorService không thể được sử dụng để điều độ lên luồng chính mà chỉ có thể điều độ lên các luồng bất đồng bộ). Chúng ta cần hiểu rõ bản chất của việc điều độ luồng: điều kiện tiên quyết để có thể điều độ một đoạn mã đến một luồng cụ thể là luồng đó phải có mộ Tên gọi của nó cho thấy đây là một vòng lặp liên tục lấy các thông điệp từ hàng đợi thông điệp và xử lý chúng. Khi chúng ta thực hiện việc điều độ luồng, về cơ bản là đang gửi các thông điệp vào hàng đợi này. Hàng đợi này đã được hệ thống đảm bảo là an toàn cho luồng (Thread Safe Queue), do đó người dùng sẽ tránh được các vấn đề về tính an toàn của luồng. Trong phát triển ứng dụng khách, hệ thống luôn tự động tạo ra một Event Loop cho luồng chính, nhưng với các luồng khác, nhà phát triển cần phải áp dụng các kỹ thuật phù hợp để tạo ra chúng. Điều thú vị là, mặc dù Android cung cấp nhiều công cụ khác nhau như Handler và AsyncTask để giúp quản lý luồng, nhưng cách tiếp cận tối ưu nhất vẫn là tận dụng các tiện ích có sẵn từ Java như ExecutorService để quản lý hiệu quả các tác vụ chạ Điều này đặc biệt quan trọng khi bạn cần xử lý dữ liệu lớn hoặc thực hiện các tác vụ phức tạp mà không làm gián đoạn trải nghiệm người dùng. Điều độ luồng đúng cách không chỉ cải thiện hiệu suất mà còn giúp duy trì trạng thái ổn định của ứng dụng, giảm thiểu nguy cơ xảy ra lỗi hoặc treo phần mềm.

Trong hầu hết các trường hợp lập trình trên client789 Club, chúng ta thường mong muốn callback kết quả xảy ra trên luồng chính (main thread), vì đây là thời điểm thích hợp để cập nhật giao diện người dùng (UI). Còn việc callback giữa chừng sẽ chạy trên luồng nào phụ thuộc vào ngữ cảnh cụ thể của ứng dụng. Trong ví dụ về Downloader trước đó, callback downloadProgress được sử dụng để báo cáo tiến độ tải xuống. Thông thường, tiến độ tải xuống cũng cần được hiển thị trên giao diện người dùng, do đó việc đưa callback downloadProgress chạy trên luồng chính sẽ là lựa chọn tối ưu hơn. Ngoài ra, khi làm việc với các ứng dụng phức tạp, việc quản lý luồng và đồng bộ dữ liệu có ý nghĩa quan trọng. Nếu không cẩn thận, việc chạy các tác vụ không đúng luồng có thể dẫn đến lỗi hoặc hiệu suất kém. Do đó, trong nhiều trường hợp, chúng ta cần thiết lập một cơ chế kiểm soát rõ ràng để đảm bảo rằng tất cả các hoạt động liên quan đến UI đều diễn ra trên luồng chính, còn các tác vụ nặng nhọc như tải dữ liệu có thể được thực hiện trên các luồng phụ để tránh ảnh hưởng đến trải nghiệm người dùng.

Tham số ngữ cảnh callback (thông số truyền thẳng)

Khi gọi một giao diện bất đồng bộ789 Club, chúng ta thường cần tạm thời lưu giữ một phần dữ liệu ngữ cảnh liên quan đến lần gọi này, để khi nhiệm vụ bất đồng bộ hoàn thành và callback được kích hoạt, chúng ta có thể lấy lại dữ liệu ngữ cảnh đó một cách dễ dàng. Trong thực tế phát triển, việc quản lý ngữ cảnh trong các tác vụ bất đồng bộ là vô cùng quan trọng. Một số lập trình viên chọn sử dụng đối tượng đặc biệt để lưu trữ thông tin ngữ cảnh, chẳng hạn như ID yêu cầu hoặc thông tin người dùng, nhằm đảm bảo rằng mọi thao tác trong quá trình xử lý đều có thể truy cập dữ liệu cần thiết mà không bị mất mát giữa các giai đoạn. Điều này giúp duy trì tính nhất quán và tránh những lỗi khó phát hiện liên quan đến trạng thái của ứng dụng.

Hãy tiếp tục sử dụng công cụ tải xuống mà chúng ta đã đề cập trước đó làm ví dụ. Để có thể phân tích rõ ràng từng trường hợptỉ lệ cược, ở đây chúng ta sẽ đặt ra một tình huống phức tạp hơn chút ít. Giả sử rằng chúng ta cần tải xuống nhiều bộ sưu tập sticker, mỗi bộ bao gồm nhiều hình ảnh sticker khác nhau. Sau khi tải xong tất cả các hình ảnh sticker, chúng ta cần cài đặt những bộ sưu tập này vào máy tính cá nhân (có thể liên quan đến việc thay đổi cơ sở dữ liệu hiện có), nhằm giúp người dùng có thể dễ dàng sử dụng chúng trong giao diện nhập liệu. Ngoài ra, chúng ta cũng cần lưu ý rằng quá trình cài đặt không chỉ đơn giản là thêm dữ liệu mới vào hệ thống, mà còn phải đảm bảo rằng không xảy ra xung đột với các bộ sưu tập sticker đã tồn tại trước đó. Điều này đòi hỏi một quy trình kiểm tra chặt chẽ để tránh làm gián đoạn trải nghiệm của người dùng.

Giả sử cấu trúc dữ liệu gói biểu tượng được định nghĩa như sau:

								
									
										public
									 class
									 EmojiPackage
									 {
									
    /**
    public
									 long
									 emojiId
									;
									
    /**
    public
									 List
									<
									String
									>
									 emojiUrls
									;
									
}
									

								

Trong quá trình tải xuốngbầu cua, chúng ta cần lưu trữ một cấu trúc ngữ cảnh như sau:

								
									
										public
									 class
									 EmojiDownloadContext
									 {
									
    /**
    public
									 EmojiPackage
									 emojiPackage
									;
									
    /**
    public
									 int
									 downloadedEmoji
									;
									
    /**
    public
									 List
									<
									String
									>
									 localPathList
									 =
									 new
									 ArrayList
									<
									String
									>();
									
}
									

								

Giả sử bộ tải xuống biểu tượng mà chúng ta cần thực hiện tuân thủ định nghĩa giao diện sau đây:

								
									
										public
									 interface
									 EmojiDownloader
									 {
									
    /** * Khởi động quá trình tải xuống gói cụ thể * @param boUnhoHinhAnh */
    void
									 startDownloadEmoji
									(
									EmojiPackage
									 emojiPackage
									);
									

    /** * Tại đây789 Club, chúng ta định nghĩa các giao diện liên quan đến việc gọi trả về, nhưng điều này không phải là trọng tâm trong cuộc thảo luận của chúng ta. */
    //TODO: Định nghĩa giao diện callback
}
									

								

Nếu sử dụng giao diện Downloader đã có sẵn để thực hiện việc tải xuống bộ sưu tập stickerbầu cua, tùy thuộc vào cách truyền bối cảnh (context), chúng ta có thể áp dụng ba phương pháp khác nhau:

(1) Lưu trữ toàn cục một ngữ cảnh.

Lưu ý: "Toàn cục" ở đây được hiểu là đối với bên trong một bộ tải xuống biểu tượng. Mã như sau:

								
									
										public
									 class
									 MyEmojiDownloader
									 implements
									 EmojiDownloader
									,
									 DownloadListener
									 {
									
    /**
    private
									 EmojiDownloadContext
									 downloadContext
									;
									
    private
									 Downloader
									 downloader
									;
									

    public
									 MyEmojiDownloader
									()
									 {
									
        Bạn có thể khởi tạo một đối tượng tải xuống. MyDownloader là một phiên bản cụ thể của giao diện Downloaderbầu cua, được thiết kế để thực hiện các chức năng tải dữ liệu theo cách riêng của nó.
        downloader
									 =
									 new
									 MyDownloader
									();
									
        downloader
									.
									setListener
									(
									this
									);
									
    }
									

    @Override
									
    public
									 void
									 startDownloadEmoji
									(
									EmojiPackage
									 emojiPackage
									)
									 {
									
        if
									 (
									downloadContext
									 ==
									 null
									)
									 {
									
            // Tạo dữ liệu ngữ cảnh tải xuống
            downloadContext
									 =
									 new
									 EmojiDownloadContext
									();
									
            downloadContext
									.
									emojiPackage
									 =
									 emojiPackage
									;
									
            // Bắt đầu tải xuống tệp hình ảnh biểu tượng thứ 0
            downloader
									.
									startDownload
									(
									emojiPackage
									.
									emojiUrls
									.
									get
									(
									0
									),
									
                    getLocalPathForEmoji
									(
									emojiPackage
									,
									 0
									));
									
        }
									
    }
									

    @Override
									
    public
									 void
									 downloadSuccess
									(
									String
									 url
									,
									 String
									 localPath
									)
									 {
									
        downloadContext
									.
									localPathList
									.
									add
									(
									localPath
									);
									
        downloadContext
									.
									downloadedEmoji
									++;
									
        EmojiPackage
									 emojiPackage
									 =
									 downloadContext
									.
									emojiPackage
									;
									
        if
									 (
									downloadContext
									.
									downloadedEmoji
									 <
									 emojiPackage
									.
									emojiUrls
									.
									size
									())
									 {
									
            // Chưa tải xuống xong789 Club, tiếp tục tải xuống tệp hình ảnh biểu tượng tiếp theo
            String
									 nextUrl
									 =
									 emojiPackage
									.
									emojiUrls
									.
									get
									(
									downloadContext
									.
									downloadedEmoji
									);
									
            downloader
									.
									startDownload
									(
									nextUrl
									,
									
                    getLocalPathForEmoji
									(
									emojiPackage
									,
									 downloadContext
									.
									downloadedEmoji
									));
									
        }
									
        else
									 {
									
            // Đã tải xuống xong
            installEmojiPackageLocally
									(
									emojiPackage
									,
									 downloadContext
									.
									localPathList
									);
									
            downloadContext
									 =
									 null
									;
									
        }
									
    }
									

    @Override
									
    public
									 void
									 downloadFailed
									(
									String
									 url
									,
									 int
									 errorCode
									,
									 String
									 errorMessage
									)
									 {
									
        ...
									
    }
									

    @Override
									
    public
									 void
									 downloadProgress
									(
									String
									 url
									,
									 long
									 downloadedSize
									,
									 long
									 totalSize
									)
									 {
									
        ...
									
    }
									

    /**
    private
									 String
									 getLocalPathForEmoji
									(
									EmojiPackage
									 emojiPackage
									,
									 int
									 i
									)
									 {
									
        ...
									
    }
									

    /**
    private
									 void
									 installEmojiPackageLocally
									(
									EmojiPackage
									 emojiPackage
									,
									 List
									<
									String
									>
									 localPathList
									)
									 {
									
        ...
									
    }
									
}
									

								

Hạn chế của cách làm này nằm ở chỗ chỉ có thể tải xuống một sticker tại một thời điểm. Bạn phải đợi quá trình tải xuống sticker trước đó hoàn tất thì mới có thể bắt đầu tải xuống sticker tiếp theo. Điều này đôi khi khiến người dùng cảm thấy bất tiện789 Club, đặc biệt là khi họ cần tải nhiều sticker cùng lúc.

lưu toàn cục một bản ngữ cảnh

(2) Sử dụng mối quan hệ ánh xạ để lưu trữ ngữ cảnh.

Dựa trên định nghĩa hiện tại của giao diện Downloader789 Club, chúng ta chỉ có thể sử dụng URL làm khóa để ánh xạ trong cấu trúc này. Tuy nhiên, vì một bộ sticker thường bao gồm nhiều URL khác nhau, chúng ta buộc phải tạo ra một bản ghi ngữ cảnh riêng cho từng URL. Dưới đây là đoạn mã tương ứng: ```python class Downloader: def __init__(self): context_mapping = {} def add_context(self, url, context): context_mapping: context_mapping[url] = [] context_mapping[url].append(context) def get_context(self, url): context_mapping.get(url, []) ``` Trong ví dụ trên, mỗi khi thêm ngữ cảnh mới vào một URL cụ thể, chúng ta sẽ lưu trữ tất cả các ngữ cảnh liên quan đến URL đó trong danh sách `context_mapping`. Điều này đảm bảo rằng ngay cả khi một bộ sticker có nhiều URL, chúng vẫn có thể được quản lý và truy xuất dễ dàng.

								
									
										public
									 class
									 MyEmojiDownloader
									 implements
									 EmojiDownloader
									,
									 DownloadListener
									 {
									
    /** * Lưu giữ mối quan hệ ánh xạ giữa ngữ cảnh và các URL. * Mỗi URL sẽ liên kết đến một đối tượng ngữ cảnh tải xuống Emoji. */
    private
									 Map
									<
									String
									,
									 EmojiDownloadContext
									>
									 downloadContextMap
									;
									
    private
									 Downloader
									 downloader
									;
									

    public
									 MyEmojiDownloader
									()
									 {
									
        downloadContextMap
									 =
									 new
									 HashMap
									<
									String
									,
									 EmojiDownloadContext
									>();
									
        Bạn có thể khởi tạo một đối tượng tải xuống. MyDownloader là một phiên bản cụ thể của giao diện Downloaderbầu cua, được thiết kế để thực hiện các chức năng tải dữ liệu theo cách riêng của nó.
        downloader
									 =
									 new
									 MyDownloader
									();
									
        downloader
									.
									setListener
									(
									this
									);
									
    }
									

    @Override
									
    public
									 void
									 startDownloadEmoji
									(
									EmojiPackage
									 emojiPackage
									)
									 {
									
        // Tạo dữ liệu ngữ cảnh tải xuống
        EmojiDownloadContext
									 downloadContext
									 =
									 new
									 EmojiDownloadContext
									();
									
        downloadContext
									.
									emojiPackage
									 =
									 emojiPackage
									;
									
        // Tạo mối quan hệ ánh xạ cho mỗi URL
        for
									 (
									String
									 emojiUrl
									 :
									 emojiPackage
									.
									emojiUrls
									)
									 {
									
            downloadContextMap
									.
									put
									(
									emojiUrl
									,
									 downloadContext
									);
									
        }
									
        // Bắt đầu tải xuống tệp hình ảnh biểu tượng thứ 0
        downloader
									.
									startDownload
									(
									emojiPackage
									.
									emojiUrls
									.
									get
									(
									0
									),
									
                getLocalPathForEmoji
									(
									emojiPackage
									,
									 0
									));
									
    }
									

    @Override
									
    public
									 void
									 downloadSuccess
									(
									String
									 url
									,
									 String
									 localPath
									)
									 {
									
        EmojiDownloadContext
									 downloadContext
									 =
									 downloadContextMap
									.
									get
									(
									url
									);
									
        downloadContext
									.
									localPathList
									.
									add
									(
									localPath
									);
									
        downloadContext
									.
									downloadedEmoji
									++;
									
        EmojiPackage
									 emojiPackage
									 =
									 downloadContext
									.
									emojiPackage
									;
									
        if
									 (
									downloadContext
									.
									downloadedEmoji
									 <
									 emojiPackage
									.
									emojiUrls
									.
									size
									())
									 {
									
            // Chưa tải xuống xongbầu cua, tiếp tục tải xuống tệp hình ảnh biểu tượng tiếp theo
            String
									 nextUrl
									 =
									 emojiPackage
									.
									emojiUrls
									.
									get
									(
									downloadContext
									.
									downloadedEmoji
									);
									
            downloader
									.
									startDownload
									(
									nextUrl
									,
									
                    getLocalPathForEmoji
									(
									emojiPackage
									,
									 downloadContext
									.
									downloadedEmoji
									));
									
        }
									
        else
									 {
									
            // Đã tải xuống xong
            installEmojiPackageLocally
									(
									emojiPackage
									,
									 downloadContext
									.
									localPathList
									);
									
            // Xóa mối quan hệ ánh xạ cho mỗi URL
            for
									 (
									String
									 emojiUrl
									 :
									 emojiPackage
									.
									emojiUrls
									)
									 {
									
                downloadContextMap
									.
									remove
									(
									emojiUrl
									);
									
            }
									
        }
									
    }
									

    @Override
									
    public
									 void
									 downloadFailed
									(
									String
									 url
									,
									 int
									 errorCode
									,
									 String
									 errorMessage
									)
									 {
									
        ...
									
    }
									

    @Override
									
    public
									 void
									 downloadProgress
									(
									String
									 url
									,
									 long
									 downloadedSize
									,
									 long
									 totalSize
									)
									 {
									
        ...
									
    }
									

    /**
    private
									 String
									 getLocalPathForEmoji
									(
									EmojiPackage
									 emojiPackage
									,
									 int
									 i
									)
									 {
									
        ...
									
    }
									

    /**
    private
									 void
									 installEmojiPackageLocally
									(
									EmojiPackage
									 emojiPackage
									,
									 List
									<
									String
									>
									 localPathList
									)
									 {
									
        ...
									
    }
									
}
									

								

Phương pháp này cũng có nhược điểm: không phải lúc nào cũng có thể tìm được một biến duy nhất để tham chiếu đến dữ liệu ngữ cảnh. Trong ví dụ về công cụ tải xuống sticker nàybầu cua, biến duy nhất được kỳ vọng để xác định việc tải xuống là emojiId, nhưng giá trị này lại không thể truy xuất được trong giao diện callback của lớ Do đó, cách giải quyết thay thế là tạo một bản đồ riêng cho từng URL để ánh xạ đến dữ liệu ngữ cảnh. Tuy nhiên, điều này dẫn đến hậu quả là nếu hai bộ sticker khác nhau có cùng một URL, sẽ xảy ra xung đột. Ngoài ra, cách làm này còn khá phức tạp khi triển khai. Mặt khác, việc duy trì một bản đồ URL riêng biệt cho mỗi sticker cũng làm tăng thêm gánh nặng cho hệ thống, đặc biệt khi số lượng sticker lớn. Điều này không chỉ ảnh hưởng đến hiệu suất mà còn khiến việc quản lý dữ liệu trở nên khó khăn hơn. Vì vậy, cần cân nhắc kỹ lưỡng giữa tính linh hoạt và độ phức tạp khi thiết kế hệ thống tải xuống này.

(3) Tạo một instance giao diện cho mỗi nhiệm vụ bất đồng bộ.

Tác vụ {task_id} đã được bắt đầu với dữ liệu ngữ cảnh: {self.context_data[task_id]}

								
									
										public
									 class
									 MyEmojiDownloader
									 implements
									 EmojiDownloader
									 {
									
    @Override
									
    public
									 void
									 startDownloadEmoji
									(
									EmojiPackage
									 emojiPackage
									)
									 {
									
        // Tạo dữ liệu ngữ cảnh tải xuống
        EmojiDownloadContext
									 downloadContext
									 =
									 new
									 EmojiDownloadContext
									();
									
        downloadContext
									.
									emojiPackage
									 =
									 emojiPackage
									;
									
        // Tạo một Downloader mới cho mỗi lần tải xuống
        final
									 EmojiUrlDownloader
									 downloader
									 =
									 new
									 EmojiUrlDownloader
									();
									
        // Lưu dữ liệu ngữ cảnh vào instance Downloader
        downloader
									.
									downloadContext
									 =
									 downloadContext
									;
									

        downloader
									.
									setListener
									(
									new
									 DownloadListener
									()
									 {
									
            @Override
									
            public
									 void
									 downloadSuccess
									(
									String
									 url
									,
									 String
									 localPath
									)
									 {
									
                EmojiDownloadContext
									 downloadContext
									 =
									 downloader
									.
									downloadContext
									;
									
                downloadContext
									.
									localPathList
									.
									add
									(
									localPath
									);
									
                downloadContext
									.
									downloadedEmoji
									++;
									
                EmojiPackage
									 emojiPackage
									 =
									 downloadContext
									.
									emojiPackage
									;
									
                if
									 (
									downloadContext
									.
									downloadedEmoji
									 <
									 emojiPackage
									.
									emojiUrls
									.
									size
									())
									 {
									
                    // Chưa tải xuống xongbầu cua, tiếp tục tải xuống tệp hình ảnh biểu tượng tiếp theo
                    String
									 nextUrl
									 =
									 emojiPackage
									.
									emojiUrls
									.
									get
									(
									downloadContext
									.
									downloadedEmoji
									);
									
                    downloader
									.
									startDownload
									(
									nextUrl
									,
									
                            getLocalPathForEmoji
									(
									emojiPackage
									,
									 downloadContext
									.
									downloadedEmoji
									));
									
                }
									
                else
									 {
									
                    // Đã tải xuống xong
                    installEmojiPackageLocally
									(
									emojiPackage
									,
									 downloadContext
									.
									localPathList
									);
									
                }
									
            }
									

            @Override
									
            public
									 void
									 downloadFailed
									(
									String
									 url
									,
									 int
									 errorCode
									,
									 String
									 errorMessage
									)
									 {
									
                //TODO:
									
            }
									

            @Override
									
            public
									 void
									 downloadProgress
									(
									String
									 url
									,
									 long
									 downloadedSize
									,
									 long
									 totalSize
									)
									 {
									
                //TODO:
									
            }
									
        });
									

        // Bắt đầu tải xuống tệp hình ảnh biểu tượng thứ 0
        downloader
									.
									startDownload
									(
									emojiPackage
									.
									emojiUrls
									.
									get
									(
									0
									),
									
                getLocalPathForEmoji
									(
									emojiPackage
									,
									 0
									));
									
    }
									

    private
									 static
									 class
									 EmojiUrlDownloader
									 extends
									 MyDownloader
									 {
									
        public
									 EmojiDownloadContext
									 downloadContext
									;
									
    }
									

    /**
    private
									 String
									 getLocalPathForEmoji
									(
									EmojiPackage
									 emojiPackage
									,
									 int
									 i
									)
									 {
									
        ...
									
    }
									

    /**
    private
									 void
									 installEmojiPackageLocally
									(
									EmojiPackage
									 emojiPackage
									,
									 List
									<
									String
									>
									 localPathList
									)
									 {
									
        ...
									
    }
									
}
									

								

Việc làm này chắc chắn có những nhược điểm rõ rệt: việc tạo ra một instance của trình tải về cho mỗi nhiệm vụ tải xuống sẽ đi ngược lại mục đích ban đầu của giao diệ Điều này dẫn đến việc tạo ra rất nhiều instance dư thừa. Đặc biệtbầu cua, khi instance của giao diện là một đối tượng nặng với dung lượng lớn, cách làm này sẽ gây ra rất nhiều chi phí không cần thiết. Một giao diện như vậy thường được thiết kế để tối ưu hóa tài nguyên và việc lạm dụng việc tạo instance sẽ phá vỡ hoàn toàn mục tiêu đó.

Execution context

  • ngữ cảnh (context)
  • tham số truyền thẳng
  • callbackData
  • cookie
  • userInfo

Dù tên của tham số này là gìbầu cua, vai trò của nó vẫn không thay đổi: khi gọi một giao diện bất đồng bộ, tham số này sẽ được truyền vào và sau đó cũng có thể được trả về khi giao diện callback được kích hoạt. Tham số ngữ cảnh này được định nghĩa bởi người gọi phía trên, còn phần thực hiện ở tầng dưới chỉ cần lo việc truyền tiếp mà không cần hiểu ý nghĩa thực sự của nó. Ngoài ra, việc sử dụng tham số ngữ cảnh này giúp tăng tính linh hoạt trong thiết kế hệ thống. Khi các module hoặc dịch vụ khác nhau tương tác với nhau, tham số ngữ cảnh có thể mang theo thông tin bổ sung như trạng thái hoặc dữ liệu tạm thời mà không làm ảnh hưởng đến logic thực thi bên trong giao diện. Điều này cho phép các tầng thấp tập trung vào việc xử lý nghiệp vụ chính mà không phải lo lắng về cách quản lý ngữ cảnh cụ thể.

Giao diện Downloader hỗ trợ tham số ngữ cảnh đã được thay đổi như sau:

								
									
										public
									 interface
									 Downloader
									 {
									
    /** * Thiết lập trình lắng nghe sự kiện. * @param listener Trình lắng nghe cần được thiết lập. */
    void
									 setListener
									(
									DownloadListener
									 listener
									);
									
    /** * Khởi động quá trình tải xuống tài nguyên. * @param url Địa chỉ của tài nguyên cần tải về. * @param localPath Vị trí lưu trữ trên thiết bị sau khi tải xong. * @param contextData Dữ liệu ngữ cảnhbầu cua, sẽ được truyền trở lại qua giao diệ Có thể là bất kỳ loại nào. */
    void
									 startDownload
									(
									String
									 url
									,
									 String
									 localPath
									,
									 Object
									 contextData
									);
									
}
									
public
									 interface
									 DownloadListener
									 {
									
    /**
    public
									 static
									 final
									 int
									 SUCCESS
									 =
									 0
									;
									// Thành công
    public
									 static
									 final
									 int
									 INVALID_PARAMS
									 =
									 1
									;
									// Sai sót tham số đầu vào
    public
									 static
									 final
									 int
									 NETWORK_UNAVAILABLE
									 =
									 2
									;
									// Mạng không khả dụng
    public
									 static
									 final
									 int
									 UNKNOWN_HOST
									 =
									 3
									;
									// Tên miền không thể phân giải
    public
									 static
									 final
									 int
									 CONNECT_TIMEOUT
									 =
									 4
									;
									// Kết nối quá hạn
    public
									 static
									 final
									 int
									 HTTP_STATUS_NOT_OK
									 =
									 5
									;
									// Yêu cầu tải xuống trả về khác 200
    public
									 static
									 final
									 int
									 SDCARD_NOT_EXISTS
									 =
									 6
									;
									// Thẻ SD không tồn tại (không có nơi lưu trữ tài nguyên tải xuống)
    public
									 static
									 final
									 int
									 SD_CARD_NO_SPACE_LEFT
									 =
									 7
									;
									// Không đủ không gian trên thẻ SD (không có nơi lưu trữ tài nguyên tải xuống)
    public
									 static
									 final
									 int
									 READ_ONLY_FILE_SYSTEM
									 =
									 8
									;
									// Hệ thống tệp chỉ đọc (không có nơi lưu trữ tài nguyên tải xuống)
    public
									 static
									 final
									 int
									 LOCAL_IO_ERROR
									 =
									 9
									;
									// Lỗi liên quan đến việc lưu trữ thẻ SD cục bộ
    public
									 static
									 final
									 int
									 UNKNOWN_FAILED
									 =
									 10
									;
									// Lỗi không xác định khác

    /** * Hàm trả về thông báo khi tải xuống thành công. * @param linkTaiNguyen Đường dẫn đến tài nguyên cần tải. * @param duongDanLuu Vị trí lưu trữ tài nguyên sau khi tải về. * @param thongTinBốiCảnh Dữ liệu ngữ cảnh liên quan. */
    void
									 downloadSuccess
									(
									String
									 url
									,
									 String
									 localPath
									,
									 Object
									 contextData
									);
									
    /** * Hàm được gọi khi việc tải xuống thất bại. * @param url Đường dẫn tài nguyên * @param errorCode Mã lỗi * @param errorMessage Mô tả ngắn gọn về lỗi giúp người dùng hiểu rõ lý do thất bại * @param contextData Dữ liệu ngữ cảnh bổ sung * @note Trong trường hợp tải xuống không thành côngbầu cua, hàm này sẽ cung cấp thông tin chi tiết để xử lý */
    void
									 downloadFailed
									(
									String
									 url
									,
									 int
									 errorCode
									,
									 String
									 errorMessage
									,
									 Object
									 contextData
									);
									

    /** * Hàm callback để theo dõi tiến độ tải xuống. * @param url Địa chỉ tài nguyên cần tải. * @param downloadedSize Kích thước dữ liệu đã tải được tính đến thời điểm hiện tại. * @param totalSize Tổng kích thước của tài nguyên cần tải. * @param contextData Dữ liệu ngữ cảnh liên quan đến quá trình tải. */
    void
									 downloadProgress
									(
									String
									 url
									,
									 long
									 downloadedSize
									,
									 long
									 totalSize
									,
									 Object
									 contextData
									);
									
}
									

								

Sử dụng giao diện Downloader mới nhất nàybầu cua, bộ tải xuống biểu tượng trước đó đã có cách triển khai thứ tư.

(4) Sử dụng giao diện bất đồng bộ hỗ trợ truyền ngữ cảnh.

Mã như sau:

								
									
										public
									 class
									 MyEmojiDownloader
									 implements
									 EmojiDownloader
									,
									 DownloadListener
									 {
									
    private
									 Downloader
									 downloader
									;
									

    public
									 MyEmojiDownloader
									()
									 {
									
        Bạn có thể khởi tạo một đối tượng tải xuống. MyDownloader là một phiên bản cụ thể của giao diện Downloaderbầu cua, được thiết kế để thực hiện các chức năng tải dữ liệu theo cách riêng của nó.
        downloader
									 =
									 new
									 MyDownloader
									();
									
        downloader
									.
									setListener
									(
									this
									);
									
    }
									

    @Override
									
    public
									 void
									 startDownloadEmoji
									(
									EmojiPackage
									 emojiPackage
									)
									 {
									
        // Tạo dữ liệu ngữ cảnh tải xuống
        EmojiDownloadContext
									 downloadContext
									 =
									 new
									 EmojiDownloadContext
									();
									
        downloadContext
									.
									emojiPackage
									 =
									 emojiPackage
									;
									
        // Bắt đầu tải xuống tệp hình ảnh biểu tượng thứ 0789 Club, truyền tham số ngữ cảnh vào
        downloader
									.
									startDownload
									(
									emojiPackage
									.
									emojiUrls
									.
									get
									(
									0
									),
									
                getLocalPathForEmoji
									(
									emojiPackage
									,
									 0
									),
									
                downloadContext
									);
									

    }
									

    @Override
									
    public
									 void
									 downloadSuccess
									(
									String
									 url
									,
									 String
									 localPath
									,
									 Object
									 contextData
									)
									 {
									
        Bạn có thể thực hiện việc ép kiểu xuống (down-casting) thông qua tham số contextData của giao diện callback để lấy được các tham số ngữ cảnh cần thiết. Điều này cho phép bạn truy cập sâu hơn vào các dữ liệu cụ thểtỉ lệ cược, từ đó dễ dàng xử lý và sử dụng trong logic của mình.
        EmojiDownloadContext
									 downloadContext
									 =
									 (
									EmojiDownloadContext
									)
									 contextData
									;
									

        downloadContext
									.
									localPathList
									.
									add
									(
									localPath
									);
									
        downloadContext
									.
									downloadedEmoji
									++;
									
        EmojiPackage
									 emojiPackage
									 =
									 downloadContext
									.
									emojiPackage
									;
									
        if
									 (
									downloadContext
									.
									downloadedEmoji
									 <
									 emojiPackage
									.
									emojiUrls
									.
									size
									())
									 {
									
            // Chưa tải xuống xongtỉ lệ cược, tiếp tục tải xuống tệp hình ảnh biểu tượng tiếp theo
            String
									 nextUrl
									 =
									 emojiPackage
									.
									emojiUrls
									.
									get
									(
									downloadContext
									.
									downloadedEmoji
									);
									
            downloader
									.
									startDownload
									(
									nextUrl
									,
									
                    getLocalPathForEmoji
									(
									emojiPackage
									,
									 downloadContext
									.
									downloadedEmoji
									),
									
                    downloadContext
									);
									
        }
									
        else
									 {
									
            // Đã tải xuống xong
            installEmojiPackageLocally
									(
									emojiPackage
									,
									 downloadContext
									.
									localPathList
									);
									
        }
									
    }
									

    @Override
									
    public
									 void
									 downloadFailed
									(
									String
									 url
									,
									 int
									 errorCode
									,
									 String
									 errorMessage
									,
									 Object
									 contextData
									)
									 {
									
        ...
									
    }
									

    @Override
									
    public
									 void
									 downloadProgress
									(
									String
									 url
									,
									 long
									 downloadedSize
									,
									 long
									 totalSize
									,
									 Object
									 contextData
									)
									 {
									
        ...
									
    }
									

    /**
    private
									 String
									 getLocalPathForEmoji
									(
									EmojiPackage
									 emojiPackage
									,
									 int
									 i
									)
									 {
									
        ...
									
    }
									

    /**
    private
									 void
									 installEmojiPackageLocally
									(
									EmojiPackage
									 emojiPackage
									,
									 List
									<
									String
									>
									 localPathList
									)
									 {
									
        ...
									
    }
									
}
									

								

Rõ ràng789 Club, phương pháp thực hiện thứ 4 là hợp lý hơn cả, với mã nguồn ngắn gọn và không có nhược điểm của ba phương pháp trước. Tuy nhiên, nó yêu cầu các giao diện đồng bộ ở tầng dưới phải hỗ trợ việc truyền ngữ cảnh một cách hoàn chỉnh. Trong thực tế, các giao diện mà chúng ta cần gọi thường đã được xác định sẵn và không thể thay đổi. Nếu giao diện mà chúng ta đang làm việc không hỗ trợ truyền tham số ngữ cảnh tốt, thì chúng ta không còn lựa chọn nào khác ngoài việc áp dụng một trong ba phương pháp đầu tiên. Nói tóm lại, việc chúng ta thảo luận về ba phương pháp đầu tiên không phải là tự làm khó mình, mà là để giải quyết những trường hợp giao diện không đủ khả năng hỗ trợ ngữ cả Những người thiết kế giao diện này đôi khi vô tình tạo ra những tình huống khiến chúng ta phải đối mặt với những thách thức như vậy.

lưu trữ toàn cục một bản sao của ngữ cảnh

Bây giờbầu cua, chúng ta có thể dễ dàng rút ra kết luận: Một định nghĩa giao diện callback tốt nên có khả năng truyền dữ liệu ngữ cảnh tùy chỉnh.

Chúng ta hãy cùng xem lại một số định nghĩa giao diện callback của hệ thống từ góc độ truyền tải ngữ cảnh. Ví dụ như trong iOS789 Club, phương thức alertView:clickedButtonAtIndex: của UIAlertViewDelegate hoặc tableView:cellForRowAtIndexPath: của UITableViewDataSource, các phương thức này đều có tham số đầu tiên trả về thể hiện của chính UIView (thực tế là trong UIKit, nhiều phương thức callback được định nghĩa theo cách tương tự). Điều này giúp truyền một phần ngữ cảnh để phân biệt các thể hiện UIView khác nhau, nhưng không thể dùng nó để phân biệt các sự kiện callback khác nhau bên trong cùng một thể hiện UIView. Nếu cần hiển thị nhiều lần bảng thông báo UIAlertView trên cùng một màn hình, mỗi lần chúng ta sẽ phải tạo một thể hiện UIAlertView mới và sử dụng tham số được trả về trong callback để xác định đó là lần hiển thị nào. Điều này khá giống với cách làm đã thảo luận trước đó ở dạng thứ ba. Ngoài ra, UIView cũng đã định nghĩa sẵn một tham số tag để truyền ngữ cảnh kiểu nguyên thủy, nhưng nếu muốn truyền ngữ cảnh ở các định dạng phức tạp hơn, thì chúng ta chỉ có thể làm như cách thứ ba đã đề cập, tức là tạo ra một lớp con của UIView riêng, thêm vào đó các thuộc tính lưu trữ ngữ cảnh. Nhìn sâu hơn, việc sử dụng một lớp con có vẻ là giải pháp linh hoạt nhất khi cần truyền ngữ cảnh tùy chỉnh. Tuy nhiên, nó cũng mang đến một số bất tiện, chẳng hạn như phải quản lý nhiều lớp thừa kế và đảm bảo rằng mỗi lớp con đều tuân thủ đúng các yêu cầu của ngữ cảnh mà nó cần xử lý. Một số lập trình viên cũng lo ngại rằng việc lạm dụng việc mở rộng lớp có thể dẫn đến cấu trúc mã nguồn trở nên phức tạp và khó bảo trì trong dài hạn. Một cách tiếp cận khác có thể cân nhắc là sử dụng một cơ chế ánh xạ giữa các thể hiện UIView và ngữ cảnh liên quan. Ví dụ, chúng ta có thể tạo một bản đồ (map) để lưu trữ ngữ cảnh tương ứng với mỗi thể hiện UIView, và sau đó truy xuất ngữ cảnh từ bản đồ dựa trên tham chiếu đến thể hiện UIView. Cách làm này không chỉ đơn giản hóa việc quản lý ngữ cảnh mà còn tránh được vấn đề thừa kế không cần thiết. Tóm lại, khi thiết kế các giao diện callback và phương thức xử lý ngữ cảnh, chúng ta cần cân nhắc kỹ lưỡng giữa việc sử dụng tham số mặc định sẵn có trong UIView, mở rộng lớp con, hay áp dụng các cơ chế quản lý ngữ cảnh bổ sung. Việc lựa chọn phù hợp sẽ phụ thuộc vào yêu cầu cụ thể của ứng dụng cũng như khả năng mở rộng trong tương lai.

Mỗi lần UIView được hiển thị lại789 Club, một phiên bản mới của đối tượng sẽ được tạo ra. Điều này không nhất thiết có nghĩa là sự tiêu tốn tài nguyên quá mức. Thực tế, mục đích chính của UIView là được tạo ra liên tục và thêm vào cấu trúc view để hiển thị. Tuy nhiên, ví dụ về IndependentVideoManager mà chúng ta đã đề cập trước đó lại mang tính chất khác biệt. Giao diện callback của nó được thiết kế để trả về đối tượng IndependentVideoManager như tham số đầu tiên, chẳng hạn như ivManager:isIndependentVideoAvailable:. Có thể suy đoán rằng định nghĩa giao diện callback này chắc chắn đã tham khảo UIKit. Nhưng tình huống của IndependentVideoManager rõ ràng khác biệt, vì nó thường chỉ cần tạo duy nhất một phiên bản và sử dụng lại cùng một đối tượng để gọi nhiều lần các phương thức nhằm phát quảng cáo. Điều quan trọng ở đây là phải hiểu rõ cách các callback khác nhau trên cùng một đối tượng xử lý những thông tin ngữ cảnh nào trong mỗi lần gọi. Điều thực sự cần thiết ở đây là khả năng truyền ngữ cảnh, điều này tương tự như cách mà chúng ta đã thảo luận ở phương pháp thứ tư. Khả năng truyền ngữ cảnh mà giao diện kiểu UIKit cung cấp là chưa đủ. Trong trường hợp của IndependentVideoManager, việc quản lý ngữ cảnh cần phải linh hoạt hơn để đảm bảo rằng mỗi lần callback đều có thể mang theo thông tin cụ thể và phù hợp cho từng trường hợp riêng lẻ.

Trong thiết kế giao diện callback789 Club, khả năng truyền ngữ cảnh, yếu tố quan trọng nhất là: Nó có thể phân biệt được nhiều lần callback từ một instance giao diện duy nhất hay không?

Dữ liệu từ DataHandler:

								
									
										Button
									 button
									 =
									 (
									Button
									)
									 findViewById
									(...);
									
button
									.
									setOnClickListener
									(
									new
									 View
									.
									OnClickListener
									()
									 {
									
    @Override
									
    public
									 void
									 onClick
									(
									View
									 v
									)
									 {
									
        ...
									
    }
									
});
									

								

Trong đoạn mã nàytỉ lệ cược, một đối tượng Button có thể liên kết với nhiều hàm callback (nhiều sự kiện nhấn nút), nhưng chúng ta không thể phân biệt cách xử lý giữa các callback khác nhau chỉ từ mã hiện tại. Tuy nhiên, may mắn là chúng ta thực sự không cần làm điều đó. Thay vào đó, bạn có thể thêm một tham số hoặc biến để giúp xác định rõ ràng nguồn gốc của mỗi lần nhấn, chẳng hạn như gán một chuỗi nhận dạng duy nhất cho mỗ Điều này sẽ giúp dễ dàng quản lý và xử lý từng hành động riêng biệt mà không gặp khó khăn nào.

Dựa trên các phân tích đã nêu789 Club, chúng ta nhận thấy rằng trong phần phát triển liên quan đến giao diện (View), vốn thường được coi là phần "frontend", việc xử lý lại nhiều lần cho một instance API thường không cần thiết, do đó cơ chế truyền ngữ cảnh phức tạp cũng ít khi được yêu cầu. Ngược lại, trong các tác vụ đồng bộ thuộc phần "backend", đặc biệt là những tác vụ có chu kỳ sống kéo dài, khả năng truyền ngữ cảnh trở nên quan trọng hơn bao giờ hết. Do đó, bài viết trước của loạt bài này đã nhấn mạnh vào vấn đề xử lý đồng bộ như một phần quan trọng của lập trình "backend".

Về vấn đề tham số ngữ cảnh (context)bầu cua, vẫn còn một số điểm nhỏ cần lưu ý: ví dụ như trong môi trường iOS, tham số ngữ cảnh (context) trong quá trình thực thi tác vụ bất đồng bộ (asynchronous task) nên được giữ dưới dạng tham chiếu mạnh (strong reference) hay tham chiếu yếu (weak reference)? Nếu là tham chiếu mạnh, thì khi người gọi (caller) truyền vào tham số ngữ cảnh là một đối tượng lớn như View Controller, điều này có thể dẫn đến hiện tượng giữ vòng tham chiếu (circular reference), từ đó gây ra rò rỉ bộ nhớ (memory leak). Ngược lại, nếu là tham chiếu yếu, và tham số ngữ cảnh mà người gọi truyền vào là một đối tượng tạm thời (temporary object), thì đối tượng này có thể bị hủy ngay sau khi được tạo ra, dẫn đến việc không thể truyền tiếp được. Đây thực chất là một bài toán hai nanh (dilemma) do cơ chế quản lý bộ nhớ dựa trên đếm tham chiếu (reference counting) gây ra. Điều này phụ thuộc vào kịch bản mà chúng ta kỳ vọng sẽ xảy ra. Trong trường hợp chúng ta đang thảo luận, tham số ngữ cảnh này được dùng để phân biệt các lần gọi lại (callback) của một giao diện cụ thể (interface instance), do đó tham số ngữ cảnh mà người gọi truyền vào không nên là đối tượng có chu kỳ sống lâu dài (long-lived object), mà phải là một đối tượng nhỏ với chu kỳ sống ngắn hơn hoặc tương đương với thời gian hoàn thành của tác vụ bất đồng bộ. Nó sẽ được tạo ra tại thời điểm bắt đầu thực thi tác vụ và giải phóng sau khi kết thúc tác vụ (khi callback xảy ra). Do đó, trong kịch bản này, chúng ta nên giữ tham số ngữ cảnh dưới dạng tham chiếu mạnh (strong reference) để đảm bảo rằng đối tượng được truyền vào vẫn tồn tại suốt quá trình xử lý tác vụ.

Thứ tự callback

rối loạn thứ tự callback

  • lỗi thứ tự callback
  • Là bên thực hiện giao diệntỉ lệ cược, khi triển khai giao diện, chúng ta cần xác định rõ liệu có phải đảm bảo thứ tự gọi lại hay không: đảm bảo rằng sẽ không xảy ra tình trạng gọi lại lộn xộn. Nếu cần phải cung cấp sự đảm bảo này, thì điều đó đồng nghĩa với việc độ phức tạp của việc triển khai giao diện sẽ gia tăng đáng kể. Hơn nữa, việc quản lý thứ tự các sự kiện hoặc tác vụ trở nên quan trọng hơn bao giờ hết trong trường hợp như vậy, đòi hỏi sự chú ý tỉ mỉ và kiểm soát chặt chẽ để tránh bất kỳ sai sót nào có thể dẫn đến lỗi trong quy trình xử lý.

Từ góc độ thực hiện giao diện bất đồng bộbầu cua, các yếu tố có thể gây mất thứ tự callback có thể có:

  • Kết quả trả về thất bại trước thời hạn. Thực tế789 Club, việc này có thể xảy ra một cách dễ dàng, nhưng lại rất khó để nhận ra rằng điều đó có thể gây ra sự hỗn loạn trong thứ tự trả về kết quả. Một ví dụ điển hình là khi một tác vụ bất đồng bộ được thiết kế để chuyển sang một luồng bất đồng bộ khác để thực thi. Tuy nhiên, ngay trước khi nó được chuyển đi, một lỗi nghiêm trọng đã được phát hiện (như tham số đầu vào không hợp lệ), dẫn đến việc toàn bộ tác vụ bị hủy và callback thất bại được kích hoạt. Điều này có nghĩa là các tác vụ bất đồng bộ khởi động muộn nhưng thất bại sớm có thể sẽ nhận được phản hồi trước cả những tác vụ khởi động trước đó mà vẫn đang chạy bình thường.
  • kết quả trả về trước thất bại
  • Việc thực thi song song của tác vụ bất đồng bộ. Đằng sau giao diện bất đồng bộ có thể có một nhóm luồng chạy cùng lúctỉ lệ cược, nhờ đó thứ tự hoàn thành của các tác vụ bất đồng bộ được thực thi song song sẽ là ngẫu nhiên. Bạn có thể tưởng tượng rằng mỗi tác vụ bất đồng bộ như một hành trình độc lập, khi tất cả cùng xuất phát từ điểm bắt đầu, nhưng không ai biết chắc chuyến đi nào sẽ kết thúc trước. Điều này xảy ra bởi vì các tác vụ này không chờ đợi lẫn nhau mà thay vào đó chúng hoạt động tự do trong nhóm luồng sẵn sàng, dẫn đến việc hoàn thành theo thứ tự ngẫu nhiên. Trong trường hợp này, quản lý hiệu quả nhóm luồng rất quan trọng để đảm bảo rằng mọi tác vụ đều có cơ hội hoàn thành một cách mượt mà, đồng thời tránh tình trạng quá tải hoặc xung đột giữa các tác vụ đang chạy.
  • Các nhiệm vụ bất đồng bộ phụ thuộc bên dưới có thể gây mất thứ tự callback.

Dù là trường hợp điều chỉnh thứ tự hay bất kỳ tình huống nào khác789 Club, chúng ta vẫn có thể đảm bảo rằng thứ tự của các hàm callback sẽ khớp với thứ tự ban đầu khi gọi các phương thức API. Để làm được điều này, chúng ta có thể thiết lập một hàng đợi (queue). Khi thực hiện mỗi lần gọi API để khởi động tác vụ đồng bộ, hãy thêm thông số gọi API cùng với một số thông tin ngữ cảnh khác vào hàng đợi. Sau đó, hãy đảm bảo rằng các hàm callback sẽ được thực thi theo đúng thứ tự mà các mục được lấy ra khỏi hàng đợi. Điều này giúp duy trì sự nhất quán trong quá trình xử lý và cho phép chúng ta kiểm soát chặt chẽ hơn thứ tự của các hoạt động.

Trong nhiều trường hợptỉ lệ cược, người sử dụng API thực tế không quá khắt khe, việc callback được trả về theo thứ tự không hoàn toàn là yếu tố quyết định sự thành công hay thất bại của toàn bộ quy trình. Tất nhiên, điều này chỉ đúng khi người dùng có cái nhìn rõ ràng và tỉnh táo về vấn đề này. Khi đó, việc chúng ta cố gắng đảm bảo callback luôn được gọi theo đúng thứ tự trong việc triển khai API sẽ không còn mang tính chất bắt buộc tuyệt đối nữa. Tuy nhiên, việc lựa chọn phương án nào cuối cùng vẫn phụ thuộc vào yêu cầu cụ thể của từng ngữ cảnh ứng dụng cũng như sở thích cá nhân của nhà phát triển API. Một số trường hợp, nếu ứng dụng không đòi hỏi độ chính xác cao hoặc không thực hiện các thao tác quan trọng dựa trên thứ tự callback, thì việc để callback hoạt động linh hoạt hơn cũng có thể giúp tối ưu hóa hiệu suất. Điều quan trọng nhất là phải hiểu rõ mục tiêu cuối cùng của sản phẩm và cân nhắc kỹ lưỡng giữa việc tuân thủ chặt chẽ thứ tự callback và khả năng mang lại lợi ích thực tế cho người dùng.

Callback dưới dạng closure và Callback Hell

Khi số lượng phương thức của giao diện đồng bộ hóa ít và giao diện trả về callback tương đối đơn giản (chỉ có một phương thức)789 Club, đôi khi chúng ta có thể định nghĩa callback dưới dạ Trên nền tảng iOS, chúng ta có thể sử dụng block để thực hiện điều này; trong khi đó, trên Android, chúng ta có thể sử dụng lớp ẩn danh nội bộ (đối ứng với biểu thức lambda trong Java 8 trở lên). Thêm vào đó, việc sử dụng closure hoặc block không chỉ giúp giảm thiểu mã nguồn mà còn làm cho code trở nên dễ đọc hơn. Đối với lập trình viên iOS, việc làm việc với block là một cách tiếp cận trực quan và linh hoạt, đặc biệt khi xử lý các tác vụ ngắn hạn như cập nhật giao diện người dùng hoặc tải dữ liệu. Còn trên Android, việc chuyển sang lambda giúp tiết kiệm thời gian và tránh việc viết nhiều đoạn mã thừa. Điều quan trọng cần lưu ý là dù bạn chọn cách nào, mục tiêu cuối cùng vẫn là đảm bảo tính hiệu quả và độ ổn định của ứng dụng. Điều này đòi hỏi sự cẩn thận trong việc quản lý vòng đời của các đối tượng và tránh những lỗi tiềm ẩn có thể xảy ra khi xử lý đồng bộ.

Giả sử giao diện DownloadListener trước đó được đơn giản hóa thành chỉ có một phương thức callback như sau:

								
									
										public
									 interface
									 DownloadListener
									 {
									
    /**
    public
									 static
									 final
									 int
									 SUCCESS
									 =
									 0
									;
									// Thành công
    //... Các mã định nghĩa mã lỗi khác (bỏ qua)

    /** * Callback khi quá trình tải xuống kết thúc. * @param errorCode Mã lỗi. "SUCCESS" biểu thị tải xuống thành côngtỉ lệ cược, các mã khác cho thấy tải xuống thất bại. * @param url Địa chỉ tài nguyên cần tải về. * @param localPath Vị trí lưu trữ tài nguyên sau khi tải xuống. * @param contextData Dữ liệu ngữ cảnh đi kèm để hỗ trợ thêm trong quá trình xử lý. */
    void
									 downloadFinished
									(
									int
									 errorCode
									,
									 String
									 url
									,
									 String
									 localPath
									,
									 Object
									 contextData
									);
									
}
									

								

Vậy thìtỉ lệ cược, giao diện Downloader cũng có thể được đơn giản hóa thay vì cần một giao diện setListener tách biệt. Thay vào đó, giao diện tải xuống có thể nhận trực tiếp giao diện callback trong bản thân nó. Cụ thể như sau: ``` interface Downloader { void download(String url, Callback callback); } interface Callback { void onSuccess(String result); void onFailure(String error); } ``` Bằng cách này, chúng ta không chỉ giảm thiểu sự phức tạp mà còn giúp mã nguồn trở nên dễ đọc và dễ bảo trì hơn. Một lợi ích khác là khi sử dụng, bạn sẽ không phải tạo ra các lớp trung gian chỉ để thiết lập trình lắng nghe. Điều này làm cho quá trình phát triển trở nên nhanh chóng và hiệu quả hơn rất nhiều.

								
									
										public
									 interface
									 Downloader
									 {
									
    /** * Khởi động quá trình tải xuống tài nguyên. * @param url Địa chỉ nguồn tài nguyên cần tải về. * @param localPath Vị trí lưu trữ trên thiết bị sau khi tải xuống. * @param contextData Dữ liệu ngữ cảnh789 Club, sẽ được truyền trở lại qua giao diệ Có thể là bất kỳ loại dữ liệu nào. * @param listener Thực thể của giao diện callback để thông báo trạng thái. */
    void
									 startDownload
									(
									String
									 url
									,
									 String
									 localPath
									,
									 Object
									 contextData
									,
									 DownloadListener
									 listener
									);
									
}
									

								

Giao diện bất đồng bộ được định nghĩa theo cách này có ưu điểm là mã nguồn khi sử dụng sẽ rất gọn gàngbầu cua, và giao tiếp qua callback có thể truyền dưới dạ Tuy nhiên, nếu số lớp lồng nhau quá sâu, vấn đề Callback Hell sẽ dễ xảy ra, khiến mã trở nên khó đọc và bảo trì. Điều này không chỉ làm giảm hiệu quả công việc mà còn tăng nguy cơ lỗi trong hệ thống, đặc biệt khi các thao tác phụ thuộc vào nhau cần được thực hiện tuần tự. http://callbackhell.com http://example.com/file1.txt

								
									final
									 Downloader
									 downloader
									 =
									 new
									 MyDownloader
									();
									
    downloader
									.
									startDownload
									(
									url1
									,
									 localPathForUrl
									(
									url1
									),
									 null
									,
									 new
									 DownloadListener
									()
									 {
									
        @Override
									
        public
									 void
									 downloadFinished
									(
									int
									 errorCode
									,
									 String
									 url
									,
									 String
									 localPath
									,
									 Object
									 contextData
									)
									 {
									
            if
									 (
									errorCode
									 !=
									 DownloadListener
									.
									SUCCESS
									)
									 {
									
                //... Xử lý lỗi
            }
									
            else
									 {
									
                // Tải xuống URL thứ hai
                downloader
									.
									startDownload
									(
									url2
									,
									 localPathForUrl
									(
									url2
									),
									 null
									,
									 new
									 DownloadListener
									()
									 {
									
                    @Override
									
                    public
									 void
									 downloadFinished
									(
									int
									 errorCode
									,
									 String
									 url
									,
									 String
									 localPath
									,
									 Object
									 contextData
									)
									 {
									
                        if
									 (
									errorCode
									 !=
									 DownloadListener
									.
									SUCCESS
									)
									 {
									
                            //... Xử lý lỗi
                        }
									
                        else
									 {
									
                            // Tải xuống URL thứ ba
                            downloader
									.
									startDownload
									(
									url3
									,
									 localPathForUrl
									(
									url3
									),
									 null
									,
									 new
									 DownloadListener
									(
									

                            )
									 {
									
                                @Override
									
                                public
									 void
									 downloadFinished
									(
									int
									 errorCode
									,
									 String
									 url
									,
									 String
									 localPath
									,
									 Object
									 contextData
									)
									 {
									
                                    //... Xử lý kết quả cuối cùng
                                }
									
                            });
									
                        }
									
                    }
									
                });
									
            }
									
        }
									
    });
									

								

Đối với Callback Helltỉ lệ cược, bài viết này http://callbackhell.com Giữ mã nguồn của bạn gọn gàng

Tuy nhiênbầu cua, đối với toàn bộ vấn đề lập trình bất đồng bộ liên quan đến việc xử lý tác vụ không đồng bộ, các giải pháp như ReactiveX không phải lúc nào cũng phù hợp trong mọi tình huống. Thực tế, bất kể là khi chúng ta đọc code của người khác hay chính bản thân mình viết ra, phần lớn các trường hợp đều gặp phải những tình huống lập trình bất đồng bộ cơ bản. Điều mà chúng ta cần suy nghĩ cẩn thận chủ yếu là vấn đề logic, chứ không phải chỉ việc áp dụng một khung (framework) nào đó là có thể tự động giải quyết tất cả mọi vấn đề. Một số trường hợp, việc hiểu rõ ngữ cảnh và mục tiêu thực sự quan trọng hơn việc sử dụng một công cụ phức tạp.


Mọi người đã nhận thấy rằngtỉ lệ cược, bài viết này đã dành phần lớn nội dung để giải thích những điều dường như hiển nhiên, có thể khiến người đọc cảm thấy hơi dài dòng. Tuy nhiên, nếu ta nghiên cứu kỹ lưỡng, sẽ thấy rằng nhiều giao diện đồng bộ mà chúng ta thường xuyên tiếp xúc không phải là dạng tối ưu mà chúng ta mong muốn. Để khai thác chúng một cách hiệu quả hơn, chúng ta cần hiểu rõ những hạn chế của chúng. Do đó, việc bỏ thời gian để tổng kết và xem xét lại các trường hợp khác nhau là điều rất đáng giá. Điều này đặc biệt quan trọng khi chúng ta đang ở thời đại mà sự tiện lợi và tốc độ luôn được ưu tiên hàng đầu. Nhiều lúc, những thứ tưởng chừng đơn giản lại ẩn chứa những phức tạp sâu xa mà chỉ khi nhìn lại từ góc độ tổng thể, chúng ta mới nhận ra. Chính vì vậy, việc tái đánh giá và tìm kiếm phương pháp cải thiện sẽ giúp chúng ta tiến xa hơn trong quá trình phát triển và ứng dụng công nghệ hiện đại.

Thực tế thì việc định nghĩa một giao diện tốt đòi hỏi sự am hiểu sâu sắc và kinh nghiệm dày dặnbầu cua, ngay cả những người đã làm việc nhiều năm cũng không phải ai cũng đạt được. Bài viết này cũng không hướng dẫn cụ thể cách nào để tạo ra các giao diện và giao diện callback hoàn hảo. Trong thực tế, không có lựa chọn nào là hoàn hảo tuyệt đối cả; điều chúng ta cần là khả năng đánh đổi giữa các ưu nhược điểm khác nhau. Ngoài ra, khi thiết kế giao diện phần mềm, chúng ta thường phải cân nhắc rất nhiều yếu tố như độ phức tạp của hệ thống, nhu cầu sử dụng thực tế, hay thậm chí cả vấn đề bảo trì trong tương lai. Chính vì vậy, việc đưa ra quyết định về cách thức hoạt động của giao diện đòi hỏi không chỉ kỹ năng mà còn cả sự sáng tạo và linh hoạt trong tư duy. Tóm lại, dù không có công thức cố định nào cho mọi trường hợp, nhưng bằng cách liên tục học hỏi và tích lũy kinh nghiệm, mỗi nhà phát triển đều có thể tiến bộ và cải thiện khả năng thiết kế giao diện của mình theo thời gian.

Cuối cùngbầu cua, chúng ta có thể thử đưa ra một số tiêu chí để đánh giá chất lượng của một giao diện (dù tiêu chí này không quá nghiêm ngặt), và tôi đã nghĩ đến những điều sau đây: Thứ nhất, giao diện phải dễ sử dụng và trực quan, giúp người dùng thực hiện thao tác một cách nhanh chóng mà không cần mất nhiều thời gian tìm hiểu. Thứ hai, thiết kế cần đảm bảo tính thẩm mỹ, với các yếu tố như màu sắc hài hòa, bố cục hợp lý, và hình ảnh phù hợp để tạo cảm giác thoải mái cho người dùng. Thứ ba, giao diện nên có khả năng tương thích với nhiều loại thiết bị khác nhau, từ máy tính bảng, điện thoại di động cho đến màn hình lớn, nhằm mang lại trải nghiệm tốt nhất cho người dùng. Cuối cùng, độ ổn định và tốc độ phản hồi cũng là một trong những yếu tố quan trọng. Giao diện cần hoạt động mượt mà, tránh tình trạng lag hoặc load chậm khi người dùng tương tác. Những tiêu chí trên có thể không hoàn toàn đầy đủ, nhưng chúng tôi tin rằng đó là những điểm cốt lõi cần được ưu tiên khi thiết kế một giao diện thành công.

  • Logic hoàn chỉnh (các logic giao diện không chồng chéo và không bị thiếu)
  • Có thể tự giải thích
  • Có một mô hình trừu tượng phù hợp với lý thuyết
  • Quan trọng nhất: Làm cho người gọi cảm thấy thoải mái và đáp ứng nhu cầu.

(Kết thúc)

Các bài viết được chọn lọc khác


Bài viết gốcbầu cua, xin vui lòng trích dẫn nguồn và bao gồm mã QR bên dưới! Nếu không, từ chối tái bản!
Liên kết bài viết: /iox6st0y.html
Hãy theo dõi tài khoản Weibo cá nhân của tôi: Tìm kiếm tên tôi "Trương Thiết Lệ" trên Weibo.
Tài khoản WeChat của tôi: tielei-blog (Trương Thiết Lệ)
Bài trước: Giải thích bằng một hình ảnh về điều khiển luồng trong RxJava
Bài sau: Xử lý bất đồng bộ trong phát triển Android và iOS (ba) —— Hợp tác giữa nhiều tác vụ bất đồng bộ

Bài viết mới nhất