看完这篇,帮你彻底搞懂Android动态加载so!

手机游戏开发者 2024-9-21 23:08:50 90 0 来自 中国
作者:Pika
对于一个平凡的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
一、so动态加载先容

动态加载,实在就是把我们的so库在打包成apk的时间剔除,在符合的时间通过网络包下载的方式,通过一些本事,在运行的时间举行分离加载的过程。这里涉及到下载器,另有下载后的版本管理等等确保一个so库被精确的加载等过程,在这里,我们不讨论这些辅助的流程,我们看下怎么实现一个最简单的加载流程。
1、从一个例子出发
我们构建一个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(        nativecpptwo        SHARED        test.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 JNICALLJava_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();2、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.name                        deleteSoName.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的生命周期。
2.png 通过对条件检索,我们就删撤除了我们想要的so,即ibnativecpptwo.so与libnativecpp.so。
3、动态加载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库的时间,就可以大概找到!
4、竣事了吗?
一样平常的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,由于我把关键的拷贝过来了,哈哈哈,好啦,我们看下怎么实现吧!不外在此这前,我们须要了解一些前置知识。
5、ELF文件
我们的so库,本质就是一个elf文件,那么so库也符合elf文件的格式,ELF文件由4部门构成,分别是ELF头(ELF header)、步伐头表(Program header table)、节(Section)和节头表(Section header table)。实际上,一个文件中不肯定包罗全部内容,而且它们的位置也未必犹如所示如许安排,只有ELF头的位置是固定的,别的各部门的位置、巨细等信息由ELF头中的各项值来决定。
4.png 那么我们so中,假如依靠于其他的so,那么这个信息存在那里呢!?没错,它实在也存在elf文件中,否则链接器怎么找嘛,它实在就存在.dynamic段中,以是我们只要找打dynamic段的偏移,就能到dynamic中,而被依靠的so的信息,实在就存在里面啦 我们可以用readelf(ndk中就有toolchains目次后) 检察,readelf -d nativecpptwo.so 这里的 -d 就是检察dynamic段的意思。
这里面涉及到动态加载so的知识,可以保举各人一本书,叫做步伐员的自我修养-链接装载与库这里就画个初略图 。
6.png 我们再看下本质,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 load        return 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 offset    final long stringTableOff = offsetFromVma(header, numProgramHeaderEntries, vStringTableOff);    for (final Long strOff : neededOffsets) {        dependencies.add(readString(buffer, stringTableOff + strOff));    }    return dependencies;}6、扩展
我们到这里,就可以大概办理so库的动态加载的干系题目了,那么另有人大概会问,项目中是会存在多处System.load方式的,假如加载的so还不存在怎么办?比如还在下载当中,实在很简单,这个时间我们字节码插桩就派上用场了,只要我们把System.load更换为我们自定义的加载so逻辑,举行肯定的逻辑处理惩罚就可以了,嘿嘿,由于笔者之前就有写一个字节码插桩的库的先容,以是在本次就不重复了,可以看Sipder,同时也可以用其他的字节码插桩框架实现,信托这不是一个题目。
https://github.com/TestPlanB/Spider
总结

看到这里的读者,信托也可以大概明白动态加载so的步调了,末了源代码可以在SillyBoy,固然也盼望各位点赞呀!固然,有更好的实现也欢迎品评!!
https://github.com/TestPlanB/SillyBoy
您需要登录后才可以回帖 登录 | 立即注册

Powered by CangBaoKu v1.0 小黑屋藏宝库It社区( 冀ICP备14008649号 )

GMT+8, 2024-11-24 15:49, Processed in 0.123556 second(s), 35 queries.© 2003-2025 cbk Team.

快速回复 返回顶部 返回列表