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


前言

相信Android 开发中大家或多或少都会集成一些第三方sdk, 而其中难免要会使用到他们的so文件。但有时,你会发现这些so文件过多,对于一些需要经常更新的应用来说,这将会大大浪费用户的流量。而有些sdk的集成仅仅是只为了一个不是必须的功能,我们完全有充足的理由用一些技术的手段来解决因这部分sdk集成带来的安装包大小问题。

so目录

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

目录 cpu类型
armeabi ARM 通用cpu
armeabi-v7a 支持浮点运算ARM cpu,向下兼容armeabi
arm64-v8a ARM 64位cpu, 向下兼容armeabi-v7a
x86 x86通用型cpu
x86_64 x86 64位cpu

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

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


动态加载so

再次回到前言中的问题,我们有没有什么办法能够减少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:

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

Runtime.java

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;  //loader这里传入的是pathclassloader, 不为空if (loader != null) {  //调用findLibrary找到so路径String filename = loader.findLibrary(libraryName);  if (filename == null) {    throw new UnsatisfiedLinkError(loader + " couldn't find \"" +  System.mapLibraryName(libraryName) + "\"");  }  //调用doLoad加载找到的so文件String error = doLoad(filename, loader);  if (error != null) {  throw new UnsatisfiedLinkError(error);  }  return;  }...//以下逻辑我们可以暂且忽略}

好的,这里我们看到加载的过程主要两步:
1. 调用pathclassloader.findLibrary,先找到对应的so文件
2. 调用doLoad加入找到的so文件

那我们来看下classloader.findLibrary是如何找到对应so文件的:

@Override
public String findLibrary(String name) {  return pathList.findLibrary(name);
}

pathList 是BaseDexClassLoader 里的DexPathList对象(注意6.0 开始nativeLibraryDirectories放的不在是File, 不过加载逻辑是一样的, 要注意适配。)

public String findLibrary(String libraryName) {String fileName = System.mapLibraryName(libraryName);for (File directory : nativeLibraryDirectories) {File file = new File(directory, fileName);if (file.exists() && file.isFile() && file.canRead()) {return file.getPath();}}return null;}

这里主要做的事:
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 变为了:

 public String findLibrary(String libraryName) {String fileName = System.mapLibraryName(libraryName);for (Element element : nativeLibraryPathElements) {String path = element.findNativeLibrary(fileName);if (path != null) {return path;}}return null;
}

Element 中的代码如下:

 public String findNativeLibrary(String name) {maybeInit();if (isDirectory) {String path = new File(dir, name).getPath();if (IoUtils.canOpenReadOnly(path)) {return path;}} else if (zipFile != null) {String entryName = new File(dir, name).getPath();if (isZipEntryExistsAndStored(zipFile, entryName)) {return zip.getPath() + zipSeparator + entryName;}}return null;}

所以我这里直接给出适配好的关键代码,供大家参考

/**
* 将 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;}

参考文章

Android中so使用知识和问题总结以及插件开发过程中加载so的方案解析

Android项目针对libs(armeabi,armeabi-v7a,x86)进行平台兼容

Android JNI之System.loadLibrary()流程

so 动态加载---解决sdk过大问题相关推荐

  1. android 中动态加载广告sdk

    关于动态加载sdk的话题,网上介绍比较多:今天通过第三方工具来取个巧,虽然方法也有点不太靠谱,但终归是一个思路. 项目里面接入了穿山甲sdk和广点通sdk广告,结果放到应用市场时说被拒了,因为有广告, ...

  2. 使用jquery的load方法设计动态加载,并解决被加载页面JavaScript失效问题

    使用jquery的load方法设计动态加载,并解决被加载页面JavaScript失效问题 参考文章: (1)使用jquery的load方法设计动态加载,并解决被加载页面JavaScript失效问题 ( ...

  3. vue 动态加载图片路径报错解决方法

    vue 动态加载图片路径报错解决方法 参考文章: (1)vue 动态加载图片路径报错解决方法 (2)https://www.cnblogs.com/qingcui277/p/8930507.html ...

  4. android 动态加载sdk,LiteAVSDK集成,因此可以动态加载库

    在Android开发中,Android Studio用于集成第三方SDK(例如,腾讯视频云移动直播,播放器,短视频以及实时音频和视频). 通常有两种集成方法: aar集成和jar + so集成. An ...

  5. Python爬虫入门 | 7 分类爬取豆瓣电影,解决动态加载问题

      比如我们今天的案例,豆瓣电影分类页面.根本没有什么翻页,需要点击"加载更多"新的电影信息,前面的黑科技瞬间被秒--   又比如知乎关注的人列表页面:   我复制了其中两个人昵称 ...

  6. 在线客服系统源码开发实战总结:动态加载js文件实现粘贴一段js的sdk代码,直接引入插件效果...

    常见的在线客服系统中,或者是统计代码中,粘贴一段js代码,就能引入某个插件的效果.这个是怎么实现的呢? 原理非常的简单: 对于不同的加载文件类型创建不同的节点,然后添加各自的属性,最后扔到head 标 ...

  7. java动态加载jar时,jar中还有第三方jar无法加载的解决方法

    java动态加载jar时,jar中还有第三方jar无法加载的解决方法 当java插件化开发时,即一个java程序在运行的情况下动态加载另一个jar,网上大多数的方法如下 public static v ...

  8. vue 动态获取的图片路径不显示_解决Vue动态加载本地图片问题

    最近遇到了个问题,用v-bind动态绑定img的src,图片加载不出来.控制台显示src是有获取到图片地址的,可是就是加载不出来. 最后才发现原因原来是浏览器中直接显示'./img/img1.png' ...

  9. 解决URL存在特殊符号、异步线程池配置、动态加载lib下所有jar包

    一.解决URL存在特殊符号|{}?&.URL中包含%2F.URL中包含%5C import lombok.extern.slf4j.Slf4j; import org.springframew ...

最新文章

  1. 取没有date的邮件发送时间
  2. Java:十六进制转换成十进制
  3. 调用webservice或wcf时,提示:无法加载协定为的终结点配置部分,因为找到了该协定的多个终结点配置。请按名称指示首选的终结点配置部分。
  4. 2015.05.15,外语,学习笔记-《Word Power Made Easy》 01 “如何讨论人格特点”
  5. 牛客 - Alice and Bob(尺取+二分)
  6. LeetCode: Longest Common Prefix
  7. 炸窝Vector简介
  8. windows10 计算文件的HASH(SHA256\MD5等)
  9. Cloudera Manager 和 CDH 4 终极安装
  10. python 整除的数组_LeetCode 974. 和可被 K 整除的子数组 | Python
  11. edui 富文本编辑_百度umeditor富文本编辑器插件扩展
  12. NLP 学习笔记9-停用词
  13. 浅层砂过滤器 全自动浅层介质过滤系统
  14. 【通过输入身份证前17位得到省份,性别,年龄,出生日期和最后一位】
  15. 负载均衡及其常见实现方式
  16. Mac中使用Mounty对NTFS文件进行读写操作,报错“卷不能在读/写模式下重新挂载,可能是因为先前没有完全卸载(安全删除)”问题的解决
  17. 白下高新区妇联、科协举办亲子活动,小朋友们走进云创大数据
  18. Havok VS PhysX 漫谈物理加速世界!
  19. 别把激励员工变成收买员工
  20. OpenWrt之网络配置文件(基于新三mt7621)

热门文章

  1. 2020年中国人工智能产业发展形势展望
  2. 区块链三加一:详解AT模型
  3. 电脑Console连接设备
  4. PDF文件在线压缩的方法
  5. IM开放平台的客户端接口设计,千牛用了JAR包形式?
  6. 中石油集团不会整体上市
  7. 敌兵布阵(树状数组)
  8. [唯一索引允许为空]的说法是不严谨的
  9. 楼教主的百度程序大赛答题源码
  10. Android用户界面设计