65.9K
CodeProject 正在变化。 阅读更多。
Home

从 Android 资源资产中读取 SQLite 数据库

starIconstarIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIcon

2.82/5 (3投票s)

2018年3月21日

CPOL

2分钟阅读

viewsIcon

22530

downloadIcon

61

演示了一种处理位于 asset 文件夹中的 SQLite 数据库的方法。

下载 app-release.7z

下载 SQLiteXTest.7z

引言

提高从 Android 资源读取 SQLite 文件时的性能

背景

  1. Android SQLiteDatabase
  2. Android 资源处理
  3. SQLite VFS
  4. SQLite URI

使用代码

通常情况下,如果我们想从 Android 资源文件读取 SQLite 数据库,我们必须将资源文件复制到本地文件夹,然后从本地文件读取数据库。

这种方法有一些缺点:

  1. 浪费磁盘空间
    如果数据库文件大小有点大
  2. 浪费 CPU
  3. 存在安全漏洞
    用户在 root 设备后可以替换本地数据库文件

为了克服这些缺点,我们将实现一个新的 SQLite VFS,它支持 Android 资源。

SQLite 在 Unix 系统上使用 unix-vfs,在 Windows 系统上使用 win32-vfs

Android 封装了 SQLite 代码并导出了一些 Java 接口 (android.database.sqlite.SQLiteDatabase),但缺少原始 SQLite 实现中的一些高级功能

  1. 自定义函数
  2. 加密
  3. URI 文件语法
  4. VFS
实际上,Android 已经包含上述功能在 libsqlite.so 中,但没有提供 Java 接口/入口点,并且根据 Android O+ 安全行为变更,开发者不应在后续 Android 版本中访问 libsqlite.so

幸运的是,SQLite 开发者已经提供了一个类似的 Java 包装器:SQLite Android Bindings,它可以提供这些功能

类名和成员名称基本相同,因此您可以导入 aar 并更改 Java 源代码的导入方式

import android.database.sqlite.SQLiteDatabase;

to

import org.sqlite.database.sqlite.SQLiteDatabase;

使用步骤

  1. 禁用 Android 资源中 SQLiteDatabase 文件的压缩
    在 build.gradle 中
    aaptOptions {
        noCompress 'db'
    }
    
  2. 实现 SQLite VFS 并注册它
    sqlite3_vfs_register(&AndroidAsset::vfs, false);
    sqlite3_vfs_register(&AssetFDMap::vfs, false);
    sqlite3_vfs_register(&AssetFD::vfs, false);
    
  3. 使用自定义 URI 打开资源文件夹中的数据库文件

我为不同的场景实现了三个 VFS:

第一个 VFS:android_asset

Java (使用自定义 SQLite URI 打开 SQLiteDatabase)

try (SQLiteDatabase db = SQLiteDatabase.openDatabase("file:asset_db.db?vfs=android_asset&immutable=1&mode=ro", null, SQLiteDatabase.OPEN_READONLY)) {
   ................................
}

原生

static int xRead(sqlite3_file *file, void *buf, int iAmt, sqlite3_int64 iOfst) {
    vfs_file *f = (vfs_file *) file;
    int expectReadLen = (iAmt + iOfst > f->length) ? (f->length - iOfst) : iAmt;
    int readLen = pread64(f->fd, buf, expectReadLen, iOfst + f->offset);
    if (readLen < 0) {
        return SQLITE_IOERR_READ;
    } else if (readLen == expectReadLen) {
        return SQLITE_OK;
    } else {
        memset((__uint8_t *) buf + readLen, 0, iAmt - readLen);
        return SQLITE_IOERR_SHORT_READ;
    }
}

static int vfsOpen(sqlite3_vfs *vfs, const char *path, sqlite3_file *file, int flags,
                   int *outflags) {

    ALOGD("%s:: path=%s flags=%x", __FUNCTION__, path, flags);
    if (g_AAssetManager == NULL) {
        return SQLITE_ERROR;
    }
    vfs_file *f = (vfs_file *) file;
    f->pMethods = &vfs_io_methods;
    AAsset *asset = AAssetManager_open(g_AAssetManager, path, AASSET_MODE_RANDOM);
    if (asset == NULL) {    //if the asset file don't exist
        return SQLITE_NOTFOUND;
    }
    f->fd = AAsset_openFileDescriptor64(asset, &f->offset, &f->length);
    AAsset_close(asset);
    if (f->fd < 0) {  //if the asset file is compressed
        return SQLITE_NOTFOUND;
    }
    *outflags = flags;
    return SQLITE_OK;
}

第二个 VFS:asset_fd_map

Java (使用自定义 SQLite URI 打开 SQLiteDatabase)

try (AssetFileDescriptor afd = getAssets().openFd("asset_db.db")) {
    try (SQLiteDatabase db = SQLiteDatabase.openDatabase(String.format("file:%X_%X_%X?vfs=asset_fd_map&immutable=1&mode=ro", afd.getParcelFileDescriptor().getFd(), afd.getStartOffset(), afd.getLength()), null, SQLiteDatabase.OPEN_READONLY)) {
     .................................
  }
}

原生

static int xRead(sqlite3_file *file, void *buf, int iAmt, sqlite3_int64 iOfst) {
    vfs_file *f = (vfs_file *) file;
    int expectReadLen = (iAmt + iOfst > f->length) ? (f->length - iOfst) : iAmt;
    memcpy(buf, (__uint8_t *) f->address + iOfst + f->offset, expectReadLen);
    int readLen = expectReadLen;
    return SQLITE_OK;
}

static int vfsOpen(sqlite3_vfs *vfs, const char *path, sqlite3_file *file, int flags,
                   int *outflags) {

    ALOGD("%s:: path=%s flags=%x", __FUNCTION__, path, flags);
    vfs_file *f = (vfs_file *) file;
    f->pMethods = &vfs_io_methods;
    if (3 > sscanf(path, "%x_%llx_%llx", &f->fd, &f->offsetFileStart, &f->length)) {
        return SQLITE_ERROR;
    }
    //because mmap() require the offset must be on the page boundary
    __int64_t offsetToPage = (f->offsetFileStart / 4096) * 4096;
    f->offsetMapStart = f->offsetFileStart - offsetToPage;
    f->mapLength = f->length + f->offsetMapStart;

    f->address = mmap64(NULL, f->mapLength, PROT_READ, MAP_PRIVATE, f->fd, offsetToPage);
    if (f->address == MAP_FAILED) {
        return SQLITE_ERROR;
    }
    *outflags = flags;
    return SQLITE_OK;
}

第三个 VFS:asset_fd

Java (使用自定义 SQLite URI 打开 SQLiteDatabase)

try (AssetFileDescriptor afd = getAssets().openFd("asset_db.db")) {
    try (SQLiteDatabase db = SQLiteDatabase.openDatabase(String.format("file:%X_%X_%X?vfs=asset_fd&immutable=1&mode=ro", afd.getParcelFileDescriptor().getFd(), afd.getStartOffset(), afd.getLength()), null, SQLiteDatabase.OPEN_READONLY)) {
     .................................
  }
}

原生

static int xRead(sqlite3_file *file, void *buf, int iAmt, sqlite3_int64 iOfst) {
    vfs_file *f = (vfs_file *) file;
    int expectReadLen = (iAmt + iOfst > f->length) ? (f->length - iOfst) : iAmt;
    int readLen = pread64(f->fd, buf, expectReadLen, iOfst + f->offset);
    if (readLen < 0) {
        return SQLITE_IOERR_READ;
    } else if (readLen == expectReadLen) {
        return SQLITE_OK;
    } else {
        memset((__uint8_t *) buf + readLen, 0, iAmt - readLen);
        return SQLITE_IOERR_SHORT_READ;
    }
}

static int vfsOpen(sqlite3_vfs *vfs, const char *path, sqlite3_file *file, int flags,
                   int *outflags) {

    ALOGD("%s:: path=%s flags=%x", __FUNCTION__, path, flags);
    vfs_file *f = (vfs_file *) file;
    f->pMethods = &vfs_io_methods;
    if (3 > sscanf(path, "%x_%llx_%llx", &f->fd, &f->offset, &f->length)) {
        return SQLITE_ERROR;
    }
    *outflags = flags;
    return SQLITE_OK;
}

在演示项目中,我比较了不同的方法。

经过的时间差异似乎不太大,这可能是因为性能瓶颈在于 SQLite 内部数据处理,而不是文件处理。

© . All rights reserved.