Firebase(GeoFire)使用服务器时间戳存位置时重复存储问题
你的代码之所以会生成四条数据,主要有两个核心原因:
1. 持续型监听器导致重复触发
你使用了addValueEventListener来监听serverTime的变化,这个监听器是持续生效的——一旦添加,它会响应serverTime的所有后续更新,并且不会自动移除。当你执行这段代码两次时:
- 第一次执行:添加了第一个监听器,后续
serverTime的任何更新都会触发它的onDataChange方法。 - 第二次执行:又添加了第二个监听器,现在有两个监听器同时监听
serverTime的变化。
每次调用dbRef.child("serverTime").setValue(ServerValue.TIMESTAMP)时,服务器会更新serverTime的值,这会触发所有已存在的监听器执行onDataChange,进而调用setLocation。两次执行代码带来两次serverTime更新,加上两个监听器,就会产生2 × 2 = 4次setLocation调用,对应四条数据。
2. ServerValue.TIMESTAMP的同步机制触发两次回调
ServerValue.TIMESTAMP是让Firebase服务器生成时间戳并写入,这个过程会触发两次onDataChange:
- 第一次:客户端发送写入请求后,本地缓存会先触发一次回调(可能返回临时值或之前的旧值)。
- 第二次:服务器生成实际时间戳并同步回客户端时,会再次触发回调返回最终的时间戳。
这就导致单个监听器也会触发两次setLocation,进一步加剧了重复存储的问题。
解决方案
方案一:改用单次监听器(推荐)
将addValueEventListener替换为addListenerForSingleValueEvent,这个监听器只会执行一次onDataChange(获取当前最新的serverTime值),之后自动移除,不会持续监听后续变化:
//Get reference to Firebase DB dbRef = FirebaseDatabase.getInstance().getReference(); // Get reference to Geofire inside FIrebaseDB dbRefGeofire = FirebaseDatabase.getInstance().getReference("geofire"); geoFire = new GeoFire(dbRefGeofire); dbRef.child("serverTime").setValue(ServerValue.TIMESTAMP); // 使用单次监听器,仅获取一次serverTime值 dbRef.child("serverTime").addListenerForSingleValueEvent(new ValueEventListener() { @Override public void onDataChange(DataSnapshot dataSnapshot) { serverTime = String.valueOf(dataSnapshot.getValue()); geoFire.setLocation(serverTime, new GeoLocation(latLng.latitude,latLng.longitude), new GeoFire.CompletionListener() { @Override public void onComplete(String key, DatabaseError error) { if (error != null) { Log.v("Informe","There was an error saving the location to GeoFire: " + error); } else { Log.v("Informe","Location saved on server successfully!"); } } }); } @Override public void onCancelled(DatabaseError databaseError) { } });
方案二:手动移除监听器
如果必须使用持续型监听器,记得在完成操作后调用removeEventListener清理,避免监听器累积:
// 先定义监听器对象 ValueEventListener timeListener = new ValueEventListener() { @Override public void onDataChange(DataSnapshot dataSnapshot) { serverTime = String.valueOf(dataSnapshot.getValue()); geoFire.setLocation(serverTime, new GeoLocation(latLng.latitude,latLng.longitude), new GeoFire.CompletionListener() { @Override public void onComplete(String key, DatabaseError error) { if (error != null) { Log.v("Informe","There was an error saving the location to GeoFire: " + error); } else { Log.v("Informe","Location saved on server successfully!"); // 完成后移除监听器 dbRef.child("serverTime").removeEventListener(timeListener); } } }); } @Override public void onCancelled(DatabaseError databaseError) { } }; // 添加监听器 dbRef.child("serverTime").addValueEventListener(timeListener);
方案三:避免依赖serverTime监听(简化逻辑)
其实你可以直接使用客户端生成的时间戳(或GeoFire的自动键)来存储位置,避免监听serverTime的复杂逻辑:
// 使用客户端当前时间戳作为键(注意:客户端时间可能有偏差,若需严格服务器时间则不推荐) String clientTime = String.valueOf(System.currentTimeMillis()); geoFire.setLocation(clientTime, new GeoLocation(latLng.latitude,latLng.longitude), new GeoFire.CompletionListener() { @Override public void onComplete(String key, DatabaseError error) { if (error != null) { Log.v("Informe","There was an error saving the location to GeoFire: " + error); } else { Log.v("Informe","Location saved on server successfully!"); } } }); // 或者使用GeoFire的push()生成唯一键(无需关心时间戳) DatabaseReference pushRef = dbRefGeofire.push(); geoFire.setLocation(pushRef.getKey(), new GeoLocation(latLng.latitude,latLng.longitude), new GeoFire.CompletionListener() { @Override public void onComplete(String key, DatabaseError error) { if (error != null) { Log.v("Informe","There was an error saving the location to GeoFire: " + error); } else { Log.v("Informe","Location saved on server successfully!"); } } });
内容的提问来源于stack exchange,提问作者CarlosMacMar

