# Android 编译与真机运行完整指南 > 本文档记录了从零开始搭建 Android 编译环境、修复 Config Plugin / Kotlin / 依赖冲突、到最终在真机上成功运行 debug APK 的完整流程。 > > 适用环境:Windows 11 + Expo SDK 54 + React Native 0.81 + PP-OCRv5 (ONNX Runtime)。 --- ## 目录 1. [环境准备](#1-环境准备) 2. [安装 Android SDK + NDK](#2-安装-android-sdk--ndk) 3. [ONNX 模型获取](#3-onnx-模型获取) 4. [expo prebuild](#4-expo-prebuild) 5. [Gradle 编译](#5-gradle-编译) 6. [安装到真机](#6-安装到真机) 7. [Metro + 运行](#7-metro--运行) 8. [常见问题与解决方案](#8-常见问题与解决方案) 9. [已修复的 Bug 清单](#9-已修复的-bug-清单) --- ## 1. 环境准备 ### JDK React Native 0.81 + Gradle 8.x 需要 **JDK 17+**。本项目使用 **JDK 21**。 ```bash # 验证 java -version # 应输出: java version "21.0.x" # 确认 JAVA_HOME 指向正确的 JDK echo $JAVA_HOME # 应输出: C:\Program Files\Java\jdk-21 ``` > **注意**:如果 `JAVA_HOME` 指向了别的用户的残留目录(如 `D:\Users\yxp\JDK`),需要通过「系统属性 → 环境变量 → 系统变量」修正为实际 JDK 路径。 ### Node.js ```bash node -v # 需要 v20+ npm -v ``` --- ## 2. 安装 Android SDK + NDK ### 方式 A:Android Studio(推荐) 1. 下载 Android Studio:https://developer.android.com/studio 2. 安装时勾选 Android SDK / SDK Platform / Android Virtual Device 3. 首次启动自动下载 SDK 到默认位置 `C:\Users\<用户>\AppData\Local\Android\Sdk` ### 方式 B:命令行(轻量) 如果已装 Android Studio 但缺少 cmdline-tools: ```bash SDK="$HOME/AppData/Local/Android/Sdk" TMP="/tmp" # 下载 cmdline-tools(官方,约 130MB) curl -L -o "$TMP/cmdline-tools.zip" \ "https://dl.google.com/android/repository/commandlinetools-win-11076708_latest.zip" # 解压到标准位置 mkdir -p "$SDK/cmdline-tools" unzip -q "$TMP/cmdline-tools.zip" -d "$TMP/" cp -r "$TMP/cmdline-tools" "$SDK/cmdline-tools/latest" ``` ### 必装组件 ```bash export JAVA_HOME="C:/Program Files/Java/jdk-21" export ANDROID_HOME="C:/Users/<用户>/AppData/Local/Android/Sdk" SDKMGR="$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager.bat" # 接受所有 license yes | "$SDKMGR" --licenses # 安装必需组件 yes | "$SDKMGR" \ "platform-tools" \ "platforms;android-35" \ "build-tools;35.0.0" ``` | 组件 | 用途 | |------|------| | `platform-tools` | adb(设备连接) | | `platforms;android-35` | 编译目标 SDK | | `build-tools;35.0.0` | aapt/dex/打包工具 | | `cmdline-tools;latest` | sdkmanager(expo prebuild 需要) | ### NDK(React Native 编译 C++ 需要) React Native 0.81 要求 **NDK 27.1.12297006**(~745MB)。 #### 国内推荐:腾讯云镜像下载 Google 官方源在国内经常断连导致 zip 损坏。**强烈推荐用腾讯云镜像**: ```bash SDK="$HOME/AppData/Local/Android/Sdk" TMP="/tmp" # 从腾讯云镜像下载(55MB/s 稳定,约 13 秒) curl -L -o "$TMP/ndk.zip" \ "https://mirrors.cloud.tencent.com/AndroidSDK/android-ndk-r27b-windows.zip" # 解压 mkdir -p "$TMP/ndk-extract" unzip -q "$TMP/ndk.zip" -d "$TMP/ndk-extract" # 安装到标准位置 mv "$TMP/ndk-extract/android-ndk-r27b" "$SDK/ndk/27.1.12297006" ``` #### expo-sqlite 的 NDK 版本冲突 `expo-sqlite` 可能要求 NDK `27.0.12077973`(不同于 RN 要求的 `27.1.12297006`)。 解决方式:用 Windows 目录联接(junction)让两个版本指向同一份 NDK: ```powershell # 在 PowerShell 中执行 cmd /c mklink /J ` "C:\Users\<用户>\AppData\Local\Android\Sdk\ndk\27.0.12077973" ` "C:\Users\<用户>\AppData\Local\Android\Sdk\ndk\27.1.12297006" ``` > 两个版本号 ABI/API 兼容,联接不会导致编译问题。 #### NDK 完整性验证 NDK 解压后必须验证 arm64 目标库完整,否则 CMake 会报 "clang broken": ```bash SDK="$HOME/AppData/Local/Android/Sdk" NDK="$SDK/ndk/27.1.12297006" # 关键文件必须存在 ls "$NDK/sysroot/usr/lib/aarch64-linux-android/24/crtbegin_dynamic.o" \ "$NDK/sysroot/usr/lib/aarch64-linux-android/libc.a" \ "$NDK/toolchains/llvm/prebuilt/windows-x86_64/lib/clang/18/lib/linux/libclang_rt.builtins-aarch64-android.a" ``` > ⚠️ 如果 C 盘空间不足(NDK 解压后约 2.3GB),解压会中途截断,导致 `libc.a` 等文件缺失。确保至少有 **5GB 可用空间**。 --- ## 3. ONNX 模型获取 本项目使用 PP-OCRv5 + ONNX Runtime(替代 NCNN,见 plan.md 决策 7)。 ### 下载社区 ONNX 模型 ```bash cd plugins/ppocr/assets mkdir -p . && cd . # det 模型(4.8 MB) curl -L -o ppocrv5_det.onnx \ "https://huggingface.co/ilaylow/PP_OCRv5_mobile_onnx/resolve/main/ppocrv5_det.onnx" # rec 模型(16.6 MB) curl -L -o ppocrv5_rec.onnx \ "https://huggingface.co/ilaylow/PP_OCRv5_mobile_onnx/resolve/main/ppocrv5_rec.onnx" # CJK 字典(26 KB,PaddleOCR 标准字典) curl -L -o ppocr_keys_v1.txt \ "https://raw.githubusercontent.com/PaddlePaddle/PaddleOCR/release/2.6/ppocr/utils/ppocr_keys_v1.txt" ``` ### 验证模型 I/O 规格 ```python import onnx for name in ['det', 'rec']: m = onnx.load(f'plugins/ppocr/assets/ppocrv5_{name}.onnx') print(f'{name}:') for i in m.graph.input: shape = [d.dim_value or d.dim_param for d in i.type.tensor_type.shape.dim] print(f' input {i.name}: {shape}') for o in m.graph.output: shape = [d.dim_value or d.dim_param for d in o.type.tensor_type.shape.dim] print(f' output {o.name}: {shape}') ``` 预期输出: - det: 输入 `x: [N, 3, H, W]`(动态),输出 `[N, 1, H, W]` - rec: 输入 `x: [N, 3, 48, W]`(高固定 48),输出 `[N, T, 18385]` --- ## 4. expo prebuild `expo prebuild` 把 Config Plugin 注入到原生工程(生成 `android/` 目录)。 ```bash export JAVA_HOME="C:/Program Files/Java/jdk-21" export ANDROID_HOME="C:/Users/<用户>/AppData/Local/Android/Sdk" npx expo prebuild --platform android --no-install ``` ### 验证注入结果 ```bash # Kotlin 源码(应有 9 个 .kt 文件) find android/app/src/main/java/com/beancount -name "*.kt" | sort # ONNX 模型 + 字典 ls android/app/src/main/assets/ # MainApplication 里 OcrPackage 注册 grep "OcrPackage" android/app/src/main/java/com/example/beanmobile/MainApplication.kt # onnxruntime gradle 依赖 grep "onnxruntime" android/app/build.gradle # AndroidManifest 服务注册 grep "Billing" android/app/src/main/AndroidManifest.xml ``` ### android/ 目录锁定问题(Windows + ZCode) 如果 `android/` 目录被进程锁定导致 prebuild 无法 `rmdir`,在临时副本项目中 prebuild 再 robocopy 回来: ```bash # 1. 创建临时副本 TMP="/tmp/bm-prebuild" mkdir -p "$TMP" cp app.json package.json package-lock.json tsconfig.json "$TMP/" cp -r app/ src/ plugins/ "$TMP/" # node_modules 用 junction 共享 powershell -NoProfile -Command "cmd /c mklink /J '$TMP/node_modules' '$(pwd)/node_modules'" # 2. 在临时项目 prebuild cd "$TMP" npx expo prebuild --platform android --no-install --clean # 3. robocopy 回原项目(能处理锁定) powershell -NoProfile -Command "robocopy '$TMP/android' '$(pwd -W || pwd)/android' /E /PURGE" ``` --- ## 5. Gradle 编译 ### 前提:onnxruntime 版本修正 Config Plugin 注入的 `app/build.gradle` 中 onnxruntime 版本可能不匹配,每次 prebuild 后需检查: ```bash # 确认是 1.20.0(不是 1.20.1,后者不存在) grep "onnxruntime" android/app/build.gradle # 如果是 1.20.1,修正: sed -i 's/onnxruntime-android:1.20.1/onnxruntime-android:1.20.0/' android/app/build.gradle ``` ### 编译 Kotlin(快速验证,不打包) ```bash cd android export JAVA_HOME="C:/Program Files/Java/jdk-21" export ANDROID_HOME="C:/Users/<用户>/AppData/Local/Android/Sdk" ./gradlew :app:compileDebugKotlin --no-daemon ``` ### 完整打包 APK ```bash ./gradlew :app:assembleDebug --no-daemon ``` APK 输出位置:`android/app/build/outputs/apk/debug/app-debug.apk`(~250MB) ### Gradle 缓存清理(遇到诡异问题时) ```bash # 删 build 缓存 rm -rf app/build build app/.cxx # 彻底重来(连 Gradle daemon 也杀) powershell -NoProfile -Command "Get-Process -Name 'java' -ErrorAction SilentlyContinue | Stop-Process -Force" ``` --- ## 6. 安装到真机 ### 开启 USB 调试 1. 手机「设置 → 关于手机」→ 连续点击「MIUI 版本」7 次 → 开启开发者选项 2. 「设置 → 更多设置 → 开发者选项」→ 打开: - ✅ USB 调试 - ✅ **USB 安装**(红米/小米必须开,否则 `adb install` 被拒为 `INSTALL_FAILED_USER_RESTRICTED`) 3. USB 连接电脑(文件传输模式) ### adb install ```bash export ANDROID_HOME="C:/Users/<用户>/AppData/Local/Android/Sdk" ADB="$ANDROID_HOME/platform-tools/adb" # 确认设备连接 "$ADB" devices # 应显示 device 状态 # 安装 "$ADB" install -r android/app/build/outputs/apk/debug/app-debug.apk ``` --- ## 7. Metro + 运行 Debug APK 需要从 Metro server 加载 JS bundle(不内嵌 JS)。 ### 启动 Metro ```bash npx expo start --dev-client --port 8081 ``` ### adb reverse(USB 连接,手机和电脑不在同一 WiFi 时必需) ```bash "$ADB" reverse tcp:8081 tcp:8081 ``` ### 启动 app ```bash # 命令行启动 "$ADB" shell am start -n com.example.beanmobile/.MainActivity # 或直接在手机桌面点击「Bean Mobile」图标 ``` ### 验证 ```bash # 进程存活? "$ADB" shell pidof com.example.beanmobile # JS 日志 "$ADB" logcat -d | grep "ReactNativeJS" # 截图 "$ADB" shell screencap -p /sdcard/screen.png "$ADB" pull /sdcard/screen.png /tmp/app-screen.png ``` --- ## 8. 常见问题与解决方案 ### `withAndroidMainApplication is not a function` **原因**:SDK 54 的 `@expo/config-plugins` 没有 `withAndroidMainApplication` / `withAndroidGradle`。 **解决**:改用 `withMainApplication` + `withAppBuildGradle` + `withDangerousMod`。 ### `Cannot read properties of undefined (reading '0')` in AndroidManifest **原因**:`manifest.application` 在 prebuild 初始模板里可能不存在。 **解决**:操作前加防御性检查: ```javascript if (!Array.isArray(manifest.application) || manifest.application.length === 0) { manifest.application = [{ $: {} }]; } ``` ### `modResults` 结构搞错导致 manifest 重复 `` 标签 **原因**:`withAndroidManifest` 的 `modResults` 结构是 `{ manifest: { ... } }`,不是直接就是 manifest。 **解决**:所有权限和 application 操作通过 `modConfig.modResults.manifest`,不是 `modConfig.modResults` 本身。 ### MainApplication 里 import OcrPackage 没注入 **原因**:Kotlin 的 package 声明**没有分号**(`package com.example.beanmobile`,不是 `package com.example.beanmobile;`),正则 `^(package [\w.]+;)` 匹配不到。 **解决**:正则去掉分号要求:`^(package\s+[\w.]+;?\s*)$`。 ### Config Plugin 返回 undefined 导致整个 config 丢失 **原因**:插件函数最后漏了 `return config;`,导致 `withAndroidManifest` 的结果没返回,config 变成 undefined,后续所有插件崩溃。 **解决**:每个插件函数末尾必须有 `return config;`。 ### `NDK at ...27.0.12077973 did not have a source.properties file` **原因**:expo-sqlite 要求 NDK 27.0,但只装了 27.1;Gradle 自动下载 27.0 失败留下空目录。 **解决**:用 `mklink /J` 创建目录联接(见第 2 节)。 ### NDK `clang broken` / `cannot open crtbegin_dynamic.o` **原因**:NDK 解压时磁盘空间不足,arm64 目标库被截断。 **解决**:确保至少 5GB 可用空间,重新从腾讯云镜像下载并解压。 ### `onnxruntime-android:1.20.1` not found **原因**:Maven Central 上最新稳定版是 `1.20.0`,`1.20.1` 不存在。 **解决**:`sed -i 's/1.20.1/1.20.0/' android/app/build.gradle` ### `NoSuchMethodError: getDirectConverter` (expo-font 崩溃) **原因**:`@expo/vector-icons@15.x` 拉进了 `expo-font@57.0.0`(SDK 55 版本),与 `expo-modules-core@3.0.x`(SDK 54)不兼容。 **解决**:在 `package.json` 加 npm override 强制降级: ```json "overrides": { "expo-font": "~14.0.12" } ``` ### `Unable to resolve "expo-linking"` **原因**:expo-router 的 peer dependencies 没有全部安装。 **解决**:补全所有 peer deps: ```bash npm install --legacy-peer-deps \ @expo/metro-runtime expo-constants expo-linking \ react-dom react-native-gesture-handler react-native-reanimated \ @react-navigation/drawer ``` ### `INSTALL_FAILED_USER_RESTRICTED` (红米/小米) **原因**:MIUI 的「USB 安装」开关默认关闭。 **解决**:开发者选项里打开「USB 安装」。 ### `EBUSY: resource busy or locked, rmdir android/` **原因**:ZCode 的子进程(NodeBabyLinkService)持有 `android/` 目录句柄,`rmdir` 失败。 **解决**:在临时副本项目里 prebuild,再用 `robocopy /E /PURGE` 复制回原项目(见第 4 节)。 ### `attribute android:canTakeScreenshots not found` **原因**:`canTakeScreenshots` 是 API 30+ 属性,部分 compileSdk 配置下 AAPT 不识别。 **解决**:从 `accessibility_service_config.xml` 删除该属性(非必需,截图功能在 Kotlin 代码里实现)。 --- ## 9. 已修复的 Bug 清单 以下是真机编译过程中暴露并修复的全部 bug(共 26 个): ### Config Plugin Bug(7 个) | 文件 | Bug | 修复 | |------|-----|------| | `plugins/ppocr/app.plugin.js` | `withAndroidMainApplication` 不存在 | 改用 `withMainApplication` + `withAppBuildGradle` + `withDangerousMod` | | `plugins/ppocr/app.plugin.js` | `withDangerousMod` 回调里 `modConfig.platformProjectRoot` undefined | 改为 `modConfig.modRequest.platformProjectRoot` | | `plugins/*/app.plugin.js`(3 个) | `manifest.application[0]` 崩溃(prebuild 初始 manifest 无 application 节点) | 加 `Array.isArray` 防御 | | `plugins/ppocr/app.plugin.js` | MainApplication import 没注入(Kotlin package 无分号) | 正则去掉分号要求 | | `plugins/accessibility/app.plugin.js` | `return config` 丢失导致整个 config 变 undefined | 补回 `return config` | ### Kotlin 编译 Bug(11 个) | 文件 | Bug | 修复 | |------|-----|------| | `FloatingTip.kt:80` | `val layoutParams` 不可重赋值 + 类型不匹配 | 拆出 apply 块 + 用 LinearLayout.LayoutParams | | `OcrTileService.kt:50` | `startActivityAndCollapse(intentSender)` 参数不匹配 | 改传 PendingIntent | | `OcrModule.kt:179` | `putDouble(confidence)` Float 不匹配 | `.toDouble()` | | `OcrModule.kt:215,235` | `longArrayOf(1,3,...)` Int vs Long | 显式 `.toLong()` + `L` 后缀 | | `OcrModule.kt:221,242` | `detOutputs.forEach{it.close()}` 未解析 | 改用 `detOutputs.close()`(OrtSession.Result.close) | | `OcrModule.kt:333` | det 输出维度多了 channel 维,`prob[y][x]` 是 FloatArray 不是 Float | `[0][0]` 剥掉 N+C 两维,函数签名改 `Array` | ### 资源/环境 Bug(8 个) | 问题 | 修复 | |------|------| | cmdline-tools 缺失 | 从 Google 官方下载安装 | | platform 35 缺失 | sdkmanager 安装 | | NDK 27.0 vs 27.1 版本冲突 | `mklink /J` 目录联接 | | NDK 下载损坏(Google 源断连) | 腾讯云镜像替代 | | `accessibility_service_config.xml` canTakeScreenshots 属性 | 删除(非必需) | | `onnxruntime:1.20.1` 不存在 | 改用 1.20.0 | | expo-font 57.0 vs 14.0 版本冲突 | npm overrides 强制降级 | | expo-router peer deps 缺失 | 补全 8 个依赖包 | --- ## 附录:完整一键流程 ```bash # === 环境变量(每次新终端都要设) === export JAVA_HOME="C:/Program Files/Java/jdk-21" export ANDROID_HOME="C:/Users/$USER/AppData/Local/Android/Sdk" # === 1. 依赖 === npm install --legacy-peer-deps # === 2. prebuild === npx expo prebuild --platform android --no-install sed -i 's/onnxruntime-android:1.20.1/onnxruntime-android:1.20.0/' android/app/build.gradle # === 3. 编译 === cd android ./gradlew :app:assembleDebug --no-daemon # === 4. 安装 === cd .. "$ANDROID_HOME/platform-tools/adb" install -r android/app/build/outputs/apk/debug/app-debug.apk # === 5. Metro + 运行 === npx expo start --dev-client --port 8081 & sleep 10 "$ANDROID_HOME/platform-tools/adb" reverse tcp:8081 tcp:8081 "$ANDROID_HOME/platform-tools/adb" shell am start -n com.example.beanmobile/.MainActivity ```