sdk 动态加载实践

1.背景

博主当前所在的项目是以 sdk 的方式为其他业务提供服务,具体的结构如下图所示,业务1,2,3是不同的应用,分别处于不同的进程,引用的 sdk 内部会通过跨进程的方式和我们进行交互。
在这里插入图片描述
我们在实际的工作当中发现会有以下的问题:
1.业务接入成本大
特别是开发的过程当中,sdk 变更会比较频繁,每次变更都需要通知业务去更新,然后重新出包,更新成本较大。

2.不方便测试
由于我们是以 sdk 的方式提供服务,具体的业务应用场景在业务端,每次测试都需要发布一个新的版本,然后更新业务代码,打包之后才能进行测试。

3.业务依赖的版本不一致,可能引发异常
不同的业务依赖的版本不一致,导致有可能需要做新旧版本的兼容逻辑,增加逻辑的复杂性。

我们想到,使用动态加载的方式也许能够解决这些问题。

2.解决方案

dex 动态加载在安卓上已经是一个比较成熟的方案,如果把 sdk 做成一个壳,只保留接口,而接口的实现放在远端的 dex 文件当中,sdk 初始化的时候,从远端获取到 dex 文件,并使用 dex 文件当中的接口实现来初始化接口类,似乎就可以实现我们的想法了。

  1. 首先我将现有的 public 方法全部都抽成了一个 interface
    在这里插入图片描述

  2. 然后新建一个 RemoteDex 模块,将接口的实现,全部都放在这个模块当中
    在这里插入图片描述

  3. 单独编译 RemoteDex 模块,得到模块的 jar 包,里面包含了 dex 文件
    在这里插入图片描述
    在这里插入图片描述

  4. 在 sdk 初始化的时候,通过 dex 文件去实例化接口
    在这里插入图片描述
    发现可以使用,这种方式可行。

3.需要解决的问题

3.1 dex文件如何访问

最开始,我们想到可以将 dex 文件存放在一个公共的目录当中,不同的应用从这个目录当中读取文件进行初始化,但实际上,处理起来会涉及到不同的读写权限,而且还需要保证其他进程只读,而我们的进程可读可写,有点麻烦。
最后我们还是使用 contentProvider 的方式来让其他进程获取文件。在编译阶段,将 dex 文件复制到 assets 目录当中,其他应用获取时,服务进程会从本地的 assets 目录获取 dex 文件,并返回。

class DexProvider : ContentProvider() {

    private val TAG = "DexProvider"

    override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
        val dexPath = context?.cacheDir?.absolutePath + "/${DEX_FILE_NAME}"
        val dexFile = File(dexPath)
        Log.i(TAG, "openFile dexFile ="+dexFile)
        if (!dexFile.exists() || (dexFile.exists() && Utils.md5(dexFile) != BuildConfig.REMOTE_DEX_MD5)) {
            Log.i(TAG, "start copy dex")
            val inputStream = context?.assets?.open("RemoteDex.jar")
            inputStream.use {
                val outputStream = FileOutputStream(dexFile)
                it?.copyTo(outputStream)
                outputStream.close()
                Log.i(TAG, "copy dex success")
            }
        }
        if (dexFile.exists()) {
            Log.i(TAG, "dexFile exists")
            return ParcelFileDescriptor.open(dexFile, ParcelFileDescriptor.MODE_READ_ONLY)
        }
        Log.i(TAG, "dexFile not exists")
        return super.openFile(uri, mode)
    }
    
    ......
}
3.2 如何编译

我们每次编译的时候都需要能够确保当前 RemoteDex 模块里面的最新的代码编出来的 dex 文件都能够被放到 assets 目录当中,所以我们需要找到一个合适的 hook 点,在项目正式编译之前,先触发模块单独编译,并将 dex 写入 assets 目录,然后再进行整体编译。
我找到了 afterEvaluate 这个时机,afterEvaluate 在项目的所有脚本和插件被完全评估之后执行,这时正好可以执行我们的脚本,触发模块单独编译,完成之后再整体编译。

afterEvaluate {
    // 编译RemoteDex模块,并将dex写入assets目录
    def paramValue = project.hasProperty("executeShell") ? project.findProperty("executeShell") : "yes"
    println "Parameter value: $paramValue"
    if (paramValue == "yes") {
        def osName = System.getenv("OS")
        if (osName != null && osName.toLowerCase().contains("windows")) {
            // Windows 系统
            println "Running on Windows"
            project.exec {
                def scriptPath = file('../API/Setting/RemoteDex/gradleDex.bat')
                commandLine("cmd", "/c", scriptPath)
            }
        } else {
            //流水线或其他系统
            println "Running on other system"
            project.exec {
                commandLine 'sh', '../API/Setting/RemoteDex/gradleDex.sh'
            }
        }
    }
}

这里需要注意的是,在我们自定义的编译脚本当中,要增加参数,来区分是单独编译还是完整编译,否则会形成死循环。

#!/bin/bash

# 在gradle中调用的脚本

echo "start RemoteDex build"

buildDir="build/outputs/aar"
buildOutputReleaseDir="build/intermediates/library_assets/release/out"
buildOutputDebugDir="build/intermediates/library_assets/debug/out"
sourceAAR="RemoteDex-release.aar"
sourceJar="classes.jar"
outputDex="RemoteDex.jar"
assetFolder="src/main/assets"

cd ../

./gradlew :API:Setting:RemoteDex:assembleRelease -PexecuteShell='no'

# 2.解压aar文件
cd API/Setting/RemoteDex
cd $buildDir
unzip -o $sourceAAR

# 3.将jar包打包成dex文件

echo "d8 $sourceJar to $outputDex"
../../../script/d8 $sourceJar --output ./$outputDex

# 4.将dex复制到对应的asset目录下和对应的output目录下
cd ../../../

if [ -d "$assetFolder" ]; then
    echo "assetFolder exist"
    if [ -e "$assetFolder/$outputDex" ]; then
        echo "delete origin dex file"
        rm "$assetFolder/$outputDex"
    fi
else
    echo "assetFolder not exist,auto create"
    mkdir $assetFolder
fi

cp "$buildDir/$outputDex" "$assetFolder"
cp -f "$buildDir/$outputDex" "$buildOutputReleaseDir" 
cp -f "$buildDir/$outputDex" "$buildOutputDebugDir" 

echo "end build"

4.总结

这样我们就实现了 sdk 远程加载的方式进行初始化。每个业务只需要引用一个壳工程,每次只需要服务进程更新即可,避免业务需要频繁更新 sdk 版本的问题和版本不一致的问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值