二、SQLiteDatabase
做移動應(yīng)用的人,應(yīng)該沒有人不知道SQLite的吧粟瞬,但SQLite與其它的關(guān)系型數(shù)據(jù)庫有多大區(qū)別航揉?Android是怎么使用和操作SQLite的?SQLite的性能怎么樣临扮?平時困擾我們的各種數(shù)據(jù)庫異常都是怎么會事兒?有沒有什么解決辦法?帶著這些問題佩番,我們來深入學習一下Android中的SQLite吧!
1罢杉、SQLite的優(yōu)勢趟畏,Android為什么要選擇SQLite
(1)SQLite是一個單進程的數(shù)據(jù)庫(和應(yīng)用程序運行在一個進程中),不分Server/Client滩租。
(2)所有數(shù)據(jù)保存在一個文件中(transaction操作過程中會有一些事務(wù)記錄文件)赋秀,跨平臺。
(3)免安裝直接使用律想。
(4)驅(qū)動引擎包很小猎莲,500K,去掉一些不常用的功能技即,可以縮到300K著洼。
(5)資源占用非常少,可以運行在100K內(nèi)存的設(shè)備上姥份。
(6)在讀寫效率郭脂、消耗總量、延遲時間和整體簡單性上具有的優(yōu)越性
(7)完全免費澈歉,商用也免費展鸡;收費版有加密的功能。
它從設(shè)計開始就是為嵌入式設(shè)備服務(wù)的埃难,所以很適合手機莹弊,它不像其它的數(shù)據(jù)庫,啟動后會有一個Server的進程涡尘,訪問時再起一個客戶端忍弛,通過IPC進行通信。實際上SQLite現(xiàn)在的性能已經(jīng)很不錯了考抄,有不少小的網(wǎng)站也在用它细疚,SQLite的官網(wǎng)上說,它可以支持一天10~30W的訪問量川梅。
2疯兼、SQLite的數(shù)據(jù)類型
SQLite支持的數(shù)據(jù)類型比別的數(shù)據(jù)庫要少很多然遏;SQLite的數(shù)據(jù)存儲是動態(tài)數(shù)據(jù)類型的。
SQLite數(shù)據(jù)庫只支持五種數(shù)據(jù)類型吧彪,基本已經(jīng)抽象到極高的層次了待侵,而實際上我們只用到4種:
Null這種類型表示一個空值 ;Integer表示數(shù)學上的整數(shù)姨裸;Real表示數(shù)學上的小數(shù)秧倾;Text表示文本,計算機中叫字符串傀缩;Blob表示二進制那先;仔細想想,這幾種值的確已經(jīng)涵蓋了平常我們用到的各有種情況赡艰。如果真想再抽象胃榕,blob可以表示一切啦,計算機里一切都是byte嘛瞄摊。
再說說SQLite的動態(tài)數(shù)據(jù)類型,或者也可以叫弱數(shù)據(jù)類型:SQLite定義的表中的每一列是可以存儲非指定的類型的數(shù)據(jù)的苦掘,比如我們的表定義如下:
如果我們在Android中使用:
contentValue.put("group_name","group_name");
contentValue.put("group_name", new byte[]{1,2 3});
contentValue.put("group_name", 123);
在做插入或修改操作時换帜,都是可以執(zhí)行成功的,看起來GROUP_NAME的類型約束TEXT根本就不生效鹤啡,實際上惯驼,我把這那個類型去掉:
SQLite照樣能把這個張表創(chuàng)建出來,可見對于SQLite而言递瑰,類型根本不是個必要條件祟牲,任何一列都可以存不同類型的數(shù)據(jù),這也是SQLite區(qū)別于其它關(guān)系型數(shù)據(jù)庫的一個重要地方抖部。
我們在平時使用時可能也有發(fā)現(xiàn)另一種現(xiàn)象说贝,我存入的是一個String="123",但在cursor.getInt()時慎颗,竟然真的能得到int = 123乡恕,而你在使用cursor.getType(columnIndex)時獲取到的對應(yīng)字段的類型又的確是Text的,太混亂了俯萎。
另外傲宜,我們也注意到Cursor提供的方法,比如對于整數(shù)夫啊,它提供了以下方法:
getInt(int columnIndex)
getLong(int columnIndex)
getShort(int columnIndex)
而SQLite只有INTEGER這一種類型呀函卒,這怎么對應(yīng)起來的呢?
這是不是說撇眯,SQLite根本沒有數(shù)據(jù)類型可言报嵌?或者SQLite根本就不需要數(shù)據(jù)類型虱咧?
答案是NO,SQLite從來都沒有改變他自己定義的數(shù)據(jù)類型沪蓬,他內(nèi)部仍然存儲著五種數(shù)據(jù)類型彤钟,他做的僅僅是列內(nèi)數(shù)據(jù)動態(tài)而已,其它的工作都是Android自己做的跷叉,我們來看看Cursor的代碼逸雹,先看getshort和getInt:(源代碼在:CursorWindow.java)
太暴力了,直接從long轉(zhuǎn)過來云挟,也不必數(shù)據(jù)丟失梆砸。再看同學暴力的getFloat和getDouble:
從這里可以看出,SQLite內(nèi)部的確只存儲基本數(shù)據(jù)類型Integer和Real园欣,對應(yīng)于Java就是最大字節(jié)長度的long和double帖世,而使用時直接強轉(zhuǎn),風險由RD自己承擔沸枯,所以我們在寫程序時對用哪種get要自己小心日矫,保證你存的是什么類型的數(shù)就用什么來取,如果真的擔心绑榴,那就直接用getLong和getDouble吧哪轿,它們不會丟失數(shù)據(jù)。
好翔怎,我們再來看第二個問題窃诉,為什么存入一種類型,可以用另一種類型來get赤套,還是直接上個代碼飘痛,從上面的getDouble代碼來看,真正獲取數(shù)據(jù)是用JNI實現(xiàn)的容握,我們先來看nativeGetDouble:(源代碼在:android_database_CursorWindow.cpp)
從上面代碼可以看出宣脉,SQLite內(nèi)部還是有類型的,只不過Android為了大家使用方便唯沮,對每種類型做了轉(zhuǎn)換脖旱,如果存的是String,它會用strtod方法轉(zhuǎn)換一下(string to double)介蛉,這個方法并不是所有string都能轉(zhuǎn)過來的萌庆,如果不是一個小數(shù)形式的币旧,它會返回0.0的践险;再看最后一個紅框,如果你存入的是blob二進制,它就無能為力啦巍虫,給你拋個異常彭则。
getLong和getDouble是類似的,我們再來看看getString的實現(xiàn):
注意紅框部分占遥,String使用utf16的形式俯抖;數(shù)向串轉(zhuǎn)換使用的是sprintf;string跟blob類型也是不兼容的瓦胎,也會出異常芬萍。我們再來看看另一個讓人疑惑的地方,nativeGetBlog:
疑惑就是string不兼容blob搔啊,但blob可不管你柬祠,string它照樣按字節(jié)數(shù)組給讀出來,可能blob是更加底層的形態(tài)吧负芋;便我們往下面兩個紅框看漫蛔,blob又不兼容integer和real。
總結(jié)一下吧:
(1)SQLite的內(nèi)部設(shè)計是每一列內(nèi)都可以存放不同類型的數(shù)據(jù)旧蛾,但我們建議大家還是按強類型的關(guān)系型數(shù)據(jù)庫來莽龟,顯式定義列類型,同時列中的確存這種類型的數(shù)據(jù)锨天,這樣可以增加代碼的可讀性轧房,又能減少潛在和隱藏bug的出現(xiàn);
(2)盡管cursor的get可以獲取一個非它存入的類型绍绘,但這個的正確性需要程序員自己來保證,而且一不小心還可能被它拋出異常迟赃,所以陪拘,建議同上,我們按強類型庫的要求來做纤壁,存什么就取什么吧左刽。
3、SQLite的線程模式
Sqlite是怎么處理多線程情況的酌媒?
實際上欠痴,SQLite的引擎對多線程做了處理的,要注意秒咨,這是sqlite層做的處理喇辽,指的是,打開一個庫后雨席,多個線程去處理這個庫菩咨,而不是多個線程各自去打開這個庫。我們看看SQLite是的各種線程模式:
(1)單線程模式,實際指的是數(shù)據(jù)庫引擎不加鎖抽米,如果調(diào)用者使用了多線程特占,那調(diào)用都自己來保證同步。這種模式下云茸,數(shù)據(jù)庫內(nèi)部的操作效率是最高的是目。
(2)多線程模式,這種模式下标捺,可以多線程操作數(shù)據(jù)庫懊纳,但有一個要求,一個DBconnection只能被一個線程使用宜岛,這一點也要求調(diào)用者自己來保證长踊。
(3)順序模式,此模式安全性最高萍倡,隨便操作數(shù)據(jù)庫身弊,當然,效率也最低列敲。
那Android使用的是哪種模式呢阱佛,我們看一下代碼:(android_database_SQLiteGlobal.cpp)
從上面代碼我們也可以看出,在Android6.0上戴而,使用的是多線程模式凑术。
我們?nèi)绻⒁獾絊QLiteDatabase的接口,會發(fā)現(xiàn)這樣一個方法:
可以看到所意,SQLiteDatabase提供了一個鎖來控制線程同步淮逊,但這個方法在api level 16就不建議用了,這又是為什么呢扶踊?我們?nèi)タ纯碅piLevel 16以前的SQLite配置吧泄鹏。
在jni中找了所有的database*.cpp文件,都沒有找到這個配置秧耗,再回去看看SQLite配置多線程模式的方法:
從這幾個配置方法和默認配置方法备籽,我猜測4.0及以前版本的SQLite用的不是默認的serialized模式,因為用這個模式Android就不用自己再加鎖了分井;那它應(yīng)該用的是編譯時設(shè)置線程模式车猬,而且是單線程模式。
那Android4.0以上是怎么實現(xiàn)每個connection只被一個線程操作的呢尺锚?我們接下來分析Android操作SQLiteDatabase的方法就能發(fā)現(xiàn)了珠闰。(4.0及以下使用的是鎖的方式,我們就不細看了)
4瘫辩、Android是如何實現(xiàn)操作SQLiteDatabase的
Android操作數(shù)據(jù)庫的流程比較簡單铸磅,牽扯到的類其實不多赡矢,一個簡單的圖就可以看清楚:
一句話描述整個流程就是:openHelper打開數(shù)據(jù)庫后,構(gòu)建DBConnection阅仔,使用sqliteQuery和SqliteStatement操作數(shù)據(jù)庫吹散。是不是很簡單,但它里面實際上還是有很多其它代碼邏輯八酒,我們主要從三個方面分析:openDatabase空民、session和兩個操作方法query+statement。
(1)Open Database
我們在使用db時羞迷,先要通過SQLiteOpenHelper獲取到SQLiteDatabase界轩,一般獲取方法有兩種:getReadableDatabase和getWritableDatabase,猛一看衔瓮,這兩個方法還挺有迷惑性浊猾,好像一個是得到一個read only的庫,一個用于得到一個可寫的庫热鞍,我們看看代碼中是這樣的嗎葫慎?
先注意第一個紅框,這兩個方法都是加了synchronized的薇宠,不會因為線程問題創(chuàng)建出兩個SQLiteDatabase;第二偷办、三個紅框 可以看出,他們都是調(diào)用了getDatabaseLocked方法澄港,只是參數(shù)不一樣椒涯,參數(shù)表明了打開哪種db;第四個紅框里的條件可以發(fā)現(xiàn),只要是非writable回梧,當前緩存的db就可能返回了废岂,或者當前數(shù)據(jù)是writable的,也可以返回了狱意,所以泪喊,獲取readable的數(shù)據(jù)庫并不一定返回的是只讀的哦,下面還有更吐血的髓涯。
第一個紅框比較好理解,要打開一個writable的哈扮,當前是readOnly的就重新打開一次纬纪;看第二個紅框,這個值是debug用的滑肉,真實環(huán)境一直是false包各,所以這個if是不會走的;看第三個紅框靶庙,我們發(fā)現(xiàn)打開數(shù)據(jù)庫時问畅,根本不管是要readable還是writable,統(tǒng)一打開Writable的,那還要傳進來的writable參數(shù)干什么呢护姆?再往下看矾端,第五個紅框,在打開readable失敗時卵皂,這時才去判斷秩铆,如果我們計劃打開的是readable的,它才去嘗試用readable去打開灯变。
結(jié)論:不管getReadableDatabase還是getWritableDatabase殴玛,OpenHelper都是優(yōu)先去打開writable的,對于getReadableDatabase的作用添祸,只是在打開writable失敗時(比如磁盤滿了),才會用第二案滚粟,嘗試用readable打開一下。
接下來就比較簡單了刃泌,同步的把各種on*方法回調(diào)走一遍凡壤,注意,這里的調(diào)用是同步依次調(diào)用蔬咬,不會有什么線程問題的鲤遥。
(2)SQLiteSession
SQLiteDatabase在操作數(shù)據(jù)庫時,都要獲取到一個session后才能開始操作林艘,從session這個單詞我們看以看出盖奈,它就像一個client一樣。
從這幾個方法可以看出,數(shù)據(jù)庫的操作都是要先獲取到session才可以操作啥酱,我們看看session定義的地方爹凹。
我們可以看到,Session是用threadLocal保存的,SQLiteDatabase正是通過ThreadLocal來保證了每個線程拿到的Session唯一镶殷,線程結(jié)束了禾酱,它也就被釋放了。每個session中有一個dbconnection绘趋,此時颤陶,andriod已經(jīng)做到了每個connection只屬于一個線程,符合SQLite線程模式Multi-Thread的要求陷遮。
SQLiteSession的類注釋寫了好多內(nèi)容滓走,對我們理解數(shù)據(jù)庫操作非常有幫助,(我使用Google翻譯了一下帽馋,翻譯的很難懂搅方,還是直接看英文看的更加明白):
database只能用session訪問數(shù)據(jù)庫比吭;多個readonly操作可以并行執(zhí)行,寫操作只能串行姨涡;session不是線程安全的衩藤,DB通過ThreadLocal來保證安全;多線程同時操作connection绣溜,有可能造成死鎖慷彤。
transaction有兩種,隱式和顯式怖喻;平時的任何直接操作都會起一個隱式的底哗;只有調(diào)用beginTransaction,才發(fā)起一個顯式的transaction锚沸;transaction是可以嵌套的跋选,嵌套內(nèi)的任何一個沒有succesfull,最外層的都會回滾哗蜈;如果一個transaction執(zhí)行時間太長前标,可以通過yieldTransaction來讓出一段時間數(shù)據(jù)庫操作,但讓出這前的提交內(nèi)容會被commit距潘,且不會回滾(比較雞肋)炼列。
顯式的transaction必須這樣用,try + finaly音比,像lock的用法一樣俭尖,否則會再現(xiàn)死鎖的情況。
一個數(shù)據(jù)庫的connection是有限的洞翩,多線程并發(fā)比較高時稽犁,一些sessoin在獲取connection是要等待的;Android做了一個connection的pool來提高效率骚亿,實際上這個pool的max size很小的(系統(tǒng)配置)已亥,也就2~5個的樣子;
為了提升數(shù)據(jù)庫操作響應(yīng)速度来屠,一定要減少transaction執(zhí)行時間虑椎,特別是writable的,它們要串行執(zhí)行俱笛;為增加響應(yīng)速度和流暢性捆姜,有幾個需要注意的點:
[1]不要在主線程操作數(shù)據(jù)庫;
[2]保持transaction時間盡可能短嫂粟;
[3]簡單的查詢條件要比復(fù)雜查詢條件的速度快的多;
[4]表內(nèi)數(shù)據(jù)量的大小是非常影響查詢速度的墨缘,100行的表跟10000行的表完全不是一個速度級的星虹。
(3)關(guān)于query和statement就不細講了零抬,我們只要理解兩點:
[1]所有的query操作,包括rawQuery宽涌,最終都是組成一個SQLiteQuery對象來執(zhí)行平夜;
[2]所有的增、刪卸亮、改操作忽妒,包括executeSQL,最終都是組成一個SQLiteStatement對象來執(zhí)行兼贸;
具體代碼大家可以從SQLiteDatabase中相關(guān)方法跟下去看看:
https://android.googlesource.com/platform/frameworks/base/+/android-6.0.1_r77/core/java/android/database/sqlite/
5段直、SQLite的讀寫性能和內(nèi)存占用情況
先說讀寫性能,我自己做的測試溶诞,一般的手機
query100條以內(nèi):1ms
query500條以內(nèi):<10ms
query1000條:>10ms
插件操作:
Insert一條記錄:20ms
從上面的數(shù)據(jù)可以看出鸯檬,正常情況下查詢少量數(shù)據(jù)是非常快的螺垢,這里說的是正常情況喧务,所以,如果我們只是查很少量的數(shù)據(jù)枉圃,有時可以放在主線程操作功茴,但是不建議,個別手機上還是有可能anr孽亲;
修改操作很快坎穿,必須放在工作線程完成。
關(guān)于內(nèi)存占用情況墨林,我測試打開一個庫赁酝,建立一個connection,正常情況下一個connection會占用2K左右的內(nèi)存旭等,而庫的主要內(nèi)存就在這里酌呆,看connectionPool的大小了。
所以搔耕,對于數(shù)據(jù)庫內(nèi)存方面的建議是:如果這個庫要經(jīng)常操作隙袁,可以不用關(guān),占用內(nèi)容并不多弃榨,因為再打開一次開銷還是挺大的(50~100ms)菩收;如果不常用的數(shù)據(jù)庫,還是及時關(guān)閉吧鲸睛。
6娜饵、開發(fā)中如何正確的使用SQLite
關(guān)于使用數(shù)據(jù)庫的建議:
(1)保證sqliteOpenHelper單例,或者全局唯一官辈,不要讓一個庫在一個進程中同時存在多個OpenHelper箱舞;
(2)不要在多個進程中操作同一個庫遍坟,如果有多進程的情況,使用contentProvider來操作庫吧晴股,各進程都通過contentProvider來獲取數(shù)據(jù)愿伴;
(3)數(shù)據(jù)庫是否需要關(guān)閉根據(jù)業(yè)務(wù)情況來,它占用的內(nèi)存其實不多电湘。