FCM 註冊管理最佳做法

如果您使用 FCM API 以程式輔助方式建構傳送要求,可能會發現隨著時間推移,您傳送訊息給註冊資訊過時的閒置裝置,導致資源浪費。這種情況可能會影響 Firebase 控制台中回報的郵件送達資料,或匯出至 BigQuery 的資料,導致送達率大幅下降 (但實際上並非如此)。本指南將探討您可以採取的一些措施,確保訊息有效指定目標,並提供有效的傳送報表。

過時和過期的註冊

如果裝置超過一個月未連上 FCM,就會產生過時的註冊記錄。隨著時間經過,裝置連線至 FCM 的可能性會越來越低。這些過時的註冊項目不太可能收到訊息和主題扇出。

註冊資訊過時的原因有很多,舉例來說,與註冊程序相關聯的裝置可能遺失、損壞或存放起來,導致您忘記。

如果是 Android,當註冊項目閒置 270 天後,FCM會將其視為過期並進行垃圾收集。註冊過期後,FCM 會將其標示為無效,並拒絕傳送訊息。請注意,Firebase 安裝 ID (FID) 本身是由 Firebase Installations 服務 (FIS) 管理,而非 FCM。在極少數情況下,裝置會再次連線,且應用程式會在註冊項目遭到垃圾收集後開啟,此時用戶端應用程式會使用從 FIS 擷取的 FID,透過 FCM 再次註冊。請注意,FID 可能會變更;如要瞭解 FID 何時會重新發放,請參閱「管理 Firebase 安裝作業」。

對於 iOS 等其他平台,FCM 依賴於底層的推送服務 (例如 APNs),這類服務沒有基於閒置時間的 270 天效期。建議您主動維持註冊資訊的有效性,並移除過時的註冊資訊

基本最佳做法

在任何使用 FCM API 以程式輔助方式建構傳送要求的應用程式中,您都應遵循一些基本做法。主要最佳做法如下:

  • FCM 擷取 Firebase 安裝 ID (FID),並儲存在應用程式伺服器上。伺服器的重要角色是追蹤每個用戶端註冊的 FID,並維護有效的 FID 清單。強烈建議您在資料庫中實作註冊時間戳記,並在每次上傳註冊資料時更新。
  • 維持註冊資料的有效性,並移除過時的註冊資料。除了移除 FCM 不再視為有效的註冊項目,您可能還想監控其他註冊項目是否過時,並主動移除這些項目。本指南將說明幾種可達成此目標的方法。

擷取及儲存 Firebase 安裝 ID

應用程式首次啟動時,FCM SDK 會向 FCM 註冊應用程式執行個體,並傳回 Firebase 安裝 ID (FID)。這是您必須在 API 的目標傳送要求中加入的 ID,或用於主題訂閱。

強烈建議您在每次上傳 FID 時,一併將 FID 和時間戳記儲存至應用程式伺服器。更新每個上傳要求的時間戳記後,伺服器就能知道應用程式例項上次開啟的時間,以及與 FCM 後端成功同步的時間。

視自動初始化功能是否啟用 (包括不支援),您應按照下列方式處理註冊和更新:

  • (建議) 啟用自動初始化功能:SDK 會自動保持註冊狀態,並監控變更。應用程式啟動時,系統會在例行同步處理期間定期叫用 onRegistered() 回呼,FID 變更時也會叫用。只要實作這個回呼,即可將 FID 上傳至伺服器,並儲存目前的時間戳記。
  • 停用自動初始化:系統不會在啟動時自動叫用 onRegistered() 回呼。如要追蹤註冊情形並保持最新狀態,請在應用程式啟動時呼叫 register(),例如在 Android 中,於主要活動的 onCreate() 呼叫。成功呼叫後,系統會使用 FID 觸發 FCM 註冊程序,並將 FID 傳送至 onRegistered() 回呼,讓應用程式上傳 FID 並更新伺服器上的時間戳記。

範例:Cloud Firestore 中的商店 FID 和時間戳記

舉例來說,您可以使用 Cloud Firestore 將 FID 儲存在名為 fcmRegistrations 的集合中。集合中的每個文件 ID 都對應至使用者 ID,且文件會儲存目前的 FID 和上次更新的時間戳記。使用如下 Kotlin 範例所示的 set 函式:

private fun sendRegistrationToServer(installationId: String?) {
    // If you're running your own server, call API to send registration details and today's date for the user

    // Example shown uses Firestore
    // Add FID and timestamp to Firestore for this user
    val deviceFid = hashMapOf(
        "installationId" to installationId,
        "timestamp" to FieldValue.serverTimestamp(),
    )
    // Get user ID from Firebase Auth or your own server
    Firebase.firestore.collection("fcmRegistrations").document("myuserid")
        .set(deviceFid)
}

每當 Firebase 安裝 ID 成功註冊或更新時,系統就會叫用 onRegistered() 回呼。您應實作這項回呼,上傳 FID 並更新時間戳記:

override fun onRegistered(installationId: String) {
    Log.d(TAG, "Registered installation ID: $installationId")

    // Send the Firebase Installation ID (FID) to your app server. Your app
    // server should save the FID and update the timestamp upon receipt.
    sendRegistrationToServer(installationId)
}

如果停用自動初始化,請在應用程式啟動時 (例如在 onCreate() 中) 呼叫 register(),透過 onRegistered() 觸發註冊流程並傳送 FID:

// Trigger manual registration if auto-initialization is turned off.
FirebaseMessaging.getInstance().register()
    .addOnCompleteListener(this) { task ->
        if (task.isSuccessful) {
            // The registration callback onRegistered() will be invoked with the current FID.
        } else {
            Log.w(TAG, "Failed to register with Firebase Cloud Messaging", task.exception)
        }
    }

維持註冊資料的即時性,並移除過時的註冊資料

判斷註冊是否為新註冊或過時註冊,有時並不容易。為涵蓋所有情況,您應採用註冊過時的門檻。根據預設,如果應用程式執行個體一個月未連線,FCM 就會將註冊視為過時。如果註冊時間超過一個月,該裝置可能處於閒置狀態;否則有效裝置會重新註冊。

視用途而定,一個月可能太短或太長,因此請自行決定適合的條件。

偵測 FCM 後端傳回的無效回應

請務必偵測 FCM 的無效回應,並從系統中刪除已知無效或已過期的任何註冊。使用 HTTP v1 API 時,這些錯誤訊息可能表示傳送要求指定了無效或過期的註冊:

  • UNREGISTERED (HTTP 404)
  • INVALID_ARGUMENT (HTTP 400)

如果您確定訊息酬載內容有效,且針對目標註冊收到其中一個回應,即可安全地刪除這項註冊記錄,因為該記錄永遠不會再有效。舉例來說,如要從 Cloud Firestore 刪除無效的註冊項目,您可以部署及執行下列函式:

        // Firebase Installation ID comes from the client FCM SDKs
        const firebaseInstallationId = 'YOUR_FIREBASE_INSTALLATION_ID';

        const message = {
            data: {
                // Information you want to send inside of notification
            },
            fid: firebaseInstallationId
        };

        // Send message to device with provided Firebase Installation ID
        getMessaging().send(message)
        .then((response) => {
            // Response is a message ID string.
        })
        .catch((error) => {
            // Delete registration for user if error code is UNREGISTERED or INVALID_ARGUMENT.
            if (error.errorCode == "messaging/registration-token-not-registered") {
                // If you're running your own server, call API to delete the registration for the user
                // Example shown uses Firestore
                // Get user ID from Firebase Auth or your own server
                Firebase.firestore.collection("fcmRegistrations").document(user.uid).delete()
            }
        });

如果 Android 裝置的註冊資訊在閒置 270 天後過期,或用戶端明確取消註冊,FCM 會傳回無效的回應。如要根據自己的定義更準確地追蹤過時狀態,可以主動移除過時的註冊內容

定期更新註冊資訊

無論註冊是根據 FID 或舊版註冊權杖,伺服器都應在每次上傳要求時,更新資料庫中的註冊時間戳記。這個時間戳記會做為應用程式安裝的信號,讓用戶端知道應用程式已成功開啟並與 FCM 後端同步。請根據您使用的 API 實作適當策略:

如果用戶端應用程式使用 FID API,則需要在用戶端應用程式中排定定期背景工作,以擷取或重新整理註冊資料。在自動初始化期間,SDK 會自動處理重新整理作業,並在應用程式啟動期間的例行同步作業中,定期將正確的目前 FID 傳送至 onRegistered() 回呼。

如要讓伺服器保持在最新狀態,請實作「擷取及儲存 Firebase 安裝 ID」一文詳述的啟動上傳策略:

  • 啟用自動初始化:SDK 會在應用程式啟動期間的例行同步作業中,自動確保將最新的 FID 傳送至伺服器。
  • 自動初始化已停用或不支援:在應用程式啟動時 (例如在 Android 中,於主要活動的 onCreate() 內) 呼叫 register(),強制執行註冊序列,並觸發 FID 傳送至 onRegistered() 回呼。

這些策略可確保伺服器一律擁有最新的有效 FID,並能自動從上傳失敗的狀態復原,讓應用程式具備高度復原能力。

已淘汰的註冊權杖 API

如果您使用舊版註冊權杖,用戶端 SDK 不會在例行同步處理時自動管理重新整理作業。因此,建議您定期擷取及更新伺服器上的所有註冊權杖。請按照下列步驟操作:

  • 在用戶端應用程式中新增應用程式邏輯,使用適當的 API 呼叫 (例如 Apple 平台的 token(completion): 或 Android 的 getToken()) 擷取目前權杖,然後將目前權杖傳送至應用程式伺服器進行儲存 (附上時間戳記)。這可能是設定為涵蓋所有用戶端或權杖的每月工作。
  • 新增伺服器邏輯,定期更新權杖的時間戳記,無論權杖是否變更。

如需使用 WorkManager 更新舊版權杖的 Android 邏輯範例,請參閱 Firebase 網誌的「管理雲端通訊權杖」一文。

無論採用哪種時間模式,請務必定期更新權杖。每月更新一次的頻率,可兼顧電池續航力和偵測閒置註冊權杖。這樣做也能確保任何閒置裝置在恢復使用時,都會重新整理註冊資訊。如果重新整理的頻率超過每週一次,不會有任何好處。

移除過時的註冊資料

將訊息傳送至裝置前,請確認裝置註冊的時間戳記在過時時間範圍內。舉例來說,您可以實作 Cloud Functions for Firebase,每天執行檢查作業,確保時間戳記在定義的過時時間範圍 (例如 const EXPIRATION_TIME = 1000 * 60 * 60 * 24 * 30;) 內,然後移除過時的註冊項目:

exports.pruneRegistrations = functions.pubsub.schedule('every 24 hours').onRun(async (context) => {
  // Get all documents where the timestamp exceeds is not within the past month
  const staleRegistrationsResult = await admin.firestore().collection('fcmRegistrations')
      .where("timestamp", "<", Date.now() - EXPIRATION_TIME)
      .get();
  // Delete devices with stale registrations
  staleRegistrationsResult.forEach(function(doc) { doc.ref.delete(); });
});
exports.pruneTokens = functions.pubsub.schedule('every 24 hours').onRun(async (context) => { // Get all documents where the timestamp exceeds is not within the past month const staleTokensResult = await admin.firestore().collection('fcmTokens') .where("timestamp", "<", Date.now() - EXPIRATION_TIME) .get(); // Delete devices with stale tokens staleTokensResult.forEach(function(doc) { doc.ref.delete(); }); });

從主題取消訂閱過時的註冊項目

如果您使用主題,可能也想從訂閱的主題中取消註冊。這項程序包含兩個步驟:

  1. 每當 Firebase 安裝 ID (FID) 變更時,應用程式就應重新訂閱主題。這樣一來,應用程式再次啟用時,訂閱項目就會自動重新顯示。
  2. 如果應用程式執行個體閒置一個月 (或您自訂的過時時間範圍),您應使用 Firebase Admin SDK 取消訂閱主題,從 FCM 後端刪除 Firebase 安裝 ID 至主題的對應。

這兩個步驟的好處是,由於要散布的過時註冊資料較少,因此散布作業會更快完成,而且過時的應用程式例項會在再次啟用時自動重新訂閱。

評估傳送成功率

如要盡可能準確掌握訊息傳送情況,建議只將訊息傳送至正在使用的應用程式例項。如果您經常傳送訊息給大量訂閱者,這點就特別重要。如果這些訂閱者中有部分處於閒置狀態,長期下來,您的傳送統計資料可能會受到顯著影響。

將訊息指定至應用程式執行個體前,請先考量下列事項:

  • Google Analytics、BigQuery 中擷取的資料或其他追蹤信號是否指出註冊程序已啟動?
  • 先前嘗試傳送郵件時,是否持續一段時間都失敗?
  • 過去一個月內,伺服器上的 Firebase 安裝 ID 是否已更新?
  • 如果是 Android 裝置,FCM Data API 報告的訊息傳送失敗率是否偏高,且失敗原因為 droppedDeviceInactive

如要進一步瞭解傳送作業,請參閱「瞭解訊息傳送作業」。