最新消息:欢迎访问Android开发中文站!商务联系微信:loading_in

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

开发进阶 loading 4460浏览 0评论

前一阵项目上要求实现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库的实现方法与问题处理

您必须 登录 才能发表评论!