学习资料
数据库,重灾区之一,除了Java
基础外也是以后的重点加强学习模块。4月入职以来,终于空闲两天,就学习了解下
项目中要求对Sqlite
数据库进行加密,百度之后,知道了SQLCipher
这个东西。过了没几天,看到微信团队开源的WCDB
,简单看了简介后,了解到支持加密,就打算学习下怎么使用的,以后再遇到需要对Sqlite
加密的需求,就考虑使用
感觉WCDB
可以看作是SQLCipher
的一个加强升级版本,除了加密外,还有一个牛B
的地方是支持数据库修复,其他的可以去wiki
看看
本篇是记录学习接入流程,以及简单地由不加密的数据库迁移到加密的数据
1. 接入
接入使用很简单很方便
我用的版本是1.0.2
dependencies {
...
compile 'com.tencent.wcdb:wcdb-android:1.0.2'
}
选择接入的CPU
架构,WCDB
包含 armeabi, armeabi-v7a, arm64-v8a, x86
四种架构的动态库,具体的就想用哪个用哪个了
关于.so
文件兼容可以看看Android SO文件的兼容和适配
android {
defaultConfig {
...
ndk {
// 接入 armeabi ,armeabi-v7a ,x86
abiFilters 'armeabi', 'armeabi-v7a','x86'
}
}
}
日常使用的手机没root
,为了看到.db
文件,就使用了Android Studio
自带的模拟器,也就引入了x86
1.1 WCDB DBHelper
WCDB
的类名方法名,基本和Android
原生提供的一样,可以按照以前的使用习惯来来使用
注意导入包时,要导入WCDB的包
1.1.1 PlainDBHelper
直接继承WCDB
包下的SQLiteOpenHelper
import com.tencent.wcdb.database.SQLiteDatabase;
import com.tencent.wcdb.database.SQLiteOpenHelper;
import java.io.File;
/**
* 简单的 SQLite Helper
*/
public class PlainDBHelper extends SQLiteOpenHelper {
// 数据库 db 文件名称
private static final String DEFAULT_NAME = "plain.db";
// 默认版本号
private static final int DEFAULT_VERSION = 1;
private Context mContext;
/**
* 通过父类构造方法创建 plain 数据库
*/
public PlainDBHelper(Context context) {
super(context, DEFAULT_NAME, null, DEFAULT_VERSION, null);
this.mContext = context;
}
/**
* 表创建
*/
@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);
}
/**
* 版本升级
*/
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// TODO
}
/**
* 删除数据库 db 文件
*/
public boolean onDelete() {
File file = mContext.getDatabasePath(DEFAULT_NAME);
return SQLiteDatabase.deleteDatabase(file);
}
}
重点在于构造方法中,super()
方法。若一开始就想创建一个加密的数据,选择对应的super()
方法,传入一个密码就可以
之后在onCreate()
中,执行创建person
表语句
1.1.2 PlainDBManager
简单的数据操作管理类,可以将增删改查
的一些操作统一放在这个类里
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 {
// 开启事务
mDB.beginTransaction();
// 执行插入语句
final String sql = "INSERT INTO person VALUES(NULL,?,?)";
Object[] objects = new Object[]{person.getName(), person.getAddress()};
mDB.execSQL(sql, objects);
// 设置事务完成成功
mDB.setTransactionSuccessful();
} finally {
// 关闭事务
mDB.endTransaction();
}
}
public boolean addPersonList(List<PlainPerson> list) {
try {
// 开启事务
mDB.beginTransaction();
// 执行插入语句
for (PlainPerson person : list) {
Object[] objects = new Object[]{person.getName(), person.getAddress()};
final String sql = "INSERT INTO person VALUES(NULL,?,?)";
mDB.execSQL(sql, objects);
}
// 设置事务完成成功
mDB.setTransactionSuccessful();
} catch (Exception e) {
return false;
} finally {
// 关闭事务
mDB.endTransaction();
}
return true;
}
/**
* 拿到数据库中所有的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);
}
/**
* 关闭 database;
*/
public void closeDB() {
mDB.close();
}
/**
* 删除数据库
*/
public Boolean deleteDatabase() {
return mDBHelper.onDelete();
}
}
关于插入
和查询
的语句如何进行优化,希望知道的同学可以留言告诉一下
1.2 Activity中使用
创建表时,随意创建了一个person
表,字段就是name,address
Activity代码
/**
* 原始:未加密的数据库
*/
public class PlainDBActivity extends AppCompatActivity {
private final String TAG = PlainDBActivity.class.getSimpleName();
// 数据库操作管理类
private PlainDBManager mDBManager;
// 适配器
private RecyclerAdapter mAdapter;
// 显示数据按钮
private Button mBtShow;
// 插入按钮
private Button mBtInsert;
// 删除按钮
private Button mBtDelete;
// 是否进行了删除操作
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);
// 删除数据库
mBtDelete = (Button) findViewById(R.id.activity_plain_bt_delete);
setOnClick();
}
private void setOnClick() {
// 插入按钮:5秒内,防止重复点击
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秒内,防止重复点击
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秒内,防止重复点击
RxView
.clicks(mBtDelete)
.throttleFirst(3, TimeUnit.SECONDS)
.subscribe(new Consumer<Object>() {
@Override
public void accept(@NonNull Object o) throws Exception {
if (isHasDeleted) {
toast("数据库不存在");
return;
}
if (mDBManager.deleteDatabase()) {
isHasDeleted = true;
mDBManager.closeDB();
toast("删除成功");
}
}
});
}
/**
* 查询数据,并显示
*/
private void selectDataFromSql() {
Observable
.just(0)
.subscribeOn(Schedulers.io())
.doOnSubscribe(new Consumer<Disposable>() {
@Override
public void accept(@NonNull Disposable disposable) throws Exception {
showProgressDialog("正在查询数据...");
}
})
.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);
}
});
}
/**
* 存入数据
*/
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("正在插入数据...");
}
})
.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 + " 号");
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("插入成功");
}
}
});
}
/**
* 关闭 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();
}
}
按钮的点击,试着用了下RxBinding
,插入数据时,插入了10000
条数据
错误:下面这段话说明的问题之前搞错了
尝试了下插入100w
条也可以,速度还可以接受。但当试着插入1亿
条字符串时,就报了OOM
,不知道如何解决。PlainDBHelper
中插入数据的方法需要进行优化
1.3 补充:之前的错误说明
2017年7月6号 20:17
今天看到一篇博客 一个Java对象到底占用多大内存?,突然想到,上面说的OOM
问题,可能就不是数据库插入操作导致的,问题出在:
.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 + " 号");
list.add(person);
}
return list;
}
})
这里,创建了大量对象,大量的对象占用过多的内存,导致的OOM
,代码根本就没有走到数据库插入操作就已经发生了OOM
为了验证,我将代码做了修改,just(10000000)
,创建1000w
个对象,并把数据库插入操作注释掉,没有进行数据库任何操作,依然OOM
,也验证了我的想法,是创建对象过多导致的OOM
而无关数据库操作
至于数据库 大量操作会不会造成OOM
,暂时不知道如何验证
2. 迁移
由非加密的数据库迁移到加密的数据库,实际开发时,一定要先做好备份
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);
// 判断旧的数据库文件是否存在
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);
}
}
/**
* 迁移数据库
*/
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, "旧数据库文件删除成功");
}
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);
}
/**
* 删除数据库 db 文件
*/
public boolean onDelete() {
File file = mContext.getDatabasePath(ENCRYPT_NAME);
return SQLiteDatabase.deleteDatabase(file);
}
}
构造方法中,传入了一个字符串密码
,加密就是这里简单。。。
实际开发时,传入的密码
字符串需要使用一些算法来生成,而不是直接在代码中写一个看一眼就知道的密码,不然加密也没啥意义
move()
方法中,是固定的套路,但具体的语句,查了下,具体啥意思就先放弃了
2.2 EncryptDBManager
这里主要就是对 EncryptDBHelper
的初始化
/**
* 迁移数据库管理
*/
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
* 内部实现了数据库的迁移
*/
public boolean init() {
try {
mDBHelper = new EncryptDBHelper(mContext, mPassword);
mDB = mDBHelper.getWritableDatabase();
return true;
} catch (Exception e) {
return false;
}
}
/**
* 拿到数据库中所有的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);
}
/**
* 关闭 database;
*/
public void closeDB() {
mDB.close();
}
/**
* 删除数据库
*/
public Boolean deleteDatabase() {
return mDBHelper.onDelete();
}
}
在Activity
中直接调用方法就成
使用db
工具查看未加密和加密后的数据库文件
3. 最后
WCDB
中还有很多东西不了解,日后慢慢接触
关于遗留的那个
插入
操作优化,有知道的同学,请留言说一下 </p>补充:这里搞错了,已改正,原因说明在上面
有错误,请指出
共勉 : )