PROJ 9.8.1 PROJ-JNI 交叉编译成 AAR 的全链路复盘构建、打包、运行时崩溃、EPSG 数据库每一环都藏着坑。前言在 Android 上做专业坐标转换可选的路子其实很窄。要在 EPSG:4326 / 4490 / 4547、Web Mercator 之间互转还得能吃下 CGCS2000 这类国内坐标系基本绕不开 PROJ 这套 C 库。麻烦的地方在于它看起来能跑交叉编译能过、so 也能正常打进 AAR、System.loadLibrary三连同样不报错。可createFromUserInput(EPSG:4326)一调就抛FactoryException: std::exception日志里干干净净连一行线索都没有 —— 接下来就是在黑盒里猜。这篇文章基于我把 PROJ 9.8.1 PROJ-JNI 用 NDK 25 交叉编译并打包成 AAR 的真实过程按「构建 → 打包 → 崩溃 → EPSG 解析」的顺序把 8 个坑逐个摊开。面向需要在 Androidarm64-v8a上做专业坐标转换的 Android / GIS 开发者。一、目标与产物最终产物是一个自包含的 AARproj-android-arm64-v8a.aar约 15MB仅 arm64-v8a内含三个 native solibsqlite3.so、libproj.so、libproj-binding.soassets/proj/PROJ 数据16 个文件含 10MB 的proj.dbclasses.jar完整org.osgeo.proj.*Java API已 Android 适配消费方只要implementation(files(libs/proj-android-arm64-v8a.aar)) 手动loadLibrary三连即可做 EPSG 坐标转换。先记住这一节目标是一个 AAR 拿走就能用。后面 8 个坑基本都是从这句话派生出来的 —— so、数据、Java API 必须自包含任何一处偷偷依赖宿主机环境都会在真机上炸。二、构建链路交叉编译的 5 个坑2.1 坑 1LTO 跨库链接失败NDK 下给libproj.so加-flto产出 bitcode再被 JNI 链接时lld报lto.tmp错误。修复关闭ENABLE_LTO改用-Os -ffunction-sections -fdata-sectionsstrip体积从 48MB 降到 4.8MB。2.2 坑 2C typeinfo/vtable undefinedproj-src/CMakeLists.txt里set(CMAKE_CXX_VISIBILITY_PRESET hidden)导致 JNI 链接时 typeinfo 符号未定义。修复改成default配合 strip 不影响最终体积。2.3 坑 3JNI 链接 PROJLIB 被当成目录交叉编译下find_library(PROJLIB NAMES proj PATHS ${PROJLIB})会把传入的目录路径当库。修复必须传完整路径-DPROJLIB.../lib/libproj.so。2.4 坑 4libproj-binding.so 输出位置诡异PROJ-JNI 把 so 输出到out/abi/classes/org/osgeo/proj/Java 包名子目录而不是lib/。打包脚本的find只查 build-jni 顶层会漏掉。修复需同时find ${abi_out}/classes。2.5 坑 5DT_NEEDED 把宿主机路径硬编码进 so最阴险build_proj.sh用-DSQLite3_LIBRARYWindows 绝对路径链接时lld把这个完整宿主机路径原样写进了libproj.so的DT_NEEDED。设备运行时dlopen会按字面量去找宿主机上的路径形如E:/xxx/proj/lib/libsqlite3.so→ 必然失败。修复写scripts/patch_dtneeded.py纯 Python 解析 ELF64 LE定位.dynstr节把绝对路径原地覆盖为libsqlite3.so这个 SONAME在打包前自动调用。三、打包 AARWindows 下的 4 个细节javac classpath 用分号;分隔不是冒号且加-encoding UTF-8源码含…/“”。用${JAVA_HOME}/bin/jar而非裸jarjar 不在 PATH。classes.jar 必须编proj-jni-src/src/main/java全部 .java--release 8依赖 gradle 缓存的geoapi-3.0.2.jarunit-api-2.1.3.jar跳过 java9 多版本类。改NativeResource.java必须同时改android-patches/版——build_aar.sh编译前会用android-patches版覆盖proj-jni-src同名文件只改一处会被覆盖。第 4 条尤其值得留意它属于改了没生效的典型调试时会让人反复怀疑编译器。四、运行时崩溃链NoSuchMethodError → VerifyError → FactoryException4.1 坑 6Android CheckJNI 不兼容java.util.loggingPROJ-JNI 的NativeResource.logger()返回Logger原生log()在 Android 上通过CallObjectMethod(Logger.log(...))输出日志。ART 的 CheckJNI 对Logger.log做签名校验时直接 abort。修复Java 侧logger()返回Object让原生GetStaticMethodID(()Ljava/util/logging/Logger;)解析失败 → 原生log()入口直接return并按Class.forName(android.os.Build)判 Android不能靠java.vm.nameART 上是 “Dalvik”/“ART”不含 “android”。4.2 坑 7C 侧 JNI 签名必须和 Java 一致只改 Java 侧不够。bindings.cpp的GetStaticMethodID(caller, logger, ()Ljava/util/logging/Logger;)在 ART 下签名不匹配仍抛NoSuchMethodError。修复必须同时把 C 签名改成()Ljava/lang/Object;并重编libproj-binding.so加if (env-ExceptionCheck()) env-ExceptionClear();防 FindClass 失败挂起异常。4.3 坑 8重编的 so 没进 AARbuild_all.shset -e陷阱build_proj.sh的cp -f在 Git Bash 下报are the same fileexit 非 0配合set -euo pipefail提前中断新版 so 停在了out/arm64-v8a/classes/org/osgeo/proj/而 AAR 打包用的是out/arm64-v8a/lib/里的旧 so。手动修正法cp-fout/arm64-v8a/classes/org/osgeo/proj/libproj-binding.so\out/arm64-v8a/lib/libproj-binding.so llvm-strip --strip-unneeded out/arm64-v8a/lib/libproj-binding.so ./build_aar.sh校验grep -ac Ljava/util/logging/Logger; libproj-binding.so应为 0Ljava/lang/Object;应为 1。五、EPSG 解析失败FactoryException: std::exception修完上面崩溃后createFromUserInput(EPSG:4326)抛FactoryException: std::exception。这是最磨人的一环根因有两层。5.1 根因 APROJ-JNI 的log()抛裸std::exceptionbindings.cpp的log()在 Android 上因logger()返回 null 抛 NPE原实现throw std::exception()被createFromUserInput误捕获成FactoryException: std::exception。关键经验FactoryException: std::exception裸std::exception不一定来自 PROJ 本身很可能是 PROJ-JNI 绑定层的 bug。遇到先grep bindings.cpp throw std::exception()。修复log()失败时ExceptionClear() 静默 return绝不 throw。5.2 根因 Baux 数据库路径被当数据库文件 ATTACH修复 log() 后真错误才透出DatabaseContext::create FAILED: SQLite error [ code 14 ] ... on ATTACH DATABASE。根因是把PROJ_DATA目录塞进了auxiliaryDatabasePathsPROJ 把它当数据库文件ATTACH 打开 → 失败目录不是文件SQLITE_CANTOPEN14。最终可靠方案三参DatabaseContext::create// get_database_context 内std::string db_filestd::string(proj_data)/proj.db;proj_context_set_search_paths(pctx,1,proj_data);// 仅设数据目录网格文件等proj_context_set_database_path(pctx,db_file.c_str(),nullptr,nullptr);// 双保险DatabaseContext::create(db_file,{},pctx);// proj.db 完整路径作第一参数要点auxiliaryDatabasePaths只放额外数据库文件路径绝不能放数据目录proj.db完整路径作为第一个参数databasePathproj_context_set_search_paths只用于网格/数据搜索目录与databasePath分离六、应用层坐标轴序与加载顺序编译和绑定层都通了最后还有两个纯应用层的问题同样能让人卡半天。PROJ 9 默认 EPSG:4326 / 4490 为lat, lon轴序。测试 APP 传[lon, lat]会报Invalid coordinate。改为doubleArrayOf(39.907, 116.391)先纬度后经度。三个 so 的加载顺序sqlite3 → proj → proj-binding跳过不存在的 tiff。org.proj.android.Proj.load()会崩它 load “tiff”需自带 loader 只 load 上述三连。本地org/osgeo/proj源码不要和 AARclasses.jar同名类共存否则checkDebugDuplicateClasses失败。七、一键诊断命令清单下面这段建议直接存成脚本改完 so 就过一遍 —— 前面几个坑都能在这里第一时间暴露。# so 内是否还残留桌面版 logging 签名grep-acLjava/util/logging/Logger;libproj-binding.so# 应0grep-acLjava/lang/Object;libproj-binding.so# 应1grep-acPROJ_DATAlibproj-binding.so# 应1含数据路径读取# DT_NEEDED 是否被正确改写为 SONAMEreadelf-dlibproj.so|grepNEEDED# 应含 libsqlite3.so非绝对路径# NativeResource 确认javap-classpathclasses.jar org.osgeo.proj.NativeResource|greplogger# 应显示static java.lang.Object logger()结论在 Android 上跑通 PROJ 的关键不在编译开关而在三个边界构建边界DT_NEEDED 路径硬编码、so 输出位置、LTO / visibility 跨库链接 —— 这三个问题都只在链接与打包阶段暴露编译能过不代表能跑。JNI 边界Android CheckJNI 不支持java.util.loggingC 侧签名必须与 Java 严格一致且log()等辅助函数绝不能抛裸异常。数据边界PROJ C 库只认环境变量或 C API不读 JavaSystem.setPropertyproj.db必须作为DatabaseContext::create的第一参数数据目录不能塞进 aux paths。把这三条边界处理对EPSG 解析与坐标转换就能稳定打通。再补三条下次直接照做的检查项加载顺序三个 so 固定为sqlite3 → proj → proj-binding跳过不存在的 tiff。改完 so 先验签名grep -ac Ljava/util/logging/Logger;应为 0、Ljava/lang/Object;应为 1。见到裸std::exception先怀疑绑定层grep bindings.cpp throw std::exception()别一头扎进 PROJ 源码。如果你也把 C 库往 Android 上搬过或者正在为 EPSG 解析、坐标轴序这类问题头疼欢迎在评论区聊聊你踩得最狠的那一个坑。关键词标签#Android#PROJ#坐标转换#交叉编译#NDK#JNI#GIS#EPSG
企业数字化 ERP 产品动态
相关推荐
主数据管理实战:统一客户、物料与供应商数据,夯实大数据分析底座 做数据的朋友应该都体会过这种痛苦:辛辛苦苦把各个系统的数据抽到数仓里,结果一统计发现同一家客户的订单数对不上,明明是同一个产品在不同报表里名称还不一样,销售部门说A门店业绩最高,财务部门说A门店亏得最多。问题… · 2026/9/26 17:51:34
Android 数据库之 Cursor:从查询到遍历的完整实践与 TaoToken 配置骨架 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 17:51:34
Python Flask校园失物招领系统:关键词匹配与部署实战 又到毕业设计季,每年这时候都有一批人被“Python Flask 管理系统”组合拳安排得明明白白。这次聊一个很接地气的题目: Python基于Flask的校园失物招领系统 。这个系统听起来简单,但仔细拆解下来,它把Flask开发、数据库设计、中… · 2026/9/26 17:51:34
从零掌握Skill:创建、修改与调试可复用AI能力模块的完整指南 1. 从零理解 Skill:它到底解决了什么问题 很多人第一次接触 Skill 这个概念时,会下意识把它和 Prompt 混为一谈。我刚开始也是这样,觉得无非就是写一段更长的提示词,让模型按格式输出罢了。但真正用起来才发现,Skill 和… · 2026/9/26 18:26:04
华为Eth-Trunk链路聚合原理与高可用配置实战 1. 为什么链路聚合不是“多接几根网线就变快”那么简单? 在机房巡检时,我常看到新来的工程师把两台华为交换机之间用四根网线连得密密麻麻,还一脸得意:“老师傅,我做了链路聚合,带宽翻四倍了!”… · 2026/9/26 18:26:04
微电网多目标调度优化:NSGA-III算法原理与Matlab实现 做微电网调度优化,绕不开多目标这个问题。运行成本、碳排放、电压质量、系统网损,这些东西互相打架,单靠Scalarization转成单目标,权重怎么定是个老大难问题。而NSGA-III这类基于Pareto支配的多目标进化算法,能一次跑出… · 2026/9/26 18:26:04
esqlite3 V1.1:易语言SQLite多线程事务锁与加密升级解析 简介:这份esqlite3 V1.1支持库与模块面向易语言开发中的SQLite数据库操作场景,在1.0基础上补齐互斥体、聚合上下文、自动提交、进度处理等全局命令,并为数据库与记录集增加事务锁状态、取行数、取数据库句柄等实用接口。资源共413个文件&… · 2026/9/26 18:26:04
Django部署全解析:Nginx、WSGI、中间件与静态文件一次说透 Django写起来是真的快,但一聊到部署上线,Nginx、WSGI、中间件、静态文件这几个词儿一股脑砸过来,很多朋友就懵了。我见过不少项目,开发环境跑得飞起,一上服务器就各种404、403、样式全丢,原因基本都出在对这… · 2026/9/26 18:26:04
Qwen图像生成模型在A100与RTX3060上的实测对比 1. 项目概述:为什么一张图要跑两套环境?Qwen图像生成模型最近在中文多模态圈子里火得有点突然——不是因为参数量碾压,而是它对中文提示词的理解、对本土化场景(比如水墨风建筑、古风人物配饰、工业设计草图)的还原能力… · 2026/9/26 18:25:58
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍 简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践 一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46