標籤:完成 static null esc class name 一點 linux body
遊戲中會大量使用到設定檔,每個項目組根據自己不同的需求會選擇不同的儲存格式,比如使用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 < 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 ) |
從這個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 <= fd); 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。
參考文章:
- http://sqlite.org/arch.html
- http://www.sqlite.org/vfs.html
- https://developer.android.com/ndk/reference/group___asset.html#ga1af4ffd050016e99961e24f550981677
- https://docs.unity3d.com/560/Documentation/ScriptReference/AndroidJNI.html
- http://answers.unity3d.com/questions/205212/android-file-open.html
- http://huili.github.io/summary/READMEG1.html
擴充SQLite使其能從apk檔案中讀取db