Android 动态化
大约 4 分钟
Android 动态化
Kuikly在Android端的动态化支持两种模式
- DEX模式: 通过动态下发和执行apk,达到动态化的目的
SO模式(已废弃): 通过动态下发和加载so,达到动态化的目的
注意
建议业务优先使用性能更好的DEX模式,SO动态化模式已废弃
DEX 模式动态化
警告
注意!小概率会遇到DEX产物在7.0或以下系统加载crash的情况,修改任意代码后重新打包可以消除。DEX产物上线前务必测试Android 7.0或以下系统的兼容性。
注意
Android Target SDK 34 开始要求必须将所有动态加载的文件标记为只读,当App的 targetSdk>=34 时,加载非只读属性的 dex文件 会抛异常。
为了避免crash,Kuikly框架侧在 1.20.1 版本增加了统一设置逻辑:当系统版本大于等于34,对传入 dex文件 设置 setReadOnly,避免出现dex加载异常。
使用安卓动态加载业务关注:
- App升级 Target SDK 34 时,关注验证动态加载页面功能是否正常
- 升级Kuikly 1.20.1 后,关注验证动态包管理逻辑正常
Kuikly的gradle插件提供了两种任务构建动态执行的DEX产物:
- distributeApkToStaticServerDebug: 该任务会将本地所有的pager打包成一个kuikly_dynamic.apk,并存放本地服务器。业务可在开发阶段使用这种模式来进行热重载开发,无需重新编译宿主
- packSplitApkRelease: 该任务会根据dynamicApk配置的pager列表,将列表中的pager分别打包成一个apk,最后压缩成zip。该任务用于构建线上动态下发的产物
distributeApkToStaticServerDebug: 热重载开发方式
- 在业务工程中,先启动本地服务器
// 如果是首次运行的话, 需要先执行 npm install
npm run serve
- 启动成功后, 在业务工程根目录执行以下命令,将本地pager编译成apk,并存放到本地服务器
:kuikly-dynamic-apk-builder:distributeApkToStaticServerDebug
- 在Android Kuikly的容器中的onCreate中,下载本地服务器的apk,并将apk路径传递给Kuikly,并将执行模式指定为DEX模式
class KuiklyRenderActivity : AppCompatActivity(), KuiklyRenderViewDelegatorDelegate {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_hr)
setupImmersiveMode()
hrContainerView = findViewById(R.id.hr_container)
loadingView = findViewById(R.id.hr_loading)
errorView = findViewById(R.id.hr_error)
if (useDexMode) {
fetchDexContextCode { code ->
runOnUiThread {
kuiklyRenderViewDelegator.onAttach(
hrContainerView,
code,
pageName,
createPageData()
)
}
}
} else {
kuiklyRenderViewDelegator.onAttach(
hrContainerView,
"",
pageName,
createPageData()
)
}
}
override fun coreExecuteMode(): KuiklyRenderCoreExecuteMode {
return if (useDexMode) { // 根据实际情况来决定是否走dex模式
KuiklyRenderCoreExecuteMode.DEX
} else {
super.coreExecuteMode()
}
}
private fun fetchDexContextCode(result: (String) -> Unit) {
// hotReloadIp为电脑的ip地址,注意手机和电脑必须连接同个wifi
execOnSubThread {
val client = OkHttpClient()
val request = Request.Builder()
.url("http://$hotReloadIp:8899/kuikly_dynamic.apk?_rij_violaUrl=1&hideNav=1&v_nav_immer=1&nv2=2&v_bundleName=$pageName&v_bg_color=f5f5f5&statusColor=1")
.build()
val outputFile = File(filesDir.absolutePath + "/kuikly_dynamic.apk")
if (outputFile.exists()) {
outputFile.delete()
}
outputFile.createNewFile()
client.newCall(request).enqueue(object: okhttp3.Callback {
override fun onFailure(call: okhttp3.Call, e: IOException) {
e.printStackTrace()
}
@Throws(IOException::class)
override fun onResponse(call: okhttp3.Call, response: okhttp3.Response) {
if (response.isSuccessful) {
val inputStream: InputStream? = response.body()?.byteStream()
val fos = FileOutputStream(outputFile)
val buffer = ByteArray(1024)
var bytesRead: Int
while (inputStream?.read(buffer).also { bytesRead = it ?: 0 } != -1) {
fos.write(buffer, 0, bytesRead)
}
fos.flush()
fos.close()
inputStream?.close()
result.invoke(outputFile.absolutePath)
} else {
// 处理请求失败的情况
}
}
})
}
}
}
hotReloadIp为电脑的ip地址,可以在启动本地服务器的时候找到这个ip

注意
建议将第三步做成通用的逻辑, 然后提交到远程分支,这样应用内的其他同事无需执行步骤3
packSplitApkRelease: 打包Dex最终产物
- 在
kuikly-dynamic-apk-builder的build.gradle.kts文件中, 配置你想要动态下发的pager列表
kuikly {
dynamicApk {
addSplitPages(listOf(
"test",
"router"
))
}
}
- 然后执行以下命令,将test和router分别打包成apk
:kuikly-dynamic-apk-builder:packSplitApkRelease
- 最终产物存放在kuikly-dynamic-apk-builder/build/outputs/kuikly/apk/release/split/中,其中final目录下的kuikly_dynamic.zip为最终产物,存放着apk列表文件。 将kuikly_dynamic.zip解压后,里面包含:
- kuikly_config.json: 配置文件,里面记录着页面的pageName对应的apk文件名字, 业务可根据此配置文件来决定pageName对应哪个apk文件
- apk文件列表: 包含哪些被打包的页面,每一个页面都是一个apk
在split目录下,有以page名字命令的目录,里面存放这mapping文件,可用于crash堆栈的还原,业务开发者需自行保存,用于后续堆栈还原。

- 接着在Kuikly的端容器中, 支持dex加载模式,识别要执行dex模式的page, 并指定为页面执行模式为dex模式
override fun coreExecuteMode(): KuiklyRenderCoreExecuteMode {
return if (useDexMode) { // 识别到要使用dex模式时, 返回DEX枚举
KuiklyRenderCoreExecuteMode.DEX
} else {
super.coreExecuteMode()
}
}
- 最后将动态下载的apk路径,在Kuikly的onAttach时,传递给Kuikly
kuiklyRenderViewDelegator.onAttach(hrContainerView, dynamicApkPath, pageName, createPageData())