Android —— 微信Sqlite数据库框架WCDB学习

学习资料

数据库,重灾区之一,除了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

《Android —— 微信Sqlite数据库框架WCDB学习》 Activity

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工具查看未加密和加密后的数据库文件

《Android —— 微信Sqlite数据库框架WCDB学习》 未加密
《Android —— 微信Sqlite数据库框架WCDB学习》 加密

3. 最后

WCDB中还有很多东西不了解,日后慢慢接触

关于遗留的那个插入操作优化,有知道的同学,请留言说一下 </p>补充:这里搞错了,已改正,原因说明在上面

有错误,请指出

共勉 : )

    原文作者:英勇青铜5
    原文地址: https://www.jianshu.com/p/bac7e59a7603
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞