獨家食用指南系列|Android端SQLite的淺嘗輒止

技術拆解官發表於2020-10-21

在這裡插入圖片描述

在這裡插入圖片描述
大家好,本週技術拆解官的第一篇文章,給大家帶來的是我們的新主題《獨家食用指南系列》。作為新主題的第一篇系列文章,這次給大家分享下AndroidSQLite的“食用指南”。之所以會拿SQLite作為系列的開篇文章,也是因為最近接觸到逆向AndroidSQLite資料庫的工作,查閱了很多資料,我也想給這次調研做下總結,所以接下來我會分三篇文章來給大家好好講講這次逆向AndroidSQLite資料庫的經歷,首當其衝的第一篇,也就是《獨家食用指南系列|AndroidSQLite的淺嘗輒止》

本篇文章使用到的專案原始碼都在我的個人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 目前支援大部分作業系統,不至電腦作業系統更在眾多的手機系統也是能夠執行,比如:AndroidIOS
  • 多語言介面
    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了。

  最新版本的SQLiteStudioSQLiteStudio3.2,地址是“https://sqlitestudio.pl/”,大家可以去官網下載,這也是個開源專案,要是大家有問題也可以去Github打擾那位大佬。

  整體介面是這樣的
在這裡插入圖片描述
  照例演示下新增新資料庫的流程

  Database->Add a Database->開啟自己選定好的db檔案即可。

在這裡插入圖片描述

3 SQLite 基礎類介紹

  上面我們講了SQLite的定義以及除錯工具,下面我們要正式講講SQLite的使用了,這一篇我們暫且不深入原始碼,之後我會單獨出一篇原始碼分析。在講如何使用SQLite資料庫之前,有必要介紹一下SQLite兩個重要的類:SQLiteDatabaseSQLiteOpenHelper,這是SQLite資料庫API中最基礎的兩個類。
在這裡插入圖片描述

3.1 SQLiteDatabase

  在Android的自帶的SQLite庫中,SQLite所有的操作都來源於SQLiteDatabase ,另一個類SQLiteOpenHelper也是基於該類衍生而來進行資料庫的建立和版本管理的。我們簡單看看這個類的分析

在這裡插入圖片描述
在這裡插入圖片描述

  可以發現insertquery等熟悉的資料庫操作的字眼,這些方法都是已經封裝好的,我們只需要傳入適當的引數即可完成諸如插入、更新、查詢等操作。當然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

  SQLiteOpenHelperSQLiteDatabase的輔助類,通過對SQLiteDatabase內部方法的封裝簡化了資料庫建立與版本管理的操作。它是一個抽象類,一般情況下,我們需要繼承並重寫這兩個父類方法:

  • onCreate
    在初次生成資料庫時才會被呼叫,我們一般重寫onCreate生成資料庫表結構並新增一些應用使用到的初始化資料
  • onUpgrade
    當資料庫版本有更新時會呼叫這個方法,我們一般會在這執行資料庫更新的操作,例如欄位更新、表的增加與刪除等

  此外父類方法中還有onConfigureonDowngradeonOpen,一般專案中很少用到它們,如果大家需要進一步瞭解可以去看看官方文件。

在這裡插入圖片描述

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物件生成流程

  總結了SQLiteOpenHelperSQLiteDatabase之間的關係後,我們捋一下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和我們建立的MySQLiteOpenHelpergetWritableDatabase來建立

在這裡插入圖片描述

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的例項,監聽Androidxxx埠,等待遠端的SQLiteStudio來連線。

4.3.3 SQLiteStudio連線手機實時除錯

  配置好App之後,只需要啟動App,便可以在目錄下建立好db檔案,然後我們就可以在SQLiteStudio介面進行遠端連線了。
在這裡插入圖片描述
在這裡插入圖片描述

  新增的步驟和之前類似,不過資料型別方面我們選擇Android SQLite,之後在選擇db檔案上,我們選擇port forwarding,連線到某臺手機的12121埠(也就是之前說的)SQLiteStudioService的例項監聽的埠,之後就可以愉快的除錯了。

在這裡插入圖片描述

5 食用薦語

  以上就是關於AndroidSQlite的淺嘗輒止的獨家食用指南了,由於篇幅問題,只是簡單的寫了幾個方面,不過相信大家也能懂得SQlite開發的基本流程了。當然,這只是SQlite這部分的第一篇文章,之後還有兩篇文章會關注另外兩個方面,包括SQlite的安全版本以及SQlite的實現原始碼分析。

在這裡插入圖片描述
在這裡插入圖片描述

相關文章