原文地址: https://blog.csdn.net/Rong_L/article/details/75212472

前言

相信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;
}

6.0和之前的:

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;
}

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()流程

[Android Pro] so 动态加载—解决sdk过大问题的更多相关文章

  1. Android中的动态加载机制

    在目前的软硬件环境下,Native App与Web App在用户体验上有着明显的优势,但在实际项目中有些会因为业务的频繁变更而频繁的升级客户端,造成较差的用户体验,而这也恰恰是Web App的优势.本 ...

  2. 分享个刚写好的 android 的 ListView 动态加载类,功能全而代码少。

    (转载声明出处:http://www.cnblogs.com/linguanh/) 简介:      该ListView 实现动态加载数据,为了方便用户充分地自定义自己的数据源.点击事件,等核心操作, ...

  3. Android 实现布局动态加载

    Android 动态加载布局 通过使用LayoutInflater 每次点击按钮时候去读取布局文件,然后找到布局文件里面的各个VIEW 操作完VIEW 后加载进我们setContentView 方面里 ...

  4. Android学习——Fragment动态加载

    动态加载原理 利用FragmentManager来添加一套Fragment事务,最后通过commit提交该事务来执行对Fragment的相关操作. FragmentManager fragmentma ...

  5. Android中ListView动态加载数据

    1. 引言: 为了提高ListView的效率和应用程序的性能,在Android应用程序中不应该一次性加载ListView所要显示的全部信息,而是采取分批加载策略,随着用户的滑动,动态的从后台加载所需的 ...

  6. Android 自定义ListView动态加载数据

    我们都知道网络取数据是耗时操作,如果我们一次性请求所有数据,假如数据量不多那还可以接受,但是如果数据量特别多,那么带来的后果就是用户的愤怒(用户是很没有耐心的),所以这时候我们就需要动态的加载数据,分 ...

  7. Android Listview异步动态加载网络图片

    1.定义类MapListImageAndText管理ListViewItem中控件的内容 package com.google.zxing.client.android.AsyncLoadImage; ...

  8. Android 动态加载 (一) 态加载机制 案例一

    在目前的软硬件环境下,Native App与Web App在用户体验上有着明显的优势,但在实际项目中有些会因为业务的频繁变更而频繁的升级客户端,造成较差的用户体验,而这也恰恰是Web App的优势.本 ...

  9. Android动态加载jar/dex

    前言 在目前的软硬件环境下,Native App与Web App在用户体验上有着明显的优势,但在实际项目中有些会因为业务的频繁变更而频繁的升级客户端,造成较差的用户体验,而这也恰恰是Web App的优 ...

随机推荐

  1. 请手动释放你的资源(Please release resources manually)

    作者: Laruence(   ) 本文地址: http://www.laruence.com/2012/07/25/2662.html 转载请注明出处 我从来不认为这个问题是个问题, 直到昨天. 昨 ...

  2. Java桌面程序打包成exe可执行文件

    前言: 我们都知道Java可以将二进制程序打包成可执行jar文件,双击这个jar和双击exe效果是一样一样的,但感觉还是不同.其实将java程序打包成exe也需要这个可执行jar文件. 准备: ecl ...

  3. XPATH语法(一)

    Xpath简介 XPath即为XML路径语言,它是一种用来确定XML(标准通用标记语言的子集)文档中某部分位置的语言.XPath基于XML的树状结构,有不同类型的节点,包括元素节点,属性节点和文本节点 ...

  4. 追求极致--纯css制作三角、圆形按钮,兼容ie6

    参考了天猫.微博等网站的做法,用纯html和css实现,效果还是不错的.以下是成果,兼容主流浏览器,包括ie6. <!DOCTYPE html PUBLIC "-//W3C//DTD ...

  5. PHP 闭包

    一.闭包基本用法闭包(Closure)又叫做匿名函数,也就是没有定义名字的函数.比如下面的例子: // 定义一个闭包,并把它赋给变量 $f $f = function () { return 7; } ...

  6. python接口自动化测试十六:unittest完成用例

    import unittestimport requests def add(a, b): print('前置条件!!!!!:如登录') return a + b class TestAAA(unit ...

  7. Zookeeper集群部署与配置(三)

    在上一篇博客中我们讲解了<Zookeeper的单机配置>,此篇博客将继续介绍Zookeeper的集群部署与配置. 环境 集群配置的环境与单机配置的环境相同,唯一不同的就是集群是在多台服务器 ...

  8. Android SDK安装及配置模拟器

    环境搭建 1.安装JDK 2.下载Android sdk exe格式和zip格式都可以 3.安装installer_r24.4.1-windows.exe文件,里面有两个应用程序: "SDK ...

  9. JavaScript错误:Maximum call stack size exceeded错误

    错误的表面意思是,因为递归次数太多而内存溢出, 当然引起溢出的原因很多 找了下问题来源,发现引用了两个版本的jquery,在layout.cshtml母模块页中和视图中都引用了jq.导致循环调用,从而 ...

  10. F 多重背包 判断能否刚好装满

    Description 有n种不同大小的数字,每种各个.判断是否可以从这些数字之中选出若干使它们的和恰好为K. Input 首先是一个正整数T(1<=T<=100)接下来是T组数据每组数据 ...