作者:Pika
转载地址:https://juejin.cn/post/7107958280097366030

1背景

对于一个普通的android应用来说,so库的占比通常都是巨高不下的,因为我们无可避免的在开发中遇到各种各样需要用到native的需求,所以so库的动态化可以减少极大的包体积,自从2020腾讯的bugly团队发布关于动态化so的相关文章后,已经过去两年了,相关文章,经过两年的考验,实际上so动态加载也是非常成熟的一项技术了,但是很遗憾,许多公司都还没有这方面的涉略又或者说不知道从哪里开始进行,因为so动态其实涉及到下载,so版本管理,动态加载实现等多方面,我们不妨抛开这些额外的东西,从最本质的so动态加载出发吧!这里是本次的例子,我把它命名为sillyboy,欢迎pr还有后续点赞呀!

https://cloud.tencent.com/developer/article/1592672

https://github.com/TestPlanB/SillyBoy

2so动态加载介绍

动态加载,其实就是把我们的so库在打包成apk的时候剔除,在合适的时候通过网络包下载的方式,通过一些手段,在运行的时候进行分离加载的过程。这里涉及到下载器,还有下载后的版本管理等等确保一个so库被正确的加载等过程,在这里,我们不讨论这些辅助的流程,我们看下怎么实现一个最简单的加载流程。

从一个例子出发

我们构建一个native工程,然后在里面编入如下内容,下面是cmake。

# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html# Sets the minimum version of CMake required to build the native library.cmake_minimum_required(VERSION 3.18.1)# Declares and names the project.project("nativecpp")# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.add_library( # Sets the name of the library.nativecpp# Sets the library as a shared library.SHARED# Provides a relative path to your source file(s).native-lib.cpp)add_library(nativecpptwoSHAREDtest.cpp)# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.find_library( # Sets the name of the path variable.log-lib# Specifies the name of the NDK library that# you want CMake to locate.log)# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.target_link_libraries( # Specifies the target library.nativecpp# Links the target library to the log library# included in the NDK.${log-lib})target_link_libraries( # Specifies the target library.nativecpptwo# Links the target library to the log library# included in the NDK.nativecpp${log-lib})

可以看到,我们生成了两个so库一个是nativecpp,还有一个是nativecpptwo(为什么要两个呢?我们可以继续看下文) 这里也给出最关键的test.cpp代码。

#include <jni.h>
#include <string>
#include<android/log.h>extern "C"
JNIEXPORT void JNICALL
Java_com_example_nativecpp_MainActivity_clickTest(JNIEnv *env, jobject thiz) {// 在这里打印一句话__android_log_print(ANDROID_LOG_INFO,"hello"," native 层方法");}

很简单,就一个native方法,打印一个log即可,我们就可以在java/kotin层进行方法调用了,即:

public native void clickTest();

so库检索与删除

要实现so的动态加载,那最起码是要知道本项目过程中涉及到哪些so吧!不用担心,我们gradle构建的时候,就已经提供了相应的构建过程,即构建的task【 mergeDebugNativeLibs】,在这个过程中,会把一个project里面的所有native库进行一个收集的过程,紧接着task【stripDebugDebugSymbols】是一个符号表清除过程,如果了解native开发的朋友很容易就知道,这就是一个减少so体积的一个过程,我们不在这里详述。所以我们很容易想到,我们只要在这两个task中插入一个自定义的task,用于遍历和删除就可以实现so的删除化了,所以就很容易写出这样的代码。

ext {deleteSoName = ["libnativecpptwo.so","libnativecpp.so"]
}
// 这个是初始化 -配置 -执行阶段中,配置阶段执行的任务之一,完成afterEvaluate就可以得到所有的tasks,从而可以在里面插入我们定制化的数据
task(dynamicSo) {
}.doLast {println("dynamicSo insert!!!! ")//projectDir 在哪个project下面,projectDir就是哪个路径print(getRootProject().findAll())def file = new File("${projectDir}/build/intermediates/merged_native_libs/debug/out/lib")//默认删除所有的so库if (file.exists()) {file.listFiles().each {if (it.isDirectory()) {it.listFiles().each {target ->print("file ${target.name}")def compareName = target.namedeleteSoName.each {if (compareName.contains(it)) {target.delete()}}}}}} else {print("nil")}
}
afterEvaluate {print("dynamicSo task start")def customer = tasks.findByName("dynamicSo")def merge = tasks.findByName("mergeDebugNativeLibs")def strip = tasks.findByName("stripDebugDebugSymbols")if (merge != null || strip != null) {customer.mustRunAfter(merge)strip.dependsOn(customer)}}

可以看到,我们定义了一个自定义task dynamicSo,它的执行是在afterEvaluate中定义的,并且依赖于mergeDebugNativeLibs,而stripDebugDebugSymbols就依赖于我们生成的dynamicSo,达到了一个插入操作。那么为什么要在afterEvaluate中执行呢?那是因为android插件是在配置阶段中才生成的mergeDebugNativeLibs等任务,原本的gradle构建是不存在这样一个任务的,所以我们才需要在配置完所有task之后,才进行的插入,我们可以看一下gradle的生命周期。

通过对条件检索,我们就删除掉了我们想要的so,即ibnativecpptwo.so与libnativecpp.so。

动态加载so

根据上文检索出来的两个so,我们就可以在项目中上传到自己的后端中,然后通过网络下载到用户的手机上,这里我们就演示一下即可,我们就直接放在data目录下面吧。

真实的项目过程中,应该要有校验操作,比如md5校验或者可以解压等等操作,这里不是重点,我们就直接略过啦!

那么,怎么把一个so库加载到我们本来的apk中呢?这里是so原本的加载过程,可以看到,系统是通过classloader检索native目录是否存在so库进行加载的,那我们反射一下,把我们自定义的path加入进行不就可以了吗?这里采用tinker一样的思路,在我们的classloader中加入so的检索路径即可,比如:

https://cs.android.com/android/platform/superproject/+/master:libcore/ojluni/src/main/java/java/lang/Runtime.java;l=1?q=Runtime.java

private static final class V25 {private static void install(ClassLoader classLoader, File folder)  throws Throwable {final Field pathListField = ShareReflectUtil.findField(classLoader, "pathList");final Object dexPathList = pathListField.get(classLoader);final Field nativeLibraryDirectories = ShareReflectUtil.findField(dexPathList, "nativeLibraryDirectories");List<File> origLibDirs = (List<File>) nativeLibraryDirectories.get(dexPathList);if (origLibDirs == null) {origLibDirs = new ArrayList<>(2);}final Iterator<File> libDirIt = origLibDirs.iterator();while (libDirIt.hasNext()) {final File libDir = libDirIt.next();if (folder.equals(libDir)) {libDirIt.remove();break;}}origLibDirs.add(0, folder);final Field systemNativeLibraryDirectories = ShareReflectUtil.findField(dexPathList, "systemNativeLibraryDirectories");List<File> origSystemLibDirs = (List<File>) systemNativeLibraryDirectories.get(dexPathList);if (origSystemLibDirs == null) {origSystemLibDirs = new ArrayList<>(2);}final List<File> newLibDirs = new ArrayList<>(origLibDirs.size() + origSystemLibDirs.size() + 1);newLibDirs.addAll(origLibDirs);newLibDirs.addAll(origSystemLibDirs);final Method makeElements = ShareReflectUtil.findMethod(dexPathList, "makePathElements", List.class);final Object[] elements = (Object[]) makeElements.invoke(dexPathList, newLibDirs);final Field nativeLibraryPathElements = ShareReflectUtil.findField(dexPathList, "nativeLibraryPathElements");nativeLibraryPathElements.set(dexPathList, elements);}
}

我们在原本的检索路径中,在最前面,即数组为0的位置加入了我们的检索路径,这样一来claaloader在查找我们已经动态化的so库的时候,就能够找到!

结束了吗?

一般的so库,比如不依赖其他的so的时候,直接这样加载就没问题了,但是如果存在着依赖的so库的话,就不行了!相信大家在看其他的博客的时候就能看到,是因为Namespace的问题。具体是我们动态库加载的过程中,如果需要依赖其他的动态库,那么就需要一个链接的过程对吧!这里的实现就是Linker,Linker 里检索的路径在创建 ClassLoader 实例后就被系统通过 Namespace 机制绑定了,当我们注入新的路径之后,虽然 ClassLoader 里的路径增加了,但是 Linker 里 Namespace 已经绑定的路径集合并没有同步更新,所以出现了 libxxx.so 文件(当前的so)能找到,而依赖的so 找不到的情况。bugly文章。

https://cloud.tencent.com/developer/article/1592672?from=article.detail.1751968

很多实现都采用了Tinker的实现,既然我们系统的classloader是这样,那么我们在合适的时候把这个替换掉不就可以了嘛!当然bugly团队就是这样做的,但是笔者认为,替换一个classloader显然对于一个普通应用来说,成本还是太大了,而且兼容性风险也挺高的,当然,还有很多方式,比如采用Relinker这个库自定义我们加载的逻辑。

为了不冷饭热炒,嘿嘿,虽然我也喜欢吃炒饭(手动狗头),这里我们就不采用替换classloader的方式,而是采用跟relinker的思想,去进行加载!具体的可以看到sillyboy的实现,其实就不依赖relinker跟tinker,因为我把关键的拷贝过来了,哈哈哈,好啦,我们看下怎么实现吧!不过在此这前,我们需要了解一些前置知识。

ELF文件

我们的so库,本质就是一个elf文件,那么so库也符合elf文件的格式,ELF文件由4部分组成,分别是ELF头(ELF header)、程序头表(Program header table)、节(Section)和节头表(Section header table)。实际上,一个文件中不一定包含全部内容,而且它们的位置也未必如同所示这样安排,只有ELF头的位置是固定的,其余各部分的位置、大小等信息由ELF头中的各项值来决定。

那么我们so中,如果依赖于其他的so,那么这个信息存在哪里呢!?没错,它其实也存在elf文件中,不然链接器怎么找嘛,它其实就存在.dynamic段中,所以我们只要找打dynamic段的偏移,就能到dynamic中,而被依赖的so的信息,其实就存在里面啦 我们可以用readelf(ndk中就有toolchains目录后) 查看,readelf -d nativecpptwo.so 这里的 -d 就是查看dynamic段的意思。

这里面涉及到动态加载so的知识,可以推荐大家一本书,叫做程序员的自我修养-链接装载与库这里就画个初略图 。

我们再看下本质,dynamic结构体如下,定义在elf.h中。

typedef struct{
Elf32_Sword d_tag;
union{
Elf32_Addr d_ptr;
....
}
}

当d_tag的数值为DT_NEEDED的时候,就代表着依赖的共享对象文件,d_ptr表示所依赖的共享对象的文件名。看到这里读者们已经知道了如果我们知道了文件名,不就可以再用System.load去加载这个不就可以了嘛!不用替换classloader就能够保证被依赖的库先加载!我们可以再总结一下这个方案的原理,如图:

比如我们要加载so3,我们就需要先加载so2,如果so2存在依赖,那我们就先加载so1,这个时候so1就不存在依赖项了,就不需要再调用Linker去查找其他so库了。我们最终方案就是,只要能够解析对应的elf文件,然后找偏移,找到需要的目标项(DT_NEED)就可以了。

public List<String> parseNeededDependencies() throws IOException {channel.position(0);final List<String> dependencies = new ArrayList<String>();final Header header = parseHeader();final ByteBuffer buffer = ByteBuffer.allocate(8);buffer.order(header.bigEndian ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN);long numProgramHeaderEntries = header.phnum;if (numProgramHeaderEntries == 0xFFFF) {/*** Extended Numbering** If the real number of program header table entries is larger than* or equal to PN_XNUM(0xffff), it is set to sh_info field of the* section header at index 0, and PN_XNUM is set to e_phnum* field. Otherwise, the section header at index 0 is zero* initialized, if it exists.**/final SectionHeader sectionHeader = header.getSectionHeader(0);numProgramHeaderEntries = sectionHeader.info;}long dynamicSectionOff = 0;for (long i = 0; i < numProgramHeaderEntries; ++i) {final ProgramHeader programHeader = header.getProgramHeader(i);if (programHeader.type == ProgramHeader.PT_DYNAMIC) {dynamicSectionOff = programHeader.offset;break;}}if (dynamicSectionOff == 0) {// No dynamic linking info, nothing to loadreturn Collections.unmodifiableList(dependencies);}int i = 0;final List<Long> neededOffsets = new ArrayList<Long>();long vStringTableOff = 0;DynamicStructure dynStructure;do {dynStructure = header.getDynamicStructure(dynamicSectionOff, i);if (dynStructure.tag == DynamicStructure.DT_NEEDED) {neededOffsets.add(dynStructure.val);} else if (dynStructure.tag == DynamicStructure.DT_STRTAB) {vStringTableOff = dynStructure.val; // d_ptr union}++i;} while (dynStructure.tag != DynamicStructure.DT_NULL);if (vStringTableOff == 0) {throw new IllegalStateException("String table offset not found!");}// Map to file offsetfinal long stringTableOff = offsetFromVma(header, numProgramHeaderEntries, vStringTableOff);for (final Long strOff : neededOffsets) {dependencies.add(readString(buffer, stringTableOff + strOff));}return dependencies;
}

扩展

我们到这里,就能够解决so库的动态加载的相关问题了,那么还有人可能会问,项目中是会存在多处System.load方式的,如果加载的so还不存在怎么办?比如还在下载当中,其实很简单,这个时候我们字节码插桩就派上用场了,只要我们把System.load替换为我们自定义的加载so逻辑,进行一定的逻辑处理就可以了,嘿嘿,因为笔者之前就有写一个字节码插桩的库的介绍,所以在本次就不重复了,可以看Sipder,同时也可以用其他的字节码插桩框架实现,相信这不是一个问题。

https://github.com/TestPlanB/Spider

3总结

看到这里的读者,相信也能够明白动态加载so的步骤了,最后源代码可以在SillyBoy,当然也希望各位点赞呀!当然,有更好的实现也欢迎评论!!

​Android动态加载so~相关推荐

  1. android 动态 dex,Android 动态加载dex

    首先如果仅仅是因为64K method的问题可以直接看这里DexGuard.Proguard.Multi-dex给出的解决方案. 本文主要讨论从编译层面,dex动态加载器选择层面以及安全层面讨论dex ...

  2. Android动态加载技术初探

    一.前言: 现在,已经有实力强大的公司用这个技术开发应用了,比如淘宝,大众点评,百度地图等,之所以采用这个技术,实际上,就是方便更新功能,当然,前提是新旧功能的接口一致,不然会报Not Found等错 ...

  3. Android动态加载黑科技 动态创建Activity模式

    基本信息 Author:kaedea GitHub:android-dynamical-loading 代理Activity模式的限制 还记得我们在代理Activity模式里谈到启动插件APK里的Ac ...

  4. Android动态加载进阶 代理Activity模式

    基本信息 作者:kaedea 项目:android-dynamical-loading 技术背景 简单模式中,使用ClassLoader加载外部的Dex或Apk文件,可以加载一些本地APP不存在的类, ...

  5. Android动态加载技术

    基本信息 Author:kaedea GitHub:android-dynamical-loading 我们很早开始就在Android项目中采用了动态加载技术,主要目的是为了达到让用户不用重新安装AP ...

  6. Android应用开发提高系列(4)——Android动态加载(上)——加载未安装APK中的类...

    前言 近期做换肤功能,由于换肤程度较高,受限于平台本身,实现起来较复杂,暂时搁置了该功能,但也积累了一些经验,将分两篇文章来写这部分的内容,欢迎交流! 关键字:Android动态加载 声明 欢迎转载, ...

  7. android jar 加入图片,Android动态加载外部jar包及jar包中图片等资源文件

    Android动态加载外部jar包及jar包中图片等资源文件 Android应用程序由Java开发,因此Java中许多实用的特性,在Android中也有体现.动态加载Class,也就是外部jar包,在 ...

  8. [转]Android动态加载jar/dex

    本文转自:http://www.cnblogs.com/over140/archive/2011/11/23/2259367.html 前言 在目前的软硬件环境下,Native App与Web App ...

  9. android 动态生成fragment,Android动态加载fragment(fragment复用)

    [实例简介] Android动态加载fragment(fragment复用) [实例截图] [核心代码] fm_reuse └── fm_reuse ├── AndroidManifest.xml ├ ...

  10. [转]Android应用开发提高系列(4)——Android动态加载(上)——加载未安装APK中的类...

    本文转自:http://www.cnblogs.com/over140/archive/2012/03/29/2423116.html 前言 近期做换肤功能,由于换肤程度较高,受限于平台本身,实现起来 ...

最新文章

  1. Codeforces.487C.Prefix Product Sequence(构造)
  2. Form_Form与OAF页面互相调用(案例)
  3. linux tar 提示 time stamp xxx in the future 解决方法
  4. 汇编语言随笔(11)- int 指令(返回到应用程序的中断例程),实验13(编写、应用中断例程)
  5. Linux学习之系统编程篇:回收子线程资源
  6. Fiori应用里如何给客户主数据维护图片
  7. 【Java】JShell工具上手即用
  8. 【NGS接龙】薛宇:漫谈生物信息圈儿的那些年、那些事!
  9. 四川专科学校排名四川计算机,四川省现代计算机职业学院2020年排名
  10. 三分钟看Netty(3) select poll VS epoll
  11. python自动视频摘要_专栏丨深度学习之视频摘要简述
  12. airflow+k8s 多用户-分布式-跨集群-容器化调度
  13. [BZOJ3124]直径
  14. oracle impdp 39002,expdp/impdp ORA-39002ORA-39166 | 学步园
  15. 实验假设与分析方法(参数与非参检验)
  16. 论文阅读 A SIMPLE BUT TOUGH-TO-BEAT BASELINE FOR SEN- TENCE EMBEDDINGS
  17. 通过企业微信SCRM营销系统,完成百万私域用户引流
  18. 7-16 新浪微博热门话题 (30分)
  19. 直播网站并发测试软件,HTTP/HLS/RTMP超级负载测试工具
  20. get_post X老师告诉小宁同学HTTP通常使用两种请求方法,你知道是哪两种吗?

热门文章

  1. LC5454.统计全1子矩阵(矩阵统计)
  2. kali破解Wi-Fi密码
  3. Java日期有效性验证
  4. js中国标准时间转换为yyyy-MM-dd
  5. 学习基金(2)避坑-如何选择基金
  6. Python笔记6 面向对象
  7. oracle ORA-28000: the account is locked 28000. 00000
  8. gradle 跳过单元测试的3种方法
  9. Java之环境变量配置
  10. Spring security的基础用法(普通的用户认证和授权)