即使您的應用程序暫時失去網絡連接,Firebase 應用程序也能正常工作。此外,Firebase 還提供了用於在本地持久保存數據、管理狀態和處理延遲的工具。
磁盤持久性
Firebase 應用程序會自動處理臨時網絡中斷。緩存數據在離線時可用,Firebase 會在網絡連接恢復時重新發送任何寫入。
當您啟用磁盤持久性時,您的應用程序會將數據本地寫入設備,這樣您的應用程序就可以在離線時保持狀態,即使用戶或操作系統重新啟動應用程序也是如此。
只需一行代碼即可啟用磁盤持久性。
Kotlin+KTX
Firebase.database.setPersistenceEnabled(true)
Java
FirebaseDatabase.getInstance().setPersistenceEnabled(true);
持久性行為
通過啟用持久性,Firebase 實時數據庫客戶端在聯機時同步的任何數據都會保留在磁盤上並且可以離線使用,即使在用戶或操作系統重新啟動應用程序時也是如此。這意味著您的應用程序通過使用存儲在緩存中的本地數據像在線一樣工作。偵聽器回調將繼續觸發本地更新。
Firebase 實時數據庫客戶端會自動保留一個隊列,其中包含您的應用離線時執行的所有寫入操作。啟用持久性後,此隊列也會持久保存到磁盤,因此當用戶或操作系統重新啟動應用程序時,您的所有寫入都可用。當應用程序重新連接時,所有操作都會發送到 Firebase 實時數據庫服務器。
如果您的應用使用Firebase Authentication ,則 Firebase 實時數據庫客戶端會在應用重新啟動時保留用戶的身份驗證令牌。如果授權令牌在您的應用離線時過期,客戶端會暫停寫入操作,直到您的應用重新對用戶進行身份驗證,否則寫入操作可能會因安全規則而失敗。
保持數據新鮮
Firebase 實時數據庫為活躍的偵聽器同步並存儲數據的本地副本。此外,您可以使特定位置保持同步。
Kotlin+KTX
val scoresRef = Firebase.database.getReference("scores") scoresRef.keepSynced(true)
Java
DatabaseReference scoresRef = FirebaseDatabase.getInstance().getReference("scores"); scoresRef.keepSynced(true);
Firebase 實時數據庫客戶端會自動下載這些位置的數據並保持同步,即使引用沒有活動的偵聽器也是如此。您可以使用以下代碼行關閉同步。
Kotlin+KTX
scoresRef.keepSynced(false)
Java
scoresRef.keepSynced(false);
默認情況下,會緩存 10MB 的先前同步數據。這對於大多數應用程序應該足夠了。如果緩存超出其配置的大小,Firebase 實時數據庫會清除最近最少使用的數據。保持同步的數據不會從緩存中清除。
離線查詢數據
Firebase 實時數據庫存儲從查詢返回的數據以供離線使用。對於離線構建的查詢,Firebase 實時數據庫會繼續處理之前加載的數據。如果請求的數據尚未加載,Firebase 實時數據庫會從本地緩存加載數據。當網絡連接再次可用時,數據將加載並反映查詢。
例如,此代碼查詢 Firebase 實時分數數據庫中的最後四項
Kotlin+KTX
val scoresRef = Firebase.database.getReference("scores") scoresRef.orderByValue().limitToLast(4).addChildEventListener(object : ChildEventListener { override fun onChildAdded(snapshot: DataSnapshot, previousChild: String?) { Log.d(TAG, "The ${snapshot.key} dinosaur's score is ${snapshot.value}") } // ... })
Java
DatabaseReference scoresRef = FirebaseDatabase.getInstance().getReference("scores"); scoresRef.orderByValue().limitToLast(4).addChildEventListener(new ChildEventListener() { @Override public void onChildAdded(@NonNull DataSnapshot snapshot, String previousChild) { Log.d(TAG, "The " + snapshot.getKey() + " dinosaur's score is " + snapshot.getValue()); } // ... });
假設用戶斷開連接、掉線並重新啟動應用程序。雖然仍處於離線狀態,但該應用程序會從同一位置查詢最後兩項。此查詢將成功返回最後兩個項目,因為應用程序已加載上述查詢中的所有四個項目。
Kotlin+KTX
scoresRef.orderByValue().limitToLast(2).addChildEventListener(object : ChildEventListener { override fun onChildAdded(snapshot: DataSnapshot, previousChild: String?) { Log.d(TAG, "The ${snapshot.key} dinosaur's score is ${snapshot.value}") } // ... })
Java
scoresRef.orderByValue().limitToLast(2).addChildEventListener(new ChildEventListener() { @Override public void onChildAdded(@NonNull DataSnapshot snapshot, String previousChild) { Log.d(TAG, "The " + snapshot.getKey() + " dinosaur's score is " + snapshot.getValue()); } // ... });
在前面的示例中,Firebase 實時數據庫客戶端通過使用持久緩存為得分最高的兩隻恐龍引發“已添加孩子”事件。但它不會引發“價值”事件,因為該應用程序從未在在線時執行過該查詢。
如果應用程序在離線時請求最後六個項目,它會立即獲得四個緩存項目的“添加孩子”事件。當設備重新聯機時,Firebase 實時數據庫客戶端與服務器同步並獲取應用程序的最後兩個“已添加子項”和“值”事件。
離線處理交易
在應用程序離線時執行的任何事務都會排隊。一旦應用程序重新獲得網絡連接,交易就會發送到實時數據庫服務器。
管理狀態
在實時應用程序中,檢測客戶端何時連接和斷開連接通常很有用。例如,您可能希望在客戶端斷開連接時將用戶標記為“離線”。
Firebase 數據庫客戶端提供簡單的原語,當客戶端與 Firebase 數據庫服務器斷開連接時,您可以使用這些原語寫入數據庫。無論客戶端是否完全斷開連接,這些更新都會發生,因此即使連接斷開或客戶端崩潰,您也可以依靠它們來清理數據。所有寫操作,包括設置、更新和刪除,都可以在斷開連接時執行。
下面是一個使用onDisconnect
原語在斷開連接時寫入數據的簡單示例:
Kotlin+KTX
val presenceRef = Firebase.database.getReference("disconnectmessage") // Write a string when this client loses connection presenceRef.onDisconnect().setValue("I disconnected!")
Java
DatabaseReference presenceRef = FirebaseDatabase.getInstance().getReference("disconnectmessage"); // Write a string when this client loses connection presenceRef.onDisconnect().setValue("I disconnected!");
onDisconnect 如何工作
當您建立onDisconnect()
操作時,該操作存在於 Firebase 實時數據庫服務器上。服務器檢查安全性以確保用戶可以執行請求的寫入事件,並在無效時通知您的應用程序。服務器然後監視連接。如果連接在任何時候超時,或者被實時數據庫客戶端主動關閉,服務器會再次檢查安全性(以確保操作仍然有效),然後調用事件。
您的應用程序可以使用寫入操作的回調來確保onDisconnect
已正確附加:
Kotlin+KTX
presenceRef.onDisconnect().removeValue { error, reference -> error?.let { Log.d(TAG, "could not establish onDisconnect event: ${error.message}") } }
Java
presenceRef.onDisconnect().removeValue(new DatabaseReference.CompletionListener() { @Override public void onComplete(DatabaseError error, @NonNull DatabaseReference reference) { if (error != null) { Log.d(TAG, "could not establish onDisconnect event:" + error.getMessage()); } } });
onDisconnect
事件也可以通過調用.cancel()
來取消:
Kotlin+KTX
val onDisconnectRef = presenceRef.onDisconnect() onDisconnectRef.setValue("I disconnected") // ... // some time later when we change our minds // ... onDisconnectRef.cancel()
Java
OnDisconnect onDisconnectRef = presenceRef.onDisconnect(); onDisconnectRef.setValue("I disconnected"); // ... // some time later when we change our minds // ... onDisconnectRef.cancel();
檢測連接狀態
對於許多與狀態相關的功能,讓您的應用知道它何時在線或離線是很有用的。 Firebase 實時數據庫在/.info/connected
提供了一個特殊位置,每次 Firebase 實時數據庫客戶端的連接狀態發生變化時都會更新該位置。這是一個例子:
Kotlin+KTX
val connectedRef = Firebase.database.getReference(".info/connected") connectedRef.addValueEventListener(object : ValueEventListener { override fun onDataChange(snapshot: DataSnapshot) { val connected = snapshot.getValue(Boolean::class.java) ?: false if (connected) { Log.d(TAG, "connected") } else { Log.d(TAG, "not connected") } } override fun onCancelled(error: DatabaseError) { Log.w(TAG, "Listener was cancelled") } })
Java
DatabaseReference connectedRef = FirebaseDatabase.getInstance().getReference(".info/connected"); connectedRef.addValueEventListener(new ValueEventListener() { @Override public void onDataChange(@NonNull DataSnapshot snapshot) { boolean connected = snapshot.getValue(Boolean.class); if (connected) { Log.d(TAG, "connected"); } else { Log.d(TAG, "not connected"); } } @Override public void onCancelled(@NonNull DatabaseError error) { Log.w(TAG, "Listener was cancelled"); } });
/.info/connected
是一個布爾值,它在實時數據庫客戶端之間不同步,因為該值取決於客戶端的狀態。換句話說,如果一個客戶端將/.info/connected
讀取為 false,則不能保證另一個客戶端也將讀取為 false。
在 Android 上,Firebase 自動管理連接狀態以減少帶寬和電池使用。當客戶端沒有活動的偵聽器、沒有掛起的寫入或onDisconnect
操作,並且沒有通過goOffline
方法明確斷開連接時,Firebase 會在 60 秒不活動後關閉連接。
處理延遲
服務器時間戳
Firebase 實時數據庫服務器提供了一種機制,可以將在服務器上生成的時間戳作為數據插入。此功能與onDisconnect
相結合,提供了一種簡單的方法來可靠地記錄實時數據庫客戶端斷開連接的時間:
Kotlin+KTX
val userLastOnlineRef = Firebase.database.getReference("users/joe/lastOnline") userLastOnlineRef.onDisconnect().setValue(ServerValue.TIMESTAMP)
Java
DatabaseReference userLastOnlineRef = FirebaseDatabase.getInstance().getReference("users/joe/lastOnline"); userLastOnlineRef.onDisconnect().setValue(ServerValue.TIMESTAMP);
時鐘偏差
雖然firebase.database.ServerValue.TIMESTAMP
更準確,並且對於大多數讀/寫操作更可取,但有時它可以用於估計客戶端相對於 Firebase 實時數據庫服務器的時鐘偏差。您可以將回調附加到位置/.info/serverTimeOffset
以獲取以毫秒為單位的值,Firebase 實時數據庫客戶端添加到本地報告的時間(以毫秒為單位的紀元時間)以估計服務器時間。請注意,此偏移量的準確性可能會受到網絡延遲的影響,因此主要用於發現時鐘時間中的大(> 1 秒)差異。
Kotlin+KTX
val offsetRef = Firebase.database.getReference(".info/serverTimeOffset") offsetRef.addValueEventListener(object : ValueEventListener { override fun onDataChange(snapshot: DataSnapshot) { val offset = snapshot.getValue(Double::class.java) ?: 0.0 val estimatedServerTimeMs = System.currentTimeMillis() + offset } override fun onCancelled(error: DatabaseError) { Log.w(TAG, "Listener was cancelled") } })
Java
DatabaseReference offsetRef = FirebaseDatabase.getInstance().getReference(".info/serverTimeOffset"); offsetRef.addValueEventListener(new ValueEventListener() { @Override public void onDataChange(@NonNull DataSnapshot snapshot) { double offset = snapshot.getValue(Double.class); double estimatedServerTimeMs = System.currentTimeMillis() + offset; } @Override public void onCancelled(@NonNull DatabaseError error) { Log.w(TAG, "Listener was cancelled"); } });
在線狀態應用程序示例
通過將斷開連接操作與連接狀態監視和服務器時間戳相結合,您可以構建一個用戶存在系統。在此系統中,每個用戶都將數據存儲在數據庫位置以指示實時數據庫客戶端是否在線。客戶端在上線時將此位置設置為 true,在斷開連接時設置時間戳。此時間戳指示給定用戶上次在線的時間。
請注意,您的應用程序應在將用戶標記為在線之前對斷開連接操作進行排隊,以避免在客戶端的網絡連接丟失的情況下在兩個命令都可以發送到服務器之前發生任何競爭情況。
這是一個簡單的用戶存在系統:
Kotlin+KTX
// Since I can connect from multiple devices, we store each connection instance separately // any time that connectionsRef's value is null (i.e. has no children) I am offline val database = Firebase.database val myConnectionsRef = database.getReference("users/joe/connections") // Stores the timestamp of my last disconnect (the last time I was seen online) val lastOnlineRef = database.getReference("/users/joe/lastOnline") val connectedRef = database.getReference(".info/connected") connectedRef.addValueEventListener(object : ValueEventListener { override fun onDataChange(snapshot: DataSnapshot) { val connected = snapshot.getValue<Boolean>() ?: false if (connected) { val con = myConnectionsRef.push() // When this device disconnects, remove it con.onDisconnect().removeValue() // When I disconnect, update the last time I was seen online lastOnlineRef.onDisconnect().setValue(ServerValue.TIMESTAMP) // Add this device to my connections list // this value could contain info about the device or a timestamp too con.setValue(java.lang.Boolean.TRUE) } } override fun onCancelled(error: DatabaseError) { Log.w(TAG, "Listener was cancelled at .info/connected") } })
Java
// Since I can connect from multiple devices, we store each connection instance separately // any time that connectionsRef's value is null (i.e. has no children) I am offline final FirebaseDatabase database = FirebaseDatabase.getInstance(); final DatabaseReference myConnectionsRef = database.getReference("users/joe/connections"); // Stores the timestamp of my last disconnect (the last time I was seen online) final DatabaseReference lastOnlineRef = database.getReference("/users/joe/lastOnline"); final DatabaseReference connectedRef = database.getReference(".info/connected"); connectedRef.addValueEventListener(new ValueEventListener() { @Override public void onDataChange(DataSnapshot snapshot) { boolean connected = snapshot.getValue(Boolean.class); if (connected) { DatabaseReference con = myConnectionsRef.push(); // When this device disconnects, remove it con.onDisconnect().removeValue(); // When I disconnect, update the last time I was seen online lastOnlineRef.onDisconnect().setValue(ServerValue.TIMESTAMP); // Add this device to my connections list // this value could contain info about the device or a timestamp too con.setValue(Boolean.TRUE); } } @Override public void onCancelled(DatabaseError error) { Log.w(TAG, "Listener was cancelled at .info/connected"); } });