21、Intent注解处理:自动生成Activity跳转代码、Bundle参数注入

说实话,做Android开发这么多年,最让我烦的就是写Activity跳转和传参。你想想看,每次都要写一堆Intent.putExtra,然后目标Activity里又要写一堆getStringExtra、getIntExtra。代码量倒是不大,但就是烦——而且特别容易出错。

我记得有一次,项目里有个页面改了参数名,结果忘了改接收方。测试的时候没发现,上线后用户反馈说页面数据不对。排查了半天,最后发现是key写错了。这种低级错误,说白了就是手写Intent传参的锅。

那怎么解决?用注解处理器自动生成跳转代码和Bundle注入代码。今天我们就来聊聊这个方案。

21.1 核心思路:编译期生成,运行时零反射

这里我要强调一点:我们用的是注解处理器(APT),不是运行时反射。很多人一听到「注解」就想到反射,其实不是一回事。注解处理器在编译期就把代码生成了,运行时根本不需要反射,性能完全没影响。

整个方案的核心流程是这样的:

// 1. 在源Activity上标记注解
@Router(path = "app/user/profile")
class UserProfileActivity : AppCompatActivity() {
    
    @Arg
    lateinit var userId: String
    
    @Arg
    var age: Int = 0
}

// 2. 编译期自动生成跳转辅助类
// 生成文件:UserProfileActivityRouter.kt
object UserProfileActivityRouter {
    fun createIntent(context: Context, userId: String, age: Int): Intent {
        return Intent(context, UserProfileActivity::class.java).apply {
            putExtra("userId", userId)
            putExtra("age", age)
        }
    }
    
    fun inject(activity: UserProfileActivity) {
        activity.userId = activity.intent?.getStringExtra("userId") ?: ""
        activity.age = activity.intent?.getIntExtra("age", 0) ?: 0
    }
}

// 3. 使用时一行代码搞定
UserProfileActivityRouter.createIntent(this, "123", 25).let { startActivity(it) }

嗯,这里要注意:生成的代码里,inject方法是在目标Activity的onCreate里调用的。我个人习惯在基类里统一处理,这样每个子类都不用写重复代码。

21.2 注解定义:@Router 和 @Arg

我们先定义两个注解。一个标记路由路径,一个标记需要注入的参数。

// @Router 注解
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)  // 编译期就够了
annotation class Router(val path: String)

// @Arg 注解
@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.SOURCE)
annotation class Arg(val key: String = "")  // 空字符串表示使用字段名作为key

为什么Retention用SOURCE?因为注解处理器在编译期就处理完了,生成的代码里没有注解信息,所以SOURCE就够了,不用浪费空间。

21.3 注解处理器:核心逻辑

接下来是重头戏——注解处理器。我直接贴核心代码,然后逐段解释。

@AutoService(Processor::class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedOptions("moduleName")
class RouterProcessor : AbstractProcessor() {
    
    private lateinit var filer: Filer
    private lateinit var messager: Messager
    
    override fun init(processingEnv: ProcessingEnvironment) {
        super.init(processingEnv)
        filer = processingEnv.filer
        messager = processingEnv.messager
    }
    
    override fun getSupportedAnnotationTypes(): Set<String> {
        return setOf(Router::class.java.canonicalName, Arg::class.java.canonicalName)
    }
    
    override fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Boolean {
        // 获取所有被@Router标记的类
        val routerElements = roundEnv.getElementsAnnotatedWith(Router::class.java)
        
        for (element in routerElements) {
            if (element.kind != ElementKind.CLASS) continue
            val classElement = element as TypeElement
            val router = classElement.getAnnotation(Router::class.java)
            
            // 获取该类中所有被@Arg标记的字段
            val argFields = mutableListOf<Pair<String, String>>()  // (fieldName, fieldType)
            for (enclosed in classElement.enclosedElements) {
                if (enclosed.kind != ElementKind.FIELD) continue
                val arg = enclosed.getAnnotation(Arg::class.java) ?: continue
                val fieldName = enclosed.simpleName.toString()
                val fieldType = enclosed.asType().toString()
                argFields.add(fieldName to fieldType)
            }
            
            // 生成代码
            generateRouterClass(classElement, router.path, argFields)
        }
        
        return true
    }
    
    private fun generateRouterClass(
        classElement: TypeElement,
        path: String,
        argFields: List<Pair<String, String>>
    ) {
        val packageName = processingEnv.elementUtils.getPackageOf(classElement).qualifiedName.toString()
        val className = classElement.simpleName.toString()
        val routerClassName = "${className}Router"
        
        // 构建代码字符串
        val code = buildString {
            appendLine("package $packageName")
            appendLine()
            appendLine("import android.content.Context")
            appendLine("import android.content.Intent")
            appendLine()
            appendLine("object $routerClassName {")
            appendLine()
            
            // createIntent 方法
            appendLine("    fun createIntent(context: Context, ${argFields.joinToString(", ") { "${it.first}: ${it.second}" }}): Intent {")
            appendLine("        return Intent(context, $className::class.java).apply {")
            for ((fieldName, _) in argFields) {
                appendLine("            putExtra(\"$fieldName\", $fieldName)")
            }
            appendLine("        }")
            appendLine("    }")
            appendLine()
            
            // inject 方法
            appendLine("    fun inject(activity: $className) {")
            for ((fieldName, fieldType) in argFields) {
                val getter = when {
                    fieldType == "String" -> "getStringExtra(\"$fieldName\") ?: \"\""
                    fieldType == "Int" || fieldType == "kotlin.Int" -> "getIntExtra(\"$fieldName\", 0)"
                    fieldType == "Boolean" || fieldType == "kotlin.Boolean" -> "getBooleanExtra(\"$fieldName\", false)"
                    fieldType == "Long" || fieldType == "kotlin.Long" -> "getLongExtra(\"$fieldName\", 0L)"
                    fieldType == "Float" || fieldType == "kotlin.Float" -> "getFloatExtra(\"$fieldName\", 0f)"
                    fieldType == "Double" || fieldType == "kotlin.Double" -> "getDoubleExtra(\"$fieldName\", 0.0)"
                    else -> "getSerializableExtra(\"$fieldName\") as? $fieldType"
                }
                appendLine("        activity.$fieldName = activity.intent?.$getter")
            }
            appendLine("    }")
            appendLine("}")
        }
        
        // 写入文件
        try {
            val file = filer.createSourceFile("$packageName.$routerClassName")
            file.openWriter().use { writer ->
                writer.write(code)
            }
        } catch (e: Exception) {
            messager.printMessage(Diagnostic.Kind.ERROR, "生成文件失败: ${e.message}")
        }
    }
}

这段代码的核心逻辑其实不复杂:

  • 遍历所有被@Router标记的类
  • 找出该类中被@Arg标记的字段
  • 根据字段类型生成对应的getter方法
  • 生成createIntent和inject两个方法

我在项目中遇到过一个问题:如果字段类型是自定义的Parcelable对象,上面的代码就处理不了了。后来我加了个判断,如果是Parcelable就用putParcelable,如果是Serializable就用putSerializable。嗯,这块其实可以做得更通用一些,但实际项目中大部分场景就是基本类型加String,所以够用了。

21.4 使用方式:简洁到极致

生成完代码后,使用起来就非常简单了。

// 发送方
class MainActivity : AppCompatActivity() {
    fun goToProfile() {
        // 一行代码跳转,参数自动传过去
        UserProfileActivityRouter.createIntent(this, "user_001", 28).let {
            startActivity(it)
        }
    }
}

// 接收方
class UserProfileActivity : AppCompatActivity() {
    
    @Arg
    lateinit var userId: String
    
    @Arg
    var age: Int = 0
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 注入参数
        UserProfileActivityRouter.inject(this)
        
        // 直接用,不用再写getStringExtra了
        Log.d("Profile", "userId=$userId, age=$age")
    }
}

你看,接收方只需要在onCreate里调一行inject,所有字段就自动赋值了。而且类型安全——如果传参类型不对,编译期就报错了,不会等到运行时崩溃。

21.5 避坑指南

我曾经踩过一个坑:Kotlin的lateinit var和可空类型混用的时候,注解处理器生成的代码可能会出问题。比如:

@Arg
lateinit var name: String  // 没问题

@Arg
var name: String? = null  // 这个也能用,但生成的代码要小心

对于可空类型,生成的getter应该用as?来安全转换,而不是直接强制解包。上面代码里我已经处理了这种情况,用as?加上默认值,保证不会NPE。

还有一个坑:如果字段名和Intent的key不一致怎么办?比如字段叫userName,但传参的key是"name"。我建议在@Arg注解里加一个key参数,允许自定义key名:

@Arg(key = "name")
var userName: String = ""

这样生成的代码就会用"name"作为key来读写Intent。这个小功能在实际项目中非常实用,尤其是对接第三方SDK或者老项目改造的时候。

21.6 整体架构图

下面这张图展示了整个注解处理流程:

Intent注解处理架构图 源码阶段 @Router / @Arg 注解 APT注解处理器 编译期生成代码 生成代码 XxxRouter.kt createIntent() inject() 类型安全 零反射 运行时使用 直接调用生成的Router类,无需反射,性能与手写代码一致 核心优势 ✅ 编译期检查 ✅ 零运行时开销 ✅ 类型安全 ✅ 减少样板代码

21.7 扩展思考

这套方案其实可以做得更强大。比如:

  • 支持Fragment传参:原理一样,用Bundle代替Intent
  • 支持startActivityForResult:生成带requestCode的重载方法
  • 支持路由拦截:在跳转前检查登录状态、权限等

我在实际项目中就把这套方案扩展成了完整的路由框架,支持模块化开发、动态加载、拦截器链等功能。但核心思想没变——就是编译期生成代码,运行时零反射。

💡 小提示:如果你不想自己写注解处理器,可以考虑用现成的库,比如ARouter、ActivityRouter等。但自己写一遍能让你更深入理解APT的原理,而且可以根据项目需求灵活定制。
⚠️ 注意:注解处理器生成的代码不要手动修改!每次编译都会重新生成,手动修改的内容会被覆盖。如果需要对生成的代码做定制,应该修改注解处理器本身。

好了,关于Intent注解处理的内容就聊到这里。这套方案我用了好几年,从最初的简单跳转,到后来支持参数注入、路由拦截、模块化通信,一步步完善。说白了,核心就是「编译期生成,运行时零反射」这八个字。你想想看,如果能用编译期解决的问题,为什么要拖到运行时呢?


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