文/何曉杰Dev(高級Android架構師)著作權歸作者所有,轉載請聯系作者獲得授權。
初看這個標題你可能會不解,SQLite 本身就是一個跨平臺的數據庫,在這里再說跨平臺有什么意義呢?
其實不然,目前我就遇到了一個項目需要使用 SQLite 數據庫,而且我甚至完全不想花多套代碼在不同的平臺上,畢竟每個平臺的包含的相關 SDK 并不一致。舉個簡單的例子,在 Android 上操作 SQLite,需要用到?SQLiteDatabase?這個類,用?Java?來操作;而在 iOS 上,除了需要引入?libsqlite3.tbd?外,還需要引入?sqlite3.h?這個頭文件,使用?Objective-C?來操作,到了 PC 上,雖然都是以使用?sqlite3.h?為主,但是依然會有不一致的地方,比如說種類繁多的編程語言,大多都有不同的封裝,API 不一致這足以讓人頭疼。
因此,在不同的平臺上操作 SQLite,必定會使用不同的代碼。當然了,除了 SQLite 之外,實現相同的功能,在不同平臺上使用不同的代碼也許已經是慣例,大家也習以為常。
請輸入標題 ? ? bcdef
Roll your eggs 的習以為常!作為一個懶人,當這樣一個鍋需要自己背的時候,自然是去找更簡單的解決方案了。目標是一套代碼走天下!
請輸入標題 ? ? abcdefg
那么也不多廢話了,直接上手寫代碼,這里有很多種技術可以選擇,比如說?C++,sqlite3.h?還是很好用的。不過我依然是折騰自己喜歡的?CodeTyphon,因為它有更讓人覺得方便的封裝。
很幸運的是,CodeTyphon?已經自帶了?sqlite3conn?單元,直接引用之即可。關于如何查找可引用的庫,可以看?CTC?的?Typhon-IDE Pkgs?和?FPC Pkgs?這兩頁,你會找到你要的。
CTC
首先先制作一個簡單的數據庫吧,用于測試代碼能否正常工作:
$ sqlite3 demo.db
> create table user(id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(32) NOT NULL);
> insert into user(name) value ('ABC');
> insert into user(name) value ('XYZ');
然后根據數據庫結構聲明一個結構體,后面會用于數據傳遞:
type
TDemoRec = record
AId: Integer;
AName: PChar;
end;
與這個結構等價的 C++ 的結構體是這樣的:
struct DemoRec {
int AId;
char* AName;
};
這一瞬間我們會發現原來操作 SQLite 是如此的簡單,在此我定義了一個類,用來保存一些數據:
TSQLite = class
private
FDatabase: TSQLite3Connection;
FQuery: TSQLQuery;
FTransaction: TSQLTransaction;
published
property Database: TSQLite3Connection read FDatabase write FDatabase;
property Transaction: TSQLTransaction read FTransaction write FTransaction;
property Query: TSQLQuery read FQuery write FQuery;
end;
有了這些東西后,就可以方便的玩起來了,比如說執行一個 SQL 語句:
function TSQLite.ExecuteSQL(ASQL: string): Boolean;
begin
FQuery.Close;
FQuery.SQL.Text:= ASQL;
try
FQuery.ExecSQL;
Exit(True);
except
Exit(False);
end;
end;
這段代碼似乎太簡單了,也許我們更加希望在出錯時能夠給出一個原因,那么可以改一下:
function TSQLite.ExecuteSQL(ASQL: string; var AError: string): Boolean;
begin
FQuery.Close;
FQuery.SQL.Text:= ASQL;
try
FQuery.ExecSQL;
Exit(True);
except
on E: Exception do begin
AError:= e.Message;
Exit(False);
end;
end;
end;
好了,現在調用這個方法時,只需要額外傳入一個字符串參數,就可以獲取出錯時的信息。
在這個體系下,要進行查詢也很簡單,需要額外封裝兩個方法:
// 根據 SQL 語句查詢
function TSQLite.Select(ASQL: string; var AError: string): Boolean;
begin
FQuery.Close;
FQuery.SQL.Text:= ASQL;
try
FQuery.Open;
Exit(True);
Except
on E: Exception do begin
AError:= e.Message;
Exit(False);
end;
end;
end;
// 獲取查詢結果的行數
function dbGetSelectResultCount(APath: PChar): Integer;
var
database: TSQLite;
begin
Result := -1;
if (DatabaseExists(string(APath))) then begin
database := GetDatabase(string(APath));
Result := database.Query.RecordCount;
end;
end;
// 獲取指定行號的一條記錄
function dbGetSelectResult(APath: PChar; AIndex: Integer): TDemoRec;
var
database: TSQLite;
tmp: string;
begin
Inc(AIndex);
if (DatabaseExists(string(APath))) then begin
database := GetDatabase(string(APath));
if (database.Query.RecordCount >= AIndex) then begin
database.Query.RecNo:= AIndex;
Result.AId:= database.Query.FieldByName('id').AsInteger;
tmp := database.Query.FieldByName('name').AsString;
Result.AName:= StrAlloc(tmp.Length);
strcopy(Result.AName, PChar(tmp));
end;
end;
end;
接下來就是導出函數了,作為一個跨平臺的庫,它需要被其他程序調用,那么必定有導出函數,而不同的平臺下,所需要的函數形態是不一樣的,特別是由于 Android 使用 JNI 來調用動態庫,導出函數必須符合 JNI 的規范。
下面的例子很好的說明了導出函數的方法:
// iOS, PC
function dbGetSelectResultCount(APath: PChar): Integer; cdecl;
function dbGetSelectResult(APath: PChar; AIndex: Integer): TDemoRec; cdecl;
// Android
function Java_com_sqlite_sample_NativeAPI_dbGetSelectResultCount(env: PJNIEnv; obj: jobject; APath: jstring): jint; stdcall;
function Java_com_sqlite_sample_NativeAPI_dbGetSelectResult(env: PJNIEnv; obj: jobject; APath: jstring; AIndex: jint): jobject; stdcall;
唯一需要注意的是調用協定,用于 JNI 的必須設為?stdcall,而其他的均設為?cdecl。
那么再下一步就是編譯,直接使用?FPC?跨平臺編譯器即可,編譯方法很簡單:
$ fpc64 -Fisqlite -Fusqlite sample.lpr
此時即可以在 Mac 端生成?libsample.dylib?以及在 Linux 端生成?libsample.so。
要跨平臺編譯的話,稍微麻煩一點,但是也比想象中簡單很多:
$ export ANDROID_LIB=/usr/local/codetyphon/binLibraries/android-5.0-api21-arm/
$ export FPC=/usr/local/codetyphon/fpc/fpc64/bin/x86_64-linux/fpc
$ ${FPC} -Tandroid -Parm -Fl${ANDROID_LIB} -Fiqslite -Fusqlite sample.lpr
此時即可生成一個供 Android 系統使用的,arm 架構的?libsample.so,通過更換?-P?后面的參數,也可以編譯 x86,mips 等架構的 so。
完成后再看一下 iOS 的庫要怎么編譯。由于 iOS 已不再允許動態加載 dylib,我們必須把代碼編譯為靜態庫,也就是?.a?文件,并且靜態鏈接到 iOS 項目內。
$ export FPC_ROOT=/usr/local/lib/fpc/3.1.1
$ export FPC=${FPC_ROOT}/ppcrossa64
$ ${FPC} -Tdarwin -dIPHONEALL -Cn -Fisqlite -Fusqlite sample.lpr
$ ar -q libsample.a `grep "\.o$" link.res`
$ ranlib libsample.a
此時可以得到一個用于 64 位真機的?libsample.a?文件,若是要在 32 位的 iOS 和模擬器上完成兼容,還必須再另外編譯兩個?.a。
32 位真機:替換編譯器為 ppcrossarm
模擬器:替換編譯器為 ppcx64,并替換 -T 參數為 iphonesim
當我們得到了 3 個不同架構的?.a?后,有些時候需要將它們合并,使用如下命令來合并之:
lipo -create libsample_A64.a libsample_ARM.a libsample_EMU.a -output libsample.a
這樣就得到了一個融合了的?.a,它可以用于各種場合。
現在一切都準備好了,看看如何使用我們做好的庫吧,以上述的?dbGetSelectResultCount?和?dbGetSelectResult?為例,分別講述在各平臺的使用方法。
Android:
package com.sqlite.sample;
public class NativeAPI {
static { ?System.loadLibrary("sample"); }
public static native int dbGetSelectResultCount(String APath);
public static native DemoRec dbGetSelectResult(String APath, int AIndex);
}
iOS:
extern int dbGetSelectResultCount(const char* APath);
extern struct DemoRec dbGetSelectResult(const char* APath, int AIndex);
PC(以 C++ 為例):
typedef int (*dbSelectResultCount)(const char* APath);
typedef struct DemoRec (*dbSelectResult)(const char* APath, int AIndex);
void* handle = dlopen("./libsample.so", RTLD_LAZY);
dbSelectResultCount mSelectResultCount = (dbSelectResultCount) dlsym(handle, "dbGetSelectResultCount");
dbSelectResult mSelectResult = (dbSelectResult) dlsym(handle, "dbGetSelectResult");
可以看到,不論在哪個平臺上,最終得到的 API 都是一致的,這樣就統一了調用方式。在此基礎上,要做二次封裝也是非常方便。另外,由于代碼耦合幾乎沒有,也能夠很方便的對 SQLite 的底層庫的邏輯進行修改,只要 API 不變,就不會影響上層的調用。
以下是一個完整的調用代碼,以 iOS 端為例,其他各端均一致:
// 復制數據庫文件
NSString * originPath = [[NSBundle mainBundle] pathForResource:@"demo" ofType:@"db"];
NSString * destPath = [ViewController getDocumentPath];
NSString * dbFile = [destPath stringByAppendingPathComponent:@"demo.db"];
[ViewController copyFile:originPath destFile:dbFile];
// 打開數據庫
int b = dbOpen([dbFile UTF8String]);
printf("Open Database => %d\n", b);
// 執行查詢
b = dbSelect([dbFile UTF8String], "select * from user");
printf("Select => %d\n", b);
// 獲取查詢結果的行數
int count = dbGetSelectResultCount([dbFile UTF8String]);
printf("Select Rows => %d\n", count);
// 取出查到的每一條數據
for (int i = 0; i < count; i++) {
struct DemoRec r = dbGetSelectResult([dbFile UTF8String], i);
printf("Data %d => {id => %d, name => %s}\n", i, r.AId, r.AName);
}
// 關閉數據庫
b = dbClose([dbFile UTF8String]);
printf("Close Database => %d\n", b);
這段代碼的輸出為:
可以看到,調用成功,并且正確的傳遞了數據。在其他平臺上的效果也是完全一樣的。
這個用于演示的項目已經開源,請訪問我的 github 獲取,地址:
https://github.com/rarnu/cross_sqlite