React Native + Detox:Android 模拟器运行测试排查
AI 速览
这篇文章讲述了从 iOS 切换到 Android 模拟器时遇到的一个典型排查误区。App 启动后卡在 Expo Dev Client 页面,
adb logcat中充斥着libappmodules.so加载失败的错误日志,很容易让人误以为是 native 模块编译问题。但这其实是一个误导信息——这些错误在 Debug 模式下本就存在且无害。真正的问题藏在 Android 模拟器的网络隔离机制中:模拟器运行在独立的 QEMU 虚拟网络中,无法直接访问宿主机的localhost:8081。解决方案是使用adb reverse进行端口转发。文章的核心教训是:排查时要区分”噪音日志”和”关键日志”,不要被非致命错误带偏方向
系列文章导航
- 架构设计与 P00 页面巡检实践 ← 推荐先读
- iOS 模拟器环境配置
- Android AOSP 模拟器配置
- Android 模拟器运行测试排查 (本文)
- 基于 CDP 的信号同步机制
- Bridge 层代码注入实践
- 页面巡检的健康检查与视觉监控
环境与目标
| 组件 | 版本 |
| React Native | 0.74.5 (Expo SDK 51) |
| Detox | 20.42.0 |
| expo-dev-client | ~4.0.26 |
| Android Emulator | Pixel 7 API 35 (AOSP) |
| Android Studio | Ladybug Feature Drop 2024.2 |
目标:在本地 Android AOSP 模拟器上成功运行 E2E 测试。
测试策略:
- 本地开发:iOS 模拟器(开发体验好)
- 本地验证:Android 模拟器(覆盖双平台)
- 云端 CI:BrowserStack Android(成本考量)
问题概述
完成 iOS E2E 测试配置后,切换到 Android 模拟器运行测试,App 启动后卡在 Expo Dev Client 的 “Development servers” 页面,Metro 终端无任何 Bundle 日志。
与 iOS 的现象类似,但根因完全不同。
问题 1:libappmodules.so 加载失败
现象
使用 adb logcat 排查时,发现大量 native 加载错误:
E linker : library "/data/app/~~xxx/lib/arm64/libappmodules.so" not found
E ReactNativeJNI: Error loading libappmodules.so排查过程
- 误判方向:以为是 native 模块编译问题
- 检查
android/app/build.gradle的ndk.abiFilters - 检查 Hermes 引擎配置
- 尝试
./gradlew clean重新构建
结论
这是一个误导信息。libappmodules.so 在 Debug 模式下本就不存在,这些日志是正常的。
关键教训:排查时要区分”噪音日志”和”关键日志”,不要被非致命错误带偏方向。
问题 2:模拟器存储空间不足
现象
构建完成后安装 APK 失败:
INSTALL_FAILED_INSUFFICIENT_STORAGE原因
Android 模拟器默认配置:
- Internal Storage: 2 GB(系统 + 应用)
- SD Card: 512 MB
Debug 构建的 APK 体积较大(含完整 Hermes bytecode、Source Map),加上系统占用,2 GB 很快耗尽。
解决方案
在 Android Studio 中调整模拟器存储配置:
- Device Manager → 选中模拟器 → Edit
- Show Advanced Settings
- 调整存储参数:
| 参数 | 原值 | 建议值 |
| Internal Storage | 2048 | 8192 |
| SD Card | 512 | 2048 |
- Wipe Data 重置模拟器(必须)
注意:修改存储配置后必须 Wipe Data,否则新配置不生效。
问题 3:Android 模拟器网络隔离(核心问题)
现象
存储问题解决后,App 依然卡在 “Development servers” 页面。使用 iOS 成功的方案(Deep Link + localhost),在 Android 上完全无效。
根因分析
Android 模拟器运行在虚拟化网络环境中,与宿主机存在网络隔离。
Android Emulator 内部使用 localhost 无法访问宿主机上的 Metro Server,必须使用特殊地址 10.0.2.2 才能正确访问宿主机的网络服务。这是 Android 模拟器网络隔离机制导致的典型问题。
关键概念:
| 地址 | 在模拟器中指向 | 说明 |
| 127.0.0.1 | 模拟器自身 | 标准回环地址,隔离在虚拟网络中 |
| localhost | 模拟器自身 | 解析为 127.0.0.1 |
| 10.0.2.2 | 宿主机 127.0.0.1 | Android 模拟器专用的宿主机别名 |
| 10.0.2.1 | 虚拟路由器 | 模拟器默认网关 |
| 10.0.2.3 | DNS 服务器 | 模拟器使用的 DNS |
这是 Android 模拟器的 QEMU 网络栈设计,10.0.2.0/24 是保留的虚拟网段。
参考文档:Android Emulator Networking
iOS vs Android 对比
问题 4:ADB Reverse 端口转发
概念说明
adb reverse 是 Android 调试桥提供的端口反向转发机制:
adb reverse tcp:8081 tcp:8081这条命令的含义:
模拟器内部访问 localhost:8081 → 转发到 → 宿主机 localhost:8081执行后,模拟器内的 App 访问 localhost:8081 就能到达宿主机的 Metro 服务器。
Detox 内置支持
好消息是 Detox 提供了 reversePorts 配置项,自动执行 adb reverse:
// .detoxrc.js
module.exports = {
apps: {
'android.emu.debug': {
type: 'android.apk',
binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
reversePorts: [8081], // 自动执行 adb reverse tcp:8081 tcp:8081
},
},
};原理:Detox 在 device.launchApp() 时自动调用 adb reverse,无需手动执行。
参考文档:Detox Android Configuration - reversePorts
问题 5:Expo Dev Client Deep Link 格式
URL Scheme 构造
Expo Dev Client 的 Deep Link 格式:
exp+{slug}://expo-development-client/?url={metroUrl}其中:
{slug}来自app.config.ts中的slug字段{metroUrl}是 Metro 服务器地址(需 URL 编码)
Android 特殊处理
Android 版本需要使用 10.0.2.2 替代 localhost:
// iOS
const metroUrl = 'http://localhost:8081';
// Android
const metroUrl = 'http://10.0.2.2:8081';参考文档:Expo Deep Linking
问题 6:Android 必须在启动时传递 URL
iOS vs Android 启动差异
在 iOS 上,使用”两阶段启动”解决 Expo Dev Client 时序问题:
// iOS: 两阶段启动
await device.launchApp({ newInstance: true }); // 阶段一:启动
await sleep(3000); // 等待初始化
await device.openURL({ url: deepLinkUrl }); // 阶段二:发送 URL但在 Android 上,device.openURL() 方法行为不同:
实测发现,Android 上分阶段发送 Deep Link 并不可靠。
解决方案
Android 必须在 launchApp 时直接传递 URL:
// Android: 一次性启动 + URL
await device.launchApp({
newInstance: true,
url: deepLinkUrl, // 必须在启动时传递
launchArgs: { ... },
});完整解决方案
jest.setup.ts 核心配置
// ============================================
// 配置常量
// ============================================
const CONFIG = {
METRO_PORT: 8081,
SCHEMA: 'exp+moego-business-2', // 来自 app.config.ts 的 slug
LAUNCH_ARGS: {
detoxDebugVisibility: 'YES',
EXPO_PUBLIC_IS_E2E: 'true',
},
IOS_INIT_DELAY: 3000, // iOS 两阶段启动等待时间
};
const PERMISSIONS = {
location: 'inuse',
notifications: 'YES',
camera: 'YES',
photos: 'YES',
};
// ============================================
// Deep Link 构造函数
// ============================================
/**
* 构建 Metro Deep Link URL
* - iOS 使用 localhost(共享宿主机网络栈)
* - Android 使用 10.0.2.2(QEMU 虚拟网络代理地址)
*/
function buildMetroDeepLink(platform: 'ios' | 'android'): string {
const metroHost = platform === 'ios' ? 'localhost' : '10.0.2.2';
const metroUrl = `http://${metroHost}:${CONFIG.METRO_PORT}`;
const searchParams = new URLSearchParams();
searchParams.set('url', metroUrl);
// 附加 launchArgs 到 URL
Object.entries(CONFIG.LAUNCH_ARGS).forEach(([key, value]) => {
searchParams.set(key, String(value));
});
return `${CONFIG.SCHEMA}://expo-development-client/?${searchParams.toString()}`;
}
// ============================================
// 平台特定启动逻辑
// ============================================
/**
* iOS 启动:两阶段策略
* 解决 Expo Dev Client + Detox 的时序问题
*/
async function launchIosDebugMode() {
// 阶段一:启动 App,不带 URL
await device.launchApp({
newInstance: true,
permissions: PERMISSIONS,
launchArgs: CONFIG.LAUNCH_ARGS,
});
// 等待 Expo Dev Client 原生模块初始化
log.info('iOS: Waiting for Expo Dev Client initialization...');
await new Promise((resolve) => setTimeout(resolve, CONFIG.IOS_INIT_DELAY));
// 阶段二:发送 Deep Link 触发 Metro 连接
const deepLinkUrl = buildMetroDeepLink('ios');
log.info('iOS: Sending Deep Link to trigger Metro connection...');
await device.openURL({ url: deepLinkUrl });
}
/**
* Android 启动:一次性策略
* Android 必须在 launchApp 时传递 URL
*/
async function launchAndroidDebugMode() {
const deepLinkUrl = buildMetroDeepLink('android');
log.info(`Android: Launching with Deep Link: ${deepLinkUrl}`);
await device.launchApp({
newInstance: true,
url: deepLinkUrl, // Android 必须在启动时传递 URL
permissions: PERMISSIONS,
launchArgs: CONFIG.LAUNCH_ARGS,
});
}
// ============================================
// 统一入口
// ============================================
beforeAll(async () => {
const platform = device.getPlatform();
log.info(`Platform: ${platform}`);
if (platform === 'ios') {
await launchIosDebugMode();
} else {
await launchAndroidDebugMode();
}
// 等待 JS Bundle 加载完成
await waitFor(element(by.text('Log in')))
.toBeVisible()
.withTimeout(60000);
log.info('App launched and ready for testing');
});Detox 配置 (.detoxrc.js)
module.exports = {
testRunner: {
args: {
$0: 'jest',
config: 'e2e/jest.config.js',
},
},
apps: {
'ios.sim.debug': {
type: 'ios.app',
binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/business.app',
build: 'xcodebuild -workspace ios/moegomobile.xcworkspace -scheme business -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build',
},
'android.emu.debug': {
type: 'android.apk',
binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
build: 'cd android && ./gradlew assembleDebug',
reversePorts: [8081], // 关键:自动执行 adb reverse
},
},
devices: {
simulator: {
type: 'ios.simulator',
device: { type: 'iPhone 16' },
},
emulator: {
type: 'android.emulator',
device: { avdName: 'Pixel_7_API_35' },
},
},
configurations: {
'ios.sim.debug': {
device: 'simulator',
app: 'ios.sim.debug',
},
'android.emu.debug': {
device: 'emulator',
app: 'android.emu.debug',
},
},
};快速排查检查清单
技术总结
Android 模拟器网络架构

核心要点
网络隔离:Android 模拟器运行在 QEMU 虚拟化环境,
localhost指向模拟器自身而非宿主机10.0.2.2 特殊地址:这是 Android 模拟器预留的地址,自动映射到宿主机的
127.0.0.1ADB Reverse:
adb reverse tcp:8081 tcp:8081创建反向隧道,让模拟器内的localhost:8081转发到宿主机Detox reversePorts:配置后自动执行
adb reverse,无需手动干预启动时序差异:
- iOS:两阶段启动(先启动后发 URL)
- Android:一次性启动(URL 必须在
launchApp时传递)
参考文档
在排查过程中查阅并验证有效的官方文档:
Android 模拟器网络基础 (
10.0.2.2)- Set up Android Emulator networking
- 验证点:官方文档写了
10.0.2.2是访问宿主机回环接口的特殊别名,可以硬编码
ADB 端口转发 (
reversePorts)- Android Debug Bridge (adb) - port forwarding
- 验证点:Detox 的
reversePorts底层就是调用的adb reverse
Android 9+ 网络安全配置
- If you do see your app running on the device
- 验证点:确认了
AndroidManifest.xml中有开启usesCleartextTraffic="true"
模拟器存储管理
- Create and manage virtual devices
- 验证点:修改 Advanced Settings 中的 Internal Storage 可以解决内存不足安装失败