(转载)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文件分为动态加载和静态加载。
-
静态加载就是通过
System.loadLibrary(Sting libname);
来直接加载,对于一个app它只能加载system的和我们自己添加到jniLibs下的so文件。图2-1
这个是我的demo项目的路径,静态加载回去这些路径下找到对应的库,否则抛出异常。
-
动态加载这是通过
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时就通过PathClassLoader
的findLibrary(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代码,本人技术有限暂时没找到答案,不知道大家是否更好的解答或者解决方法。