Envoy Mobile 实验性 Kotlin 应用:在 Android 模拟器中从零运行 hello_envoy_kt
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
本文以 Envoy Mobile(本项目mobile/子工程)中的实验性 Kotlin 示例应用 mobile/test/kotlin/apps/experimental 为对象,完整讲解如何在 Android 模拟器中构建、安装并运行它:包括模拟器镜像准备、Bazel 构建 APK、adb 安装与启动,以及该应用内置的 Engine 配置和三类响应过滤器(DemoFilter / BufferDemoFilter / AsyncDemoFilter)的实现原理。读完本文,你将掌握 Envoy Mobile 在 Android 端从“脚本启动”到“源码级理解”的完整闭环。
一、这份 README 在讲什么
仓库中 mobile/test/kotlin/apps/experimental/README.md 是一份极简的“如何运行”指南,全文核心只有两条命令,且要求从mobile根目录执行:
$ test/kotlin/apps/experimental/start_emulator.sh # 等待模拟器完全启动 $ test/kotlin/apps/experimental/start_app.sh这两条命令分别负责两件事:
start_emulator.sh:自动下载 Android 30(API 30)模拟器系统镜像、创建名为test_android_emulator的 AVD(Pixel 4)并启动模拟器;start_app.sh:用 Bazel 构建hello_envoy_ktAPK,通过 adb 安装到模拟器,并拉起应用主界面。
README 虽短,但它指向的示例应用却是 Envoy Mobile Kotlin API 的实验场:应用内置了异步 Engine 初始化、DNS 缓存、Socket Tagging、原生 buffer 过滤器、String Accessor、事件追踪以及三种 Kotlin 响应过滤器。下文将以这两条脚本为主线,逐层拆解每一步做了什么,并结合源码说明应用内部机制。
二、运行前置条件
两个脚本开头都做了同一个硬性校验(见 start_emulator.sh 与 start_app.sh):
if [[ -z "${ANDROID_HOME}" ]]; then echo "ANDROID_HOME environment variable must be set." exit 1 fi因此运行前必须确保:
- 环境变量
ANDROID_HOME已指向 Android SDK 根目录,否则脚本立即退出并提示错误; - SDK 内包含
cmdline-tools/latest(提供sdkmanager与avdmanager)、emulator与platform-tools(提供adb); - 系统已安装 Bazel(用于构建 APK),并具备 Envoy Mobile 仓库的 Bazel 工作区(
--config=mobile-android-release这一配置在 mobile/BUILD 与仓库 Bazel 配置中定义,面向 Android release 构建)。
应用本身对 Android 版本的要求见 AndroidManifest.xml:
<uses-sdk android:minSdkVersion="24" android:targetSdkVersion="27"/>即最低支持 Android 7.0(API 24),同时声明了INTERNET与ACCESS_NETWORK_STATE两个网络权限——这正是该应用要发起真实网络请求(https://api.lyft.com/ping)所必需的。
三、第一步:启动模拟器(start_emulator.sh 详解)
脚本完整逻辑如下:
#!/usr/bin/env bash set -e if [[ -z "${ANDROID_HOME}" ]]; then echo "ANDROID_HOME environment variable must be set." exit 1 fi echo "y" | "${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" --install 'system-images;android-30;google_apis;x86_64' --channel=3 echo "no" | "${ANDROID_HOME}/cmdline-tools/latest/bin/avdmanager" create avd -n test_android_emulator -k 'system-images;android-30;google_apis;x86_64' --device pixel_4 --force "${ANDROID_HOME}/emulator/emulator" -avd test_android_emulator它依次完成三个动作:
安装系统镜像:
sdkmanager --install 'system-images;android-30;google_apis;x86_64' --channel=3system-images;android-30;google_apis;x86_64表示 Android 30(API 30)、带 Google APIs、x86_64 架构的模拟器镜像;--channel=3允许从早期访问渠道获取镜像,echo "y"自动确认许可协议;- 该命令是幂等的:镜像已安装时直接跳过。
创建 AVD:
avdmanager create avd -n test_android_emulator -k 'system-images;...' --device pixel_4 --force- 创建一个名为
test_android_emulator的虚拟设备,设备型号为pixel_4; --force允许覆盖同名已有 AVD,因此脚本可重复执行;echo "no"回答“是否创建自定义硬件配置”的交互提问,使用默认配置。
- 创建一个名为
启动模拟器:
emulator -avd test_android_emulator- 该命令会阻塞在前台,直到模拟器退出。README 特意提示“等待模拟器完全启动(Wait until the emulator is fully booted up)”,即需要在另一个终端继续执行下一步脚本,或等待模拟器桌面完全出现后再执行
start_app.sh。
- 该命令会阻塞在前台,直到模拟器退出。README 特意提示“等待模拟器完全启动(Wait until the emulator is fully booted up)”,即需要在另一个终端继续执行下一步脚本,或等待模拟器桌面完全出现后再执行
说明:
set -e使脚本在任何一步失败时立即中止,保证镜像缺失、AVD 创建失败等情况不会带病继续。
四、第二步:构建、安装并启动应用(start_app.sh 详解)
脚本完整逻辑如下:
#!/usr/bin/env bash set -e if [[ -z "${ANDROID_HOME}" ]]; then echo "ANDROID_HOME environment variable must be set." exit 1 fi bazel build --config=mobile-android-release //test/kotlin/apps/experimental:hello_envoy_kt "${ANDROID_HOME}/platform-tools/adb" install -r --no-incremental bazel-bin/test/kotlin/apps/experimental/hello_envoy_kt.apk "${ANDROID_HOME}/platform-tools/adb" shell am start -n io.envoyproxy.envoymobile.helloenvoyexperimentaltest/.MainActivity三个步骤逐一拆解:
Bazel 构建 APK:
bazel build --config=mobile-android-release //test/kotlin/apps/experimental:hello_envoy_kt- 目标
//test/kotlin/apps/experimental:hello_envoy_kt在 mobile/test/kotlin/apps/experimental/BUILD 中定义为android_binary,输出 APK 位于bazel-bin/test/kotlin/apps/experimental/hello_envoy_kt.apk; android_binary依赖hello_envoy_kt_lib(kt_android_library),后者编译 MainActivity.kt、DemoFilter.kt、BufferDemoFilter.kt、AsyncDemoFilter.kt 四个 Kotlin 源文件,并依赖//:envoy_mobile_android(Envoy Mobile 的 Android 引擎库)与//examples/kotlin/shared:hello_envoy_shared_lib(共享的 UI 组件,如ResponseRecyclerViewAdapter);- BUILD 中还定义了
detekt静态检查目标hello_envoy_kt_lint,说明该应用同时纳入 Kotlin 代码风格门禁。
- 目标
安装 APK:
adb install -r --no-incremental bazel-bin/test/kotlin/apps/experimental/hello_envoy_kt.apk-r允许覆盖安装(reinstall);--no-incremental禁用增量安装,确保完整安装、避免缓存不一致。
启动主界面:
adb shell am start -n io.envoyproxy.envoymobile.helloenvoyexperimentaltest/.MainActivity- 直接通过组件名拉起
MainActivity;包名io.envoyproxy.envoymobile.helloenvoyexperimentaltest同时出现在 AndroidManifest.xml 与 BUILD 的custom_package中,二者必须保持一致。
- 直接通过组件名拉起
五、应用内部做了什么:Engine 构建与请求循环
启动后,MainActivity.kt 的onCreate使用AndroidEngineBuilder完成引擎装配,这是理解 Envoy Mobile 配置面的最佳样例:
engine = AndroidEngineBuilder(application) .setLogLevel(LogLevel.DEBUG) .setLogger { _, msg -> Log.d(TAG, msg) } .addPlatformFilter(::DemoFilter) .addPlatformFilter(::BufferDemoFilter) .addPlatformFilter(::AsyncDemoFilter) .enableDNSCache(true) // required by DNS cache .addKeyValueStore("reserved.platform_store", SharedPreferencesStore(preferences)) .enableInterfaceBinding(true) .enableSocketTagging(true) .enableProxying(true) .addNativeFilter( "envoy.filters.http.buffer", Any.newBuilder() .setTypeUrl("type.googleapis.com/envoy.extensions.filters.http.buffer.v3.Buffer") .setValue(ByteString.empty()) .build() .toByteArray() .toString(Charsets.UTF_8) ) .addStringAccessor("demo-accessor", { "PlatformString" }) .setOnEngineRunning { Log.d(TAG, "Envoy async internal setup completed") } .setEventTracker({ ... }) .build()关键配置点及其源码印证:
- 三个平台过滤器:
DemoFilter、BufferDemoFilter、AsyncDemoFilter以构造函数引用注册到引擎,分别演示同步、缓冲、异步三类响应过滤模式(详见下文第六节); - DNS 缓存与键值存储:
enableDNSCache(true)后必须配套addKeyValueStore("reserved.platform_store", SharedPreferencesStore(preferences))(源码注释明确标注 “required by DNS cache”),缓存持久化落在 AndroidSharedPreferences上; - 网络能力开关:
enableInterfaceBinding(true)、enableSocketTagging(true)、enableProxying(true)分别开启接口绑定、Socket 打标(配合下面的.addSocketTag(1, 2))与系统代理支持; - 原生过滤器:通过
addNativeFilter注入 Envoy 的 HTTP buffer 原生过滤器(envoy.filters.http.buffer),参数以 protobufAny编码传输; - String Accessor:
addStringAccessor("demo-accessor", { "PlatformString" })注册了一个可被过滤器链读取的字符串访问器; - 事件追踪:
setEventTracker将引擎内部事件(Event emitted: key, value)全部打到 Logcat。
引擎构建完成后,MainActivity在独立后台线程(HandlerThread("hello_envoy_kt"))上开启每秒一次的请求循环:
handler.postDelayed( object : Runnable { override fun run() { try { makeRequest() recordStats() } catch (e: IOException) { ... } handler.postDelayed(this, TimeUnit.SECONDS.toMillis(1)) } }, TimeUnit.SECONDS.toMillis(1) )makeRequest()使用RequestHeadersBuilder(RequestMethod.GET, "https", "api.lyft.com", "/ping")构造请求,并显式添加.addSocketTag(1, 2);代码注释特别说明该请求会走 h2 流(而 Java 示例使用 http/1.1),这是为了在 CI 端到端测试中同时覆盖两条协议路径。响应回调中:
- 只筛选
FILTERED_HEADERS(server、filter-demo、buffer-filter-demo、async-filter-demo、x-envoy-upstream-service-time)展示到RecyclerView列表; - 状态码 200 时记为
Success,否则记录为Failure; recordStats()通过engine.pulseClient().counter(Element("foo"), Element("bar"), Element("counter"))对自定义计数器执行increment()与increment(5),演示 Envoy Mobile 的 Stats 脉冲上报 API。
六、三类实验性响应过滤器的实现差异
这是该实验应用区别于 baseline 普通示例的核心部分,三个过滤器展示了 Kotlin 过滤器的三种复杂程度。
6.1 DemoFilter:透传式同步过滤
DemoFilter.kt 实现ResponseFilter,是最简单的“处理即继续”模式:在onResponseHeaders中给响应头追加filter-demo: 1,数据、尾随头均直接透传:
override fun onResponseHeaders( headers: ResponseHeaders, endStream: Boolean, streamIntel: StreamIntel ): FilterHeadersStatus<ResponseHeaders> { Log.d("DemoFilter", "On headers!") val builder = headers.toResponseHeadersBuilder() builder.add("filter-demo", "1") return FilterHeadersStatus.Continue(builder.build()) }Continue表示不阻塞过滤链,立即将修改后的头传给下一个过滤器;onResponseData/onResponseTrailers同样以Continue透传。
6.2 BufferDemoFilter:暂停 + 缓冲 + 恢复
BufferDemoFilter.kt 演示“暂停过滤链、缓冲完整响应、再恢复迭代”的复杂模式(类注释原文:“pauses processing on the response filter chain, buffers until the response is complete, then resumes filter iteration”):
onResponseHeaders返回FilterHeadersStatus.StopIteration(),暂停过滤链迭代并暂存响应头;onResponseData每次收到数据都暂存(由于请求了缓冲,每次回调携带的是截至当前的累计数据),若endStream为真,则用FilterDataStatus.ResumeIteration(builder.build(), body)恢复迭代,同时追加buffer-filter-demo: 1;否则返回StopIterationAndBuffer()继续缓冲;onResponseTrailers出现尾随头即代表流结束,同样通过ResumeIteration恢复并追加头。
该过滤器让开发者理解 Envoy Mobile 过滤链“暂停/缓冲/恢复”的完整生命周期控制。
6.3 AsyncDemoFilter:异步恢复(回调式)
AsyncDemoFilter.kt 实现AsyncResponseFilter,是三者中最复杂的:它通过ResponseFilterCallbacks.resumeResponse()在任意异步上下文中恢复过滤链(类注释原文:“asynchronously triggers filter chain resumption”):
setResponseFilterCallbacks(callbacks)在回调中保存ResponseFilterCallbacks引用;onResponseHeaders若endStream,用Timer("AsyncResume", false).schedule(100) { callbacks.resumeResponse() }延迟 100ms 异步恢复;onResponseData返回StopIterationAndBuffer()并同样在流结束时异步恢复;onResponseTrailers出现即触发异步恢复;- 真正恢复时进入
onResumeResponse,在其中追加async-filter-demo: 1并返回FilterResumeStatus.ResumeIteration(builder.build(), data, trailers)。
该过滤器展示了 Envoy Mobile 对“异步回调中安全重入(re-entrancy)”的支持——过滤链的恢复可以由过滤器自身掌控时机,而不必局限于同步调用栈。
七、Bazel 目标结构与校验
应用的两个 Bazel 目标定义在 mobile/test/kotlin/apps/experimental/BUILD:
| 目标 | 规则 | 说明 |
|---|---|---|
hello_envoy_kt | android_binary | 最终 APK,custom_package = io.envoyproxy.envoymobile.helloenvoyexperimentaltest,应用//library:proguard_rules混淆规则 |
hello_envoy_kt_lib | kt_android_library | 编译四个 Kotlin 源文件与res/layout/activity_main.xml,依赖//:envoy_mobile_android、//examples/kotlin/shared:hello_envoy_shared_lib及 recyclerview/protobuf-javalite 等外部 artifact |
hello_envoy_kt_lint | detekt | Kotlin 静态检查目标,基于//:kotlin_lint_config配置 |
APK 安装启动后,可通过以下命令验证运行状态:
# 观察 Envoy 引擎与过滤器日志(TAG 分别为 MainActivity、DemoFilter 等) adb logcat -s MainActivity DemoFilter BufferDemoFilter AsyncDemoFilter # 预期可见:每秒一次的请求循环日志、filter-demo/buffer-filter-demo/async-filter-demo 响应头、 # 以及 "Envoy async internal setup completed" 引擎就绪日志八、常见问题与排查
ANDROID_HOME environment variable must be set.:脚本硬性退出,说明 SDK 路径未配置,设置环境变量后重跑即可。sdkmanager: command not found:$ANDROID_HOME/cmdline-tools/latest目录不存在,需先安装 Android 命令行工具。- 模拟器未就绪导致安装失败:
adb install在模拟器完全启动前执行会报device offline/no devices。README 已提示“等待模拟器完全启动”,可用adb wait-for-device或轮询adb shell getprop sys.boot_completed确认。 - 重复执行脚本:
avdmanager create ... --force覆盖同名 AVD、sdkmanager --install幂等、adb install -r覆盖安装,三处设计都保证脚本可重复执行。 - 构建失败:确认 Bazel 版本与仓库
mobile/MODULE.bazel声明一致,并提前拉取 Envoy 依赖(首次构建耗时较长)。
九、总结
从 mobile/test/kotlin/apps/experimental/README.md 的两条命令出发,本文完整还原了 Envoy Mobile 实验性 Kotlin 应用在 Android 模拟器上的运行链路:start_emulator.sh(镜像安装 → AVD 创建 → 模拟器启动)→start_app.sh(Bazel 构建 → adb 安装 → am start 拉起)。在此基础上,进一步剖析了 MainActivity.kt 的 Engine 装配(DNS 缓存、Socket Tagging、原生 buffer 过滤器、事件追踪等)与三个实验性过滤器(同步透传、缓冲恢复、异步回调恢复)的源码实现。对于希望在 Android 端深入 Envoy Mobile 过滤链编程与引擎配置的开发者,这个实验应用是最贴近生产 API 的入门样例——只需要一个配置好ANDROID_HOME的 Linux 环境、Bazel 与一个 x86_64 模拟器即可复现全部行为。
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考