Android 动态化

大约 4 分钟

Android 动态化

Kuikly在Android端的动态化支持两种模式

  1. DEX模式: 通过动态下发和执行apk,达到动态化的目的
  2. 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加载异常。

使用安卓动态加载业务关注:

  1. App升级 Target SDK 34 时,关注验证动态加载页面功能是否正常
  2. 升级Kuikly 1.20.1 后,关注验证动态包管理逻辑正常

Kuikly的gradle插件提供了两种任务构建动态执行的DEX产物:

  1. distributeApkToStaticServerDebug: 该任务会将本地所有的pager打包成一个kuikly_dynamic.apk,并存放本地服务器。业务可在开发阶段使用这种模式来进行热重载开发,无需重新编译宿主
  2. packSplitApkRelease: 该任务会根据dynamicApk配置的pager列表,将列表中的pager分别打包成一个apk,最后压缩成zip。该任务用于构建线上动态下发的产物

distributeApkToStaticServerDebug: 热重载开发方式

  1. 在业务工程中,先启动本地服务器
// 如果是首次运行的话, 需要先执行 npm install
npm run serve
  1. 启动成功后, 在业务工程根目录执行以下命令,将本地pager编译成apk,并存放到本地服务器
:kuikly-dynamic-apk-builder:distributeApkToStaticServerDebug
  1. 在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

本地ip地址
本地ip地址

注意

建议将第三步做成通用的逻辑, 然后提交到远程分支,这样应用内的其他同事无需执行步骤3

packSplitApkRelease: 打包Dex最终产物

  1. kuikly-dynamic-apk-builder的build.gradle.kts文件中, 配置你想要动态下发的pager列表
kuikly {
    dynamicApk {
        addSplitPages(listOf(
            "test",
            "router"
        ))
    }
}
  1. 然后执行以下命令,将test和router分别打包成apk
:kuikly-dynamic-apk-builder:packSplitApkRelease
  1. 最终产物存放在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堆栈的还原,业务开发者需自行保存,用于后续堆栈还原

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