你可以在 Capacitor 应用中使用自定义 SQLite 扩展,但只能作为编译进应用的原生 code:在 iOS 上,需要静态链接扩展,并在每次连接时调用其 init 函数;在 Android 上,需要打包一个支持加载扩展的 SQLite 构建,并按 CPU 架构将扩展作为原生库随应用提供。本指南会说明两个平台为何不同,给出一个可用的 C 扩展示例,并展示针对以下内容的具体原生改动: @capgo/capacitor-fast-sql 在 Capacitor 8 上。
在写 C 之前,先确认是否真的需要。很多“扩展”其实已经内置了。
什么是 SQLite 扩展
扩展是原生 code,会向 SQLite 连接注册新内容:标量函数(haversine_km(...))、聚合函数、排序规则 (collations)、虚拟表和 FTS5 分词器。每个扩展都暴露一个入口点:
int sqlite3_<name>_init(sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi);
SQLite 每个连接会调用一次。你注册的内容只在该连接中有效。
在移动应用里写扩展的常见原因:
- 为内置分词器处理不佳的语言
unicode61编写自定义 FTS5 分词器; - 在应用中需要的数学或地理函数
WHERE以及ORDER BY(如距离排序、评分); - 能按用户预期方式排序名称的排序规则 (collation),
- 用于设备端检索的向量搜索扩展。
检查已内置的功能
在支持的最旧 iOS 和 Android 版本上,通过你的插件运行这些:
import { FastSQL } from '@capgo/capacitor-fast-sql';
const db = await FastSQL.connect({ database: 'probe' });
const [{ v }] = await db.query('SELECT sqlite_version() AS v');
const options = await db.query('PRAGMA compile_options');
const functions = await db.query('SELECT name FROM pragma_function_list ORDER BY name');
console.log(v, options.map((r) => r.compile_options));
通常无需扩展即可实现的功能:
| 需要 | 内置功能 | 查看 |
|---|---|---|
| 列中的 JSON | JSON 函数(json_extract, ->>) |
SQLite 3.38 起内置。旧版 Android 系统 SQLite 可能缺少这些功能 |
| 全文搜索 | FTS4、FTS5 | ENABLE_FTS4 / ENABLE_FTS5 中的 compile_options |
| 空间边界框 | R*Tree | ENABLE_RTREE |
sin, cos, sqrt, pow |
数学函数 | ENABLE_MATH_FUNCTIONS,SQLite 3.35+ |
如果每个目标平台都支持该功能,就到此为止。Fast SQL 的 Web build 使用官方 SQLite Wasm,其中包含 JSON、FTS5 和数学函数。
为什么 iOS 和 Android 的表现不同
iOS。 系统 libsqlite3 使用以下技术构建 SQLITE_OMIT_LOAD_EXTENSION,因此 load_extension() 并不存在。App Store 规则也禁止下载并执行新的 code (指南 2.5.2),因此加载一个 .dylib 运行时加载本来就行不通。扩展必须编译进 App 二进制文件。许多教程随后会在 sqlite3_auto_extension() 启动时调用。不要依赖它:在 Xcode 自带的 iOS SDK 中, sqlite3.h 用它声明 API_DEPRECATED("Process-global auto extensions are not supported on Apple platforms", ...). 可靠的做法是每个连接句柄一打开就调用你的初始化函数。
Android。 Android 框架内置的 SQLite (android.database.sqlite) 没有用于加载扩展的 API。你需要一个随 App 打包且支持该功能的 SQLite 构建。 requery/sqlite-android 是框架类的即插即用替代品,自带较新的 SQLite(含 FTS5 和 JSON),并支持通过以下方式使用可加载扩展 SQLiteCustomExtension. 你的扩展会变成 .so APK 内每个 ABI 各一个文件。
Web。 标准 SQLite Wasm 构建无法加载原生 code。你需要一个将扩展编译进去的自定义 SQLite Wasm 构建。
插件目前支持情况
| iOS | Android | Web 端 | |
|---|---|---|---|
@capgo/capacitor-fast-sql 8.2 |
无需 hook,需要打补丁 | 使用 framework 的 SQLite,需要打补丁 | 标准 SQLite Wasm |
@capacitor-community/sqlite 8.1 |
loadExtension() 在 JS API 中声明,但原生端未实现 |
同上 | 未实现 |
所以目前添加扩展意味着要打一个小型原生补丁。这个改动范围可控,Bun 可以在多次安装中保持补丁生效。
步骤 1:编写扩展
距离函数就是一个很好的例子:实用、独立且易于验证。将其保存为 geo.c:
#include <math.h>
#include <stddef.h>
#include "sqlite3ext.h"
SQLITE_EXTENSION_INIT1
static void haversine_km(sqlite3_context *ctx, int argc, sqlite3_value **argv) {
(void)argc;
for (int i = 0; i < 4; i++) {
if (sqlite3_value_type(argv[i]) == SQLITE_NULL) {
sqlite3_result_null(ctx);
return;
}
}
const double earth_radius_km = 6371.0088;
const double to_rad = M_PI / 180.0;
double lat1 = sqlite3_value_double(argv[0]) * to_rad;
double lon1 = sqlite3_value_double(argv[1]) * to_rad;
double lat2 = sqlite3_value_double(argv[2]) * to_rad;
double lon2 = sqlite3_value_double(argv[3]) * to_rad;
double dlat = lat2 - lat1;
double dlon = lon2 - lon1;
double a = sin(dlat / 2) * sin(dlat / 2) +
cos(lat1) * cos(lat2) * sin(dlon / 2) * sin(dlon / 2);
sqlite3_result_double(ctx, 2 * earth_radius_km * asin(sqrt(a)));
}
int sqlite3_geo_init(sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi) {
SQLITE_EXTENSION_INIT2(pApi);
(void)pzErrMsg;
return sqlite3_create_function_v2(
db, "haversine_km", 4,
SQLITE_UTF8 | SQLITE_DETERMINISTIC | SQLITE_INNOCUOUS,
NULL, haversine_km, NULL, NULL, NULL);
}
同一个文件支持两种构建方式:
- 作为可加载扩展 (Android):
SQLITE_EXTENSION_INIT1/2将所有 SQLite 调用路由到pApiSQLite 传入的指针,因此.so不会链接 SQLite。 - 静态链接,使用
-DSQLITE_CORE(iOS):宏变为空操作,调用直接指向应用链接的 SQLite 库。
SQLITE_DETERMINISTIC 让 SQLite 能在表达式索引中使用该函数。 SQLITE_INNOCUOUS 标记该函数在视图和触发器中可安全使用。
我在 macOS 上用两种方式编译了这个文件:静态链接使用 -DSQLITE_CORE 并调用了 sqlite3_geo_init(db, NULL, NULL) 在系统 SQLite 连接上,以及作为可加载库通过 load_extension。两者返回的巴黎到伦敦距离均约为 343.6 公里。
步骤 2:iOS,编译到应用中并按连接注册
将源文件添加到应用目标
- 在 Xcode 中,将
geo.c拖入App组,并勾选App目标。 - Xcode 会提示创建桥接头文件。接受提示,或创建
App/App-Bridging-Header.h并将其设置在 Build Settings > Swift Compiler - General > Objective-C Bridging Header. - 在 Build Phases > Compile Sources,双击
geo.c并添加单文件标志-DSQLITE_CORE.
iOS SDK 同时提供 sqlite3.h 以及 sqlite3ext.h,因此无需额外的头文件。
App-Bridging-Header.h:
#include <sqlite3.h>
int sqlite3_geo_init(sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi);
修补 Fast SQL 以暴露 open 钩子
该插件在其自身模块内打开连接,因此需要一个应用可以设置的钩子。Bun 可以修补依赖项,并在每次安装时重新应用该补丁:
bun patch @capgo/capacitor-fast-sql
编辑 node_modules/@capgo/capacitor-fast-sql/ios/Sources/CapgoCapacitorFastSqlPlugin/SQLDatabase.swift。在文件顶层添加一个公共钩子:
public enum FastSqlExtensions {
/// Called with every new connection handle right after it opens.
/// Return SQLITE_OK (0) on success.
public static var onOpen: ((OpaquePointer) -> Int32)?
}
然后在 init(...)中,在加密块之后、 try applyPerformanceOptions(...),调用它:
if let hook = FastSqlExtensions.onOpen {
let rc = hook(db)
if rc != SQLITE_OK {
throw SQLError.openFailed(message: "Extension init failed with code \(rc)")
}
}
保存补丁:
bun patch --commit node_modules/@capgo/capacitor-fast-sql
Bun 会在以下路径写入文件: patches/ ,并将其记录在 package.json 在……下 patchedDependencies。提交这两个文件。我们关于 如何修补 Capacitor 插件 的指南详细介绍了此工作流程。
在启动时注册扩展
在 ios/App/App/AppDelegate.swift,导入插件模块并在 application(_:didFinishLaunchingWithOptions:):
import UIKit
import Capacitor
import CapgoCapacitorFastSql // CocoaPods module name. With SPM: import CapgoCapacitorFastSqlPlugin
// Inside the existing AppDelegate class:
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
FastSqlExtensions.onOpen = { db in
sqlite3_geo_init(db, nil, nil)
}
return true
}
由于该钩子在插件的打开路径中运行,因此每个连接都会获得该函数,包括在 WebView 重新加载后打开的连接。在 didFinishLaunching 确保在 JavaScript 调用之前已就位 connect().
SQLCipher 构建。 如果你使用插件的 SQLCipher subspec,连接属于 SQLCipher 的 SQLite,而你的 -DSQLITE_CORE 文件可能链接到系统库的符号。在发布前明确测试此组合,或将依赖扩展的数据存储在未加密的数据库中。
步骤 3:Android,打包可加载扩展的 SQLite
为每个 ABI 编译扩展
你需要 Android NDK(从 Android Studio 的 SDK 管理器安装)以及 sqlite3ext.h 加 sqlite3.h 与 SQLite amalgamation 旁边 geo.c.
export NDK=$HOME/Library/Android/sdk/ndk/<version>
export TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/darwin-x86_64 # linux-x86_64 on Linux
export API=24 # your minSdkVersion
for pair in \
aarch64-linux-android:arm64-v8a \
armv7a-linux-androideabi:armeabi-v7a \
x86_64-linux-android:x86_64 \
i686-linux-android:x86
do
triple=${pair%%:*}
abi=${pair##*:}
mkdir -p android/app/src/main/jniLibs/$abi
$TOOLCHAIN/bin/clang --target=$triple$API \
-shared -fPIC -O2 \
-Wl,-z,max-page-size=16384 \
-o android/app/src/main/jniLibs/$abi/libgeo.so \
geo.c -lm
done
-Wl,-z,max-page-size=16384 为具有 16 KB 内存页的设备对齐库。Google Play 要求针对 Android 15 及更高版本的应用支持 16 KB,参见 16 KB 页面大小与 Capacitor 插件。如果您的应用仅面向 64 位设备发布,可以移除两个 32 位 ABI。
该库命名为 libgeo.so,因此 SQLite 的默认入口点名称为 sqlite3_geo_init,与 C 函数相匹配。
在安装时提取原生库
较新版本的 Android Gradle Plugin 会保留 .so 文件在 APK 中未压缩,而不是提取它们。SQLite 通过文件路径加载扩展,因此请在 android/app/build.gradle:
android {
packaging {
jniLibs {
useLegacyPackaging = true
}
}
}
添加 requery sqlite-android
在 android/build.gradle (root),添加 JitPack:
allprojects {
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
}
}
给 Fast SQL 打补丁以使用它
打开同一个 Bun 补丁会话 (bun patch @capgo/capacitor-fast-sql) 并编辑两个文件。
node_modules/@capgo/capacitor-fast-sql/android/build.gradle,在 dependencies:
implementation 'com.github.requery:sqlite-android:3.49.0'
使用 JitPack 为该仓库显示的最新已构建版本。
node_modules/@capgo/capacitor-fast-sql/android/src/main/java/app/capgo/capacitor/fastsql/SQLDatabase.java:将 framework 的 import 换成 requery 的,并通过包含这些扩展的配置打开。
// Replace:
// import android.database.sqlite.SQLiteDatabase;
// import android.database.sqlite.SQLiteStatement;
import io.requery.android.database.sqlite.SQLiteCustomExtension;
import io.requery.android.database.sqlite.SQLiteDatabase;
import io.requery.android.database.sqlite.SQLiteDatabaseConfiguration;
import io.requery.android.database.sqlite.SQLiteStatement;
public class SQLDatabase implements DatabaseConnection {
/** Set from MainActivity before the first connection opens. */
public static final java.util.List<SQLiteCustomExtension> extensions =
new java.util.concurrent.CopyOnWriteArrayList<>();
private SQLiteDatabase db;
private volatile boolean inTransaction = false;
public SQLDatabase(String path, boolean walMode, boolean performancePresets) throws Exception {
SQLiteDatabaseConfiguration config = new SQLiteDatabaseConfiguration(
path, SQLiteDatabase.CREATE_IF_NECESSARY);
config.customExtensions.addAll(extensions);
this.db = SQLiteDatabase.openDatabase(config, null, null);
SQLPerformanceConfig.apply(db::execSQL, walMode, performancePresets);
}
// ... the rest of the class stays as it is
}
requery 的类与 framework API 一一对应 (rawQuery, compileStatement, execSQL,transactions),因此文件其余部分无需改动即可编译。 android.database.Cursor 保持原样。用以下命令提交补丁: bun patch --commit node_modules/@capgo/capacitor-fast-sql.
这只改动未加密路径。加密连接使用 SQLCipher 自己的类,本补丁不会触及。
在 MainActivity 中注册扩展
package com.example.app;
import android.os.Bundle;
import app.capgo.capacitor.fastsql.SQLDatabase;
import com.getcapacitor.BridgeActivity;
import io.requery.android.database.sqlite.SQLiteCustomExtension;
public class MainActivity extends BridgeActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
String path = getApplicationInfo().nativeLibraryDir + "/libgeo.so";
SQLDatabase.extensions.add(new SQLiteCustomExtension(path, "sqlite3_geo_init"));
super.onCreate(savedInstanceState);
}
}
先注册 super.onCreate 这样列表会在 WebView 运行任何 JavaScript 之前填充好。
无 C 的 Android:Java 函数
如果你只需要一个标量函数,requery 可以注册一个用 Java 编写的函数,无需 NDK。在修补后的 SQLDatabase 构造函数中,打开数据库后:
db.addFunction("haversine_km", 4, (args, result) -> {
double r = 6371.0088;
double lat1 = Math.toRadians(args.getDouble(0));
double lat2 = Math.toRadians(args.getDouble(2));
double dlat = lat2 - lat1;
double dlon = Math.toRadians(args.getDouble(3) - args.getDouble(1));
double a = Math.pow(Math.sin(dlat / 2), 2)
+ Math.cos(lat1) * Math.cos(lat2) * Math.pow(Math.sin(dlon / 2), 2);
result.set(2 * r * Math.asin(Math.sqrt(a)));
});
对于重型查询,这比 C 慢,因为每次调用都要跨越 JNI,但维护起来简单得多。FTS5 分词器和虚拟表仍然需要 C。
步骤 4:从 JavaScript 中使用
在 bunx cap sync 以及一次原生重新构建:
import { FastSQL } from '@capgo/capacitor-fast-sql';
const db = await FastSQL.connect({ database: 'places' });
await db.execute(`CREATE TABLE IF NOT EXISTS places (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
lat REAL NOT NULL,
lon REAL NOT NULL
)`);
const nearby = await db.query(
`SELECT id, name, haversine_km(?, ?, lat, lon) AS km
FROM places
WHERE lat BETWEEN ? AND ? AND lon BETWEEN ? AND ?
ORDER BY km
LIMIT 20`,
[userLat, userLon, userLat - 0.5, userLat + 0.5, userLon - 0.7, userLon + 0.7],
);
本指南中介绍的 BETWEEN box 让 SQLite 使用索引来 (lat, lon) 在函数计算精确距离之前缩小候选范围。如果没有它,函数将在每一行上运行。
Web 端的 JavaScript 回退方案
Web build 无法加载你的 C code,请检测并回退:
import { Capacitor } from '@capacitor/core';
export const hasGeoExtension = Capacitor.isNativePlatform();
// On the web: query the bounding box, then sort in JS with the same formula.
自定义 FTS5 分词器
同一机制会注册 FTS5 分词器。在你的 init 函数中,获取 fts5_api 指针,并使用 SELECT fts5(?1) 来自 FTS5 文档的模式,然后调用 xCreateTokenizer。还有两个额外要求:
- SQLite 库必须包含 FTS5。requery 的 build 已包含。在 iOS 上,请检查
PRAGMA compile_options来实现ENABLE_FTS5. - 所有对 FTS 表进行读写的连接都必须注册该分词器,因为 FTS5 会在表定义中存储分词器名称。这就是每个连接的 hook 很重要的原因。
CREATE VIRTUAL TABLE docs USING fts5(title, body, tokenize = 'my_tokenizer');
发布与维护
- 需要原生重新构建。 扩展、补丁和 Gradle 更改属于原生层。它们需要重新构建 App Store 和 Play Store 版本。 Capgo 构建 可以在云端完成构建和签名。自 2026 年 4 月起,iOS 上传必须使用 Xcode 26 或更高版本构建。
- JavaScript 依然可以快速迭代。 调用
haversine_km位于你的 web bundle 中,并可通过 实时更新进行分发,前提是包含该扩展的原生构建已安装。如果用户可能仍在使用旧版二进制文件,请通过原生版本检查来控制新 SQL 的启用。 - 插件升级。 当你更新 Fast SQL 时,Bun 会重新应用补丁。如果补丁涉及的行发生移动,安装会明确失败,而不是静默丢弃你的 hook。请在新版本上重新创建补丁。
故障排除
no such function: haversine_km 在 iOS 上协调。 之前未设置 hook connect(),或者数据库在此之前就已打开 didFinishLaunching 运行了。请在 hook 内打日志,确认它被调用。
内部崩溃 sqlite3_geo_init 在 iOS 上协调。 -DSQLITE_CORE 缺失,因此该文件被编译为可加载扩展,并解引用了 pApi 指针,该指针位于 nil 此处。请在 Compile Sources 中为该文件添加 flag。
no such function 在 Android 上。 打过补丁的 SQLDatabase 仍会导入 android.database.sqlite.SQLiteDatabase,或者该扩展是在首次连接后才添加到列表中的。记录 SQLDatabase.extensions.size() 连接时的日志。
dlopen failed: library "…/libgeo.so" not found. 设备 ABI 缺少库文件(大多数手机是 arm64-v8a),或者原生库未被解压。检查 useLegacyPackaging 并用 Android Studio 的 APK Analyzer 检查 APK。
undefined symbol: sqlite3_geo_init. 传递给 SQLiteCustomExtension 与 C 函数不匹配,或者该函数被声明为 static.
Play Console 会警告 16 KB 对齐问题。 重新构建 .so 文件,使用 -Wl,-z,max-page-size=16384 较新的 NDK,并将 requery 更新为使用 16 KB 对齐构建的版本。
该函数在 Android 上正常,但在 iOS 模拟器中不起作用。 请确保 geo.c 是……的成员 App target,而不仅仅存在于文件树中。
总结
| 平台 | 扩展如何被引入 | 注册位置 |
|---|---|---|
| iOS | 通过以下方式编译进 App: -DSQLITE_CORE |
调用以下内容的插件钩子: sqlite3_geo_init 在每次建立新连接时 |
| Android | .so 每个 ABI 在 jniLibs,由 requery sqlite-android 加载 |
SQLiteCustomExtension 每次连接打开时读取的列表 |
| Web 端 | 原生 SQLite Wasm 不支持 | JavaScript 回退方案 |
先检查内置功能。如果仍需要扩展,请用 C 编写一次,保持原生补丁精简,并在你支持的最旧设备上测试。关于通用插件配置,请参阅 如何在 Capacitor 应用中使用 SQLite.