iOS Bluetooth Guide 4: Xử lý background

Khi lập trình iOS app, có một việc rất quan trọng đó là xác định xem app đang chạy ở background hay foreground bởi app khi thực thi ở background sẽ khác với foreground vì tài nguyên hệ thống rất giới hạn.

Mặc định, rất nhiều tác vụ của Core Bluetooth, ở cả phía central và peripheral, bị disable trong khi app của ta chạy ở background hoặc trạng thái suspend, vì thế nên ta cần phải bật Background Mode cho app. Làm vậy cho phép app có thể được gọi lên từ trạng thái suspend để xử lý các sự kiện nhất định liên quan tới bluetooth.

Và chú ý rằng, thậm chí khi ta đã bật Background Mode cho bluetooth thì app của chúng ta cũng sẽ bị kill vào một lúc nào đó để giải phóng bộ nhớ cho những app đang chạy ở foreground, vì thế sẽ có thể xảy ra việc mất các kết nối bluetooth đang active hoặc pending. Từ iOS 7.0, Core Bluetooth hỗ trợ lưu các thông tin trạng thái cho central và peripheral manager objects và restore lại trạng thái đó khi app chạy. Ta có thể sử dụng tính năng này để duy trì các action có tính lâu dài.

Foreground-Only Apps

Là những app mà không bật Background Mode.

Phần lớn các ứng dụng iOS sẽ chuyển sang trạng thái suspend trong một thời gian ngắn sau khi ứng dụng được đưa về background nếu ta không bật Background Mode. Khi ở trạng thái suspend, ứng dụng không thể thực thi các tác vụ bluetooth hay nhận thức được các bluetooth event.

Về phía central, foreground-only app không thể quét và tìm kiếm các peripheral khi ở trạng thái background hay suspend.

Về phía peripheral, việc advertise sẽ bị disable, central nào mà đang kết nối với các dynamic characteristic sẽ nhận về error.

Tuy nhiên việc này có ảnh hưởng nhiều hay không thì cũng tùy theo trường hợp sử dụng. Ví dụ như ta đang tương tác với dữ liệu của một peripheral nhưng lại quên mất và switch sang app khác và app của chúng ta chuyển sang trạng thái suspend thì kết nối đó sẽ bị mất mà có thể chúng ta chẳng hề hay biết cho đến khi app trở lại foreground, việc đó có thể gây ra một ảnh hưởng nhất định.

Tận dụng lợi thế của Peripheral Connection Options

Tất cả các bluetooth event xảy ra khi app ở foreground thì khi ở trạng thái suspend sẽ được đưa vào hàng đợi của hệ thống và được thực thi chỉ khi app tiếp tục trở lại foreground. Core Bluetooth cung cấp khả năng thông báo cho người dùng biết khi một số các sự kiện (của central role) xảy ra. Và từ đó người dùng có thể quyết định đưa app trở lại foreground hay không.

Ta sử dụng những thông báo này bằng việc thêm vào một trong các peripheral connection options sau khi gọi hàm connect(_:options:) từ CBCentralManager khi kết nối với peripheral:

  • CBConnectPeripheralOptionNotifyOnConnectionKey: dùng key này khi ta muốn hệ thống hiển thị thông báo nếu app bị suspend ở thời điểm *có kết nối thành công tới một peripheral.
  • CBConnectPeripheralOptionNotifyOnDisconnectionKey: dùng key này khi ta muốn hệ thống hiển thị thông báo nếu app bị suspend ở thời điểm bị đứt kết nối với một peripheral.
  • CBConnectPeripheralOptionNotifyOnNotificationKey: dùng key này khi ta muốn hệ thống hiển thị thông báo nếu app bị suspend ở thời điểm mà nó nhận được một thông báo bất kì từ peripheral.

Core Bluetooth Background Execution Modes

Nếu ứng dụng cần phải chạy ở background để thực hiện một số tác vụ nhất định liên quan tới bluetooth thì nó cần phải được bật Background Mode trong Info.plist (thực ra cái này thì có thể bật ở tab Capabilities, nó sẽ tự add vào Info.plist). Khi đã bật Background Mode, hệ thống sẽ tự gọi app của ta từ trạng thái suspend để cho phép ta xử lý các tác vụ liên quan tới bluetooth.

Việc xử lý ở background này khá là quan trọng với những ứng dụng có những tương tác truyền tải dữ liệu ở những khoảng thời gian đều đặn như máy đo nhịp tim.

Có hai mode trong Background Mode cho bluetooth mà ứng dụng có thể bật:

  • App đang thực thi với vai trò central.
  • App đang thực thi với vai trò peripheral.

Nếu app thực thi cả 2 vài trò, thì tất nhiên là bật cả hai.

Sau khi bật, trong Info.plist sẽ xuất hiện key UIBackgroundModes với value là dạng array bao gồm một hoặc các string sau:

  • bluetooth-central: App giao tiếp với các thiết bị peripheral sử dụng Core Bluetooth framework.
  • bluetooth-peripheral: App chia sẻ dữ liệu sử dụng Core Bluetooth framework.

Chú ý: Chế độ hiểu thị kiểu Property list trong Xcode nó sẽ hiển thị theo kiểu human-readable cho một số key được định nghĩa trước thay vì hiển thị đúng key name. Nên do đó ta sẽ thấy nó ghi là Required background modes thay vì UIBackgroundModes.

Mode: bluetooth-central

Khi bật mode này, Core Bluetooth sẽ cho phép app được chạy trong background để thực hiện một số tác vụ của central. Cụ thể là app có thể quét và kết nối tới peripheral, tìm kiếm và tương tác với dữ liệu của peripheral.

Thêm vào đó, hệ thống sẽ gọi app lên khi có bất kì một hàm delegate nào từ CBCentralManagerDelegate hoặc CBPeripheralDelegate được gọi, cho phép app có thể xử lý những event quan trọng, như là khi một kết nối được thiết lập hay bị đứt, khi một peripheral cập nhật một giá trị characteristic, và khi trạng thái của central manager thay đổi.

Mặc dù ta có thể thực hiệm rất nhiều tác vụ Bluetooth khi ở background, nhưng phải hiểu rằng việc quét peripheral khi ứng dụng ở background khác với khi ở foreground. Cụ thể, khi app quét thiết bị khi ở background:

  • CBCentralManagerScanOptionAllowDuplicatesKey scan option sẽ bị bỏ qua, và việc tìm kiếm nhiều peripheral sẽ được kết hợp lại thành một event tìm kiếm duy nhất.
  • Nếu có nhiều app đang tìm kiếm peripheral ở background, thì khoảng thời gian để quét các gói tin adveristing sẽ tăng lên. Đại khái là, việc tìm peripheral sẽ lâu hơn.

iOS tạo ra những thay đổi này khi ở background để giảm thiểu việc sử dụng sóng điện từ và để đỡ tốn pin.

Mode: bluetooth-peripheral

Khi bật mode này, Core Bluetooth sẽ cho phép app được chạy trong background để thực hiện một số tác vụ của peripheral. Cụ thể là hệ thống sẽ gọi app lên để xử lý các quá trình đọc, ghi và subscription event. Thêm vào đó, Core Bluetooth cho phép app advertise khi ở trạng thái background.

Tuy nhiên, ta cũng phải hiểu rằng việc advertise khi app đang ở background khác với foreground, cụ thể khi app advertise ở trạng thái background thì:

  • CBAdvertisementDataLocalNameKey advertisement key sẽ bị bỏ qua, và local name của peripheral sẽ không được advertise.
  • Tất cả các service UUID có chứa giá trị cho key CBAdvertisementDataServiceUUIDsKey được đặt vào một vùng "overflow" đặc biệt, mà chỉ có thể được tìm ra khi có một central nào chỉ định rõ là tìm đến UUID đó.
  • Nếu có nhiều app đã advertise ở background, thì tần số mà app gửi các gói tin advertising sẽ giảm xuống.

Sử dụng Background Mode một cách khôn khéo

Mặc dù việc bật Background Mode là cần thiết để đáp ứng cho một số trường hợp cụ thể, ta cũng nên xử lý background một cách có trách nhiệm. Bởi vì việc thực thi các tác vụ background yêu cầu thiết bị iOS luôn phải bật sóng điện từ và tất nhiên là việc này sẽ làm giảm tuổi thọ pin.

Thế nên hãy giảm thiểu tối đa số lượng tác vụ xử lý ở background. App được gọi lên cho các tác vụ bluetooth nên xử lý một cách nhanh chóng để quay lại trạng thái suspend nhanh nhất có thể. Bất cứ app nào bật Background Mode cho bluetooth phải tuân thủ một số guideline cơ bản sau:

  • App nên xử lý theo từng session và cung cấp một giao diện để người dùng có thể quyết định việc start hay stop các bluetooth events.
  • Khi được gọi lên từ trạng thái suspend, một app có 10 giây để hoàn thành một tác vụ. Lý tưởng mà nói thì app cần phải xử lý nhanh nhất có thể để quay lại trạng thái suspend. App sử dụng quá nhiều thời gian để xử lý background có thể bị hệ thống kill.
  • App không nên được gọi lên từ trạng thái suspend để thực thi các tác vụ không liên quan đến lý do mà app được gọi.

Thực hiện các long-term action ở background

Một số app cần sử dụng Core Bluetooth để thực hiện các long-term actions ở background. Lấy ví dụ ta đang phát triển một home security app cho iOS mà cần phải tương tác với khóa cửa (tất nhiên đây là khóa cửa thông minh có gắn bluetooth). App và khóa tương tác với nhau để tự động khóa cửa khi người dùng rời khỏi nhà hay mở cửa khi người dùng trở về, và những việc này được thực hiện ở background. Khi người dùng rời khỏi nhà, thiết bị iOS có thể bị cách quá xa khóa, khiến kết nốt bluetooth bị đứt. Ở thời điểm này, app có thể gọi connect(_:options:) từ CBCentralManager, và vì yêu cầu kết nối không có time out, nên khi người dùng trở về nhà, thiết bị iOS sẽ kết nối lại với cái khóa.

Bây giờ giả sử rằng người dùng đi xa nhà vài ngày, nếu app bị kill bởi hệ thống khi người dùng ở xa, app sẽ không thể kết nối tới cái khóa khi người dùng trở về, và người dùng sẽ không mở được cửa. Điều quan trọng nhất với những app kiểu này là việc có thể sử dụng Core Bluetooth để thực hiện long-term action hay không, như là việc theo dõi trạng thái của connection.

Lưu giữ và khôi phục trạng thái

Vì việc lưu giữ và khôi phục trạng thái được xây dựng sẵn trong Core Bluetooth nên ứng dụng có thể yêu cầu hệ thống lưu giữ trạng thái của central/peripheral manager và tiếp tục thực hiện hộ các tác vụ, ngay cả khi ứng dụng không chạy. Khi một trong những tác vụ này hoàn thành, hệ thống sẽ chạy app để đưa vào trạng thái background và từ đó app có thể khởi tạo lại trạng thái và xử lý các event tương ứng. Trong trường hợp home security app ở trên, hệ thống sẽ theo dõi connection request hộ app, và sẽ chạy lại app để xử lý hàm delegate callback centralManager(_:didConnect:) khi có kết nối thành công (tức là người dùng trở về nhà), và cửa sẽ được mở khóa.

Core Bluetooth hỗ trợ việc lưu giữ và khôi phục trạng thái cho app với vai trò central hoặc peripheral hoặc cả hai. Khi app đóng vai trò central, hệ thống sẽ lưu trạng thái của central manager object khi hệ thống kill app để tăng bộ nhớ (nếu app có nhiều central manager object thì ta có thể chọn những cái nào sẽ được hệ thống track).

Cụ thể, với một CBCentralManager, hệ thống sẽ theo dõi:

  • Các service mà central manager đã quét.
  • Các peripheral mà central manager đang cố kết nối tới hoặc đã kết nối.
  • Các characteristic mà central manager đang subscribe.

Còn với một CBPeripheralManager, hệ thống sẽ theo dõi:

  • Dữ liệu mà peripheral manager đang advertise.
  • Các service và characteristic mà peripheral manager publish vào device's database.
  • Các central đã subscribe tới các giá trị characteristic.

Khi hệ thống khởi chạy app vào background, ta có thể khởi tạo lại central và peripheral managers và khôi phục trạng thái của chúng. Phần dưới mô tả chi tiết làm thế nào để thực hiện việc lưu trữ và khôi phục trạng thái này.

Tích hợp tính năng Lưu giữ và Khôi phục trạng thái

  • Bước 1 (required): Bật tính năng này bằng cách thêm một key trong options khi khởi tạo central và peripheral manager object.
  • Bước 2 (required): Khởi tạo lại bất cứ central hoặc peripheral manager object nào khi app được khởi chạy bởi hệ thống.
  • Bước 3 (required): Thực thi các hàm delegate tương ứng dùng để khôi phục trạng thái.
  • Bước 4 (optional): Cập quá trình khởi tạo của central và peripheral manager.

Bật tính năng Lưu giữ và Khôi phục trạng thái

Để bật tính năng này, ta chỉ đơn giản là thêm một restoration ID khi khởi tạo central hoặc peripheral manager. Một restoration ID là một string định danh central/peripheral manager trong app của bạn với Core Bluetooth. Core Bluetooth sẽ lưu giữ trạng thái chỉ của những object nào có restoration ID. Code mô tả:

let myCentralManager = CBCentralManager(delegate: self, queue: nil, options: [CBCentralManagerOptionRestoreIdentifierKey: "myCentralManagerIdentifier"])

Khi khởi tạo peripheral thì tương tự ta dùng key: CBPeripheralManagerOptionRestoreIdentifierKey.

Chú ý: Bởi vì app có thể có nhiều manager object, nên ta phải đảm bảo restoration ID là duy nhất để hệ thống có thể phân biệt được các object này.

Khởi tạo lại Central và Peripheral Manager

Khi hệ thống khởi chạy app vào background, điều đầu tiên ta cần làm đó là khởi tạo lại các central và peripheral manager tương ứng với các restoration ID. Nếu app có nhiều hơn một central hoặc peripheral manager, app cần phải xem xem manager nào cần được khởi tạo lại khi hệ thống khởi chạy app. Ta có thể lấy danh sách của tất cả các restoration ID thông qua giá trị của key UIApplicationLaunchOptionsKey.bluetoothCentrals hoặc UIApplicationLaunchOptionsKey.bluetoothPeripherals khi thực thi hàm application(_:didFinishLaunchingWithOptions:) của AppDelegate:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    let identifiers = launchOptions?[.bluetoothPeripherals]
}

Sau khi có danh sách các restoration ID, đơn giản là lặp qua và khởi tạo các manager object tương ứng.

Chú ý: Khi app khởi chạy, hệ thống chỉ cung cấp các restoration ID của các central và peripheral có các tác vụ thực hiện trong khi app bị ngắt, không phải tất cả các restoration ID.

Thực thi các hàm delegate tương ứng để khôi phục

Sau khi khởi tạo lại các central và peripheral manager, ta cần khôi phục trạng thái của chúng bằng việc đồng bộ với trạng thái của Bluetooth system, và để làm vậy ta cần phải thực thi một số hàm delegate. Với central manager, ta thực thi hàm centralManager(_:willRestoreState:). Với peripheral, ta thực thi hàm peripheralManager(_:willRestoreState:)

Quan trọng: Với app bật có bật tính năng lưu trữ và khôi phục trạng thái của Core Bluetooth thì những hàm này (centralManager(_:willRestoreState:)peripheralManager(_:willRestoreState:)) là những hàm đầu tiên được gọi lên khi app khởi chạy vào background. Với những app không bật tính năng lưu giữ trạng thái (hoặc chẳng có gì cần để khôi phục khi khởi chạy), thì các hàm delegate centralManagerDidUpdateState(_:)peripheralManagerDidUpdateState(_:) sẽ được gọi trước.

Trong cả hai hàm delegate trên, tham số cuối là một dictionary chứa các thông tin về các manager được lưu giữ ở thời điểm app bị kill. Để xem danh sách của các key trong dictionary, tham khảo CBCentralManagerDelegate Protocol ReferenceCBPeripheralManagerDelegate Protocol Reference.

Để khôi phục trạng thái của CBCentralManager object, ta lấy giá trị của các key trong dictionary được cung cấp trong hàm centralManager(_:willRestoreState:). Ví dụ nếu như central manager của ta có một số các kết nối nào đang bật hoặc đang chờ ở thời điểm app bị kill, hệ thống sẽ tiếp tục theo dõi chúng thay cho app. Đoạn code dưới đây cho thấy, ta có thể sử dụng CBCentralManagerRestoredStatePeripheralsKey dictionary key để lấy danh sách tất cả các peripherals (thể hiện bởi các CBPeripheral object) mà central manager đã kết nối hoặc đang cố gắng kết nối tới:

func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
    let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey]
    //...
}

Tiếp theo thì việc làm gì với cái list lấy được ở trên thì tùy vào tình huống của ứng dụng đưa ra. Ta có thể khôi phục trạng thái của CBPeripheralManager object bằng cách tương tự.

Cập nhật quá trình khởi tạo

Sau khi thực thi 3 bước cần thiết trên, ta có thể sẽ muốn cập nhật lại quá trình khởi tạo peripheral và central manager. Mặc dù đây là bước optional, nó cũng khá là quan trọng để đảm bảo rằng mọi thứ có thể chạy mượt mà. Ví dụ, app của ta có thể bị kill trong khi nó đang ở giữa quá trình explore data của một peripheral. Khi app được khởi tạo với peripheral này, nó sẽ không biết được quá trình truyền tải dữ liệu tới đâu rồi. Thế nên ta cũng cần phải xử lý vấn đề này. Ví dụ, khi khởi tạo app và hàm delegate centralManagerDidUpdateState(_:) được gọi, ta có thể xem xem ta đã discover thành công service của peripheral hay chưa bằng cách sau:

func centralManagerDidUpdateState(_ central: CBCentralManager) {
    let serviceUUIDIndex = peripheral.services?.index(where: { service -> Bool in
        return service.uuid.uuidString == myServiceUUIDString
    })
    if serviceUUIDIndex == NSNotFound {
        peripheral.discoverServices([CBUUID(string: myServiceUUIDString)])
        //...
    }
}

Như ví dụ trên, nếu hệ thống ngắt app trước khi kết thúc discover service, thì ta sẽ discover lại bằng việc gọi hàm discoverServices(_:). Còn nếu discover xong rồi thì ta sẽ kiểm tra xem các characteristic đã được discover chưa (hoặc đã được subscribe chưa tùy trường hợp). Việc cập nhật lại quá trình khởi tạo theo kiểu này, ta sẽ đảm bảo được việc ta gọi đúng hàm vào đúng thời điểm.


Dịch và tham khảo từ Core Bluetooth Programming Guide