关于google:万字详解-Google-Play-上架应用标准包格式-AAB

16次阅读

共计 32805 个字符,预计需要花费 83 分钟才能阅读完成。

利用出海绕不开 Google Play 这个外围渠道。关注【融云寰球互联网通信云】理解更多

依据数据平台 Statcounter 的数据,截至 2022 年 6 月,Android 在寰球挪动设施操作系统的市场份额占比高达 72.12%。而 Google Play 是相对的 Android 流量巨头,也是 Android 利用上架的第一抉择。

自 2021 年 8 月起,Google 要求在 Google Play 中公布的利用应用 Android App Bundle(AAB)格局。AAB 能够提供更小的 App 体积,晋升用户的下载转化率并缩小卸载量,其要求应用程序的大小不超过 150MB。

对于须要超过 150MB 的应用程序,App Bundles 引入了 Play Asset Delivery(PAD)性能,通过资源动静下发,实现更顺畅的公布且尔后的更新会变快,因为更新包不会蕴含所有的内容。

本文以一个出海 App 为案例,分享其采纳 Android App Bundle 格局及 Play Asset Delivery 计划“瘦身”上架的实际过程。


Android App Bundle

AAB 的劣势

应用 AAB 进行公布,将由 Google Play 负责 APK 的生成和签名。并且包体积大小从 APK 的 100M 限度,变为了 AAB 的 150M 限度。

应用 App Bundle 会将 APK 的生成和签名工作转到 Google Play 上实现,用户在下载时,Google Play 会依据用户应用的设施生成通过优化的 APK,其中仅蕴含设施在运行时所需的代码和资源,可取得更小、更有针对性的下载包。

(应用 App Bundle 可取得更小下载包)

AAB 包的形成

Android App Bundle 是一种新的公布格局,其中蕴含了所有通过编译的代码和资源。在 AAB 包中有三种类型的模块:

  1. 根本 APK(Base.apk):提供根本的性能,当用户下载利用时,首先会下载并装置该 APK。
  2. 配置 APK:针对不同的屏幕密度、CPU 架构或者语言会生成对应的 APK 文件,当用户下载利用时,只会下载并装置对应配置的 APK。
  3. 功能模块 APK:咱们能够将非根底并绝对独立的性能打包成 APK,当用户须要应用的时候再进行装置。

    (AAB 包次要模块)

正如上图所示:
base/:此目录蕴含了利用根本模块代码。
feature1/ 和 feature2/:此目录蕴含了须要动静装置的代码。
asset_pack_1/ 和 asset_pack_2/:此目录蕴含了须要动静加载的资源。
BUNDLE-METADATA/:此目录下蕴含了元数据文件。
BundleConfig.pb:提供了无关 Bundle 自身的信息,如用于构建 App Bundle 工具的版本。
manifest/:每个模块的 AndroidManifest.xml 文件独自存储在这个目录下。
dex/:寄存每个模块的所有 dex 文件。

如何应用 AAB?

要生成一个 AAB 包十分的简略,咱们只须要在打包的时候从抉择 APK 改为抉择 Andriod App Bundle,剩下的流程跟生成 APK 文件完全一致。

这里要留神:

  1. 应用 AAB 格局后,不再反对 APK 扩大文件(*.obb)。
  2. 应用 AAB 格局进行公布,必须开启 Google Play 的利用签名性能。

Google Play 会应用此密钥对通过优化的 APK 签名。这里举荐勾选 Export encrypted key for enrolling published apps in Google Play App Signing 将密钥导出并且上传到 Google Play,如下图所示。

AAB 资源配置

默认状况下,在构建 App Bundle 时,反对为每一组语言资源、屏幕密度资源和 ABI 库生成配置 APK。如果你想停用对某种配置 APK 类型的反对,能够在根底模块的 build.gradle 文件中进行配置。

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 文件。

而后应用以下命令输入 APKS 文件:

java -jar bundletool-all-1.10.0.jar build-apks –bundle=app-debug.aab –output=app.apks –local-testing

应用 Zip 工具解压生成的 APKS 文件,能够看见在 splits 目录下,针对不同的语言、分辨率、ABI 生成了不同的 APK 文件。

如果 language 的 enableSplit 设置为 false,则不会针对语言生成不同的 APK 文件。

如果只对以后连贯 PC 的设施生成 APKS 文件能够应用以下命令:

java -jar bundletool-all-1.10.0.jar build-apks –connected-device –bundle=app-debug.aab –output=app.apks

通过应用该命令,生成的 APKS 文件中,就只蕴含针对于该设施的 base APK + 配置 APK。

咱们应用以下命令能够取得以后连贯设施的配置 json 文件:

java -jar bundletool-all-1.10.0.jar get-device-spec –output=device.json

{"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 “ 包名 ”

也能够应用以下命令达到同样的目标:

adb shell dumpsys package 包名 | findstr split

这在调试 AAB 包是否正确装置时十分有用。


Play Asset Delivery

在应用 AAB 格局之后,咱们发现案例利用最终输入的 AAB 文件仍旧很大,而且上传到 Google Play 仍旧超过了下限。

这时候就要应用 Play Asset Delivey(PAD)性能,PAD 宽泛用于游戏 App,它能够将游戏资源(纹理、声音等)独自公布,并且在 Google Play 上传 AAB 包时,独自计算大小。咱们能够将 App 内占用空间较大的资源放到独自的 Asset Pack 中,绕过 Google Play 上传 AAB 包 150M 的限度。

PAD 有三种散发模式:

  1. install-time:Asset Pack 在用户装置利用时候发,也被称为“事后”资源包,能够在利用启动时立刻应用。这些资源包会减少 Google Play 商店上列出的利用大小,且用户无奈批改或删除。
  2. fast-follow:Asset Pack 会在用户装置利用后立刻主动下载。用户无需关上利用即可开始下载,并且不会阻塞用户应用 App。这些资源包会减少 Google Play 商店上列出的利用大小。
  3. on-demand:Asset Pack 会在利用运行的时候下载。

不同的散发模式,Asset Pack 的大小下限不同:

  1. 每个 fast-follow 和 on-demand 模式的 Asset Pack 大小下限为 512MB。
  2. 所有 install-time 模式的 Asset Pack 总下限为 1GB。

3. 一个 AAB 包中所有 Asset Pack 的总大小下限为 2GB。

4. 一个 AAB 包中最多能够应用 50 个 Asset Pack。

资源动静加载

install-time 计划

第一步 创立一个新的 Module 来寄存资源。

第二步 在 install_time_asset_pack Module 的 build.gradle 中代码批改为:

// In the asset pack’s build.gradle file:
apply plugin: 'com.android.asset-pack'

assetPack {
    packName = "install_time_asset_pack" 
    dynamicDelivery {
        // 只能指定一种散发模式
        deliveryType = "install-time"
    }
}

第三步 在 App Module 的 build.gradle 中增加以下代码:

第四步 增加 install_time_asset_pack 模块到 setting.gradle 文件中。

include ‘:install_time_asset_pack’

第五步 将占用空间较大的资源放入 install_time_asset_pack Module。

这里须要在 mian 目录上面创立一个 assets 目录,将资源放入该目录下即可。

第六步 因为咱们将资源放入了 Asset Pack 包中,所以须要将这些资源在原来的目录删除。

applicationVariants.all {
    variant ->
        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()办法。

总结

应用 install-time 模式,Asset Pack 就绪后应用 AssetManager API 拜访资源即可。

应用 fast-follow 或 on-demand 模式,需先判断 Asset Pack 是否曾经下载。如果下载胜利,间接获取门路应用。如果还未开始下载,则须要触发下载资源包并且监听其状态,以便给用户反馈。

SO 动静加载

SO 动静加载跟资源的动静加载略微有一点不同。咱们晓得在 Android 中加载 SO 文件个别有两种形式:

  • System.load()
  • System.loadLibrary()

所以咱们要动静加载 SO,就得晓得这两种加载 SO 的形式有什么区别,依据他们的不同去寻找解决方案。

首先在办法应用上,System.load() 办法须要传入一个残缺的文件门路。而 System.loadLibrary() 只须要传入库文件名就行。

其次 System.load() 办法须要先加载依赖库。例如:LibA.so 依赖于 LibB.so 文件,应用 load()办法加载 LibA.so 文件时,就算 LibB.so 文件跟 LibA.so 文件在同一目录,也会因为找不到 LibB.so 而加载失败。

须要先用 load() 办法加载 LibB.so 再加载 LibA.so。这就要求咱们在加载 SO 文件时晓得文件的依赖关系。而应用 loadLibrary() 办法只须要将有依赖关系的 SO 放在同一目录即可。

System.load() 计划

要应用 System.load() 办法来动静加载 SO,咱们首先须要解决 SO 库依赖的问题。SO 库的依赖肯定被定义在了某个中央,只有咱们能找到这个中央,并且递归获取依赖,咱们就能失去残缺的依赖门路。

ELF 构造

咱们晓得 Android 是基于 Linux 操作系统,而 SO 文件在 Linux 下是依照 ELF 格局进行存储。所以咱们只须要按 ELF 格局解析 SO 文件,就可能获取到依赖。

要解析 ELF,咱们须要晓得 ELF 的构造。

ELF 中的信息以 Segment(段) 的模式进行存储,上图中列出了比拟常见的段:

.text:也就是咱们所谓的的代码段,源程序编译后的机器指令常常被寄存在此处。

.data:数据段用于寄存全局变量和部分动态变量。

.bss:未初始化的全局变量和部分动态变量个别寄存在此,因为这些变量没有初始化,它们的默认值为 0,放在数据段没有必要,独自在 .bss 段为它们预留地位,所以 .bss 段在 ELF 文件中也不占空间。

.got:全局偏移表(Global Offset Table)是为了解决动态链接库可能被多个过程共享而设计的。每个应用程序将援用的动静库符号收集起来,保留到 got 表中,用这个表来记录各个援用符号的地址,当程序中须要援用这些符号时,通过这个表查问各符号的地址。

这样做的益处在于,内存中只须要加载一份动静库,当不同程序运行时,只须要批改各自的 got 表,它们援用的符号都能够指向同一份动静库,这样就能够实现不同程序共享同一个动静库的目标。

.plt:PLT 表是用来解决提早绑定的。在程序开始运行时就将所有动静库中的符号收集起来,保留到 GOT 表中的做法是不可取的。为了实现提早绑定,能够通过 PLT 表减少一层跳转。

test@plt:
jmp *(test@GOT)
push n
push moduleID
jump _dl_runtime_resolve

下面伪代码中,test@plt 的第一条指令是跳转到 test@GOT 表查问 test() 办法的地址。

因为是第一次拜访 test() 办法,这时候 test@GOT 表中并没有 test() 办法的真正地址,而是保留的 test@plt 表中第二条指令 push n 的地址,所以会持续跳转到 test@plt 表中执行 push n 操作,这个数字 n 是 test 这个符号援用在重定位表 (.rel.plt) 中的下标。

直到执行完_dl_runtime_resolve 指令后,会将 test() 办法的真正地址保留到 test@GOT 表中,之后再次调用 test@plt 时,就会跳转到 test() 办法的真正地址。

目前 Android Native Hook 计划中,其中一种就是基于 PLT/GOT 表进行 Hook。

.dynamic:动静段中保留了动静链接器须要的根本信息,包含动静链接符号表的地位、依赖于哪些共享对象、动静链接重定位表的地位等。能够通过 readelf -d 来查看。

section header table: 段表形容了 ELF 各段的信息,包含段名,段的长度,在文件中的偏移等。咱们能够应用 readelf -S 或者 objdump -h 来进行查看。

program header table:程序头表是一个数组,数组中的每个元素称为“程序头”,每个程序头都形容了一个 Segment(段)信息。能够应用 readelf -h 来查看。

string table:字符串表中蕴含以 null 结尾的字符串,这些字符串可能是符号的名字或者 Segment 的名字,须要援用某个字符串的时候,只须要提供该字符串在字符串表中的序号即可。

须要留神字符串表中的第一个字符串永远是空串(null), 因为每个字符串都以 null 结尾,所以最初一个字节也必然是 null。

symbol table:符号表中记录了 ELF 文件中用到的所有符号(函数、变量)。每个符号都有一个对应值,对变量和函数来说,这个值就是它们的地址。

获取依赖信息

对 ELF 文件格式有了大略理解当前,咱们晓得在 .dynamic 段中寄存了以后 SO 文件的依赖信息。咱们只有能拿到这些信息,就能递归的拿到残缺的依赖。要读取 .dynamic 段中的内容,必须晓得该段在文件中的偏移。该段的偏移能够在 section header table 或 program header table 中找到。

而 section header table 和 program header table 的地址咱们又能够从 ELF 头中失去。

整体思路如下:

  1. 首先读取 ELF 头信息
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 魔数:

前 4 个字节是所有 ELF 文件都雷同的标识码,咱们在解析 ELF 文件的时候须要通过该标识码来判断以后是否解析的是 ELF 格局。

能够看到咱们以后解析的 ELF 文件是一个 32 位的 ELF 文件,并且是小端序。这两个信息很重要,间接关系到咱们在解析 ELF 文件时的正确性。例如这里应用了小端序,所以在判断 ELF 的魔数时,应该与 0x464C457F 进行比拟。有了这些信息后,咱们在读取 ELF 文件头时就有了根据,因为是 32 位的 ELF 文件,咱们就须要依据 32 位的数据结构来进行解析。

typedef struct {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 头中几个重要字段:

1.e_type:标记文件属于哪种类型。

表格中并没有齐全列出所有的值,在这里咱们只须要判断以后是否为 ET_DYN 类型即可。

2.e_phoff:程序头表偏移量,即程序头表的地址,以字节为单位。这里对应:
Start of program headers: 52 (bytes into file)
须要从 0x1C 处读取 4 个字节。

3.e_shoff:段头表偏移量,即段头表的地址,以字节为单位。这里对应:
Start of section headers: 12588 (bytes into file)
须要从 0x20 处读取 4 个字节。

4.e_phentsize:程序头表中每个 segment 的大小,以字节为单位。这里对应:
Size of program headers: 32 (bytes)
须要从 0x2A 处读取 2 个字节。

5.e_phnum:程序头表中 segment 的个数。这里对应:
Number of program headers: 7
须要从 0x2C 处读取 2 个字节。

6.e_shentsize:段头表中每个 segment 的大小,以字节为单位。这里对应:
Size of section headers: 40 (bytes)
须要从 0x2E 处读取 2 个字节。

7:e_shnum:段头表中 segment 的个数。这里对应:
Number of section headers: 20
须要从 0x30 处读取 2 个字节。

8.e_shstrndx:段头表中字符串表的索引。这里对应:
Section header string table index: 19
须要从 0x32 处读取 2 个字节。

拿到了程序头表的偏移后,咱们就能够对程序头表进行遍历,本例中有 7 个程序头。

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...
   00
   01     .dynsym .dynstr .hash .rel.dyn .rel.plt .plt .text .ARM.extab .ARM.exidx
   02     .fini_array .init_array .dynamic .got .data
   03     .dynamic
   04
   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 框架。

然而这就导致了 Treble 引入了两套本地库:框架和供应商的。在某些状况下,这两局部的库文件中可能存在雷同名称,但不同的实现。因为库的符号会裸露给一个过程的所有代码,所以会产生抵触。

为了解决这些问题,Android 动静链接器引入了基于命名空间的动静链接(namespace based dynamic linking),它和 Java 类加载器隔离资源的办法相似。

通过这种设计,每个库都加载到一个特定的命名空间 中,除非它们通过命名空间链接(namespace link)共享,否则不能拜访其余命名空间中的库。

咱们晓得不论是 load() 办法还是 loadLibrary() 办法,最终都是调用 dlopen() 办法来关上 SO 库的。dlopen() 办法最终会调用到 Linker.cpp 中的代码。

从 Android N 开始,Linker.cpp 的 loader_library() 办法进行了权限的判断。

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:

通过计算,根本 APK+ 配置 APK 的总大小为:84M。资源包大小不计算在 Google Play 上传限度之内。

参考资料:
1.https://developer.android.com…

2.https://jackwish.net/blog/201…
3.https://cloud.tencent.com/dev…

4.https://www.52pojie.cn/thread…

正文完
 0