基于生命周期的 Android 调试注入框架,将散落在业务代码里的临时调试逻辑收敛为可统一管理、可复用、与业务代码严格隔离的调试能力。
// 全部调试配置集中在一处,不改动任何业务代码
DebugInjector.init(application) { api ->
api.registerHandlers(
PageHandlers.forActivity(OrderActivity::class.java)
.addButton("mock_order", "填充测试数据") { activity ->
MockDataAction(listOf(
MockDataAction.Binding.byId(R.id.et_order_id, "ORD-20260101"),
MockDataAction.Binding.byId(R.id.et_amount, "128.00")
)).execute(activity)
}
.autoClick(R.id.btn_confirm, "auto_confirm", oncePerActivity = true)
.build()
)
}Android 开发中调试代码的典型问题:
- 分散:
if (debug)块、临时按钮、日志语句散落在各页面,难以统一管理 - 污染:容易忘记清理,不小心进入 release 包
- 低效:复杂场景(订单流、异常状态)需要手动构造数据、反复操作才能复现
Android-DebugOnly 的解法:通过 ActivityLifecycleCallbacks 在 Activity resume 时自动触发,将调试行为以 DebugAction 为单位模块化,通过 PageHandler 匹配到具体页面。调试代码只存在于 src/debug,release 构建零残留。
// app/build.gradle.kts — 只在 debug 构建引入
debugImplementation("io.github.xesam:android-debugonly:0.0.3")minSdk 21,无额外传递依赖。
在 app/src/debug/java/…/DebugBootstrap.kt(与 src/main 平级)创建初始化类:
object DebugBootstrap {
fun init(application: Application) {
DebugInjector.init(application) { api ->
api.registerHandlers(
// 在这里配置各页面的调试行为
)
}
}
}在 app/src/release/java/…/DebugBootstrap.kt 放一个什么都不做的空实现:
object DebugBootstrap {
fun init(application: Application) = Unit
}class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
DebugBootstrap.init(this) // debug 有内容,release 是空实现
}
}Application.onCreate()
└─ DebugInjector.init()
└─ 注册 ActivityLifecycleCallbacks
└─ onActivityResumed(activity)
└─ 遍历所有 PageHandler
└─ handler.match(activity) ?
└─ 执行 handler 持有的 DebugAction 列表
| 概念 | 职责 |
|---|---|
DebugInjector |
统一入口,注册 Handler,监听生命周期 |
PageHandler |
页面匹配策略(默认按 Activity 类型匹配) |
DebugAction |
单一调试行为的最小执行单元,可组合复用 |
PageHandlers |
流式 Builder,快速组合 Action 并生成 Handler |
所有能力通过 PageHandlers.forActivity(XxxActivity::class.java) 的流式 API 配置。
在页面右下角注入悬浮按钮,不影响原有 UI 布局。
.addButton("btn_key", "按钮文字") { activity ->
// 点击回调
}
// 指定垂直位置(slot,每个约 56dp)
.addButton("btn_key", "按钮文字", slot = 1) { activity ->
// ...
}在页面顶部注入半透明文字面板,可动态展示当前状态。
// 只读面板
.addInfoPanel { "当前用户 ID:${Session.userId}" }
// 可点击面板
.addInfoPanel(
textProvider = { "环境:${Config.env} 点击切换" },
clickBehavior = { activity, _ ->
Config.toggleEnv()
Toast.makeText(activity, "已切换", Toast.LENGTH_SHORT).show()
}
)将测试数据直接填充到页面 View,支持按 ID、Tag、自定义查询定位。
.mockData(
MockDataAction.Binding.byId(R.id.et_name, "Alice"),
MockDataAction.Binding.byId(R.id.et_phone, "13800138000"),
MockDataAction.Binding.byTag("input_amount", "128.00")
)页面 resume 时自动触发指定 View 的点击,省去重复手动操作。
// 每次 resume 都触发
.autoClick(R.id.btn_submit, actionKey = "auto_submit", oncePerActivity = false)
// 仅在该 Activity 实例的首次 resume 触发(适合自动登录等场景)
.autoClick(R.id.btn_login, actionKey = "auto_login", oncePerActivity = true)在不修改、不覆盖原有点击监听器的前提下,附加额外行为。
// 在原有点击之后执行
.attachClick(R.id.btn_pay, "pay_log", runBeforeOriginal = false) { activity, view ->
Log.d("Debug", "支付按钮被点击,当前金额:${view.tag}")
}
// 在原有点击之前执行(可用于拦截/埋点)
.attachClick(R.id.btn_submit, "submit_before", runBeforeOriginal = true) { activity, _ ->
Toast.makeText(activity, "即将提交", Toast.LENGTH_SHORT).show()
}获取 View 引用,执行任意自定义操作。
// 按 ID 查找
.onView(R.id.btn_hidden) { activity, view ->
view.visibility = View.VISIBLE
}
// 按 Tag 查找
.onView("debug_trigger") { activity, view ->
view.setBackgroundColor(Color.RED)
}所有内置能力均实现自 DebugAction 接口,自定义能力同理:
class ClearCacheAction : DebugAction {
override fun execute(activity: Activity) {
CacheManager.clear()
Toast.makeText(activity, "缓存已清除", Toast.LENGTH_SHORT).show()
}
}
// 在 Builder 中注册
.addAction(ClearCacheAction())不向业务 UI 注入任何元素,通过系统通知栏提供调试入口。适合对 UI 洁癖有要求的场景,也适合在后台或 release-like 环境下触发调试。
DebugInjector.init(application) { api ->
api.registerHandlers(...)
api.setNotification(
title = "MyApp Debug",
content = "点击打开调试面板"
) { context ->
// context 为 ApplicationContext
// 可以 startActivity、发广播、切换配置等
val intent = Intent(context, DebugPanelActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
}
}行为说明:
- 通知在每次 Activity resume 时自动保活:未授权时静默等待,授权后下次 resume 自动显示;被 ROM 强制划掉后,回到 App 自动恢复
- 点击通知中的 Stop 按钮关闭通知,再次调用
init()后重置
需要在
AndroidManifest.xml中声明(如果直接依赖本库的 AAR,已自动合并):<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />Android 13+ 还需在合适时机引导用户手动授权通知权限。
框架本身不内置开关,推荐由接入方实现,保持灵活性。示例项目中 ExampleDebugSwitchCenter 演示了全局开关 + 页面级开关的组合方案,可直接参考复制。
// 结合开关的 PageHandler 示例
PageHandlers.forActivity(MainActivity::class.java)
.addAction(object : DebugAction {
override fun execute(activity: Activity) {
if (!DebugSwitchCenter.isEnabled("main_page")) return
// 执行调试行为
}
})
.build()| 项目 | 说明 |
|---|---|
| 线程安全 | DebugInjector.init() 应在 Application.onCreate() 主线程调用 |
| 反射兼容性 | AttachClickBehaviorAction 依赖 View$ListenerInfo 反射,在严格限制反射的系统(如部分厂商定制 ROM)可能失效,失败时 logcat 会输出警告 |
| 重复注入防护 | 内置 Action(按钮、面板等)通过 View tag 防止 onResume 多次触发时重复注入,自定义 Action 需自行处理 |
| 通知权限 | Android 13+(API 33)需运行时授权 POST_NOTIFICATIONS,框架不自动申请,由接入方在合适时机引导 |
# 编译库
./gradlew :DebugOnly:assembleRelease
# 运行示例 App(连接设备后)
./gradlew :app:installDebug