资讯详情

一文带你彻底搞懂Android动态加载so

一个普通的android应用来说,so库的比例通常很高,因为我们在开发中不可避免地会遇到各种需要native所以so库的动态化可以大大降低包体积。 如今,so动态加载也是一项非常成熟的技术,但许多公司还没有参与这方面,或者不知道从哪里开始,因为so事实上,动态涉及下载,so在版本管理、动态加载实现等方面,我们不妨从最本质上抛开这些额外的东西so从动态加载开始!这是这个例子。我把它命名为sillyboy,欢迎pr后续点赞!

一、so动态加载介绍

动态加载实际上是我们的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(         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 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(); 

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("mergeDebugNatieLibs")
    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。

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 找不到的情况。

很多实现都采用了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头中的各项值来决定。

那么我们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 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,当然也希望各位点赞呀!当然,有更好的实现也欢迎评论!!

作者:不落魄Android开发 链接:https://www.jianshu.com/p/0d8b9e4ecee6 更多资讯请扫描下方二维码获取👇👇👇

标签: 400v25kvar电容电抗器

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台