Android车载黑盒服务断电时CSV数据安全写入方案咨询
问题背景
我正在开发一款作为车载黑盒的Android后台服务,服务随设备开机启动,负责将GPS获取的位置、速度等信息记录到CSV文件。以下是我的服务代码:
public class VehicleBlackBox extends Service implements SimpleLocation.Listener { private final String NOTIFICATION_CHANNEL_ID; private final String channelName; private SimpleLocation location; private FileOutputStream fos; private PrintWriter pw; private final MessageFormat formatter; public VehicleBlackBox() { NOTIFICATION_CHANNEL_ID = "com.example.andy.myapplication"; channelName = "My Background Service"; formatter = new MessageFormat("{0},{1},{2},{3},{4}"); } @Override public void onCreate() { super.onCreate(); location = new SimpleLocation(this, true, false, 3000); } @Override public IBinder onBind(Intent intent) { return null; } @Override public int onStartCommand(Intent intent, int flags, int startId) { Notification notification = GenerateNotification(); startForeground(1337, notification); try { LocalDateTime current = LocalDateTime.now(); DateTimeFormatter format = DateTimeFormatter.ofPattern("dd-MM-yyyy"); String formatedDateTime = current.format(format); File dir = new File(Environment.getExternalStorageDirectory(), "SweetSwift"); if (!dir.exists()) { dir.mkdirs(); } File file = new File(dir, "Session " + formatedDateTime + ".csv"); fos = new FileOutputStream(file, true); pw = new PrintWriter(fos); } catch (Exception e) { e.printStackTrace(); fos = null; pw = null; } location.beginUpdates(); return START_STICKY; } @Override public void onDestroy() { super.onDestroy(); location.endUpdates(); try { if(pw != null) pw.close(); if(fos != null) fos.close(); } catch (Exception e) { e.printStackTrace(); } stopForeground(true); } private Notification GenerateNotification(){ NotificationChannel chan = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_NONE); chan.setLightColor(Color.RED); chan.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC); NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); manager.createNotificationChannel(chan); Intent notificationIntent = new Intent(this, MainActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE); return new Notification.Builder(this,NOTIFICATION_CHANNEL_ID) .setSmallIcon(R.mipmap.ic_launcher) .setContentTitle("Swift Black Box") .setOngoing(true) .setTicker("Your riding session is being monitored for later anaylisis") .setSubText("Vehicle driving performance measuring system") .setContentIntent(pendingIntent).build(); } private void updateTripInfo(double longitude, double latitude, double altitude, double speed, long timestamp) { if(fos != null && pw != null) { pw.println(formatter.format(new Object[]{longitude, latitude, altitude, speed, timestamp})); pw.flush(); } } @Override public void onPositionChanged() { updateTripInfo(location.getLongitude(), location.getLatitude(), location.getAltitude(), location.getSpeed(), location.getTimestampInMilliseconds()); } }
当前代码中,我担心车辆熄火断电导致设备直接关机时,onDestroy()等生命周期方法无法被调用,未关闭的文件可能导致数据丢失。每次写入都开关文件的方案因性能问题不可行,想寻求其他安全可靠的文件写入方案。
可行解决方案
1. 强制刷写缓冲区+定期同步磁盘
你已经在每次写入后调用了pw.flush(),但这只是把数据刷到操作系统的缓冲区,并没有真正写入磁盘。可以在每次flush后,调用fos.getFD().sync()强制将缓冲区数据同步到磁盘。这样即使突然断电,已经flush并sync的数据会被持久化。
修改updateTripInfo方法:
private void updateTripInfo(double longitude, double latitude, double altitude, double speed, long timestamp) { if(fos != null && pw != null) { pw.println(formatter.format(new Object[]{longitude, latitude, altitude, speed, timestamp})); pw.flush(); try { fos.getFD().sync(); // 强制同步到磁盘 } catch (IOException e) { e.printStackTrace(); } } }
注意:sync()会有一定性能开销,但相比数据丢失,这个开销在车载场景下是可接受的——你的GPS更新频率是3秒一次,频率不高,不会有明显性能问题。
2. 使用BufferedWriter替代PrintWriter,精准控制IO
PrintWriter的缓冲机制可能隐藏细节,直接使用BufferedWriter配合FileOutputStream,既能减少IO次数,又能更精准控制数据流向:
// 替换原有PrintWriter为BufferedWriter private BufferedWriter bw; // 在onStartCommand中初始化: bw = new BufferedWriter(new OutputStreamWriter(fos)); // updateTripInfo方法修改为: private void updateTripInfo(double longitude, double latitude, double altitude, double speed, long timestamp) { if(bw != null) { try { String line = formatter.format(new Object[]{longitude, latitude, altitude, speed, timestamp}) + "\n"; bw.write(line); bw.flush(); fos.getFD().sync(); } catch (IOException e) { e.printStackTrace(); } } }
BufferedWriter会缓存少量数据,减少实际磁盘写入次数,同时配合sync()保证数据持久化。
3. 监听系统关机广播(处理正常关机场景)
虽然设备突然断电时广播无法送达,但如果是系统正常触发的关机(比如车辆熄火后设备电池耗尽前的关机),可以监听ACTION_SHUTDOWN广播,在广播接收器中执行文件关闭和同步操作:
private BroadcastReceiver shutdownReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (Intent.ACTION_SHUTDOWN.equals(intent.getAction())) { // 执行文件关闭和同步 try { if(pw != null) { pw.flush(); pw.close(); } if(fos != null) { fos.getFD().sync(); fos.close(); } } catch (IOException e) { e.printStackTrace(); } } } }; @Override public void onCreate() { super.onCreate(); location = new SimpleLocation(this, true, false, 3000); // 注册关机广播 IntentFilter filter = new IntentFilter(Intent.ACTION_SHUTDOWN); registerReceiver(shutdownReceiver, filter); } @Override public void onDestroy() { super.onDestroy(); location.endUpdates(); // 注销广播接收器 unregisterReceiver(shutdownReceiver); // 原有关闭逻辑 try { if(pw != null) pw.close(); if(fos != null) fos.close(); } catch (Exception e) { e.printStackTrace(); } stopForeground(true); }
注意:这个方法只能处理系统正常关机的情况,突然断电时依然依赖前面的sync()操作。
4. 用SQLite暂存数据,定期导出CSV
如果数据可靠性要求极高,可以先将GPS数据写入SQLite数据库,然后定期(比如每5分钟)导出为CSV文件。数据库本身有事务和持久化机制,即使突然断电,已提交的数据库数据不会丢失。后续可以在设备重启后补充导出CSV,或者直接从数据库读取数据。
这种方案的优势是数据可靠性更高,缺点是增加了数据库操作的复杂度,但对于车载黑盒这种对数据完整性要求高的场景,是值得考虑的。
内容的提问来源于stack exchange,提问作者Zain Ul Abidin

