交換數據格式
Google 推出的 Protocal Buffers 是一種更輕便高效的存儲結構,但消耗內存較大。
FlatBuffers 同樣由 Google 推出,專注性能,適合移動端。占用存儲比 Protocal 要大。
SharePreferences 優化
當 SharedPreferences 文件還沒有被加載到內存時,調用 getSharedPreferences 方法會初始化文件并讀入內存,這容易導致 耗時更長。
Editor 的 commit 或者 apply 方法的區別在于同步寫入和異步 寫入,以及是否需要返回值。在不需要返回值的情況下,使用 apply 方法可以極大提高性能。
SharedPreferences 類 中的 commitToMemory() 會鎖定 SharedPreference 對象,put() 和 getEditor() 方法會鎖定 Editor 對象,在寫入磁盤時更會鎖定一個寫入鎖。因此,最好的優化方法就是避免頻繁地讀寫 SharedPreferences,減少無謂的調用。對于 SharedPreferences 的批量操作,最好先獲取一個 editor 進行批量操作,然后調用 apply 方法。
Bitmap 解碼
- 4.4 以上 decodeFile 內部沒有使用緩存,效率不高。要使用 decodeStream,同時傳入的文件流為 BufferedInputStream。
- decodeResource 同樣存在性能問題,用 decodeResourceStream。
數據庫優化
使用 StringBuilder 代替 String
-
查詢時返回更少的結果集及更少的字段
查詢時只取需要的字段和結果集,更多的結果集會消耗更多的時間及內存,更多的字段會導致更多的內存消耗。
-
少用 cursor.getColumnIndex
根據性能調優過程中的觀察 cursor.getColumnIndex 的時間消耗跟 cursor.getInt 相差無幾。可以在建表的時候用 static 變量記住某列的 index,直接調用相應 index 而不是每次查詢。
-
異步線程
Android 中數據不多時表查詢可能耗時不多,不會導致 ANR,不過大于 100ms 時同樣會讓用戶感覺到延時和卡頓,可以放在線程中運行,但 sqlite 在并發方面存在局限,多線程控制較麻煩,這時候可使用單線程池,在任務中執行 db 操作,通過 handler 返回結果和 UI 線程交互,既不會影響 UI 線程,同時也能防止并發帶來的異常。
-
SQLiteOpenHelper 維持一個單例
因為 SQLite 對多線程的支持并不是很完善,如果兩個線程同時操作數據庫,因為數據庫被另一個線程占用, 這種情況下會報“Database is locked” 的異常。所以在數據庫管理類中使用單例模式,就可以保證無論在哪個線程中獲取數據庫對象,都是同一個。
最好的方法是所有的數據庫操作統一到同一個線程隊列管理,而業務層使用緩存同步,這樣可以完全避免多線程操作數據庫導致的不同步和死鎖問題。
-
Application 中初始化
- 使用 Application 的 Context 創建數據庫,在 Application 生命周期結束時再關閉。
- 在應用啟動過程中最先初始化完數據庫,避免進入應用后再初始化導致相關操作時間變長。
-
少用 AUTOINCREMENT
主鍵加上 AUTOINCREMENT 后,可以保證主鍵嚴格遞增,但并不能保證每次都加 1,因為在插入失敗后,失敗的行號不會被復用,會造成主鍵有間隔,繼而使 INSERT 耗時 1 倍以上。
這個 AUTOINCREMENT 關鍵詞會增加 CPU,內存,磁盤空間和磁盤 I/O 的負擔,所以 盡量不要用,除非必需。通常情況下都不是必需的。
事務
使用事務的兩大好處是原子提交和更優性能:
- 原子提交:意味著同一事務內的所有修改要么都完成要么都不做,如果某個修改失敗,會自動回滾使得所有修改不生效。
- 更優性能:Sqlite 默認會為每個插入、更新操作創建一個事務,并且在每次插入、更新后立即提交。這樣如果連續插入 100 次數據實際是創建事務、執行語句、提交這個過程被重復執行了 100 次。如果顯式的創建事務,這個過程只做一次,通過這種一次性事務可以使得性能大幅提升。尤其當數據庫位于 sd 卡時,時間上能節省兩個數量級左右。
主要三個方法:beginTransaction,setTransactionSuccessful,endTransaction。
SQLiteStatement
使用 Android 系統提供的 SQLiteStatement 來插入數據,在性能上有一定的提高,并且也解決了 SQL 注入的問題。
SQLiteStatement statement = dbOpenHelper.getWritableDatabase().compileStatement("INSERT INTO EMPERORS(name, dynasty, start_year) values(?,?,?)");
statement.clearBindings();
statement.bindString(1, "Max");
statement.bindString(2, "Luk");
statement.bindString(3, "1998");
statement.executeInsert();
SQLiteStatement 只能插入一個表中的數據,在插入前要清除上一次的數據。
索引
索引就像書本的目錄,目錄可以快速找到所在頁數,數據庫中索引可以幫助快速找到數據,而不用全表掃描,合適的索引可以大大提高數據庫查詢的效率。
優點:大大加快了數據庫檢索的速度,包括對單表查詢、連表查詢、分組查詢、排序查詢。經常是一到兩個數量級的性能提升,且隨著數據數量級增長。
缺點:
- 索引的創建和維護存在消耗,索引會占用物理空間,且隨著數據量的增加而增加。
- 在對數據庫進行增刪改時需要維護索引,所以會對增刪改的性能存在影響。
分類
-
直接創建索引和間接創建索引
- 直接創建: 使用 sql 語句創建,Android 中可以在 SQLiteOpenHelper 的 onCreate 或是 onUpgrade 中直接 excuSql 創建語句,如
CREATE INDEX mycolumn_index ON mytable (myclumn)
- 間接創建: 定義主鍵約束或者唯一性鍵約束,可以間接創建索引,主鍵默認為唯一索引。
- 直接創建: 使用 sql 語句創建,Android 中可以在 SQLiteOpenHelper 的 onCreate 或是 onUpgrade 中直接 excuSql 創建語句,如
-
普通索引和唯一性索引
- 普通索引:
CREATEINDEXmycolumn_indexONmytable(myclumn)
- 唯一性索引:保證在索引列中的全部數據是唯一的,對聚簇索引和非聚簇索引都可以使用,語句為
CREATE UNIQUE COUSTERED INDEX myclumn_cindex ON mytable(mycolumn)
- 普通索引:
-
單個索引和復合索引
- 單個索引:索引建立語句中僅包含單個字段,如上面的普通索引和唯一性索引創建示例。
- 復合索引:又叫組合索引,在索引建立語句中同時包含多個字段,如
CREATEINDEXname_indexONusername(firstname,lastname)
,其中 firstname 為前導列。
-
聚簇索引和非聚簇索引 (聚集索引,群集索引)
- 聚簇索引:物理索引,與基表的物理順序相同,數據值的順序總是按照順序排列,如
CREATE CLUSTERED INDEX mycolumn_cindex ON mytable(mycolumn) WITH ALLOW_DUP_ROW
,其中WITH ALLOW_DUP_ROW
表示允許有重復記錄的聚簇索引 - 非聚簇索引:
CREATEUNCLUSTEREDINDEXmycolumn_cindexONmytable(mycolumn)
,索引默認為非聚簇索引
- 聚簇索引:物理索引,與基表的物理順序相同,數據值的順序總是按照順序排列,如
使用場景
- 當某字段數據更新頻率較低,查詢頻率較高,經常有范圍查詢
(>, <, =,>=, <=)
或order by
、group by
發生時建議使用索引。并且選擇度(一個字段中唯一值的數量 / 總的數量)越大,建索引越有優勢 - 經常同時存取多列,且每列都含有重復值可考慮建立復合索引
使用規則
- 對于復合索引,把使用最頻繁的列做為前導列 (索引中第一個字段)。如果查詢時前導列不在查詢條件中則該復合索引不會被使用。如
create unique index PK_GRADE_CLASS on student (grade, class)
,select * from student where class = 2
未使用到索引,select * from dept where grade = 3
使用到了索引 - 避免對索引列進行計算,對 where 子句列的任何計算如果不能被編譯優化,都會導致查詢時索引失效
select * from student where tochar(grade)=’2
- 比較值避免使用 NULL
- 多表查詢時要注意是選擇合適的表做為內表。連接條件要充份考慮帶有索引的表、行數多的表,內外表的選擇可由公式:外層表中的匹配行數
*
內層表中每一次查找的次數確定,乘積最小為最佳方案。實際多表操作在被實際執行前,查詢優化器會根據連接條件,列出幾組可能的連接方案并從中找出系統開銷最小的最佳方案 - 查詢列與索引列次序一致
- 用多表連接代替 EXISTS 子句
- 把過濾記錄數最多的條件放在最前面
- 善于使用存儲過程,它使 sql 變得更加靈活和高效 (Sqlite 不支持存儲過程)
其它通用優化
- 經常用的數據讀取后緩存起來,以免多次重復讀寫造成“寫入放大”
- 子線程讀寫數據
- ObjectOutputStream 在序列化磁盤時,會把內存中的每個對象保存到磁盤,在保存對象的 時候,每個數據成員會帶來一次 I/O 操作。在 ObjectOutputStream 上面再封裝一個輸出流 ByteArrayOutputStream 或 BufferedOutputStream,先將對象序列化后的信息寫到緩存區中,然后再一次性地寫到磁盤上;相應的,用 ByteArrayInputStream 或 BufferedInputStream 替代 ObjectInputStream。
- 合理選擇緩沖區 Buffer 的大小。太小導致 I/O 操作次數增多,太大導致申請時間變長。比如 4-8 KB。