很多团队在做 uni-app 离线打包时,最容易遇到的不是"能不能打出来",而是"同一个工程里,正式离线包和自定义调试基座怎么长期共存"。
一开始我也走过最直觉的路线:直接改 assets/data/dcloud_control.xml,调试时改成 debug,发版时再改回去。这个办法短期能用,但很快就会失控。因为一旦同一个工程同时承担"正式离线包"和"调试基座"两种职责,靠手工切文件几乎一定会出错。
我这次整理 HBuilder-Integrate-AS 项目时,目标不是"把包打出来一次",而是把这个过程固化成一个长期可维护的工程结构。最后落下来的方案很简单:同一个 Android Studio 工程里保留两种构建模式,但通过 Gradle 参数、生成目录和脚本入口,把它们彻底分开。
这篇文章就按这个思路,把我最后采用的结构和原因讲清楚。
1. 为什么不要直接改 src/main/assets/data/dcloud_control.xml
uni-app 离线打包项目里,很多资料都会提到 dcloud_control.xml。问题不在于这个文件重不重要,而在于很多人把它当成了"手工切换开关"。
如果项目里只有一种打包方式,这样做的风险还不算大;但只要你同时有下面两类产物,这种方式就会开始变脆:
- 正式离线包
- 自定义调试基座
这两个产物虽然底层都跑在 uni-app 离线 SDK 上,但用途完全不同。正式离线包追求的是稳定和可发布;自定义调试基座追求的是可联调、可热刷新、可接 HBuilderX 调试链路。
如果还是让同一个 dcloud_control.xml 承担两种职责,风险会非常直接:
- 调试时改成了
debug配置,结果打正式包前忘了切回去。 - 构建机和开发机对同一个文件产生了不同版本。
- Android Studio、命令行和脚本入口对同一份文件的认知不一致。
所以我最后把思路彻底调了一下:不再让工程直接使用 src/main/assets/data/dcloud_control.xml,而是在构建前根据模式自动生成一份目标文件。
2. 我最后采用的目录结构
我把项目里的职责拆成了下面几块:
text
HBuilder-Integrate-AS/
build-debug-base.ps1
build-release.ps1
simpleDemo/
build.gradle
config/
dcloud_control.debug.xml
dcloud_control.release.xml
libs/
lib.5plus.base-release.aar
uniapp-v8-release.aar
debug-server-release.aar
src/main/
AndroidManifest.xml
assets/
res/
这个结构里最关键的不是目录多整齐,而是每个目录的边界足够清楚:
config/只存两种模式下的dcloud_control模板。libs/放离线 SDK 和调试基座所需的aar。build.gradle只负责根据构建参数挑选模式。- PowerShell 脚本只负责把正确命令跑起来。
这样做以后,正式包和调试基座就不再靠"人记得去改文件",而是靠构建参数来驱动。
3. 真正解决问题的关键,不是两个 xml,而是 Gradle 参数
我在 simpleDemo/build.gradle 里定义了一个非常简单的入口参数:
groovy
def customBaseMode = (project.findProperty("customBaseMode") ?: "release").toString().toLowerCase()
def isCustomBaseDebug = customBaseMode == "debug"
这个参数的意义很大。因为从这一步开始,工程终于能明确回答一个问题:你这次构建,到底是在打正式离线包,还是在打自定义调试基座。
接下来再把这个判断串到资源生成逻辑里:
groovy
def generatedAssetsDir = file("$buildDir/generated/customBaseAssets")
tasks.register("prepareCustomBaseAssets", Copy) {
from(isCustomBaseDebug
? "${projectDir}/config/dcloud_control.debug.xml"
: "${projectDir}/config/dcloud_control.release.xml")
into("${generatedAssetsDir}/data")
rename { "dcloud_control.xml" }
}
tasks.named("preBuild").configure {
dependsOn("prepareCustomBaseAssets")
}
这段逻辑的价值在于,它把"手工改配置"变成了"构建前生成配置"。
也就是说:
customBaseMode=release时,自动生成正式包需要的dcloud_control.xmlcustomBaseMode=debug时,自动生成调试基座需要的dcloud_control.xml
从这里开始,工程就不需要再直接依赖固定写死在 src/main/assets/data/ 里的那份文件了。
4. 为什么我会把生成目录单独挂到 assets.srcDirs
如果只是生成文件,但 Android 构建流程不用这份文件,那前面的工作就白做了。所以我又把生成目录挂到了 sourceSets 里:
groovy
sourceSets {
main {
assets.srcDirs += generatedAssetsDir
}
}
这一步看起来普通,实际上很关键。因为它意味着 build/generated/customBaseAssets 会被当成真正的 assets 输入参与打包。
这样构建链路就完整了:
- 读取
customBaseMode - 拷贝对应的
dcloud_control.*.xml - 重命名为统一的
dcloud_control.xml - 放进生成目录
- 让 Android 构建流程使用这份生成结果
一旦这条链路跑通,正式包和调试基座就不再依赖开发者手工切文件。
5. 自定义调试基座和正式包,真正的差别不只在 xml
刚开始做的时候,我也以为两种模式的核心差异只是 dcloud_control.xml。但项目真正落地后会发现,还差一步:调试基座模式往往还有额外依赖。
我这边在 dependencies 里做的是按模式注入:
groovy
dependencies {
implementation fileTree(dir: 'libs', include: ['*.aar', '*.jar'], exclude: ['debug-server-release.aar'])
if (isCustomBaseDebug) {
implementation files("${projectDir}/libs/debug-server-release.aar")
implementation 'com.squareup.okhttp3:okhttp:3.12.12'
implementation 'net.lingala.zip4j:zip4j:2.11.5'
}
}
这一步非常重要,因为它决定了两件事:
第一,正式包不会把调试基座专用依赖误带进去。
第二,调试基座模式缺少关键 aar 时,会尽早在构建阶段失败,而不是安装到手机上才发现问题。
很多工程之所以后面越来越难维护,根本原因就是"所有依赖都堆在一起",最后没人知道哪个是正式包必须的,哪个只是调试链路临时加的。
6. AndroidManifest 里要确认哪些东西
uni-app 离线打包工程里,AndroidManifest.xml 通常不是最复杂的文件,但它有几项是必须对上的。
像我这个项目里,至少确认了这些点:
xml
<activity
android:name="io.dcloud.PandoraEntry"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize" />
<activity
android:name="io.dcloud.PandoraEntryActivity"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize" />
<meta-data
android:name="dcloud_appkey"
android:value="你的 appkey" />
这里我建议公开文章里不要直接暴露真实 appkey、签名口令和 keystore 信息。工程里可以有,文章里最好脱敏处理。因为这类内容一旦公开出去,已经不是"经验分享",而是安全边界问题了。
另外,这个项目里还配置了:
applicationId "uni.app.patrolcar"- 启动 Activity 为
io.dcloud.PandoraEntry abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86_64'
这里的 x86_64 其实很实用,因为它能让 release 包直接安装到本地模拟器上,联调阶段会方便很多。
7. 一键脚本为什么值得补
如果一个项目只有你自己维护,命令行命令记在脑子里问题不大。但只要工程会交给别人用,或者半年后还要回来重新打一次包,最好还是把入口固化下来。
我最后在项目根目录补了两个 PowerShell 脚本:
正式离线包:
powershell
.\build-release.ps1
自定义调试基座:
powershell
.\build-debug-base.ps1
脚本里做的事情其实很直接:
- 固定
JAVA_HOME到 JDK 17 - 设置好
PATH - 调用带参数的 Gradle 命令
- 在构建完成后打印产物路径
比如正式包走的是:
powershell
.\gradlew.bat -PcustomBaseMode=release :simpleDemo:assembleRelease
调试基座走的是:
powershell
.\gradlew.bat -PcustomBaseMode=debug :simpleDemo:assembleDebug
这种做法的好处很务实:让"正确命令"变成项目的一部分,而不是团队记忆的一部分。
8. Android Studio 里最容易踩的坑是什么
这一步我反而觉得最容易误导人。因为很多人会默认认为 Android Studio 里的 Build -> Generate APK 和自定义的 Gradle 运行配置是一回事,但实际上不是。
如果你的正式包和调试基座切换依赖 -PcustomBaseMode=... 这种 Gradle 参数,那么最稳妥的做法不是去点默认的 Build 菜单,而是单独建两个 Gradle Run Configuration:
assembleRelease-customBaseassembleDebug-customBase
对应的 Run 输入分别是:
text
-PcustomBaseMode=release :simpleDemo:assembleRelease
text
-PcustomBaseMode=debug :simpleDemo:assembleDebug
这样做的意义非常直接:你不再依赖 IDE 的默认行为,而是把"打什么包"这件事显式写进运行配置里。
9. 这套方案最终解决了什么问题
回头看,这次整理最重要的收获不是"我成功打出了一个自定义基座",而是我把下面几件事从"靠人记忆"改成了"靠工程约束":
- 正式离线包和调试基座分开管理
dcloud_control.xml不再手工切换- 调试基座专用依赖按模式注入
- 构建入口统一为脚本或 Gradle 参数
- Android Studio 和命令行行为保持一致
这类改造最大的价值,不在于第一次搭起来有多快,而在于三个月后、换一个人、换一台机器,工程仍然能按同样方式运行。
如果你现在也在做 uni-app 离线打包,而且项目已经同时存在"正式包"和"自定义调试基座"两条链路,我的建议很明确:不要再让两者共用一套手工切换的配置文件。尽早把模式切换前移到 Gradle 层,把生成结果收敛到构建目录里,这一步会比你后面补多少说明文档都更有效。