热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

插件化so库加载原理及实现

系统加载so库的工作流程当我们调用当调用System#loadLibrary(“xxx”)后,AndroidFramework都干了些了啥?
系统加载 so 库的工作流程

当我们调用当调用 System#loadLibrary(“xxx” ) 后,Android Framework 都干了些了啥?

插件化so库加载原理及实现
插件化so库加载原理及实现

static { System.loadLibrary("ymm_log"); }

在看下System类的实现:

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

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) { 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 candidates = new ArrayList(); 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); } private String doLoad(String name, ClassLoader loader) { // Android apps are forked from the zygote, so they can't have a custom LD_LIBRARY_PATH, // which means that by default an app's shared library directory isn't on LD_LIBRARY_PATH. // The PathClassLoader set up by frameworks/base knows the appropriate path, so we can load // libraries with no dependencies just fine, but an app that has multiple libraries that // depend on each other needed to load them in most-dependent-first order. // We added API to Android's dynamic linker so we can update the library path used for // the currently-running process. We pull the desired path out of the ClassLoader here // and pass it to nativeLoad so that it can call the private dynamic linker API. // We didn't just change frameworks/base to update the LD_LIBRARY_PATH once at the // beginning because multiple apks can run in the same process and third party code can // use its own BaseDexClassLoader. // We didn't just add a dlopen_with_custom_LD_LIBRARY_PATH call because we wanted any // dlopen(3) calls made from a .so's JNI_OnLoad to work too. // So, find out what the native library search path is for the ClassLoader in question... String librarySearchPath = null; if (loader != null && loader instanceof BaseDexClassLoader) { BaseDexClassLoader dexClassLoader = (BaseDexClassLoader) loader; librarySearchPath = dexClassLoader.getLdLibraryPath(); } // nativeLoad should be synchronized so there's only one LD_LIBRARY_PATH in use regardless // of how many ClassLoaders are in the system, but dalvik doesn't support synchronized // internal natives. synchronized (this) { return nativeLoad(name, loader, librarySearchPath); } }

获取so文件名的方式,就是从classLoader中获取,最终加载时通过本地方法nativeLoad实现

String filename = loader.findLibrary(libraryName)

其实现在BaseDexClassLoader

@Override public String findLibrary(String name) { return pathList.findLibrary(name); } 方案分析:

1. JNI 代码内置方案

插件化so库加载原理及实现

代码隔离方案比较适合新增的 Native 模块,一开始就奔着动态化、延迟加载的方向去。

2. 插件化方案

插件化so库加载原理及实现

单独把 so 文件单独打包进插件包,JNI 代码保留在宿主代码内部

由于 nativeLibraryDirectories 的具体实现是一个 ArrayList 实例,其元素读写操作自身是不保证线程安全的

急需解决的问题

1. 安全性问题

所有可执行代码在拷贝安装到安全路径(比如 Android 的 data/data 内部路径)之前,都有被劫持或者破坏的风险。

最好的做法是每次加载 so 库之前都对其做一次安全性校验

最简单的方式是记录 so 文件的 MD5 或者 CRC 等 Hash 信息(粒度可以是每个单独的 so 文件,或者一批 so 文件的压缩包)

如果本地下载目录中的 so 文件总数目,少于预定义在集合里 so 文件数目,说明不完整

2. 版本控制问题

通过版本控制流程,我们可以在服务端禁用这个版本的 so 插件,从而使客户端进入“so 插件不可用”的逻辑,而不至于执行有问题的代码。

3. abi 兼容性判断

检查 so 插件包里的 so 库 abi 信息是否与宿主目前运行时的 abi 一致。

直接指定你 so 下载的路径,通过反射获取 android.os.SystemProperties 私有方法 get ro.product.cpu.abi 可以动态获取 CPU 架构

/** * 获取设备的cpu架构类型 */ public static String getCpuArchType() { if (!TextUtils.isEmpty(cpuArchType)) { return cpuArchType; } try { Class> clazz = Class.forName("android.os.SystemProperties"); Method get = clazz.getDeclaredMethod("get", new Class[]{String.class}); cpuArchType = (String) get.invoke(clazz, new Object[]{"ro.product.cpu.abi"}); } catch (Exception e) { } try { if (TextUtils.isEmpty(cpuArchType)) { cpuArchType = Build.CPU_ABI;//获取不到,重新获取,可能不准确? } } catch (Exception e) { } if (TextUtils.isEmpty(cpuArchType)) { cpuArchType = "armeabi-v7a"; } cpuArchType = cpuArchType.toLowerCase(); return cpuArchType; }

4. System#load 加载代码侵入问题

通过 System#loadLibrary(“xxx” ) 加载 so 库, Android Framework 会遍历当前上下文的 ClassLoader 实例里的 nativeLibraryDirectories 数组,在数组里所有的文件路径下查找文件名为 libxxx.so 的文件,所以我们的解决思路就是在安装好 so 插件之后,将其所在的内部安全路径注入到这个 nativeLibraryDirectories 数组里,即可实现通过 System#loadLibrary 加载,代码如下:

第一步: 通过反射,注入 so 文件注入到 nativeLibraryDirectories 路径

private static final class V14 { private static void install(ClassLoader classLoader, File folder) throws Throwable { // 反射宿主 APK 的 ClassLoader 的 pathList成员变量 Field pathListField = MkReflectUtil.findField(classLoader, "pathList"); // 获取这个成员变量 在 宿主 APK 的 ClassLoader 对象的取值 Object dexPathList = pathListField.get(classLoader); // 将被加载的 被加载的 so 实例存储到 dexPathList MkReflectUtil.expandArray(dexPathList, "nativeLibraryDirectories", new File[]{folder}); } }

private static final class V23 { private static void install(ClassLoader classLoader, File folder) throws Throwable { Field pathListField = MkReflectUtil.findField(classLoader, "pathList"); Object dexPathList = pathListField.get(classLoader); Field nativeLibraryDirectories = MkReflectUtil.findField(dexPathList, "nativeLibraryDirectories"); List libDirs = (List) nativeLibraryDirectories.get(dexPathList); //去重 if (libDirs == null) { libDirs = new ArrayList(2); } final Iterator libDirIt = libDirs.iterator(); while (libDirIt.hasNext()) { final File libDir = libDirIt.next(); if (folder.equals(libDir)) { libDirIt.remove(); Log.d(TAG, "dq libDirIt.remove() " + folder.getAbsolutePath()); break; } } libDirs.add(0, folder); Field systemNativeLibraryDirectories = MkReflectUtil.findField(dexPathList, "systemNativeLibraryDirectories"); List systemLibDirs = (List) systemNativeLibraryDirectories.get(dexPathList); //判空 if (systemLibDirs == null) { systemLibDirs = new ArrayList(2); } //Log.d(TAG, "dq systemLibDirs,size=" + systemLibDirs.size()); // 获得Element[] 数组 Method makePathElements = MkReflectUtil.findMethod(dexPathList, "makePathElements", List.class, File.class, List.class); ArrayList suppressedExceptiOns= new ArrayList(); libDirs.addAll(systemLibDirs); // 输出调用对象,插件APK所在目录,插件APK的全路径,和用于存储IO异常的List,获得Element[] 返回 Object[] elements = (Object[]) makePathElements.invoke(dexPathList, libDirs, null, suppressedExceptions); Field nativeLibraryPathElements = MkReflectUtil.findField(dexPathList, "nativeLibraryPathElements"); nativeLibraryPathElements.setAccessible(true); nativeLibraryPathElements.set(dexPathList, elements); } }

private static final class V25 { private static void install(ClassLoader classLoader, File folder) throws Throwable { Field pathListField = MkReflectUtil.findField(classLoader, "pathList"); Object dexPathList = pathListField.get(classLoader); Field nativeLibraryDirectories = MkReflectUtil.findField(dexPathList, "nativeLibraryDirectories"); List libDirs = (List) nativeLibraryDirectories.get(dexPathList); //去重 if (libDirs == null) { libDirs = new ArrayList(2); } final Iterator libDirIt = libDirs.iterator(); while (libDirIt.hasNext()) { final File libDir = libDirIt.next(); if (folder.equals(libDir)) { libDirIt.remove(); Log.d(TAG, "dq libDirIt.remove()" + folder.getAbsolutePath()); break; } } libDirs.add(0, folder); //system/lib Field systemNativeLibraryDirectories = MkReflectUtil.findField(dexPathList, "systemNativeLibraryDirectories"); List systemLibDirs = (List) systemNativeLibraryDirectories.get(dexPathList); //判空 if (systemLibDirs == null) { systemLibDirs = new ArrayList(2); } //Log.d(TAG, "dq systemLibDirs,size=" + systemLibDirs.size()); Method makePathElements = MkReflectUtil.findMethod(dexPathList, "makePathElements", List.class); libDirs.addAll(systemLibDirs); Object[] elements = (Object[]) makePathElements.invoke(dexPathList, libDirs); Field nativeLibraryPathElements = MkReflectUtil.findField(dexPathList, "nativeLibraryPathElements"); nativeLibraryPathElements.setAccessible(true); nativeLibraryPathElements.set(dexPathList, elements); } }

注入 so 路径的逻辑如下:

  1. APK 的 ClassLoader 的 pathList 的成员变量,
  2. pathList 实际上是 SoPathList, 类的实例 的内部 成员变量 List 实例
  3. 这个 List 存储的是 被加载的 so 文件实例

/** * 1. 通过反射拿到dexElements的取值 * 2. 将 findField 方法获取到的 object[] 插入到数组的最前面。 * 3. 被插入的 object[] 数组就是外部修复包存储路径集合编译后形成的队列 * 即外部修复包的资源和 .class 队列 * @param instance 宿主 APK 的 ClassLoader实例的成员变量 pathList(DexPathList类似) * @param fieldName 需要被反射和替换的 DexPathList 类对象的成员变量 "dexElements", 用于存储 .dex 加载对象dex * @param extraElements 被加载的插件 apk 的 .dex实例列表 */ public static void expandFieldArray(Object instance, String fieldName, Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException { // 1 通过反射获取 classLoader 实例的成员变量 pathList(DexPathList类的实例)的成员变量dexElements Field jlrField = findField(instance, fieldName); // 2 获取当前dexElements 这个成员变量在classLoader 实例的成员变量 pathList(DexPathList类的实例)中的取值 Object[] original = (Object[]) jlrField.get(instance); // 3 新建一个数组,这个数组用来容纳 宿主 apk .dex 文件加载出来的elements[] 和 插件apk .dex 文件加载出来的 elements[] Object[] combined = (Object[]) Array.newInstance(original.getClass().getComponentType(), original.length + extraElements.length); // 4 先把插件 apk 中获取的elements[] 以及 dexFileArr复制到数组里面,方便我们动态加载 System.arraycopy(extraElements, 0, combined, 0, extraElements.length); // 5 再把apk所有的 dexElements 成员变量取值复制到数组里面 System.arraycopy(original, 0, combined, extraElements.length, original.length); // 6 覆盖 dexElements 成员变量取值 jlrField.set(instance, combined); } 参考

App极限瘦身: 动态下发so
插件化so库加载原理及实现


推荐阅读
  • 本文介绍了闭包的定义和运转机制,重点解释了闭包如何能够接触外部函数的作用域中的变量。通过词法作用域的查找规则,闭包可以访问外部函数的作用域。同时还提到了闭包的作用和影响。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • Go Cobra命令行工具入门教程
    本文介绍了Go语言实现的命令行工具Cobra的基本概念、安装方法和入门实践。Cobra被广泛应用于各种项目中,如Kubernetes、Hugo和Github CLI等。通过使用Cobra,我们可以快速创建命令行工具,适用于写测试脚本和各种服务的Admin CLI。文章还通过一个简单的demo演示了Cobra的使用方法。 ... [详细]
  • Android系统源码分析Zygote和SystemServer启动过程详解
    本文详细解析了Android系统源码中Zygote和SystemServer的启动过程。首先介绍了系统framework层启动的内容,帮助理解四大组件的启动和管理过程。接着介绍了AMS、PMS等系统服务的作用和调用方式。然后详细分析了Zygote的启动过程,解释了Zygote在Android启动过程中的决定作用。最后通过时序图展示了整个过程。 ... [详细]
  • Java如何导入和导出Excel文件的方法和步骤详解
    本文详细介绍了在SpringBoot中使用Java导入和导出Excel文件的方法和步骤,包括添加操作Excel的依赖、自定义注解等。文章还提供了示例代码,并将代码上传至GitHub供访问。 ... [详细]
  • Annotation的大材小用
    为什么80%的码农都做不了架构师?最近在开发一些通用的excel数据导入的功能,由于涉及到导入的模块很多,所以开发了一个比较通用的e ... [详细]
  • 基于分布式锁的防止重复请求解决方案
    一、前言关于重复请求,指的是我们服务端接收到很短的时间内的多个相同内容的重复请求。而这样的重复请求如果是幂等的(每次请求的结果都相同,如查 ... [详细]
  • 现在比较流行使用静态网站生成器来搭建网站,博客产品着陆页微信转发页面等。但每次都需要对服务器进行配置,也是一个重复但繁琐的工作。使用DockerWeb,只需5分钟就能搭建一个基于D ... [详细]
  • 初识java关于JDK、JRE、JVM 了解一下 ... [详细]
  • php7 curl_init(),php7.3curl_init获取301、302跳转后的数据
    最近在做一个蜘蛛项目,发现在抓取数据时,有时会碰到301的页面,原本写的curl_init函数php7-远程获取api接口或网页内容&#x ... [详细]
  • Android跨进程通信IPC之9——Binder通信机制
    移步系列Android跨进程通信IPC系列1Android整体架构Android系统架构及系统源码目录Android系统架构 ... [详细]
  • 本文介绍了如何使用C#制作Java+Mysql+Tomcat环境安装程序,实现一键式安装。通过将JDK、Mysql、Tomcat三者制作成一个安装包,解决了客户在安装软件时的复杂配置和繁琐问题,便于管理软件版本和系统集成。具体步骤包括配置JDK环境变量和安装Mysql服务,其中使用了MySQL Server 5.5社区版和my.ini文件。安装方法为通过命令行将目录转到mysql的bin目录下,执行mysqld --install MySQL5命令。 ... [详细]
  • 【技术分享】一个 ELF 蠕虫分析
    【技术分享】一个 ELF 蠕虫分析 ... [详细]
  • docker安装到基本使用
    记录docker概念,安装及入门日常使用Docker安装查看官方文档,在"Debian上安装Docker",其他平台在"这里查 ... [详细]
author-avatar
平凡无求
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有