前一阵项目上要求实现App的so库动态加载功能,因为这块本来就有成熟的方案,所以一般的实现没什么难度。可是到项目测试中,才发现有不少意料之外的情况,需要一一针对处理,故此记录一下具体的解决办法,以供后来者参考。
按App加载so库的正常流程,在编译前就要把so文件放到工程的jniLibs目录,这样会把so直接打包进apk安装包,然后App在启动时就会预先加载so库。具体的加载代码一般是在Activity页面中增加下面几行,表示在实例化该页面的时候,一开始就从系统目录加载名为libjni_mix.so的库:
若要运用动态加载技术,编译前不把so文件放入jniLibs目录(原因很多,比如想减小安装包的大小),自然打包生成的安装包也不包含该so。接着在手机上安装这个apk并启动App,如果App的运行不涉及到jni方法的调用,那相安无事就当so不存在;如果App打开了某个页面,而该页面又需要调用jni方法,则App自动到指定地址下载需要的so文件,然后保存到用户目录,并从用户目录加载该so,最后再调用jni方法。
把下载完成的so文件复制到用户目录,可参考以下代码(注意判断文件大小,如果用户目录已经存在相同大小的文件,就无需重复拷贝了):
so文件复制完成,接下来就可以加载用户目录下的so了,完整的加载代码如下所示:
不出意外的话,以上代码已经实现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,
2、删除EasyAR.jar里面的EasyARNative.class文件,另外在项目工程新建同样类名且同样文件内容的EasyARNative.java,只是把里面的下述代码删除:
正所谓一波三折,麻烦事还没结束,换台运行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开发笔记的完整目录
__________________________________________________________________________
博主现已开通微信公众号“老欧说安卓”,打开微信扫一扫下面的二维码,或者直接搜索公众号“老欧说安卓”添加关注,更快更方便地阅读技术干货。
按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开发笔记的完整目录
__________________________________________________________________________
博主现已开通微信公众号“老欧说安卓”,打开微信扫一扫下面的二维码,或者直接搜索公众号“老欧说安卓”添加关注,更快更方便地阅读技术干货。
作者:aqi00 发表于2017/5/26 10:09:25 原文链接
阅读:211 评论:0 查看评论