23、Service注解处理:自动生成Service绑定代码、AIDL注解封装
Service 绑定这块代码,说实话,我早期写 Android 的时候没少被它折磨。每次都要写 bindService、写 ServiceConnection、处理 onServiceConnected 回调,还得小心翼翼地管理生命周期。一个项目里 Service 多了,这些模板代码能堆成山。
后来我琢磨着,既然我们有注解处理器,为什么不让它帮我们生成这些绑定代码呢?说白了,就是把那些重复劳动交给机器,我们只关心业务逻辑。
Service 绑定的痛点在哪
你想想看,一个标准的 Service 绑定流程,需要几步?
- 定义 AIDL 接口
- 在 Service 端实现 Stub
- 在客户端写 ServiceConnection
- 在 onServiceConnected 里做类型转换
- 记得在合适时机解绑
这些步骤里,真正有业务价值的其实只有第 2 步——实现具体的接口方法。其他全是模板代码。我在项目中遇到过好几次,因为忘记解绑导致的内存泄漏,排查起来特别头疼。
注解驱动的 Service 绑定设计
我的思路是这样的:用注解标记 Service 接口和实现类,然后让注解处理器在编译期生成绑定代码。这样运行时只需要调用生成好的代码,干净利落。
先看核心注解的设计:
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class BindService(
val packageName: String = "",
val action: String = ""
)
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class AidlInterface(
val value: KClass<*>
)
@BindService 标记在 Service 实现类上,@AidlInterface 标记在 Stub 实现类上,告诉处理器哪个 AIDL 接口对应哪个实现。
注解处理器的核心逻辑
处理器要做的事情其实不复杂:
- 扫描所有被
@BindService标记的类 - 找到对应的 AIDL 接口
- 生成 ServiceConnection 实现类
- 生成绑定/解绑的辅助方法
我贴一段核心的处理器代码,你感受一下:
@AutoService(Processor::class)
@SupportedOptions("servicePackageName")
class ServiceBindingProcessor : AbstractProcessor() {
override fun getSupportedAnnotationTypes(): MutableSet<String> {
return mutableSetOf(
BindService::class.java.canonicalName,
AidlInterface::class.java.canonicalName
)
}
override fun process(
annotations: MutableSet<out TypeElement>,
roundEnv: RoundEnvironment
): Boolean {
val serviceMap = mutableMapOf<String, ServiceBindingInfo>()
// 收集所有 @BindService 标记的类
roundEnv.getElementsAnnotatedWith(BindService::class.java)
.filterIsInstance<TypeElement>()
.forEach { element ->
val bindService = element.getAnnotation(BindService::class.java)
val aidlAnnotation = element.getAnnotation(AidlInterface::class.java)
if (aidlAnnotation != null) {
val aidlInterface = aidlAnnotation.value
serviceMap[element.qualifiedName.toString()] = ServiceBindingInfo(
serviceClass = element,
aidlInterfaceClass = aidlInterface.java,
packageName = bindService.packageName,
action = bindService.action
)
}
}
// 为每个 Service 生成绑定代码
serviceMap.forEach { (_, info) ->
generateServiceBinding(info)
}
return true
}
private fun generateServiceBinding(info: ServiceBindingInfo) {
val packageName = info.packageName.ifEmpty {
processingEnv.elementUtils.getPackageOf(info.serviceClass).qualifiedName.toString()
}
val className = "${info.serviceClass.simpleName}Binding"
val fileBuilder = TypeSpec.classBuilder(className)
.addModifiers(Modifier.PUBLIC)
.addField(createServiceConnectionField(info))
.addMethod(createBindMethod(info))
.addMethod(createUnbindMethod())
.addMethod(createGetServiceMethod(info))
val javaFile = JavaFile.builder(packageName, fileBuilder.build()).build()
javaFile.writeTo(processingEnv.filer)
}
}
嗯,这里要注意,生成代码时一定要处理好泛型信息。AIDL 接口的 Stub 类是个抽象类,生成的绑定代码需要正确引用到它。
生成的绑定代码长什么样
假设我们有一个音乐播放 Service,用注解标记后,处理器会生成类似这样的代码:
public class MusicServiceBinding {
private IMusicPlayer musicPlayer;
private ServiceConnection connection;
public void bind(Context context) {
Intent intent = new Intent();
intent.setPackage("com.example.music");
intent.setAction("com.example.action.PLAY_MUSIC");
connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
musicPlayer = IMusicPlayer.Stub.asInterface(service);
// 自动回调,不用手动转换类型
onServiceReady(musicPlayer);
}
@Override
public void onServiceDisconnected(ComponentName name) {
musicPlayer = null;
}
};
context.bindService(intent, connection, Context.BIND_AUTO_CREATE);
}
public void unbind(Context context) {
if (connection != null) {
context.unbindService(connection);
connection = null;
musicPlayer = null;
}
}
public IMusicPlayer getService() {
return musicPlayer;
}
// 这个方法是留给开发者实现的回调
protected void onServiceReady(IMusicPlayer service) {
// 子类重写此方法
}
}
你看,所有模板代码都自动生成了。开发者只需要继承这个生成的类,重写 onServiceReady 方法,就能直接拿到 AIDL 接口实例。
核心思路:把 Service 绑定的「连接-转换-回调」三步走,压缩成一步注解声明。处理器在编译期帮你补齐剩下的所有代码。
AIDL 注解封装
AIDL 本身其实已经很成熟了,但它的使用方式偏底层。我个人习惯再包一层,让调用方完全感知不到 AIDL 的存在。
封装思路是这样的:
// 定义 AIDL 接口的 Kotlin 封装
interface MusicPlayerService {
fun play(songId: String)
fun pause()
fun stop()
fun getCurrentPosition(): Long
}
// 注解标记实现类
@AidlInterface(IMusicPlayer::class)
class MusicPlayerImpl : IMusicPlayer.Stub() {
override fun play(songId: String) {
// 实际播放逻辑
}
override fun pause() = Unit
override fun stop() = Unit
override fun getCurrentPosition(): Long = 0L
}
处理器会检测 @AidlInterface 注解,自动生成从 IMusicPlayer.Stub 到 MusicPlayerService 接口的适配代码。这样客户端代码就完全不用碰 AIDL 了:
class MainActivity : AppCompatActivity() {
private val musicService = MusicServiceBinding()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
musicService.bind(this)
musicService.onServiceReady = { player ->
// player 是 MusicPlayerService 类型,不是 IMusicPlayer
player.play("song_001")
}
}
override fun onDestroy() {
musicService.unbind(this)
super.onDestroy()
}
}
小技巧:生成的绑定类可以做成抽象类,把 onServiceReady 设计成抽象方法。这样子类必须实现,就不会出现忘记处理 Service 连接的情况。我在项目里就是这么干的,效果很好。
避坑指南
我曾经踩过一个坑:多个 Service 同时绑定时,ServiceConnection 的回调顺序是不确定的。如果生成的绑定代码里直接做了全局状态判断,很容易出问题。
解决方案是:每个绑定实例维护自己的状态,不要共享静态变量。生成的代码里,connection 字段是实例级别的,这样就安全了。
注意事项:
- AIDL 接口的方法调用是同步的,不要在 UI 线程调用耗时操作
- 生成的绑定代码默认使用 BIND_AUTO_CREATE,如果不需要自动创建 Service,记得在注解里配置
- 跨进程传递的数据对象必须实现 Parcelable 接口
- 注解处理器生成的代码不要手动修改,否则重新编译后会被覆盖
整体流程回顾
我把整个 Service 注解处理的流程画了张图,方便你理解:
从图上可以看得很清楚:源码里的注解标记,经过处理器加工,生成绑定代码,最后在运行时发挥作用。整个过程是编译期完成的,对性能没有影响。
总结一下
Service 注解处理的核心价值,就是把那些「不得不写但又毫无技术含量」的模板代码自动化。你只需要关注两件事:
- 用
@BindService标记你的 Service 实现 - 用
@AidlInterface标记你的 Stub 实现
剩下的绑定、解绑、类型转换,统统交给处理器。我在实际项目中用这套方案重构了原有的 Service 层,代码量减少了大概 60%,而且再也没出现过忘记解绑导致的内存泄漏。
说白了,好的架构设计就是让正确的事情更容易发生,错误的事情更难发生。注解处理器帮我们做到了这一点。
公众号:蓝海资料掘金营,微信deep3321