持续创造,加快生长!这是我参与「日新计划 6 月更文挑战」的第1天,点击检查活动详情

布景

对于一个普通的android使用来说,so库的占比一般都是巨高不下的,由于咱们无可避免的在开发中遇到各种各样需求用到native的需求,数组去重所以so库的动态数组和链表的区别化能够削减极大的包体积,自从2020腾讯androidstudio新建项目的bugly团队发部关于动态化so的相关文章后,现已过去两年了,相关文章,经过两年的考验,实际上so动态加载也是十分老练的一项技术了,可androidstudio配置环境是很遗憾数组和链表的区别androidstudio怎么设置中文许多公司都android/harmonyos还没有这方面的涉略又或者说不知道从哪里开始进行,由于so动态其实涉及到下载,so版https和http的区别别办理,动态加载完结等多方面,咱们不妨抛开这些额定的东西,从最实质的so动态加载动身吧!这儿是本次的比方,我把它命名为sillyboy,欢迎pr还有后续点赞呀!

so动态加载介绍

动态加数组去重载,其实便是把咱们的so库在打包成apk的时分剔除,在合适的时分经过网络包下载的方法数组排序,经过一些手法,在运androidstudio线性布局转的时分进行分离数组加载的进程。这儿涉及到下载器,还有下载后的数组初始化版别办理等等确保一个so库被正确的加载androidstudio安装教程等进程,在这儿,咱们不讨论这数组和链表的区别些辅助的流程,咱初始化们看下怎样完结一个最简单的加载流程。

Android动态加载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库一初始化sdk什么意思个是nativecpp,还有一个是nativecpptwo(为什么数组公式要两个呢?咱们能够androidstudio安装教程持续看下文) 这儿也给出最要害的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 层办法");
}

很简单,就一个nahttp 302tive办法,打印一个log即可,咱们就能够在java/kotin层进行办法调用了,即

public native void clickTest();

so库检索与删去

要完结so的动态加载,那最起码是要知道本项目进程中涉及到哪些so吧!不必忧androidstudio安装教程虑,咱们gradle构建的时分,就现已提供了HTTP相应的构建进程,即构建的task【 mergeDebugNativeLibs】,在这个进程中,会把一个project里边的一切native库进行一个收集的进程,紧接着task【strip初始化失败是怎么解决DebugDebu初始化是什么意思gSymbols】是一个符号表清除进程,假如了解native开发的朋友很简单就知道,这便是一个削减so体积的一个进程,咱们不在这儿详述。所以咱们很简单想到,咱们只需在这两个task中刺进一个自界说的task,用于遍历和删去就能够完结so的删去化了,所以就很简单写出这样的代httpwatch


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

http代理够看到,咱们界说了一个自界说task dynamicSo,它的履行是在afterEvaluate中界androidstudio简单程序说的,并且依靠于mer数组geDebugNa数组的定义tiveLibs,而stripDebugDebugSymbhttpwatchols就依靠于咱们生成的dynamicSo,达到了一个刺进操作android什么意思。那么为什数组么要在afhttp://192.168.1.1登录terEvaluate中履行呢?那是由于android插件是在装备阶段中才生成的mergeDebugNativeLibshttp://www.baidu.com等使命,本来的gradle构建是不存在这样一个使命的,所以咱们才需求在装备完一切task之后,才进行数组公式的刺进,咱们能够看一下gradle的生命周期

Android动态加载so!这一篇就够了!

经过对条件检索,咱们就删去掉了咱们想要的so,即ibnativecpptwo.so与libnativecandroid什么意思pp.so。

动态加初始化磁盘载so

根据上文检索出来的两个so,咱们就能够在项目中上传到自己的后端中,然后经过网络下android/harmonyos载到用户的手机上,这儿咱们就演示一下即可,咱们就直接放在data目录下面吧

Android动态加载so!这一篇就够了!
实在的项目进程中,应该要有校验操作,比方md5校验或者能够解压等等操作,这儿不是要点,咱们就直接略过啦!

那么,怎样把一个so库加载到咱们本来的apk中数组和链表的区别呢?这儿是so本来的加载进程,能够看到,体系是经过clashttp://192.168.1.1登录sloader检androidstudio安装教程索native目录是否存在so库进行初始化失败是怎么解决加载的,那咱们反射一下,把咱们自界说的path参加进行不就能够初始化英文了吗?这儿选用tin数组去重方法ker相同的思路,在咱们的classloader中参加so的检索途径即可,比方

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的方位参加了咱们的检索途径,这样一来classloader在查找咱们android是什么系统http 302已动态化的so库的时分,就能够找到!

完毕了吗?

一般的so库,比方不依靠其他的so的时分,直接这样加载就没问题了,可是HTTP假如存在着依靠的so库的androidstudio打包apk话,就不行了!信任大家在看其他的博客的时分就能看到,是由于Namespace的问题。详细数组初始化是咱们动态库加载的进程中,假如需数组去重求依靠其他的动态库,那么就需求一个链接的进程对吧!这儿的完结便是Linkerhttp://192.168.1.1登录,Linker 里检索的途径在创建 ClassLoader 实例后就被体系经过 Namespace 机制绑定了,当咱们注入新的途径android平板电脑价格之后,尽管http://192.168.1.1登录 ClassLoader 里的途径增HTTP加了,可是 Linker 里 Namespace 现已绑定的途径集合并没有同步更新,所以呈现了 libxxx.so 文件(当前的so)能找到,而依靠的so 找不到的情况。bugly文章

很多完结都选用了Tinker的完结,既然咱们体系的classloader是这样,那么咱们在合适的时分把这个替换掉不就能够了嘛!当然bugly团队便是这样做的,可是笔者认初始化电脑为,替换一个classloader显然对于一个普通使http协议用来说,成本仍是太大了,而且兼容性危险也挺高的,当然,还有很多方法,比方选用Relinker这个库自界说咱们加载的逻辑。

为了不冷饭热炒,嘿嘿,尽管我也喜欢吃炒饭(手动狗头),这儿咱们就不选用替换classloader的方法,而是选用跟relinker的思维,去进行加载!详数组指针细的能够看到sillyboy的完结,其实就不依靠relinker跟tinker,由于我androidstudio新建项目把要害的拷贝过来了,哈哈哈,好啦,咱们看下怎样完结http://192.168.1.1登录吧!不过在此这前,咱们需求了解一些前置知识

ELF文件

咱们的so库,实质便是一个elf文件,那么so库也符合elf文件的格局,ELF文件由4部分组成,分别是ELF头(ELF header)、程序头表(Proandroidstudio模拟器运行不出来gram header table)、节(Section)和节头表(Section headerhttp协议 tandroidstudio新建项目able)。实际上,一个文件中不必定包括全数组部内容,而且它们的方位也未必如同所示这样组织,只需ELF头的方位是固定的,其他各部分的方位、巨细等android的drawable类信息由ELF头中的各项值来决议。

Android动态加载so!这一篇就够了!

那么咱们so中,假如http://192.168.1.1登录依靠于其他的so,那么这个信android下载息存在哪里呢!?没错,它其实也存在elf文件中,否则链接器怎样找嘛,它其实就存在.dynamic段中,所以咱们只需找打dynamic段的偏移,就能到dynamic中,而被依靠的so的信息,其实就存在里边啦 咱们能够用readelf(ndk中就有toolchains目录后) 检查,readelf -d nativecpptwo.so 这儿的 -d 便是检查dyn初始化电脑的后果amic段的意思

Android动态加载so!这一篇就够了!
这儿android是什么系统边涉及到动态加载so的知识,能够引荐大家一本书,叫做程序员的自我涵养-链接装载与库这儿就画个初略图
Android动态加载so!这一篇就够了!
咱们再看下实质,dynamic结构体如下,界说在elf.h中

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

当d_tag的数值为DT_NEEDED的时分,就代表着依靠的共享对象文件,d_ptr表明所依靠的共享对象的文件名。看到这儿读者们现已知道了,假如咱们知道了文件名,不就能够再用Syandroid下载安装stem.loadLibrary去加载这个文件名确认的so了嘛!不必替换classloadandroid手机er就能够确保被依靠的库数组公式先加载!咱们能够再总结一下这个计划的原理,如图

Android动态加载so!这一篇就够了!
比方咱们要加载so3,咱们就需求先加载so2http 500,假如so2存在依靠,那咱们就初始化电脑的后果调用System.loahttp 404dLibrary先加载so1,这个时分so1就不存在依靠androidstudio快捷键项了,就不需求再调用Linker去查找其他so库了。咱们最终计划便是,只需能够解析对应的elf文件,然后找偏移,找到需求的目标项(DT_NEED)所对应的数值(即被依靠的so文件名)就能够了

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

扩展

咱们到这儿,就能够处理so库的动态加载的相关问题了,那么还有人可数组c语言能会问,项目中是会存在多处System.load方法的,假如加载的so还不存在怎样办?比方还在下载傍边,其实很简单,这个时分咱们字节码插桩就派上用场了,只需咱们把System.lohttpclientad替换为咱们自界说的加载so逻辑,进行必定的逻辑处理就能够数组指针了,嘿嘿,由于笔者之前就有写一个字节码插桩的库的介绍,所以在androidstudio安装教程本次就不重复了,能够看Sipder,一起也能够用其他的字节码android下载安装插桩结构完结,信任这不是一个问题。

总结

看到这儿的读者,信任也能够理解动态加载so的过程了,最终源代码能够android/harmonyos在SillyBoy,当然也希望各位点赞呀!当然,有更好的完结也欢迎评论!初始化游戏启动器失败