23、Service注解处理:自动生成Service绑定代码、AIDL注解封装

Service 绑定这块代码,说实话,我早期写 Android 的时候没少被它折磨。每次都要写 bindService、写 ServiceConnection、处理 onServiceConnected 回调,还得小心翼翼地管理生命周期。一个项目里 Service 多了,这些模板代码能堆成山。

后来我琢磨着,既然我们有注解处理器,为什么不让它帮我们生成这些绑定代码呢?说白了,就是把那些重复劳动交给机器,我们只关心业务逻辑。

Service 绑定的痛点在哪

你想想看,一个标准的 Service 绑定流程,需要几步?

  1. 定义 AIDL 接口
  2. 在 Service 端实现 Stub
  3. 在客户端写 ServiceConnection
  4. 在 onServiceConnected 里做类型转换
  5. 记得在合适时机解绑

这些步骤里,真正有业务价值的其实只有第 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 接口对应哪个实现。

注解处理器的核心逻辑

处理器要做的事情其实不复杂:

  1. 扫描所有被 @BindService 标记的类
  2. 找到对应的 AIDL 接口
  3. 生成 ServiceConnection 实现类
  4. 生成绑定/解绑的辅助方法

我贴一段核心的处理器代码,你感受一下:

@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.StubMusicPlayerService 接口的适配代码。这样客户端代码就完全不用碰 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 + @AidlInterface 注解处理器 ServiceBindingProcessor 生成绑定代码 XxxBinding.java 生成的绑定类结构 • ServiceConnection 字段 • bind() / unbind() 方法 • getService() 方法 • onServiceReady() 回调 注解处理器内部流程 1. 扫描 @BindService 标记 2. 提取 AIDL 接口信息 3. 生成 ServiceConnection 4. 生成绑定/解绑方法 运行时 调用生成的绑定类,自动完成 Service 连接

从图上可以看得很清楚:源码里的注解标记,经过处理器加工,生成绑定代码,最后在运行时发挥作用。整个过程是编译期完成的,对性能没有影响。

总结一下

Service 注解处理的核心价值,就是把那些「不得不写但又毫无技术含量」的模板代码自动化。你只需要关注两件事:

  • @BindService 标记你的 Service 实现
  • @AidlInterface 标记你的 Stub 实现

剩下的绑定、解绑、类型转换,统统交给处理器。我在实际项目中用这套方案重构了原有的 Service 层,代码量减少了大概 60%,而且再也没出现过忘记解绑导致的内存泄漏。

说白了,好的架构设计就是让正确的事情更容易发生,错误的事情更难发生。注解处理器帮我们做到了这一点。


公众号:蓝海资料掘金营,微信deep3321