Skip to content

技术探索全过程记录

🕒 Published at:

技术探索全过程记录 ​

项目目标 ​

在 Android 手机上运行 One API(AI API 中转站),要求:

  • 无 Root
  • 纯 APK 方案(不接受 Termux)
  • 可启停
  • 有 GUI 管理界面

环境 ​

  • 电脑: Windows, 192.168.31.x (EasyTier 192.168.123.1)
  • 手机: Android (API 28+), 连接便携 WiFi (Symmetric NAT)
  • 网络: EasyTier v2.6.4 组网, relay 中继 128-150ms

Phase 1: EasyTier 组网 (成功) ​

电脑端 ​

  • 程序: D:\devtools\easytier-gui\easytier-gui.exe v2.6.4
  • 网络: daqingda, IP 192.168.123.1/24, MTU 1360
  • 公网中继服务器 (4个):
    • et.gbc.moe:11010
    • easytier.weiai.org.cn:11010
    • boi.de5.net:11010
    • ros.scpsl.com.cn:11010

手机端 ​

  • Android App 配置相同网络
  • 通过 relay 中继连接, 128-150ms 延迟
  • 手机 IP: 192.168.123.2

问题: SOCKS5 代理无效 ​

EasyTier v2.6.4 的 SOCKS5 代理在 Android 上有 bug(Issue #1825),导致无法使用 SOCKS5 穿透。不走 SOCKS5 也能正常通信。


Phase 2: Termux + proot-distro (成功但用户不满足) ​

方案 ​

在 Termux 内安装 proot-distro Ubuntu, 然后运行 One API。

步骤 ​

  1. Termux 安装 proot-distro
  2. proot-distro install ubuntu (Ubuntu 26.04)
  3. 进入 Ubuntu: proot-distro login ubuntu
  4. 安装 Go, 编译 One API
  5. 运行: ./one-api --port 3000
  6. PM2 管理进程生命周期

结果 ​

  • One API 正常运行在 Termux proot 环境中
  • 电脑浏览器通过 EasyTier 访问 http://192.168.123.2:3000 (root/123456)
  • PM2 实现开机自启和进程守护

为什么放弃 ​

  • 用户要求纯 APK 方案
  • Termux 需要单独安装和配置
  • 界面不够优雅

Phase 3: 首次交叉编译尝试 ​

环境 ​

  • Go 1.24.1
  • 目标: Android ARM64 (arm64-v8a)
  • 项目: github.com/songquanpeng/one-api (使用 go-sqlite3, CGO 依赖)

第一次尝试: CGO_ENABLED=0 ​

$env:CGO_ENABLED="0"
$env:GOOS="android"
$env:GOARCH="arm64"
go build -o one-api-arm64

结果: 编译成功, 但运行时崩溃 原因: go-sqlite3 需要 CGO, CGO_ENABLED=0 时使用 stub, 数据库操作报错

第二次尝试: NDK 22 + CGO_ENABLED=1 ​

$env:CC="<NDK22>/toolchains/llvm/prebuilt/windows-x86_64/bin/aarch64-linux-android21-clang"
$env:CGO_ENABLED="1"

结果: 链接失败, undefined reference to pthread_create 等 原因: NDK 22 太老, 没有 Go 1.24 需要的 pthread 符号

第三次尝试: NDK r27c + CGO_ENABLED=1 ​

下载 NDK r27c (27.0.12077973)

结果: 编译成功, 但运行时还是有问题

  • 需要 -buildmode=pie (Android 5.0+ 要求 PIE)
  • 需要 netgo osusergo 标签 (但 netgo 后来发现导致 DNS 问题)
  • 需要 -extldflags "-Wl,-z,max-page-size=4096"

最终参数:

CC=<NDK>/bin/aarch64-linux-android31-clang.cmd
CGO_ENABLED=1 GOOS=android GOARCH=arm64
go build -tags 'netgo osusergo' -buildmode=pie
  -ldflags '-s -w -extldflags "-Wl,-z,max-page-size=4096"'

Phase 4: TLS Alignment Bug ​

症状 ​

编译出的 32MB 二进制在 Android 上运行时 闪退, logcat 无明确错误。

根因 ​

ARM64 Android Bionic libc 要求 PT_TLS segment 的 p_align 必须是 64。 Go 1.24.1 交叉编译出的 ELF 中 PT_TLS p_align=8, 不兼容。

修复方式 ​

编写 align_fix.py 修补二进制:

python
# 读取 ELF header, 找到 Program Headers
# 扫描 p_type==7 (PT_TLS)
# 如果 p_align < 64, 改为 64
with open(sys.argv[1], 'r+b') as f:
    hdr = f.read(16)
    if hdr[4] == 2:  # ELF64
        f.seek(32)
        offset = struct.unpack('<Q', f.read(8))[0]  # e_phoff
        f.seek(54)
        phsize = struct.unpack('<H', f.read(2))[0]  # e_phentsize
        phnum = struct.unpack('<H', f.read(2))[0]   # e_phnum
        for i in range(phnum):
            f.seek(offset + i * phsize)
            t = struct.unpack('<I', f.read(4))[0]
            if t == 7:  # PT_TLS
                f.seek(44, 1)
                align = struct.unpack('<Q', f.read(8))[0]
                if align < 64:
                    f.seek(-8, 1)
                    f.write(struct.pack('<Q', 64))

用法: python align_fix.py one-api-android

关于 Go 官方修复 ​

GitHub Issue: golang/go#68541 (ARM64 Android TLS alignment) 预计 Go 1.25 会修复此问题, 届时不再需要 align_fix.py。


Phase 5: 初步 APK 构建 (纯 WebView) ​

方案 ​

简单 Android 项目, 布局是 WebView (加载 http://127.0.0.1:3000), 启动时 OneApiService 在后台执行二进制。

核心组件 ​

  • MainActivity.java: WebView 全屏, 启动时 startForegroundService
  • OneApiService.java: 提取 token file → spawn 二进制 → 读取 stdout

问题: Token Encoder 下载失败 ​

[FATAL] failed to get gpt-3.5-turbo token encoder:
  Get "https://openaipublic.blob.core.windows.net/...":
    lookup ... on [::1]:53: connection refused

二进制在 APK 子进程里无法解析 DNS。经过 3 天尝试:

  1. ✅ GODEBUG=netdns=cgo=1 → 没生效 (因为用了 netgo 标签)
  2. ❌ 重新编译去掉 netgo → 还是不行 (Android 子进程没有 Binder context)
  3. ✅ LD_PRELOAD hook + 自定义 resolv.conf + GODEBUG=netdns=go=1

前端白屏问题 ​

  • 原因: web/build/ 目录为空 (.gitkeep 占位)
  • Go //go:embed web/build/* 嵌入了空目录
  • 需要先 npm run build 编译 React 前端, 产物放 web/build/default/

Phase 6: 最终方案 — 可配置 Java 外壳 ​

新增组件 ​

  1. MainActivity.java — 原生配置 UI (SharedPreferences 存配置)
  2. WebViewActivity.java — 独立 WebView 页面
  3. cpp/dns_hook.c — LD_PRELOAD DNS 重定向库 (NDK 编译, ~10KB)
  4. res/layout/activity_main.xml — 配置界面布局
  5. res/layout/activity_webview.xml — WebView 布局

DNS 修复原理 ​

用户配置 DNS (8.8.8.8)
  ↓
Java 写 filesDir/resolv.conf:
    nameserver 8.8.8.8
    nameserver 8.8.4.4
  ↓
LD_PRELOAD libdns_hook.so:
    open("/etc/resolv.conf")  → 返回自定义文件
    fopen("/etc/resolv.conf") → 返回自定义文件
  ↓
GODEBUG=netdns=go=1:
    Go 内置解析器读 resolv.conf → 8.8.8.8 → DNS 正常

数据流 ​

用户输入端口/DNS → SharedPreferences → Intent → OneApiService
  → 写 resolv.conf → setenv(LD_PRELOAD, GODEBUG, CUSTOM_RESOLV_CONF)
  → ProcessBuilder → liboneapi.so 进程
  → stdout → logcat + logBuffer → MainActivity 日志滚动显示

问题汇总 ​

问题阶段原因解决方案
SOCKS5 不通Phase 1EasyTier v2.6.4 bug #1825不用 SOCKS5, 直接通信
go-sqlite3 stubPhase 3CGO_ENABLED=0改为 CGO_ENABLED=1 + NDK
NDK 22 缺 pthreadPhase 3NDK 版本太老升级 NDK r27c
闪退无错误Phase 3缺少 PIE flag-buildmode=pie
闪退无错误Phase 4TLS p_align=8 不兼容align_fix.py 补丁
DNS 解析失败Phase 5netgo 标签禁用 cgo去掉 netgo + LD_PRELOAD
DNS 解析失败Phase 5子进程无 Binder contextLD_PRELOAD 劫持 resolv.conf
前端白屏Phase 5web/build/ 为空npm run build
Token 下载崩溃Phase 5logger.FatalLogTIKTOKEN_CACHE_DIR 预缓存

Android 版本兼容性 ​

层级最低版本说明
APK (Java)Android 9 (API 28)minSdk 28
Go 二进制Android 12 (API 31)aarch64-linux-android31-clang
ARM64Android 5.0+ (API 21+)仅 arm64-v8a
PIEAndroid 5.0+ (API 21+)-buildmode=pie

实际支持: Android 12+ (API 31), arm64-v8a

降级到 Android 9-11: 编译器改 aarch64-linux-android28-clang。

文件清单 ​

  • C:\Users\a1\Desktop\oneapi-apk\ — Android APK 项目
  • C:\Users\a1\Desktop\one-api-android — 最终编译的 Go 二进制 (40MB, 含前端)
  • C:\Users\a1\Desktop\one-api-arm64 — 早期 CGO_ENABLED=0 二进制 (无效)
  • C:\Users\a1\Desktop\one-api\ — One API 源代码
  • D:\devtools\Android\Sdk\ndk\27.0.12077973\ — NDK r27c
  • D:\devtools\easytier-gui\easytier-gui.exe — EasyTier GUI v2.6.4