背景

对于一个普通的android应用来说,so库的占比通常都是巨高不下的,因为我们无可避免的在开发中遇到各种各样需要用到native的需求,所以so库的动态化可以减少极大的包体积,自从2020腾讯的bugly团队发部关于动态化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_libHrary( # 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.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的生命周期

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

动态加载so

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

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

那么,怎么把一个so库加载到我们本来的apk中呢?这里是so原本的加载过程,可以看到,系统是通过classloader检索native目录是否存在so库进行加载的,那我们反射一下,把我们自定义的path加入进行不就可以了吗?这里采用tinker一样的思路,在我们的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在查找我们已经动态化的so库的时候,就能够找到!

结束了吗?

一般的so库,比如不依赖其他的so的时候,直接这样加载就没问题了,但是如果存在着依赖的so库的话,就不行了!相信大家在看其他的博客的时候就能看到,是因为Namespace的问题。具体是我们动态库加载的过程中,如果需要依赖其他的动态库,那么就需要一个链接的过程对吧!

这里的实现就是Linker,Linker 里检索的路径在创建 ClassLoader 实例后就被系统通过 Namespace 机制绑定了,当我们注入新的路径之后,虽然 ClassLoader 里的路径增加了,但是 Linker 里 Namespace 已经绑定的路径集合并没有同步更新,所以出现了 libxxx.so 文件(当前的so)能找到,而依赖的so 找不到的情况。bugly文章

很多实现都采用了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.loadLibrary去加载这个文件名确定的so了嘛!不用替换classloader就能够保证被依赖的库先加载!我们可以再总结一下这个方案的原理,如图

比如我们要加载so3,我们就需要先加载so2,如果so2存在依赖,那我们就调用System.loadLibrary先加载so1,这个时候so1就不存在依赖项了,就不需要再调用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库的动态加载的相关问题了,那么还有人可能会问,项目中是会存在多处System.load方式的,如果加载的so还不存在怎么办?

比如还在下载当中,其实很简单,这个时候我们字节码插桩就派上用场了,只要我们把System.load替换为我们自定义的加载so逻辑,进行一定的逻辑处理就可以了,嘿嘿,因为笔者之前就有写一个字节码插桩的库的介绍,所以在本次就不重复了,可以看 https://github.com/TestPlanB/Spider ,同时也可以用其他的字节码插桩框架实现,相信这不是一个问题。

总结

看到这里的读者,相信也能够明白动态加载so的步骤了

源代码 https://github.com/TestPlanB/SillyBoy

以上就是Android 动态加载 so实现示例详解的详细内容,更多关于Android 动态加载 so的资料请关注Devmax其它相关文章!

Android 动态加载 so实现示例详解的更多相关文章

  1. html5 canvas合成海报所遇问题及解决方案总结

    这篇文章主要介绍了html5 canvas合成海报所遇问题及解决方案总结,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  2. Html5 video标签视频的最佳实践

    这篇文章主要介绍了Html5 video标签视频的最佳实践,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

  3. HTML5在微信内置浏览器下右上角菜单的调整字体导致页面显示错乱的问题

    HTML5在微信内置浏览器下,在右上角菜单的调整字体导致页面显示错乱的问题,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧

  4. ios – containerURLForSecurityApplicationGroupIdentifier:在iPhone和Watch模拟器上给出不同的结果

    我使用默认的XCode模板创建了一个WatchKit应用程序.我向iOSTarget,WatchkitAppTarget和WatchkitAppExtensionTarget添加了应用程序组权利.(这是应用程序组名称:group.com.lombax.fiveminutes)然后,我尝试使用iOSApp和WatchKitExtension访问共享文件夹URL:延期:iOS应用:但是,测试NSURL

  5. Ionic – Splash Screen适用于iOS,但不适用于Android

    我有一个离子应用程序,其中使用CLI命令离子资源生成的启动画面和图标iOS版本与正在渲染的启动画面完美配合,但在Android版本中,只有在加载应用程序时才会显示白屏.我检查了config.xml文件,所有路径看起来都是正确的,生成的图像出现在相应的文件夹中.(我使用了splash.psd模板来生成它们.我错过了什么?这是config.xml文件供参考,我觉得我在这里做错了–解决方法在config.xml中添加以下键:它对我有用!

  6. ios – 无法启动iPhone模拟器

    /Library/Developer/CoreSimulator/Devices/530A44CB-5978-4926-9E91-E9DBD5BFB105/data/Containers/Bundle/Application/07612A5C-659D-4C04-ACD3-D211D2830E17/ProductName.app/ProductName然后,如果您在Xcode构建设置中选择标准体系结构并再次构建和运行,则会产生以下结果:dyld:lazysymbolbindingFailed:Symbol

  7. Xamarin iOS图像在Grid内部重叠

    heyo,所以在Xamarin我有一个使用并在其中包含一对,所有这些都包含在内.这在Xamarin.Android中看起来完全没问题,但是在Xamarin.iOS中,图像与标签重叠.我不确定它的区别是什么–为什么它在Xamarin.Android中看起来不错但在iOS中它的全部都不稳定?

  8. 在iOS上向后播放HTML5视频

    我试图在iPad上反向播放HTML5视频.HTML5元素包括一个名为playbackRate的属性,它允许以更快或更慢的速率或相反的方式播放视频.根据Apple’sdocumentation,iOS不支持此属性.通过每秒多次设置currentTime属性,可以反复播放,而无需使用playbackRate.这种方法适用于桌面Safari,但似乎在iOS设备上的搜索限制为每秒1次更新–在我的情况下太慢了.有没有办法在iOS设备上向后播放HTML5视频?解决方法iOS6Safari现在支持playbackRat

  9. 使用 Swift 语言编写 Android 应用入门

    Swift标准库可以编译安卓armv7的内核,这使得可以在安卓移动设备上执行Swift语句代码。做梦,虽然Swift编译器可以胜任在安卓设备上编译Swift代码并运行。这需要的不仅仅是用Swift标准库编写一个APP,更多的是你需要一些框架来搭建你的应用用户界面,以上这些Swift标准库不能提供。简单来说,构建在安卓设备上使用的Swiftstdlib需要libiconv和libicu。通过命令行执行以下命令:gitclonegit@github.com:SwiftAndroid/libiconv-libi

  10. Android – 调用GONE然后VISIBLE使视图显示在错误的位置

    我有两个视图,A和B,视图A在视图B上方.当我以编程方式将视图A设置为GONE时,它将消失,并且它正下方的视图将转到视图A的位置.但是,当我再次将相同的视图设置为VISIBLE时,它会在视图B上显示.我不希望这样.我希望视图B回到原来的位置,这是我认为会发生的事情.我怎样才能做到这一点?编辑–代码}这里是XML:解决方法您可以尝试将两个视图放在RelativeLayout中并相对于彼此设置它们的位置.

随机推荐

  1. Flutter 网络请求框架封装详解

    这篇文章主要介绍了Flutter 网络请求框架封装详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  2. Android单选按钮RadioButton的使用详解

    今天小编就为大家分享一篇关于Android单选按钮RadioButton的使用详解,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧

  3. 解决android studio 打包发现generate signed apk 消失不见问题

    这篇文章主要介绍了解决android studio 打包发现generate signed apk 消失不见问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧

  4. Android 实现自定义圆形listview功能的实例代码

    这篇文章主要介绍了Android 实现自定义圆形listview功能的实例代码,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  5. 详解Android studio 动态fragment的用法

    这篇文章主要介绍了Android studio 动态fragment的用法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  6. Android用RecyclerView实现图标拖拽排序以及增删管理

    这篇文章主要介绍了Android用RecyclerView实现图标拖拽排序以及增删管理的方法,帮助大家更好的理解和学习使用Android,感兴趣的朋友可以了解下

  7. Android notifyDataSetChanged() 动态更新ListView案例详解

    这篇文章主要介绍了Android notifyDataSetChanged() 动态更新ListView案例详解,本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下

  8. Android自定义View实现弹幕效果

    这篇文章主要为大家详细介绍了Android自定义View实现弹幕效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  9. Android自定义View实现跟随手指移动

    这篇文章主要为大家详细介绍了Android自定义View实现跟随手指移动,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. Android实现多点触摸操作

    这篇文章主要介绍了Android实现多点触摸操作,实现图片的放大、缩小和旋转等处理,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

返回
顶部