Qt Android直连MySQL:驱动交叉编译与六大避坑实践 简介面向基于 Qt 5.12.1 的 Android 与 MySQL 集成开发场景这份资源包集中提供编译 QMYSQL 驱动所需的源码、配置与构建辅助文件帮助开发者在 Windows/Ubuntu 环境下完成 NDK 交叉编译解决 Android 端默认缺少 MySQL 驱动的常见问题。包内共 934 个文件压缩包体积 16.51MB其中 302 个 .h 头文件、54 个 .c 源文件构成本地库核心131 个 txt 说明文档与 changelog/readme 提供编译指引m4/configure/in/def 等为自动化构建配置sh/bat 脚本辅助环境操作po/gmo 等包含多语言资源目录覆盖多平台适配结构便于按架构裁剪移植。资料记录了从依赖安装、驱动编译到 APK 打包的完整路径可直接对照排查头文件、链接库与 .so 生成环节还能依据版本兼容说明规避 MySQL 驱动与服务器版本错配问题configure 等自动配置脚本可辅助快速适配不同 NDK 版本与目标架构。目前已有 777 人学习下载适合具有一定 NDK 与 Qt 基础、希望在 Android 应用中接入 MySQL 数据库的开发者参考学习。1. Qt Android MySQL这条老路为什么第一步就卡在驱动上把 PC 上跑得好好的 Qt 程序往 Android 平板上搬第一反应是数据库也跟着搬过去然后你就撞上了这组合里最硬的一堵墙Qt 官方给 Android 发布的构建里默认只带 SQLite 驱动QMYSQL 是缺席的。日志里那行QSqlDatabase: QMYSQL driver not loaded不是你的代码写错了而是插件压根没进 APK。这意味着“Qt 客户端直连 MySQL 服务器”这条路官方没有给你铺好得自己动手把 MySQL 驱动交叉编译出来或者换一条更聪明的数据通路。这篇文章就解决一件事在 Android 端让 Qt 代码真正连上 MySQL并说清楚什么时候值得直连、什么时候该走 HTTP 桥接以及自编译驱动路上的那些坑。2. 三条数据通路怎么选SQLite 本地、HTTP 桥接还是直连 MySQL2.1 先看清 Android 端的处境不是 Qt 不行是平台限制太多Android 上的 Qt 程序跑在沙箱进程里没有 PC 上那么自由的网络环境。MySQL 官方客户端库 libmysqlclient 是给桌面和服务器编的Android 系统里根本没有这个库Qt 官方构建自然不会把 QMYSQL 插件编进去。这不是发行版的疏漏而是 Android NDK 工具链和 MySQL 官方库的构建体系本来就很难对齐。另一个隐性问题是 Android 应用的网络权限。Qt 程序要在 Android 上访问网络必须在 AndroidManifest.xml 里显式声明android.permission.INTERNET。很多从 PC 移植过来的程序第一个编译期错误就是缺这个权限程序跑起来后连接超时先查权限再查代码能省半小时。所以选择数据通路之前先列清楚约束条件设备是内网工业平板还是公网手机数据量级是每秒几条还是每分钟几条是否需要离线缓存客户端和服务端之间有没有可信网络环境。这些条件直接决定下面三条路线哪条合适。2.2 路线一SQLite 本地库——离线缓存的首选但不是真正的“MySQL 方案”SQLite 是 Qt 在 Android 上开箱即用的数据库QSQLITE驱动内置不需要任何额外编译。如果你的场景是设备端采集数据、断网时先存本地、联网后再同步SQLite 是最稳妥的起点。典型的做法是建一张本地表采集线程往 SQLite 写同步线程定时把未上传的数据发到服务端。QSqlDatabase localDb QSqlDatabase::addDatabase(QSQLITE, local_cache); localDb.setDatabaseName(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) /cache.db); if (!localDb.open()) { qWarning() 打开本地SQLite失败: localDb.lastError().text(); }这段代码指定了 SQLite 数据库文件放在应用私有目录不依赖任何外部服务。addDatabase的第二个参数是连接名多连接场景必须显式命名避免污染默认连接。SQLite 的优点是零配置、单文件、事务能力强缺点是它只是本地库解决不了“多个 Android 端要读写同一份数据”的问题。所以这条路线本质上是“先用 SQLite 顶着后面还是要走网络”。2.3 路线二HTTP 桥接——生产环境里最常见的妥协方案真正在生产环境里跑得最稳的不是 OLEDB 直连而是“Android 端 Qt 程序只跟服务端接口说话MySQL 藏在服务端后面”。Android 端用QNetworkAccessManager发 HTTP 请求服务端无论用什么框架负责解析请求、读写 MySQL、返回 JSON 或 XML。这个方案的好处是把 MySQL 的端口、账号、密码全部收进内网Android 端拿到的只是一个 URL。就算 APK 被反编译也泄露不了数据库凭据。QNetworkRequest request(QUrl(https://api.example.com/device/upload)); request.setHeader(QNetworkRequest::ContentTypeHeader, application/json); QNetworkAccessManager manager; QNetworkReply *reply manager.post(request, QJsonDocument(payload).toJson(QJsonDocument::Compact)); connect(reply, QNetworkReply::finished, this, [reply]() { if (reply-error() QNetworkReply::NoError) { QByteArray resp reply-readAll(); qDebug() 上传成功: resp; } else { qWarning() 上传失败: reply-errorString(); } reply-deleteLater(); });这段代码用QNetworkAccessManager发了一个 JSON POST 请求。注意deleteLater()QNetworkReply如果不手动释放会泄漏。ContentTypeHeader必须设置很多服务端框架会直接拒掉格式不对的请求。HTTP 桥接的代价是你要多写一套服务端接口而且实时性不如直连数据库——但绝大多数业务场景根本不需要毫秒级数据库访问HTTP 的延迟完全够用。2.4 路线三直连 MySQL——什么时候值得自编译 QMYSQL 驱动直连 MySQL 的优势是代码简单、实时性最好、不需要中间服务端Qt 的QSqlDatabase把 SQL 语句直接发到 MySQL 服务器。适合的场景是内网固定 IP 的工控机群、设备数量少且网络可控、需要复杂 SQL 事务而 HTTP 接口难以表达。典型例子是车间里的检测设备两三台平板在一个局域网里直接连 MySQL 比搭一套 HTTP 服务端轻量得多。直连的代价就是你要自己搞定 QMYSQL 驱动。这不仅仅是编译一个 .so 的问题还牵扯到 MySQL 客户端库的交叉编译、SSL 支持、ABI 匹配、APK 打包路径——每一环都能让新手卡一整天。我的经验是如果这个项目是 5 台设备以内的内网应用直连值得做如果设备量超过 10 台或者网络上云直接走 HTTP 桥接别在驱动上浪费时间。三条路线的对比如下对比项SQLite 本地HTTP 桥接直连 MySQL驱动准备内置零成本不需要数据库驱动需自编译 QMYSQL实时性仅本地取决于网络最好多端并发不支持支持支持数据库凭据暴露无无APK 内会携带适合规模单机离线任意规模内网小规模3. 自己编 QMYSQL 驱动NDK 交叉编译命令与打包步骤3.1 准备工具链NDK、Qt for Android、MySQL 客户端库一个都不能少自编译 QMYSQL 驱动本质上是用 Android 的交叉编译器把你本机下载的 MySQL 客户端库编成 Qt 的 SQL 驱动插件。三个前提缺一不可一是 Android NDK版本要和你的 Qt 版本匹配Qt 官方文档里每个 Qt 版本都会标明推荐的 NDK 版本宁旧勿新二是 Qt for Android 的安装目录编译完成后的插件要放回它的plugins/sqldrivers目录让androiddeployqt打包时能发现三是 MySQL 客户端库有两个选择——官方 MySQL Connector/C或者 MariaDB Connector/C后者对 MySQL 协议完全兼容而且编译链更轻量我在实际项目中更喜欢用 MariaDB 的库。常见做法是先把 MySQL 客户端库用 NDK 编出来。以 MariaDB Connector/C 为例源码拿到后直接用它的 CMake 构建系统指定 Android 工具链编译不需要改代码。库编好之后你会得到libmariadb.so或libmysqlclient.so和一堆头文件这两个东西是下一步编译 QMYSQL 插件的原料。3.2 用 CMake 交叉编译 QMYSQL 插件最小命令与参数说明Qt 的 SQL 驱动插件在 Qt 源码的qtbase/src/plugins/sqldrivers目录下可以直接对这个子目录单独跑 CMake。下面的命令是一个能跑通的最小集合我把路径换成通用变量你按自己环境替换cmake -S $QT_SOURCE/qtbase/src/plugins/sqldrivers \ -B build-sqldrivers \ -DCMAKE_TOOLCHAIN_FILE$ANDROID_NDK/build/cmake/android.toolchain.cmake \ -DANDROID_ABIarm64-v8a \ -DANDROID_PLATFORMandroid-23 \ -DQT_HOST_PATH$QT_HOST_INSTALL_DIR \ -DCMAKE_INSTALL_PREFIX$QT_ANDROID_INSTALL_DIR \ -DFEATURE_sql_mysqlON \ -DMySQL_INCLUDE_DIR$MYSQL_CLIENT_PREFIX/include \ -DMySQL_LIBRARY$MYSQL_CLIENT_PREFIX/lib/libmariadb.so-DANDROID_ABIarm64-v8a指定目标架构现在绝大多数 Android 设备都是 64 位如果你的平板还是老的 32 位系统要改成armeabi-v7a两种架构的产物不能混用。-DFEATURE_sql_mysqlON是打开 MySQL 驱动的编译开关不加这个选项默认不会编。MySQL_INCLUDE_DIR和MySQL_LIBRARY指向上一节编译出来的客户端库。编译完成后在build-sqldrivers/plugins/sqldrivers/目录下会生成libqsqlmysql.so。这个文件就是我们要的驱动插件。如果你在编译时遇到mysql.h not found说明MySQL_INCLUDE_DIR路径不对或者客户端库的头文件没安装到指定目录遇到cannot find -lmariadb则说明MySQL_LIBRARY没指向实际的 .so 文件而是给了一个不存在的路径。3.3 把驱动打包进 APK插件路径与 androiddeployqt 的部署逻辑驱动插件编译出来只是第一步更隐蔽的问题是怎么让libqsqlmysql.so进到 APK 里并且在运行时被 Qt 加载。Qt for Android 的打包流程是androiddeployqt遍历 Qt 安装目录下的plugins目录把需要的插件拷进 APK 的libs/abi/目录。所以最省事的做法是把编好的libqsqlmysql.so直接复制到 Qt for Android 安装目录下的plugins/sqldrivers/里让androiddeployqt在打包时自动发现它。cp build-sqldrivers/plugins/sqldrivers/libqsqlmysql.so \ $QT_ANDROID_INSTALL_DIR/plugins/sqldrivers/复制完成后重新用 Qt Creator 构建 Android 工程或者手动跑一遍androiddeployqt然后检查生成的 APK 里有没有libqsqlmysql.so。检查方法很简单把 APK 后缀改成.zip解压看lib/arm64-v8a/目录下是否有这个文件。这一步很多人会漏掉结果代码里addDatabase(QMYSQL)依然报driver not loaded折腾半天发现是打包阶段压根没把插件带上。还有一种情况是你不想往 Qt 安装目录里塞文件那就要在应用代码里手动指定插件路径用QCoreApplication::addLibraryPath()指向 APK 内的libs目录然后确保插件 .so 在打包时被放进 assets 或 libs。这个方法适合自定义打包脚本的场景但要注意 Android 的dlopen只能从应用私有目录加载 .so路径写错一样加载失败。我的建议是用官方androiddeployqt的自动机制少碰手动加载的玄学。4. Qt 客户端连 MySQL连接参数、断线重连与编码设置4.1 QSqlDatabase 连接参数主机、端口、驱动选项一个都别省驱动插件就绪后连接 MySQL 的代码和 PC 端没太大区别但有几个参数在 Android 上尤其重要。先看一个完整的连接示例QSqlDatabase db QSqlDatabase::addDatabase(QMYSQL, iot_conn); db.setHostName(192.168.1.100); db.setPort(3306); db.setDatabaseName(device_data); db.setUserName(app_user); db.setPassword(your_password_here); db.setConnectOptions( MYSQL_OPT_CONNECT_TIMEOUT5; MYSQL_OPT_READ_TIMEOUT10; MYSQL_OPT_WRITE_TIMEOUT10; ); if (!db.open()) { qWarning() MySQL连接失败: db.lastError().text(); } else { qDebug() MySQL连接成功; }MYSQL_OPT_CONNECT_TIMEOUT5是 TCP 连接超时Android 网络环境不稳定如果不设这个值默认可能要等几十秒才报错用户早就以为程序死了。MYSQL_OPT_READ_TIMEOUT和MYSQL_OPT_WRITE_TIMEOUT是读写超时防止慢查询把 UI 线程卡死。这三个参数在 PC 上往往不用管但在移动网络或弱网环境下必须显式设置。用户名和密码这里建议不要写死在代码里Android APK 很容易被解包字符串常量直接暴露。常见做法是把数据库凭据放在服务端下发或者至少做一层简单混淆这个后面避坑章节会细说。4.2 断线重连与连接复用移动网络下别裸连也别频繁重连Android 设备会频繁切换 Wi-Fi 和移动网络TCP 长连接随时可能被系统断掉。MySQL 服务端的wait_timeout默认是 8 小时但 Android 系统的网络栈可不管这些锁屏休眠、基站切换都会让连接失效。所以客户端必须处理断线重连。一个简单的做法是在 SQL 执行前检查连接状态发现失效就重连bool ensureConnected(QSqlDatabase db) { if (!db.isOpen() || !db.isValid()) { return db.open(); } // 用轻量查询探测连接是否真的活着 QSqlQuery probe(db); if (!probe.exec(SELECT 1)) { db.close(); return db.open(); } return true; }db.isOpen()在本地层面可能返回 true但网络早断了所以要发一个SELECT 1做真实探测。这个查询非常轻不会给服务端带来压力。探测失败后close()再open()等于重新建立连接。这套逻辑要封装成公共函数所有数据库操作前都调用它而不是每处写一遍判断。连接复用方面建议整个应用只维护一个数据库连接或者一个连接池不要每次操作都addDatabase新建。Qt 的QSqlDatabase::addDatabase如果重复用同一个连接名会先移除旧的这会导致旧连接未关闭积累到一定数量就报too many connections。我见过一个项目每写入一条数据就新建连接跑了半天就把 MySQL 的连接数打满了教训很深刻。4.3 中文乱码与 SSL两个最容易忽视的连接选项Android 的 Qt 程序往 MySQL 写中文最常见的坑是乱码。Qt 侧字符串默认是 UTF-8MySQL 服务端字符集如果是latin1写入必然变问号。解决办法是在建库建表时指定utf8mb4同时连接时设置字符集db.setConnectOptions(MYSQL_SET_CHARSET_NAMEutf8mb4);MYSQL_SET_CHARSET_NAME驱动选项会执行SET NAMES utf8mb4保证客户端和服务端的字符集一致。注意utf8mb4和utf8的区别utf8在 MySQL 里最多存 3 字节emoji 和生僻字会存不进去直接报错或变乱码所以一律用utf8mb4。SSL 选项则是个双刃剑。MySQL 8 默认开启caching_sha2_password认证插件如果客户端库不带 SSL 支持认证可能会失败。你可以在服务端对特定用户指定mysql_native_password但这又降低安全性。更稳妥的做法是编译 MySQL 客户端库时开启 OpenSSL 支持然后在连接选项里加MYSQL_OPT_SSL_MODEREQUIRED。SSL 的代价是握手时间变长内网场景感知不明显公网场景则必须开启。如果编译客户端库时没带 OpenSSL连接时强行要求 SSL 会直接报SSL connection error这个坑在第 5 章还会详细讲。5. 避坑排查驱动 not loaded 到握手失败的 6 个典型坑5.1 驱动 not loaded 的真相插件没进 APK还是加载了但依赖缺失现象QSqlDatabase: QMYSQL driver not loaded程序在db.open()前就报错。原因分两层。第一层是插件根本没编出来或者没复制到 Qt 的 plugins 目录androiddeployqt打包时没把它收进去。第二层更隐蔽插件编出来了也进 APK 了但它依赖的libmariadb.so或libmysqlclient.so没有一并打包Android 的dlopen加载插件时找不到依赖库直接静默失败。解决先检查 APK 里有没有libqsqlmysql.so再看有没有客户端库 .so。用解压 APK 的方式确认两个文件都在lib/abi/目录下。如果只有驱动没有依赖库需要在 CMake 或部署脚本里把客户端库也复制到 Qt 的lib目录或者在.pro文件里用ANDROID_EXTRA_LIBS指定额外打包的 .so。这一步解决了 80% 的driver not loaded。5.2 MySQL 8 的 caching_sha2_password 把老驱动挡在门外现象驱动能加载但db.open()报Authentication plugin caching_sha2_password cannot be loaded或类似错误。原因MySQL 8.0 默认认证插件是caching_sha2_password而你编译 QMYSQL 驱动用的 MySQL 客户端库版本较老不支持这个认证方式握手时被服务端拒绝。解决最省事的是在服务端把该用户的认证方式改回mysql_native_passwordALTER USER app_user% IDENTIFIED WITH mysql_native_password BY your_password; FLUSH PRIVILEGES;但这个方法只适合内网可信环境。生产环境更推荐的做法是升级编译用的客户端库到支持 caching_sha2_password 的版本或者给客户端库加上 OpenSSL 支持后再编译 QMYSQL。改认证方式属于“后悔药”能快速让项目跑起来但别当作长期方案。5.3 服务端 wait_timeout 把空闲连接断掉客户端一无所知现象程序跑一段时间后第一次数据库操作会卡住或报MySQL server has gone away第二次操作又正常了。原因MySQL 服务端的wait_timeout默认 8 小时但很多运维会把空闲超时调小到几分钟。Android 设备锁屏后Qt 程序可能几十分钟没有数据库操作连接被服务端主动关闭客户端不知道还在用这条死连接。解决除了第 4 章的ensureConnected()探测重连机制还可以把连接选项里的MYSQL_OPT_RECONNECT打开db.setConnectOptions(MYSQL_OPT_RECONNECT1);这个选项让 MySQL 客户端库在检测到连接断开时自动重连。注意它只对空闲断开有效如果网络彻底断了TCP 层重连也无法恢复。所以在代码层面保留SELECT 1探测仍然是必要的两条防线一起用Android 端断线重连才算完整。5.4 32 位与 64 位 ABI 混用驱动能编出来但一运行就崩现象自编译的 QMYSQL 驱动在 arm64-v8a 设备上正常运行换到老的 armeabi-v7a 设备上一打开数据库就段错误崩溃。原因交叉编译时ANDROID_ABI指定的是arm64-v8a编出来的 .so 是 64 位指令集装到 32 位系统上直接跑不了。反过来也一样用armeabi-v7a编的驱动在 64 位设备上虽然能用但因为 Android 兼容层只能跑 32 位进程整个应用都得是 32 位限制了内存使用。解决如果你的应用需要同时支持两种 ABI就要分别编两次 QMYSQL 驱动和客户端库然后把两个版本的 .so 都放到 Qt 安装目录的对应 ABI 子目录下。Qt 的androiddeployqt在打包时会根据应用的minSdk和targetSdk自动选择 ABI 集合你只需要保证每个 ABI 目录里都有对应的 .so 即可。检查方法还是在打包后解压 APK看lib/下的子目录结构和 .so 文件是否齐全。5.5 服务端权限表没放行Android 设备连不上电脑却能连现象同一个 MySQL 服务器PC 上的 MySQL Workbench 连得通Android 平板上的 Qt 程序连不通报Host 192.168.1.50 is not allowed to connect to this MySQL server。原因MySQL 的用户权限表里app_user的Host字段可能只允许了localhost或某个固定 IPAndroid 设备的 IP 在权限范围之外。解决在服务端执行GRANT ALL PRIVILEGES ON device_data.* TO app_user% IDENTIFIED BY your_password; FLUSH PRIVILEGES;用%表示允许任意 IP 访问只建议在内网使用。如果服务器有公网 IP强烈建议在防火墙层限制 3306 端口的来源 IP否则任何人都能尝试爆破数据库。这个坑和 Qt 本身无关但却是“电脑能连、手机不能连”最常见的原因排查顺序排在网络配置检查之后。5.6 服务器防火墙拦 3306连接超时的隐形元凶现象db.open()一直卡到连接超时报Connection timed out或者Cant connect to MySQL server on x.x.x.x (110)。原因云服务器或公司服务器防火墙没有放行 3306 端口。PC 上可能因为走内网或防火墙规则不同而能连Android 设备走的是另一条网段被拦住了。解决先在服务器上用telnet或nc从外部测端口nc -vz 192.168.1.100 3306如果Connection refused或超时确认服务端 MySQL 是否监听0.0.0.0:3306netstat -tlnp | grep 3306看监听地址然后检查防火墙规则放行 3306。注意 MySQL 默认bind-address127.0.0.1只监听本机回环这种情况外部设备永远连不上需要改成bind-address0.0.0.0或指定网卡 IP改完重启 MySQL。这属于服务器配置问题但排查时最容易和 Qt 驱动问题搞混建议当成第一优先级检查。6. 验证与进阶服务器抓包确认连接设备端心跳保活驱动编译好、代码写完之后别急着上线先用抓包确认连接真的通了吗。在 MySQL 服务器上跑一条命令tcpdump -i eth0 -nn port 3306 -c 20 -w /tmp/mysql_android.pcap然后在 Android 端触发一次数据库连接操作回到服务器上看抓到的包。MySQL 握手流程有固定的字节特征客户端发一个握手响应包服务端回一个 OK 包。如果抓到了完整的来回说明连接链路、认证方式、权限表全部通过了如果只看到 SYN 没有后续说明 TCP 层被防火墙挡了如果看到握手后马上 RST通常是认证插件不匹配对照第 5 章逐项排查。进阶场景里设备端更省心的保活方式不是频繁做SELECT 1探测而是启动一个定时器按服务端wait_timeout的一半周期发心跳。比如服务端超时设置 60 秒客户端每 25 秒发一次SELECT 1保证连接永远不被服务端回收。这个定时器放在后台线程不要干扰 UI 线程的数据库操作。心跳的代价是每天几千次极轻量的查询对 MySQL 压力很小但换来的连接稳定性非常值。我在一个车间数据采集项目里用过这个方案十几台 Android 平板跑了大半年没有再出现过gone away的报错。每次遇到数据库连不上的问题我的习惯是先抓包看 TCP 层通不通再谈驱动和认证这个顺序能让排查效率翻倍。希望帮到你。本文还有配套的精品资源点击获取