游戲中會(huì )大量使用到配置文件,每個(gè)項目組根據自己不同的需求會(huì )選擇不同的存儲格式,比如使用Json或者SQLite來(lái)存儲數據。此處我們只對使用SQLite的情況來(lái)做討論。一般情況下會(huì )選擇把它放在可讀寫(xiě)目錄里面,這樣SQLite可以直接使用它原來(lái)的io API來(lái)對db文件進(jìn)行讀取。在PC或者iOS平臺上這不是問(wèn)題。但是如果在A(yíng)ndroid平臺上,游戲安裝后還是以一個(gè)apk文件的形式存在。如果我們的數據放在了db中,使用SQLite原來(lái)自帶的io功能是不能進(jìn)行讀取的。這里有3種方式可以供選擇:
一般大家可能會(huì )選擇第一種方法,這沒(méi)有什么好說(shuō)的。我們接下來(lái)看看第3種方法的可能性。
為了實(shí)現上述的想法,我們需要兩個(gè)條件:
當然上述兩個(gè)條件是滿(mǎn)足的,下面我們來(lái)具體看看這兩個(gè)條件。
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 < 0 if direct fd access is not possible (for example, if the asset is compressed). int AAsset_openFileDescriptor (AAsset * asset, off_t * outStart, off_t * outLength ) |
從這個(gè)API可以看出,它可以返回一個(gè)用于讀取當前asset的一個(gè)文件描述符。但是如果當前asset被壓縮了,那么就回返回一個(gè)小于0的值。如果我們想要讀取db的話(huà),那么它必須是沒(méi)有壓縮過(guò)的。
示例代碼大體如下所示:
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 <= fd); AAsset_close(Asset); |
注意,這個(gè)fd返回的是整個(gè)apk的句柄,start代表這個(gè)文件在apk中的偏移,length代表長(cháng)度,使用的時(shí)候要注意。
下圖是 SQLite官方給出的架構圖:

我們這里主要關(guān)注的就是OS Interface這一層,它使用了VFS這一對象來(lái)為不同系統之間的可移植性提供了保證,。具體細節可以參照官網(wǎng)對于VFS的介紹。SQLite目前提供了對類(lèi)unix系統和windows系統的支持,分別在os_unix.c以及os_win.c中實(shí)現的。其中os_unix.c提供了對mac os、iOS、Android以及Linux的支持。如果讀取想對這塊有一個(gè)比較深入了了解可以看官方文檔以及查看一些示例如test_demovfs.c等來(lái)深入了解。
有了上面的理論支持,那么我們就可以著(zhù)手可以寫(xiě)代碼了。我們目前有兩種方案可以選擇:
第一種方案需要寫(xiě)的代碼比較多,而且需要讀者對SQLite有一個(gè)比較深入的了解。所以我們這里選擇第二種方案。在原來(lái)的os_unix.c的基礎上進(jìn)行改動(dòng)。
通過(guò)分析os_unix.c文件,我們能得出它主要使用了open() read() write()等io操作。這跟我們上面Android NDK提供的AAsset_openFileDescriptor正好完美的結合起來(lái)。我們正好可以使用它返回的句柄進(jìn)行類(lèi)似的操作。只不過(guò)在A(yíng)ndroid的情況下特殊一點(diǎn)。
這樣下來(lái),我們基本上大體需要改的幾個(gè)函數和結構體如下所示
以上基本是需要改到的函數,當然根據實(shí)現的不同可能具體需要改動(dòng)的函數不一樣。這只是比較粗暴的改法。像我們需要支持從apk里面讀取以及從一個(gè)散文件里面讀取,所以跟上面的改動(dòng)多少有一些不一樣的地方,但是基本思想是通的。當然由于本人對SQLite不了 解,可能有需要改動(dòng)的地方?jīng)]有注意到,如果說(shuō)的有錯誤希望能及時(shí)指正。方法已經(jīng)說(shuō)的比較明白了,這里也就不貼代碼了。
上面的例子提到的AAssetManager_open在打開(kāi)時(shí)需要一個(gè)AAssetManager的對象,這個(gè)對象只能從Java里面獲取。如果你是直接使用Android開(kāi)發(fā)那么這個(gè)對象就比較容易獲取,那么如果你是使用Unity或者UE4開(kāi)發(fā)怎么獲取這個(gè)對象呢。
SQLite的修改跟上面是一樣的,只是我們在Unity中如何獲取這個(gè)對象呢。讀者可以具體對照一下這個(gè)類(lèi)AndroidJNI AndroidJNIHelper AndroidJavaClass AndroidJavaObject AndroidJavaProxy這幾個(gè)類(lèi)。
示例代碼如下所示:
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); |
這樣我們就得到了這個(gè)AssetManager,這個(gè)時(shí)候我們就可以通過(guò)C#把這個(gè)對象傳遞給SQLite庫了。
UE4在C++中可以直接拿到AAssetManager對象,具體實(shí)現細節UE4已經(jīng)幫我們做了,具體可以查看AndroidJNI.cpp中的代碼。我們拿到AAssetManager這個(gè)對象并把它設置給SQLite就可以了。
到此,我們對SQLite擴展讀取apk中db的方法已經(jīng)寫(xiě)完了。由于A(yíng)ndroid NDK返回了文件描述符以及SQLite提供的OS Interface層讓我們很比較容易的實(shí)現了對SQLite擴展。由于作者對SQLite原來(lái)并沒(méi)有了解,所以難免有錯誤之處,如果有錯誤請及時(shí)指正。如果讀者想對SQLite有一個(gè)比較深入的認識,也可以看看參考文章6。
作者: 風(fēng)戀殘雪
出處: http://www.cnblogs.com/ghl_carmack
關(guān)于作者:專(zhuān)注游戲引擎,關(guān)注VR,對操作系統、編譯原理有深厚興趣!
本文版權歸作者和博客園共有,歡迎轉載,但未經(jīng)作者同意必須保留此段聲明,且在文章頁(yè)面明顯位置給出, 原文鏈接,否則保留追究法律責任的權利。
聯(lián)系客服