獨家食用指南系列|Android端SQLite的淺嘗輒止
大家好,本週技術拆解官的第一篇文章,給大家帶來的是我們的新主題《獨家食用指南系列》。作為新主題的第一篇系列文章,這次給大家分享下Android端SQLite的“食用指南”。之所以會拿SQLite作為系列的開篇文章,也是因為最近接觸到逆向Android端SQLite資料庫的工作,查閱了很多資料,我也想給這次調研做下總結,所以接下來我會分三篇文章來給大家好好講講這次逆向Android端SQLite資料庫的經歷,首當其衝的第一篇,也就是《獨家食用指南系列|Android端SQLite的淺嘗輒止》
本篇文章使用到的專案原始碼都在我的個人Github上面:https://github.com/lateautumn4lin/TechPaoding/tree/main/practice_demo/Cattle
1 認識SQLite
1.1 SQLite定義
SQLite是一款輕量級的關係型資料庫,為什麼是輕量級呢?主要是因為它佔用資源很少,通常只需要幾百K的記憶體就足夠了,也因為它的結構足夠簡單,運算速度非常快,特別適合在移動裝置上使用。
1.2 SQLite特點
- 輕量級
使用 SQLite 只需要帶一個動態庫(也就是NDK開發的SO庫),就可以享受它的全部功能,而且那個動態庫的尺寸想當小,因此對於移動端的使用來說可謂是“百利而無一害”。 - 獨立性
SQLite 資料庫的核心引擎不需要依賴第三方軟體,也不需要所謂的“安裝”。 - 隔離性
SQLite 資料庫中所有的資訊(比如表、檢視、觸發器等)都包含在一個資料夾內,方便管理和維護,具體的表現形式就是每個資料庫就是一個db檔案,相當於庫與庫之前是彼此隔離的。 - 跨平臺
SQLite 目前支援大部分作業系統,不至電腦作業系統更在眾多的手機系統也是能夠執行,比如:Android和IOS。 - 多語言介面
SQLite 資料庫支援多語言程式設計介面(在Android端話一般會使用JDK或者NDK來開發)。 - 安全性
SQLite 資料庫通過資料庫級上的獨佔性和共享鎖來實現獨立事務處理。這意味著多個程式可以在同一時間從同一資料庫讀取資料,但只能有一個可以寫入資料(涉及併發問題的處理方式類似於另一個關係型資料庫-MySQL)。 - 弱型別的欄位
同一列中的資料可以是不同型別(編碼自由者的福音、編碼強迫症的噩夢)
1.3 SQLite資料型別
SQLite具有以下五種常用的資料型別,都是比較常見的基本型別:
型別 | 含義 |
---|---|
NULL | 值是一個 NULL 值 |
INTEGER | 值是一個帶符號的整數,根據值的大小儲存在 1、2、3、4、6 或 8 位元組中 |
REAL | 值是一個浮點值,儲存為 8 位元組的 IEEE 浮點數字 |
TEXT | 值是一個文字字串,使用資料庫編碼(UTF-8、UTF-16BE 或 UTF-16LE)儲存 |
BLOB | 值是一個 blob 資料,完全根據它的輸入儲存 |
以上就是關於SQLite的基本知識,大家大致瞭解就好,詳細的資訊可以看看它的WIKI(https://zh.wikipedia.org/wiki/SQLite)
1.4 SQLite安裝
關於SQLite的安裝,這裡我們舉CentOS7來說,最簡單的方式當然是“懶人一鍵安裝”了
sudo yum install sqlite-devel
安裝好之後就可以和正常的資料庫那樣,大家就可以試試“增刪改查”了,比如下面這樣
- 首先我們指定一個db檔案進入互動式的介面,指定db檔案也是為了我們之後的建表的操作能夠得到儲存
- 然後就是正常對於資料庫的“增刪改查”了
當然在命令列的操作也並不是我們這次關心的重點,我們之後會利用Java的庫來操作。
2 SQLite的除錯工具
上面我們提到了SQLite它其實是一個關係型資料庫,那麼聯想到MySQL的話,SQLite也有它的特定的視覺化工具又或者可以說是除錯工具,因為我們後面會利用這個工具來實時檢視App的資料儲存情況。
2.1 直接命令列除錯
命令列除錯的方式和上面我們說的在CentOS7運算元據庫的方式基本類似,因為Android本身也是一個Linux系統,相關命令也是一致的。如果不嫌麻煩的話就是可以直接adb shell
進手機系統去操作。
2.2 SQLiteStudio工具
相比於命令列我想更多人會選擇視覺化的工具來操作,這裡我推薦一下我自己目前在使用的工具SQLiteStudio,其實在選擇工具的時候我也是在很多工具中糾結了一番,不過其他工具不是不能實時連線Android機除錯就是在安全加密方面功能方面差一截,所以最後直接選擇了SQLiteStudio了。
最新版本的SQLiteStudio是SQLiteStudio3.2,地址是“https://sqlitestudio.pl/”,大家可以去官網下載,這也是個開源專案,要是大家有問題也可以去Github打擾那位大佬。
整體介面是這樣的
照例演示下新增新資料庫的流程
Database->Add a Database->開啟自己選定好的db檔案即可。
3 SQLite 基礎類介紹
上面我們講了SQLite的定義以及除錯工具,下面我們要正式講講SQLite的使用了,這一篇我們暫且不深入原始碼,之後我會單獨出一篇原始碼分析。在講如何使用SQLite資料庫之前,有必要介紹一下SQLite兩個重要的類:SQLiteDatabase 和 SQLiteOpenHelper,這是SQLite資料庫API中最基礎的兩個類。
3.1 SQLiteDatabase
在Android的自帶的SQLite庫中,SQLite所有的操作都來源於SQLiteDatabase ,另一個類SQLiteOpenHelper也是基於該類衍生而來進行資料庫的建立和版本管理的。我們簡單看看這個類的分析
可以發現insert、query等熟悉的資料庫操作的字眼,這些方法都是已經封裝好的,我們只需要傳入適當的引數即可完成諸如插入、更新、查詢等操作。當然SQLiteDatabase也提供了直接執行SQL語句的方法,如
- execSQL
例如db.execSQL("create table if not exists " + TABLE_NAME +"(id text primary key,name text)");
- rawQuery
用於執行select語句
例如db.rawQuery("SELECT * FROM test", null);
3.2 SQLiteOpenHelper
SQLiteOpenHelper是SQLiteDatabase的輔助類,通過對SQLiteDatabase內部方法的封裝簡化了資料庫建立與版本管理的操作。它是一個抽象類,一般情況下,我們需要繼承並重寫這兩個父類方法:
- onCreate
在初次生成資料庫時才會被呼叫,我們一般重寫onCreate生成資料庫表結構並新增一些應用使用到的初始化資料 - onUpgrade
當資料庫版本有更新時會呼叫這個方法,我們一般會在這執行資料庫更新的操作,例如欄位更新、表的增加與刪除等
此外父類方法中還有onConfigure、onDowngrade、onOpen,一般專案中很少用到它們,如果大家需要進一步瞭解可以去看看官方文件。
3.3 SQLiteOpenHelper與SQLiteDatabase的關係
介紹完了SQLiteOpenHelper以及SQLiteDatabase之後,那它們是怎麼關聯在一起的呢?我們從原始碼中來看看
SQLiteOpenHelper提供了兩個建立資料庫的方法
- getWritableDatabase
public SQLiteDatabase getWritableDatabase() {
synchronized (this) {
return getDatabaseLocked(true);
}
}
- getReadableDatabase
public SQLiteDatabase getReadableDatabase() {
synchronized (this) {
return getDatabaseLocked(false);
}
}
從原始碼中我們可以看到,這兩個方法有個共同點,也就是都呼叫了getDatabaseLocked這個方法,我們再看getDatabaseLocked這個方法
- 第一步
# 初始化SQLiteDatabase型別的mDatabase物件
if (mDatabase != null) {
if (!mDatabase.isOpen()) {
// Darn! The user closed the database by calling mDatabase.close().
mDatabase = null;
} else if (!writable || !mDatabase.isReadOnly()) {
// The database is already open for business.
return mDatabase;
}
}
if (mIsInitializing) {
throw new IllegalStateException("getDatabase called recursively");
}
- 第二步
mIsInitializing = true;
if (db != null) {
if (writable && db.isReadOnly()) {
db.reopenReadWrite();
}
} else if (mName == null) {
db = SQLiteDatabase.createInMemory(mOpenParamsBuilder.build());
} else {
# 獲取db檔案位置
final File filePath = mContext.getDatabasePath(mName);
# 獲取構建db的引數
SQLiteDatabase.OpenParams params = mOpenParamsBuilder.build();
try {
# 生成db物件
db = SQLiteDatabase.openDatabase(filePath, params);
// Keep pre-O-MR1 behavior by resetting file permissions to 660
# 設定檔案許可權,便於後續資料寫入
setFilePermissionsForDb(filePath.getPath());
} catch (SQLException ex) {
if (writable) {
throw ex;
}
Log.e(TAG, "Couldn't open " + mName
+ " for writing (will try read-only):", ex);
params = params.toBuilder().addOpenFlags(SQLiteDatabase.OPEN_READONLY).build();
db = SQLiteDatabase.openDatabase(filePath, params);
}
}
onConfigure(db);
- 第三步
# 獲取db版本
final int version = db.getVersion();
if (version != mNewVersion) {
if (db.isReadOnly()) {
throw new SQLiteException("Can't upgrade read-only database from version " +
db.getVersion() + " to " + mNewVersion + ": " + mName);
}
if (version > 0 && version < mMinimumSupportedVersion) {
File databaseFile = new File(db.getPath());
onBeforeDelete(db);
db.close();
if (SQLiteDatabase.deleteDatabase(databaseFile)) {
mIsInitializing = false;
return getDatabaseLocked(writable);
} else {
throw new IllegalStateException("Unable to delete obsolete database "
+ mName + " with version " + version);
}
} else {
# 開啟事務處理
db.beginTransaction();
try {
if (version == 0) {
# 呼叫重寫的onCreate方法
onCreate(db);
} else {
if (version > mNewVersion) {
onDowngrade(db, version, mNewVersion);
} else {
onUpgrade(db, version, mNewVersion);
}
}
db.setVersion(mNewVersion);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
}
onOpen(db);
if (db.isReadOnly()) {
Log.w(TAG, "Opened " + mName + " in read-only mode");
}
mDatabase = db;
return db;
根據上面的分析我們可以看到SQLiteOpenHelper是如何一步步通過呼叫SQLiteDatabase的方法來生成一個db例項的,也就表明了SQLiteOpenHelper是一個更高維度的SQLiteDatabase的封裝。
3.4 SQLiteDatabase物件生成流程
總結了SQLiteOpenHelper和SQLiteDatabase之間的關係後,我們捋一下SQLiteDatabase物件的生成流程,也可以當成我們之後在開發過程中使用的SQLite的一個模板,我們可以以下三種方式可以來生成SQLiteDatabase。
- 繼承SQLiteOpenHelper,呼叫getWritableDatabase / getReadableDatabase開啟或建立資料庫(推薦初學者使用)
- 呼叫SQLiteDatabase.openOrCreateDatabase開啟或建立資料庫
- 呼叫Context.openOrCreateDatabase開啟或建立資料庫
三種方法最終都是要呼叫SQLiteDatabase.openDatabase方法
既然都呼叫了SQLiteDatabase.openDatabase,那我們看看它的原始碼
public static SQLiteDatabase openDatabase(String path, CursorFactory factory, int flags,
DatabaseErrorHandler errorHandler) {
SQLiteDatabase db = new SQLiteDatabase(path, flags, factory, errorHandler);
db.open();//繼續執行開啟或建立資料庫的操作
return db;
}
解釋一下各個引數
- String path
資料庫檔案路徑 - CursorFactory factory
用於構造自定義的Cursor子類物件,在執行查詢操作時返回,若傳入 null 則使用預設的factory構造Cursor - int flags
用於控制資料庫的訪問模式,可傳入的引數有- CREATE_IF_NECESSARY:當資料庫不存在時建立該資料庫檔案
- ENABLE_WRITE_AHEAD_LOGGING:繞過資料庫的鎖機制,以多執行緒運算元據庫的方式進行讀寫
- NO_LOCALIZED_COLLATORS:開啟資料庫時,不根據本地化語言對資料庫進行排序
- OPEN_READONLY:以只讀方式開啟資料庫
- OPEN_READWRITE:以讀寫方式開啟資料庫
- DatabaseErrorHandler errorHandler
當檢測到資料庫損壞時進行回撥的介面,一般沒有特殊需要傳入 null 即可
可以看到,我們通過openDatabase生成SQLiteDatabase的例項,並且將db例項的狀態置為開啟,最終返回db例項。
3.5 建立資料庫的路徑
SQLiteDatabase原始碼中有一行程式碼是關於db檔案路徑相關的
final File filePath = mContext.getDatabasePath(mName);
這段程式碼我們得到的路徑是
/data/data/<package_name>/databases/
一般情況下我們在建立資料庫時path引數只需傳入“xxx.db”,系統自動會在該預設路徑下建立名為“xxx.db”的資料庫檔案,這樣做最大的好處就是安全,因為從Android7開始,Android的策略就限制了App彼此間的訪問許可權,這也使App的安全性得到了保證。
4 SQLite Demo開發
通過上面的關於SQLiteDatabase類的基本的瞭解,下面我們直接上手搞個Demo
4.1 建立MySQLiteOpenHelper
第一步我們使用最快捷的建立SQLiteDatabase的方式,也就是繼承SQLiteOpenHelper來建立一個我們自己MySQLiteOpenHelper,如下
public class MySQLiteOpenHelper extends SQLiteOpenHelper {
/**
* The constant FILE_DIR.
*/
public static final String FILE_DIR = Environment.getExternalStorageDirectory().getPath() + "/SQLiteTest/";
/**
* The constant DATABASE_VERSION.
*/
public static final int DATABASE_VERSION = 1;
/**
* The constant TABLE_NAME.
*/
public static final String TABLE_NAME = "test";
/**
* Instantiates a new My sq lite open helper.
*
* @param context the context
* @param name the name
*/
public MySQLiteOpenHelper(Context context, String name) {
this(context, name, null, DATABASE_VERSION);
}
/**
* Instantiates a new My sq lite open helper.
*
* @param context the context
* @param name the name
* @param factory the factory
* @param version the version
*/
public MySQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
this(context, name, factory, version, null);
}
/**
* Instantiates a new My sq lite open helper.
*
* @param context the context
* @param name the name
* @param factory the factory
* @param version the version
* @param errorHandler the error handler
*/
public MySQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version, DatabaseErrorHandler errorHandler) {
super(context, name, factory, version, errorHandler);
}
@Override
public void onCreate(SQLiteDatabase db) {
File dir = new File(FILE_DIR);
if (!dir.exists()) {
dir.mkdir();
}
# 重寫onCreate方法,在建立例項的時候新建一張表
try {
db.execSQL("create table if not exists " + TABLE_NAME +
"(id text primary key,name text)");
} catch (SQLException se) {
se.printStackTrace();
}
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (newVersion > oldVersion) {
String sql = "DROP TABLE IF EXISTS " + TABLE_NAME;
db.execSQL(sql);
onCreate(db);
}
}
}
4.2 建立SQLiteCattleActivity
建立好了MySQLiteOpenHelper之後,我們需要在一個新的Activity中來使用它
public class SQLiteCattleActivity extends AppCompatActivity {
private SQLiteDatabase database;
/**
* The constant DATABASE_NAME.
*/
@SuppressLint("SdCardPath")
public static final String DATABASE_NAME = "/data/data/com.lateautumn4lin.cattle/databases/test.db";
private static final int CODE_PERMISSION_REQUEST = 100;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_sqlite_cattle);
// 判斷是否具備儲存許可權以及重建db
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
//申請寫入許可權
ActivityCompat.requestPermissions(this, new String[]{
Manifest.permission.WRITE_EXTERNAL_STORAGE
}, CODE_PERMISSION_REQUEST);
Logger.logi("儲存申請許可權");
} else {
createDB();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
// requestCode即所宣告的許可權獲取碼,在checkSelfPermission時傳入
if (requestCode == CODE_PERMISSION_REQUEST) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
createDB();
}
}
}
/**
* Click back.
*
* @param view the view
*/
public void clickBack(View view){
Intent intent = new Intent(SQLiteCattleActivity.this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
startActivity(intent);
}
/**
* Click event.
*
* @param view the view
*/
public void clickEvent(View view) {
switch (view.getId()) {
case R.id.btn_insert:
if (database != null) {
ContentValues values = new ContentValues();
values.put("id", "1");
values.put("name", "name1");
database.insert("test", null, values);
database.execSQL("insert into test(id, name) values(2, 'name2')");
}
break;
case R.id.btn_delete:
if (database != null) {
//新增多條資料用來測試
database.execSQL("insert into test(id, name) values(3, 'name3')");
database.execSQL("insert into test(id, name) values(4, 'name4')");
database.execSQL("insert into test(id, name) values(5, 'name5')");
String whereClause = "id=?";
String[] whereArgs = {"3"};
database.delete("test", whereClause, whereArgs);
database.execSQL("delete from test where name = 'name4'");
}
break;
case R.id.btn_update:
if (database != null) {
ContentValues values = new ContentValues();
values.put("name", "update2");
String whereClause = "id=?";
String[] whereArgs = {"2"};
database.update("test", values, whereClause, whereArgs);
database.execSQL("update test set name = 'update5' where id = 5");
}
break;
case R.id.btn_query:
if(database!=null){
@SuppressLint("Recycle") Cursor cursor = database.rawQuery("SELECT * FROM test", null);
while (cursor.moveToNext()){
String id = cursor.getString(0);
String name=cursor.getString(1);
Logger.logi(String.format("Query Result: %s,%s",id,name));
}
}
break;
}
}
private void createDB() {
try {
Logger.logi(String.format("Create Normal Database %s",DATABASE_NAME));
// 兩種獲取database例項的不同方法
// 方法一
database = this.openOrCreateDatabase(DATABASE_NAME, MODE_PRIVATE, null);
// 方法二
MySQLiteOpenHelper sqLiteOpenHelper = new MySQLiteOpenHelper(this, DATABASE_NAME);
this.database = sqLiteOpenHelper.getWritableDatabase();
// 獲取database例項之後建立表
database.execSQL("create table if not exists " + "test" +
"(id text primary key,name text)");
} catch (Exception e) {
Logger.loge(e.toString());
}
}
}
這裡我們使用兩種方法來建立db例項,分別是SQLiteDatabase 自身的openOrCreateDatabase和我們建立的MySQLiteOpenHelper的getWritableDatabase來建立
4.3 連線SQLiteStudio實時除錯
如上面的兩個步驟,我們開發好了基本的App,實現了基本的點選按鈕完成“增刪改查的功能”,但畢竟資料庫是在手機裡面,我們要如何進行實時的資料庫除錯呢?下面介紹一種方法是基於SQLiteStudio進行的實時除錯方案
由於SQLite資料庫是以db檔案的形式儲存在手機目錄上的,我們無法直接便捷的實時獲取到SQLite的內容,因此我們需要藉助SQLiteStudio的端到端通訊功能來獲取SQLite的資料。
4.3.1 SQLiteStudio環境配置
第一步我們需要匯出SQLiteStudio 的Remote Jar包,匯出的步驟是Tools->Get Android Connect Jar File來獲取到Jar包,並把Jar包放在專案的根目錄/libs下面。
我們需要開啟SQLiteStudio 的SQLite資料庫除錯的許可權,步驟是通過Tools->Open configuration dialog開啟配置介面,在外掛欄勾選SQLite選項。
4.3.2 Android專案環境配置
配置好了SQLiteStudio之後,下面我們來配置我們的App專案,我們之前已經引入了Remote Jar包,放在我們的根目錄/libs下面,下面我們還需要專案的build.gradle檔案中在寫入implementation fileTree(include: ['*.jar'], dir: 'libs')
來保證libs下所有的Jar包能夠正常的匯入。
匯入Jar包之後我們只需要在我們的Activity中加上一行程式碼SQLiteStudioService.instance().start(this);
即可在我們的Activity一開啟的時候就啟動SQLiteStudioService的例項,監聽Android的xxx埠,等待遠端的SQLiteStudio來連線。
4.3.3 SQLiteStudio連線手機實時除錯
配置好App之後,只需要啟動App,便可以在目錄下建立好db檔案,然後我們就可以在SQLiteStudio介面進行遠端連線了。
新增的步驟和之前類似,不過資料型別方面我們選擇Android SQLite,之後在選擇db檔案上,我們選擇port forwarding,連線到某臺手機的12121埠(也就是之前說的)SQLiteStudioService的例項監聽的埠,之後就可以愉快的除錯了。
5 食用薦語
以上就是關於Android端SQlite的淺嘗輒止的獨家食用指南了,由於篇幅問題,只是簡單的寫了幾個方面,不過相信大家也能懂得SQlite開發的基本流程了。當然,這只是SQlite這部分的第一篇文章,之後還有兩篇文章會關注另外兩個方面,包括SQlite的安全版本以及SQlite的實現原始碼分析。
相關文章
- 淺嘗輒止,React是如何工作的React
- 騰訊入場鏈遊淺嘗輒止,區塊鏈遊戲難點在哪兒?區塊鏈遊戲
- GameFramework食用指南GAMFramework
- Android原生整合Flutter的淺嘗AndroidFlutter
- [日常填坑系列]CAP食用指南-版本引用問題
- SQLite 命令列客戶端 sqlite3 使用指南SQLite命令列客戶端
- fish_redux 「食用指南」Redux
- 【 FlutterUnit 食用指南】 開源篇Flutter
- OpenAPI 3.0 規範-食用指南API
- 淺嘗UIAppearance的使用UIAPP
- Angular 從入坑到挖坑 - 元件食用指南Angular元件
- android SQLite的使用AndroidSQLite
- android:SQliteAndroidSQLite
- 覆蓋率檢查工具:JaCoCo 食用指南
- 擁抱 OpenAPI 3:springdoc-openapi 食用指南APISpring
- 淺嘗flutter中的http請求FlutterHTTP
- 淺嘗flutter中的flex佈局FlutterFlex
- Scrcpy投屏原理淺析-嘗試用Flutter重寫它的客戶端Flutter客戶端
- svg 線條動畫淺嘗SVG動畫
- Android中SQLiteAndroidSQLite
- EntityFramework系列:SQLite的CodeFrist和RowVersionFrameworkSQLite
- 淺嘗Node檔案系統
- 淺嘗Vue.js元件(二)Vue.js元件
- Go語言淺嘗之方法Go
- Ruby 札記 - 淺嘗 Ruby 特性
- Flutter Android 端啟動流程淺析FlutterAndroid
- 終止Android中HandlerThread的方法Androidthread
- Android的SQLite----重新認識Android(10)AndroidSQLite
- 嘗試用自己的話淺談 Nginx 反向代理Nginx
- 墨者安全淺談音樂版權的獨家與共享
- Flutter + Rust 高效能的跨端嘗試FlutterRust跨端
- 未來的元件化標準 —— 淺嘗Web Components元件化Web
- Android SQLite學習筆記AndroidSQLite筆記
- Android 中 SQLite 效能優化AndroidSQLite優化
- Android SQLite快速入門教程AndroidSQLite
- Python面試通關指南及獨家自學祕籍Python面試
- 《視不可當》獨家樣章——傳播資訊圖指南
- 在Android Studio中檢視Sqlite的方法AndroidSQLite