學(xué)習(xí)資料
數(shù)據(jù)庫(kù)软驰,重災(zāi)區(qū)之一鳍烁,除了Java
基礎(chǔ)外也是以后的重點(diǎn)加強(qiáng)學(xué)習(xí)模塊蹲缠。4月入職以來(lái),終于空閑兩天症概,就學(xué)習(xí)了解下
項(xiàng)目中要求對(duì)Sqlite
數(shù)據(jù)庫(kù)進(jìn)行加密,百度之后早芭,知道了SQLCipher
這個(gè)東西彼城。過(guò)了沒(méi)幾天,看到微信團(tuán)隊(duì)開(kāi)源的WCDB
退个,簡(jiǎn)單看了簡(jiǎn)介后募壕,了解到支持加密,就打算學(xué)習(xí)下怎么使用的语盈,以后再遇到需要對(duì)Sqlite
加密的需求舱馅,就考慮使用
感覺(jué)WCDB
可以看作是SQLCipher
的一個(gè)加強(qiáng)升級(jí)版本,除了加密外刀荒,還有一個(gè)牛B
的地方是支持?jǐn)?shù)據(jù)庫(kù)修復(fù)代嗤,其他的可以去wiki
看看
本篇是記錄學(xué)習(xí)接入流程棘钞,以及簡(jiǎn)單地由不加密的數(shù)據(jù)庫(kù)遷移到加密的數(shù)據(jù)
1. 接入
接入使用很簡(jiǎn)單很方便
我用的版本是1.0.2
dependencies {
...
compile 'com.tencent.wcdb:wcdb-android:1.0.2'
}
選擇接入的CPU
架構(gòu),WCDB
包含 armeabi, armeabi-v7a, arm64-v8a, x86
四種架構(gòu)的動(dòng)態(tài)庫(kù)干毅,具體的就想用哪個(gè)用哪個(gè)了
關(guān)于.so
文件兼容可以看看Android SO文件的兼容和適配
android {
defaultConfig {
...
ndk {
// 接入 armeabi ,armeabi-v7a ,x86
abiFilters 'armeabi', 'armeabi-v7a','x86'
}
}
}
日常使用的手機(jī)沒(méi)root
,為了看到.db
文件宜猜,就使用了Android Studio
自帶的模擬器,也就引入了x86
1.1 WCDB DBHelper
WCDB
的類名方法名硝逢,基本和Android
原生提供的一樣姨拥,可以按照以前的使用習(xí)慣來(lái)來(lái)使用
注意導(dǎo)入包時(shí),要導(dǎo)入WCDB的包
1.1.1 PlainDBHelper
直接繼承WCDB
包下的SQLiteOpenHelper
import com.tencent.wcdb.database.SQLiteDatabase;
import com.tencent.wcdb.database.SQLiteOpenHelper;
import java.io.File;
/**
* 簡(jiǎn)單的 SQLite Helper
*/
public class PlainDBHelper extends SQLiteOpenHelper {
// 數(shù)據(jù)庫(kù) db 文件名稱
private static final String DEFAULT_NAME = "plain.db";
// 默認(rèn)版本號(hào)
private static final int DEFAULT_VERSION = 1;
private Context mContext;
/**
* 通過(guò)父類構(gòu)造方法創(chuàng)建 plain 數(shù)據(jù)庫(kù)
*/
public PlainDBHelper(Context context) {
super(context, DEFAULT_NAME, null, DEFAULT_VERSION, null);
this.mContext = context;
}
/**
* 表創(chuàng)建
*/
@Override
public void onCreate(SQLiteDatabase db) {
final String SQL_CREATE = "CREATE TABLE IF NOT EXISTS person (_id INTEGER PRIMARY KEY AUTOINCREMENT , name VARCHAR(20) , address TEXT)";
db.execSQL(SQL_CREATE);
}
/**
* 版本升級(jí)
*/
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// TODO
}
/**
* 刪除數(shù)據(jù)庫(kù) db 文件
*/
public boolean onDelete() {
File file = mContext.getDatabasePath(DEFAULT_NAME);
return SQLiteDatabase.deleteDatabase(file);
}
}
重點(diǎn)在于構(gòu)造方法中渠鸽,super()
方法叫乌。若一開(kāi)始就想創(chuàng)建一個(gè)加密的數(shù)據(jù),選擇對(duì)應(yīng)的super()
方法徽缚,傳入一個(gè)密碼就可以
之后在onCreate()
中憨奸,執(zhí)行創(chuàng)建person
表語(yǔ)句
1.1.2 PlainDBManager
簡(jiǎn)單的數(shù)據(jù)操作管理類,可以將增刪改查
的一些操作統(tǒng)一放在這個(gè)類里
public class PlainDBManager {
private PlainDBHelper mDBHelper;
private SQLiteDatabase mDB;
public PlainDBManager(Context context) {
mDBHelper = new PlainDBHelper(context);
mDB = mDBHelper.getWritableDatabase();
}
public void addPersonData(PlainPerson person) {
try {
// 開(kāi)啟事務(wù)
mDB.beginTransaction();
// 執(zhí)行插入語(yǔ)句
final String sql = "INSERT INTO person VALUES(NULL,?,?)";
Object[] objects = new Object[]{person.getName(), person.getAddress()};
mDB.execSQL(sql, objects);
// 設(shè)置事務(wù)完成成功
mDB.setTransactionSuccessful();
} finally {
// 關(guān)閉事務(wù)
mDB.endTransaction();
}
}
public boolean addPersonList(List<PlainPerson> list) {
try {
// 開(kāi)啟事務(wù)
mDB.beginTransaction();
// 執(zhí)行插入語(yǔ)句
for (PlainPerson person : list) {
Object[] objects = new Object[]{person.getName(), person.getAddress()};
final String sql = "INSERT INTO person VALUES(NULL,?,?)";
mDB.execSQL(sql, objects);
}
// 設(shè)置事務(wù)完成成功
mDB.setTransactionSuccessful();
} catch (Exception e) {
return false;
} finally {
// 關(guān)閉事務(wù)
mDB.endTransaction();
}
return true;
}
/**
* 拿到數(shù)據(jù)庫(kù)中所有的Person并放入集合中
*/
public List<PlainPerson> getPersonListData() {
List<PlainPerson> listData = new ArrayList<>();
Cursor c = getAllPersonInfo();
while (c.moveToNext()) {
PlainPerson person = new PlainPerson();
person.setName(c.getString(c.getColumnIndex("name")));
person.setAddress(c.getString(c.getColumnIndex("address")));
listData.add(person);
}
c.close();
return listData;
}
private Cursor getAllPersonInfo() {
return mDB.rawQuery("SELECT * FROM person", null);
}
/**
* 關(guān)閉 database猎拨;
*/
public void closeDB() {
mDB.close();
}
/**
* 刪除數(shù)據(jù)庫(kù)
*/
public Boolean deleteDatabase() {
return mDBHelper.onDelete();
}
}
關(guān)于插入
和查詢
的語(yǔ)句如何進(jìn)行優(yōu)化膀藐,希望知道的同學(xué)可以留言告訴一下
1.2 Activity中使用
創(chuàng)建表時(shí)屠阻,隨意創(chuàng)建了一個(gè)person
表红省,字段就是name,address
Activity代碼
/**
* 原始:未加密的數(shù)據(jù)庫(kù)
*/
public class PlainDBActivity extends AppCompatActivity {
private final String TAG = PlainDBActivity.class.getSimpleName();
// 數(shù)據(jù)庫(kù)操作管理類
private PlainDBManager mDBManager;
// 適配器
private RecyclerAdapter mAdapter;
// 顯示數(shù)據(jù)按鈕
private Button mBtShow;
// 插入按鈕
private Button mBtInsert;
// 刪除按鈕
private Button mBtDelete;
// 是否進(jìn)行了刪除操作
private Boolean isHasDeleted = false;
private ProgressDialog mDialog;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_plain_db);
initDB();
initView();
}
private void initDB() {
mDBManager = new PlainDBManager(PlainDBActivity.this);
}
private void initView() {
// RecyclerView
RecyclerView rv = (RecyclerView) findViewById(R.id.activity_plain_rv);
rv.addItemDecoration(new DividerItemDecoration(PlainDBActivity.this, DividerItemDecoration.VERTICAL));
LinearLayoutManager manager = new LinearLayoutManager(PlainDBActivity.this);
rv.setLayoutManager(manager);
mAdapter = new RecyclerAdapter(rv, R.layout.item_layout);
rv.setAdapter(mAdapter);
// 插入按鈕
mBtInsert = (Button) findViewById(R.id.activity_plain_bt_insert);
// 顯示按鈕
mBtShow = (Button) findViewById(R.id.activity_plain_bt_show);
// 刪除數(shù)據(jù)庫(kù)
mBtDelete = (Button) findViewById(R.id.activity_plain_bt_delete);
setOnClick();
}
private void setOnClick() {
// 插入按鈕:5秒內(nèi),防止重復(fù)點(diǎn)擊
RxView
.clicks(mBtInsert)
.throttleFirst(5, TimeUnit.SECONDS)
.subscribe(new Consumer<Object>() {
@Override
public void accept(@NonNull Object o) throws Exception {
if (isHasDeleted) {
initDB();
}
addDataIntoSql();
}
});
// 顯示按鈕:1秒內(nèi),防止重復(fù)點(diǎn)擊
RxView
.clicks(mBtShow)
.throttleFirst(1, TimeUnit.SECONDS)
.subscribe(new Consumer<Object>() {
@Override
public void accept(@NonNull Object o) throws Exception {
if (isHasDeleted) {
initDB();
}
selectDataFromSql();
}
});
// 刪除按鈕:3秒內(nèi)国觉,防止重復(fù)點(diǎn)擊
RxView
.clicks(mBtDelete)
.throttleFirst(3, TimeUnit.SECONDS)
.subscribe(new Consumer<Object>() {
@Override
public void accept(@NonNull Object o) throws Exception {
if (isHasDeleted) {
toast("數(shù)據(jù)庫(kù)不存在");
return;
}
if (mDBManager.deleteDatabase()) {
isHasDeleted = true;
mDBManager.closeDB();
toast("刪除成功");
}
}
});
}
/**
* 查詢數(shù)據(jù)吧恃,并顯示
*/
private void selectDataFromSql() {
Observable
.just(0)
.subscribeOn(Schedulers.io())
.doOnSubscribe(new Consumer<Disposable>() {
@Override
public void accept(@NonNull Disposable disposable) throws Exception {
showProgressDialog("正在查詢數(shù)據(jù)...");
}
})
.subscribeOn(AndroidSchedulers.mainThread())
.map(new Function<Integer, List<PlainPerson>>() {
@Override
public List<PlainPerson> apply(@NonNull Integer integer) throws Exception {
return mDBManager.getPersonListData();
}
})
.filter(new Predicate<List<PlainPerson>>() {
@Override
public boolean test(@NonNull List<PlainPerson> plainList) throws Exception {
return plainList.size() > 0;
}
})
.observeOn(AndroidSchedulers.mainThread())
.doFinally(new Action() {
@Override
public void run() throws Exception {
closeProgressDialog();
}
})
.subscribe(new Consumer<List<PlainPerson>>() {
@Override
public void accept(@NonNull List<PlainPerson> plainList) throws Exception {
mAdapter.setData(plainList);
}
});
}
/**
* 存入數(shù)據(jù)
*/
private void addDataIntoSql() {
Observable
.just(10000)
.subscribeOn(Schedulers.io())
.doOnSubscribe(new Consumer<Disposable>() {
@Override
public void accept(@NonNull Disposable disposable) throws Exception {
mBtShow.setEnabled(false);
showProgressDialog("正在插入數(shù)據(jù)...");
}
})
.subscribeOn(AndroidSchedulers.mainThread())
.map(new Function<Integer, List<PlainPerson>>() {
@Override
public List<PlainPerson> apply(@NonNull Integer integer) throws Exception {
List<PlainPerson> list = new ArrayList<>();
for (int i = 0; i < integer; i++) {
PlainPerson person = new PlainPerson();
person.setName("隔壁老王" + i);
person.setAddress("天使大街 " + i + " 號(hào)");
list.add(person);
}
return list;
}
})
.map(new Function<List<PlainPerson>, Boolean>() {
@Override
public Boolean apply(@NonNull List<PlainPerson> plainList) throws Exception {
return mDBManager.addPersonList(plainList);
}
})
.observeOn(AndroidSchedulers.mainThread())
.doFinally(new Action() {
@Override
public void run() throws Exception {
closeProgressDialog();
}
})
.subscribe(new Consumer<Boolean>() {
@Override
public void accept(@NonNull Boolean aBoolean) throws Exception {
if (aBoolean) {
mBtShow.setEnabled(true);
toast("插入成功");
}
}
});
}
/**
* 關(guān)閉 ProgressDialog
*/
private void closeProgressDialog() {
if (null != mDialog && mDialog.isShowing()) {
mDialog.dismiss();
}
}
/**
* 顯示 ProgressDialog
*/
private void showProgressDialog(String info) {
final String TITLE = "提示";
mDialog = ProgressDialog.show(PlainDBActivity.this, TITLE, info);
mDialog.show();
}
private void toast(String info) {
Toast.makeText(PlainDBActivity.this, info, Toast.LENGTH_SHORT).show();
}
@Override
protected void onDestroy() {
super.onDestroy();
mDBManager.closeDB();
}
}
按鈕的點(diǎn)擊,試著用了下RxBinding
麻诀,插入數(shù)據(jù)時(shí)痕寓,插入了10000
條數(shù)據(jù)
錯(cuò)誤:下面這段話說(shuō)明的問(wèn)題之前搞錯(cuò)了
嘗試了下插入100w
條也可以,速度還可以接受蝇闭。但當(dāng)試著插入1億
條字符串時(shí)呻率,就報(bào)了OOM
,不知道如何解決呻引。PlainDBHelper
中插入數(shù)據(jù)的方法需要進(jìn)行優(yōu)化
1.3 補(bǔ)充:之前的錯(cuò)誤說(shuō)明
2017年7月6號(hào) 20:17
今天看到一篇博客 一個(gè)Java對(duì)象到底占用多大內(nèi)存礼仗?,突然想到逻悠,上面說(shuō)的OOM
問(wèn)題元践,可能就不是數(shù)據(jù)庫(kù)插入操作導(dǎo)致的,問(wèn)題出在:
.map(new Function<Integer, List<PlainPerson>>() {
@Override
public List<PlainPerson> apply(@NonNull Integer integer) throws Exception {
List<PlainPerson> list = new ArrayList<>();
for (int i = 0; i < integer; i++) {
PlainPerson person = new PlainPerson();
person.setName("隔壁老王" + i);
person.setAddress("天使大街 " + i + " 號(hào)");
list.add(person);
}
return list;
}
})
這里童谒,創(chuàng)建了大量對(duì)象单旁,大量的對(duì)象占用過(guò)多的內(nèi)存,導(dǎo)致的OOM
饥伊,代碼根本就沒(méi)有走到數(shù)據(jù)庫(kù)插入操作就已經(jīng)發(fā)生了OOM
為了驗(yàn)證象浑,我將代碼做了修改蔫饰,just(10000000)
,創(chuàng)建1000w
個(gè)對(duì)象愉豺,并把數(shù)據(jù)庫(kù)插入操作注釋掉死嗦,沒(méi)有進(jìn)行數(shù)據(jù)庫(kù)任何操作,依然OOM
,也驗(yàn)證了我的想法粒氧,是創(chuàng)建對(duì)象過(guò)多導(dǎo)致的OOM
而無(wú)關(guān)數(shù)據(jù)庫(kù)操作
至于數(shù)據(jù)庫(kù) 大量操作會(huì)不會(huì)造成OOM
越除,暫時(shí)不知道如何驗(yàn)證
2. 遷移
由非加密的數(shù)據(jù)庫(kù)遷移到加密的數(shù)據(jù)庫(kù),實(shí)際開(kāi)發(fā)時(shí)外盯,一定要先做好備份
2.1 EncryptDBHelper
/**
* 將不加密的 plain.db 遷移到加密的 encrypt.db
*/
public class EncryptDBHelper extends SQLiteOpenHelper {
private final String TAG = EncryptDBHelper.class.getSimpleName();
private final static String ENCRYPT_NAME = "encrypt.db";
private final static String PLAIN_NAME = "plain.db";
private final static int VERSION = 2;
private Context mContext;
public EncryptDBHelper(Context context, String password) {
super(context, ENCRYPT_NAME, password.getBytes(), null, VERSION, null);
mContext = context;
}
@Override
public void onCreate(SQLiteDatabase db) {
File plainFile = mContext.getDatabasePath(PLAIN_NAME);
// 判斷舊的數(shù)據(jù)庫(kù)文件是否存在
if (plainFile.exists()) {
move(plainFile, db);
} else {
final String SQL_CREATE = "CREATE TABLE IF NOT EXISTS person (_id INTEGER PRIMARY KEY AUTOINCREMENT , name VARCHAR(20) , address TEXT)";
db.execSQL(SQL_CREATE);
}
}
/**
* 遷移數(shù)據(jù)庫(kù)
*/
private void move(File file, SQLiteDatabase db) {
db.endTransaction();
String sql = String.format("ATTACH DATABASE %s AS old KEY '';",
DatabaseUtils.sqlEscapeString(file.getPath()));
db.execSQL(sql);
db.beginTransaction();
DatabaseUtils.stringForQuery(db, "SELECT sqlcipher_export('main', 'old');", null);
db.setTransactionSuccessful();
db.endTransaction();
int oldVersion = (int) DatabaseUtils.longForQuery(db, "PRAGMA old.user_version;", null);
db.execSQL("DETACH DATABASE old;");
if (file.delete()) {
Log.e(TAG, "舊數(shù)據(jù)庫(kù)文件刪除成功");
}
db.beginTransaction();
// 是否要更新 schema
if (oldVersion > VERSION) {
onDowngrade(db, oldVersion, VERSION);
} else if (oldVersion < VERSION) {
onUpgrade(db, oldVersion, VERSION);
}
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.e(TAG, "----" + oldVersion + "===> " + newVersion);
}
/**
* 刪除數(shù)據(jù)庫(kù) db 文件
*/
public boolean onDelete() {
File file = mContext.getDatabasePath(ENCRYPT_NAME);
return SQLiteDatabase.deleteDatabase(file);
}
}
構(gòu)造方法中摘盆,傳入了一個(gè)字符串密碼
,加密就是這里簡(jiǎn)單饱苟。孩擂。。
實(shí)際開(kāi)發(fā)時(shí)箱熬,傳入的密碼
字符串需要使用一些算法來(lái)生成类垦,而不是直接在代碼中寫(xiě)一個(gè)看一眼就知道的密碼,不然加密也沒(méi)啥意義
move()
方法中城须,是固定的套路蚤认,但具體的語(yǔ)句,查了下糕伐,具體啥意思就先放棄了
2.2 EncryptDBManager
這里主要就是對(duì) EncryptDBHelper
的初始化
/**
* 遷移數(shù)據(jù)庫(kù)管理
*/
public class EncryptDBManager {
private Context mContext;
private String mPassword;
private SQLiteDatabase mDB;
private EncryptDBHelper mDBHelper;
public EncryptDBManager(Context mContext, String mPassword) {
this.mContext = mContext;
this.mPassword = mPassword;
}
/**
* 初始化 EncryptDBHelper
* 內(nèi)部實(shí)現(xiàn)了數(shù)據(jù)庫(kù)的遷移
*/
public boolean init() {
try {
mDBHelper = new EncryptDBHelper(mContext, mPassword);
mDB = mDBHelper.getWritableDatabase();
return true;
} catch (Exception e) {
return false;
}
}
/**
* 拿到數(shù)據(jù)庫(kù)中所有的Person并放入集合中
*/
public List<PlainPerson> getPersonListData() {
List<PlainPerson> listData = new ArrayList<>();
Cursor c = getAllPersonInfo();
while (c.moveToNext()) {
PlainPerson person = new PlainPerson();
person.setName(c.getString(c.getColumnIndex("name")));
person.setAddress(c.getString(c.getColumnIndex("address")));
listData.add(person);
}
c.close();
return listData;
}
private Cursor getAllPersonInfo() {
return mDB.rawQuery("SELECT * FROM person", null);
}
/**
* 關(guān)閉 database砰琢;
*/
public void closeDB() {
mDB.close();
}
/**
* 刪除數(shù)據(jù)庫(kù)
*/
public Boolean deleteDatabase() {
return mDBHelper.onDelete();
}
}
在Activity
中直接調(diào)用方法就成
使用db
工具查看未加密和加密后的數(shù)據(jù)庫(kù)文件
3. 最后
WCDB
中還有很多東西不了解,日后慢慢接觸
關(guān)于遺留的那個(gè)
插入
操作優(yōu)化良瞧,有知道的同學(xué)陪汽,請(qǐng)留言說(shuō)一下 </p>補(bǔ)充:這里搞錯(cuò)了,已改正褥蚯,原因說(shuō)明在上面
有錯(cuò)誤挚冤,請(qǐng)指出
共勉 : )