您的位置:首頁>正文

擴展SQLite使其能從apk檔中讀取db

遊戲中會大量使用到設定檔, 每個專案組根據自己不同的需求會選擇不同的存儲格式, 比如使用Json或者SQLite來存儲資料。 此處我們只對使用SQLite的情況來做討論。 一般情況下會選擇把它放在可讀寫目錄裡面, 這樣SQLite可以直接使用它原來的io API來對db檔進行讀取。 在PC或者iOS平臺上這不是問題。 但是如果在Android平臺上, 遊戲安裝後還是以一個apk檔的形式存在。 如果我們的資料放在了db中, 使用SQLite原來自帶的io功能是不能進行讀取的。 這裡有3種方式可以供選擇:

在程式第一次啟動時, 把apk中的所有檔解壓出來放到可讀寫目錄中,
這樣存在的問題是第一次打開程式時會比較慢。 在需要的讀取某個db的時候才把這個檔從apk中解壓出來, 這樣的話可能會導致卡頓, 或者使用協程等非同步作業來完成, 但是這樣對於邏輯層的代碼書寫成本比較高。 對SQLite做一定的改動, 使它可以讀取apk中的db檔。

一般大家可能會選擇第一種方法, 這沒有什麼好說的。 我們接下來看看第3種方法的可能性。

理論

為了實現上述的想法, 我們需要兩個條件:

android提供對apk中單個檔的讀取的能力。 SQLite提供了對io層(不同平臺)進行快速修改的能力, 這就要求SQLite有比較好的抽象以較少的模組之間的耦合。

當然上述兩個條件是滿足的, 下面我們來具體看看這兩個條件。

Android對apk中單個文件的讀取能力

Open a new file descriptor that can be used to read the asset data. If the start or length cannot be represented by a 32-bit number, it will be truncated. If the file is large, use AAsset_openFileDescriptor64 instead.

Returns

int AAsset_openFileDescriptor (AAsset * asset, off_t * outStart, off_t * outLength )

從這個API可以看出, 它可以返回一個用於讀取當前asset的一個檔描述符。 但是如果當前asset被壓縮了, 那麼就回返回一個小於0的值。 如果我們想要讀取db的話, 那麼它必須是沒有壓縮過的。

示例代碼大體如下所示:

AAsset* asset = AAssetManager_open(mgr, filename, AASSET_MODE_UNKNOWN);

if (NULL == asset)

{

//LOGD("file not found! Stop preload file: %s", filename);

return FILE_NOT_FOUND;

}

// open asset as file descriptor

int fd = AAsset_openFileDescriptor(asset, &start, &length);

assert(0

AAsset_close(Asset);

注意, 這個fd返回的是整個apk的控制碼, start代表這個檔在apk中的偏移, length代表長度, 使用的時候要注意。

SQLite對於檔io層的支持

下圖是 SQLite官方給出的架構圖:

我們這裡主要關注的就是OS Interface這一層, 它使用了VFS這一物件來為不同系統之間的可攜性提供了保證, 。 具體細節可以參照官網對於VFS的介紹。 SQLite目前提供了對類unix系統和windows系統的支援, 分別在os_unix.c以及os_win.c中實現的。 其中os_unix.c提供了對mac os、iOS、Android以及Linux的支持。 如果讀取想對這塊有一個比較深入了瞭解可以看官方文檔以及查看一些示例如test_demovfs.c等來深入瞭解。

實現

有了上面的理論支援, 那麼我們就可以著手可以寫代碼了。 我們目前有兩種方案可以選擇:

單獨為安卓實現一個VFS。 在os_unix.c的基礎上進行修改。

第一種方案需要寫的代碼比較多, 而且需要讀者對SQLite有一個比較深入的瞭解。

所以我們這裡選擇第二種方案。 在原來的os_unix.c的基礎上進行改動。

SQLite改造

通過分析os_unix.c檔, 我們能得出它主要使用了open read write等io操作。 這跟我們上面Android NDK提供的AAsset_openFileDescriptor正好完美的結合起來。 我們正好可以使用它返回的控制碼進行類似的操作。 只不過在Android的情況下特殊一點。

這樣下來, 我們基本上大體需要改的幾個函數和結構體如下所示

struct unixFile 我們需要在其中添加檔在apk中的偏移以及大小 。 posixOpen 通過openFileDescriptor返回檔描述符。 seekAndRead 讀取時要加上檔偏移。 unixFileSize 返回前面unixFile中記錄的檔大小的值。

以上基本是需要改到的函數, 當然根據實現的不同可能具體需要改動的函數不一樣。 這只是比較粗暴的改法。 像我們需要支援從apk裡面讀取以及從一個散文件裡面讀取, 所以跟上面的改動多少有一些不一樣的地方,

但是基本思想是通的。 當然由於本人對SQLite不了 解, 可能有需要改動的地方沒有注意到, 如果說的有錯誤希望能及時指正。 方法已經說的比較明白了, 這裡也就不貼代碼了。

上面的例子提到的AAssetManager_open在打開時需要一個AAssetManager的物件, 這個物件只能從Java裡面獲取。 如果你是直接使用Android開發那麼這個物件就比較容易獲取, 那麼如果你是使用Unity或者UE4開發怎麼獲取這個物件呢。

Unity實現細節

SQLite的修改跟上面是一樣的, 只是我們在Unity中如何獲取這個物件呢。 讀者可以具體對照一下這個類AndroidJNI AndroidJNIHelper AndroidJavaClass AndroidJavaObject AndroidJavaProxy這幾個類。

示例代碼如下所示:

IntPtr cls_Activity = (IntPtr)AndroidJNI.FindClass("com/unity3d/player/UnityPlayer");

IntPtr fid_Activity = AndroidJNI.GetStaticFieldID(cls_Activity, "currentActivity", "Landroid/app/Activity;");

IntPtr obj_Activity = AndroidJNI.GetStaticObjectField(cls_Activity, fid_Activity);

IntPtr obj_cls = AndroidJNI.GetObjectClass(obj_Activity);

IntPtr asset_func = AndroidJNI.GetMethodID(obj_cls, "getAssets", "Landroid/content/res/AssetManager;");

jvalue asset_array = new jvalue[2]; //

IntPtr assetManager = AndroidJNI.CallObjectMethod(obj_Activity, asset_func, asset_array);

這樣我們就得到了這個AssetManager, 這個時候我們就可以通過C#把這個物件傳遞給SQLite庫了。

UE4實現細節

UE4在C++中可以直接拿到AAssetManager物件, 具體實現細節UE4已經幫我們做了,具體可以查看AndroidJNI.cpp中的代碼。我們拿到AAssetManager這個物件並把它設置給SQLite就可以了。

總結

到此,我們對SQLite擴展讀取apk中db的方法已經寫完了。由於Android NDK返回了檔描述符以及SQLite提供的OS Interface層讓我們很比較容易的實現了對SQLite擴展。由於作者對SQLite原來並沒有瞭解,所以難免有錯誤之處,如果有錯誤請及時指正。如果讀者想對SQLite有一個比較深入的認識,也可以看看參考文章6。

具體實現細節UE4已經幫我們做了,具體可以查看AndroidJNI.cpp中的代碼。我們拿到AAssetManager這個物件並把它設置給SQLite就可以了。

總結

到此,我們對SQLite擴展讀取apk中db的方法已經寫完了。由於Android NDK返回了檔描述符以及SQLite提供的OS Interface層讓我們很比較容易的實現了對SQLite擴展。由於作者對SQLite原來並沒有瞭解,所以難免有錯誤之處,如果有錯誤請及時指正。如果讀者想對SQLite有一個比較深入的認識,也可以看看參考文章6。

同類文章
Next Article
喜欢就按个赞吧!!!
点击关闭提示