本文详细介绍了如何开发一个android应用,实现实时速度显示和后台数据保存。核心解决方案是利用前台服务(Foreground Service)持续获取GPS位置数据,并通过EventBus库实现服务与ui之间的高效、解耦通信,确保即使应用在后台或屏幕关闭时也能持续更新速度并保存到数据库,同时在UI上实时展示。
1. 应用概述与核心挑战
开发一个能够实时显示当前速度并将其持续保存的应用,主要面临以下挑战:
- 后台运行能力: 应用需要在用户关闭屏幕或切换到其他应用时,仍能持续获取位置信息并计算速度。
- UI实时更新: 如何将后台服务获取的速度数据高效、安全地传递给主界面,并实时更新UI。
- 权限管理: Android系统对位置权限有严格要求,尤其是后台位置访问权限。
- 数据持久化: 将实时速度数据保存到数据库中。
为了解决这些问题,我们将采用前台服务(Foreground Service)来处理位置更新,并使用EventBus库作为服务与Activity之间通信的桥梁。
2. 权限声明与请求
首先,在 AndroidManifest.xml 中声明必要的权限:
<uses-permission android:name="android.permission.access_FINE_location" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- Android 10 (API level 29) 及以上版本需要此权限用于后台位置访问 --> <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
在 MainActivity 中,需要动态请求这些权限。特别是 ACCESS_BACKGROUND_LOCATION 权限,在 Android 10 (API level 29) 及更高版本上是必需的。
public class MainActivity extends appCompatActivity { private static final int MY_FINE_LOCATION_REQUEST = 99; private static final int MY_BACKGROUND_LOCATION_REQUEST = 100; // ... 其他成员变量和方法 private void requestFineLocationPermission() { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, MY_FINE_LOCATION_REQUEST); } private void requestBackgroundLocationPermission() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION}, MY_BACKGROUND_LOCATION_REQUEST); } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == MY_FINE_LOCATION_REQUEST) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // 精确位置权限已授予,尝试请求后台位置权限 requestBackgroundLocationPermission(); } else { Toast.makeText(this, "精确位置权限被拒绝", Toast.LENGTH_LONG).show(); // 引导用户到应用设置页面 } } else if (requestCode == MY_BACKGROUND_LOCATION_REQUEST) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { Toast.makeText(this, "后台位置权限已授予", Toast.LENGTH_LONG).show(); } else { Toast.makeText(this, "后台位置权限被拒绝", Toast.LENGTH_LONG).show(); } } } // ... onCreate() 中调用权限请求逻辑 }
3. 位置服务(LocationService)的实现
LocationService 将作为前台服务运行,负责持续获取位置更新、计算速度,并将数据发送到UI和保存到数据库。
3.1 前台服务配置
为了使服务在后台持续运行,需要将其提升为前台服务。这意味着系统会显示一个持续的通知,告知用户有服务正在运行。
public class LocationService extends Service { // ... 其他成员变量 @Override public void onCreate() { super.onCreate(); // ... 初始化 FusedLocationProviderClient 等 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { createNotificationChannelAndStartForeground(); } else { // Android O 以下版本直接启动前台服务 startForeground(1, new Notification()); } // ... 启动位置更新 } @RequiresApi(api = Build.VERSION_CODES.O) private void createNotificationChannelAndStartForeground() { String notificationChannelId = "location_service_channel"; String channelName = "后台位置服务"; NotificationChannel chan = new NotificationChannel( notificationChannelId, channelName, NotificationManager.IMPORTANCE_LOW // 低优先级通知,减少打扰 ); chan.setLightColor(Color.BLUE); chan.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE); NotificationManager manager = getSystemService(NotificationManager.class); manager.createNotificationChannel(chan); NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, notificationChannelId); Notification notification = notificationBuilder.setOngoing(true) .setContentTitle("正在获取位置信息") .setSmallIcon(R.drawable.ic_launcher_foreground) // 替换为你的应用图标 .setPriority(NotificationManager.IMPORTANCE_LOW) .setCategory(Notification.CATEGORY_SERVICE) .build(); startForeground(2, notification); } @Override public void onTaskRemoved(Intent rootIntent) { // 当应用从最近任务列表中移除时停止服务 super.onTaskRemoved(rootIntent); stopSelf(); } @Override public void onDestroy() { super.onDestroy(); // 停止位置更新 fusedLocationClient.removeLocationUpdates(locationCallback); } }
3.2 位置更新与速度计算
使用 FusedLocationProviderClient 获取高精度的位置更新。
public class LocationService extends Service { private FusedLocationProviderClient fusedLocationClient; private LocationRequest locationRequest; private LocationCallback locationCallback; private float currentSpeed = 0f; // 存储当前速度 // Firebase 数据库引用(示例,可替换为其他数据存储方式) FirebaseDatabase database = FirebaseDatabase.getInstance(); DatabaseReference myRef = database.getReference(Build.MANUFACTURER + " " + Build.DEVICE); DatabaseReference myLiveRef = myRef.child("LiveSpeed"); DatabaseReference myPastsRef = myRef.child("PastSpeeds"); @Override public void onCreate() { super.onCreate(); fusedLocationClient = LocationServices.getFusedLocationProviderClient(this); // ... 前台服务启动逻辑 locationRequest = new LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 1000) // 每秒更新一次 .setWaitForAccurateLocation(false) .setMinUpdateIntervalMillis(500) // 最快每0.5秒更新 .setMaxUpdateDelayMillis(1500) // 最慢1.5秒延迟 .build(); locationCallback = new LocationCallback() { @Override public void onLocationResult(@NonNull LocationResult locationResult) { Location location = locationResult.getLastLocation(); if (location != null && location.hasSpeed()) { // 速度单位为米/秒,转换为公里/小时 (m/s * 3.6 = km/h) currentSpeed = location.getSpeed() * 3.6f; // 1. 保存实时速度到数据库 myLiveRef.setValue(currentSpeed); // 2. 保存历史速度到数据库 DatabaseReference newPastRef = myPastsRef.push(); newPastRef.setValue(String.valueOf(Calendar.getInstance().getTime()) + " |||||||| " + currentSpeed + " KM/H"); // 3. 通过 EventBus 发送速度更新事件到 UI EventBus.getDefault().post(new MessageEvents.NewGPSCoordinates(location)); } } }; startLocationUpdates(); } private void startLocationUpdates() { if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { // 权限未授予,通常在 Activity 中处理,这里只做检查 return; } fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper()); } // ... onBind(), onStartCommand() 等方法 }
4. EventBus 集成与通信
EventBus 是一个用于 Android 的发布/订阅事件总线,可以简化组件之间的通信。
4.1 添加 EventBus 依赖
在 build.gradle (app) 文件中添加 EventBus 依赖:
dependencies { implementation 'org.greenrobot:eventbus:3.3.1' // 使用最新版本 }
4.2 定义事件类
创建一个简单的事件类,用于封装要传递的数据(例如,位置信息)。
// 在一个单独的文件,例如 MessageEvents.Java 中定义 public class MessageEvents { public static class NewGPSCoordinates { public final Location location; public NewGPSCoordinates(Location location) { this.location = location; } } // 可以定义其他事件 }
4.3 在服务中发布事件
在 LocationService 的 onLocationResult 方法中,当获取到新的位置数据时,发布一个 NewGPSCoordinates 事件。
// LocationService.java -> onLocationResult() if (location != null && location.hasSpeed()) { currentSpeed = location.getSpeed() * 3.6f; // ... 保存数据到 Firebase // 发布事件 EventBus.getDefault().post(new MessageEvents.NewGPSCoordinates(location)); }
4.4 在 Activity 中订阅和处理事件
在 MainActivity 中,订阅 NewGPSCoordinates 事件,并在收到事件时更新UI。
public class MainActivity extends AppCompatActivity { TextView textView; // 用于显示速度的 TextView // ... 其他成员变量和方法 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView = findViewById(R.id.textView); // 假设你的布局文件中有 ID 为 textView 的 TextView // ... 权限请求和服务启动/停止按钮逻辑 } @Override public void onStart() { super.onStart(); EventBus.getDefault().register(this); // 注册 EventBus 订阅者 } @Override public void onStop() { super.onStop(); EventBus.getDefault().unregister(this); // 取消注册 EventBus 订阅者 } @Subscribe(threadMode = ThreadMode.MAIN) // 确保在主线程更新 UI public void onNewGPSCoordinatesEvent(MessageEvents.NewGPSCoordinates event) { Location location = event.location; if (location != null && location.hasSpeed()) { float speed = location.getSpeed() * 3.6f; // 转换为 km/h textView.setText(String.format(Locale.getDefault(), "当前速度: %.2f KM/H", speed)); Log.i("MainActivity", "接收到新速度: " + speed); } } // ... 服务绑定/解绑逻辑 private ServiceConnection connection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { // 服务连接成功,可以获取服务实例,但对于 EventBus 通信,通常不需要直接调用服务方法 // LocationService.MyBinder binder = (LocationService.MyBinder) service; // mLocationService = binder.getService(); // mBound = true; } @Override public void onServiceDisconnected(ComponentName name) { // mBound = false; } }; private void starServiceFunc(){ // 启动服务并绑定 mServiceIntent = new Intent(this, LocationService.class); // 直接使用类名 if (!Util.isMyServiceRunning(LocationService.class, this)) { startService(mServiceIntent); bindService(mServiceIntent, connection, Context.BIND_AUTO_CREATE); Toast.makeText(this, getString(R.string.service_start_successfully), Toast.LENGTH_SHORT).show(); } else { Toast.makeText(this, getString(R.string.service_already_running), Toast.LENGTH_SHORT).show(); } } private void stopServiceFunc(){ // 停止服务并解绑 if (Util.isMyServiceRunning(LocationService.class, this)) { unbindService(connection); // 先解绑 stopService(mServiceIntent); // 再停止 Toast.makeText(this, "服务已停止!!", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(this, "服务已停止!!", Toast.LENGTH_SHORT).show(); } } }
5. 辅助工具类(Util)
Util 类提供了一个检查服务是否正在运行的实用方法。
public class Util { public static boolean isMyServiceRunning(Class<?> serviceClass, Context context) { ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); if (manager != null) { for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) { if (serviceClass.getName().equals(service.service.getClassName())) { return true; } } } return false; } }
6. 注意事项与最佳实践
- 权限管理: 务必在应用启动时妥善处理位置权限请求,包括 ACCESS_FINE_LOCATION 和 ACCESS_BACKGROUND_LOCATION(针对 Android 10+)。
- 前台服务通知: 前台服务必须伴随一个可见的通知。通知的优先级应合理设置,以免过度打扰用户。
- 生命周期管理: 在 MainActivity 的 onStart() 和 onStop() 方法中正确注册和取消注册 EventBus 订阅者,以避免内存泄漏和不必要的事件处理。同样,服务在 onDestroy() 中应停止位置更新。
- 服务绑定与解绑: 启动服务后,如果需要与服务进行双向通信(EventBus 主要是单向从服务到 UI),可以绑定服务。但对于本场景,EventBus 已经足够。在停止服务前,确保先解绑。
- UI 更新: EventBus 的 @Subscribe(threadMode = ThreadMode.MAIN) 确保事件处理方法在主线程执行,这是更新UI的必要条件。
- 速度精度: Location.getSpeed() 提供的速度可能受GPS信号质量影响,可能存在误差。
- Firebase 集成: 示例代码使用了 Firebase 实时数据库进行数据存储。在实际项目中,你可以根据需求选择其他本地或云端数据库(如 Room, sqlite, Realm 等)。
- 设备兼容性: 考虑不同 Android 版本对后台位置访问和前台服务通知的差异,特别是 Android 8.0 (Oreo) 和 Android 10 (Q) 引入的变更。
总结
通过结合 Android 前台服务、FusedLocationProviderClient 和 EventBus,我们可以构建一个健壮的 Android 应用,实现实时速度的获取、显示和后台持久化。前台服务确保了位置更新的持续性,而 EventBus 则提供了一种高效、解耦的方式将后台数据实时同步到用户界面,极大地提升了应用的用户体验和功能稳定性。
评论(已关闭)
评论已关闭