数据持久化 / 数据本地存储方式
Android本地存储方式有5种,分别是SharedPreferences存储、文件存储、SQLite存储、ContentProvider和网络存储方式。
存储方式 | 简介 | 特点 | 默认存储路径 | 项目应用 |
---|---|---|---|---|
SharedPreferences | SharedPreferences是一种轻量级存储类,数据存储格式为键值对 | 保存一些简单的配置参数等轻量级数据 | /data/data/packageName/shared_prefs/xxx.xml | 登录界面保存上次登录成功的用户名和密码 |
文件存储 | 文件存储是通过I/O流从内部存储或SD卡(外存)中读写数据 | 内存中存储一些较小、安全性较高的数据 外存存储较大的文件或简单的文本/二进制数据 | /data/data/packageName/files | 项目所需图片、音频文件 较大的数据信息(.json/.xml) |
SQLite | 通过SQLite,一种轻型、嵌入式的ACID关系型数据库对数据存储,使用SQL语言 Android为此数据库提供了SQLiteDatabase类,封装了操作数据库的API | 数据量不是很大且逻辑关系较为复杂的数据(结构性数据) | /data/data/packageName/databases | 存储本地数据信息(结构性数据) |
ContentProvider | 作为Android四大组件之一,ContentProvider一般为存储和获取数据提供统一的接口,可以在不同的应用程序之间共享数据 仅作为传输数据的媒介,数据源具有多样性 | Android手机系统数据 跨进程数据 | 通过URI对象 | 获取手机短信、联系人等 进程间数据共享、交换 |
网络存储 | 与后台交互,将数据存储在后台数据库中 | 数据量大,逻辑关系复杂的数据交给后台处理 | 远程服务器 | 庞大的数据库 较大的音频、图片 |
SharedPreferences
- 简介
SharedPreferences是Android平台上一个轻量级的存储类,主要是保存一些常用的配置比如窗口状态。是Android最简单数据存储方式。
- 只支持Java基本数据类型 & String类型数据存储
如果要用 SharedPreferences 存取复杂的数据类型(类,图像等),就需要对这些数据进行编码。通常会将复杂类型的数据转换成Base64编码,然后将转换后的数据以字符串的形式保存在XML文件中。 - 数据存储类型为key-value对。
- 使用SharedPreferences保存数据,其背后是用xml文件存放数据,文件 存放在/data/data/ < package name > /shared_prefs目录下。
- 是一种轻量级存储类,常用于保存一些常用的配置比如窗口状态。之所以说SharedPreference是一种轻量级的存储方式,是因为它在创建的时候会把存储数据的整个xml文件全部加载进内存。
- SharedPreferences读取数据都使用awaitLoadedLocked同步锁,故是线程安全的。
- SharedPreferences一般采用单例模式
- 使用
存储数据
//获取一个文件名为test、权限为private的xml文件的SharedPreferences对象
SharedPreferences sharedPreferences = getSharedPreferences("test", MODE_PRIVATE);
//得到SharedPreferences.Editor对象,并保存key-value键值对数据到该对象中
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString("username", et_username.getText().toString().trim());
editor.putString("password", et_password.getText().toString().trim());
//apply提交数据,保存key-value对到文件中
editor.apply();
// editor.commit();
// apply没有返回值而commit返回boolean表明修改是否提交成功
// apply是将修改数据原子提交到内存, 而后异步真正提交到硬件磁盘,
// 而commit是在当前线程同步的提交到硬件磁盘,会阻塞调用它的线程。
// 因此,推荐使用apply:在多个并发的提交commit的时候,他们会等待正在处理的commit保存到磁盘后在操作,从而降低了效率。而apply只是原子的提交到内容,后面有调用apply的函数的将会直接覆盖前面的内存数据,这样从一定程度上提高了很多效率。
super.onStop();
获取数据
et_username = (EditText) findViewById(R.id.et_username);
et_password = (EditText) findViewById(R.id.et_password);
SharedPreferences sharedPreferences = this.getSharedPreferences("test", MODE_PRIVATE);
et_username.setText(sharedPreferences.getString("username",""));
et_password.setText(sharedPreferences.getString("password",""));
- 性能优化
SharedPreferences是Android平台上一个轻量级的存储类,主要是保存一些常用的配置比如窗口状态。本质是通过存放在/data/data//shared_prefs目录下xml文件存放数据。由于创建时候会把整个xml文件全部加载进内存,故若SharedPreference文件比较大,会带来如下问题:
- 第一次从sp中获取值时,可能会阻塞主线程,使界面卡顿、掉帧。
- 解析sp时会产生大量临时对象,导致频繁GC,引起界面卡顿
- 这些存储的键值对会永远存储在内存中,不会释放,若存取较大的数据则十分消耗内存。
故优化建议如下: - 不要存放大的数据,不相关的配置项不要放在一起。
- 频繁读取的key和不宜变动的key不要放在一起,影响读写速度。
- 不要多次apply和edit,尽量批量修改一次提交。多次apply会阻塞主线程(引起ANR)。
SP 调用 apply 方法,会创建一个等待锁mcr.writtenToDiskLatch.await()放到 QueuedWork 中,并将真正数据持久化封装成一个任务放到异步队列中执行,任务执行结束会释放锁。
Activity onStop 以及 Service 处理 onStop,onStartCommand 时,执行 QueuedWork.waitToFinish() 等待所有的等待锁释放。
- 尽量不要存放JSON和HTML。直接使用JSON配置文件。
- 不应使用SharedPreference进行跨进程通信(一般用ContentProvider。)
文件存储
- 内部存储(Internal Storage)
- 简介
- 位于/data/data/< package-name >/目录下的文件
- 依附于应用,位于和应用包名相同的目录下,只能被应用内部读写,且当应用卸载后,内部存储的文件也被删除。
- 内部存储控件有限,且存放系统本身和系统应用程序数据,包括SharedPreferences和SQLite数据库。
- 适用于存储体积小且安全性高的文件信息(内部存储一般用于系统存储APP专属文件)
- 存储路径 & 获取方式
方法 | 路径 | 解释 |
---|---|---|
getFilesDir() | /data/data/package-name/files | 返回应用内部存放文件的目录的绝对路径。 |
getCacheDir() | /data/data/package-name/cache | 返回应用内部存储的临时目录。系统内部存储即将耗尽的时候,可能会删除这个目录下的文件。 |
getDir(String name, int mode) | /data/data/package-name/name | 可用于在应用内部存储根目录下创建或打开自定义的文件目录。name表示自定义的文件目录名。mode表示操作模式,用来控制该目录的读写权限,默认为MODE_PRIVATE,表示仅仅应用自身可以访问。 |
附:/data/user/0/packname/目录(系统创建App专属文件):
cache下存放缓存数据,databases下存放使用SQLite存储的数据,files下存放普通数据(log数据,json型数据等),shared_prefs下存放使用SharedPreference存放的数据。这些文件夹都是由系统创建的。
- 存储方式
public static void writeInternal(String fileName, String content) throws IOException {
// 获取文件绝对路径
String filePathName = context.getFilesDir().getAbsolutePath()+"/"+fileName;
// 打开文件输出流
FileOutputStream fileOutputStream = new FileOutputStream(filePathName);
// 写数据到文件中
fileOutputStream.write(content.getBytes());
// 关闭输出流
fileOutputStream.close();
}
- 外部存储(External Storage)
- 简介
- 位于/storage目录下的文件
- 4.4系统及以上的手机的外部存储分为机身存储(/storage/emulated/) & SD卡(/storage/sdcard/)两部分。(SD卡属于USB存储设备的形式装载外部存储,可拆卸)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
files = getExternalFilesDirs(Environment.MEDIA_MOUNTED);
for(File file:files){
Log.e("main",file);
}
}
// 如果有SD卡,打印路径有2条:
// /storage/emulated/0/Android/data/packname/files/mounted 机身存储的外部存储路径
// /storage/sdcard/0/Android/data/packname/files/mounted SD卡存储的外部存储路径
- 不依附于应用。外部存储的文件可以被全局访问,且用户卸载应用时,系统只卸载通过调用getExternalFilesDir()获取的目录里的文件(/storage/emulated/0/Android/data/packagename/files)。
- 适用于存放希望被其他应用共享的及被用户访问的文件(外部存储一般用于开发人员存储APP专属文件)
- 存储路径 & 获取方式
方法 | 路径 | 解释 |
---|---|---|
Environment.getExternalStoragePublicDirectory(Environment.Type) Environment.getExternalStorageDirectory(Environment.Type) | /storage/emulated/0 | 获取外部存储的公共文件路径 |
getExternalFilesDir(Environment.Type) | /storage/emulated/0/Android/data/package-name/files | 获取某个应用在外部存储的私有文件路径 |
getExternalCacheDir() | /storage/emulated/0/Android/data/package-name/cache | 获取某个应用在外部存储的cache路径 |
其中,Environment的Type参数有:
Environment的Type参数 | 对应模拟路径 | 解释说明 |
---|---|---|
DIRECTORY_DCIM | /storage/emulated/0/DCIM | 相册 |
DIRECTORY_DOCUMENTS | /storage/emulated/0/Documents | 文件 |
DIRECTORY_DOWNLOADS | /storage/emulated/0/Download | 下载文件 |
DIRECTORY_MUSIC | /storage/emulated/0/Music | 音乐 |
DIRECTORY_PICTURES | /storage/emulated/0/Pictures | 图片 |
- 存储方式
(1)获取外部存储权限
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
(2)检测外部存储是否可用(外部存储可能不可用,比如用户将其挂载到了电脑或者移除了提供外部存储的SD卡)
public boolean isExternalStorageWritable() {
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state)) {
return true;
}
return false;
}
public boolean isExternalStorageReadable() {
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state) ||
Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
return true;
}
return false;
}
(3)公共文件目录的获取
公共文件目录可以通过getExternalStoragePublicDirectory()方法获取,需要指定文件类型参数,以便外部统一处理。比如DIRECTORY_MUSIC或DIRECTORY_PICTURES。比如:
public File getAlbumStorageDir(String albumName) {
// Get the directory for the user's public pictures directory.
File file = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES), albumName);
if (!file.mkdirs()) {
Log.e(LOG_TAG, "Directory not created");
}
return file;
}
应用卸载时,系统不会删除这些文件。
(4)私有文件目录的获取
调用getExternalFilesDir()方法传入目录名字获取相应目录。当用户卸载应用时候,系统会删除这些文件。
比如,使用下面方法创建个人相册目录:
public File getAlbumStorageDir(Context context, String albumName) {
// Get the directory for the app's private pictures directory.
File file = new File(context.getExternalFilesDir(
Environment.DIRECTORY_PICTURES), albumName);
if (!file.mkdirs()) {
Log.e(LOG_TAG, "Directory not created");
}
return file;
}
上述方法会在Environment.DIRECTORY_PICTURES目录下创建albumName值的目录,当然你也可以将第一个参数传为null,则会在你应用外部存储私有目录的根目录下创建。
- 其他存储
路径 | 系统文件 | 缓存文件 |
---|---|---|
路径 | /system | /cache |
获取方式 | Environment.getRootDirectory() | Environment.getDownloadCacheDirectory() |
- 案例:Android 保存网络图片到系统相册
- 确定存储路径
1、内部存储/data/data/packageName/(不采用)
一个应用对内部存储的所有访问都被限制在这个文件夹中,也就是说Android应用只能在该目录中读取,创建,修改文件。对该目录之外的其他内部存储中的目录都没有任何操作的权限。
因此,如果将图片保存在内部存储中,只能被应用自身读取,其他应用均无法读取。如果需要让系统图库,相册或其他应用能够找到保存的图片,必须将图片保存到外部存储中。
2、外部存储
(1)/storage/emulated/0/Android/data/packageName/(不采用)
这个路径会随着应用的销毁而销毁,无法长期存储在内存中。因此,也不能将图片保存在这个目录中。
(2)/storage/emulated/0/packageName/image/(采用)
除外部存储的/Android目录之外的其他目录一般都是可以被其他应用访问的。目前,大多数应用都会在外部存储的根路径下建立一个类似包名的多层目录,以存储需要共享的文件。
获取外部存储路径:由于Android系统的碎片化问题,不同设备上外部存储的路径很可能会不同,因此,不能直接使用/storage/emulated/0/作为外部存储的根路径。 Android SDK中 Environment类 提供了getExternalStorageDirectory()方法来获取外部存储的根路径。
Environment.getExternalStorageDirectory().getAbsolutePath();// /storage/emulated/0
String dir = Environment.getExternalStorageDirectory().getAbsolutePath() + "/tencent/MicroMsg/WeiXin/";
- 获取外部存储权限
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
- 确定外部存储状态
由于外部存储需要被挂载,也可以被卸载,在写入文件之前,需要先判断外部存储的状态是否正常。只有状态正常情况下才可以执行保存文件的操作。
挂载(mounting)是指由操作系统使一个存储设备(诸如硬盘、CD-ROM或共享资源)上的计算机文件和目录可供用户通过计算机的文件系统访问的一个过程。
//获取内部存储状态
String state = Environment.getExternalStorageState();
//如果状态不是mounted,无法读写
if (!state.equals(Environment.MEDIA_MOUNTED)) {
return;
}
- 确定文件名
保存的图片文件名可以由应用根据自身需要自行确定,一般来说需要有一个命名规则,然后根据命名规则计算得到文件名。 常用:
(1)时间命名
根据保存图片的当前系统时间来对图片命名。
Calendar now = new GregorianCalendar();
SimpleDateFormat simpleDate = new SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault());
String fileName = simpleDate.format(now.getTime());
(2)文件URL命名
每张网络图片都有一个对应的图片URL,可以根据图片的URL来对图片命名。
- 保存到文件中
try {
File file = new File(dir + fileName + ".jpg");
FileOutputStream out = new FileOutputStream(file);
mBitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);//将Bitmap压缩到一个文件输出流
out.flush();
out.close();
} catch (Exception e) {
e.printStackTrace();
}
- 发送广播,通知系统扫描保存后的文件
将Bitmap对象保存成外部存储中的一个jpg格式的文件。为了让其他应用能够知道图片文件被创建,必须通知MediaProvider服务将新建的文件添加到图片数据库中。
Android系统中常驻一个MediaProvider服务,对应的进程名为android.process.media,此服务用来管理本机上的媒体文件,提供媒体管理服务。在系统开机或者收到外部存储的挂载消息后,MediaProvider会调用MediaScanner,MediaScanner会扫描外部存储中的所有文件,根据文件类型的后缀将文件信息保存到对应的数据库中,供其他APP使用。
MediaScannerReceiver是一个广播接收者,当它接收到特定的广播请求后,就会去扫描指定的文件,并根据文件信息将其添加到数据库中。当图片文件被创建后,就可以发送广播给MediaScannerReceiver,通知其扫描新建的图片文件。
//保存图片后发送广播通知更新系统图库(将图片保存在系统图库)
Uri uri = Uri.fromFile(file);
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri));
- 大图/多图的异步保存
保存图片文件时,如果图片很大,或需要同时保存多张图片时,就需要较多的时间。为了避免阻塞UI线程,出现帧率下降或ANR,通常需要将图片保存操作放到线程中去执行。当图片保存完毕后通过sendMessage()方法通知UI线程保存结果。 - 完整代码
1、保存Bitmap到本地指定路径下
2、通过广播,通知系统相册图库刷新数据
public class ImgUtils {
//保存文件到指定路径
public static boolean saveImageToGallery(Context context, Bitmap bmp) {
// 首先保存图片
String storePath = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "dearxy";
File appDir = new File(storePath);
if (!appDir.exists()) {
appDir.mkdir();
}
String fileName = System.currentTimeMillis() + ".jpg";
File file = new File(appDir, fileName);
try {
FileOutputStream fos = new FileOutputStream(file);
//通过io流的方式来压缩保存图片
boolean isSuccess = bmp.compress(Bitmap.CompressFormat.JPEG, 60, fos);
fos.flush();
fos.close();
//把文件插入到系统图库
//MediaStore.Images.Media.insertImage(context.getContentResolver(), file.getAbsolutePath(), fileName, null);
//保存图片后发送广播通知更新数据库
Uri uri = Uri.fromFile(file);
context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri));
if (isSuccess) {
return true;
} else {
return false;
}
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
}
SQLite
- 简介
一种轻量级Android 内置的数据库,是遵守ACID的关联式数据库管理系统。
- 存储结构型、关系型数据,可使用SQL语言,支持事务处理。
- 轻量级。占用资源非常低(可能只有几百K内存)。
- 位于 /data/data/package-name/databases/database-name.db(内部存储,只能应用程序内部访问),Sqlite中每个数据库以单个文件形式存在,以B-Tree的形式存储在磁盘。
- Sqlite共享锁和独享锁机制,保证线程安全。
一个共享锁允许多个数据库联接在同一时刻从这个数据库文件中读取信息。“共享”锁将不允许其他联接针对此数据库进行写操作。
一个临界锁允许其他所有已经取得共享锁的进程从数据库文件中继续读取数据。但是它会阻止新的共享锁的生成。也就说,临界锁将会防止因大量连续的读操作而无法获得写入的机会。
- 使用
- 创建一个类继承SQLiteOpenHelper,复写 onCreat()、onUpgrade()
public class DatabaseHelper extends SQLiteOpenHelper {
// 数据库版本号
private static Integer Version = 1;
/**
* 构造函数
* 在SQLiteOpenHelper的子类中,必须有该构造函数
*/
public DatabaseHelper(Context context, String name, SQLiteDatabase.CursorFactory factory,
int version) {
// 参数说明
// context:上下文对象 name:数据库名称 param:一个可选的游标工厂(通常是 Null) version:当前数据库的版本,值必须是整数并且是递增的状态
// 必须通过super调用父类的构造函数
super(context, name, factory, version);
}
/**
* 复写onCreate()
* 调用时刻:当数据库第1次创建时调用
* 作用:创建数据库 表 & 初始化数据(getWritableDatabase() / getReadableDatabase() 第一次被调用时)
* SQLite数据库创建支持的数据类型: 整型数据、字符串类型、日期类型、二进制
*/
@Override
public void onCreate(SQLiteDatabase db) {
// 创建数据库1张表
// 通过execSQL()执行SQL语句(此处创建了1个名为person的表)
String sql = "create table person(id integer primary key autoincrement,name varchar(64),address varchar(64))";
db.execSQL(sql);
}
/**
* 复写onUpgrade()
* 调用时刻:当数据库升级时则自动调用(即 数据库版本 发生变化时)
* 作用:更新数据库表结构
* 注:创建SQLiteOpenHelper子类对象时,必须传入一个version参数,该参数 = 当前数据库版本, 若该版本高于之前版本, 就调用onUpgrade()
*/
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// 参数说明:
// db : 数据库 oldVersion : 旧版本数据库 newVersion : 新版本数据库
// 使用 SQL的ALTER语句,在person表中增加 sex 列
String sql = "alter table person add sex varchar(8)";
db.execSQL(sql);
}
}
- 创建数据库
// 步骤1:创建DatabaseHelper对象(注:此时还未创建数据库)
SQLiteOpenHelper dbHelper = new DatabaseHelper(SQLiteActivity.this,"test_carson");
// 步骤2:真正创建 / 打开数据库
SQLiteDatabase sqliteDatabase = dbHelper.getWritableDatabase(); // 创建 or 打开 可读/写的数据库
SQLiteDatabase sqliteDatabase = dbHelper.getReadableDatabase(); // 创建 or 打开 可读的数据库
- 操作数据库
// 插入数据
String sql = "insert into user (id,name) values (1,'carson')";
db.execSQL(sql) ;
// 修改数据
String sql = "update [user] set name = 'zhangsan' where id="1";
db.execSQL(sql);
// 删除数据
String sql = "delete from user where id="1";
db.execSQL(sql);
// 查询数据
// db.query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy);
Cursor cursor = sqliteDatabase.query("user", new String[] { "id","name" }, "id=?", new String[] { "1" }, null, null, null);
if(cursor.moveToFirst()) {
String name = cursor.getString(cursor.getColumnIndex("name"));
}
// 关闭数据库
db.close();