返回

Android插件化之动态库、私有文件加载详解

Android

前言

上一篇我们实现了插件的皮毛,本打算兼并火狐浏览器的GeckoView,作为webview的又一个可选替代,但没能成功,加载动态库时总是失败。怪只怪功力不够深厚。最近又要给tesseract做一个插件,那就趁此机会再续前篇,把插件化做完整,并且深挖一些细节,以期对需要了解插件化技术的同学有所帮助。

加载native库

要使用JNI开发,首先要知道如何加载native库,也就是c++编写的代码,也是so库。在旧时代,我们是通过System.loadLibrary("xxx")来加载native库,缺点是无法检查加载是否成功。新时代,我们有System.load()。这个方法有2个参数:1是lib名字,必须与lib前缀的so库文件名一致;2是可变参数,每个元素代表一个搜索路径。如果加载成功,函数返回lib句柄,也就是native代码函数所在内存地址,加载失败返回0。可以利用System.load()特性,写一个通用的so库加载方法。

public static int loadLib(String libName, String... libraryPaths) {
    for (String libraryPath : libraryPaths) {
        File file = new File(libraryPath);
        if (!file.exists()) {
            continue;
        }
        try {
            System.load(new File(file, "lib" + libName + ".so").getAbsolutePath());
            return 0;
        } catch (Throwable e) {
            Log.e(TAG, "loadLib error: " + e.toString());
        }
    }
    return -1;
}

记得把加载结果打印出来,便于出错时排查问题。

Android 6.0及以上版本检查SO库版本

在Android 6.0及以上版本,我们还需要检测so库的版本。只要我们新增了native层功能,我们就得升级so库,或者换句话说,so库版本号发生变化了。这时,我们需要检测so库版本,确保apk和so库版本一致,不一致就升级插件。如何升级,这里暂不讨论,请读者自行解决,难度也不大。

so库版本存储在so库里的一个段里,可以用readelf -s查看。插件jar包里so库的版本从插件jar包apk文件中查看,apk文件的manifest部分有版本信息,对应的节点是<meta-data android:name="android.native.version" android:value="1"/>,其中value就是so库版本号。

下面是检测so库版本的方法。

private int checkLibVersion(Context context, String libName) {
    String soVersion = getSoVersion(context, libName);
    if (TextUtils.isEmpty(soVersion)) {
        return -1;
    }
    String metaData = AndroidPlugin.getInstance(context).getPluginApkMetaData(PLUGIN_LIB_VERSION);
    if (TextUtils.isEmpty(metaData)) {
        return -1;
    }
    int oldVersion = Integer.parseInt(metaData);
    int newVersion = Integer.parseInt(soVersion);
    if (oldVersion == newVersion) {
        return 0;
    }
    return -2;
}

private String getSoVersion(Context context, String libName) {
    String[] abis = getSupportedAbis();
    String version = null;
    for (String abi : abis) {
        version = getSoVersion(context, libName, abi);
        if (!TextUtils.isEmpty(version)) {
            break;
        }
    }
    return version;
}

private String getSoVersion(Context context, String libName, String abi) {
    String nativeLibDir = context.getApplicationInfo().nativeLibraryDir;
    if (TextUtils.isEmpty(nativeLibDir)) {
        return null;
    }
    File file = new File(nativeLibDir, abi + "/" + "lib" + libName + ".so");
    if (!file.exists()) {
        return null;
    }
    try {
        ProcessBuilder processBuilder = new ProcessBuilder("readelf", "-s", file.getAbsolutePath());
        Process process = processBuilder.start();
        BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()));
        String line;
        while ((line = br.readLine()) != null) {
            if (line.startsWith("Version:")) {
                return line.substring(8).trim();
            }
        }
        br.close();
        process.waitFor();
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

加载私有文件

通过插件化,我们完全可以加载私有文件。思路和加载native库一样,只不过我们加载的是其他apk中的资源文件,而不是so库。资源文件可以是任意格式,比如图片、文字、配置文件等,在插件apk的AndroidManifest.xml文件里配置即可。

<manifest>
    <application>
        <meta-data android:name="assets" android:value="assets/123.txt"/>
    </application>
</manifest>

在宿主的代码里,调用Context.getAssets()方法获取apk的assets目录,然后根据资源路径打开文件即可。

AssetManager assetManager = context.getAssets();
InputStream is = assetManager.open("123.txt");

插件化实现的难点在于,我们需要能找到包含资源的apk。不同于so库,资源文件的路径无法写死,不同apk安装在手机上的位置也不同。这时,我们需要借助AndroidPlugin里的apk查找功能。AndroidPlugin可以帮助我们找到已安装的插件apk,有了apk,我们就能获取其assets目录。需要注意的是,有些资源文件是放在raw目录的,而不是assets目录,这需要我们根据具体情况做特殊处理。

总结

Android插件化涉及的技术点很多,但核心原理并不难,关键是要理解apk的本质以及native层与java层的关联。本文介绍的动态库加载、私有文件加载,是插件化的基本功,希望对读者有所帮助。