Android Studio+Sqlite3:利用原生 Sqlite3 安卓库实现自定义函数

2020年10月28日 15点29分

背景

  1. 安卓自带的 package android.database.sqlite 中 SQLiteCustomFunction.callback 的返回为 void,如下:
    /**
     * A callback interface for a custom sqlite3 function.
     * This can be used to create a function that can be called from
     * sqlite3 database triggers.
     * @hide
     */
    public interface CustomFunction {
        public void callback(String[] args);
    }

也就是说,自定义 Sqlite 函数只能在 java 层接受参数,而不能返回 native 层给 Sqlite 调用。这不是逗嘛。。。

  1. 所以考虑重写 Sqlite 提供给 android 的 java 接口,由于 android 自带的 sqlite package 不好修改。
    因此使用 Sqlite 原版提供给 android 调用的带C++源码的包SQLite Android Bindings。
  2. 将原版的包在自己的工程里面配置好,通过编译运行正常后,执行如下步骤。

Step1

进入 ..\sqlite3\src\main\java\org\sqlite\database\sqlite\SQLiteCustomFunction.java 文件,找到如下代码段

    // Called from native.
    @SuppressWarnings("unused")
    private void dispatchCallback(String[] args) {
        callback.callback(args);
    }

修改为

    // Called from native.
    @SuppressWarnings("unused")
    private String dispatchCallback(String[] args) {
        return callback == null ? null : callback.callback(args);
    }

Step2

进入 ..\sqlite3\src\main\java\org\sqlite\database\sqlite\SQLiteDatabase.java 文件,找到如下代码段

    /**
     * A callback interface for a custom sqlite3 function.
     * This can be used to create a function that can be called from
     * sqlite3 database triggers.
     * @hide
     */
    public interface CustomFunction {
        public void callback(String[] args);
    }

修改为

    /**
     * A callback interface for a custom sqlite3 function.
     * This can be used to create a function that can be called from
     * sqlite3 database triggers.
     * @hide
     */
    public interface CustomFunction {
        String callback(String[] args);
    }

Step3

进入 ..\sqlite3\src\main\jni\sqlite\android_database_SQLiteConnection.cpp 文件,找到如下代码段

int register_android_database_SQLiteConnection(JNIEnv *env)
{
    jclass clazz;
    FIND_CLASS(clazz, "org/sqlite/database/sqlite/SQLiteCustomFunction");

    GET_FIELD_ID(gSQLiteCustomFunctionClassInfo.name, clazz,
            "name", "Ljava/lang/String;");
    GET_FIELD_ID(gSQLiteCustomFunctionClassInfo.numArgs, clazz,
            "numArgs", "I");
    GET_METHOD_ID(gSQLiteCustomFunctionClassInfo.dispatchCallback,
            clazz, "dispatchCallback", "([Ljava/lang/String;)V");

    FIND_CLASS(clazz, "java/lang/String");
    gStringClassInfo.clazz = jclass(env->NewGlobalRef(clazz));

    return jniRegisterNativeMethods(env, 
        "org/sqlite/database/sqlite/SQLiteConnection",
        sMethods, NELEM(sMethods)
    );
}

    GET_METHOD_ID(gSQLiteCustomFunctionClassInfo.dispatchCallback,
            clazz, "dispatchCallback", "([Ljava/lang/String;)V");

修改为

    GET_METHOD_ID(gSQLiteCustomFunctionClassInfo.dispatchCallback,
            clazz, "dispatchCallback", "([Ljava/lang/String;)Ljava/lang/String;");

Step4

进入 ..\sqlite3\src\main\jni\sqlite\android_database_SQLiteConnection.cpp 文件,找到如下代码段

// Called each time a custom function is evaluated.
static void sqliteCustomFunctionCallback(sqlite3_context *context,
        int argc, sqlite3_value **argv) {

    JNIEnv* env = 0;
    gpJavaVM->GetEnv((void**)&env, JNI_VERSION_1_4);

    // Get the callback function object.
    // Create a new local reference to it in case the callback tries to do something
    // dumb like unregister the function (thereby destroying the global ref) while it is running.
    jobject functionObjGlobal = reinterpret_cast(sqlite3_user_data(context));
    jobject functionObj = env->NewLocalRef(functionObjGlobal);

    jobjectArray argsArray = env->NewObjectArray(argc, gStringClassInfo.clazz, NULL);
    if (argsArray) {
        for (int i = 0; i < argc; i++) {
            const jchar* arg = static_cast(sqlite3_value_text16(argv[i]));
            if (!arg) {
                ALOGW("NULL argument in custom_function_callback.  This should not happen.");
            } else {
                size_t argLen = sqlite3_value_bytes16(argv[i]) / sizeof(jchar);
                jstring argStr = env->NewString(arg, argLen);
                if (!argStr) {
                    goto error; // out of memory error
                }
                env->SetObjectArrayElement(argsArray, i, argStr);
                env->DeleteLocalRef(argStr);
            }
        }

        // TODO: Support functions that return values.
        env->CallVoidMethod(functionObj,
                gSQLiteCustomFunctionClassInfo.dispatchCallback, argsArray);

error:
        env->DeleteLocalRef(argsArray);
    }

    env->DeleteLocalRef(functionObj);

    if (env->ExceptionCheck()) {
        ALOGE("An exception was thrown by custom SQLite function.");
        /* LOGE_EX(env); */
        env->ExceptionClear();
    }
}

修改为

// Called each time a custom function is evaluated.
static void sqliteCustomFunctionCallback(sqlite3_context *context,
        int argc, sqlite3_value **argv) {

    JNIEnv* env = 0;
    gpJavaVM->GetEnv((void**)&env, JNI_VERSION_1_4);

    // Get the callback function object.
    // Create a new local reference to it in case the callback tries to do something
    // dumb like unregister the function (thereby destroying the global ref) while it is running.
    jobject functionObjGlobal = reinterpret_cast(sqlite3_user_data(context));
    jobject functionObj = env->NewLocalRef(functionObjGlobal);

    jobjectArray argsArray = env->NewObjectArray(argc, gStringClassInfo.clazz, NULL);

    jstring callBackFromJava;
    const char* resultStr;

    if (argsArray) {
        for (int i = 0; i < argc; i++) {
            const jchar* arg = static_cast(sqlite3_value_text16(argv[i]));
            if (!arg) {
                ALOGW("NULL argument in custom_function_callback.  This should not happen.");
            } else {
                size_t argLen = sqlite3_value_bytes16(argv[i]) / sizeof(jchar);
                jstring argStr = env->NewString(arg, argLen);
                if (!argStr) {
                    goto error; // out of memory error
                }
                env->SetObjectArrayElement(argsArray, i, argStr);
                env->DeleteLocalRef(argStr);
            }
        }

        // TODO: Support functions that return values.
//        env->CallVoidMethod(functionObj,
//                            gSQLiteCustomFunctionClassInfo.dispatchCallback, argsArray);
        callBackFromJava = reinterpret_cast(env->CallObjectMethod(functionObj,
                gSQLiteCustomFunctionClassInfo.dispatchCallback, argsArray));

        resultStr = env->GetStringUTFChars(callBackFromJava, NULL);
//        __android_log_write(ANDROID_LOG_INFO, "resultStr.", resultStr);

        sqlite3_result_text(context, resultStr, env->GetStringLength(callBackFromJava), sqlite3_free);
        env->ReleaseStringUTFChars(callBackFromJava, resultStr);

error:
        env->DeleteLocalRef(argsArray);
    }

    env->DeleteLocalRef(functionObj);

    if (env->ExceptionCheck()) {
        ALOGE("An exception was thrown by custom SQLite function.");
        /* LOGE_EX(env); */
        env->ExceptionClear();
    }
}

Android 使用测试

伪代码:配置数据库

SQLiteOpenHelper mSQLiteOpenHelper = new SQLiteOpenHelper() {
    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {

    }

    @Override
    public void onOpen(@NotNull SQLiteDatabase db) {
        // 实现简单的字符串连接功能
        db.addCustomFunction("StringFunc", 1, args -> {
            return args[0] + "@" + args[0];
        });
    }

    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {

    }
};

伪代码:执行查询

SQLiteDatabase db = mSQLiteOpenHelper.getWritableDatabase();
try {
    Cursor cursor = db.rawQuery("SELECT StringFunc(id) From TableA WHERE id < 5" , null);
    cursor.moveToFirst();
    int row = 0;
    while (!cursor.isAfterLast()) {
        Log.d("test", "row." + (i++) + " StringFunc. " + cursor.getString(0));
        cursor.moveToNext();
    }
    cursor.close();
} catch (Exception e) {
    e.printStackTrace();
} finally {
    db.close();
}

进阶 1

如果不是必须使用 android 层的 java 函数,在已经可以重新编译 Sqlite3 的情况下,为什么要考虑重构它提供的 java-native 层调用呢?直接修改 sqlite3.c 源文件,将其变成 build-in 内置函数不香嘛?
实现思路:在 sqlite3.c 源文件里找到如下函数,在里面增加注册额外的自定义函数。

SQLITE_PRIVATE void sqlite3RegisterBuiltinFunctions(void) {
//...
}

进阶 2

还可以利用 Sqlite3 load_extension(X) 加载扩展的方法加载 .so 库,实现更多功能。不过 android 原本集成的 sqlite 没有启用加载权限,对数据库执行如下语句加载路径“...\libabc.so”的 extension 会出现“not authorized”错误:

SELECT load_extension('...\libabc.so')

所以还是要修改 sqlite3.c 源文件在适合的地方添加语句使其允许加载扩展。

方法一

在 sqlite3.c 文件里找到如下语句

static void loadExt(sqlite3_context *context, int argc, sqlite3_value **argv){
//...
}

在其真正有效执行的开头加入代码,如下:

static void loadExt(sqlite3_context *context, int argc, sqlite3_value **argv){
//...
  sqlite3_enable_load_extension(db, 1);
//...
}

此法比较暴力,任何加载 extension 的行为都首先得到允许。

方法二

在相应 Anroid.mk 文件【ps: android studio + Sqlite3(SQLite Android Bindings) 环境】添加配置语句

LOCAL_CFLAGS += -DSQLITE_ENABLE_LOAD_EXTENSION

且找到 sqlite3.c 文件的如下代码段

#ifdef SQLITE_ENABLE_LOAD_EXTENSION
                 | SQLITE_LoadExtension
#endif

修改成

#ifdef SQLITE_ENABLE_LOAD_EXTENSION
                 | SQLITE_LoadExtension
                 | SQLITE_LoadExtFunc
#endif

最后重新编译,即可允许 Sqlite3 数据库使用如下语句加载地址为“...\libabc.so”的扩展:

SELECT load_extension('...\libabc.so')

结束

你可能感兴趣的:(Android Studio+Sqlite3:利用原生 Sqlite3 安卓库实现自定义函数)