视频加载失败

Android 开发环境判断:BuildConfig、Product Flavor 与运行时检测

1824 字
9 分钟
Android 开发环境判断:BuildConfig、Product Flavor 与运行时检测

前言#

在 Android 开发中,我们经常需要让 App 在不同环境下表现出不同行为:

  • 开发环境下显示调试菜单、输出详细日志、连接本地服务器
  • 测试环境下连接测试服务器、开启性能检测
  • 生产环境下关闭所有调试功能、连接正式服务器

问题来了:如何让代码知道当前运行在什么环境下?

这篇文章将为你梳理 Android 生态中判断开发环境的几种主流方案,从 Gradle 编译期注入到运行时动态检测,一网打尽。

Note

本文基于 Kotlin + Gradle (KTS) 编写,Gradle 插件版本 8.x,AGP 8.x。


1. 方案总览#

方案原理时机可靠性适用场景
BuildConfig.DEBUGdebug buildType 自动生成编译期⭐⭐⭐简单判断 debug/release
自定义 BuildConfig 字段Gradle 注入常量编译期⭐⭐⭐⭐自定义环境标识
Product FlavorGradle 多维度构建编译期⭐⭐⭐⭐⭐多环境(dev/staging/prod)
设备 ID / 签名判断运行时检测运行时⭐⭐⭐兜底方案
Content Provider 自动初始化应用启动时运行时⭐⭐⭐⭐第三方 SDK 初始化

2. BuildConfig.DEBUG:最简单的方案#

当你用 Android Studio 创建项目时,Gradle 自动生成了 debug 和 release 两个 buildType。debug 类型下 BuildConfig.DEBUG 自动为 true。

2.1 使用#

// 在代码中直接使用
if (BuildConfig.DEBUG) {
Log.d("MyApp", "当前是调试模式")
// 显示调试面板
} else {
// 生产环境逻辑
}

2.2 内部原理#

app/build.gradle.kts
android {
buildTypes {
debug {
// debug 模式下 isDebuggable 默认为 true
}
release {
isDebuggable = false
isMinifyEnabled = true
proguardFiles(...)
}
}
}

Gradle 编译时自动生成:

app/build/generated/source/buildConfig/debug/.../BuildConfig.java
public final class BuildConfig {
public static final boolean DEBUG = true; // debug 时为 true
public static final String BUILD_TYPE = "debug";
// ...
}

2.3 局限性#

BuildConfig.DEBUG 只能区分 debug 和 release 两种 buildType。如果你需要区分开发环境、测试环境、预发布环境、生产环境,这个字段就不够用了。


3. 自定义 BuildConfig 字段#

如果 debug/release 不够用,可以在 Gradle 中向 BuildConfig 注入自定义字段。

3.1 添加自定义字段#

app/build.gradle.kts
android {
buildTypes {
debug {
buildConfigField("String", "API_BASE_URL", "\"https://dev-api.example.com\"")
buildConfigField("String", "ENV_NAME", "\"development\"")
buildConfigField("boolean", "ENABLE_ANALYTICS", "false")
buildConfigField("int", "LOG_LEVEL", "2")
}
release {
buildConfigField("String", "API_BASE_URL", "\"https://api.example.com\"")
buildConfigField("String", "ENV_NAME", "\"production\"")
buildConfigField("boolean", "ENABLE_ANALYTICS", "true")
buildConfigField("int", "LOG_LEVEL", "0")
}
}
}

3.2 在代码中使用#

object AppConfig {
val apiBaseUrl: String get() = BuildConfig.API_BASE_URL
val envName: String get() = BuildConfig.ENV_NAME
val isAnalyticsEnabled: Boolean get() = BuildConfig.ENABLE_ANALYTICS
val logLevel: Int get() = BuildConfig.LOG_LEVEL
val isDebug: Boolean
get() = envName == "development"
}

3.3 封装为枚举#

enum class AppEnvironment(val envName: String) {
DEVELOPMENT("development"),
STAGING("staging"),
PRODUCTION("production");
companion object {
fun current(): AppEnvironment = when (BuildConfig.ENV_NAME) {
"development" -> DEVELOPMENT
"staging" -> STAGING
else -> PRODUCTION
}
}
}
// 使用
fun initializeApp() {
when (AppEnvironment.current()) {
AppEnvironment.DEVELOPMENT -> {
// 开启严格模式
StrictMode.enableDefaults()
}
AppEnvironment.STAGING -> {
// 连接测试服务器
}
AppEnvironment.PRODUCTION -> {
// 初始化崩溃上报
CrashReport.init()
}
}
}

4. Product Flavor:多环境构建的正确姿势#

对于需要多套环境配置的项目,Product Flavor 是官方推荐的标准做法。

4.1 定义 Flavor#

app/build.gradle.kts
android {
flavorDimensions += "environment"
productFlavors {
create("dev") {
dimension = "environment"
applicationIdSuffix = ".dev"
versionNameSuffix = "-dev"
buildConfigField("String", "API_BASE_URL", "\"https://dev-api.example.com\"")
buildConfigField("String", "ENV", "\"dev\"")
manifestPlaceholders["appLabel"] = "MyApp Dev"
}
create("staging") {
dimension = "environment"
applicationIdSuffix = ".staging"
versionNameSuffix = "-staging"
buildConfigField("String", "API_BASE_URL", "\"https://staging-api.example.com\"")
buildConfigField("String", "ENV", "\"staging\"")
manifestPlaceholders["appLabel"] = "MyApp Staging"
}
create("prod") {
dimension = "environment"
buildConfigField("String", "API_BASE_URL", "\"https://api.example.com\"")
buildConfigField("String", "ENV", "\"prod\"")
manifestPlaceholders["appLabel"] = "MyApp"
}
}
}

4.2 Flavor + BuildType 的组合#

Product Flavor 和 BuildType 是正交的,Gradle 会为每种组合生成一个 variant:

devDebug # 开发环境 + 调试构建
devRelease # 开发环境 + 发布构建
stagingDebug # 测试环境 + 调试构建
stagingRelease # 测试环境 + 发布构建
prodDebug # 生产环境 + 调试构建
prodRelease # 生产环境 + 发布构建

4.3 按 Variant 存放资源#

你可以为不同 flavor 创建不同的资源目录:

app/
└── src/
├── main/ # 公共代码和资源
├── dev/ # dev flavor 专属
│ ├── java/.../DevConfig.kt
│ └── res/values/strings.xml
├── staging/ # staging flavor 专属
│ ├── java/.../StagingConfig.kt
│ └── res/values/strings.xml
└── prod/ # prod flavor 专属
├── java/.../ProdConfig.kt
└── res/values/strings.xml

关键技巧:在 main 中定义接口,在各 flavor 中提供实现:

src/main/java/.../EnvironmentConfig.kt
interface EnvironmentConfig {
val baseUrl: String
val enableLogging: Boolean
val enableStrictMode: Boolean
}
// src/dev/java/.../DevEnvironmentConfig.kt
object DevEnvironmentConfig : EnvironmentConfig {
override val baseUrl = "https://dev-api.example.com"
override val enableLogging = true
override val enableStrictMode = true
}
// src/prod/java/.../ProdEnvironmentConfig.kt
object ProdEnvironmentConfig : EnvironmentConfig {
override val baseUrl = "https://api.example.com"
override val enableLogging = false
override val enableStrictMode = false
}

然后在 Dagger / Koin 等 DI 容器中注入对应的实现。

4.4 在 AndroidManifest 中区分#

src/dev/AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="${appLabel}"
android:networkSecurityConfig="@xml/network_security_config_dev">
<!-- 允许明文流量(开发环境需要) -->
<uses-library android:name="..." />
</application>
</manifest>
<!-- src/main/res/xml/network_security_config_dev.xml -->
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">localhost</domain>
<domain includeSubdomains="true">10.0.2.2</domain>
</domain-config>
<debug-overrides>
<trust-anchors>
<certificates src="system" />
<certificates src="user" /> <!-- 信任用户安装的证书(抓包用) -->
</trust-anchors>
</debug-overrides>
</network-security-config>

5. 运行时检测方案#

有些情况下,你可能需要在运行时判断环境,而不是依赖编译期注入。

5.1 检测是否开启调试#

fun isDebugBuild(context: Context): Boolean {
return (context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0
}
// 使用
if (isDebugBuild(this)) {
Log.d("Debug", "当前 App 开启了 debuggable 标志")
}

5.2 检测是否从 Google Play 安装#

fun isFromPlayStore(context: Context): Boolean {
val installer = context.packageManager.getInstallerPackageName(context.packageName)
return installer == "com.android.vending"
|| installer == "com.google.android.feedback"
}

5.3 检测应用签名是否与发布签名一致#

import android.content.pm.PackageManager
import java.security.MessageDigest
fun isReleaseSignature(context: Context): Boolean {
val releaseSignature = "A1:B2:C3:D4:..." // 正式签名的 SHA-1
return try {
val packageInfo = context.packageManager.getPackageInfo(
context.packageName,
PackageManager.GET_SIGNING_CERTIFICATES
)
val signatures = packageInfo.signingInfo.apkContentsSigners
val currentSignature = signatures.map { sig ->
val md = MessageDigest.getInstance("SHA-1")
md.digest(sig.toByteArray()).joinToString(":") {
"%02X".format(it)
}
}.firstOrNull() ?: ""
currentSignature.equals(releaseSignature, ignoreCase = true)
} catch (e: Exception) {
false
}
}

5.4 检测是否为模拟器#

fun isEmulator(): Boolean {
return (Build.FINGERPRINT.startsWith("generic")
|| Build.FINGERPRINT.startsWith("unknown")
|| Build.MODEL.contains("google_sdk")
|| Build.MODEL.contains("Emulator")
|| Build.MODEL.contains("Android SDK built for x86")
|| Build.MANUFACTURER.contains("Genymotion")
|| (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"))
|| "google_sdk" == Build.PRODUCT)
}

5.5 检测是否 Root#

fun isDeviceRooted(): Boolean {
// 检测常见的 su 二进制文件
val suPaths = arrayOf(
"/system/bin/su",
"/system/xbin/su",
"/system/sbin/su",
"/sbin/su",
"/vendor/bin/su",
"/data/local/su",
"/data/local/bin/su",
"/data/local/xbin/su"
)
return suPaths.any { path -> File(path).exists() }
}

6. Content Provider 自动初始化#

有些库(如 LeakCanary、Stetho)利用 Content Provider 的自动初始化特性,在 Application 启动前就完成初始化。

DevInitProvider.kt
class DevInitProvider : ContentProvider() {
override fun onCreate(): Boolean {
val context = context ?: return false
if (BuildConfig.DEBUG) {
// 在 Application.onCreate() 之前自动初始化
Stetho.initializeWithDefaults(context)
LeakCanary.install(context as Application)
}
return true
}
override fun query(...): Cursor? = null
override fun getType(...): String? = null
override fun insert(...): Uri? = null
override fun delete(...): Int = 0
override fun update(...): Int = 0
}
AndroidManifest.xml
<provider
android:name=".DevInitProvider"
android:authorities="${applicationId}.DevInitProvider"
android:exported="false" />

注意:${applicationId} 可以配合 flavor 的 applicationIdSuffix,让不同环境的 Provider 不受影响。


7. 综合实践:一套完整的方案#

以下是一套推荐的最佳实践:

7.1 Gradle 配置#

app/build.gradle.kts
android {
flavorDimensions += "environment"
productFlavors {
create("dev") {
dimension = "environment"
applicationIdSuffix = ".dev"
buildConfigField("String", "ENV", "\"dev\"")
}
create("prod") {
dimension = "environment"
buildConfigField("String", "ENV", "\"prod\"")
}
}
buildTypes {
debug {
isDebuggable = true
}
release {
isMinifyEnabled = true
proguardFiles(...)
}
}
}

7.2 统一的环境管理类#

Environment.kt
object Environment {
enum class Type {
DEV, PROD
}
val type: Type get() = when (BuildConfig.ENV) {
"dev" -> Type.DEV
else -> Type.PROD
}
val isDebug: Boolean get() = type == Type.DEV || BuildConfig.DEBUG
val baseUrl: String get() = when (type) {
Type.DEV -> "https://dev-api.example.com"
Type.PROD -> "https://api.example.com"
}
}

7.3 使用示例#

class MyApp : Application() {
override fun onCreate() {
super.onCreate()
when (Environment.type) {
Environment.Type.DEV -> {
Timber.plant(Timber.DebugTree())
StrictMode.enableDefaults()
}
Environment.Type.PROD -> {
Timber.plant(ReleaseTree())
CrashReport.init(this)
}
}
initRetrofit(Environment.baseUrl)
}
}

8. 总结对比#

方案一句话描述适合谁
BuildConfig.DEBUG最快速的方式,零配置个人项目、简单场景
自定义 BuildConfig 字段灵活注入常量需要自定义环境标识
Product Flavor官方正解,一套代码多套应用正式项目、多环境部署
运行时检测兜底方案安全校验、特殊需求
Content Provider自动初始化第三方 SDK

推荐路径:小项目直接用 BuildConfig.DEBUG,正规项目上 Product Flavor,再搭配 BuildConfig.DEBUG 做双重判断,完美覆盖所有场景。


参考资料#

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Android 开发环境判断:BuildConfig、Product Flavor 与运行时检测
https://ksayus.top/posts/android-dev-env-check/
作者
Ksayus
发布于
2026-09-13
许可协议
GPLv3

评论区

Profile Image of the Author
Ksayus
细碎的美好也是生活的意义
Ksayus の 猫窝
This is Ksayus`s blog, is will be write some tech article.
分类
标签
最新动态
站点统计
文章
35
分类
7
标签
111
总字数
58,408
运行时长
0 天
最后活动
0 天前
站点信息
构建平台
GitHub Actions
博客版本
Firefly v6.16.8
文章许可
GPLv3
文章目录