第12章 Service组件原理:启动与绑定、前台后台限制、通信与远程服务

Service,说白了就是Android里跑在后台的“隐形工人”。它没有界面,但能干很多活——下载文件、播放音乐、轮询服务器……嗯,我刚开始学Android时,总觉得Service和Thread差不多,后来踩了坑才明白,这俩完全不是一回事。

这一章,我会带你彻底搞懂Service的启动与绑定流程、前台与后台服务的限制、Service与Activity的通信方式,以及RemoteService的跨进程玩法。每个知识点我都会结合自己项目里的真实经历来讲,希望能帮你少走弯路。

核心要点:Service不是线程,它运行在主线程。所有耗时操作必须开子线程处理。

12.1 Service的两种启动方式

Service有两种启动方式:startServicebindService。它们的行为完全不同,我习惯把它们比作“临时工”和“长期合作”。

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简单得多。我画了一张图,帮你快速理解:

Service生命周期流程图 startService() onCreate() onStartCommand() onDestroy() bindService() onCreate() onBind() onUnbind() → onDestroy() 左侧:startService方式 | 右侧:bindService方式

从图中可以看到,两种方式都会先调用 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() 启动前台服务
  • 使用 JobSchedulerWorkManager 替代后台服务

我的建议:如果你的任务不需要立即执行,优先使用 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