Xin chào mọi người, tôi là Riccardo. Kỹ sư iOS cấp cao tại Bending Spoons, tôi tin tưởng vào việc phát triển iOS, cả ứng dụng và các công cụ và tôi thích chia sẻ kiến ​​thức của mình với người khác.

 

 

Tuần trước, trong nhóm thảo luận của chúng tôi, một câu hỏi được đặt ra: iOS cần bao nhiêu thời gian để ứng dụng hoàn thành tác vụ nền (background task)?

Như thường xảy ra trong thế giới công nghệ, tài liệu được đưa ra đôi khi không chính xác. Có rất nhiều thông tin sai lệch khiến bạn khó tìm ra câu trả lời: nhiều người nói thời gian trong khoảng từ 30 giây đến 3 phút. Vì vậy… tôi quyết định chạy một thử nghiệm nhỏ để tìm hiểu và đây là những gì tôi đã làm và những gì tôi đã khám phá ra.

Công nghệ

Công nghệ được chọn để chạy thử nghiệm này là công nghệ cũ UIApplication.beginBackgroundTask(withName:expirationHandler:) . Phương pháp này thực hiện hai điều:

  1. Nó thông báo cho hệ điều hành rằng, khi được đặt ở chế độ nền, ứng dụng cần thêm thời gian để hoàn thành tác vụ chạy lâu dài.
  2. Nó trả về một mã định danh có thể được sử dụng để thông báo cho hệ điều hành rằng nhiệm vụ đã hoàn thành (bằng cách gọi phương thức UIApplication.endBackgroundTask(_:))

Nếu chúng ta quên gọi phương thức beginBackgroundTask, hệ điều hành sẽ chỉ định một cái gì đó chừng 5 giây để hoàn thành các tác vụ đang diễn ra và điều đó không thể đủ. 

Tại thời điểm này, tài liệu không ghi rõ ràng: chúng ta có bao nhiêu thời gian thực thi sau khi gọi phương thức đó? Hãy cùng tìm hiểu!

 

 Kể từ iOS 13, Apple đã giới thiệu một cách hoàn toàn mới để xử lý các tác vụ nền: BackgroundTaskScheduler. Tài liệu có thể được tìm thấy ở đây. Tuy nhiên, điều này sẽ giới hạn đối tượng của ứng dụng của bạn chỉ những người dùng đã cập nhật điện thoại của họ lên iOS mới nhất. Vào tháng 3 năm 2020, Apple tuyên bố rằng 77% thiết bị iOS hiện đang chạy iOS 13. Nếu bạn định chỉ sử dụng công nghệ mới nhất cho các tác vụ nền, 23% người dùng iOS sẽ không thể sử dụng ứng dụng của bạn.

Thí nghiệm

Thí nghiệm này khá đơn giản. Ý tưởng là viết trong UserDefaults:

  1. đánh dấu thời gian khi ứng dụng chuyển sang chế độ nền
  2. đánh dấu thời gian khi expirationHandler được gọi

Sau đó, chúng tôi chỉ cần tính toán sự khác biệt giữa hai dấu thời gian này. Chúng tôi sẽ cần lặp lại thử nghiệm nhiều lần để tính toán thời gian thực hiện trung bình và để cung cấp thông số cho việc đánh giá khoa học.

Tôi quyết định thêm giao diện người dùng đơn giản để xem kết quả và hệ thống ước tính phụ, dựa trên bộ đếm thời gian. Tuy nhiên, đây chỉ là những bổ sung để làm cho kết quả trở nên chắc chắn hơn hoặc dễ hiểu hơn và không ảnh hưởng đến cốt lõi của thử nghiệm.

Mã nguồn

Trong phần này, tôi sẽ không báo cáo mã của toàn bộ ứng dụng nhưng tôi sẽ hiển thị phần có liên quan cho thử nghiệm. Nếu không, bài viết sẽ dẫn đến nhiều dòng mã UI chung, khá nhàm chán.

Hãy bắt đầu với các khối xây dựng. Tất cả mã bên dưới sẽ nằm trong AppDelegate. Hãy nhớ rằng đây là một thử nghiệm để lấy một phần thông tin cụ thể, nó sẽ không phải là một ứng dụng hoàn chỉnh. Đó là lý do tại sao tôi quyết định thực hiện một cách tiếp cận nhanh chóng và hiệu quả. 😉

Bắt đầu nhiệm vụ lâu dài

Đoạn mã đầu tiên là chức năng chúng tôi cho phép cho hệ điều hành biết rằng ứng dụng cần thêm thời gian để hoàn thành một tác vụ chạy dài. 

func longRunningTask() {
  self.backgroundTaskId = UIApplication.shared.beginBackgroundTask(
    withName: "test demon",
    expirationHandler: {
      UserDefaults.standard.set(
        Date().timeIntervalSince1970,
        forKey: bgTaskExpiredKey
      )
      UIApplication.shared.endBackgroundTask(self.backgroundTaskId!)
  })
}

Như bạn có thể thấy, chúng tôi chỉ đang gọi hàm và chúng tôi đang lưu từ định danh vào một biến của AppDelegate. Sau đó, chúng tôi sử dụng mã định danh đó vào để cho hệ thống biết rằng tác vụ đã được hoàn thành.

Nếu chúng ta quên bước cuối cùng đó, khi hết thời gian, hệ thống sẽ kill ứng dụng (app bị crash) của chúng ta thay vì đưa nó vào trạng thái bị treo. Trong một ứng dụng thực, điều này thực sự tồi tệ: nếu người dùng mở lại ứng dụng sau một lúc, ứng dụng sẽ khởi động lại thay vì hiển thị thứ cuối cùng mà người dùng nhìn thấy.

Đánh dấu sự bắt đầu của background

Chúng ta cần lưu vào UserDefaults khi ứng dụng chạy ở chế độ nền. Để làm như vậy, chúng ta có thể sử dụng đoạn mã sau:

func applicationDidEnterBackground(_ application: UIApplication) {
  UserDefaults.standard.set(
    Date().timeIntervalSince1970,
    forKey: appEntereBGKey
  )
}

Với mã này, chúng tôi thực thi trong một trong các phương thức của UIApplicationDelegate. Ngay sau khi ứng dụng đã vào nền, chúng tôi ghi dấu thời gian vào UserDefaults.

Lấy thông tin

Bây giờ, chúng ta cần lấy thông tin từ UserDefaults mỗi khi ứng dụng khởi động hoặc vào nền trước. Điều này có thể được thực hiện với đoạn mã sau:

func retrieveAndUpdateState() -> State {
  // get the reference to the UserDefaults
  let ud = UserDefaults.standard
  // retrieve the AppState stored at the previous execution
  var state: State = ud.codable(forKey: stateKey) ?? State()

  // Retrieve the information from the user defaults 
  let lastTaskStart: TimeInterval = ud.double(forKey: appEntereBGKey)
  let lastTaskEnd: TimeInterval = ud.double(forKey: bgTaskExpiredKey)
  
  // Create the execution object
  let execution = Execution(
    startTime: lastTaskStart,
    endTime: lastTaskEnd
  )

  // append the data of the last execution
  state.executions.append(execution)
  
  // update the state
  ud.set(codable: state, forKey: stateKey)
  
  // returns it
  return state
  }

Như bạn có thể thấy, đoạn mã khá đơn giản: chúng ta có một đối tượng State chứa một mảng các lần chạy trước của ứng dụng. Điều này là cần thiết để hiển thị kết quả khi kết thúc thử nghiệm.

Đối tượng StateExcution trông giống như sau:


struct Execution: Codable {
  let startTime: TimeInterval
  let endTime: TimeInterval
}

struct State: Codable {
  var executions: [Execution] = []
}

Đặt mọi thứ lại với nhau

Tại thời điểm này, chúng tôi có tất cả các thành phần để chạy thử nghiệm. Mã cuối cùng trông tương tự như thế này (nó thiếu tất cả mã giao diện người dùng, nó đã được bỏ qua cho ngắn gọn):

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

  var window: UIWindow?
  var backgroundTaskId: UIBackgroundTaskIdentifier? = nil
  var vc: ViewController!

  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.
    let window = UIWindow()

    let state = retrieveAndUpdateState()

    let nc = UINavigationController()
    self.vc = ViewController(state: state)
    nc.viewControllers = [vc]

    window.rootViewController = nc
    self.window = window
    window.makeKeyAndVisible()
    
    self.longRunningTask()
   
    return true
  }
  
  func applicationDidEnterBackground(_ application: UIApplication) {
    UserDefaults.standard.set(
      Date().timeIntervalSince1970,
      forKey: appEntereBGKey
    )
  }

  func applicationWillEnterForeground(_ application: UIApplication) {
    self.vc.state = self.retrieveAndUpdateState()
    self.longRunningTask()
  }
  
  func longRunningTask() {
    self.backgroundTaskId = UIApplication.shared.beginBackgroundTask(
      withName: "test demon",
      expirationHandler: {
        UserDefaults.standard.set(
          Date().timeIntervalSince1970,
          forKey: bgTaskExpiredKey
        )
        UIApplication.shared.endBackgroundTask(self.backgroundTaskId!)
    })
  }
  
  func retrieveAndUpdateState() -> State {
    // get the reference to the UserDefaults
    let ud = UserDefaults.standard
    // retrieve the AppState stored at the previous execution
    var state: State = ud.codable(forKey: stateKey) ?? State()

    // Retrieve the information from the user defaults 
    let lastTaskStart: TimeInterval = ud.double(forKey: appEntereBGKey)
    let lastTaskEnd: TimeInterval = ud.double(forKey: bgTaskExpiredKey)

    // Create the execution object
    let execution = Execution(
      startTime: lastTaskStart,
      endTime: lastTaskEnd
    )

    // append the data of the last execution
    state.executions.append(execution)

    // update the state
    ud.set(codable: state, forKey: stateKey)

    // returns it
    return state
  }
}

Kết quả

Hệ thống cho phép chúng tôi thực hiện một số tác vụ chạy dài trong khoảng 26 giây.

Tôi đã mở và đóng ứng dụng nhiều lần, đợi tác vụ nền hoàn thành. Đôi khi tôi đợi 30 giây và một số lần khác tôi đợi hàng phút. Đây là kết quả:

Kết quả của một số lần thực hiện. Tôi đã thêm một số mã khác để thử và tạo ước tính nhưng nó không liên quan đến mục đích của thử nghiệm.

Khóa học IOS Swift cơ bản với nhiều ưu đãi khi đăng ký theo nhóm.

Cảm ơn các bạn đã theo dõi, hãy like và chia sẻ để mình dịch nhiều bài hay hơn nhé!

 

Bài viết gốc trên medium.