Android插件化之动态库、私有文件加载详解
2023-11-19 21:43:30
前言
上一篇我们实现了插件的皮毛,本打算兼并火狐浏览器的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层的关联。本文介绍的动态库加载、私有文件加载,是插件化的基本功,希望对读者有所帮助。