(转载)【Android进阶】Android动态加载so文件

(转载)http://blog.cyning.cc/2017/07/18/dynamic-load-so/

随着业务的增大,我们的业务代码也随之增多,包的大小是有增无减,所以适当的时候思考下:怎么做减法–减小包的体积。

结合最近在做的公司的项目,觉得动态加载so文件是一个很好精简apk包的方法。举个例子,视频播放器的SDK(如IJKplayer,VLC player),他们的各种视频的解码器一般都是通过C/C++编译的so文件,这些so文件其实都不小,这样导致我们从市场上下载的apk包很大,所以能不能让so文件不随apk一起发布呢,而是按需下载(只有当需要播放视频时才去服务器下载,然后再在本地load)。

为什么要动态加载

其实刚才已经解释了,可以有效避免apk安装包过大,因为这些so文件是依赖server的下发,本地只是load的过程。
其次,动态加载可以动态升级so文件,也是动态化的一部分。可以在不发版的情况下,升级so文件。

动态加载so文件,必须进行安全性校验,避免不必要的安全事故。

动态加载so文件

1. System.load(String filePath)

加载so文件分为动态加载和静态加载。

  1. 静态加载就是通过System.loadLibrary(Sting libname);来直接加载,对于一个app它只能加载system的和我们自己添加到jniLibs下的so文件。
    图2-1图2-1

    这个是我的demo项目的路径,静态加载回去这些路径下找到对应的库,否则抛出异常。

  2. 动态加载这是通过System.load(String filePath)来加载filePath对应路径下的so文件,这个路径不可以是外置SDcard等拓展路径,必须是/data/**{package}下。

所以下发的so没有权限放到图2-1下,只能通过加载的so文件路径的方式来动态加载so文件。

方案1: 将so文件copy到/data/**{package}下,system.load(filePath).

2. 支持静态加载

但是我们这样做还是解决不了问题,因为有些so文件加载的过程是放到sdkxia的,如百度地图sdk,已经封装了加载so文件(静态加载),即使你已经实现了方案1仍然扔出UnsatisfiedLinkError的异常。
要弄清这个过程,就必须了解so的加载过程,以我的本地的android skd(Android)为例。
System源码

1
2
3
public static void loadLibrary(String libname) {
              Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
   }

 

RunningTime

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
synchronized void loadLibrary0(ClassLoader loader, String libname) {
      if (libname.indexOf((int)File.separatorChar) != -1) {
          throw new UnsatisfiedLinkError(
  "Directory separator should not appear in library name: " + libname);
      }
      String libraryName = libname;
      if (loader != null) {
      // 去loade中查找libraryName命令的library
          String filename = loader.findLibrary(libraryName);
          if (filename == null) {
              // It's not necessarily true that the ClassLoader used
              // System.mapLibraryName, but the default setup does, and it's
              // misleading to say we didn't find "libMyLibrary.so" when we
              // actually searched for "liblibMyLibrary.so.so".
              throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
                                             System.mapLibraryName(libraryName) + "\"");
          }
          String error = doLoad(filename, loader);
          if (error != null) {
              throw new UnsatisfiedLinkError(error);
          }
          return;
      }

      String filename = System.mapLibraryName(libraryName);
      List<String> candidates = new ArrayList<String>();
      String lastError = null;
      for (String directory : getLibPaths()) {
          String candidate = directory + filename;
          candidates.add(candidate);

          if (IoUtils.canOpenReadOnly(candidate)) {
              String error = doLoad(candidate, loader);
              if (error == null) {
                  return; // We successfully loaded the library. Job done.
              }
              lastError = error;
          }
      }

      if (lastError != null) {
          throw new UnsatisfiedLinkError(lastError);
      }
      throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates);
  }

 

代码中的loader是ClassLoader的对象,对于Android实际上是PathClassLoader,这个意思就是当有classLoader时就通过PathClassLoaderfindLibrary(libraryName)来加载(这个好像加载class),若无classLoader就通过mapLibraryName1()

建议大家看下native层怎么实现的:深入理解 System.loadLibrary

我们加载so看classLoader是怎么实现的,Android 5.0的源码源码:
BaseDexClassLoader.java的源码

1
2
3
4
@Override
   public String findLibrary(String name) {
       return pathList.findLibrary(name);
   }

 

pathList就是我们的DexPathList对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
final class DexPathList {

    private static final String DEX_SUFFIX = ".dex";
    /** class definition context */
    private final ClassLoader definingContext;
    /**
     * List of dex/resource (class path) elements.
     * Should be called pathElements, but the Facebook app uses reflection
     * to modify 'dexElements' (http://b/7726934).
     */
    private final Element[] dexElements;
    /** List of native library directories. */
    private final File[] nativeLibraryDirectories;
     …………
    public String findLibrary(String libraryName) {
        String fileName = System.mapLibraryName(libraryName);
        for (File directory : nativeLibraryDirectories) {
            String path = new File(directory, fileName).getPath();
            if (IoUtils.canOpenReadOnly(path)) {
                return path;
            }
        }
        return null;
    }
    …………
}

看到了吧,会先找system下的so文件,再找nativeLibraryDirectories下的,而这个nativeLibraryDirectories就是我们的自己项目中jniLibs下对应的so文件的路径。
当以当我们静态加载时,其实找的so文件就是nativeLibraryDirectories,所以我们可以以此作为突破口,利用反射,将这个nativeLibraryDirectories的开始处加上我们自己放so的文件夹下(感觉像QQ空间对class做patch的方式哦,其实替换旧的so文件这种可以可行的)。
开始hook啦。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
PathClassLoader pathClassLoader = (PathClassLoader) context.getApplicationContext().getClassLoader();
       try {
           Object pathList = getPathList(pathClassLoader);
                
           // 拿到nativeLibraryDirectories的Field
           Field nativeLibraryDirectoriesField = pathList.getClass().getDeclaredField("nativeLibraryDirectories");
           nativeLibraryDirectoriesField.setAccessible(true);
           File[] libPaths = (File[]) nativeLibraryDirectoriesField.get(pathList);
           File[] envilLibPaths = new File[libPaths.length + 1];
           // 将存放我们自己so的文件夹加到第一位
           envilLibPaths[0] = dir;
           // 将原来的路径追加到后面
           for (int i = 0; i < libPaths.length; i++) {
               envilLibPaths[i + 1] = libPaths[i];
           }
           // 将新的nativeLibraryDirectories设置给pathList
           nativeLibraryDirectoriesField.set(pathList, envilLibPaths);
       } catch (NoSuchFieldException e) {
           e.printStackTrace();
       } catch (IllegalAccessException e) {
           e.printStackTrace();
       } catch (ClassNotFoundException e) {
           e.printStackTrace();
       }

 

这个代码是在14-22都是ok的,但是23源码不是这样滴,看源码吧:
23的源码先放这,hook起来也不难。

Android 23源码建议hook nativeLibraryPathElements这个而不是nativeLibraryDirectories;

方案2:Hook DexPathList的nativeLibraryPathElements或者nativeLibraryDirectories,将我们自定义存so文件的文件夹作为他们的第一个元素。

出现的问题

刚开始我把所有视频相关的so文件扔到本地的一个文件下,再copy到/data/**{package}下,居然报32-bit instead of 64-bit 这个错误,我把so再放到jniLibs/armeabi下再跑可以啊,后来google了下发现有人在动态化时也遇到了,其中Anjon-github提到了一个方案:只要找任意一个32位的so文件(当然越小越好了)放到主程序中即可,于是我找了个1k的so文件放到了项目的jniLibs/armeabi下居然真的可以,这个原因不知为何,这个涉及到native代码,本人技术有限暂时没找到答案,不知道大家是否更好的解答或者解决方法。

  • 0
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值