第12章 Service组件原理:启动与绑定、前台后台限制、通信与远程服务
Service,说白了就是Android里跑在后台的“隐形工人”。它没有界面,但能干很多活——下载文件、播放音乐、轮询服务器……嗯,我刚开始学Android时,总觉得Service和Thread差不多,后来踩了坑才明白,这俩完全不是一回事。
这一章,我会带你彻底搞懂Service的启动与绑定流程、前台与后台服务的限制、Service与Activity的通信方式,以及RemoteService的跨进程玩法。每个知识点我都会结合自己项目里的真实经历来讲,希望能帮你少走弯路。
核心要点:Service不是线程,它运行在主线程。所有耗时操作必须开子线程处理。
12.1 Service的两种启动方式
Service有两种启动方式:startService 和 bindService。它们的行为完全不同,我习惯把它们比作“临时工”和“长期合作”。
12.1.1 startService:启动即运行
调用 startService() 后,Service会一直运行,直到你调用 stopService() 或 Service 内部调用 stopSelf()。它和调用者没有直接关联——调用者退出了,Service还在跑。
// 启动Service
Intent intent = new Intent(this, MyService.class);
startService(intent);
// 停止Service
stopService(intent);
我在项目中遇到过一个问题:用户退出App后,Service还在后台下载文件。这其实是正常行为,但如果你忘了在合适的时机停止Service,就会造成内存泄漏。嗯,这里要注意——startService启动的Service,必须手动停止。
12.1.2 bindService:绑定即合作
调用 bindService() 后,Service会与调用者“绑定”在一起。调用者销毁时,Service也会自动解绑并销毁。这种模式适合需要交互的场景。
// 绑定Service
Intent intent = new Intent(this, MyService.class);
bindService(intent, connection, Context.BIND_AUTO_CREATE);
// 解绑Service
unbindService(connection);
你想想看,如果Activity需要从Service获取数据,用bindService就对了。我曾经在一个音乐播放器项目里,用bindService让Activity控制播放、暂停、获取当前进度——非常方便。
个人经验:如果Service既需要长期运行,又需要与Activity交互,可以同时使用startService和bindService。先startService保证Service不因Activity销毁而停止,再bindService获取交互接口。
12.2 Service的生命周期
Service的生命周期比Activity简单得多。我画了一张图,帮你快速理解:
从图中可以看到,两种方式都会先调用 onCreate(),但后续流程完全不同:
- startService:onCreate() → onStartCommand() → onDestroy()
- bindService:onCreate() → onBind() → onUnbind() → onDestroy()
我曾经犯过一个错误:在 onStartCommand() 里做了大量初始化工作,结果每次启动Service都会重复执行。后来才意识到,onCreate() 才是只执行一次的地方。
避坑指南:我曾经在 onStartCommand() 里返回了 START_NOT_STICKY,结果Service被系统杀死后没有自动重启,导致后台任务中断。如果你的Service需要保证稳定性,建议返回 START_STICKY。
12.3 前台服务与后台服务限制
Android从8.0开始对后台服务做了严格限制。说白了,系统不想让App在后台偷偷耗电。如果你的Service在后台运行,系统可能会随时杀掉它。
12.3.1 前台服务
前台服务会显示一个持续的通知,告诉用户“这个App正在干活”。比如音乐播放器、导航软件,都会用前台服务。
// 创建通知渠道(Android 8.0+)
NotificationChannel channel = new NotificationChannel(
"music_channel",
"音乐播放",
NotificationManager.IMPORTANCE_LOW
);
NotificationManager manager = getSystemService(NotificationManager.class);
manager.createNotificationChannel(channel);
// 构建通知
Notification notification = new Notification.Builder(this, "music_channel")
.setContentTitle("正在播放")
.setContentText("周杰伦 - 七里香")
.setSmallIcon(R.drawable.ic_music)
.build();
// 启动前台服务
startForeground(1, notification);
嗯,这里要注意——Android 9.0以后,前台服务需要申请 FOREGROUND_SERVICE 权限。我有个朋友没加这个权限,App在Android 12上直接崩溃了。
12.3.2 后台服务限制
从Android 8.0开始,后台App不能通过 startService() 启动服务。如果你尝试这样做,系统会抛出 IllegalStateException。解决办法有两个:
- 使用
startForegroundService()启动前台服务 - 使用
JobScheduler或WorkManager替代后台服务
我的建议:如果你的任务不需要立即执行,优先使用 WorkManager。它兼容性好,还能保证任务在合适时机执行。我在一个数据同步项目里用WorkManager替代了后台Service,电池续航提升了30%。
12.4 Service与Activity通信
Service和Activity通信,说白了就是“跨组件传数据”。常用的方式有三种:
12.4.1 通过Binder直接通信
这是bindService最常用的方式。Service返回一个Binder对象,Activity通过它调用Service的方法。
// Service端
public class MyService extends Service {
private final IBinder binder = new LocalBinder();
public class LocalBinder extends Binder {
MyService getService() {
return MyService.this;
}
}
public int add(int a, int b) {
return a + b;
}
@Override
public IBinder onBind(Intent intent) {
return binder;
}
}
// Activity端
private MyService myService;
private ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
MyService.LocalBinder binder = (MyService.LocalBinder) service;
myService = binder.getService();
int result = myService.add(3, 5); // 直接调用
}
@Override
public void onServiceDisconnected(ComponentName name) {
myService = null;
}
};
这种方式简单直接,但只能在同一个进程内使用。我习惯在小型项目里用这种方式,代码量少,逻辑清晰。
12.4.2 通过BroadcastReceiver通信
如果Service需要主动向Activity发送消息,可以用广播。比如下载进度更新:
// Service中发送广播
Intent intent = new Intent("DOWNLOAD_PROGRESS");
intent.putExtra("progress", 50);
sendBroadcast(intent);
// Activity中注册广播
BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
int progress = intent.getIntExtra("progress", 0);
// 更新UI
}
};
registerReceiver(receiver, new IntentFilter("DOWNLOAD_PROGRESS"));
嗯,这里要注意——记得在Activity销毁时取消注册广播,否则会内存泄漏。我曾经因为这个被测试同学提了bug,尴尬得很。
12.4.3 通过Messenger通信
Messenger是基于Handler的跨进程通信方式。它比Binder更安全,因为数据会序列化到Message中。
// Service端
Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
// 处理来自Activity的消息
}
};
Messenger messenger = new Messenger(handler);
@Override
public IBinder onBind(Intent intent) {
return messenger.getBinder();
}
// Activity端
Messenger serviceMessenger;
ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
serviceMessenger = new Messenger(service);
Message msg = Message.obtain();
msg.what = 1;
msg.obj = "Hello Service";
serviceMessenger.send(msg);
}
};
我个人觉得,Messenger适合需要双向通信的场景,但性能不如Binder。如果数据量大,还是用Binder吧。
12.5 RemoteService:跨进程服务
RemoteService,说白了就是运行在另一个进程里的Service。它通过AIDL(Android Interface Definition Language)实现跨进程通信。
12.5.1 定义AIDL接口
// IRemoteService.aidl
interface IRemoteService {
int add(int a, int b);
String getData();
}
定义好AIDL文件后,Android Studio会自动生成对应的Java接口。你只需要在Service中实现它:
// Service端
public class RemoteService extends Service {
private final IRemoteService.Stub binder = new IRemoteService.Stub() {
@Override
public int add(int a, int b) throws RemoteException {
return a + b;
}
@Override
public String getData() throws RemoteException {
return "来自远程服务的数据";
}
};
@Override
public IBinder onBind(Intent intent) {
return binder;
}
}
12.5.2 客户端调用
// 绑定远程服务
Intent intent = new Intent("com.example.REMOTE_SERVICE");
intent.setPackage("com.example.remoteapp");
bindService(intent, connection, Context.BIND_AUTO_CREATE);
// 通过AIDL接口调用
IRemoteService remoteService;
ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
remoteService = IRemoteService.Stub.asInterface(service);
try {
int result = remoteService.add(10, 20);
String data = remoteService.getData();
} catch (RemoteException e) {
e.printStackTrace();
}
}
};
避坑指南:我曾经在AIDL接口里传了一个大对象,结果跨进程通信卡了整整3秒。后来才意识到,跨进程传输数据需要序列化和反序列化,数据量越大,性能越差。建议只传基本类型或小型Parcelable对象。
12.6 实战经验总结
最后,我把自己这些年用Service的经验整理成了一张表,方便你参考:
| 场景 | 推荐方案 | 注意事项 |
|---|---|---|
| 后台下载文件 | 前台Service + 子线程 | Android 8.0+必须用前台Service |
| 音乐播放 | 前台Service | 需要显示通知,且不能被杀 |
| 定时任务 | WorkManager | 比Service更省电,兼容性更好 |
| 与Activity交互 | Binder(同进程)或AIDL(跨进程) | 注意线程安全,Binder方法默认在主线程执行 |
| 跨进程调用 | AIDL + RemoteService | 数据量不要太大,避免ANR |
嗯,Service这块内容确实不少。但只要你理解了启动与绑定的区别,掌握了前台服务的用法,再配合Binder或AIDL做通信,大部分场景都能搞定。我在项目里踩过的坑,希望你能避开。
最后一个小技巧:调试Service时,可以在 onCreate() 和 onDestroy() 里加日志,这样你能清楚看到Service的生命周期。我曾经靠这个定位了一个Service被反复创建的问题——原来是某个Activity在onResume里重复调用了startService。
公众号:蓝海资料掘金营,微信deep3321