ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Qt for Android 连接 MySQL:交叉编译与驱动部署全攻略

Qt for Android 连接 MySQL:交叉编译与驱动部署全攻略 简介在 Qt 的 Android 应用开发中官方未预置 MySQL 数据库驱动这使得许多开发者时常面临数据库无法连接的困境。这份资料包正是针对该场景围绕 Qt 5.12.1、Android 与 MySQL 三者的衔接问题整理了从依赖安装、驱动交叉编译、插件配置到应用打包的关键文件与说明。压缩包内共 934 个文件整体大小 16.51 MB文件类型涵盖源代码文件、工程配置、文本说明、动态链接库以及多种语言的资源文件其中头文件与源码可用于核验编译细节工程文件与说明文档则可辅助快速理解各模块的作用。目前已有 776 人学习下载。对于需要自行编译 MySQL 驱动的开发者可从根目录的分类结构中快速定位所需内容免去四处收集依赖与脚本的周折该包在 Windows 与 Ubuntu 环境下均具有参考价值是一份可直接对照实践的集成参考资料。1. 在 Android 上跑通 Qt MySQL先把三个组件的关系理清如果你在 Qt 里用QSqlDatabase连接过本机或服务器的 MySQL会觉得这事情平淡无奇。但同样的代码放到 Android 真机上最常见的结局不是报错而是加载不到数据库驱动、链接不到libmysqlclient、或者一进QSqlDatabase::open()就崩溃。反直觉的一点是Qt 官方为 Windows、Linux 预编译的安装包里自带 QMYSQL 插件但 Android 的预编译包里从来不带 SQL 驱动。这导致很多从 PC 端迁到 Android 端的人第一周全耗在“驱动从哪来”上。Qt 负责界面和业务线程Android 提供的是 ARM 交叉编译环境和受限的移动网络能力MySQL 是远端数据服务。三者真正的交界处有三个交叉编译工具链、MySQL 客户端库的 ARM 产物、Qt 网络层与移动网络的适配。这篇博文按“环境搭建 → 编库 → 写连接层 → 部署”的顺序把一条能复现的完整路径讲清楚适合已经写过 Qt 桌面端、但第一次把 Qt 应用往 Android 真机上部署的人。2. Qt for Android 交叉编译环境JDK、SDK、NDK 版本组合2.1 版本组合Qt 6.5 LTS NDK r25 是我默认的选择Qt for Android 的环境搭建看起来是“装三个东西”实际上卡的往往是版本组合。JDK、Android SDK、NDK、Qt 自带的 Android 工具链之间有一个隐性约束NDK 版本太新Qt 的 mkspec 不认NDK 太老编译时std::filesystem和 C17 特性会出各种诡异错误。我用得最稳的一套组合是JDK 17OpenJDK、Android SDK Platform 33、NDK r25b、Qt 6.5 LTS。如果你还在用 Qt 5.15.2NDK 要降到 r23因为 Qt 5.15 内部的libc链接方式和新版 NDK 有兼容问题。考虑到长周期维护新项目我一般直接选 Qt 6.5 LTS它的安卓目标支持arm64-v8a和armeabi-v7a也支持 CMake 的预设工具链。安装时有一处容易踩Android SDK 里要确认装的是 “NDK” 而不是 “Side-by-side NDK” 的某个测试版。Qt 在检测 NDK 时读的是source.properties里的版本号若版本号在 Qt 的 known-good 列表之外Qt Creator 不报硬错误但到交叉编译时会出现 “No rule to make target” 这种误导性错误。2.2 环境变量与 Qt Creator 配置的关键项以下是我在 Linux 主机上配 Qt for Android 的常用环境变量组合Windows 上路径规则一致只是目录分隔符不同export ANDROID_SDK_ROOT/opt/android-sdk export ANDROID_NDK_ROOT$ANDROID_SDK_ROOT/ndk/r25b export ANDROID_NDK_HOST_TAGlinux-x86_64 export ANDROID_API_LEVEL33 export JAVA_HOME/usr/lib/jvm/java-17-openjdk-amd64这几个环境变量决定三件事Qt Creator 用哪份 NDK 做交叉编译、用哪个 API level 做平台头文件、签名和打包时用哪份 JDK。ANDROID_API_LEVEL不直接等于minSdkVersion它决定的是链接时用的android.jar版本。我一般设置成 24 以上因为 Android 7.0 以下设备对 TLS 1.2 的支持不完整而 MySQL 8 默认就要求连接走 TLS 的话低版本设备容易握手失败。Qt Creator 里的配置路径是Options → Devices → Android把上面列出的目录逐项填进去。这里有一个新手常忽略的操作填完路径后要点一次 “Install” 按钮旁边的 “Check” 按钮让 Qt 自己去拉取平台工具和构建工具。如果你之前用 Android Studio 装过 SDKQt 可以直接复用如果两个 IDE 共用 SDKadb 版本尽量用 SDK 自带的那份不要用 PATH 里更旧的系统 adb。3. 编译 MySQL 客户端库为 ARM64 产出可链接的静态库3.1 为什么不能用 PC 上的 libmysqlclient.so 直接推给 AndroidMySQL 官方提供的 Connector/C 通常只有 x86 和 x64 的预编译二进制。Android 手机是 ARM 指令集即使你把 x64 的.so塞进 APK系统加载动态库时直接报dlopen failed: is 32-bit instead of 64-bit或cannot locate symbol。如果你负责过嵌入式设备上的数据库客户端就会知道Android 场景几乎只有一条路拿 NDK 交叉编译 MySQL 客户端库的源码编出libmysqlclient.a静态库。另一个选择是 MariaDB Connector/C协议兼容 MySQL且 CMake 工程对 Android 交叉编译的支持更直接。网络上大量资料建议在 Android 上用 MariaDB 客户端库原因不是性能差异而是其 CMake 脚本没有那么多平台判断。不过我下面给的命令在两者上都能跑区别只在 TARGET 名不同。3.2 用 NDK 的 CMake 工具链编译 ARM64 客户端库先把源码准备好。这里我以 MySQL Connector/C 8.0 分支为例MariaDB Connector/C 3.3.x 也可以并假设源代码放在~/mysql-connector-ccmake -S ~/mysql-connector-c -B ~/build-mysql-android \ -DCMAKE_TOOLCHAIN_FILE$ANDROID_NDK_ROOT/build/cmake/android.toolchain.cmake \ -DANDROID_ABIarm64-v8a \ -DANDROID_PLATFORMandroid-24 \ -DCMAKE_BUILD_TYPERelease \ -DWITH_UNIT_TESTSOFF \ -DWITH_DEFAULT_COMPILER_OPTIONSON cmake --build ~/build-mysql-android --target mysqlclient -j$(nproc)逐个解释关键参数CMAKE_TOOLCHAIN_FILE指向 NDK 自带的工具链文件它做了三件事指定编译器为aarch64-linux-android-24-clang、引入-fPIC、把 sysroot 指向 Android 平台头文件。ANDROID_ABIarm64-v8a对应 64 位 ARM。如果你要兼容老设备再加一组armeabi-v7a的构建ABI 不能混在一个构建目录里必须分两次编译。ANDROID_PLATFORMandroid-24是给编译器看的 API level编译出来的库可以在更高版本系统上运行但最低支持到 API 24。WITH_UNIT_TESTSOFF必须关掉Android 交叉环境跑不了 CTest 用例不关会卡在测试阶段。--target mysqlclient只编客户端库不需要编服务器工具节省大量时间。编译成功后产物是~build-mysql-android/libs/libmysqlclient.aMariaDB 客户端是libmariadb.a。注意这份静态库编译时默认是 Release、无调试符号如果你想在 NDK 层面排查崩溃可以重新编一个CMAKE_BUILD_TYPERelWithDebInfo的副本后续链接进 APK 会让体积增加 1~2 MB但我个人认为排查价值大。3.3 用编译好的客户端库编 Qt 的 QMYSQL 驱动插件Qt 常见的 “找不到驱动” 问题根源在这里Qt 源码里的qtbase/src/plugins/sqldrivers/mysql默认不编进 Android 发布包。你需要手动编译并把插件libqsqlmysql.so放进 APK 的plugins/sqldrivers目录。在 Qt 源码目录下执行以 Qt 6.5 为例cd ~/Qt/6.5.2/Src/qtbase/src/plugins/sqldrivers qmake -- MYSQL_INCDIR~/build-mysql-android/include \ MYSQL_LIBDIR~/build-mysql-android/libs make -j$(nproc)qmake在这里读MYSQL_INCDIR和MYSQL_LIBDIR两个变量分别指向上一节编译出的库的头文件和.a所在目录。编出来的产物在plugins/sqldrivers/libqsqlmysql.so。由于libmysqlclient.a是静态库Qt 驱动插件编译时会把 MySQL 客户端代码直接并进.so这带来一个额外好处APK 不需要动态库依赖不会出现 “java.lang.UnsatisfiedLinkError: dlopen failed: library libmysqlclient.so not found” 的运行时崩溃。如果你用的是 CMake 构建 Qt 项目规则一致只是把上述内容写进CMakeLists.txt里的外部源码依赖。注意如果你在 Windows 主机上编 Android 目标qmake 参数里MYSQL_LIBDIR的路径要写成正斜杠反斜杠会被 qmake 转义成别的字符部分 qmake 版本会静默吞掉路径。4. 在 Qt 层连接 MySQL驱动装载与参数化查询实操4.1 指定自定义插件路径并加载 QMYSQL把编译好的libqsqlmysql.so放进 Android 项目的libs/arm64-v8a/plugins/sqldrivers目录后Qt 默认扫描路径可能不是这里。Qt 在 Android 上有额外的“部署时路径重映射”插件目录最终会被放到assets/plugins/sqldrivers所以应用启动时要主动把该目录加入库搜索路径#include QCoreApplication #include QSqlDatabase #include QSqlQuery #include QSqlError #include QDebug void initMysqlDriver() { QString pluginsPath QCoreApplication::applicationDirPath() QStringLiteral(/plugins/sqldrivers); QCoreApplication::addLibraryPath(pluginsPath); qDebug() available drivers: QSqlDatabase::drivers(); }QSqlDatabase::drivers()打印结果里出现QMYSQL才说明驱动加载链路通了。如果只有QSQLITE先不要查网络回到插件路径用 adb shell 进入应用私有目录看plugins/sqldrivers是否真的打进 APK 了。这里有个高概率踩坑点把.so放进了构建目录但没在 .pro 文件里声明部署规则Qt 打包时不会自动搬运动态库。4.2 连接参数与 CONFIG 的合理配置驱动声明成功后下一步是连数据库。Android 上网络不稳定是常态所以我对连接选项的控制比在桌面端严得多。下面的代码是一个可复现的连接模板QSqlDatabase createMysqlConnection(const QString connectionName) { QSqlDatabase db QSqlDatabase::addDatabase( QStringLiteral(QMYSQL), connectionName); db.setHostName(QStringLiteral(your-mysql-host)); db.setPort(3306); db.setDatabaseName(QStringLiteral(your_db)); db.setUserName(QStringLiteral(qt_user)); db.setPassword(QStringLiteral(your_password)); db.setConnectOptions( QStringLiteral(MYSQL_OPT_CONNECT_TIMEOUT5;) QStringLiteral(MYSQL_OPT_RECONNECT1;) QStringLiteral(MYSQL_OPT_READ_TIMEOUT10;) QStringLiteral(MYSQL_OPT_WRITE_TIMEOUT10;) QStringLiteral(MYSQL_OPT_SSL_MODEDISABLED)); return db; }对参数逐一说明MYSQL_OPT_CONNECT_TIMEOUTTCP 建连超时5 秒在移动网络下是合理下限。设成 1 秒用户在地铁里稍微抖一下信号就必失败设成 30 秒界面会卡到你怀疑人生。MYSQL_OPT_RECONNECT这个参数在 MySQL 8 默认被服务端忽略客户端库会尝试自动重连但如果连接被杀掉旧事务已失效。不要依赖它它只是降低“掉线后重发查询”时的报错概率业务逻辑上要自己处理断线重查。MYSQL_OPT_READ_TIMEOUT和MYSQL_OPT_WRITE_TIMEOUT10 秒足够覆盖大查询的响应也不会让 UI 等待过久。MYSQL_OPT_SSL_MODEDISABLED注意这里的写法。Qt 6 里选项前缀已经从 Qt 5 的QMYSQL_改成了MYSQL_很多人从老代码复制过来发现setConnectOptions静默解析不成功就是这个前缀差异。提示是否禁用 SSL 取决于你的 MySQL 是否强制require_secure_transport。如果你在服务器上开着这个选项本地手机关闭最后会看到一个非常误导的报错SSL connection error但根因在服务端配置。4.3 参数化查询与 MySQL 端的注意事项连接建好后写 SQL 要遵守一个铁律所有外部输入一律走参数绑定不做字符串拼接。移动端应用的账号就是走公网暴露的拼接 SQL 等于把数据库权限丢给数据包监听者。正确做法QSqlQuery query(db); query.prepare( QStringLiteral( SELECT id, name, status FROM device_info WHERE user_id ? AND status ! 0 LIMIT 50)); query.addBindValue(userId); query.addBindValue(QDateTime::currentMSecsSinceEpoch()); if (!query.exec()) { qWarning() query failed: query.lastError().text(); }执行后立刻调用lastError()是移动端排查问题的关键因为驱动层面的错误信息在 Android 调试日志里未必会被打出来你不主动捞就只能瞪着空白列表页发愣。MySQL 端的设计也要跟着移动端特性走。一条实践在device_info上建联合索引时把user_id放第一列status放第二列比单独在status上建索引命中率高得多。关于存储过程和复杂报表可以放到服务端去做移动端只调一个CALL proc_get_snapshot(?, ?)免去把大段 SQL 打进 APK 的维护成本。5. 线程模型、连接池与 Android 网络环境的特殊处理5.1 为什么不能在 UI 线程执行 SQLQt 的 UI 主线程负责事件循环任何阻塞调用都会卡住触摸事件的分发。在 Android 上更明显主线程阻塞超过 5 秒系统直接弹 ANRApplication Not Responding对话框用户点“关闭”就是在关你的应用。所以 MySQL 查询必须放进工作线程QSqlDatabase的连接也不能跨线程混用每个线程用自己创建的连接对象或者用线程内信号槽接力结果。一个简单的做法是QtConcurrent::run配合信号槽回传结果QVariantMap doQueryInBackground() { QSqlDatabase db createMysqlConnection( worker_ QString::number(quintptr(QThread::currentThread()))); QSqlQuery query(db); query.exec(...); return resultToVariantMap(query); } QFutureWatcherQVariantMap *watcher new QFutureWatcherQVariantMap(this); connect(watcher, QFutureWatcherQVariantMap::finished, this, ...); watcher-setFuture(QtConcurrent::run(doQueryInBackground));注意 MySQL 服务端的max_connections每个线程各建一个连接线程多了会积压到服务端上限。控制并发数是一个方面另一个是复用手头连接这就要求上面的工作线程长期存活连接不频繁开关否则一次界面刷新五次查询就产生五次 TCP 握手用户体验在弱网下会雪上加霜。5.2 简版连接池按线程存放、空闲校验真正上线前我都会给数据库访问加一层轻量连接池核心逻辑是“每个线程一个连接句柄用前校验坏了重连”。不引第三方库纯用QThreadStorage就可以class DbManager { public: static QSqlDatabase connection() { QThreadStorageQSqlDatabase storage instanceStorage(); if (!storage.hasLocalData()) { storage.setLocalData(createMysqlConnection( thread_db_ QString::number( quintptr(QThread::currentThread())))); } QSqlDatabase db storage.localData(); if (!db.isOpen()) { db.open(); } return db; } };这个连接池的重点不在代码量在校验逻辑isOpen()是 Qt 客户端的本地状态服务器主动断连后本地状态不即时更新。实际项目里我一般在连接池里放一个 “心跳任务”每次拿到连接先跑SELECT 1失败就 close 后重建。对 MySQL 而言SELECT 1命中内部缓存开销可以忽略但对移动端弱网是值得的。5.3 Android 特有的网络切断场景与心跳周期表移动网络和桌面网络的差距在切换时体现得最彻底从 Wi-Fi 切到蜂窝网TCP 连接和旧 IP 直接失效此时 MySQL 服务端还没发 FIN所以你的QSqlDatabase永远不会收到断开通知。下一次查询会卡到READ_TIMEOUT才报错一个 10 秒的等待足以让用户对 App 的好感清零。场景推荐心跳周期说明Wi-Fi 固定环境公司内网测试60 秒网络质量高心跳太频繁浪费日志公网 Wi-Fi商场/咖啡店20 秒客户端间隔离但出口 IP 可能变蜂窝网络 4G/5G10~15 秒切换基站或 NAT 表项老化断连风险高App 退后台被挂起不心跳恢复时重查Android 的 doze 模式下定时器被冻结只能被动重连Android 的 Doze 模式在低电量和长时间待机时会暂停应用的网络访问和QTimer所以“前台心跳一切正常”的体验在退后台 20 分钟后会被彻底打破。恢复前台时第一步就是检查连接有效性不要复用退后台前缓存的那个db对象。6. 真机上线前值得做的 4 个排查技巧6.1 用 logcat 抓驱动加载失败的直接原因Qt 在 Android 上的崩溃日志经常被logcat的冗长输出淹没。驱动加载失败时你会拿到QSqlDatabase: QMYSQL driver not loaded这个信息量很低。我一般首先过滤 Qt 自身的调试输出adb logcat | grep -E qt|QSql|mysql|libc出现dlopen failed: library libqsqlmysql.so not found时问题几乎都可以定位到addLibraryPath的路径或打包规则。若能打印出available drivers列表里面同时存在 QSQLITE 和 QMYSQL就说明链接层没问题往下走网络。6.2 核对 MySQL 侧的连接日志在 MySQL 服务端开启通用日志或连接日志是判断“请求到底有没有到达数据库”的最快办法。如果服务端没有到来路 IP 的握手记录问题一定出在设备端网络或防火墙如果有握手记录但连接立刻断开又没到应用层往往就是MYSQL_OPT_SSL_MODE与服务端强制 TLS 的配置冲突。6.3 真机上验证秒级重连的兜底路径Android 的ConnectivityManager会广播CONNECTIVITY_ACTIONQt 里可以通过安卓 JNI 接口注册监听但这里有一个更轻量的纯 Qt 做法捕获QAbstractSocket::stateChanged失败信号后启动一个QTimer::singleShot(800, ...)来触发重连。800ms 是推荐参数——太快Wi-Fi 切换还没完成太慢用户已感到卡顿。6.4 用SELECT 1 时间戳打印做链路验证我在现场调试时会在所有核心查询前后打上日志格式形如[DB][start] query_ping和[DB][end] cost12ms。对照adb logcat和 MySQL 的general_log中同一条 SQL 的时间戳就能判断延迟到底消耗在设备端、公网链路还是 MySQL 自身查询。这一步做完连接链路上还剩哪一段的问题就一目了然。本文还有配套的精品资源点击获取
返回列表