
1. 应用概述与核心挑战
开发一个能够实时显示当前速度并将其持续保存的应用,主要面临以下挑战:
- 后台运行能力: 应用需要在用户关闭屏幕或切换到其他应用时,仍能持续获取位置信息并计算速度。
- UI实时更新: 如何将后台服务获取的速度数据高效、安全地传递给主界面,并实时更新UI。
- 权限管理: Android系统对位置权限有严格要求,尤其是后台位置访问权限。
- 数据持久化: 将实时速度数据保存到数据库中。
为了解决这些问题,我们将采用前台服务(Foreground Service)来处理位置更新,并使用EventBus库作为服务与Activity之间通信的桥梁。
2. 权限声明与请求
首先,在 AndroidManifest.xml 中声明必要的权限:
在 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 的发布/订阅事件总线,可以简化组件之间的通信。
Android文件存取与数据库编程知识,文件操作主要是读文件、写文件、读取静态文件等,同时还介绍了创建添加文件内容并保存,打开文件并显示内容;数据库编程方面主要介绍了SQLite数据库的使用、包括创建、删除、打开数据库、非查询SQL操作指令、查询SQL指令-游标Cursors等知识。
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 则提供了一种高效、解耦的方式将后台数据实时同步到用户界面,极大地提升了应用的用户体验和功能稳定性。








