Android的兩種數(shù)據(jù)存儲(chǔ)方式分析(二)

二、SQLiteDatabase

做移動(dòng)應(yīng)用的人,應(yīng)該沒(méi)有人不知道SQLite的吧,但SQLite與其它的關(guān)系型數(shù)據(jù)庫(kù)有多大區(qū)別?Android是怎么使用和操作SQLite的?SQLite的性能怎么樣?平時(shí)困擾我們的各種數(shù)據(jù)庫(kù)異常都是怎么會(huì)事兒?有沒(méi)有什么解決辦法?帶著這些問(wèn)題,我們來(lái)深入學(xué)習(xí)一下Android中的SQLite吧!

1、SQLite的優(yōu)勢(shì),Android為什么要選擇SQLite

(1)SQLite是一個(gè)單進(jìn)程的數(shù)據(jù)庫(kù)(和應(yīng)用程序運(yùn)行在一個(gè)進(jìn)程中),不分Server/Client。

(2)所有數(shù)據(jù)保存在一個(gè)文件中(transaction操作過(guò)程中會(huì)有一些事務(wù)記錄文件),跨平臺(tái)。

(3)免安裝直接使用。

(4)驅(qū)動(dòng)引擎包很小,500K,去掉一些不常用的功能,可以縮到300K。

(5)資源占用非常少,可以運(yùn)行在100K內(nèi)存的設(shè)備上。

(6)在讀寫(xiě)效率、消耗總量、延遲時(shí)間和整體簡(jiǎn)單性上具有的優(yōu)越性

(7)完全免費(fèi),商用也免費(fèi);收費(fèi)版有加密的功能。

它從設(shè)計(jì)開(kāi)始就是為嵌入式設(shè)備服務(wù)的,所以很適合手機(jī),它不像其它的數(shù)據(jù)庫(kù),啟動(dòng)后會(huì)有一個(gè)Server的進(jìn)程,訪問(wèn)時(shí)再起一個(gè)客戶(hù)端,通過(guò)IPC進(jìn)行通信。實(shí)際上SQLite現(xiàn)在的性能已經(jīng)很不錯(cuò)了,有不少小的網(wǎng)站也在用它,SQLite的官網(wǎng)上說(shuō),它可以支持一天10~30W的訪問(wèn)量。

2、SQLite的數(shù)據(jù)類(lèi)型

SQLite支持的數(shù)據(jù)類(lèi)型比別的數(shù)據(jù)庫(kù)要少很多;SQLite的數(shù)據(jù)存儲(chǔ)是動(dòng)態(tài)數(shù)據(jù)類(lèi)型的。

SQLite數(shù)據(jù)庫(kù)只支持五種數(shù)據(jù)類(lèi)型,基本已經(jīng)抽象到極高的層次了,而實(shí)際上我們只用到4種:

SQlite的五種數(shù)據(jù)類(lèi)型

Null這種類(lèi)型表示一個(gè)空值 ;Integer表示數(shù)學(xué)上的整數(shù);Real表示數(shù)學(xué)上的小數(shù);Text表示文本,計(jì)算機(jī)中叫字符串;Blob表示二進(jìn)制;仔細(xì)想想,這幾種值的確已經(jīng)涵蓋了平常我們用到的各有種情況。如果真想再抽象,blob可以表示一切啦,計(jì)算機(jī)里一切都是byte嘛。

再說(shuō)說(shuō)SQLite的動(dòng)態(tài)數(shù)據(jù)類(lèi)型,或者也可以叫弱數(shù)據(jù)類(lèi)型:SQLite定義的表中的每一列是可以存儲(chǔ)非指定的類(lèi)型的數(shù)據(jù)的,比如我們的表定義如下:

一個(gè)Sqlite的表定義

如果我們?cè)贏ndroid中使用:

contentValue.put("group_name","group_name");
contentValue.put("group_name", new byte[]{1,2 3});
contentValue.put("group_name", 123);

在做插入或修改操作時(shí),都是可以執(zhí)行成功的,看起來(lái)GROUP_NAME的類(lèi)型約束TEXT根本就不生效,實(shí)際上,我把這那個(gè)類(lèi)型去掉:

無(wú)類(lèi)型定義的表創(chuàng)建語(yǔ)句

SQLite照樣能把這個(gè)張表創(chuàng)建出來(lái),可見(jiàn)對(duì)于SQLite而言,類(lèi)型根本不是個(gè)必要條件,任何一列都可以存不同類(lèi)型的數(shù)據(jù),這也是SQLite區(qū)別于其它關(guān)系型數(shù)據(jù)庫(kù)的一個(gè)重要地方。

我們?cè)谄綍r(shí)使用時(shí)可能也有發(fā)現(xiàn)另一種現(xiàn)象,我存入的是一個(gè)String="123",但在cursor.getInt()時(shí),竟然真的能得到int = 123,而你在使用cursor.getType(columnIndex)時(shí)獲取到的對(duì)應(yīng)字段的類(lèi)型又的確是Text的,太混亂了。

另外,我們也注意到Cursor提供的方法,比如對(duì)于整數(shù),它提供了以下方法:

getInt(int columnIndex)
getLong(int columnIndex)
getShort(int columnIndex)

而SQLite只有INTEGER這一種類(lèi)型呀,這怎么對(duì)應(yīng)起來(lái)的呢?

這是不是說(shuō),SQLite根本沒(méi)有數(shù)據(jù)類(lèi)型可言?或者SQLite根本就不需要數(shù)據(jù)類(lèi)型?

答案是NO,SQLite從來(lái)都沒(méi)有改變他自己定義的數(shù)據(jù)類(lèi)型,他內(nèi)部仍然存儲(chǔ)著五種數(shù)據(jù)類(lèi)型,他做的僅僅是列內(nèi)數(shù)據(jù)動(dòng)態(tài)而已,其它的工作都是Android自己做的,我們來(lái)看看Cursor的代碼,先看getshort和getInt:(源代碼在:CursorWindow.java)

Cursor的getShort和getInt

太暴力了,直接從long轉(zhuǎn)過(guò)來(lái),也不必?cái)?shù)據(jù)丟失。再看同學(xué)暴力的getFloat和getDouble:

getFloat

從這里可以看出,SQLite內(nèi)部的確只存儲(chǔ)基本數(shù)據(jù)類(lèi)型Integer和Real,對(duì)應(yīng)于Java就是最大字節(jié)長(zhǎng)度的long和double,而使用時(shí)直接強(qiáng)轉(zhuǎn),風(fēng)險(xiǎn)由RD自己承擔(dān),所以我們?cè)趯?xiě)程序時(shí)對(duì)用哪種get要自己小心,保證你存的是什么類(lèi)型的數(shù)就用什么來(lái)取,如果真的擔(dān)心,那就直接用getLong和getDouble吧,它們不會(huì)丟失數(shù)據(jù)。

好,我們?cè)賮?lái)看第二個(gè)問(wèn)題,為什么存入一種類(lèi)型,可以用另一種類(lèi)型來(lái)get,還是直接上個(gè)代碼,從上面的getDouble代碼來(lái)看,真正獲取數(shù)據(jù)是用JNI實(shí)現(xiàn)的,我們先來(lái)看nativeGetDouble:(源代碼在:android_database_CursorWindow.cpp)

JNI中g(shù)etDouble

從上面代碼可以看出,SQLite內(nèi)部還是有類(lèi)型的,只不過(guò)Android為了大家使用方便,對(duì)每種類(lèi)型做了轉(zhuǎn)換,如果存的是String,它會(huì)用strtod方法轉(zhuǎn)換一下(string to double),這個(gè)方法并不是所有string都能轉(zhuǎn)過(guò)來(lái)的,如果不是一個(gè)小數(shù)形式的,它會(huì)返回0.0的;再看最后一個(gè)紅框,如果你存入的是blob二進(jìn)制,它就無(wú)能為力啦,給你拋個(gè)異常。

getLong和getDouble是類(lèi)似的,我們?cè)賮?lái)看看getString的實(shí)現(xiàn):

nativeGetString

注意紅框部分,String使用utf16的形式;數(shù)向串轉(zhuǎn)換使用的是sprintf;string跟blob類(lèi)型也是不兼容的,也會(huì)出異常。我們?cè)賮?lái)看看另一個(gè)讓人疑惑的地方,nativeGetBlog:

nativeGetBlob

疑惑就是string不兼容blob,但blob可不管你,string它照樣按字節(jié)數(shù)組給讀出來(lái),可能blob是更加底層的形態(tài)吧;便我們往下面兩個(gè)紅框看,blob又不兼容integer和real。

總結(jié)一下吧:

(1)SQLite的內(nèi)部設(shè)計(jì)是每一列內(nèi)都可以存放不同類(lèi)型的數(shù)據(jù),但我們建議大家還是按強(qiáng)類(lèi)型的關(guān)系型數(shù)據(jù)庫(kù)來(lái),顯式定義列類(lèi)型,同時(shí)列中的確存這種類(lèi)型的數(shù)據(jù),這樣可以增加代碼的可讀性,又能減少潛在和隱藏bug的出現(xiàn);

(2)盡管cursor的get可以獲取一個(gè)非它存入的類(lèi)型,但這個(gè)的正確性需要程序員自己來(lái)保證,而且一不小心還可能被它拋出異常,所以,建議同上,我們按強(qiáng)類(lèi)型庫(kù)的要求來(lái)做,存什么就取什么吧。

3、SQLite的線程模式

Sqlite是怎么處理多線程情況的?

實(shí)際上,SQLite的引擎對(duì)多線程做了處理的,要注意,這是sqlite層做的處理,指的是,打開(kāi)一個(gè)庫(kù)后,多個(gè)線程去處理這個(gè)庫(kù),而不是多個(gè)線程各自去打開(kāi)這個(gè)庫(kù)。我們看看SQLite是的各種線程模式:

SQLite的線程模式

(1)單線程模式,實(shí)際指的是數(shù)據(jù)庫(kù)引擎不加鎖,如果調(diào)用者使用了多線程,那調(diào)用都自己來(lái)保證同步。這種模式下,數(shù)據(jù)庫(kù)內(nèi)部的操作效率是最高的。

(2)多線程模式,這種模式下,可以多線程操作數(shù)據(jù)庫(kù),但有一個(gè)要求,一個(gè)DBconnection只能被一個(gè)線程使用,這一點(diǎn)也要求調(diào)用者自己來(lái)保證。

(3)順序模式,此模式安全性最高,隨便操作數(shù)據(jù)庫(kù),當(dāng)然,效率也最低。

那Android使用的是哪種模式呢,我們看一下代碼:(android_database_SQLiteGlobal.cpp)

android 6.0上SQLite初始化的代碼

從上面代碼我們也可以看出,在Android6.0上,使用的是多線程模式。

我們?nèi)绻⒁獾絊QLiteDatabase的接口,會(huì)發(fā)現(xiàn)這樣一個(gè)方法:

SQLiteDatabase的一lock方法

可以看到,SQLiteDatabase提供了一個(gè)鎖來(lái)控制線程同步,但這個(gè)方法在api level 16就不建議用了,這又是為什么呢?我們?nèi)タ纯碅piLevel 16以前的SQLite配置吧。

在jni中找了所有的database*.cpp文件,都沒(méi)有找到這個(gè)配置,再回去看看SQLite配置多線程模式的方法:

SQLite的線程模式配置方法

從這幾個(gè)配置方法和默認(rèn)配置方法,我猜測(cè)4.0及以前版本的SQLite用的不是默認(rèn)的serialized模式,因?yàn)橛眠@個(gè)模式Android就不用自己再加鎖了;那它應(yīng)該用的是編譯時(shí)設(shè)置線程模式,而且是單線程模式。

那Android4.0以上是怎么實(shí)現(xiàn)每個(gè)connection只被一個(gè)線程操作的呢?我們接下來(lái)分析Android操作SQLiteDatabase的方法就能發(fā)現(xiàn)了。(4.0及以下使用的是鎖的方式,我們就不細(xì)看了)

4、Android是如何實(shí)現(xiàn)操作SQLiteDatabase的

Android操作數(shù)據(jù)庫(kù)的流程比較簡(jiǎn)單,牽扯到的類(lèi)其實(shí)不多,一個(gè)簡(jiǎn)單的圖就可以看清楚:

Sqlite操作流程

一句話描述整個(gè)流程就是:openHelper打開(kāi)數(shù)據(jù)庫(kù)后,構(gòu)建DBConnection,使用sqliteQuery和SqliteStatement操作數(shù)據(jù)庫(kù)。是不是很簡(jiǎn)單,但它里面實(shí)際上還是有很多其它代碼邏輯,我們主要從三個(gè)方面分析:openDatabase、session和兩個(gè)操作方法query+statement。

(1)Open Database

我們?cè)谑褂胐b時(shí),先要通過(guò)SQLiteOpenHelper獲取到SQLiteDatabase,一般獲取方法有兩種:getReadableDatabase和getWritableDatabase,猛一看,這兩個(gè)方法還挺有迷惑性,好像一個(gè)是得到一個(gè)read only的庫(kù),一個(gè)用于得到一個(gè)可寫(xiě)的庫(kù),我們看看代碼中是這樣的嗎?

打開(kāi)SQLiteDatabase

先注意第一個(gè)紅框,這兩個(gè)方法都是加了synchronized的,不會(huì)因?yàn)榫€程問(wèn)題創(chuàng)建出兩個(gè)SQLiteDatabase;第二、三個(gè)紅框 可以看出,他們都是調(diào)用了getDatabaseLocked方法,只是參數(shù)不一樣,參數(shù)表明了打開(kāi)哪種db;第四個(gè)紅框里的條件可以發(fā)現(xiàn),只要是非writable,當(dāng)前緩存的db就可能返回了,或者當(dāng)前數(shù)據(jù)是writable的,也可以返回了,所以,獲取readable的數(shù)據(jù)庫(kù)并不一定返回的是只讀的哦,下面還有更吐血的。

打開(kāi)Sqlite數(shù)據(jù)庫(kù)

第一個(gè)紅框比較好理解,要打開(kāi)一個(gè)writable的,當(dāng)前是readOnly的就重新打開(kāi)一次;看第二個(gè)紅框,這個(gè)值是debug用的,真實(shí)環(huán)境一直是false,所以這個(gè)if是不會(huì)走的;看第三個(gè)紅框,我們發(fā)現(xiàn)打開(kāi)數(shù)據(jù)庫(kù)時(shí),根本不管是要readable還是writable,統(tǒng)一打開(kāi)Writable的,那還要傳進(jìn)來(lái)的writable參數(shù)干什么呢?再往下看,第五個(gè)紅框,在打開(kāi)readable失敗時(shí),這時(shí)才去判斷,如果我們計(jì)劃打開(kāi)的是readable的,它才去嘗試用readable去打開(kāi)。

結(jié)論:不管getReadableDatabase還是getWritableDatabase,OpenHelper都是優(yōu)先去打開(kāi)writable的,對(duì)于getReadableDatabase的作用,只是在打開(kāi)writable失敗時(shí)(比如磁盤(pán)滿(mǎn)了),才會(huì)用第二案,嘗試用readable打開(kāi)一下。

打開(kāi)SQLiteDatabase

接下來(lái)就比較簡(jiǎn)單了,同步的把各種on*方法回調(diào)走一遍,注意,這里的調(diào)用是同步依次調(diào)用,不會(huì)有什么線程問(wèn)題的。

(2)SQLiteSession

SQLiteDatabase在操作數(shù)據(jù)庫(kù)時(shí),都要獲取到一個(gè)session后才能開(kāi)始操作,從session這個(gè)單詞我們看以看出,它就像一個(gè)client一樣。

transaction,query,statement都是要先獲取一個(gè)session

從這幾個(gè)方法可以看出,數(shù)據(jù)庫(kù)的操作都是要先獲取到session才可以操作,我們看看session定義的地方。

SQLiteDatabase中的SQLiteSession

我們可以看到,Session是用threadLocal保存的,SQLiteDatabase正是通過(guò)ThreadLocal來(lái)保證了每個(gè)線程拿到的Session唯一,線程結(jié)束了,它也就被釋放了。每個(gè)session中有一個(gè)dbconnection,此時(shí),andriod已經(jīng)做到了每個(gè)connection只屬于一個(gè)線程,符合SQLite線程模式Multi-Thread的要求。

SQLiteSession的類(lèi)注釋寫(xiě)了好多內(nèi)容,對(duì)我們理解數(shù)據(jù)庫(kù)操作非常有幫助,(我使用Google翻譯了一下,翻譯的很難懂,還是直接看英文看的更加明白):

sqliteSession說(shuō)明

database只能用session訪問(wèn)數(shù)據(jù)庫(kù);多個(gè)readonly操作可以并行執(zhí)行,寫(xiě)操作只能串行;session不是線程安全的,DB通過(guò)ThreadLocal來(lái)保證安全;多線程同時(shí)操作connection,有可能造成死鎖。

sqliteSession說(shuō)明

transaction有兩種,隱式和顯式;平時(shí)的任何直接操作都會(huì)起一個(gè)隱式的;只有調(diào)用beginTransaction,才發(fā)起一個(gè)顯式的transaction;transaction是可以嵌套的,嵌套內(nèi)的任何一個(gè)沒(méi)有succesfull,最外層的都會(huì)回滾;如果一個(gè)transaction執(zhí)行時(shí)間太長(zhǎng),可以通過(guò)yieldTransaction來(lái)讓出一段時(shí)間數(shù)據(jù)庫(kù)操作,但讓出這前的提交內(nèi)容會(huì)被commit,且不會(huì)回滾(比較雞肋)。

顯示transaction用法

顯式的transaction必須這樣用,try + finaly,像lock的用法一樣,否則會(huì)再現(xiàn)死鎖的情況。

connection和responsiveness

一個(gè)數(shù)據(jù)庫(kù)的connection是有限的,多線程并發(fā)比較高時(shí),一些sessoin在獲取connection是要等待的;Android做了一個(gè)connection的pool來(lái)提高效率,實(shí)際上這個(gè)pool的max size很小的(系統(tǒng)配置),也就2~5個(gè)的樣子;

為了提升數(shù)據(jù)庫(kù)操作響應(yīng)速度,一定要減少transaction執(zhí)行時(shí)間,特別是writable的,它們要串行執(zhí)行;為增加響應(yīng)速度和流暢性,有幾個(gè)需要注意的點(diǎn):

[1]不要在主線程操作數(shù)據(jù)庫(kù);
[2]保持transaction時(shí)間盡可能短;
[3]簡(jiǎn)單的查詢(xún)條件要比復(fù)雜查詢(xún)條件的速度快的多;
[4]表內(nèi)數(shù)據(jù)量的大小是非常影響查詢(xún)速度的,100行的表跟10000行的表完全不是一個(gè)速度級(jí)的。

(3)關(guān)于query和statement就不細(xì)講了,我們只要理解兩點(diǎn):

[1]所有的query操作,包括rawQuery,最終都是組成一個(gè)SQLiteQuery對(duì)象來(lái)執(zhí)行;
[2]所有的增、刪、改操作,包括executeSQL,最終都是組成一個(gè)SQLiteStatement對(duì)象來(lái)執(zhí)行;

具體代碼大家可以從SQLiteDatabase中相關(guān)方法跟下去看看:
https://android.googlesource.com/platform/frameworks/base/+/android-6.0.1_r77/core/java/android/database/sqlite/

5、SQLite的讀寫(xiě)性能和內(nèi)存占用情況

先說(shuō)讀寫(xiě)性能,我自己做的測(cè)試,一般的手機(jī)
query100條以?xún)?nèi):1ms
query500條以?xún)?nèi):<10ms
query1000條:>10ms

插件操作:
Insert一條記錄:20ms

從上面的數(shù)據(jù)可以看出,正常情況下查詢(xún)少量數(shù)據(jù)是非??斓模@里說(shuō)的是正常情況,所以,如果我們只是查很少量的數(shù)據(jù),有時(shí)可以放在主線程操作,但是不建議,個(gè)別手機(jī)上還是有可能anr;

修改操作很快,必須放在工作線程完成。

關(guān)于內(nèi)存占用情況,我測(cè)試打開(kāi)一個(gè)庫(kù),建立一個(gè)connection,正常情況下一個(gè)connection會(huì)占用2K左右的內(nèi)存,而庫(kù)的主要內(nèi)存就在這里,看connectionPool的大小了。
所以,對(duì)于數(shù)據(jù)庫(kù)內(nèi)存方面的建議是:如果這個(gè)庫(kù)要經(jīng)常操作,可以不用關(guān),占用內(nèi)容并不多,因?yàn)樵俅蜷_(kāi)一次開(kāi)銷(xiāo)還是挺大的(50~100ms);如果不常用的數(shù)據(jù)庫(kù),還是及時(shí)關(guān)閉吧。

6、開(kāi)發(fā)中如何正確的使用SQLite

關(guān)于使用數(shù)據(jù)庫(kù)的建議:

(1)保證sqliteOpenHelper單例,或者全局唯一,不要讓一個(gè)庫(kù)在一個(gè)進(jìn)程中同時(shí)存在多個(gè)OpenHelper;

(2)不要在多個(gè)進(jìn)程中操作同一個(gè)庫(kù),如果有多進(jìn)程的情況,使用contentProvider來(lái)操作庫(kù)吧,各進(jìn)程都通過(guò)contentProvider來(lái)獲取數(shù)據(jù);

(3)數(shù)據(jù)庫(kù)是否需要關(guān)閉根據(jù)業(yè)務(wù)情況來(lái),它占用的內(nèi)存其實(shí)不多。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,777評(píng)論 25 709
  • 作為一個(gè)完成的應(yīng)用程序,數(shù)據(jù)存儲(chǔ)操作是必不可少的。因此,Android系統(tǒng)一共提供了四種數(shù)據(jù)存儲(chǔ)方式。分別是:Sh...
    AiPuff閱讀 660評(píng)論 0 0
  • 驚蟄天氣,風(fēng)雨如晦。 粉桃玉李紛繁,與豆蔻白芷一道提前綻開(kāi)了美的盛宴。 山村小樓一夜春雨。夢(mèng)里都是馬尾松翻涌的波...
    疏蟬閱讀 473評(píng)論 0 2
  • 短短三日,北京之行結(jié)束。伴著腳板的酸痛和游玩的余興未盡,決定記錄這次旅行。 前言 兩個(gè)人,來(lái)返火車(chē)??此坪苄量啵瑓s...
    c28369096728閱讀 433評(píng)論 0 0
  • 關(guān)于選擇,首先要了解一個(gè)概念“奧卡姆剃刀”。維基百科的解釋有:切勿浪費(fèi)較多的東西,去做用較少的東西同樣可以做好的事...
    雷哥復(fù)利筆記閱讀 391評(píng)論 1 1

友情鏈接更多精彩內(nèi)容