数据持久化就是指将那些内存中的瞬时数据保存到存储设备中,保证即使在手机或电脑关机的情况下,这些数据仍然不会丢失。保存在内存中的数据是处于瞬时状态的,而保存在存储设备中的数据是处于持久状态的,持久化技术则提供了一种机制可以让数据在瞬时状态和持久状态之间转换。
Android系统中主要提供了三种方式用于简单地实现数据持久化功能,即文件存储、SharePreference存储以及数据库存储。
文件存储是Android中最基本的一种数据存储方式,它不对存储的内容进行任何格式化处理,所有数据都是原封不动地保存到文件当中的,因而它比较适合用于存储一些比较简单的文本数据或者二进制数据。如果想使用文件存储的方式来保存一些较为复杂的文本数据,就需要定义一套自己的格式规范,方便之后将数据从文件中重新解析出来。
将数据存储到文件中
Context类中提供了一个openFileOutput()方法,可以用于将数据存储到指定的文件中,这个方法接收两个参数,第一个参数是文件名,在文件创建的时候使用的就是这个名称,这里指定的文件名不可以包含路径,因为所有的文件都是默认存储到/data/data//files/目录下的。第二个参数是文件的操作模式,主要有两种模式可选,MODE_PRIVATE和MODEL_APPEND。其中MODE_PRIVATE是默认的操作模式,表示当指定同样文件名的时候,所写人的内容将会覆盖源文件中的内容,而MODEL_APPEND则表示如果该文件已存在,就往文件里面追加内容,不存在就创建新文件。
String data="Data to save";
FileOutputStream out=null;
BufferedWriter writer=null;
try{
out=openFileOutput("data", Context.MODE_PRIVATE);
writer =new BufferedWriter(new OutputStreamWriter(out));
writer.write(data);
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
try {
if(writer!=null)
writer.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
从文件中读取数据
Context类中还提供了一个openFileInput()方法,用于从文件中读取数据,这个方法只接收一个参数,即要读取的文件名。然后系统会自动到/data/data//files/目录下去加载这个文件,并返回一个FileInputStream对象,得到这个对象之后再通过Java流的方式就可以将数据读取出来。
public String load(){
FileInputStream in=null;
BufferedReader reader=null;
StringBuilder content=new StringBuilder();
try{
in=openFileInput("data");
reader=new BufferedReader(new InputStreamReader(in));
String line="";
while((line=reader.readLine())!=null){
content.append(line);
}
}catch (IOException e){
e.printStackTrace();
}finally {
if(reader!=null){
try {
reader.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
Log.v("fileReader",content.toString());
return content.toString();
}
输入框加载存储的数据
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
edit=(EditText) findViewById(R.id.edit);
String inputText=load();
if (!TextUtils.isEmpty(inputText)) {
edit.setText(inputText);
edit.setSelection(inputText.length());
Toast.makeText(this,"restoring succeeded",Toast.LENGTH_SHORT).show();
}
}
对字符串进行非空判断的时候使用了TextUtils.isEmpty()方法,它可以一次性进行两种空值的判断。当传入的字符串等于null或者等于空字符串的时候,这个方法都会返回true。EditText的setText()方法将内容填充到EditText里,并调用setSelection()方法将输入光标移动到文本的末尾位置以便于继续输入。
SharedPreferences是使用键值对的方式来存储数据的。当保存一条数据的时候,需要给这条数据提供一个对应的键,这样在读取数据的时候就可以通过这个键把相应的值取出来。而且SharedPreferences还支持多种不同的数据类型存储,如果存储的数据类型是整型,那么读取出来的数据也是整型的;如果存储的数据是一个字符串,那么读取出来的数据仍然是字符串。
将数据存储到SharedPreferences中
获得SharedPreferences对象的方法:
getSharedPreferences()
方法,此方法接收两个参数,第一个参数用于指定SharedPreferences文件的名称,如果指定的文件不存在则会创建一个,SharedPreferences文件都是存放在/data/data//shared_prefs/目录下的。第二个参数用于指定操作模式,目前只有MODE_PRIVATE这一种模式可选,它是默认的操作模式,和直接传入0效果是相同的,表示只有当前的应用程序才可以对这个SharedPreferences文件进行读写。其他几种操作模式均以被废弃。getPreferences()
方法,该方法只接收一个操作模式参数,因为使用这个方法会自动将当前活动的类名作为SharedPreferences的文件名。getDefaultSharedPreferences()
方法。这是一个静态方法,它接收一个Context参数,并自动使用当前应用程序的包名作为前缀来命名SharedPreferences文件。得到了SharedPreferences对象之后,就可以开始向SharedPreferences文件中存储数据了。edit()
方法来获取一个SharedPreferences.Editor对象。putBoolean()
方法,添加一个字符串则使用putString()
方法apply()
方法将添加的数据提交,从而完成数据存储操作。 SharedPreferences.Editor se=getSharedPreferences("data",MODE_PRIVATE).edit();
se.putString("data","data");
se.apply();
从SharedPreferences中读取数据
SharedPreferences对象中提供了一系列的get方法,用于对存储的数据进行读取,每种get方法都对应了SharedPerferences.Editor中的一种put方法,比如读取一个布尔型数据就使用了getBoolean()方法,读取一个字符串就使用getString()方法。这些get方法接收两个参数,第一个参数是键,传入存储数据时使用的键就可以得到相应的值了;第二个参数是默认值,即表示当传入的键找不到对应的值时会以什么样的默认值进行返回。
SharedPreferences pref=getSharedPreferences("data",MODE_PRIVATE);
String data=pref.getString("data","");
Log.d("data",data);
内容提供器主要用于在不同的应用程序之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访数据的安全性。目前,使用内容提供器是Android实现跨程序共享数据的标准方式。内容提供器可以选择只对哪一部分进行共享,从而保证程序中的隐私数据不会有泄漏的风险。
需要在AndroidManifest.xml中添加权限声明。
这样用户在打开该程序时会给出提示,也可以清楚地知晓该程序一共申请了哪些权限。
运行时权限:可以在软件的使用过程中再对某一项权限申请进行授权。所有的权限可以分为两类,一类是普通权限,一类是危险权限。普通权限指的是那些不会直接威胁到用户的安全和隐私的权限,对于这部分的权限申请,系统会自动帮我们进行授权,而不需要用户去手动操作。危险权限则表示那些可能会触及用户隐私,或者对设备安全性造成影响的权限,如获取设备联系人信息。对于这部分权限申请,必须要由用户手动点击授权才可以,否则程序就无法使用相应的功能。
在程序运行时申请权限
使用CALL_PHONE这个权限作为本节的示例。CALL_PHONE这个权限是编写拨打电话功能的时候需要声明的,因为拨打电话会涉及到用户手机的资费问题,因此被列为危险权限。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button makeCall=findViewById(R.id.make_call);
makeCall.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(ContextCompat.checkSelfPermission(MainActivity.this,Manifest.permission.CALL_PHONE)!=PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(MainActivity.this,new String[]{Manifest.permission.CALL_PHONE},1);
}else {
call();
}
}
});
}
private void call(){
try {
Intent intent=new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);
}catch (SecurityException e){
e.printStackTrace();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode){
case 1:
if(grantResults.length>0&&grantResults[0]== PackageManager.PERMISSION_GRANTED){
call();
}else {
Toast.makeText(this,"you denied the permission",Toast.LENGTH_SHORT).show();
}
break;
default:
}
}
}
**运行时权限的核心就是在程序运行过程中由用户授权我们去执行某些危险操作,程序是不可以擅自做主去执行这些危险操作的。**首先要判断用户是否已经给过授权,借助的是ContextCompat.checkSelfPermission()
方法接收两个参数,第一个参数是Context,第二个参数是具体的权限名。将方法的返回值和PackageManager.PERMISSION_GRANTED作比较,相等就说明用户已经授权,不等就表示用户没有授权。如果没有授权的话,则需要调用ActivityCompat.requestPermissions()
方法来向用户申请授权
内容提供器的用法一般有两种,一种是使用现有的内容提供器来读取和操作相应程序中的数据,另一种是创建自己的内容提供器给我们程序的数据提供外部访问接口。
ContentResolver的基本用法
对于每一个应用程序来说,如果想要访问内容提供器中的共享的数据,就一定要借助ContentResolver类,可以通过Context中的getContentResolver()
方法来获取到该类的实例。ContentResolver中提供了一系列的方法对数据进行CRUD操作。其中insert()方法用于添加数据,update()用于更新数据,delete()用于删除数据,quert()用于查询数据。
读取手机的联系人
private void readContacts(){
Cursor cursor=null;
try{
//查询联系人数据
cursor =getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,null,null,null,null);
if(cursor!=null){
Log.d("read","succeed");
while (cursor.moveToNext()) {
//获取联系人姓名
String displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
//获取联系人手机号
String number = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
contactsList.add(displayName + "\n" + number);
}
adapter.notifyDataSetChanged();
}
}catch (Exception e){
e.printStackTrace();
}finally {
if(cursor!=null){
cursor.close();
}
}
}
如果想要实现跨程序共享数据的功能,官方推荐的方式是使用内容提供器,可以通过新建一个类去继承ContentProvider的方式来创建一个自己的内容提供器。
Content类中有六个抽象方法,需要全部重写。
onCreate()
初始化内容提供器的时候调用,通常在这里完成对数据库的创建和升级等操作,返回true表示内容提供器初始化成功,返回false则表示失败。query()
从内容提供器中查询数据,使用uri参数来确定查询哪张表,projection参数用于确定查询哪些列,selection和selectionArgs参数用于约束查询哪些行,sortOrder参数用于对结果进行排序,查询的结果存放在Cursor对象中返回。insert()
向内容提供器添加一条数据。使用uri参数来确定要添加到的表,待添加的数据保存在values参数中。添加完成后,返回一个用于表示这条新纪录的URI。update()
更新内容提供器已有的数据。使用uri参数来确定要更新到哪一张表中的数据,新数据保存在values参数中,selection和selectionArgs参数用于约束更新哪些行,受影响的行数将作为返回值返回。delete()
从内容提供器中删除数据。使用uri参数来确定删除哪一张表中的数据,selection和selectionArgs参数用于约束删除哪些行,被删除的行数将作为返回值返回。getType()
根据传入的内容URI来返回相应的MIME类型。addURI()
方法。 private static UriMatcher uriMatcher;
static{
uriMatcher=new UriMatcher(uriMatcher.NO_MATCH);
uriMatcher.addURI("com.example.app.provider","table1",0);
}
调用UriMatcher的match()
方法,可以将一个Uri对象传入,返回值是某个能够匹配这个Uri对象所对应的自定义代码。
getType()
,它是所有的内容提供器都必须提供的一个方法,用于获取Uri对象所对应的MIME类型。一个内容URI所对应的MIME字符串主要由3部分组成。
发现许多方法已经过时,还是决定看官方API。
官方API地址