2

动态加载so库的实现方法与问题处理

 3 years ago
source link: http://www.androidchina.net/7036.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
动态加载so库的实现方法与问题处理 – Android开发中文站

前一阵项目上要求实现App的so库动态加载功能,因为这块本来就有成熟的方案,所以一般的实现没什么难度。可是到项目测试中,才发现有不少意料之外的情况,需要一一针对处理,故此记录一下具体的解决办法,以供后来者参考。

按App加载so库的正常流程,在编译前就要把so文件放到工程的jniLibs目录,这样会把so直接打包进apk安装包,然后App在启动时就会预先加载so库。具体的加载代码一般是在Activity页面中增加下面几行,表示在实例化该页面的时候,一开始就从系统目录加载名为libjni_mix.so的库:

static {
System.loadLibrary("jni_mix");
}

若要运用动态加载技术,编译前不把so文件放入jniLibs目录(原因很多,比如想减小安装包的大小),自然打包生成的安装包也不包含该so。接着在手机上安装这个apk并启动App,如果App的运行不涉及到jni方法的调用,那相安无事就当so不存在;如果App打开了某个页面,而该页面又需要调用jni方法,则App自动到指定地址下载需要的so文件,然后保存到用户目录,并从用户目录加载该so,最后再调用jni方法。

把下载完成的so文件复制到用户目录,可参考以下代码(注意判断文件大小,如果用户目录已经存在相同大小的文件,就无需重复拷贝了):

public static boolean copyLibraryFile(Context context, String origPath, String destPath) {
boolean copyIsFinish = false;
try {
File dirFile = new File(destPath.substring(0, destPath.lastIndexOf("/")));
if (dirFile.exists() != true) {
dirFile.mkdirs();
}
FileInputStream is = new FileInputStream(new File(origPath));
File file = new File(destPath);
if (file.exists()) {
Log.d(TAG, "src file size="+is.available());
Log.d(TAG, "dest file size="+file.length());
if (file.length() == is.available()) {
return true;
}
}
file.createNewFile();
FileOutputStream fos = new FileOutputStream(file);
byte[] temp = new byte[1024];
int i = 0;
while ((i = is.read(temp)) > 0) {
fos.write(temp, 0, i);
}
fos.close();
is.close();
copyIsFinish = true;
catch (Exception e) {
e.printStackTrace();
}
return copyIsFinish;
}

so文件复制完成,接下来就可以加载用户目录下的so了,完整的加载代码如下所示:

File dir = this.getDir("libs", Activity.MODE_PRIVATE);
File destFile = new File(dir.getAbsolutePath() + File.separator + fileName);
if (copyLibraryFile(this, path, destFile.getAbsolutePath())){
//使用load方法加载内部储存的SO库
System.load(destFile.getAbsolutePath());
//下面调用jni方法,举例如下:
//String desc = JniCpuActivity.cpuFromJNI(1, 0.5f, 99.9, true);
}

不出意外的话,以上代码已经实现so库的动态加载功能。可是这并不意味着大功告成,因为项目里面用到了第三方的sdk,即一个增强现实厂商推出的EasyAR,他们的sdk除了libEasyAR.so,还有另外一个jar包即EasyAR.jar。虽然App工程里面对so文件做了动态加载处理,但运行时加载so仍然报错“java.lang.UnsatisfiedLinkError: dalvik.system.PathClassLoader *** couldn’t find “libEasyAR.so””。排查结果发现,EasyAR.jar里面的EasyARNative类会从系统目录加载so库,也就是仍然调用了“System.loadLibrary(“EasyAR”);”。因为App无法把so文件复制到系统目录,所以导致System.loadLibrary方法找不到libEasyAR.so。

关于系统目录找不到so库的问题,解决办法找到了以下两个:
1、把App动态加载so的目录加入到系统目录列表nativeLibraryDirectories,

private static void createNewNativeDir(Context context) throws Exception {
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
Field declaredField = Class.forName("dalvik.system.BaseDexClassLoader").getDeclaredField("pathList");
declaredField.setAccessible(true);
Object pathList = declaredField.get(pathClassLoader);
// 获取当前类的属性
Object nativeLibraryDirectories = pathList.getClass().getDeclaredField("nativeLibraryDirectories");
((Field) nativeLibraryDirectories).setAccessible(true);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
// 获取 DEXPATHList中的属性值
File[] files = (File[]) ((Field) nativeLibraryDirectories).get(pathList);
Object filesss = Array.newInstance(File.class, files.length + 1);
// 添加自定义.so路径
Array.set(filesss, 0, getLibraryDir(context));
// 将系统自己的追加上
for (int i = 1; i < files.length + 1; i++) {
Array.set(filesss, i, files[i - 1]);
}
((Field) nativeLibraryDirectories).set(pathList, filesss);
else {
ArrayList<File> files = (ArrayList<File>) ((Field) nativeLibraryDirectories).get(pathList);
ArrayList<File> filesss = (ArrayList<File>) files.clone();
filesss.add(0, getLibraryDir(context));
((Field) nativeLibraryDirectories).set(pathList, filesss);
}
}

不料好事多磨,该办法在4.4真机上测试通过,但在6.0真机上依然出现闪退。

2、删除EasyAR.jar里面的EasyARNative.class文件,另外在项目工程新建同样类名且同样文件内容的EasyARNative.java,只是把里面的下述代码删除:

static {
System.loadLibrary("EasyAR");
}

这样做的目的是不从系统目录加载so,只从用户目录加载so文件。接下来重新编译程序,4.4真机和6.0真机都能正常调用jni方法了。

正所谓一波三折,麻烦事还没结束,换台运行Android7.0的真机,动态加载so时再次出现闪退,真叫人欲哭无泪(出错日志为Java.lang.UnsatisfiedLinkError: dlopen failed: “***.so” is 32-bit instead of 64-bit)。只能硬着头皮再三想办法,查阅了大量资料,最终定位原因如下:
一、所有的App在运行时,都是由Zygote进程创建VM再运行的。

二、一般设备只支持32位系统,但有些新设备已经支持64位(同时兼容32位)。对于这些新设备来说,有两个Zytgote(一个32位,一个64位)进程同时运行。

三、当App运行在64位系统上,又区分以下三种情况:

1、如果App只包含64位的so库,则它将运行在一个64位的进程中,即VM是由Zytgote 64创建的。

2、如果App包含32位的so库,则它将运行在一个32位的进程中,即VM是由Zytgote创建的。

3、如果App不包含任何so库,则它将默认运行在64位的进程中。

显然上面采用动态加载的App属于第三种情况,此时启动了64位进程,但动态加载的so库却是32位的,所以会闪退。如果不采用动态加载,一开始就把so库打进安装包,则属于第二种情况,App运行时启动的是32位进程,此时不会闪退。

因此,对于7.0真机这种64位的系统,处理动态加载so的可能办法有两个:

1、所有so文件都编译为64位版本,但这样就无法在32位系统上调用so,故而不可行;

2、先把一个32位的so文件打进安装包,其它so库在运行时动态加载,这样App启动的是32位进程,动态加载的so库也是32位版本,运行时就不再闪退;

转载请注明:Android开发中文站 » 动态加载so库的实现方法与问题处理


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK