在安卓项目的对外合作、二次开发支持或生态共建场景中,经常会遇到 "需要提供源码方便对接,但必须保护核心业务逻辑" 的需求。直接开放完整源码会导致核心算法、数据处理逻辑、接口协议等敏感信息泄露;仅提供编译后的 APK 或 SDK 又无法满足合作方对 UI 定制、页面适配的灵活需求。此时,"仅暴露 UI 层源码,隐藏核心层实现" 成为最优解。本文将从设计原则、实现方案、实操步骤三个维度,详解如何实现这一目标。
一、核心设计原则:隔离与解耦是前提
要实现 "仅 UI 层对外暴露",核心是通过分层设计 和接口解耦,让 UI 层与核心层完全分离 ------UI 层只负责页面展示、用户交互,核心层负责业务逻辑、数据处理、网络请求等核心能力,且两者通过标准化接口通信。具体需遵循三大原则:
-
依赖倒置原则
:UI 层不直接依赖核心层的具体实现,而是依赖抽象接口;核心层实现接口并提供服务。
-
最小暴露原则
:仅对外提供 UI 层的布局文件、控件逻辑、主题样式等必要源码,核心层相关的 Java/Kotlin 类、配置文件、依赖库完全隐藏。
-
通信标准化原则
:UI 层与核心层的交互通过接口、回调、事件总线等标准化方式实现,避免硬编码耦合。
二、具体实现方案:从模块化到源码隔离
(一)第一步:项目模块化拆分
首先需要对现有项目进行模块化重构,将代码按 "功能职责" 拆分为独立模块,为后续的 "源码暴露 / 隐藏" 提供基础。推荐拆分以下核心模块:
| 模块类型 | 职责描述 | 对外暴露方式 |
|---|---|---|
| UI 模块(ui-module) | 包含 Activity、Fragment、布局文件(layout)、控件自定义、主题样式(style)、资源文件(drawable/values)等纯 UI 相关代码 | 提供完整源码 |
| 核心业务模块(core-module) | 包含业务逻辑、数据模型、网络请求、本地存储、核心算法等敏感代码 | 打包为 AAR,仅暴露接口 |
| 基础库模块(base-module) | 包含工具类、通用控件、基础配置等非敏感公共能力(若无需对外定制,可合并到核心模块) | 打包为 AAR(可选源码) |
| 壳工程(app-module) | 负责模块依赖管理、Application 初始化、入口配置,无核心业务代码 | 不对外暴露(或仅提供配置模板) |
模块依赖关系:UI 模块 → 核心模块接口 → 核心模块实现(AAR),即 UI 模块仅依赖核心模块的 "接口定义",不依赖具体实现。
(二)第二步:核心层封装为 AAR,隐藏实现细节
核心业务模块(core-module)是需要重点保护的部分,通过打包为 AAR 文件的方式,仅对外提供 "接口",隐藏具体实现。AAR 是安卓特有的归档格式,可包含编译后的 class 文件、资源文件,且支持混淆,能有效防止反编译破解。
1. 核心模块的接口定义
在核心模块中,首先定义 UI 层需要调用的抽象接口,用于规范两者的通信方式。例如:
kotlin
// core-module 中定义接口(对外暴露的"通信协议")interface UserService { // UI层需要的"获取用户信息"能力 fun getUserInfo(callback: (UserInfo?) -> Unit) // UI层需要的"提交用户操作"能力 fun submitUserAction(action: String, result: (Boolean) -> Unit)}interface ConfigService { // UI层需要的"获取配置信息"能力 fun getUIConfig(): UIConfig}
这些接口是核心模块对外提供的 "能力清单",需要单独放在核心模块的 "public-api" 目录下(后续通过 Gradle 配置暴露接口,隐藏实现)。
2. 核心模块实现接口并混淆
在核心模块中实现上述接口,包含具体的业务逻辑(如网络请求、数据处理),并通过 ProGuard/R8 进行混淆,防止反编译后读取核心代码:
kotlin
// core-module 中实现接口(隐藏的核心逻辑)class UserServiceImpl : UserService { override fun getUserInfo(callback: (UserInfo?) -> Unit) { // 核心逻辑:调用接口、解析数据(对外隐藏) ApiClient.requestUserInfo { data -> callback(data) } } override fun submitUserAction(action: String, result: (Boolean) -> Unit) { // 核心逻辑:提交数据、验证权限(对外隐藏) val isSuccess = DataRepository.submitAction(action) result(isSuccess) }}
- 核心模块打包为 AAR
在核心模块的build.gradle(KTS)中配置 AAR 打包:
javascript
plugins { id("com.android.library") id("org.jetbrains.kotlin.android")}android { namespace = "com.example.core" compileSdk = 34 defaultConfig { minSdk = 21 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } // 混淆配置:仅保留接口,混淆实现类 buildTypes { release { isMinifyEnabled = true // 开启混淆 proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } // 暴露接口:将接口目录标记为public-api(仅对外暴露接口class) kotlin { explicitApi() sourceSets { named("main") { kotlin.srcDirs("src/main/kotlin", "src/main/public-api") } } }}dependencies { // 核心模块依赖(如网络库、数据库库等,打包进AAR) implementation("androidx.core:core-ktx:1.12.0") implementation("com.squareup.retrofit2:retrofit:2.9.0")}
核心模块的proguard-rules.pro需配置 "保留接口,混淆实现类":
perl
# 保留接口(UI层需要依赖接口编译)-keep interface com.example.core.service.** { *; }# 混淆所有实现类(隐藏核心逻辑)-dontkeep com.example.core.service.impl.** { *; }# 保留实体类(用于接口通信的数据模型)-keep class com.example.core.model.** { *; }
配置完成后,通过Gradle → core-module → Tasks → build → assembleRelease打包生成 AAR 文件(路径:core-module/build/outputs/aar/core-release.aar)。
(三)第三步:UI 模块依赖核心 AAR,仅暴露 UI 源码
UI 模块(ui-module)是对外提供源码的核心部分,需确保其仅依赖核心模块的 "接口" 和 AAR 文件,不包含任何核心逻辑。
1. UI 模块的依赖配置
在 UI 模块的build.gradle(KTS)中,依赖核心模块的 AAR 文件(而非源码),确保 UI 模块编译时能调用核心能力,但无法获取核心实现:
javascript
plugins { id("com.android.library") id("org.jetbrains.kotlin.android")}android { namespace = "com.example.ui" compileSdk = 34 defaultConfig { minSdk = 21 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } // UI模块无需混淆(对外提供源码,方便调试) buildTypes { release { isMinifyEnabled = false } } // UI模块的资源配置(布局、样式等) sourceSets { named("main") { res.srcDirs("src/main/res") layout.srcDirs("src/main/res/layout") } }}dependencies { // 依赖核心模块的AAR(本地依赖或maven仓库依赖) implementation(files("../core-module/build/outputs/aar/core-release.aar")) // 依赖核心模块的接口(若AAR已包含接口,可省略) implementation(project(mapOf("path" to ":core-module", "configuration" to "default")))
// UI相关依赖(如RecyclerView、ConstraintLayout等) implementation("androidx.recyclerview:recyclerview:1.3.2") implementation("androidx.constraintlayout:constraintlayout:2.1.4")}
2. UI 模块的代码规范
UI 模块仅包含 "与页面展示和交互相关" 的代码,严禁出现核心业务逻辑:
-
正确示例:Activity/Fragment 加载布局、初始化控件、响应点击事件,通过核心接口调用能力:
kotlin// ui-module 中的Activity(对外暴露源码)class UserProfileActivity : AppCompatActivity() { // 依赖核心模块的接口(无具体实现) private lateinit var userService: UserService override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_user_profile) // 初始化核心服务(通过核心模块提供的工厂类获取,隐藏实现) userService = ServiceFactory.getUserService() // UI交互:点击按钮获取用户信息 btn_get_info.setOnClickListener { userService.getUserInfo { userInfo -> tv_name.text = userInfo?.name tv_avatar.load(userInfo?.avatarUrl) } } }} -
错误示例:在 UI 模块中直接编写网络请求、数据解析、算法逻辑(需迁移到核心模块)。
(四)第四步:控制源码暴露范围
通过 Gradle 配置和目录结构设计,确保对外发布时仅包含 UI 模块的源码,核心模块仅提供 AAR 和接口定义:
1. 目录结构规范
cpp
Project/├── app-module/ // 壳工程(不对外暴露)├── ui-module/ // UI模块(对外暴露完整源码)│ ├── src/main/│ │ ├── kotlin/ // Activity、Fragment、自定义控件等│ │ ├── res/ // 布局、样式、资源等│ │ └── AndroidManifest.xml // UI相关页面配置├── core-module/ // 核心模块(不对外暴露源码)│ ├── src/main/│ │ ├── kotlin/ // 核心逻辑实现(混淆后打包AAR)│ │ ├── public-api/ // 接口定义(打包进AAR)│ │ └── AndroidManifest.xml // 核心服务配置(如ContentProvider)└── docs/ // 对外文档(接口说明、UI定制指南)
2. 对外发布包构建
通过 Gradle 脚本编写 "对外发布包" 任务,自动打包 UI 模块源码、核心 AAR、接口文档,过滤核心模块源码:
kotlin
// 根目录build.gradle.ktstasks.register("buildExternalRelease") { dependsOn(":core-module:assembleRelease", ":ui-module:assembleRelease")
doLast { val outputDir = file("$rootDir/external-release") outputDir.mkdirs() // 复制UI模块源码(过滤build、.idea等无关目录) val uiSourceDir = file("$rootDir/ui-module/src/main") uiSourceDir.copyTo( target = file("$outputDir/ui-source"), overwrite = true, filter = { file -> !file.name.contains("build") && !file.name.contains(".idea") } ) // 复制核心模块AAR val coreAar = file("$rootDir/core-module/build/outputs/aar/core-release.aar") coreAar.copyTo(file("$outputDir/core-aar/core-release.aar"), overwrite = true) // 复制接口文档、使用指南 val docsDir = file("$rootDir/docs") docsDir.copyTo(file("$outputDir/docs"), overwrite = true) println("对外发布包构建完成:$outputDir") }}
执行./gradlew buildExternalRelease后,external-release目录即为对外发布的内容,包含:UI 源码、核心 AAR、接口文档,无任何核心实现源码。
(五)第五步:加固与防护(可选)
为进一步防止核心逻辑被破解,可对核心 AAR 进行额外加固:
-
深度混淆
:优化 ProGuard 规则,移除调试信息、混淆类名和方法名,仅保留必要接口。
-
代码加密
:使用 DexGuard 等商业加固工具对核心 AAR 的 dex 文件进行加密,防止反编译。
-
校验机制
:在核心模块中添加 AAR 完整性校验、调用方签名校验,防止 AAR 被篡改或非法使用。
三、实操注意事项:避免踩坑的关键细节
-
接口设计需稳定
:核心模块暴露的接口是 UI 层与核心层的 "契约",一旦对外发布,应避免频繁变更;若需升级,需遵循语义化版本规范(如 1.0→1.1 兼容,2.0 可 breaking change)。
-
资源冲突处理
:UI 模块与核心 AAR 的资源(如 string、drawable、layout 名称)需添加前缀区分(如 UI 模块用 "ui_",核心模块用 "core_"),避免集成时资源冲突。
-
调试支持
:对外提供的 UI 源码应保留必要的日志(通过 BuildConfig 控制开关),核心 AAR 可提供 "调试版 AAR"(关闭混淆)供合作方联调,正式发布时使用 " release 版 AAR"(开启混淆)。
-
法律协议约束
:在对外发布包中添加《源码使用授权协议》,明确 UI 层源码的使用范围(如禁止二次分发、禁止修改核心 AAR)、核心技术的知识产权归属,避免法律风险。
四、总结
实现 "安卓工程仅暴露 UI 层源码" 的核心是 "模块化拆分 + 接口解耦 + AAR 封装":通过模块化将 UI 层与核心层分离,通过接口定义两者的通信规则,通过 AAR 封装核心层实现并隐藏源码,最终对外提供 "UI 源码 + 核心 AAR + 接口文档" 的组合包。这种方案既满足了合作方对 UI 定制、灵活对接的需求,又最大限度保护了核心业务逻辑的安全性,适用于对外合作、SDK 分发、二次开发支持等多种场景。
在实际落地时,需根据项目规模灵活调整:小型项目可简化模块拆分(仅分 UI 和核心两个模块),大型项目可进一步拆分基础库、插件模块等;同时需注重接口设计的合理性和稳定性,确保合作方能够高效集成,同时降低自身的维护成本。
关注我获取更多知识或者投稿

