so 动态加载—解决sdk过大问题

作者 : VIP闪拼 本文共6324个字,预计阅读时间需要16分钟 发布时间: 2019-11-5 共16.63K人阅读
广告位招租500/月
s o 动态加载—解决sdk过大问题
前言
相信Android 开发中大家或多或少都会集成一些第三方sdk, 而其中难免要会使用到他们的so文件。但有时,你会发现这些so文件过多,对于一些需要经常更新的应用来说,这将会大大浪费用户的流量。而有些sdk的集成仅仅是只为了一个不是必须的功能,我们完全有充足的理由用一些技术的手段来解决因这部分sdk集成带来的安装包大小问题。

so目录
观察发现,很多sdk的大小主要集中在so文件上。为了尽可能多的适应不同cup,sdk通常会提供不同二进制文件,这些文件被分门别类地放在armeabi,x86,mips等目录下。这里我们有必要了解下这些目录的含义。

不同cpu在apk应用安装时,会查找对应的目录,比如,arm64位机子,会优先查看apk中是否有arm64-v8a目录,如果有,则采用该目录下的so文件,如果没有,则会查找兼容的目录。一旦确定下目录之后,其他的目录便不会再去管了。(日后如果在确定的目录下没有找到对应的so文件,也不会去其他目录中找到)

目前市面上大部分手机都兼容armeabi-v7a,哪怕x86的cpu也会兼容(性能会有损耗)。所以armeabi-v7a目录建议一定配置,其相比armeabi在性能上有很大的提升。

再次回到前言中的问题,我们有没有什么办法能够减少so的大小,从而减少apk安装包的大小呢?
1. 如果不太在意性能的损耗,那么我们完全可以只适配armeabi-v7a包和x86包,让64位机器运行32位的so文件。
2. 单独出arm版本和x86版本,这样也可以减少一半的so大小。

可如果你觉得这样包还是太大,比如我们现在用的crosswalk浏览器内核,单个so文件就达到了27M,同时适配x86的话,会达到58M, 这是我们所无法接受的事情!

于是乎开始想有没有什么办法能把so文件与apk文件分离开来,在程序运行的时候来把so文件下载下来,并引导程序去加载。从而实现动态加载so文件的目的。

System.load 与 System.loadLibrary
google出的结果直接导向了System.load和System.loadLibrary这两个方法。
system.load 参数中加载的so的路径,比如:system.load(“/data/data/com.codemao.android/libs/libcrosswalk.so”)
system.loadLibrary参数中传入的是so的名称,比如system.loadLibrary(“crosswalk”), 系统会自动根据名称与机器的cpu型号,找到对应的so目录,并加载对应的lib crosswalk.so文件。
(两者文件都只能在app的私有目录下)

那这样子的话,是不是我们从远程下载完so文件之后,解压到app私有目录下,在调用sdk的地方调用system.load主动加载so之后,就可以实现动态加载so文件了呢?

同学,你真是太天真啦!我们回想下自己写的so文件是如何调用的?是不是在需要使用的类里主动调了system.loadLibrary呢?sdk也一样,sdk在自己的代码里主动调用了system.loadLibrary。而这时,我们so文件因为没有随着apk安装到手机上,并不在它的寻找范围之内,最后的结果是你即使调用了system.load加载了so文件,理论是可以找到相应的native方法了,但是sdk在调用system.loadLibrary时会抛出找不到对应的so文件的错误。

插件化如何处理so
这该如何处理sdk内部调用loadLibrary抛出的异常信息呢?apk内的so文件最终被放到了/data/app/com.codemao.android/lib/下面,我们总不能把远程下载下来的so文件放入这里吧,可/data/app这个目录下面的文件我们是没有权限去执行读写操作的。

这里我们想到了另一个问题,插件化可以运行另一个apk,而apk里面难免会有so,那宿主程序又是如何处理插件的so文件呢?
查询之后发现
————————————————
有时候我们在开发插件的时候,可能会调用so文件,一般来说有两种方案:
一种是在加载插件的时候,先把插件中的so文件释放到本地目录,然后在把目录设置到DexClassLoader类加载器的nativeLib中。
一种在插件初始化的时候,释放插件中的so文件到本地目录,然后使用System.load方法去全路径加载so文件
这两种方式的区别在于,第一种方式的代码逻辑放在了宿主工程中,同时so文件可以放在插件的任意目录中,然后在解压插件文件找到这个so文件释放即可。第二种方式的代码逻辑是放在了插件中,同时so文件只能放在插件的assets目录中,然后通过把插件文件设置到程序的AssetManager中,最后通过访问assets中的so文件进行释放。
————————————————
我们自己apk使用的classloader是pathclassloader, 那我们是不是只要把so所在的目录加入到pathclassloader的nativeLib之中就好了呢? 
让我们再次来看下system.loadLibrary:
 

Runtime.java

好的,这里我们看到加载的过程主要两步:
1. 调用pathclassloader.findLibrary,先找到对应的so文件
2. 调用doLoad加入找到的so文件那我们来看下classloader.findLibrary是如何找到对应so文件的:  pathList 是BaseDexClassLoader 里的DexPathList对象(注意6.0 开始nativeLibraryDirectories放的不在是File, 不过加载逻辑是一样的, 要注意适配。)  这里主要做的事: 1. 调用system.mapLibraryName, 补全名称, 如比libraryName=crosswalk, 补全之后会是lib crosswalk.so 2. 遍历nativeLibraryDirectories,看下目录下面有对应的文件吗
哈哈,到这里,机会来了,我们只要把远程下载so的目录通过反射的方式放入nativeLibraryDirectories中就ok啦,真是太激动啦!!!适配与实现方案为了尽量减少性能损耗,我们先根据cpu的类型确定自己要下载的so文件,之后再用反射的方式把so的目录加入到classloader中,这样便可以解决so过大而引起apk包过大的问题。
但我们前面说过,6.0之后的DexPathList与6.0之前的DexPathList不一样,这里要注意适配的问题,6.0之后findLibrary 变为了:  Element 中的代码如下: 所以我这里直接给出适配好的关键代码,供大家参考/*** 将 so所在的目录放入PathClassLoader里的nativeLibraryDirectories中*@Param context*/public void installSoDir(Context context) {
    //安卓4.0以下不维护    if(Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {        return ;    }    File soDirFile = context.getDir(soDir, Context.MODE_PRIVATE);    if(!soDirFile.exists()) {        soDirFile.mkdirs();    }    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {        v23Install(soDirFile, context);    } else {        v14Install(soDirFile, context);    }}
private void v14Install(File soDirFile, Context context) {        PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();        Object pathList = getPathList(pathClassLoader);        if(pathList != null) {            //获取当前类的属性            try {                Field nativeLibraryDirectoriesField = pathList.getClass().getDeclaredField(“nativeLibraryDirectories”);                nativeLibraryDirectoriesField.setAccessible(true);                Object list = nativeLibraryDirectoriesField.get(pathList);                if(list instanceof List) {                    ((List) list).add(soDirFile);                } else if(list instanceof File[]) {                    File[] newList = new File[((File[]) list).length + 1];                    System.arraycopy(list, 0 , newList, 0, ((File[]) list).length);                    newList[((File[]) list).length] = soDirFile;                    nativeLibraryDirectoriesField.set(pathList, newList);                }            } catch (NoSuchFieldException e) {                e.printStackTrace();            } catch (IllegalAccessException e) {                e.printStackTrace();            }        }    }
private void v23Install(File soDirFile, Context context) {        PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();        Object pathList = getPathList(pathClassLoader);        if(pathList != null) {            //获取当前类的属性            try {                Field nativeLibraryPathField = pathList.getClass().getDeclaredField(“nativeLibraryPathElements”);                nativeLibraryPathField.setAccessible(true);                Object list = nativeLibraryPathField.get(pathList);                Class<?> elementType = nativeLibraryPathField.getType().getComponentType();                Constructor<?> constructor = elementType.getConstructor(File.class, boolean.class, File.class, DexFile.class);                constructor.setAccessible(true);                Object element = constructor.newInstance(soDirFile, true, null, null);                if(list instanceof List) {                    ((List) list).add(element);                } else if(list instanceof Object[]) {                    Object[] newList = new File[((Object[]) list).length + 1];                    System.arraycopy(list, 0 , newList, 0, ((Object[]) list).length);                    newList[((Object[]) list).length] = element;                    nativeLibraryPathField.set(pathList, newList);                }            } catch (NoSuchFieldException e) {                e.printStackTrace();            } catch (IllegalAccessException e) {                e.printStackTrace();            } catch (NoSuchMethodException e) {                e.printStackTrace();            } catch (InstantiationException e) {                e.printStackTrace();            } catch (InvocationTargetException e) {                e.printStackTrace();            }        }    }private Object getPathList(Object classLoader) {        Class cls = null;        String pathListName = “pathList”;        try {            cls = Class.forName(“dalvik.system.BaseDexClassLoader”);            Field declaredField = cls.getDeclaredField(pathListName);            declaredField.setAccessible(true);            return declaredField.get(classLoader);        } catch (ClassNotFoundException e) {            e.printStackTrace();        } catch (NoSuchFieldException e) {            e.printStackTrace();        } catch (IllegalAccessException e) {            e.printStackTrace();        }        return null;    }

版权说明:

1:如非特殊说明,本站对提供的源码不拥有任何权利,其版权归原著者拥有。
2:请勿将该源码、软件进行商业交易、转载等行为,该源码、软件只为研究、学习所提供,该软件使用后发生的一切问题与本站无关。
3:本网站所有源码和软件均为作者提供和网友推荐收集整理而来,仅供学习和研究使用。
4:如有侵犯你版权的,请来信(邮箱:admin@vipshanpin.com)指出,本站将立即改正。

VIP闪拼 » so 动态加载—解决sdk过大问题

常见问题FAQ

免费下载或者VIP会员专享资源能否直接商用?
本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
提示下载完但解压或打开不了?
最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。 若排除这种情况,可在对应资源底部留言,或 联络我们。
找不到素材资源介绍文章里的示例图片?
对于会员专享、整站源码、程序插件、网站模板、网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。

定 制 开 发 服 务

♥ 了解详情 在线客服