摘要: Android MediaProvider 使用 SQLite 數據庫存儲圖片、視頻、音頻等多媒體文件的信息,供視頻播放器、音樂播放器、圖庫使用。本文詳細分析了 Android MediaProvider 多媒體數據庫(以 SDK 2.3.3 爲例)的模式(schema),並簡要敘述與系統媒體掃描服務 MediaScanner 的交互。html
1. 如何提取數據庫
以 root 權限進入 adb shell,使用 sqlite3 打開位於手機上 /data/data/com.android.providers.media/databases 上的一個數據庫。以 external 開頭的數據庫存儲的是 SD 卡媒體信息,一張卡對應一個,因此若是手機使用過多張卡會有多個數據庫。以 internal 開頭的數據庫存儲手機內部存儲器的媒體信息。由於通常用戶沒法訪問手機內部存儲器,並且這兩個數據庫結構是大致上是相同的,因此只須要關注 external 數據庫便可。mysql
Note: 數據庫都是以相似 external-ffffffff.db 的形式命名的, 後面的 8 個 16 進制字符是該 SD 卡 FAT 分區的 Volume ID。該 ID 是分區時決定的,只有從新分區或者手動改變纔會更改,能夠防止插入不一樣 SD 卡時數據庫衝突。要簡單瞭解 FAT 文件系統請看 Understanding FAT Filesystems android
接着在 sqlite3 執行命令 .schema 便可導出建立數據庫的 SQL 語句,也就是數據庫模式,具體以下(單擊展開代碼):git
1 |
CREATE TABLE album_art (album_id INTEGER PRIMARY KEY ,_data TEXT); |
2 |
CREATE TABLE albums (album_id INTEGER PRIMARY KEY ,album_key TEXT NOT NULL UNIQUE ,album TEXT NOT NULL ); |
3 |
CREATE TABLE android_metadata (locale TEXT); |
4 |
CREATE TABLE artists (artist_id INTEGER PRIMARY KEY ,artist_key TEXT NOT NULL UNIQUE ,artist TEXT NOT NULL ); |
5 |
CREATE TABLE audio_genres (_id INTEGER PRIMARY KEY , name TEXT NOT NULL ); |
6 |
CREATE TABLE audio_genres_map (_id INTEGER PRIMARY KEY ,audio_id INTEGER NOT NULL ,genre_id INTEGER NOT NULL ); |
7 |
CREATE TABLE audio_meta (_id INTEGER PRIMARY KEY ,_data TEXT UNIQUE NOT NULL ,_display_name TEXT,_size INTEGER ,mime_type TEXT,date_added INTEGER ,date_modified INTEGER ,title TEXT NOT NULL ,title_key TEXT NOT NULL ,duration INTEGER ,artist_id INTEGER ,composer TEXT,album_id INTEGER ,track INTEGER , year INTEGER CHECK ( year !=0),is_ringtone INTEGER ,is_music INTEGER ,is_alarm INTEGER ,is_notification INTEGER , is_podcast INTEGER , bookmark INTEGER ); |
8 |
CREATE TABLE audio_playlists (_id INTEGER PRIMARY KEY ,_data TEXT, name TEXT NOT NULL ,date_added INTEGER ,date_modified INTEGER ); |
9 |
CREATE TABLE audio_playlists_map (_id INTEGER PRIMARY KEY ,audio_id INTEGER NOT NULL ,playlist_id INTEGER NOT NULL ,play_order INTEGER NOT NULL ); |
10 |
CREATE TABLE images (_id INTEGER PRIMARY KEY ,_data TEXT,_size INTEGER ,_display_name TEXT,mime_type TEXT,title TEXT,date_added INTEGER ,date_modified INTEGER ,description TEXT,picasa_id TEXT,isprivate INTEGER ,latitude DOUBLE ,longitude DOUBLE ,datetaken INTEGER ,orientation INTEGER ,mini_thumb_magic INTEGER ,bucket_id TEXT,bucket_display_name TEXT); |
11 |
CREATE TABLE thumbnails (_id INTEGER PRIMARY KEY ,_data TEXT,image_id INTEGER ,kind INTEGER ,width INTEGER ,height INTEGER ); |
12 |
CREATE TABLE video (_id INTEGER PRIMARY KEY ,_data TEXT NOT NULL ,_display_name TEXT,_size INTEGER ,mime_type TEXT,date_added INTEGER ,date_modified INTEGER ,title TEXT,duration INTEGER ,artist TEXT,album TEXT,resolution TEXT,description TEXT,isprivate INTEGER ,tags TEXT,category TEXT,language TEXT,mini_thumb_data TEXT,latitude DOUBLE ,longitude DOUBLE ,datetaken INTEGER ,mini_thumb_magic INTEGER , bucket_id TEXT, bucket_display_name TEXT, bookmark INTEGER ); |
13 |
CREATE TABLE videothumbnails (_id INTEGER PRIMARY KEY ,_data TEXT,video_id INTEGER ,kind INTEGER ,width INTEGER ,height INTEGER ); |
15 |
CREATE VIEW album_info AS SELECT audio.album_id AS _id, album, album_key, MIN ( year ) AS minyear, MAX ( year ) AS maxyear, artist, artist_id, artist_key, count (*) AS numsongs,album_art._data AS album_art FROM audio LEFT OUTER JOIN album_art ON audio.album_id=album_art.album_id WHERE is_music=1 GROUP BY audio.album_id; |
16 |
CREATE VIEW artist_info AS SELECT artist_id AS _id, artist, artist_key, COUNT ( DISTINCT album_key) AS number_of_albums, COUNT (*) AS number_of_tracks FROM audio WHERE is_music=1 GROUP BY artist_key; |
17 |
CREATE VIEW artists_albums_map AS SELECT DISTINCT artist_id, album_id FROM audio_meta; |
18 |
CREATE VIEW audio as SELECT * FROM audio_meta LEFT OUTER JOIN artists ON audio_meta.artist_id=artists.artist_id LEFT OUTER JOIN albums ON audio_meta.album_id=albums.album_id; |
19 |
CREATE VIEW search AS SELECT _id, 'artist' AS mime_type,artist, NULL AS album, NULL AS title,artist AS text1, NULL AS text2,number_of_albums AS data1,number_of_tracks AS data2,artist_key AS match, 'content://media/external/audio/artists/' ||_id AS suggest_intent_data,1 AS grouporder FROM artist_info WHERE (artist!= '<unknown>' ) UNION ALL SELECT _id, 'album' AS mime_type,artist,album, NULL AS title,album AS text1,artist AS text2, NULL AS data1, NULL AS data2,artist_key|| ' ' ||album_key AS match, 'content://media/external/audio/albums/' ||_id AS suggest_intent_data,2 AS grouporder FROM album_info WHERE (album!= '<unknown>' ) UNION ALL SELECT searchhelpertitle._id AS _id,mime_type,artist,album,title,title AS text1,artist AS text2, NULL AS data1, NULL AS data2,artist_key|| ' ' ||album_key|| ' ' ||title_key AS match, 'content://media/external/audio/media/' ||searchhelpertitle._id AS suggest_intent_data,3 AS grouporder FROM searchhelpertitle WHERE (title != '' ); |
20 |
CREATE VIEW searchhelpertitle AS SELECT * FROM audio ORDER BY title_key; |
22 |
CREATE INDEX album_id_idx on audio_meta(album_id); |
23 |
CREATE INDEX album_idx on albums(album); |
24 |
CREATE INDEX albumkey_index on albums(album_key); |
25 |
CREATE INDEX artist_id_idx on audio_meta(artist_id); |
26 |
CREATE INDEX artist_idx on artists(artist); |
27 |
CREATE INDEX artistkey_index on artists(artist_key); |
28 |
CREATE INDEX image_bucket_index ON images(bucket_id, datetaken); |
29 |
CREATE INDEX image_id_index on thumbnails(image_id); |
30 |
CREATE INDEX sort_index on images(datetaken ASC , _id ASC ); |
31 |
CREATE INDEX title_idx on audio_meta(title); |
32 |
CREATE INDEX titlekey_index on audio_meta(title_key); |
33 |
CREATE INDEX video_bucket_index ON video(bucket_id, datetaken); |
34 |
CREATE INDEX video_id_index on videothumbnails(video_id); |
36 |
CREATE TRIGGER albumart_cleanup1 DELETE ON albums BEGIN DELETE FROM album_art WHERE album_id = old.album_id; END ; |
37 |
CREATE TRIGGER albumart_cleanup2 DELETE ON album_art BEGIN SELECT _DELETE_FILE(old._data); END ; |
38 |
CREATE TRIGGER audio_delete INSTEAD OF DELETE ON audio BEGIN DELETE from audio_meta where _id=old._id; DELETE from audio_playlists_map where audio_id=old._id; DELETE from audio_genres_map where audio_id=old._id; END ; |
39 |
CREATE TRIGGER audio_genres_cleanup DELETE ON audio_genres BEGIN DELETE FROM audio_genres_map WHERE genre_id = old._id; END ; |
40 |
CREATE TRIGGER audio_meta_cleanup DELETE ON audio_meta BEGIN DELETE FROM audio_genres_map WHERE audio_id = old._id; DELETE FROM audio_playlists_map WHERE audio_id = old._id; END ; |
41 |
CREATE TRIGGER audio_playlists_cleanup DELETE ON audio_playlists BEGIN DELETE FROM audio_playlists_map WHERE playlist_id = old._id; SELECT _DELETE_FILE(old._data); END ; |
42 |
CREATE TRIGGER images_cleanup DELETE ON images BEGIN DELETE FROM thumbnails WHERE image_id = old._id; SELECT _DELETE_FILE(old._data); END ; |
43 |
CREATE TRIGGER thumbnails_cleanup DELETE ON thumbnails BEGIN SELECT _DELETE_FILE(old._data); END ; |
44 |
CREATE TRIGGER video_cleanup DELETE ON video BEGIN SELECT _DELETE_FILE(old._data); END ; |
45 |
CREATE TRIGGER videothumbnails_cleanup DELETE ON videothumbnails BEGIN SELECT _DELETE_FILE(old._data); END ; |
Note: 若是手機沒有 sqlite3 程序,能夠搜索編譯過的源代碼的 out 目錄找到可執行文件,大約 90kb,而後 adb push 到手機的 /system/bin/ 目錄。安裝 sqlite三、查詢數據庫均須要 adb root 權限。 Android 的多媒體數據庫主要由表、視圖、索引以及觸發器組成。算法
接着還須要把數據庫轉換成圖,手工轉換的話就是根據 SQL 語句自行畫圖;推薦懶人使用自動轉換,先使用 adb pull 把數據庫導出,再使用 Power Designer 或者 Visio 的逆向工程(Reverse Engineer)功能生成物理數據模型(Physical Data Model)。注意要鏈接 sqlite 數據庫文件的話須要先安裝 sqlite 的 ODBC 驅動,教程在這裏:SQLite ODBC Driversql
2. 數據庫模式分析
圖片數據庫
圖片數據庫由兩個表組成,分別是 images 和 thumbnails,物理數據模型以下所示(Power Designer 逆向工程生成)shell
Note: 如何數據庫物理模型圖:<pk> 表示此爲主鍵。其他的表名、字段名、數據類型應該都能看明白。數據庫
Note: SQLite 從 3.6.19 版纔開始支持外鍵約束,Android 2.3.3 使用的是 3.7.x,但並無使用此特性,而是經過操做數據庫的程序(如 MediaScanner)以及觸發器來維護數據庫的一致性。這裏能夠了解 SQLite 的外鍵支持狀況緩存
數據表字段解析以下:網絡
images:圖片信息
字段 |
解析 |
_id |
主鍵。圖片 id,從 1 開始自增 |
_data |
圖片絕對路徑 |
_size |
文件大小,單位爲 byte |
_display_name |
文件名 |
mime_type |
相似於 image/jpeg 的 MIME 類型 |
title |
不帶擴展名的文件名 |
date_added |
添加到數據庫的時間,單位秒 |
date_modified |
文件最後修改時間,單位秒 |
description |
|
picasa_id |
用於 picasa 網絡相冊 |
isprivate |
|
latitude |
緯度,須要照片有 GPS 信息 |
longitude |
經度,須要照片有 GPS 信息 |
datetaken |
取自 EXIF 照片拍攝時間,若爲空則等於文件修改時間,單位毫秒 |
orientation |
取自 EXIF 旋轉角度,在圖庫旋轉圖片也會改變此值 |
mini_thumb_magic |
取小縮略圖時生成的一個隨機數,見 MediaThumbRequest |
bucket_id |
等於 path.toLowerCase.hashCode(),見 MediaProvider.computeBucketValues() |
bucket_display_name |
直接包含圖片的文件夾就是該圖片的 bucket,就是文件夾名 |
thumbnails:縮略圖
字段 |
解析 |
_id |
主鍵。縮略圖 id,從 1 開始自增 |
_data |
圖片絕對路徑 |
image_id |
縮略圖所對應圖片的 id,依賴於 images 表 _id 字段,可創建外鍵 |
kind |
縮略圖類型,1 是大縮略圖,2 基本不用,3 是微型縮略圖但其信息不保存在數據庫 |
width |
縮略圖寬度 |
height |
縮略圖高度 |
視頻數據庫
數據表字段解析以下:
video:視頻信息
字段 |
解析 |
_id |
主鍵。視頻 id |
_data |
視頻絕對路徑 |
_display_name |
文件名 |
_size |
文件大小,單位爲 byte |
mime_type |
相似於 video/avi 的 MIME 類型 |
date_added |
添加到數據庫的時間,單位秒 |
date_modified |
文件最後修改時間,單位秒 |
title |
不帶擴展名的文件名 |
duration |
視頻時長,單位毫秒 |
artist |
藝術家 |
album |
專輯名,通常爲文件夾名 |
resolution |
|
description |
|
isprivate |
|
tags |
|
category |
|
language |
|
mini_thumb_data |
|
latitude |
|
longitude |
|
datetaken |
|
mini_thumb_magic |
取小縮略圖時生成的一個隨機數,見 MediaThumbRequest |
bucket_id |
等於 path.toLowerCase.hashCode(),見 MediaProvider.computeBucketValues() |
bucket_display_name |
直接包含視頻的文件夾就是該圖片的 bucket,就是文件夾名 |
bookmark |
|
videothumbnails:視頻縮略圖
字段 |
解析 |
_id |
主鍵。縮略圖 id |
_data |
縮略圖絕對路徑 |
video_id |
縮略圖所對應視頻的 id,依賴於 video 表 _id 字段 |
kind |
縮略圖類型,1 是大圖,視頻只能取類型 1 |
width |
縮略圖寬度 |
height |
縮略圖高度 |
音頻數據庫
音頻數據庫是最複雜的,由 10 個表組成。物理數據模型以下所示:
album_art:專輯封面
字段 |
解析 |
album_id |
主鍵。專輯 id |
_data |
專輯封面緩存的路徑 |
albums:專輯信息
字段 |
解析 |
album_id |
主鍵。專輯 id |
album_key |
全大寫字母,用於字母索引 |
album |
專輯名 |
android_metadata:當前字符編碼
字段 |
解析 |
locale |
默認字符編碼,例如 zh_CN |
artists:藝術家
字段 |
解析 |
artist_id |
主鍵。藝術家 id |
artist_key |
全大寫字母,用於字母索引 |
artist |
藝術家 |
audio_genres:流派
字段 |
解析 |
_id |
主鍵。流派 id |
name |
流派名稱 |
audio_genres_map:音頻流派映射
字段 |
解析 |
_id |
主鍵。映射 id |
audio_id |
音頻 id |
genre_id |
流派 id |
Note: 爲什麼要創建映射表:爲了消除數據冗餘。假若有大量音頻屬於同一流派,若是沒有映射表則須要每一個音頻都須要記錄一樣的流派數據,有了映射表以後則只有一條記錄就夠了。這符合數據庫設計的第三範式(the 3rd normal form)
audio_meta:音頻信息
字段 |
解析 |
_id |
主鍵。音頻 id |
_data |
文件絕對路徑 |
_display_name |
文件名 |
_size |
文件大小,單位 byte |
mime_type |
相似於 audio/mpeg 的 MIME 類型 |
date_added |
添加到數據庫的時間,單位秒 |
date_modified |
文件最後修改時間,單位秒 |
title |
來自 ID3 信息的標題,無則爲不帶擴展名的文件名 |
title_key |
全大寫字母的標題 |
duration |
時長 |
artist_id |
藝術家 id |
composer |
來自 ID3 信息,做曲家 |
album_id |
專輯 id |
track |
來自 ID3 信息,音軌 |
year |
來自 ID3 信息,年代 |
is_ringtone |
是否鈴聲,0 或 1 |
is_music |
是否音樂,1 纔會在音樂播放器顯示 |
is_alarm |
是否鬧鐘鈴聲 |
is_notification |
是否通知鈴聲 |
is_podcast |
是否 podcast |
bookmark |
|
audio_playlists:播放列表
字段 |
解析 |
_id |
主鍵。播放列表 id |
_data |
|
name |
播放列表名 |
date_added |
|
date_modified |
|
audio_playlists_map:音頻播放列表映射
字段 |
解析 |
_id |
主鍵。映射 id |
audio_id |
音頻 id |
playlist_id |
播放列表 id |
play_order |
播放順序 |
索引
在 Android 數據庫當中基本上使用自增 id 值做爲主鍵,並創建了索引。索引能夠加快數據查找速度,但因爲須要維護索引因此插入/刪除等寫入操做速度會變慢。索引以下:
1 |
CREATE INDEX album_id_idx on audio_meta(album_id); |
2 |
CREATE INDEX album_idx on albums(album); |
3 |
CREATE INDEX albumkey_index on albums(album_key); |
4 |
CREATE INDEX artist_id_idx on audio_meta(artist_id); |
5 |
CREATE INDEX artist_idx on artists(artist); |
6 |
CREATE INDEX artistkey_index on artists(artist_key); |
7 |
CREATE INDEX image_bucket_index ON images(bucket_id, datetaken); |
8 |
CREATE INDEX image_id_index on thumbnails(image_id); |
9 |
CREATE INDEX sort_index on images(datetaken ASC , _id ASC ); |
10 |
CREATE INDEX title_idx on audio_meta(title); |
11 |
CREATE INDEX titlekey_index on audio_meta(title_key); |
12 |
CREATE INDEX video_bucket_index ON video(bucket_id, datetaken); |
13 |
CREATE INDEX video_id_index on videothumbnails(video_id); |
因爲比較簡單就不解釋了,要深刻了解索引能夠參考這個關於 SQL Server 的分析MySQL索引背後的數據結構及算法原理,原理應該是差很少的。
視圖
視圖相似於表,但並不是獨立存在,是從其餘表裏面查詢數據獲得的。使用視圖能夠加快數據庫查詢速度,不用每次都執行復雜的 SQL 語句查詢。圖以下所示:
Note: 如何看視圖:圖下面的部分是數據來源的表,中間是從表中選取的字段,但相似於 COUNT 等 SQL 查詢操做沒法在圖上體現,最好仍是看實際 SQL 語句。
Note: SQLite 當中視圖都是隻讀的,也就是說不能對視圖進行插入、更新、刪除等操做。可是能夠在視圖創建 INSTEAD OF 觸發器來達到一樣的目的,多媒體數據庫當中的 audio_delete 觸發器就是如此。
觸發器
觸發器是爲了維護數據庫刪除操做而創建的,由於所刪除的表可能與另外的表有關係,須要同時刪除另一個表的字段。能夠看如下一個例子:
1 |
CREATE TRIGGER audio_meta_cleanup |
4 |
DELETE FROM audio_genres_map WHERE audio_id = old._id; |
5 |
DELETE FROM audio_playlists_map WHERE audio_id = old._id; |
這是關於 audio_meta 表的觸發器,意思是當刪除此表上的記錄時,同時刪除 audio_genres_map 表上 audio_id 與此表 id 相同的記錄,刪除 audio_playlists_map 表上 audio_id 與此表 id 相同的記錄。這樣當刪除 audio_meta 表的記錄時,另外兩個表的相應記錄也會自動刪除,不會因爲漏刪除而殘留多餘數據。
3. 如何維護數據庫
插入
插入、更新主要由 MediaScanner 進行,當刪除/移動媒體文件時 MediaScanner 會掃描磁盤並更新數據庫。數據插入主要在 endFile() 方法中進行,例如插入音頻記錄時相關的表都會插入相應的記錄。而圖片、視頻縮略圖,專輯封面這幾個則是第一次取圖片的時候纔會生成縮略圖保存到磁盤,並把記錄插入到數據庫中。
刪除
刪除操做主要由觸發器維護。例如當一個應用刪除圖片時,通常只會刪除圖片數據庫,因此必需要有觸發器同時刪除縮略圖數據庫。