相关文章推荐
爽快的墨镜  ·  能否直接将React ...·  3 月前    · 
坚韧的稀饭  ·  Perl单行(Perl ...·  3 年前    · 
android {
    // Instead, use the bundle block to control which types of configuration APKs
    // you want your app bundle to support.
    bundle {
        language {
            // Specifies that the app bundle should not support
            // configuration APKs for language resources. These
            // resources are instead packaged with each base and
            // feature APK.
            enableSplit = true|false
        density {
            // This property is set to true by default.
            enableSplit = true|false
        abi {
            // This property is set to true by default.
            enableSplit = true|false

使用 Bundletool 测试 AAB

在开发过程中,我们可能需要对 AAB 文件进行分析与调试。这时候就需要用到 Bundletool 工具。使用该工具,我们可以完成以下功能:

1. 将 AAB 转换为 APKS

AAB 格式无法直接安装在手机上,需要将 AAB 格式转换为 APKS 文件,再安装对应的 APK。

在开发阶段,我们可以使用 Build Bundle 来生成 AAB 文件。

"supportedAbis": ["arm64-v8a", "armeabi-v7a", "armeabi"], "supportedLocales": ["zh-CN", "ar-JO", "en-US"], "deviceFeatures": ["reqGlEsVersion\u003d0x30002", "android.hardware.audio.low_latency", "android.hardware.audio.output", "android.hardware.audio.pro", "android.hardware.bluetooth", "android.hardware.bluetooth_le", "android.hardware.camera", "android.hardware.camera.any", "android.hardware.camera.autofocus", "android.hardware.camera.capability.manual_post_processing", "android.hardware.camera.capability.manual_sensor", "android.hardware.camera.capability.raw", "android.hardware.camera.concurrent", "android.hardware.camera.flash", "android.hardware.camera.front", "android.hardware.camera.level.full", "android.hardware.context_hub", "android.hardware.device_unique_attestation", "android.hardware.faketouch", "android.hardware.fingerprint", "android.hardware.identity_credential\u003d202101", "android.hardware.location", "android.hardware.location.gps", "android.hardware.location.network", "android.hardware.microphone", "android.hardware.nfc", "android.hardware.nfc.any", "android.hardware.nfc.ese", "android.hardware.nfc.hce", "android.hardware.nfc.hcef", "android.hardware.nfc.uicc", "android.hardware.opengles.aep", "android.hardware.ram.normal", "android.hardware.reboot_escrow", "android.hardware.screen.landscape", "android.hardware.screen.portrait", "android.hardware.se.omapi.ese", "android.hardware.se.omapi.uicc", "android.hardware.security.model.compatible", "android.hardware.sensor.accelerometer", "android.hardware.sensor.barometer", "android.hardware.sensor.compass", "android.hardware.sensor.gyroscope", "android.hardware.sensor.hifi_sensors", "android.hardware.sensor.light", "android.hardware.sensor.proximity", "android.hardware.sensor.stepcounter", "android.hardware.sensor.stepdetector", "android.hardware.strongbox_keystore", "android.hardware.telephony", "android.hardware.telephony.carrierlock", "android.hardware.telephony.cdma", "android.hardware.telephony.euicc", "android.hardware.telephony.gsm", "android.hardware.telephony.ims", "android.hardware.touchscreen", "android.hardware.touchscreen.multitouch", "android.hardware.touchscreen.multitouch.distinct", "android.hardware.touchscreen.multitouch.jazzhand", "android.hardware.usb.accessory", "android.hardware.usb.host", "android.hardware.vulkan.compute", "android.hardware.vulkan.level\u003d1", "android.hardware.vulkan.version\u003d4198400", "android.hardware.wifi", "android.hardware.wifi.aware", "android.hardware.wifi.direct", "android.hardware.wifi.passpoint", "android.hardware.wifi.rtt", "android.software.activities_on_secondary_displays", "android.software.app_enumeration", "android.software.app_widgets", "android.software.autofill", "android.software.backup", "android.software.cant_save_state", "android.software.companion_device_setup", "android.software.connectionservice", "android.software.controls", "android.software.cts", "android.software.device_admin", "android.software.device_id_attestation", "android.software.file_based_encryption", "android.software.home_screen", "android.software.incremental_delivery\u003d2", "android.software.input_methods", "android.software.ipsec_tunnels", "android.software.live_wallpaper", "android.software.managed_users", "android.software.midi", "android.software.opengles.deqp.level\u003d132383489", "android.software.picture_in_picture", "android.software.print", "android.software.secure_lock_screen", "android.software.securely_removes_users", "android.software.sip", "android.software.sip.voip", "android.software.verified_boot", "android.software.voice_recognizers", "android.software.vulkan.deqp.level\u003d132383489", "android.software.webview", "com.google.android.apps.dialer.SUPPORTED", "com.google.android.feature.ADAPTIVE_CHARGING", "com.google.android.feature.AER_OPTIMIZED", "com.google.android.feature.D2D_CABLE_MIGRATION_FEATURE", "com.google.android.feature.DREAMLINER", "com.google.android.feature.EXCHANGE_6_2", "com.google.android.feature.GOOGLE_BUILD", "com.google.android.feature.GOOGLE_EXPERIENCE", "com.google.android.feature.GOOGLE_FI_BUNDLED", "com.google.android.feature.NEXT_GENERATION_ASSISTANT", "com.google.android.feature.PIXEL_2017_EXPERIENCE", "com.google.android.feature.PIXEL_2018_EXPERIENCE", "com.google.android.feature.PIXEL_2019_EXPERIENCE", "com.google.android.feature.PIXEL_2019_MIDYEAR_EXPERIENCE", "com.google.android.feature.PIXEL_2020_EXPERIENCE", "com.google.android.feature.PIXEL_2020_MIDYEAR_EXPERIENCE", "com.google.android.feature.PIXEL_EXPERIENCE", "com.google.android.feature.TURBO_PRELOAD", "com.google.android.feature.WELLBEING", "com.nxp.mifare", "com.verizon.hardware.telephony.ehrpd", "com.verizon.hardware.telephony.lte"], "glExtensions": ["GL_OES_EGL_image", "GL_OES_EGL_image_external", "GL_OES_EGL_sync", "GL_OES_vertex_half_float", "GL_OES_framebuffer_object", "GL_OES_rgb8_rgba8", "GL_OES_compressed_ETC1_RGB8_texture", "GL_AMD_compressed_ATC_texture", "GL_KHR_texture_compression_astc_ldr", "GL_KHR_texture_compression_astc_hdr", "GL_OES_texture_compression_astc", "GL_OES_texture_npot", "GL_EXT_texture_filter_anisotropic", "GL_EXT_texture_format_BGRA8888", "GL_EXT_read_format_bgra", "GL_OES_texture_3D", "GL_EXT_color_buffer_float", "GL_EXT_color_buffer_half_float", "GL_QCOM_alpha_test", "GL_OES_depth24", "GL_OES_packed_depth_stencil", "GL_OES_depth_texture", "GL_OES_depth_texture_cube_map", "GL_EXT_sRGB", "GL_OES_texture_float", "GL_OES_texture_float_linear", "GL_OES_texture_half_float", "GL_OES_texture_half_float_linear", "GL_EXT_texture_type_2_10_10_10_REV", "GL_EXT_texture_sRGB_decode", "GL_EXT_texture_format_sRGB_override", "GL_OES_element_index_uint", "GL_EXT_copy_image", "GL_EXT_geometry_shader", "GL_EXT_tessellation_shader", "GL_OES_texture_stencil8", "GL_EXT_shader_io_blocks", "GL_OES_shader_image_atomic", "GL_OES_sample_variables", "GL_EXT_texture_border_clamp", "GL_EXT_EGL_image_external_wrap_modes", "GL_EXT_multisampled_render_to_texture", "GL_EXT_multisampled_render_to_texture2", "GL_OES_shader_multisample_interpolation", "GL_EXT_texture_cube_map_array", "GL_EXT_draw_buffers_indexed", "GL_EXT_gpu_shader5", "GL_EXT_robustness", "GL_EXT_texture_buffer", "GL_EXT_shader_framebuffer_fetch", "GL_ARM_shader_framebuffer_fetch_depth_stencil", "GL_OES_texture_storage_multisample_2d_array", "GL_OES_sample_shading", "GL_OES_get_program_binary", "GL_EXT_debug_label", "GL_KHR_blend_equation_advanced", "GL_KHR_blend_equation_advanced_coherent", "GL_QCOM_tiled_rendering", "GL_ANDROID_extension_pack_es31a", "GL_EXT_primitive_bounding_box", "GL_OES_standard_derivatives", "GL_OES_vertex_array_object", "GL_EXT_disjoint_timer_query", "GL_KHR_debug", "GL_EXT_YUV_target", "GL_EXT_sRGB_write_control", "GL_EXT_texture_norm16", "GL_EXT_discard_framebuffer", "GL_OES_surfaceless_context", "GL_OVR_multiview", "GL_OVR_multiview2", "GL_EXT_texture_sRGB_R8", "GL_KHR_no_error", "GL_EXT_debug_marker", "GL_OES_EGL_image_external_essl3", "GL_OVR_multiview_multisampled_render_to_texture", "GL_EXT_buffer_storage", "GL_EXT_external_buffer", "GL_EXT_blit_framebuffer_params", "GL_EXT_clip_cull_distance", "GL_EXT_protected_textures", "GL_EXT_shader_non_constant_global_initializers", "GL_QCOM_texture_foveated", "GL_QCOM_texture_foveated_subsampled_layout", "GL_QCOM_shader_framebuffer_fetch_noncoherent", "GL_QCOM_shader_framebuffer_fetch_rate", "GL_EXT_memory_object", "GL_EXT_memory_object_fd", "GL_EXT_EGL_image_array", "GL_NV_shader_noperspective_interpolation", "GL_KHR_robust_buffer_access_behavior", "GL_EXT_EGL_image_storage", "GL_EXT_blend_func_extended", "GL_EXT_clip_control", "GL_OES_texture_view", "GL_EXT_fragment_invocation_density", "GL_QCOM_motion_estimation", "GL_QCOM_validate_shader_binary", "GL_QCOM_YUV_texture_gather"], "screenDensity": 440, "sdkVersion": 31

从生成的 json 文件中可以看出,该设备当前添加支持的地区语言为中文、阿语和英语。所以在之前生成的 APK 中,也包含这三种语言的 APK。

2. 将 APKS 部署到连接设备

在生成 APKS 文件以后,使用以下命令可以将 APKS  文件部署到当前所连接的设备上:

java -jar bundletool-all-1.10.0.jar install-apks –apks=app_release.apks

安装成功后,我们可以使用 adb 命令来确认是否成功安装配置 APK:

adb shell pm path “包名”

variant.mergeAssetsProvider.configure { doLast { def file = fileTree(dir: outputDir, includes: ['model/ai_body.bundle', 'model/ai_face.bundle', 'model/ai_green.bundle', 'model/ai_human.bundle', 'graphics/body.bundle', 'graphics/controller.bundle', 'graphics/face.bundle', 'graphics/tongue.bundle']) delete(file)

第七步编写代码,将 Asset Pack 中的资源拷贝到项目的私有目录并且返回路径。在原有加载逻辑中修改为该路径。

 public String copyResource(String relativeAssetPath){
    AssetFileDescriptor openFd = mAssetManager.openFd(relativeAssetPath);
    String filePath = mContext.getExternalFilesDir(null).getAbsolutePath() + File.separator + relativeAssetPath;
    File file = new File(filePath);
    if (file.exists()) {
      return filePath;
    } else {
      new File(file.getParent() + "/").mkdirs();
    copyFile(openFd.createInputStream(), filePath)
    return filePath;
 private  void copyFile(FileInputStream fileInputStream, String outFilePath) throws IOException {
    if (fileInputStream != null) {
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(outFilePath);
            byte[] bytes = new byte[1024];
            int temp = 0;
            while ((temp = fileInputStream.read(bytes)) != -1) {
                fos.write(bytes, 0, temp);
        } catch (Exception exception) {
            Log.e(TAG, "copyFile: e=" + exception.getMessage());
        } finally {
            if (fos != null) {
                fos.close();

fast-follow、on-demand Asset 方案

采用 fast-follow 或 on-demand Asset 方案,在 Asset Pack 的 build.gralde 中需要修改 deliveryType 属性。

apply plugin: 'com.android.asset-pack'
assetPack {
    packName = "on_demand_asset" // Directory name for the asset pack
    dynamicDelivery {
        deliveryType = "fast-follow | on-demand"

然后需判断资源包是否存在,如果不存在需要开启下载并且监听其下载状态。

private void getAssetResource() { if (mAssetPackManager != null) { AssetPackLocation assetLocation = mAssetPackManager.getPackLocation(AssetPackName); if (assetLocation == null) { //跟踪资源包的安装进度 mAssetPackManager.registerListener(new AssetPackStateUpdateListener() { @Override public void onStateUpdate(@NonNull AssetPackState assetPackState) { switch (assetPackState.status()) { case AssetPackStatus.PENDING: break; case AssetPackStatus.DOWNLOADING: //这里监控下载进度 break; case AssetPackStatus.TRANSFERRING: // 100% downloaded and assets are being transferred. // Notify user to wait until transfer is complete. break; case AssetPackStatus.COMPLETED: //下载成功后加载数据 loadData(); break; case AssetPackStatus.FAILED: //如果下载失败了在这里进行处理 break; case AssetPackStatus.CANCELED: // Request canceled. Notify user. break; case AssetPackStatus.WAITING_FOR_WIFI: if (!waitForWifiConfirmationShown) { mAssetPackManager.showCellularDataConfirmation(MainActivity.this) .addOnSuccessListener(new OnSuccessListener<Integer>() { @Override public void onSuccess(Integer resultCode) { if (resultCode == RESULT_OK) { Log.d(TAG, "Confirmation dialog has been accepted."); } else if (resultCode == RESULT_CANCELED) { Log.d(TAG, "Confirmation dialog has been denied by the user."); waitForWifiConfirmationShown = true; break; case AssetPackStatus.NOT_INSTALLED: // Asset pack is not downloaded yet. break; case AssetPackStatus.UNKNOWN: break; //下载资源包 mAssetPackManager.fetch(Collections.singletonList(AssetPackName)); } else { //资源如果已经下载,直接进行加载 loadData();

在下载过程中,若下载内容超过 150M 且用户未连接到 Wi-Fi,那在用户明确同意使用移动网络下载之前,是不会下载的。

同样,如果下载内容较大并且用户中途 Wi-Fi 断开,下载也会被暂停,需要用户明确同意使用移动网络下载才会继续。

这时候监听到的状态为 WAITING_FOR_WIFI。要触发用户使用移动网络下载的提示,需要调用 showCellularDataConfirmation()方法。

ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  Machine:                           ARM
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          52 (bytes into file)
  Start of section headers:          12588 (bytes into file)
  Flags:                             0x5000000, Version5 EABI
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         7
  Size of section headers:           40 (bytes)
  Number of section headers:         20
  Section header string table index: 19

从头信息中我们可以看到 ELF 魔数:

unsigned char e_ident[16]; //0x00-0x0f Elf32_Half e_type; //0x10-0x11 Elf32_Half e_machine; //0x12-0x13 Elf32_Word e_version; //0x14-0x17 Elf32_Addr e_entry; //0x18-0x1b Elf32_Off e_phoff; //0x1c-0x1f Elf32_Off e_shoff; //0x20-0x23 Elf32_Word e_flags; //0x24-0x27 Elf32_Half e_ehsize; //0x28-0x29 Elf32_Half e_phentsize; //0x2a-0x2b Elf32_Half e_phnum; //0x2c-0x2d Elf32_Half e_shentsize; //0x2e-0x2f Elf32_Half e_shnum; //0x30-0x31 Elf32_Half e_shstrndx; //0x32-0x33 } Elf32_Ehdr;

其中 Elf32_Half、ELF32_Word 等都是自定义类型,长度分别如下:

Elf file type is DYN (Shared object file)
Entry point 0x0
There are 7 program headers, starting at offset 52
Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x00000034 0x00000034 0x000e0 0x000e0 R   0x4
  LOAD           0x000000 0x00000000 0x00000000 0x02160 0x02160 R E 0x1000
  LOAD           0x002eac 0x00003eac 0x00003eac 0x00158 0x00158 RW  0x1000
  DYNAMIC        0x002eb8 0x00003eb8 0x00003eb8 0x00100 0x00100 RW  0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0
  EXIDX          0x002088 0x00002088 0x00002088 0x000d8 0x000d8 R   0x4
  GNU_RELRO      0x002eac 0x00003eac 0x00003eac 0x00154 0x00154 RW  0x4
 Section to Segment mapping:
  Segment Sections...
   01     .dynsym .dynstr .hash .rel.dyn .rel.plt .plt .text .ARM.extab .ARM.exidx
   02     .fini_array .init_array .dynamic .got .data
   03     .dynamic
   05     .ARM.exidx
   06     .fini_array .init_array .dynamic .got

我们从偏移位置为 0x52 处开始遍历,每次增加步长为 32(e_phentsize)。同样的,程序头也有自己的数据结构:

typedef struct { 
 Elf32_Word p_type;//0x52-0x55 
 Elf32_Off p_offset; //0x56-0x59
 Elf32_Addr p_vaddr; //0x5a-0x5d
 Elf32_Addr p_paddr; //0x5e-0x62
 Elf32_Word p_filesz; //0x63-0x66
 Elf32_Word p_memsz; //0x67-0x6a
 Elf32_Word p_flags; //0x6b-0x6e
 Elf32_Word p_align; //0x6f-0x73
} Elf32_Phdr;

1.p_type:程序头所描述的段的类型,这里我们只需要找到 PT_DYNAMIC 类型即可。

2.p_offset:程序头所描述的段的偏移量,相对于文件开头的偏移量 以字节为单位。

3.p_vaddr:本段内容的开始位置在进程中的虚拟地址,以字节为单位。

4.p_memsz:本段内容的大小,以字节为单位。

从 readelf 打印出的内容中可见,我们其实要找的只是这行内容:

DYNAMIC        0x002eb8 0x00003eb8 0x00003eb8 0x00100 0x00100 RW  0x4

这里的偏移地址就是 .dynamic 段的偏移地址。

有了 .dynamic 段的偏移地址后,我们就可以读取到所依赖的 SO 文件。

Dynamic section at offset 0x2eb8 contains 27 entries:
  Tag        Type                         Name/Value
 0x00000003 (PLTGOT)                     0x3fd4
 0x00000002 (PLTRELSZ)                   64 (bytes)
 0x00000017 (JMPREL)                     0xb28
 0x00000014 (PLTREL)                     REL
 0x00000011 (REL)                        0xae8
 0x00000012 (RELSZ)                      64 (bytes)
 0x00000013 (RELENT)                     8 (bytes)
 0x6ffffffa (RELCOUNT)                   6
 0x00000006 (SYMTAB)                     0x114
 0x0000000b (SYMENT)                     16 (bytes)
 0x00000005 (STRTAB)                     0x494
 0x0000000a (STRSZ)                      1238 (bytes)
 0x00000004 (HASH)                       0x96c
 0x00000001 (NEEDED)                     Shared library: [libhello.so]
 0x00000001 (NEEDED)                     Shared library: [libstdc++.so]
 0x00000001 (NEEDED)                     Shared library: [libm.so]
 0x00000001 (NEEDED)                     Shared library: [libc.so]
 0x00000001 (NEEDED)                     Shared library: [libdl.so]
 0x0000000e (SONAME)                     Library soname: [libhellojni.so]
 0x0000001a (FINI_ARRAY)                 0x3eac
 0x0000001c (FINI_ARRAYSZ)               8 (bytes)
 0x00000019 (INIT_ARRAY)                 0x3eb4
 0x0000001b (INIT_ARRAYSZ)               4 (bytes)
 0x00000010 (SYMBOLIC)                   0x0
 0x0000001e (FLAGS)                      SYMBOLIC BIND_NOW
 0x6ffffffb (FLAGS_1)                    Flags: NOW
 0x00000000 (NULL)                       0x0

.dynamic 段的数据结构比较简单

typedef struct { 
 Elf32_Word p_type;//0x52-0x55 
 Elf32_Off p_offset; //0x56-0x59
 Elf32_Addr p_vaddr; //0x5a-0x5d
 Elf32_Addr p_paddr; //0x5e-0x62
 Elf32_Word p_filesz; //0x63-0x66
 Elf32_Word p_memsz; //0x67-0x6a
 Elf32_Word p_flags; //0x6b-0x6e
 Elf32_Word p_align; //0x6f-0x73
} Elf32_Phdr;

我们需要遍历 .dynamic 段,找到 d_tag 为:NEEDED 和 STRTAB 的内容。

其中 NEEDED 指明了所依赖的库,但是该元素本身并不是一个字符串,它指向 STRTAB 表中的索引。所以我们也需要获取到 STRTAB 的偏移量。

在本例中我们会获取到 5 个 NEEDED:

0x00000001 (NEEDED)     

Shared library: [libhello.so] 

0x00000001 (NEEDED)                     

Shared library: [libstdc++.so] 

0x00000001 (NEEDED)                     

Shared library: [libm.so] 

0x00000001 (NEEDED)                     

Shared library: [libc.so] 

0x00000001 (NEEDED)                     

Shared library: [libdl.so]

以及 STRTAB 的偏移:

0x00000005 (STRTAB)                     

0x494

有了这些信息,我们就可以获取到所依赖的 SO 文件,然后再递归去查找这些 SO 的依赖,得到完整的依赖路径。有了依赖路径,我们就可以按照依赖的先后顺序来加载 SO 文件。

全局替换 load 方法

由于是动态加载 SO 文件,所以 SO 文件地址跟之前有可能不一样。在解决完 load() 方法的依赖问题后,我们需要修改成新的地址以及加载逻辑。

这时候可以将整个 SO 获取依赖信息以及加载的逻辑进行封装,封装好以后可以通过 ASM 在编译期进行字节码修改,将调用系统 System.load() 方法指令全部替换成自己封装的方法。如果不想自己封装,也可以使用 ReLinker或者 Facebook 开源的 SoLoader。

System.loadLibrary() 方案

loadLibrary() 方案比 load() 简单,它不需要我们去解析 ELF 文件读取依赖信息。很多时候我们也都采用这个方法来加载 SO 库。

我们先分析一下 loadLibrary() 方法是如何加载 SO 库的。

public static void loadLibrary(String libname) {
    Runtime.getRuntime().loadLibrary0(Reflection.getCallerClass(), libname);
void loadLibrary0(Class<?> fromClass, String libname) {
    ClassLoader classLoader = ClassLoader.getClassLoader(fromClass);
    loadLibrary0(classLoader, fromClass, libname);
private synchronized void loadLibrary0(ClassLoader loader, Class<?> callerClass, String libname) {
    if (libname.indexOf((int)File.separatorChar) != -1) {
        throw new UnsatisfiedLinkError(
"Directory separator should not appear in library name: " + libname);
    String libraryName = libname;
    if (loader != null && !(loader instanceof BootClassLoader)) {
        String filename = loader.findLibrary(libraryName);
        if (filename == null &&
                (loader.getClass() == PathClassLoader.class ||
                 loader.getClass() == DelegateLastClassLoader.class)) {
            filename = System.mapLibraryName(libraryName);
        if (filename == null) {
            throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
                                           System.mapLibraryName(libraryName) + "\"");
        String error = nativeLoad(filename, loader);
        if (error != null) {
            throw new UnsatisfiedLinkError(error);
        return;
    getLibPaths();
    String filename = System.mapLibraryName(libraryName);
    String error = nativeLoad(filename, loader, callerClass);
    if (error != null) {
        throw new UnsatisfiedLinkError(error);

以上是 loadLibrary 的调用顺序,关键逻辑在 loadLibrary0() 方法中。在该方法中会使用 ClassLoader 的 findLibrary() 方法去获取 SO 文件。获取成功后交给 native 方法 nativeLoad() 对 SO 文件进行加载。

//BaseDexClassLoader
 public String findLibrary(String name) {
        return pathList.findLibrary(name);
 //DexPathList.java
    /** List of native library path elements. */
    // Some applications rely on this field being an array or we'd use a final list here
    @UnsupportedAppUsage
    /* package visible for testing */ NativeLibraryElement[] nativeLibraryPathElements;
    /** List of application native library directories. */
    @UnsupportedAppUsage
    private final List<File> nativeLibraryDirectories;
    /** List of system native library directories. */
    @UnsupportedAppUsage
    private final List<File> systemNativeLibraryDirectories;
   public String findLibrary(String libraryName) {
        String fileName = System.mapLibraryName(libraryName);
        for (NativeLibraryElement element : nativeLibraryPathElements) {
            String path = element.findNativeLibrary(fileName);
            if (path != null) {
                return path;
        return null;
    private static NativeLibraryElement[] makePathElements(List<File> files) {
        NativeLibraryElement[] elements = new NativeLibraryElement[files.size()];
        int elementsPos = 0;
        for (File file : files) {
            String path = file.getPath();
            if (path.contains(zipSeparator)) {
                String split[] = path.split(zipSeparator, 2);
                File zip = new File(split[0]);
                String dir = split[1];
                elements[elementsPos++] = new NativeLibraryElement(zip, dir);
            } else if (file.isDirectory()) {
                // We support directories for looking up native libraries.
                elements[elementsPos++] = new NativeLibraryElement(file);
        if (elementsPos != elements.length) {
            elements = Arrays.copyOf(elements, elementsPos);
        return elements;
     DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
        this.definingContext = definingContext;
        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext, isTrusted);
        this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
        this.systemNativeLibraryDirectories =
                splitPaths(System.getProperty("java.library.path"), true);
        this.nativeLibraryPathElements = makePathElements(getAllNativeLibraryDirectories());
        if (suppressedExceptions.size() > 0) {
            this.dexElementsSuppressedExceptions =
                suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
        } else {
            dexElementsSuppressedExceptions = null;
     private List<File> getAllNativeLibraryDirectories() {
        List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
        allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
        return allNativeLibraryDirectories;

上面源码有点多,简单总结就是:

1.从 java.library.pah 中获取到系统 native 库的目录;

2.把应用程序的 native 库的目录一起放入一个 List 中,传给 makePathElements() 方法;

3.makePathElements() 方法经过处理后返回一个  NativeLibraryElement[]  数组给 nativeLibraryPathElements 变量;

4.查找 SO 库时,从 nativeLibraryPathElements 这个变量包含的目录中进行查找。

知道原理以后,要实现 loadlibrary() 动态加载 SO 就很简单了。

只需要将动态加载的 SO 库存放目录通过反射添加到 nativeLibraryPathElements 数组的第一个位置,这样系统按照 nativeLibraryPathElements 中包含的目录进行查找时,就能找到我们的 SO 文件。

private static void install(ClassLoader classLoader, File folder) throws Throwable { final Field pathListField = ReflectionUtils.findField(classLoader, PATH_LIST); final Object dexPathList = pathListField.get(classLoader); final Field nativeLibraryDirectories = ReflectionUtils.findField(dexPathList, NATIVE_LIBRARY_DIRECTORIES); 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 = ReflectionUtils.findField(dexPathList, SYSTEM_NATIVE_LIBRARY_DIRECTORIES); List<File> origSystemLibDirs = (List<File>) systemNativeLibraryDirectories.get(dexPathList); if (origSystemLibDirs == null) { origSystemLibDirs = new ArrayList<>(2); //创建新的list,方式并发修改异常 final List<File> newLibDirs = new ArrayList<>(origLibDirs.size() + origSystemLibDirs.size() + 1); newLibDirs.addAll(origLibDirs); newLibDirs.addAll(origSystemLibDirs); final Method makeElements = ReflectionUtils.findMethod(dexPathList, MAKE_PATH_ELEMENTS, List.class); final Object[] elements = (Object[]) makeElements.invoke(dexPathList, newLibDirs); final Field nativeLibraryPathElements = ReflectionUtils.findField(dexPathList, NATIVE_LIBRARY_PATH_ELEMENTS); nativeLibraryPathElements.set(dexPathList, elements);

由于 Android 各版本的实现有稍许差异,所以我们需要对版本进行适配。具体可以参考腾讯 Tinker的实现。

dlopen 问题

本来一切都很美好,直到 Android N(7.0)到来。

Android 平台一直都是高度碎片化的,设备制造商不愿意将旧的设备升级到新的 Android 平台,因为需要很多工作量,这就迫使开发者需要在大量的设备上去测试他们的应用程序。

为了解决这个问题,谷歌发布了 Project Treble。

Treble 将 Android 平台分为框架(Framework)和供应商(Vendor)两部分,它们之间通过稳定的接口进行交互。因此通过 Treble 可以实现在保持供应商部分不变的情况下,升级 Android 框架。

static bool load_library(android_namespace_t* ns,
                         LoadTask* task,
                         LoadTaskList* load_tasks,
                         int rtld_flags,
                         const std::string& realpath,
                         bool search_linked_namespaces) {
  if ((fs_stat.f_type != TMPFS_MAGIC) && (!ns->is_accessible(realpath))) {
      return false;
  return true;

其中 is_accessible() 方法会判断给定的绝对路径是否在以下三个列表中:

1. ld_library_paths

2. default_library_paths

3. permitted_paths

bool android_namespace_t::is_accessible(const std::string& file) {
  if (!is_isolated_) {
    return true;
  if (!allowed_libs_.empty()) {
    const char *lib_name = basename(file.c_str());
    if (std::find(allowed_libs_.begin(), allowed_libs_.end(), lib_name) == allowed_libs_.end()) {
      return false;
  for (const auto& dir : ld_library_paths_) {
    if (file_is_in_dir(file, dir)) {
      return true;
  for (const auto& dir : default_library_paths_) {
    if (file_is_in_dir(file, dir)) {
      return true;
  for (const auto& dir : permitted_paths_) {
    if (file_is_under_dir(file, dir)) {
      return true;
  return false;

如果给定的路径不在以上三个列表中,load_library() 方法就会返回 false,导致加载失败。程序会报出以下异常:

java.lang.UnsatisfiedLinkError: dlopen failed: library "/storage/emulated/0/Android/data/org.zzy.nativetest/files/bundle/jni/arm64-v8a/libnativetest.so" needed or dlopened by "/apex/com.android.art/lib64/libnativeloader.so" is not accessible for the namespace "classloader-namespace"
        at java.lang.Runtime.loadLibrary0(Runtime.java:1077)
        at java.lang.Runtime.loadLibrary0(Runtime.java:998)
        at java.lang.System.loadLibrary(System.java:1656)
        at org.zzy.nativetest.so.SoTestActivity.onCreate(SoTestActivity.java:28)
        at android.app.Activity.performCreate(Activity.java:8051)
        at android.app.Activity.performCreate(Activity.java:8031)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1329)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3608)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3792)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2210)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loopOnce(Looper.java:201)
        at android.os.Looper.loop(Looper.java:288)
        at android.app.ActivityThread.main(ActivityThread.java:7838)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)

这会有什么问题呢?我们之前通过反射的方式将 SO 库存放的目录添加到了 nativeLibraryPathElements 数组中,但是从 Android N 以后,SO 库的存放目录如果不在以上三个列表中,就会导致 dlopen 打开失败。

好在天无绝人之路,在 Logcat 的日志中,打印了  ld_library_paths,default_library_paths,permitted_paths 的值。

 [name="classloader-namespace", ld_library_paths="", default_library_paths="/data/app/~~-ARvezkrvMHNPn30p76eTg==/org.zzy.nativetest-oouE9DDeRvsCdlkHBgca1g==/lib/arm64:/data/app/~~-ARvezkrvMHNPn30p76eTg==/org.zzy.nativetest-oouE9DDeRvsCdlkHBgca1g==/base.apk!/lib/arm64-v8a", permitted_paths="/data:/mnt/expand:/data/data/org.zzy.nativetest"]

我们可以发现 permitted_paths 中包含了应用的沙盒目录。也就是说我们只要把需要动态加载的 SO 文件放到应用的沙盒目录下,就可以解决这个问题。

Asset Delivery 动态加载 SO

在了解完 SO 动态加载的方案之后,就可以开始使用 Asset Delivery 来动态加载 SO 库。我们采用的方案还是 install-time 模式,在应用安装时就进行分发。

首先我们还是要将原来的 SO 文件从项目中去掉。可以在 App module 中的 build.gradle 文件中使用以下方式去除:

packagingOptions {
        exclude 'META-INF/DEPENDENCIES'
        if (packAAB) {
            exclude 'lib/arm64-v8a/libxxxSDK.so'
            exclude 'lib/arm64-v8a/libxxx.so'
            exclude 'lib/arm64-v8a/libxxx_view.so'
            exclude 'lib/armeabi-v7a/libxxxSDK.so'
            exclude 'lib/armeabi-v7a/libxxx.so'
            exclude 'lib/armeabi-v7a/libxxx_view.so'

这里使用了一个变量来控制是否生成 AAB 包,如果生成的是 APK 包,SO 文件将不会被去除。

接着将 SO 库放入到之前创建的 install_time_asset_pack Module 中。记得按 ABI 版本进行区分。在 Application 初始化的时候把 SO 库拷贝到应用的沙盒目录,这里需要做一下检查,如果已经存在了,就别再拷贝了,要不然每次应用启动都拷贝一次挺耗性能的。

也需要判断一下当前设备是 64 位还是 32 位的,把相应 ABI 对应的 SO 文件拷贝过去就行。最后再使用我们前面提到的方案,将 SO 文件的存放目录通过反射添加到 nativeLibraryPathElements 数组中。

总结而言,对于使用 System.load() 方法来加载 SO 的,需要自己封装 ELF 的解析以及 SO 的加载逻辑,并且在编译期插桩来替换掉原来的加载逻辑。

对于使用 System.loadLibrary() 方法来加载 SO 的,需要通过反射将 SO 的加载目录注入到 nativeLibraryPathElements 变量。

不论是采用哪种方式动态加载 SO,SO 的存放路径必须放在应用的沙盒目录下。

每次 SO 文件更新,Asset Pack 中的 SO 也需要相应的更新。

未使用 AAB 格式发布之前,案例应用的 APK 大小达 227.49 MB,远超于 Google Play 的限制。

在改为 AAB 格式进行发布以后,Google Play 对于包大小的限制变为了:当用户下载您的应用时,安装应用所需的压缩 APK(例如,基本 APK + 配置 APK)的总大小不得超过 150 MB。

在 Pixel 5 手机上,如果安装案例 App,会获取以下 APK: