Skip to content

xesam/Android-DebugOnly

Repository files navigation

Android-DebugOnly

基于生命周期的 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 构建零残留。


集成

1. 添加依赖

// app/build.gradle.kts — 只在 debug 构建引入
debugImplementation("io.github.xesam:android-debugonly:0.0.3")

minSdk 21,无额外传递依赖。

2. 创建 debug 初始化入口

app/src/debug/java/…/DebugBootstrap.kt(与 src/main 平级)创建初始化类:

object DebugBootstrap {
    fun init(application: Application) {
        DebugInjector.init(application) { api ->
            api.registerHandlers(
                // 在这里配置各页面的调试行为
            )
        }
    }
}

3. release 提供空实现

app/src/release/java/…/DebugBootstrap.kt 放一个什么都不做的空实现:

object DebugBootstrap {
    fun init(application: Application) = Unit
}

4. 在 Application 中调用

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()
    }
)

Mock 数据

将测试数据直接填充到页面 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

获取 View 引用,执行任意自定义操作。

// 按 ID 查找
.onView(R.id.btn_hidden) { activity, view ->
    view.visibility = View.VISIBLE
}

// 按 Tag 查找
.onView("debug_trigger") { activity, view ->
    view.setBackgroundColor(Color.RED)
}

自定义 Action

所有内置能力均实现自 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

About

一个面向 Android 的 Debug 注入模板项目,用来在 debug 模式下启用注入调试内容

Resources

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors