Làm cho tác vụ chạy nền thật sự chạy
Tôi đăng ký một tác vụ làm mới chạy nền, lên lịch cho nó, và nó không bao giờ chạy. Không phải “chạy muộn” — mà là không bao giờ, suốt vài ngày, trên một thiết bị tôi vẫn dùng bình thường.
Mọi thứ đều đúng, trừ sáu thứ.
Việc đăng ký phải xảy ra trước khi khởi động xong
func application(_ application: UIApplication,
didFinishLaunchingWithOptions: …) -> Bool {
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.example.refresh",
using: nil
) { task in
self.handleRefresh(task as! BGAppRefreshTask)
}
return true
}
Đăng ký sau khi didFinishLaunching trả về sẽ ném lỗi. Đăng ký trong một bộ khởi tạo lười, hay bên
trong một modifier .task, hay ở bất cứ đâu chạy sau khung hình đầu tiên, đều là quá muộn.
Cái này ít nhất cũng ồn ào — nó sập chứ không hỏng lặng lẽ.
Định danh phải nằm trong Info.plist
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.example.refresh</string>
</array>
Bỏ sót cái này thì việc đăng ký hỏng một cách lặng lẽ. Mọi định danh bạn đăng ký đều cần một mục, khớp chính xác.
Bạn cũng cần khả năng Background Modes, với Background fetch hoặc Background processing được tích tùy theo loại tác vụ.
Bạn phải lên lịch lại mỗi lần
Đây là thứ đã bẫy tôi. Một tác vụ chạy một lần. Nếu bạn muốn nó chạy lại thì cái handler phải lên lịch cho lần kế tiếp:
func handleRefresh(_ task: BGAppRefreshTask) {
scheduleNextRefresh() // ĐẦU TIÊN — trước khi làm bất cứ việc gì
let operation = RefreshOperation()
task.expirationHandler = {
operation.cancel()
}
operation.completionBlock = {
task.setTaskCompleted(success: !operation.isCancelled)
}
queue.addOperation(operation)
}
func scheduleNextRefresh() {
let request = BGAppRefreshTaskRequest(identifier: "com.example.refresh")
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
try? BGTaskScheduler.shared.submit(request)
}
Hãy lên lịch cho lần kế tiếp trước, trước phần công việc. Nếu công việc ném lỗi hoặc tác vụ bị giết lúc hết hạn thì bạn vẫn còn một tác vụ tương lai đã được lên lịch — nếu không thì một lần hỏng sẽ chấm dứt vĩnh viễn việc làm mới chạy nền cho tới lần mở ứng dụng kế tiếp.
Bạn phải gọi setTaskCompleted
Nếu không gọi, iOS giết tác vụ lúc hết hạn và ghi nhận nó là cư xử không đúng mực. Vài lần như thế và hệ thống thôi lên lịch cho bạn.
Mọi nhánh — thành công, thất bại, bị hủy — đều phải gọi nó đúng một lần.
Cảnh báo
expirationHandler cho bạn vài giây báo trước khi tác vụ bị giết. Hãy dùng nó để hủy công việc
rồi gọi setTaskCompleted(success: false). Một tác vụ bị giết mà chưa hoàn tất là tín hiệu tiêu
cực mạnh nhất bạn có thể gửi cho bộ lập lịch.
earliestBeginDate là một sàn, không phải một lịch
Cái tên thì thành thật và người ta đọc nó như một lời hứa. Nó nghĩa là “không sớm hơn thời điểm này”, và iOS quyết định thời điểm thật dựa trên mức pin, trạng thái sạc, khả năng có mạng, trạng thái nhiệt, và — quan trọng nhất — tần suất người dùng mở ứng dụng của bạn.
Một ứng dụng người dùng mở hằng ngày thì được cấp thời gian chạy nền. Một ứng dụng họ mở đúng một lần tuần trước thì không, và không lượng code đúng đắn nào thay đổi được điều đó. Đây là thiết kế có chủ đích.
Đặt earliestBeginDate thành một phút không khiến nó chạy sau một phút. Mười lăm phút là một cái sàn
hợp lý cho một tác vụ làm mới; ngắn hơn thì dù sao cũng bị lờ đi.
Kiểm thử mà không phải chờ nhiều ngày
Đây là phần khiến toàn bộ chuyện này trở nên khả thi. Hãy tạm dừng trong trình gỡ lỗi rồi chạy:
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.example.refresh"]
Câu đó kích hoạt tác vụ ngay lập tức. Có một câu tương đương để ép hết hạn:
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"com.example.refresh"]
Cả hai đều là API riêng tư và cả hai đều chỉ dùng trong trình gỡ lỗi — đừng đưa chúng vào code phát hành. Chúng biến một vòng phản hồi kéo dài nhiều ngày thành mười giây, và nếu không có chúng thì việc kiểm thử nhánh hết hạn gần như bất khả thi.
Hai loại tác vụ
BGAppRefreshTaskRequest — ngắn, khoảng 30 giây, để cập nhật nội dung. Chạy thường xuyên hơn.
BGProcessingTaskRequest — vài phút, cho công việc nặng như bảo trì cơ sở dữ liệu hay tải lên
hàng loạt. Có thể yêu cầu nguồn điện ngoài và mạng:
let request = BGProcessingTaskRequest(identifier: "com.example.cleanup")
request.requiresExternalPower = true
request.requiresNetworkConnectivity = true
Đặt requiresExternalPower khiến nó có khả năng chạy cao hơn hẳn — thường là qua đêm trong lúc sạc
— đổi lại là nó không chạy với một người dùng chẳng bao giờ cắm sạc qua đêm.
Thứ rốt cuộc đã hiệu quả
Với tôi, cách chữa là ba thứ: lên lịch lại ở đầu cái handler thay vì ở cuối, gọi setTaskCompleted
trên nhánh hết hạn (thứ tôi đã hoàn toàn không cài đặt), và chấp nhận rằng tác vụ chạy khoảng một hai
lần một ngày chứ không phải mười lăm phút một lần.
Phần cuối đó mới là điều chỉnh thật sự. Việc chạy nền là một hạn mức mà hệ thống phân bổ dựa trên mức độ người dùng coi trọng ứng dụng của bạn, và thiết kế đúng là thiết kế chạy tốt khi được cấp chút thời gian chạy nền và xuống cấp một cách duyên dáng khi chẳng được cấp gì.