SharedPreferences是Android的一个轻量级存储工具,它采用的存储结构是Key-Value的键值对方式,存储介质是XML文件,且以XML标记保存键值对。保存共享参数键值对信息的文件路为:/data/data/应用包名/shared_prefs/文件名.xml。
例如:
<map>
<string name="name">Mr Leestring>
<int nane="age" value="30"/>
<boolean name="married" value="true" />
<float name="weight" value="100.0"/>
map>
基于XML格式的特点,共享参数主要用于如下场合:
(1)简单且孤立的数据。若是复杂且相互关联的数据,则要保存于关系数据库。
(2)文本形式的数据。若是二进制数据,则要保存至文件。
(3)需要持久化存储的数据。App退出后再次启动时,之前保存的数据仍然有效。
共享参数类似map集合,也有get和put方法。
第一步:获取共享参数实例:
SharedPreferences preferences = getSharedPreferences("config", Context.MODE_PRIVATE);
上面的config就是文件名,如果没有会自动创建,MODE_PRIVATE是私有模式,只有当前app可以访问。
第二步:写入文件
//获取编辑器
SharedPreferences.Editor edit = preferences.edit();
edit.putString("name",name);
edit.putInt("age",Integer.parseInt(age));
edit.putFloat("height",Float.parseFloat(height));
edit.putFloat("weight",Float.parseFloat(weight));
edit.putBoolean("married",cb_married.isChecked());
//提交编辑器中的修改
edit.commit();
第三步:读取config文件
String name = preferences.getString("name", null);
int age = preferences.getInt("age", 0);
Float height = preferences.getFloat("height", 0f);
Float weight = preferences.getFloat("weight", 0f);
Boolean married = preferences.getBoolean("married", false);
SQLiteDatabase是Android提供的SQLite数据库管理器,开发者可以在活动页面代码调用openOrCreateDatabase方法获取数据库实例,参考代码如下:
// 创建名为test.db的数据库。数据库如果不存在就创建它,如果存在就打开它
SQLiteDatabase db = openOrCreateDatabase(getFilesDir() + "/test.db",Context.MODE_PRIVATE, null);
String desc = String.format("数据库%s创建%s", db.getPath(), (db!=null)?"成功":"失败");
tv_database.setText(desc);
// deleteDatabase(getFilesDir() + "/test.db"); // 删除名为test.db数据库
获得数据库实例之后,就能对该数据库开展各项操作了。数据库管理器SQLiteDatabase提供了若干操作数据表的API,常用的方法有3类,列举如下:
1.管理类,用于数据库层面的操作
2.事务类,用于事务层面的操作
3.数据处理类,用于数据表层面的操作
Android提供了数据库帮助器SQLiteOpenHelper,帮助开发者合理使用SQLite。
SQLiteOpenHelper的具体使用步骤如下:
步骤一,新建一个继承自SQLiteOpenHelper的数据库操作类,按提示重写onCreate和onUpgrade两个
方法。其中,onCreate方法只在第一次打开数据库时执行,在此可以创建表结构;而onUpgrade方法在数据库版本升高时执行,在此可以根据新旧版本号变更表结构。
步骤二,为保证数据库安全使用,需要封装几个必要方法,包括获取单例对象、打开数据库连接、关闭数据库连接,说明如下:
查询结果会返回一个游标类Cursor,Cursor的常用方法可分为3类,说明如下:
1.游标控制类方法,用于指定游标的状态
2.游标移动类方法,把游标移动到指定位置
3.获取记录类方法,可获取记录的数量、类型以及取值
SQLite帮助类
public class UserDBHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "user.db";
private static final String TABLE_NAME = "user_info";
private static final int DB_VERSION = 2;
private static UserDBHelper mHelper = null;
private static SQLiteDatabase mRDB = null;
private static SQLiteDatabase mWDB = null;
public UserDBHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
//单例模式
public static UserDBHelper getInstance(Context context){
if (mHelper == null) {
mHelper = new UserDBHelper(context);
}
return mHelper;
}
//打开数据库的读连接
public SQLiteDatabase openReadLink(){
if (mRDB == null || !mRDB.isOpen()) {
mRDB = mHelper.getReadableDatabase();
}
return mRDB;
}
//打开数据库的写连接
public SQLiteDatabase openWriteLink(){
if (mWDB == null || !mWDB.isOpen()) {
mWDB = mHelper.getWritableDatabase();
}
return mWDB;
}
//关闭数据库连接
public void closeLink(){
if (mRDB!=null && mRDB.isOpen()){
mRDB.close();
mRDB = null;
}
if (mWDB!=null && mWDB.isOpen()){
mWDB.close();
mWDB = null;
}
}
//创建数据库,执行建表语句
@Override
public void onCreate(SQLiteDatabase db) {
String sql = "CREATE TABLE IF NOT EXISTS "+TABLE_NAME+" (" +
"_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
"name VARCHAR NOT NULL ," +
"age INTEGER NOT NULL," +
"height LONG NOT NULL," +
"weight FLOAT NOT NULL," +
"married INTEGER NOT NULL);";
db.execSQL(sql);
}
//数据库版本号DB_VERSION更新时执行
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
String sql1 = "ALTER TABLE "+TABLE_NAME+" ADD COLUMN phone VARCHAR";
db.execSQL(sql1);
String sql2 = "ALTER TABLE "+TABLE_NAME+" ADD COLUMN password VARCHAR";
db.execSQL(sql2);
}
//插入
public long insert(User user){
ContentValues values = new ContentValues();
values.put("name",user.name);
values.put("age",user.age);
values.put("height",user.height);
values.put("weight",user.weight);
values.put("married",user.married);
//执行插入记录动作,该语句返回插入记录的行号
//如果第三个参数values为Nul1或者元素 个数为0,由 于insert()方法要求必须添加一条除 了主键之外其它字段为Null值的记录,
//为了满足sQL语法的需要,insert 语句必须给定一个字段名 ,如: insert into person (name) values (NULL)
// 倘若不给定字段名 ,insert语句就成 J这样: insert into person() values(), 显然这不满足标准SQL,
//.如果第三个参数values不为Nu11并且元素的个数大于0,可以把第二个参数设置为null。
try {
//开始事务
mWDB.beginTransaction();
//int num = 10/ 0;
mWDB.insert(TABLE_NAME,null,values);
//标识成功,表示前面代码没有报错
mWDB.setTransactionSuccessful();
}catch (Exception e){
e.printStackTrace();
}finally {
//结束事务
mWDB.endTransaction();
}
return 1;
}
//删除
public long deleteByName(String name){
return mWDB.delete(TABLE_NAME,"name = ?",new String[]{name});
}
//更新
public long update(User user){
ContentValues values = new ContentValues();
values.put("name",user.name);
values.put("age",user.age);
values.put("height",user.height);
values.put("weight",user.weight);
values.put("married",user.married);
return mWDB.update(TABLE_NAME,values,"name=?",new String[]{user.name});
}
//查询所有
public List<User> queryAll(){
List<User> list = new ArrayList<>();
//执行记录查询动作,该语句返回结果集的游标
Cursor cursor = mRDB.query(TABLE_NAME, null, null, null, null, null, null);
//循环取出游标指向的每条记录
while (cursor.moveToNext()){
User user = new User();
user.id = cursor.getInt(0);
user.name = cursor.getString(1);
user.age = cursor.getInt(2);
user.height = cursor.getLong(3);
user.weight = cursor.getFloat(4);
//SQLite没有布尔型,用0表示false, 用1表示true
user.married = cursor.getInt(5)==0?false:true;
list.add(user);
}
return list;
}
}
系统给每个App都分配了默认的私有存储空间。App在私有空间上读写文件无须任何授权,但是若想在公共空间读写文件,则要在AndroidManifest.xml里面添加下述的权限配置。
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
上面虽然设置了读写权限,但是app的存储权限设置中仍然是禁止的,要去手动打开。
既然存储卡分为公共空间和私有空间两部分,它们的空间路径获取也就有所不同。
若想获取公共空间的存储路径,调用的是Environment.getExternalStoragePublicDirectory方法;
若想获取应用私有空间的存储路径,调用的是getExternalFilesDir方法。
下面是分别获取两个空间路径的代码例子:
// 获取系统的公共存储路径
String publicPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString();
// 获取当前App的私有存储路径
String privatePath = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString();
boolean isLegacy = true;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Android10的存储空间默认采取分区方式,此处判断是传统方式还是分区方式
isLegacy = Environment.isExternalStorageLegacy();
}
String desc = "系统的公共存储路径位于" + publicPath +
"\n\n当前App的私有存储路径位于" + privatePath +
"\n\nAndroid7.0之后默认禁止访问公共存储目录" +
"\n\n当前App的存储空间采取" + (isLegacy?"传统方式":"分区方式");
tv_path.setText(desc);
首先创建一个写文件的工具类,进行封装:
public class FileUtil {
//把字符串保存到指定路径的文本文件.
public static void saveText(String path,String txt){
BufferedWriter bw = null;
try{
bw = new BufferedWriter(new FileWriter(path));
bw.write(txt);
}catch (Exception e){
e.printStackTrace();
}finally {
if (bw != null) {
try{
bw.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
}
//从指定路径的文本文件中读取内容字符串
public static String openText(String path){
BufferedReader br = null;
StringBuilder builder = new StringBuilder();
try{
br = new BufferedReader(new FileReader(path));
String line = null;
while ((line = br.readLine())!=null){
builder.append(line);
}
}catch (Exception e){
e.printStackTrace();
}finally {
if (br != null) {
try{
br.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
return builder.toString();
}
}
app的主要java代码:
//外部存储的公共空间
directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString();
//内部存储的私有空间
path = directory + File.separatorChar + fileName;
Log.d("path",path);
FileUtil.saveText(path,sb.toString());
ToastUtil.show(this,"保存成功");
文本文件读写可以转换为对字符串的读写,而图片文件保存的是图像数据,需要专门的位图工Bitmap处理。位图对象依据来源不同又分成3种获取方式,分别对应位图工厂BitmapFactory的下列3种方法:
Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.huawei);
// 从指定路径的图片文件中读取位图数据
public static Bitmap openImage(String path) {
Bitmap bitmap = null; // 声明一个位图对象
// 根据指定的文件路径构建文件输入流对象
try (FileInputStream fis = new FileInputStream(path)) {
bitmap = BitmapFactory.decodeStream(fis); // 从文件输入流中解码位图数据
} catch (Exception e) {
e.printStackTrace();
}
return bitmap; // 返回图片文件中的位图数据
}
得到位图对象之后,就能在图像视图上显示位图。图像视图ImageView提供了下列方法显示各种来源的图片:
读文件的操作很多,但写入文件的方式只有通过位图对象的compress方法将位图数据压缩到文件输出流:
//把位图数据保存到指定路径的图片文件
public static void saveImage(String path, Bitmap bitmap) {
FileOutputStream fos = null;
try{
fos = new FileOutputStream(path);
//把位图数据压缩到文件输出流中
bitmap.compress(Bitmap.CompressFormat.JPEG,100,fos);
}catch (Exception e){
e.printStackTrace();
}finally {
if (fos != null) {
try{
fos.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
}
读取图片:
//从指定路径的图片文件中读取位图数据
public static Bitmap readImage(String path) {
Bitmap bitmap = null;
FileInputStream fis = null;
try{
fis = new FileInputStream(path);
bitmap = BitmapFactory.decodeStream(fis);
}catch (Exception e){
e.printStackTrace();
}finally {
if (fis != null) {
try{
fis.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
return bitmap;
}
app代码:
public void onClick(View v) {
switch (v.getId()){
case R.id.btn_save:
String fileName = System.currentTimeMillis() + ".jpeg";
//获取当前App的私有下载目录
path = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() + File.pathSeparator + fileName;
Log.d("path",path);
//从指定的资源文件中获取位图对象
Bitmap b1 = BitmapFactory.decodeResource(getResources(), R.drawable.test1);
//把位图对象保存为图片文件
FileUtil.saveImage(path,b1);
ToastUtil.show(this,"保存成功");
break;
case R.id.btn_read:
//第一种方式读取
Bitmap b2 = FileUtil.readImage(path);
iv_image.setImageBitmap(b2);
//第二种方式读取
//Bitmap b3 = BitmapFactory.decodeFile(path);
//iv_image.setImageBitmap(b3);
//第三种方式:直接调用setImageURI方法,设置图像视图的路径对象
//iv_image.setImageURI(Uri.parse(path));
break;
}
}
Application是Android的一大组件,在App运行过程中有且仅有一个Application对象贯穿应用的整个生
命周期。打开AndroidManifest.xml,发现activity节点的上级正是application节点,不过该节点并未指
定name属性,此时App采用默认的Application实例。
自定义一个类继承Application类:
public class MyApplication extends Application {
//单例对象
private static MyApplication mApp;
public static MyApplication getInstance(){
return mApp;
}
//在App启动时调用
public void onCreate() {
super.onCreate();
mApp = this;
}
//在App终止时调用,只用于虚拟环境,在开发设备中默认会删除这个方法,不被回调
public void onTerminate() {
super.onTerminate();
Log.d("MyApplication","onTerminate");
}
//在配置改变时调用,例如从竖屏变为横屏。
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
Log.d("MyApplication","onConfigurationChanged");
}
}
继承Application,继承之后可供重写的方法主要有以下3个:
修改AndroidManifest.xml,设置application节点的name属性为MyApplication,此时app采用的是我们自己定义的application
<application
android:name=".MyApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.TestMyApplicaion">
</application>
适合在Application中保存的全局变量主要有下面3类数据:
(1)会频繁读取的信息,例如用户名、手机号码等。
(2)不方便由意图传递的数据,例如位图对象、非字符串类型的集合对象等。
(3)容易因频繁分配内存而导致内存泄漏的对象,例如Handler处理器实例等。
由于全局变量操作的是内存,所以速度比较快
下面演示如何使用全局变量去存储一个人的信息:
先在MyApplication中定义一个map集合用于存储数据
public class MyApplication extends Application {
//单例对象
private static MyApplication mApp;
//声明一个公共的信息映射对象,可当作全局变量使用
public HashMap<String,String> infoMap = new HashMap<>();
public static MyApplication getInstance(){
return mApp;
}
//在App启动时调用
public void onCreate() {
super.onCreate();
mApp = this;
}
//在App终止时调用,只用于虚拟环境,在开发设备中默认会删除这个方法,不被回调
public void onTerminate() {
super.onTerminate();
Log.d("MyApplication","onTerminate");
}
//在配置改变时调用,例如从竖屏变为横屏。
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
Log.d("MyApplication","onConfigurationChanged");
}
}
读写操作使用:
//获取MyApplication实例
MyApplication app = MyApplication.getInstance();
//写入数据
String name = et_name.getText().toString();
String age = et_age.getText().toString();
String height = et_height.getText().toString();
String weight = et_weight.getText().toString();
//将数据保存到application中
app.infoMap.put("name",name);
app.infoMap.put("age",age);
app.infoMap.put("height",height);
app.infoMap.put("weight",weight);
app.infoMap.put("married",cb_married.isChecked()?"是":"否");
//从application中获取数据
String name = app.infoMap.get("name");
if (name == null){
return;
}
String age = app.infoMap.get("age");
String height = app.infoMap.get("height");
String weight = app.infoMap.get("weight");
String married = app.infoMap.get("married");
et_name.setText(name);
et_age.setText(age);
et_height.setText(height);
et_weight.setText(weight);
if ("是".equals(married)){
cb_married.setChecked(true);
}else{
cb_married.setChecked(false);
}
虽然Android提供了数据库帮助器,但是开发者在进行数据库编程时仍有诸多不便,比如每次增加一张
新表,开发者都得手工实现代码逻辑。
数据库框架—Room,该框架同样基于SQLite,但它通过注解技术极大地简化了数据库操作,减少了原来相当一部分编码工作量。因为是第三方框架所以要在build.gradle中引入依赖:
dependencies节点添加下面两行配置,表示导入指定版本的Room库
implementation 'androidx.room:room-runtime:2.5.2'
annotationProcessor 'androidx.room:room-compiler:2.5.2'
导入Room库之后,还要编写若干对应的代码文件。以录入图书信息为例,此时要对图书信息表进行增
删改查,则具体的编码过程分为下列5个步骤:
1.编写图书信息表对应的实体类
假设图书信息类名为BookInfo,且它的各属性与图书信息表的各字段一一对应,那么要给该类添加
“@Entity”注解,表示该类是Room专用的数据类型,对应的表名称也叫BookInfo。如果BookInfo表的
name字段是该表的主键,则需给BookInfo类的name属性添加“@PrimaryKey”与“@NonNull”两个注
解,表示该字段是个非空的主键。
//书籍信息
@Entity
public class BookInfo {
@PrimaryKey // 该字段是主键,不能重复
@NonNull // 主键必须是非空字段
private String name; // 书籍名称
private String author; // 作者
private String press; // 出版社
private double price; // 价格
public void setName(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public void setAuthor(String author) {
this.author = author;
}
public String getAuthor() {
return this.author;
}
public void setPress(String press) {
this.press = press;
}
public String getPress() {
return this.press;
}
public void setPrice(double price) {
this.price = price;
}
public double getPrice() {
return this.price;
}
}
2.编写图书信息表对应的持久化类
所谓持久化,指的是将数据保存到磁盘而非内存,其实等同于增删改等SQL语句。要在接口上添加@Dao注解
@Dao
public interface BookDao {
@Query("SELECT * FROM BookInfo") // 设置查询语句
List<BookInfo> queryAllBook(); // 加载所有书籍信息
@Query("SELECT * FROM BookInfo WHERE name = :name") // 设置带条件的查询语句
BookInfo queryBookByName(String name); // 根据名字加载书籍
@Insert(onConflict = OnConflictStrategy.REPLACE) // 记录重复时替换原记录
void insertOneBook(BookInfo book); // 插入一条书籍信息
@Insert
void insertBookList(List<BookInfo> bookList); // 插入多条书籍信息
@Update(onConflict = OnConflictStrategy.REPLACE)// 出现重复记录时替换原记录
int updateBook(BookInfo book); // 更新书籍信息
@Delete
void deleteBook(BookInfo book); // 删除书籍信息
@Query("DELETE FROM BookInfo WHERE 1=1") // 设置删除语句
void deleteAllBook(); // 删除所有书籍信息
}
3.编写图书信息表对应的数据库类
因为先有数据库然后才有表,所以图书信息表还得放到某个数据库里,这个默认的图书数据库要从
RoomDatabase派生而来,并添加“@Database”注解。
//entities表示该数据库有哪些表,version表示数据库的版本号
//exportSchema表示是否导出数据库信息的json串,建议设为false,若设为true还需指定json文件的保存路径
@Database(entities = {BookInfo.class},version = 1, exportSchema = false)
public abstract class BookDatabase extends RoomDatabase {
// 获取该数据库中某张表的持久化对象
public abstract BookDao bookDao();
}
4.在自定义的Application类中声明图书数据库的唯一实例
为了避免重复打开数据库造成的内存泄漏问题,每个数据库在App运行过程中理应只有一个实例,此时要求开发者自定义新的Application类,在该类中声明并获取图书数据库的实例,并将自定义的
Application类设为单例模式,保证App运行之时有且仅有一个应用实例。
public class MainApplication extends Application {
private final static String TAG = "MainApplication";
private static MainApplication mApp; // 声明一个当前应用的静态实例
// 声明一个公共的信息映射对象,可当作全局变量使用
public HashMap<String, String> infoMap = new HashMap<String, String>();
private BookDatabase bookDatabase; // 声明一个书籍数据库对象
// 利用单例模式获取当前应用的唯一实例
public static MainApplication getInstance() {
return mApp;
}
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "onCreate");
mApp = this; // 在打开应用时对静态的应用实例赋值
// 构建书籍数据库的实例
bookDatabase = Room.databaseBuilder(mApp, BookDatabase.class,"BookInfo")
.addMigrations() // 允许迁移数据库(发生数据库变更时,Room默认删除原数据库再创建新数据库。如此一来原来的记录会丢失,故而要改为迁移方式以便保存原有记录)
.allowMainThreadQueries() // 允许在主线程中操作数据库(Room默认不能在主
线程中操作数据库)
.build();
}
// 获取书籍数据库的实例
public BookDatabase getBookDB(){
return bookDatabase;
}
}
5.在操作图书信息表的地方获取数据表的持久化对象
// 从App实例中获取唯一的图书持久化对象
BookDao bookDao = MainApplication.getInstance().getBookDB().bookDao();