- 切换到 expo-router 文件路由,删除 App.tsx - 新增 5 个 Expo 原生插件:ppocr (OCR), accessibility (账单抓取), notification-listener, screenshot-monitor, sms-receiver - 实现核心领域逻辑:billPipeline (账单流水), dedup (去重), transferRecognizer (转账识别), ruleEngine + categories (双轨制分类), budgets, creditCards, recurring, sync, ocrProcessor - 增强 ledger.ts:支持 balance assertion, option, pad/note 指令, posting 级 metadata, cost/price 解析 - 新增完整 UI:tabs (首页/报表/设置), 交易详情, 预算, 日历热力图, 分类管理, 信用卡, 定期交易, 规则管理 - 实现 Zustand 状态管理:ledgerStore, importStore, settingsStore, metadataStore, automationStore + 持久化 - 新增 AI 功能:chatAssistant, monthlySummary, voiceInput - 实现多端同步:gitSync, webdavSync, icloudSync - 新增主题系统 (tokens/presets) 和 i18n (zh/en) - 添加 30+ 单元测试覆盖核心逻辑
16 KiB
Android 编译与真机运行完整指南
本文档记录了从零开始搭建 Android 编译环境、修复 Config Plugin / Kotlin / 依赖冲突、到最终在真机上成功运行 debug APK 的完整流程。
适用环境:Windows 11 + Expo SDK 54 + React Native 0.81 + PP-OCRv5 (ONNX Runtime)。
目录
1. 环境准备
JDK
React Native 0.81 + Gradle 8.x 需要 JDK 17+。本项目使用 JDK 21。
# 验证
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
node -v # 需要 v20+
npm -v
2. 安装 Android SDK + NDK
方式 A:Android Studio(推荐)
- 下载 Android Studio:https://developer.android.com/studio
- 安装时勾选 Android SDK / SDK Platform / Android Virtual Device
- 首次启动自动下载 SDK 到默认位置
C:\Users\<用户>\AppData\Local\Android\Sdk
方式 B:命令行(轻量)
如果已装 Android Studio 但缺少 cmdline-tools:
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"
必装组件
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 损坏。强烈推荐用腾讯云镜像:
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 中执行
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":
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 模型
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 规格
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/ 目录)。
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
验证注入结果
# 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 回来:
# 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 后需检查:
# 确认是 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(快速验证,不打包)
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
./gradlew :app:assembleDebug --no-daemon
APK 输出位置:android/app/build/outputs/apk/debug/app-debug.apk(~250MB)
Gradle 缓存清理(遇到诡异问题时)
# 删 build 缓存
rm -rf app/build build app/.cxx
# 彻底重来(连 Gradle daemon 也杀)
powershell -NoProfile -Command "Get-Process -Name 'java' -ErrorAction SilentlyContinue | Stop-Process -Force"
6. 安装到真机
开启 USB 调试
- 手机「设置 → 关于手机」→ 连续点击「MIUI 版本」7 次 → 开启开发者选项
- 「设置 → 更多设置 → 开发者选项」→ 打开:
- ✅ USB 调试
- ✅ USB 安装(红米/小米必须开,否则
adb install被拒为INSTALL_FAILED_USER_RESTRICTED)
- USB 连接电脑(文件传输模式)
adb install
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
npx expo start --dev-client --port 8081
adb reverse(USB 连接,手机和电脑不在同一 WiFi 时必需)
"$ADB" reverse tcp:8081 tcp:8081
启动 app
# 命令行启动
"$ADB" shell am start -n com.example.beanmobile/.MainActivity
# 或直接在手机桌面点击「Bean Mobile」图标
验证
# 进程存活?
"$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 初始模板里可能不存在。
解决:操作前加防御性检查:
if (!Array.isArray(manifest.application) || manifest.application.length === 0) {
manifest.application = [{ $: {} }];
}
modResults 结构搞错导致 manifest 重复 <root> 标签
原因: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 强制降级:
"overrides": {
"expo-font": "~14.0.12"
}
Unable to resolve "expo-linking"
原因:expo-router 的 peer dependencies 没有全部安装。
解决:补全所有 peer deps:
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<FloatArray> |
资源/环境 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 个依赖包 |
附录:完整一键流程
# === 环境变量(每次新终端都要设) ===
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