Rock with Android


数据存储

接下来来聊聊数据层,而做处理数据时,有一条原则请大家铭记于心:耗时的IO操作要异步进行,不能阻塞UI渲染线程!
安卓的数据存储主要分成下面3种类型,分别有适用的场景和条件。

数据库SQLite

安卓框架提供的数据库是SQLite,本身是一个典型的关系数据库,是设计给嵌入式设备或是轻量级应用来用的。当然我记得官方宣称单表能到4G等指标,但是一般还是轻量级应用和客户端用的比较多。

SQLite最大的优点就是用起来和mysql一样,裸写SQL或者用安卓的ORM都比较方便,默认的数据库文件的路径是在应用目录里,一般是在内部存储里。

下面是一个典型的数据库类模板:

public class HelloDB {

    private static int DATABASE_VERSION = 1;
    private static final String DATABASE_NAME = "hello.db";

    private DatabaseHelper helper;
    private Context context;

    public HelloDB(Context context) {
        helper = new DatabaseHelper(context);
        this.context = context;
    }

    public Context getContext() {
        return context;
    }

    private class DatabaseHelper extends SQLiteOpenHelper {

        public DatabaseHelper(Context context) {
            super(context, DATABASE_NAME, null, DATABASE_VERSION);
        }
        public void onCreate(SQLiteDatabase db) {
            db.execSQL("CREATE TABLE notification (id INTEGER PRIMARY KEY, json TEXT, INTEGER new)");
        }

        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        }
    }
}
        

SQLite的使用需要注意的方面有:

  1. 数据库适合存Api返回的结构,尤其是需要过滤,排序等操作的,可以使用数据库操作简化代码量的情况。
  2. 表结构设计可以使用一些技巧,应对结构变动。一般使用JSON的Api,字段可以如下设计:

    id | 需要排序的字段 | json字符串

    直接存json字符串,这样做的好处是服务端数据结构有变化时,并不需要改动表的列,比较灵活。但是注意,需要排序(也就是where后面可能会出现的字段)还是单独成列比较好,用数据库就要充分利用SQL的特性,如果只是存储,那还不如用文件

  3. Cursor需要及时关闭,而数据库对象一般不需要关闭,或者在主Activity退出时做清理时,关闭数据库对象。
  4. 需要根据数据库的版本,在Helper的onUpgrade里面处理相应的逻辑。
  5. 如果有比较大的文本或者媒体数据,不建议直接放数据库,而是单独存成文件,数据库里放索引。
  6. 如果有批量的查询或者写入操作,使用事务来提高效率。如下面的例子
        try {
            SQLiteDatabase db = helper.getWritableDatabase();
            db.beginTransaction();
            db.execSQL("update notification set new = 0");
            db.setTransactionSuccessful();
            db.endTransaction();
        } catch (SQLiteException e) {
            e.printStackTrace();
        }
                
  7. 毕竟数据库有IO操作,如果批量插入或查询大量结果时,最好不用放到主线程来处理,那样会阻塞UI渲染。可以放到专门处理数据库操作的线程或者使用AsyncTask来处理。

SharePreferences

SharePreferences是key/value的存储,实质上是应用目录里的一个XML文件。一般用来存储设置信息,应用现场信息等。

        SharedPreferences sp = context.getSharedPreferences("my_pref", MODE_PRIVATE);
        String s = pref.getString("key", "default_value");
        //edit这个名字略脑残,其实安卓的底层代码还是有很多丑陋的存在的 _(:з」∠)_
        sp.edit().putString("key", "new_value").commit();//注意!一定要commit,否则不会生效。
        

getSharedPreferences的第二个参数mode,具体含义如下

  1. MODE_PRIVATE : 默认模式,只能当前application可读写
  2. MODE_WORLD_READABLE/MODE_WORLD_WRITEABLE 在API 17里废弃,因为所有应用可读写是很危险的。
  3. MODE_MULTI_PROCESS: 在API 11引入,用于一个应用,不同进程读写同一个SP,在安卓2.3里合法,但是行为不可预期,慎用。

SP注意事项:

  1. 因为SP本质是XML,也就是文本文件,所以密码或者token之类等敏感信息最好加密后存储,否则在root的机器上直接内容可见。
  2. SP本身不能存过大的数据,如果数据比较大,会有读取和写入的性能问题。
  3. 可以用SP在不同Activity直接传递数据和通讯,但是更推荐使用BroadcastReceiver或者startActivityForResult来传递信息。

文件

文件一般用于存储cache或者图片等大量数据,典型的如某个列表的前20条数据,存成一个cache文件;或图片的本地缓存。

一些数据量比较小,但是需要快速存取的可以使用内部存储(其实内部存储和外部存储的性能差别并不太大)

        String FILENAME = "hello_file";
        String string = "hello world!";

        FileOutputStream fos = openFileOutput(FILENAME, Context.MODE_PRIVATE);
        fos.write(string.getBytes());
        fos.close(); //注意!因为IO操作,一定要注意异步处理。
        

如果是图片等比较占空间的数据,或者增长比较快数据,是要放到外部存储的。因为外部存储比较大,但是因为外部存储一般是SD卡,是可以被卸载的,需要监控具体的状态

        boolean mExternalStorageAvailable = false;
        boolean mExternalStorageWriteable = false;
        String state = Environment.getExternalStorageState();

        if (Environment.MEDIA_MOUNTED.equals(state)) {
            // We can read and write the media
            mExternalStorageAvailable = mExternalStorageWriteable = true;
        } else if (Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
            // We can only read the media
            mExternalStorageAvailable = true;
            mExternalStorageWriteable = false;
        } else {
            // Something else is wrong. It may be one of many other states, but all we need
            //  to know is we can neither read nor write
            mExternalStorageAvailable = mExternalStorageWriteable = false;
        }
        

打开外部存储的文件的api是context的getExternalFilesDir(),是从API 8引入的,可以设定一个type,其实就是一个子目录。API 7以下使用getExternalStorageDirectory()得到外部存储的目录。因为现在的版本分布,我们可以认为API 8是兼容的最低线了。

目录上一章网络请求