《第一行代码》读书笔记(六)

Author Avatar
wshunli 6月 03, 2018
  • 在其它设备中阅读本文章

《第一行代码》读书笔记 — 数据存储方案

《第一行代码》读书笔记(一)— 平台架构 (第1章)
《第一行代码》读书笔记(二)— 应用组件之 Activity (第2、4章)
《第一行代码》读书笔记(三)— 应用组件之 Service (第10章)
《第一行代码》读书笔记(四)— 应用组件之 BroadcastReceiver (第5章)
《第一行代码》读书笔记(五)— 应用组件之 ContentProvider (第7章)
《第一行代码》读书笔记(六)— 数据存储方案 (第6章)
《第一行代码》读书笔记(七)— 多媒体资源 (第8章)
《第一行代码》读书笔记(八)— 网络编程 (第9章)

第6章 数据存储全方案

Android 系统中主要提供了 3 种方式:文件存储、SharedPreference 以及数据库存储。

文件存储

对于文件存储,Android 系统不对数据进行格式化处理,适合一些简单的文本数据或者二进制数据。

1、将数据保存到文件中

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

FileOutputStream fos = openFileOutput(FILENAME, Context.MODE_PRIVATE);
fos.write(string.getBytes());
fos.close();

// 异常捕捉 ==>

FileOutputStream fos = null;
try {
    fos = openFileOutput(FILENAME, Context.MODE_PRIVATE);
    fos.write(string.getBytes());
} catch (FileNotFoundException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}finally {
    try {
        if (fos != null) {
            fos.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

书上是这样写的,主要为了提高性能:

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

FileOutputStream fileOutputStream;
BufferedWriter writer = null;

try {
    fileOutputStream = openFileOutput(FILENAME, Context.MODE_PRIVATE);
    writer = new BufferedWriter(new OutputStreamWriter(fileOutputStream));
    writer.write(string);
} catch (FileNotFoundException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
} finally {
    try {
        if (writer != null) {
            writer.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }

其中 MODE_PRIVATE 表示新创建文件,已经存在的文件会覆盖掉;
如果使用 MODE_APPEND 表示向存在的文件中追加内容,如果文件不存在会新建。

还有 MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE 已经过时了。

2、从文件中读取数据

int SIZE = 4096;
byte[] buf = new byte[SIZE];
FileInputStream fis = null;
try {
    fis = openFileInput(FILENAME);
    int len = fis.read(buf);
    while (len != -1) {
        System.out.println(new String(buf, 0, len));
    }
} catch (FileNotFoundException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
} finally {
    try {
        if (fis != null) {
            fis.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

书上是这样写的,也是为了提高性能:

FileInputStream fileInputStream;
BufferedReader reader = null;
StringBuilder content = new StringBuilder();

try {
    fileInputStream = openFileInput(FILENAME);
    reader = new BufferedReader(new InputStreamReader(fileInputStream));
    String line;
    while ((line = reader.readLine() )!= null) {
        content.append(line);
    }
} catch (FileNotFoundException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
} finally {
    try {
        if (reader != null) {
            reader.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}
System.out.println(content.toString());

3、Android 系统数据存储位置

对于 Android 设备可分为 内部存储 和 外部存储。

3.1、内部存储

内部存储默认应用私有,其他 应用(和用户) 不能访问这些文件。卸载应用系统会自动删除。

应用的内部存储空间一般会有 files 和 cache 两个文件夹,分别代表永久存储和缓存数据。

File Context.getFilesDir() 方法 /data/user/0/com.wshunli.store.demo/files

File Context.getCacheDir() 方法 /data/user/0/com.wshunli.store.demo/cache

其中 0 代表不同用户,Android 6.0 以前不存在。

文件在 /data/data/com.wshunli.store.demo/ 目录下可以看到。

3.2、外部存储

外部存储可能是可移除的存储介质(例如 SD 卡)或内部(不可移除)存储,用户可以删除。

使用外部存储前应 检查介质可用性

/* Checks if external storage is available for read and write */
public boolean isExternalStorageWritable() {
    String state = Environment.getExternalStorageState();
    if (Environment.MEDIA_MOUNTED.equals(state)) {
        return true;
    }
    return false;
}

/* Checks if external storage is available to at least read */
public boolean isExternalStorageReadable() {
    String state = Environment.getExternalStorageState();
    if (Environment.MEDIA_MOUNTED.equals(state) ||
        Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
        return true;
    }
    return false;
}

这部分比较特殊,也可以分为两种,可以保存与其他应用共享的文件,也可以保存应用私有文件(类似于内部存储)。

(1)对于外部存储的 私有 文件夹,一般也会有 files 和 cache 两个文件夹,4.4 及以后读写不再需要权限。

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
                     android:maxSdkVersion="18" />

访问私有文件夹主要有两种方法:

File Context.getExternalFilesDir() 方法 /storage/emulated/0/Android/data/com.wshunli.store.demo/files

File Context.getExternalCacheDir() 方法 /storage/emulated/0/Android/data/com.wshunli.store.demo/cache

其中 getExternalFilesDir() 方法需要传入目录类型。传入 null 表示获取根目录。

目录类型 包括以下几种: DIRECTORY_MUSIC, DIRECTORY_PODCASTS, DIRECTORY_RINGTONES, DIRECTORY_ALARMS, DIRECTORY_NOTIFICATIONS, DIRECTORY_PICTURES, DIRECTORY_MOVIES.
https://developer.android.com/reference/android/os/Environment#DIRECTORY_MUSIC

有时,已分配某个内部存储器 分区 用作外部存储的设备可能还提供了 SD 卡槽。

在 Android 4.3 以前 Context.getExternalFilesDir() 只能获取内部分区的访问权限;
从 Android 4.4 开始 Context.getExternalFilesDirs() 可以同时访问两个位置,及内部分区和 SD 卡。

对于 Android 4.3 或者更低版本使用 ContextCompat.getExternalFilesDirs() 方法有同样的效果,可以同时访问两个位置。

(2)对于外部存储的 共享 文件夹,必须申请读写权限,而且 6.0 以后要申请运行时权限。

Environment.getExternalStorageDirectory() 方法返回值 /storage/emulated/0

Android 7.0 提供简化的 API 来访问常见的外部存储目录。
https://developer.android.com/training/articles/scoped-directory-access

Environment.getExternalStoragePublicDirectory() 返回设备上的 “公共” 位置。

同样需要传入目录类型,例如:

Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM);

返回相机文件所在目录 /storage/emulated/0/DCIM

在外部文件目录中包含名为 .nomedia 的空文件(注意文件名中的点前缀),可在媒体扫描程序中隐藏文件。

4、应用安装包内文件数据访问

安装包内数据文件有很多种,这里主要介绍两部分:assetsraw

这两部分都会原封不动地打包进 apk 安装包,并不会编译成 二进制文件。

assets 文件夹在 app/src/main/assets/ 允许创建目录结构。

AssetManager assetManager = getResources().getAssets();
assetManager.open(FILENAME);

raw 文件夹在 app/src/main/res/raw/ 不允许创建目录结构,但会在 R.java 中自动进行资源标识。

InputStream inputStream = getResources().openRawResource(R.id.data);

SharedPreferences 存储

SharedPreferences 主要用于保存检索原始数据类型的永久性键值对。

1、使用 SharedPreferences 存储数据首先应 获取 SharedPreferences 对象,主要有三种方法:

(1)Context 类中 的 getSharedPreferences 方法,按照文件名称识别 preferences 。

SharedPreferences getSharedPreferences (String name, int mode)

SharedPreferences sharedPreferences = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);

其中 PREFS_NAME 表示 preferences 文件名称,在 /data/data/com.wshunli.store.demo/shared_prefs/ 路径下 。

MODE_PRIVATE 表示操作模式,目前只有这一种模式可选,其他 MODE_WORLD_READABLE, MODE_WORLD_WRITEABLE , MODE_MULTI_PROCESS 已经废弃。

(2)Activity 类中的 getPreferences 方法,按照 Activity 识别 preferencs ,仅用于本 Activity 。

SharedPreferences getPreferences (int mode)

SharedPreferences preferences = getPreferences(MODE_PRIVATE);

仅指定 操作模式 即可。

(3)PreferenceManager 类中的 getDefaultSharedPreferences 静态方法,以应用包名作为前缀识别。

SharedPreferences getDefaultSharedPreferences (Context context)

SharedPreferences defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);

2、获取 SharedPreferences 对象后就可以 存储和检索 数据了。

对于 数据存储 需要使用 SharedPreferences.Editor 接口。

public static final String PREFS_NAME = "wshunli";

SharedPreferences sharedPreferences = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString("website", "wshunli.com");
editor.putBoolean("is", true);
editor.apply();

我们可以在 shared_prefs 文件夹下发现 wshunli.xml 文件:

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="website">wshunli.com</string>
    <boolean name="is" value="true" />
</map>

前面三种方式在 xml 文件命名不太一样,内容是一样的:

/data/data/com.wshunli.store.demo/shared_prefs/wshunli.xml
/data/data/com.wshunli.store.demo/shared_prefs/SPActivity.xml
/data/data/com.wshunli.store.demo/shared_prefs/com.wshunli.store.demo_preferences.xml

SharedPreferences.Editor 提交数据有两种方法,apply() 和 commit() ,两者主要区别有两点:

(1) commit() 有返回值,apply() 没有返回值。apply() 失败了是不会报错的。
(2) apply() 写入文件的操作是 异步 的,会把 Runnable 放到线程池中执行,而 commit() 的写入文件的操作是在当前线程 同步 执行的。

因此当两者都可以使用的时候还是推荐使用 apply() ,因为 apply() 写入文件操作是异步执行的,不会占用主线程资源。

3、从 SharedPreferences 中读取数据

直接使用 SharedPreferences 对象实例方法即可。

String getString (String key, String defValue)

String website = sharedPreferences.getString("website", "");

其他方法还有:
MapgetAll () // 获取所有键值对
boolean contains (String key) // 是否包含某键值数据

数据库存储

Android 提供了对 SQLite 数据库的完全支持。

也有很多其他数据库可以使用,比如 Realm 、ObjectBox 等等。
一些数据库 ORM 框架也要学习,比如 GreenDao 、LitePal 等等。

这里只介绍 SQLite 数据库。

1、创建数据库

在 Android 中操纵 SQLite 数据库可以使用 SQLiteOpenHelper 类。

public class MSQLiteOpenHelper extends SQLiteOpenHelper {

    public MSQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
        this(context, name, factory, version, null);
    }

    public MSQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version, DatabaseErrorHandler errorHandler) {
        super(context, name, factory, version, errorHandler);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {

    }

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

    }
}

其中构造函数:context 即上下文;name 为数据库名称;factory 为自定义 Cursor ,一般传入 null ;version 为数据库版本号,用于数据库升级。

然后调用 SQLiteOpenHelper 对象实例的 getReadableDatabase() 或者 getWritableDatabase() 即可创建数据库。

MSQLiteOpenHelper dbHelper = new MSQLiteOpenHelper(this, "book.db", null, 1);
dbHelper.getWritableDatabase();

数据库文件存储位置:/data/data/com.wshunli.sqlite.demo/databases/book.db

2、添加表结构

前面只是创建了空数据库,下面添加表。

使用 SQLiteDatabase 对象的 execSQL(String sql) 方法。

public class MSQLiteOpenHelper extends SQLiteOpenHelper {

    public static final String CREATE_BOOK = "create table Book ("
            + "id integer primary key autoincrement, "
            + "author text, "
            + "price real, "
            + "pages integer, "
            + "name text)";

    private Context context;

    public MSQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
        this(context, name, factory, version,null);
    }

    public MSQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version, DatabaseErrorHandler errorHandler) {
        super(context, name, factory, version, errorHandler);
        this.context = context;
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_BOOK);
        Toast.makeText(context, "数据写入成功", Toast.LENGTH_LONG).show();
    }

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

    }
}

3、升级数据库

在添加数据库表时,非常有用。其实就是实现 onUpgrade 方法。

public class MSQLiteOpenHelper extends SQLiteOpenHelper {

    public static final String CREATE_BOOK = "create table Book ("
            + "id integer primary key autoincrement, "
            + "author text, "
            + "price real, "
            + "pages integer, "
            + "name text)";
    public static final String CREATE_CATEGORY = "create table Category ("
            + "id integer primary key autoincrement, "
            + "category_name text, "
            + "category_code integer)";

    private Context context;

    public MSQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
        this(context, name, factory, version,null);
    }

    public MSQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version, DatabaseErrorHandler errorHandler) {
        super(context, name, factory, version, errorHandler);
        this.context = context;
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_BOOK);
        db.execSQL(CREATE_CATEGORY);
        Toast.makeText(context, "数据写入成功", Toast.LENGTH_LONG).show();
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        db.execSQL("drop table if exists Book");
        db.execSQL("drop table if exists Category");
        onCreate(db);
    }
}

这时我们在构造函数中 版本号大于 1 即可。

dbHelper = new MSQLiteOpenHelper(this, "book.db", null, 2);

如果在 onCreate 方法里直接添加 db.execSQL(CREATE_CATEGORY); 语句是不会执行的。

4、添加数据

对数据库的操作无非就是 CRUD :C 添加、R 查询、U 更新、D 删除。

添加数据使用 SQLiteDatabase 对象的 insert 方法。

long insert(String table, String nullColumnHack, ContentValues values)

SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
// 开始组装第一条数据
values.put("name", "The Da Vinci Code");
values.put("author", "Dan Brown");
values.put("pages", 454);
values.put("price", 16.96);
db.insert("Book", null, values); // 插入第一条数据
values.clear();
// 开始组装第二条数据
values.put("name", "The Lost Symbol");
values.put("author", "Dan Brown");
values.put("pages", 510);
values.put("price", 19.95);
db.insert("Book", null, values); // 插入第二条数据

其中第二个参数 nullColumnHack 用于未指定添加数据的情形,一般直接传入 null 即可。

5、更新数据

更新数据使用 SQLiteDatabase 对象的 update 方法。

int update(String table, ContentValues values, String whereClause, String[] whereArgs)

SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("price", 10.99);
db.update("Book", values, "name = ?", new String[] { "The Da Vinci Code" });

其中 whereClause 、 whereArgs 用于约束数据第几行或者某几行,不指定则更新所有行。

6、删除数据

删除数据使用 SQLiteDatabase 对象的 delete 方法。

int delete(String table, String whereClause, String[] whereArgs)

SQLiteDatabase db = dbHelper.getWritableDatabase();
db.delete("Book", "pages > ?", new String[] { "500" });

其中 whereClause 、 whereArgs 用于约束数据第几行或者某几行,不指定则删除所有行。

7、查询数据

查询数据使用 SQLiteDatabase 对象的 query 方法。

Cursor query(String table, String[] columns, // 指定表名、列名
             String selection, String[] selectionArgs, 
             String groupBy, String having, 
             String orderBy)

其中 selection 、 selectionArgs 用于约束数据第几行或者某几行,不指定则查询所有行。
然后 groupBy 指定 group by 的列,having 对 group by 结果进一步约束。
最后 orderBy 指定排序方式。

SQLiteDatabase db = dbHelper.getWritableDatabase();
// 查询Book表中所有的数据
Cursor cursor = db.query("Book", null, null, null, null, null, null);
if (cursor.moveToFirst()) {
    do {
        // 遍历Cursor对象,取出数据并打印
        String name = cursor.getString(cursor.getColumnIndex("name"));
        String author = cursor.getString(cursor.getColumnIndex("author"));
        int pages = cursor.getInt(cursor.getColumnIndex("pages"));
        double price = cursor.getDouble(cursor.getColumnIndex("price"));
        Log.d("MainActivity", "book name is " + name);
        Log.d("MainActivity", "book author is " + author);
        Log.d("MainActivity", "book pages is " + pages);
        Log.d("MainActivity", "book price is " + price);
    } while (cursor.moveToNext());
}
cursor.close();

8、使用 SQL 语句操纵数据库

使用 SQLiteDatabase 对象的 execSQL 方法,还是挺麻烦的,可以使用一些 ORM 框架。

参考资料
1、存储选项 | Android Developers
https://developer.android.com/guide/topics/data/data-storage
2、FileInputStream读取文件数据的两种方式 - CSDN博客
https://blog.csdn.net/a909301740/article/details/52574602
3、全面的Android文件目录解析和获取方法(包含对6.0系统的说明) - CSDN博客
https://blog.csdn.net/zhangbuzhangbu/article/details/23257873
4、android 目录/data/data/ 跟 /data/user/0/ 差别 - V2EX
https://www.v2ex.com/t/259080
5、提供资源 | Android Developers
https://developer.android.com/guide/topics/resources/providing-resources
6、Android数据存储之Assets、Raw - CSDN博客
https://blog.csdn.net/sjm19901003/article/details/47026503
7、SharePreferences源码分析(commit与apply的区别以及原理) - CSDN博客
https://blog.csdn.net/Double2hao/article/details/53871640
8、Room,Realm,,ObjectBox 你选择哪个? - 泡在网上的日子
http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2017/0926/8551.html
9、【Android 数据库框架总结,总有一个适合你!】 - CSDN博客
https://blog.csdn.net/da_caoyuan/article/details/61414626
10、android基础——>SQLite数据库的使用 - huhx - 博客园
http://www.cnblogs.com/huhx/p/sqliteDatabase.html

如果本文对您有所帮助,且您手头还很宽裕,欢迎打赏赞助我,以支付网站服务器和域名费用。 https://paypal.me/wshunli 您的鼓励与支持是我更新的最大动力,我会铭记于心,倾于博客。
本文链接:https://www.wshunli.com/posts/461ff372.html