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

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/aqi00/article/details/72763742
前一阵项目上要求实现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开发笔记的完整目录

__________________________________________________________________________
博主现已开通微信公众号“老欧说安卓”,打开微信扫一扫下面的二维码,或者直接搜索公众号“老欧说安卓”添加关注,更快更方便地阅读技术干货。

阅读更多

没有更多推荐了,返回首页