Android 提供了很多種持久化存儲(chǔ)的方案,存儲(chǔ)就是把特定的數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)化成可以被記錄和還原的格式,這個(gè)數(shù)據(jù)格式可以是二進(jìn)制的,也可以是 XML、JSON、Protocol Buffer 這些格式。既然有那么多存儲(chǔ)的方案,那我們?cè)谶x擇數(shù)據(jù)存儲(chǔ)方法時(shí),一般需要考慮哪些關(guān)鍵要素呢?
在選擇數(shù)據(jù)存儲(chǔ)方法時(shí),一般會(huì)考慮下面這幾要數(shù):
這些要素哪個(gè)最重要呢?數(shù)據(jù)存儲(chǔ)方法不能脫離場(chǎng)景來(lái)考慮,任何一項(xiàng)目都不可能把這六個(gè)要素都做成最完美。首要考慮的是正確性,那我們可能需要采用冗余、雙寫(xiě)等方案,那就要容忍對(duì)時(shí)間開(kāi)銷(xiāo)產(chǎn)生的額外影響。同樣如果非常在意安全,加解密環(huán)節(jié)的開(kāi)銷(xiāo)也必不可小。如果想針對(duì)啟動(dòng)場(chǎng)景,可以選擇在初始化時(shí)間和讀取時(shí)間更有優(yōu)勢(shì)的方案。
總的來(lái)說(shuō),我們需要結(jié)合應(yīng)用場(chǎng)景選擇合適的數(shù)據(jù)存儲(chǔ)方法。
SharedPreferences是 Android 中比較常用的存儲(chǔ)方法,它可以用來(lái)存儲(chǔ)一些比較小的鍵值對(duì)集合。雖然 SharedPreferences 使用非常簡(jiǎn)便,但也是我們?cè)嵅”容^多的存儲(chǔ)方法。它的性能問(wèn)題比較多:
坦白來(lái)講,系統(tǒng)提供的 SharedPreferences 的應(yīng)用場(chǎng)景是用來(lái)存儲(chǔ)一些非常簡(jiǎn)單、輕量的數(shù)據(jù)。我們不要使用它來(lái)存儲(chǔ)過(guò)于復(fù)雜的數(shù)據(jù),例如 HTML、JSON 等。而且 SharedPreference 的文件存儲(chǔ)性能與文件大小相關(guān),每個(gè) SP 文件不能過(guò)大,我們不要將毫無(wú)關(guān)聯(lián)的配置項(xiàng)保存在同一個(gè)文件中,同時(shí)考慮將頻繁修改的條目單獨(dú)隔離出來(lái)。
我們也可以替換通過(guò)復(fù)寫(xiě) Application 的 getSharedPreferences 方法替換系統(tǒng)默認(rèn)實(shí)現(xiàn),比如優(yōu)化卡頓、合并多次 apply 操作、支持跨進(jìn)程操作等。具體:
public class MyApplication extends Application {
@Override
public SharedPreferences getSharedPreferences(String name, int mode)
{
return SharedPreferencesImpl.getSharedPreferences(name, mode);
}
}
對(duì)系統(tǒng)提供的 SharedPreferences 的小修小補(bǔ)雖然性能有所提升,但是依然不能徹底解決問(wèn)題。
為什么 Android 系統(tǒng)不把 SharedPreferences 設(shè)計(jì)成跨進(jìn)程安全的呢?那是因?yàn)?Android 系統(tǒng)更希望我們?cè)谶@個(gè)場(chǎng)景選擇使用 ContentProvider 作為存儲(chǔ)方式。ContentProvider 作為 Android 四大組件中的一種,為我們提供了不同進(jìn)程甚至是不同應(yīng)用程序之間共享數(shù)據(jù)的機(jī)制。
Android 系統(tǒng)中比如相冊(cè)、日歷、音頻、視頻、通訊錄等模塊都提供了 ContentProvider 的訪(fǎng)問(wèn)支持。它的使用也比較簡(jiǎn)單。
當(dāng)然,在使用過(guò)程也需要注意以下幾點(diǎn)。
ContentProvider 的生命周期默認(rèn)在 Application onCreate() 之前,而且都是在主線(xiàn)程創(chuàng)建的。我們自定義的 ContentProvider 類(lèi)的構(gòu)造函數(shù)、靜態(tài)代碼塊、onCreate 函數(shù)都盡量不要做耗時(shí)的操作,會(huì)拖慢啟動(dòng)速度。
ContentProvider 在進(jìn)行跨進(jìn)程數(shù)據(jù)傳遞時(shí),利用了 Android 的 Binder 和匿名共享內(nèi)存機(jī)制。就是通過(guò) Binder 傳遞 CursorWindow 對(duì)象內(nèi)部的匿名共享內(nèi)存的文件描述符。這樣在跨進(jìn)程傳輸中,結(jié)果數(shù)據(jù)并不需要跨進(jìn)程傳輸,而是在不同進(jìn)程中通過(guò)傳輸?shù)哪涿蚕韮?nèi)存文件描述符來(lái)操作同一塊匿名內(nèi)存,這樣來(lái)實(shí)現(xiàn)不同進(jìn)程訪(fǎng)問(wèn)相同數(shù)據(jù)的目的。
Android 的 Binder 傳輸是有大小限制的,一般來(lái)說(shuō)限制是 1~2 MB。ContentProvider 的接口調(diào)用參數(shù)和 call 函數(shù)調(diào)用并沒(méi)有使用匿名共享機(jī)制,比如要批量插入很多數(shù)據(jù),那么就會(huì)出現(xiàn)一個(gè)插入數(shù)據(jù)的數(shù)組,如果這個(gè)數(shù)組太大了,那么這個(gè)操作就可能會(huì)出現(xiàn)數(shù)據(jù)超大異常。
雖然 ContentProvider 為應(yīng)用程序之間的數(shù)據(jù)共享提供了很好的安全機(jī)制,但是如果 ContentProvider 是 exported,當(dāng)支持執(zhí)行 SQL 語(yǔ)句時(shí)就需要注意 SQL 注入的問(wèn)題。另外如果我們傳入的參數(shù)是一個(gè)文件路徑,然后返回文件的內(nèi)容,這個(gè)時(shí)候也要校驗(yàn)合法性,不然整個(gè)應(yīng)用的私有數(shù)據(jù)都有可能被別人拿到,在 intent 傳遞參數(shù)的時(shí)候可能經(jīng)常會(huì)犯這個(gè)錯(cuò)誤。
總的來(lái)說(shuō),ContentProvider 這套方案實(shí)現(xiàn)相對(duì)比較笨重,適合傳輸大的數(shù)據(jù)。
對(duì)于大部分的開(kāi)發(fā)者來(lái)說(shuō),我們不一定有精力去“創(chuàng)造”一種數(shù)據(jù)序列化的格式。
對(duì)象的序列化:應(yīng)用程序中的對(duì)象存儲(chǔ)在內(nèi)存中,如果我們想把對(duì)象存儲(chǔ)下來(lái)或者在網(wǎng)絡(luò)上傳輸,這個(gè)時(shí)候就需要用到對(duì)象的序列化和反序列化。
對(duì)象序列化就是把一個(gè) Object 對(duì)象所有的信息表示成一個(gè)字節(jié)序列,這包括 Class 信息、繼承關(guān)系信息、訪(fǎng)問(wèn)權(quán)限、變量類(lèi)型以及數(shù)值信息等。
Serializable 是 Java 原生的序列化機(jī)制,在 Android 中也有被廣泛使用。我們可以通過(guò) Serializable 將對(duì)象持久化存儲(chǔ),也可以通過(guò) Bundle 傳遞 Serializable 的序列化數(shù)據(jù)。
Serializable 的原理
Serializable 的原理是通過(guò) ObjectInputStream 和 ObjectOutputStream 來(lái)實(shí)現(xiàn)的,ObjectOutputStream的部分源碼實(shí)現(xiàn):
private void writeFieldValues(Object obj, ObjectStreamClass classDesc) {
for (ObjectStreamField fieldDesc : classDesc.fields()) {
...
Field field = classDesc.checkAndGetReflectionField(fieldDesc);
...
}
...
}
整個(gè)序列化過(guò)程使用了大量的反射和臨時(shí)變量,而且在序列化對(duì)象的時(shí)候,不僅會(huì)序列化當(dāng)前對(duì)象本身,還需要遞歸序列化對(duì)象引用的其他對(duì)象。
整個(gè)過(guò)程計(jì)算非常復(fù)雜,而且因?yàn)榇嬖诖罅糠瓷浜?GC 的影響,序列化的性能會(huì)比較差。另外一方面因?yàn)樾蛄谢募枰男畔⒎浅6?,?dǎo)致它的大小比 Class 文件本身還要大很多,這樣又會(huì)導(dǎo)致 I/O 讀寫(xiě)上的性能問(wèn)題。
Serializable 的進(jìn)階
既然 Serializable 性能那么差,那它有哪些優(yōu)勢(shì)呢?Serializable 序列化支持替代默認(rèn)流程,它會(huì)先反射判斷是否存在我們自己實(shí)現(xiàn)的序列化方法 writeObject 或反序列化方法 readObject。通過(guò)這兩個(gè)方法,我們可以對(duì)某些字段做一些特殊修改,也可以實(shí)現(xiàn)序列化的加密功能。
writeReplace 和 readResolve 方法。這兩個(gè)方法代理序列化的對(duì)象,可以實(shí)現(xiàn)自定義返回的序列化實(shí)例。那它有什么用呢?我們可以通過(guò)它們實(shí)現(xiàn)對(duì)象序列化的版本兼容,例如通過(guò) readResolve 方法可以把老版本的序列化對(duì)象轉(zhuǎn)換成新版本的對(duì)象類(lèi)型。
Serializable 的序列化與反序列化的調(diào)用流程如下。
// 序列化
E/test:SerializableTestData writeReplace
E/test:SerializableTestData writeObject
// 反序列化
E/test:SerializableTestData readObject
E/test:SerializableTestData readResolve
Serializable 的注意事項(xiàng)
Serializable 雖然使用非常簡(jiǎn)單,但是也有一些需要注意的事項(xiàng)字段。
不被序列化的字段。類(lèi)的 static 變量以及被聲明為 transient 的字段,默認(rèn)的序列化機(jī)制都會(huì)忽略該字段,不會(huì)進(jìn)行序列化存儲(chǔ)。當(dāng)然我們也可以使用進(jìn)階的 writeReplace 和 readResolve 方法做自定義的序列化存儲(chǔ)。
serialVersionUID。在類(lèi)實(shí)現(xiàn)了 Serializable 接口后,我們需要添加一個(gè) Serial Version ID,它相當(dāng)于類(lèi)的版本號(hào)。這個(gè) ID 我們可以顯式聲明也可以讓編譯器自己計(jì)算。通常我建議顯式聲明會(huì)更加穩(wěn)妥,因?yàn)殡[式聲明假如類(lèi)發(fā)生了一點(diǎn)點(diǎn)變化,進(jìn)行反序列化都會(huì)由于 serialVersionUID 改變而導(dǎo)致 InvalidClassException 異常。
構(gòu)造方法。Serializable 的反序列默認(rèn)是不會(huì)執(zhí)行構(gòu)造函數(shù)的,它是根據(jù)數(shù)據(jù)流中對(duì) Object 的描述信息創(chuàng)建對(duì)象的。如果一些邏輯依賴(lài)構(gòu)造函數(shù),就可能會(huì)出現(xiàn)問(wèn)題,例如一個(gè)靜態(tài)變量只在構(gòu)造函數(shù)中賦值,當(dāng)然我們也可以通過(guò)進(jìn)階方法做自定義的反序列化修改。
由于 Java 的 Serializable 的性能較低,Android 需要重新設(shè)計(jì)一套更加輕量且高效的對(duì)象序列化和反序列化機(jī)制。Parcelable 正是在這個(gè)背景下產(chǎn)生的,它核心的作用就是為了解決 Android 中大量跨進(jìn)程通信的性能問(wèn)題。
Parcelable 的永久存儲(chǔ)
Parcelable 的原理十分簡(jiǎn)單,它的核心實(shí)現(xiàn)都在Parcel.cpp。
你可以發(fā)現(xiàn) Parcel 序列化和 Java 的 Serializable 序列化差別還是比較大的,Parcelable 只會(huì)在內(nèi)存中進(jìn)行序列化操作,并不會(huì)將數(shù)據(jù)存儲(chǔ)到磁盤(pán)里。
當(dāng)然我們也可以通過(guò)Parcel.java的 marshall 接口獲取 byte 數(shù)組,然后存在文件中從而實(shí)現(xiàn) Parcelable 的永久存儲(chǔ)。
// Returns the raw bytes of the parcel.
public final byte[] marshall() {
return nativeMarshall(mNativePtr);
}
// Set the bytes in data to be the raw bytes of this Parcel.
public final void unmarshall(byte[] data, int offset, int length) {
nativeUnmarshall(mNativePtr, data, offset, length);
}
Parcelable 的注意事項(xiàng)
在時(shí)間開(kāi)銷(xiāo)和使用成本的權(quán)衡上,Parcelable 機(jī)制選擇的是性能優(yōu)先。
所以它在寫(xiě)入和讀取的時(shí)候都需要手動(dòng)添加自定義代碼,使用起來(lái)相比 Serializable 會(huì)復(fù)雜很多。但是正因?yàn)檫@樣,Parcelable 才不需要采用反射的方式去實(shí)現(xiàn)序列化和反序列化。
雖然通過(guò)取巧的方法可以實(shí)現(xiàn) Parcelable 的永久存儲(chǔ),但是它也存在兩個(gè)問(wèn)題。
一般來(lái)說(shuō),如果需要持久化存儲(chǔ)的話(huà),一般還是不得不選擇性能更差的 Serializable 方案。
對(duì)象的序列化要記錄的信息還是比較多,在操作比較頻繁的時(shí)候,對(duì)應(yīng)用的影響還是不少的,這個(gè)時(shí)候我們可以選擇使用數(shù)據(jù)的序列化。
JSON
JSON 是一種輕量級(jí)的數(shù)據(jù)交互格式,它被廣泛使用在網(wǎng)絡(luò)傳輸中,很多應(yīng)用與服務(wù)端的通信都是使用 JSON 格式進(jìn)行交互。
JSON 的確有很多得天獨(dú)厚的優(yōu)勢(shì),主要有:
因?yàn)槊總€(gè)應(yīng)用基本都會(huì)用到 JSON,所以每個(gè)大廠(chǎng)也基本都有自己的“輪子”。例如 Android 自帶的 JSON 庫(kù)、Google 的Gson、阿里巴巴的Fastjson、美團(tuán)的MSON。
各個(gè)自研的 JSON 方案主要在下面兩個(gè)方面進(jìn)行優(yōu)化:
便利性。例如支持 JSON 轉(zhuǎn)換成 JavaBean 對(duì)象,支持注解,支持更多的數(shù)據(jù)類(lèi)型等。
性能。減少反射,減少序列化過(guò)程內(nèi)存與 CPU 的使用,特別是在數(shù)據(jù)量比較大或者嵌套層級(jí)比較深的時(shí)候效果會(huì)比較明顯。
在數(shù)據(jù)量比較少的時(shí)候,系統(tǒng)自帶的 JSON 庫(kù)還稍微有一些優(yōu)勢(shì)。但在數(shù)據(jù)量大了之后,差距逐漸被拉開(kāi)??偟膩?lái)說(shuō),Gson 的兼容性最好,一般情況下它的性能與 Fastjson 相當(dāng)。但是在數(shù)據(jù)量極大的時(shí)候,F(xiàn)astjson 的性能更好。
Protocol Buffers
相比對(duì)象序列化方案,JSON 的確速度更快、體積更小。不過(guò)為了保證 JSON 的中間結(jié)果是可讀的,它并沒(méi)有做二進(jìn)制的壓縮,也因此 JSON 的性能還沒(méi)有達(dá)到極致。
如果應(yīng)用的數(shù)據(jù)量非常大,又或者對(duì)性能有更高的要求,此時(shí)Protocol Buffers是一個(gè)非常好的選擇。它是 Google 開(kāi)源的跨語(yǔ)言編碼協(xié)議,Google 內(nèi)部的幾乎所有 RPC 都在使用這個(gè)協(xié)議。
總結(jié)一下它的優(yōu)缺點(diǎn):
對(duì)于 Android 來(lái)說(shuō),官方的 Protocol Buffers 會(huì)導(dǎo)致生成的方法數(shù)很多。我們可以修改它的自動(dòng)代碼生成工具,例如在微信中,每個(gè).proto 生成的類(lèi)文件只會(huì)包含一個(gè)方法即 op 方法。
/**
* Protobuf enum {@code Transport}
*/
public enum Transport
implements com.google.protobuf.Internal.EnumLite {
/**
* <code>BLUETOOTH_LOW_ENERGY = 0;</code>
*/
BLUETOOTH_LOW_ENERGY(0),
/**
* <code>BLUETOOTH_RFCOMM = 1;</code>
*/
BLUETOOTH_RFCOMM(1),
/**
* <code>BLUETOOTH_IAP = 2;</code>
*/
BLUETOOTH_IAP(2),
UNRECOGNIZED(-1),
;
/**
* <code>BLUETOOTH_LOW_ENERGY = 0;</code>
*/
public static final int BLUETOOTH_LOW_ENERGY_VALUE = 0;
/**
* <code>BLUETOOTH_RFCOMM = 1;</code>
*/
public static final int BLUETOOTH_RFCOMM_VALUE = 1;
/**
* <code>BLUETOOTH_IAP = 2;</code>
*/
public static final int BLUETOOTH_IAP_VALUE = 2;
@Override
public final int getNumber() {
if (this == UNRECOGNIZED) {
throw new IllegalArgumentException(
"Can't get the number of an unknown enum value.");
}
return value;
}
Google 后面還推出了壓縮率更高的 FlatBuffers。
前面講到的存儲(chǔ)方法的使用場(chǎng)景:少量的 Key Value 數(shù)據(jù)可以直接使用 SharedPreferences,稍微復(fù)雜一些的數(shù)據(jù)類(lèi)型也可以通過(guò)序列化成 JSON 或者 Protocol Buffers 保存,并且在開(kāi)發(fā)中獲取或者修改數(shù)據(jù)也很簡(jiǎn)單。
不過(guò)這幾種方法中,數(shù)據(jù)量在幾百上千條這個(gè)量級(jí)時(shí)它們的性能還可以接受,但如果是幾萬(wàn)條的呢?而且如何實(shí)現(xiàn)快速地對(duì)某幾個(gè)聯(lián)系人的數(shù)據(jù)做增刪改查呢?
對(duì)于大數(shù)據(jù)的存儲(chǔ)場(chǎng)景,我們需要考慮穩(wěn)定性、性能和可擴(kuò)展性,講存儲(chǔ)優(yōu)化一定繞不開(kāi)數(shù)據(jù)庫(kù),而數(shù)據(jù)庫(kù)這個(gè)主題又非常大。那么考慮到我們大多是從事移動(dòng)開(kāi)發(fā)的工作,這里重點(diǎn)說(shuō)說(shuō)移動(dòng)端數(shù)據(jù)庫(kù) SQLite 的使用和優(yōu)化。
雖然市面上有很多的數(shù)據(jù)庫(kù),但受限于庫(kù)體積和存儲(chǔ)空間,適合移動(dòng)端使用的還真不多。
坦白說(shuō)可能很多 BAT 的高級(jí)開(kāi)發(fā)工程師都不完全了解 SQLite 的內(nèi)部機(jī)制,也不能正確地寫(xiě)出高效的 SQL 語(yǔ)句。大部分應(yīng)用為了提高開(kāi)發(fā)效率,會(huì)引入 ORM 框架。ORM(Object Relational Mapping)也就是對(duì)象關(guān)系映射,用面向?qū)ο蟮母拍畎褦?shù)據(jù)庫(kù)中表和對(duì)象關(guān)聯(lián)起來(lái),可以讓我們不用關(guān)心數(shù)據(jù)庫(kù)底層的實(shí)現(xiàn)。
Android 中最常用的 ORM 框架有開(kāi)源 GreenDAO 和 Google 官方的 Room,那使用 ORM 框架會(huì)帶來(lái)什么問(wèn)題呢?
使用 ORM 框架比較簡(jiǎn)單,但是簡(jiǎn)易性是需要犧牲部分執(zhí)行效率為代價(jià)的,具體的損耗跟 ORM 框架寫(xiě)得好不好很有關(guān)系。但可能更大的問(wèn)題是讓很多的開(kāi)發(fā)者的思維固化,最后可能連簡(jiǎn)單的 SQL 語(yǔ)句都不會(huì)寫(xiě)了。
如果在項(xiàng)目中有使用 SQLite,那么下面這個(gè)SQLiteDatabaseLockedException就是經(jīng)常會(huì)出現(xiàn)的一個(gè)問(wèn)題。
android.database.sqlite.SQLiteDatabaseLockedException: database is locked
at android.database.sqlite.SQLiteDatabase.dbopen
at android.database.sqlite.SQLiteDatabase.openDatabase
at android.database.sqlite.SQLiteDatabase.openDatabase
SQLiteDatabaseLockedException 歸根到底是因?yàn)椴l(fā)導(dǎo)致,而 SQLite 的并發(fā)有兩個(gè)維度,一個(gè)是多進(jìn)程并發(fā),一個(gè)是多線(xiàn)程并發(fā)。下面我們分別來(lái)講一下它們的關(guān)鍵點(diǎn)。
SQLite 默認(rèn)是支持多進(jìn)程并發(fā)操作的,它通過(guò)文件鎖來(lái)控制多進(jìn)程的并發(fā)。SQLite 鎖的粒度并沒(méi)有非常細(xì),它針對(duì)的是整個(gè) DB 文件,內(nèi)部有 5 個(gè)狀態(tài)。
簡(jiǎn)單來(lái)說(shuō),多進(jìn)程可以同時(shí)獲取 SHARED 鎖來(lái)讀取數(shù)據(jù),但是只有一個(gè)進(jìn)程可以獲取 EXCLUSIVE 鎖來(lái)寫(xiě)數(shù)據(jù)庫(kù)。在 EXCLUSIVE 模式下,數(shù)據(jù)庫(kù)連接在斷開(kāi)前都不會(huì)釋放 SQLite 文件的鎖,從而避免不必要的沖突,提高數(shù)據(jù)庫(kù)訪(fǎng)問(wèn)的速度。
相比多進(jìn)程,多線(xiàn)程的數(shù)據(jù)庫(kù)訪(fǎng)問(wèn)可能會(huì)更加常見(jiàn)。SQLite 支持多線(xiàn)程并發(fā)模式,需要開(kāi)啟下面的配置,當(dāng)然系統(tǒng) SQLite 會(huì)默認(rèn)開(kāi)啟多線(xiàn)程Multi-thread 模式。
跟多進(jìn)程的鎖機(jī)制一樣,為了實(shí)現(xiàn)簡(jiǎn)單,SQLite 鎖的粒度都是數(shù)據(jù)庫(kù)文件級(jí)別,并沒(méi)有實(shí)現(xiàn)表級(jí)甚至行級(jí)的鎖。還有需要說(shuō)明的是,同一個(gè)句柄同一時(shí)間只有一個(gè)線(xiàn)程在操作,這個(gè)時(shí)候我們需要打開(kāi)連接池 Connection Pool。
如果使用 WCDB 在初始化的時(shí)候可以指定連接池的大小,在微信中我們?cè)O(shè)置的大小是 4。
public static SQLiteDatabase openDatabase (String path,
SQLiteDatabase.CursorFactory factory,
int flags,
DatabaseErrorHandler errorHandler,
int poolSize)
跟多進(jìn)程類(lèi)似,多線(xiàn)程可以同時(shí)讀取數(shù)據(jù)庫(kù)數(shù)據(jù),但是寫(xiě)數(shù)據(jù)庫(kù)依然是互斥的。
說(shuō)到數(shù)據(jù)庫(kù)的查詢(xún)優(yōu)化,你第一個(gè)想到的肯定是建索引,那我就先來(lái)講講 SQLite 的索引優(yōu)化。
正確使用索引在大部分的場(chǎng)景可以大大降低查詢(xún)速度,關(guān)鍵在于如何正確的建立索引,很多時(shí)候我們以為已經(jīng)建立了索引,但事實(shí)上并沒(méi)有真正生效。例如使用了 BETWEEN、LIKE、OR 這些操作符、使用表達(dá)式或者 case when 等。
BETWEEN:myfiedl 索引無(wú)法生效
SELECT * FROM mytable WHERE myfield BETWEEN 10 and 20;
轉(zhuǎn)換成:myfiedl 索引可以生效
SELECT * FROM mytable WHERE myfield >= 10 AND myfield <= 20;
總的來(lái)說(shuō)索引優(yōu)化是 SQLite 優(yōu)化中最簡(jiǎn)單同時(shí)也是最有效的,但是它并不是簡(jiǎn)單的建一個(gè)索引就可以了,有的時(shí)候我們需要進(jìn)一步調(diào)整查詢(xún)語(yǔ)句甚至是表的結(jié)構(gòu),這樣才能達(dá)到最好的效果。
關(guān)于 SQLite 的使用優(yōu)化還有很多很多,例如:
在日常的開(kāi)發(fā)中,我們都應(yīng)該對(duì)這些知識(shí)有所了解,通過(guò)引進(jìn) ORM,可以大大的提升我們的開(kāi)發(fā)效率。通過(guò) WAL 模式和連接池,可以提高 SQLite 的并發(fā)性能。通過(guò)正確的建立索引,可以提升 SQLite 的查詢(xún)速度。通過(guò)調(diào)整默認(rèn)的頁(yè)大小和緩存大小,可以提升 SQLite 的整體性能。
除了 SQLite 的優(yōu)化經(jīng)驗(yàn),也有其他的一些經(jīng)驗(yàn)。
加密與安全
數(shù)據(jù)庫(kù)的安全主要有兩個(gè)方面,一個(gè)是防注入,一個(gè)是加密。防注入可以通過(guò)靜態(tài)安全掃描的方式,而加密一般會(huì)使用 SQLCipher 支持。
SQLite 的加解密都是以頁(yè)為單位,默認(rèn)會(huì)使用 AES 算法加密,加 / 解密的耗時(shí)跟選用的密鑰長(zhǎng)度有關(guān)。下面是WCDB Android Benchmark的數(shù)據(jù),詳細(xì)的信息請(qǐng)查看鏈接里的說(shuō)明,從結(jié)論來(lái)說(shuō)對(duì) Create 來(lái)說(shuō)影響會(huì)高達(dá)到 10 倍。
首先我想說(shuō),正確使用索引,正確使用事務(wù)。對(duì)于大型項(xiàng)目來(lái)說(shuō),參與的開(kāi)發(fā)人員可能有幾十幾百人,開(kāi)發(fā)人員水平參差不齊,很難保證每個(gè)人都可以正確而高效地使用 SQLite,所以這次時(shí)候需要建立完善的監(jiān)控體系。
本地測(cè)試
作為一名靠譜的開(kāi)發(fā)工程師,我們每寫(xiě)一個(gè) SQL 語(yǔ)句,都應(yīng)該先在本地測(cè)試。我們可以通過(guò) EXPLAIN QUERY PLAN 測(cè)試 SQL 語(yǔ)句的查詢(xún)計(jì)劃,是全表掃描還是使用了索引,以及具體使用了哪個(gè)索引等。
sqlite> EXPLAIN QUERY PLAN SELECT * FROM t1 WHERE a=1 AND b>2;
QUERY PLAN
|--SEARCH TABLE t1 USING INDEX i2 (a=? AND b>?)
關(guān)于 SQLite 命令行與 EXPLAIN QUERY PLAN 的使用,可以參考Command Line Shell For SQLite以及EXPLAIN QUERY PLAN。
耗時(shí)監(jiān)控
本地測(cè)試過(guò)于依賴(lài)開(kāi)發(fā)人員的自覺(jué)性,所以很多時(shí)候我們依然需要建立線(xiàn)上大數(shù)據(jù)的監(jiān)控。我們想要監(jiān)控某些特定的模塊,可以通過(guò)這些接口監(jiān)控?cái)?shù)據(jù)庫(kù) busy、損耗以及執(zhí)行耗時(shí)。針對(duì)耗時(shí)比較長(zhǎng)的 SQL 語(yǔ)句,需要進(jìn)一步檢查是 SQL 語(yǔ)句寫(xiě)得不好,還是需要建立索引。
數(shù)據(jù)存儲(chǔ)是一個(gè)開(kāi)發(fā)人員的基本功,如何在合適的場(chǎng)景選擇合適的存儲(chǔ)方法是存儲(chǔ)優(yōu)化的必修課,你應(yīng)該學(xué)會(huì)通過(guò)正確性、時(shí)間開(kāi)銷(xiāo)、空間開(kāi)銷(xiāo)、安全、開(kāi)發(fā)成本以及兼容性這六大關(guān)鍵要素來(lái)分解某個(gè)存儲(chǔ)方法。
在設(shè)計(jì)某個(gè)存儲(chǔ)方案的時(shí)候也是同樣的道理,我們無(wú)法同時(shí)把所有的要素都做得最好,因此要學(xué)會(huì)取舍和選擇,在存儲(chǔ)的世界里不存在全局最優(yōu)解,我們要找的是局部的最優(yōu)解。這個(gè)時(shí)候更應(yīng)明確自己的訴求,大膽犧牲部分關(guān)鍵點(diǎn)的指標(biāo),將自己場(chǎng)景最關(guān)心的要素點(diǎn)做到最好。