这一天,关羽正在屋里聚精会神的写代码。连日来的魔鬼特训搞得他已经有些疲惫,胡子也好几天没有修整过,有几根胡子已经黏在一起打成了结。但他搞开发的兴致却越来越浓。做开发难道不正是这样吗,过程虽然看似有些枯燥、乏味,但真正入迷了之后却好似慢慢品尝一杯醇厚的美酒一样,妙趣横生,回味无穷。当看到自己写的代码在手机上成功的跑起来时,有一种喜悦会自心底迸发出来,让人情不自禁手舞足蹈,笑逐颜开,就好像又回到了小时候打游戏通关时的时候。
入迷。这就是关羽保持高效编程状态的秘诀。像老僧入定一般,浑然忘我,摒除一切杂念,沉浸在代码的怀抱当中……
“二哥!看啥呢!”张飞用他那熊掌往关羽肩膀上狠狠一拍,大声问道,另一只手上拿着不知又从哪里搞来的啃到一半的鸡腿。
这一拍把关羽拍的眼冒金星,头晕目眩,差点昏死过去。一时半会竟答不上来。
“二哥!二哥!你这是肿么了!你咋眼神这么热血?看的老飞我好迷茫,要不……你吃鸡腿?”
关羽当时就想学刘备惯用的招式把鸡腿糊到张飞脸上,可惜一口真气提不上来,手脚酸软无力,只好作罢,答道:“算了,鸡腿就不用了,三弟你咋忽然出现到我身后?”
张飞说:“我最近在编程中遇到一些问题,孔明好像在睡觉,大哥又不知道哪去了,所以过来跟二哥商量探讨一下。”
关羽说:“哦,是何问题?”
张飞说:“嗯,是关于Android存储方面的一些问题。我感觉Android的数据存储方式有很多种,各有不同。不知道应该怎么挑选这些存储方式。”
关羽说:“从三弟的嘴里说出这么严肃的术语我真是好不适应……”
张飞说:“二哥,请不要讽刺我的人格!”说完又啃了一口鸡腿。
关羽说:“正好我从孔明那里借了一本Android数据存储。三弟不如和我来一起研究一下!”
1.1. Android数据存取介绍
数据存取的是应用和系统中静止的数据,这些数据包括在应用程序运行过程中产生的临时文件或用到的信息。作为一名开发人员,不可避免地需要存储应用程序所需的各种数据。在Android中,所有的应用软件数据为该应用软件所私有。Android同样也提供了一种标准方式供应用软件将私有数据开放给其他应用软件。本章重点介绍Android所提供的以下5种数据存取的机制:内部存取、SD卡存取、资源文件读取、Preferences存取和内容提供器(Content Provider)。
1.2. Android内部存取
一般来说,当需要存储数据的时候,首先想到的是将数据存储在内存当中,这样存取方便,效率高。在手机上Android也提供了内存机制,用于将数据直接保存在设备的内部存储器中。Android系统默认保存到内部存储器上的文件是应用程序私有,其他应用无法访问。当用户卸载你的应用时,这些文件将会被移除。
孔明:手机内存分为两类,一种是运行内存(ram),另一种是非运行内存(rom),将软件装到手机上占用的就是非运行内存。本节所指的内部存取主要是指对非运行内存上所进行的数据存取。
|
下面将通过一个实例讲解如何获取系统的内存信息和操作内存文件,该例子通过存储一个字符串减少了可用的内存,实例的运行结果如图13-1所示:
图13-1内存存储操作
新建一个Activity,命名为MemoryActivity,具体代码如下所示:
MemoryActivity.java代码清单13-2-0:
/**
*@author张飞:旅游是指从自己活腻的地方去别人活腻的地方。
*/
public class MemoryActivity extendsActivity {
// 显示手机总内存和可用内存信息
private TextView memoryTextView = null;
// 显示存储文件内容
private TextView showTextView = null;
// 存储文件按钮
private Button saveButton = null;
// 存储文件按钮的监听
private OnClickListener saveButtonListener = null;
// 读取文件内容按钮
private Button loadButton = null;
// 读取文件内容按钮的监听
private OnClickListener loadButtonListener = null;
// 输入存储数据
private EditText dataEditText = null;
// 内存文件名称
private String fileName = "zhangfei.txt";
// 系统内存信息文件
private String infoSystem ="/proc/meminfo";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.memory);
// 初始化监听事件
setListener();
// 初始化控件、监听器
findView();
}
/**
* @author张飞:娃哈哈,把控件初始化放一个方法里,我好厉害呀
*/
private void findView() {
memoryTextView =(TextView)findViewById(R.id.memoryTextView);
memoryTextView.setText("手机总内存: " + this.getTotalMemory() + ", "+ "可用内存: "
+ this.getAvailMemory());
showTextView =(TextView)findViewById(R.id.showTextView);
saveButton =(Button)findViewById(R.id.saveButton);
loadButton =(Button)findViewById(R.id.loadButton);
saveButton.setOnClickListener(saveButtonListener);
loadButton.setOnClickListener(loadButtonListener);
dataEditText =(EditText)findViewById(R.id.dataEditText);
}
/**
* @author张飞:嘿咻咻,把按钮监听都放在一个方法里,我不得了的厉害了呀。哈哈,明天就去找大哥,告诉他我的威武
*/
private void setListener() {
saveButtonListener = new OnClickListener() {
@Override
public void onClick(View v) {
// 保存信息方法
save();
}
};
loadButtonListener = new OnClickListener() {
@Override
public void onClick(View v) {
// 读取信息方法
load();
}
};
}
/**
* @author张飞:哈哈,获取android当前可用内存大小,我都会玩内存了
* @return
*/
private String getAvailMemory() {
// 获得ActivityManager
ActivityManager am =(ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
MemoryInfo mi = newMemoryInfo();
//获取系统可用内存信息,数据封装在mi对象中
am.getMemoryInfo(mi);
// mi.availMem; 当前系统的可用内存
// 将Byte转换为KB或者MB,内存大小规格化
return Formatter.formatFileSize(getBaseContext(), mi.availMem);
}
/**
* @author张飞:哈哈,获取android总内存大小,你害怕的颤抖了吧,少年.
* @return
*/
private String getTotalMemory() {
// 系统内存信息文件
String str;
String[] arrayOfString;
longinitial_memory = 0;
try {
FileReader localFileReader = newFileReader(infoSystem);
BufferedReader localBufferedReader = new BufferedReader(localFileReader,8192);
// 读取meminfo第一行,系统总内存大小
str =localBufferedReader.readLine();
// 按空格分隔字符串,存入数组
arrayOfString =str.split("\\s+");
// 获得系统总内存,单位是KB,乘以1024转换为Byte
initial_memory = Integer.valueOf(arrayOfString[1]).intValue()* 1024;
localBufferedReader.close();
} catch (IOException e){
}
// Byte转换为KB或者MB,内存大小规格化
return Formatter.formatFileSize(getBaseContext(), initial_memory);
}
/**
* @author张飞:保存用户输入的内容到文件,无敌
*/
private void save() {
String content = dataEditText.getText().toString();
// 检查是否有数据
if (content == null|| content.length() == 0) {
Toast.makeText(MemoryActivity.this, "没有数据不要瞎点,会崩溃的!",Toast.LENGTH_LONG).show();
return;
}
try {// 获得输出流
FileOutputStream outputStream =openFileOutput(fileName, Activity.MODE_PRIVATE);
outputStream.write(content.getBytes());
outputStream.flush();
outputStream.close();
Toast.makeText(MemoryActivity.this,"保存成功", Toast.LENGTH_LONG).show();
//刷新可用内存情况
memoryTextView.setText("手机总内存: " +getTotalMemory() + ", " + "可用内存: "
+getAvailMemory());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e){
e.printStackTrace();
}
}
/**
* @author张飞:读取文件获得用户输入的内容,厉害
*/
private void load() {
try {
// 获得文件输入流
FileInputStream inputStream = this.openFileInput(fileName);
BufferedReader br = new BufferedReader(newInputStreamReader(inputStream));
String content = "";
String data = null;
while ((data = br.readLine()) != null) {
content += data;
}
showTextView.setText(content);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e){
e.printStackTrace();
}
}
}
该MainActivity创建了zhangfei.txt文件,将输入的字符串存放到这个文件中,从而减小了可用的内存。在getAvailMemory()方法中,代码通过调用getSystemService(Context.ACTIVITY_SERVICE)方法获得一个ActivityManager实例,通过ActivityManager的getMemoryInfo()方法就可以获取当前的内存信息。另外可以通过读取“/proc/meminfo”文件,获得了内存总大小。
1.2.1. 获取内存信息
在Android开发中,有时候我们想要获取手机的一些硬件信息,比如Android手机的总内存和可用内存大小。通过读取文件“/proc/meminfo”我们能够获取手机内存的总量,而通过“ActivityManager.getMemoryInfo(ActivityManager.MemoryInfo)”方法可以获取当前的可用内存。从图13-1上我们可以看到当前手机总内存为91.89MB,可用内存由26.86MB减小至26.29MB。
meminfo文件
“/proc/meminfo”文件记录了Android手机的一些内存信息,meminfo文件的内容如下表13-1所示:
表13-1 meninfo文件字段
字段 | 解释 |
MemTotal | 所有可用RAM大小。 |
MemFree | 被系统预留,未使用的内存。 |
Buffers | 用来做缓冲区域的大小。 |
Cached | 高速缓冲存储器(cache memory)使用的内存大小。 |
SwapCached | 高速缓冲存储器使用的交换空间大小。已经被交换出来的内存,仍然被存放在swapfile中,用来在需要的时候很快的被替换而不需要再次打开I/O端口。 |
Active | 在活跃使用中的缓冲或高速缓冲存储器页面文件的大小,除非非常必要,否则不会被移作他用。 |
Inactive | 在不经常使用中的缓冲或高速缓冲存储器页面文件的大小,可能被用于其他途径。 |
SwapTotal | 交换空间的总大小。 |
SwapFree | 未被使用交换空间的大小。 |
Dirty | 等待被写回到磁盘的内存大小。 |
Writeback | 正在被写回到磁盘的内存大小。 |
AnonPages | 未映射页的内存大小。 |
Mapped | 设备和文件等映射的大小。 |
Slab | 内核数据结构缓存的大小,可以减少申请和释放内存带来的消耗。 |
SReclaimable | 可收回Slab的大小。 |
SUnreclaim | 不可收回Slab的大小(SUnreclaim+SReclaimable=Slab)。 |
PageTables | 管理内存分页页面的索引表的大小。 |
NFS_Unstable | 不稳定页表的大小。 |
由表13-1可以看出要获取Android手机总内存大小,只需读取“/proc/meminfo”文件的第1行,并进行简单的字符串处理即可。关键代码如下所示:
FileReader localFileReader= new FileReader(infoSystem);
BufferedReader localBufferedReader = new BufferedReader(localFileReader,8192);
// 读取meminfo第一行,系统总内存大小
str =localBufferedReader.readLine();
// 按空格分隔字符串,存入数组
arrayOfString =str.split("\\s+");
// 获得系统总内存,单位是KB,乘以1024转换为Byte
initial_memory= Integer.valueOf(arrayOfString[1]).intValue()* 1024;
localBufferedReader.close();
张飞:大哥,我们还可以通过读取“/proc/cupinfo”来获取android手机的CPU参数,通过读取“/proc/stat”文件来计算CPU的使用率。
|
ActivityManager
ActivityManager在Android操作系统中有重要的作用。ActivityManager为系统中所有运行着的Activity提供了接口。通过ActivityManager可以获取运行中的进程信息、任务信息、服务信息等。可以用ActivityManager获取当前可用内存信息MemoryInfo。
张飞:大哥,常用的静态内部类除了系统可用内存信息ActivityManager.MemoryInfo外,还有最近的任务信息ActivityManager.RecentTaskInfo、正在运行的任务信息ActivityManager.RunningTaskInfo、正在运行的进程信息ActivityManager.RunningAppProcessInfo和正在运行的服务信息ActivityManager.RunningServiceInfo。 |
1.2.2. 内存文件操作
Activity主要提供了openFileOutput()方法用于把数据输出到“/data/data/<package name>/files”目录文件中,openFileInput()方法用于打开存放在“/data/data/<package name>/files”目录下的应用程序的私有文件。
openFileOutput(string,int)方法的第一参数用于指定文件名称,不能包含路径分隔符“/”,如果文件不存在,Android 会自动创建它。在DDMS的File Explorer视图中展开“/data/data/<package name>/files”目录就可以看到该文件,如图13-2所示:
图13-2内存存储位置
openFileOutput()方法的第二参数用于指定操作模式,有四种模式,分别为:
l Context.MODE_PRIVATE:默认操作模式,代表该文件是私有数据,只能被应用程序本身访问,在该模式下,写入的内容会覆盖原文件的内容。
l Context.MODE_APPEND:该模式会检查文件是否存在,存在就往文件追加内容,否则就创建新文件。
l Context.MODE_WORLD_READABLE和Context.MODE_WORLD_WRITEABLE用来控制其他应用是否有权限读写该文件。MODE_WORLD_READABLE:表示当前文件可以被其他应用读取;MODE_WORLD_WRITEABLE:表示当前文件可以被其他应用写入。
例如文件允许被其他应用读和写,可以传入:openFileOutput("itcast.txt",Context.MODE_WORLD_READABLE+Context.MODE_WORLD_WRITEABLE)。
张飞:大哥,Activity还提供了getCacheDir()方法用于获取“/data/data/<package name>/cache”目录和getFilesDir()方法用于获取“/data/data/<package name>/files”目录。 |
1.3.Android SD卡存储
手机的内存空间一般来说都不是很大,随着应用程序所需资源的增加,内部存储器的存储空间就越发的不够用。应用程序可以将常用的配置文件或者重要文件放入到内部存储器中,而对高清图片、视频文件,可以把它们放到SD卡中。SD卡可以看作是手机的外部存储器,在手机中充当移动硬盘或U盘的角色。对于存储设备,不可避免的需要经常查看剩余空间、操作存储设备里的文件,下面将通过一个实例讲解如何操作SD卡和获取系统的SD卡信息,运行结果如图13-3所示:
图13-3 SD卡存储操作
大哥还不知道我小飞飞是怎么实现SD卡的读取的,这是我的小秘密。其实主要功能实现的源代码就是下面的SdcardActivity文件。
SdcardActivity.java代码清单13-2:
/**
*@author张飞:天没降大任于我,照样苦我心智,劳我筋骨,逼我编程。
*/
public class SdcardActivity extendsActivity {
// 显示手机SD卡总内存和可用内存信息
private TextView sdcardTextView = null;
// 显示存储文件内容
private TextView showTextView = null;
// 存储文件按钮
private Button saveButton = null;
// 存储文件按钮的监听
private OnClickListener saveButtonListener = null;
// 读取文件内容按钮
private Button loadButton = null;
// 读取文件内容按钮的监听
private OnClickListener loadButtonListener = null;
// 输入存储数据
private EditText dataEditText = null;
// sdcard文件名称
private String fileName = "zhangfei.txt";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.sdcard);
setListener();
findView();
}
/**
* @author张飞:娃哈哈,把控件初始化放一个方法里,我好厉害呀
*/
private void findView() {
sdcardTextView = (TextView)findViewById(R.id.sdcardTextView);
// 获取sdcard存储信息,并显示
sdCardSize();
showTextView = (TextView)findViewById(R.id.showTextView);
saveButton= (Button)findViewById(R.id.saveButton);
loadButton= (Button)findViewById(R.id.loadButton);
saveButton.setOnClickListener(saveButtonListener);
loadButton.setOnClickListener(loadButtonListener);
dataEditText = (EditText)findViewById(R.id.dataEditText);
}
/**
* @author张飞:嘿咻咻,把按钮监听都放在一个方法里,我不得了的厉害了呀
*/
private void setListener() {
saveButtonListener = newOnClickListener() {
@Override
public void onClick(View v) {
// 保存信息方法
saveToSDCard();
}
};
loadButtonListener = newOnClickListener() {
@Override
public void onClick(View v) {
// 读取信息方法
loadFromSDCard();
}
};
}
/**
* @author张飞:SD卡总容量和剩余容量获取,我进步太快了,会骄傲的
*/
private void sdCardSize() {
// 取得SD卡当前的状态
String sDcString = android.os.Environment.getExternalStorageState();
if (sDcString.equals(android.os.Environment.MEDIA_MOUNTED)){
// 取得SD卡文件路径
File pathFile = android.os.Environment.getExternalStorageDirectory();
android.os.StatFs statfs= newandroid.os.StatFs(pathFile.getPath());
// 获取SD卡上BLOCK总数
longnTotalBlocks = statfs.getBlockCount();
// 获取SD卡上每个block的SIZE
longnBlocSize = statfs.getBlockSize();
// 获取可供程序使用的Block的数量
longnAvailaBlock = statfs.getAvailableBlocks();
// 获取剩下的所有Block的数量(包括预留的一般程序无法使用的块)
longnFreeBlock = statfs.getFreeBlocks();
// 计算SD卡总容量大小B
longnSDTotalSize = nTotalBlocks * nBlocSize;
// 计算 SD卡剩余大小B
longnSDFreeSize = nAvailaBlock * nBlocSize;
sdcardTextView.setText("手机SD总存储: " + nSDTotalSize+ "B, " + "可用存储: " + nSDFreeSize + "B");
}
}
/**
* @author张飞:文件名,内容,一个也不能少,写成独立的方法形式,可用性变大了,我骄傲了
*/
public void saveToSDCard() {
try {
String content =dataEditText.getText().toString();
// 检查是否有数据
if (content == null|| content.length() == 0) {
Toast.makeText(SdcardActivity.this, "没有数据不要瞎点,会崩溃的!",Toast.LENGTH_LONG).show();
return;
}
// SD卡路径在2.2以前是/sdcard,在2.2以上版本中是/mnt/sdcard,最好采用下面的方式灵活获取,适用于所有版本
File file = new File(Environment.getExternalStorageDirectory(),fileName);
// 文件输出流
FileOutputStream fos;
fos = new FileOutputStream(file);
// 写入数据
fos.write(content.getBytes());
// 关闭输出流
fos.close();
sdCardSize();
Toast.makeText(SdcardActivity.this, "保存成功",Toast.LENGTH_LONG).show();
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* @author张飞:文件名一个就可以,写成独立的方法形式,可用性变大了,我骄傲two
*/
public void loadFromSDCard() {
try {
// 文件输入流
File file = new File(Environment.getExternalStorageDirectory(),fileName);
FileInputStream fis = new FileInputStream(file);
byte[] buf = newbyte[1024];
intlen = 0;
ByteArrayOutputStream baos= new ByteArrayOutputStream();
// 读取数据
while ((len = fis.read(buf)) != -1) {
baos.write(buf, 0, len);
}
byte[] data =baos.toByteArray();
// 关闭流
baos.close();
fis.close();
showTextView.setText(newString(data));
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
该SdCardActivity创建了zhangfei.txt文件,将输入的字符串存放到这个文件中,占用了SD卡的存储空间。其中,saveToSDCard()方法使用输出流,将文字保存到SD卡的文件中。loadFromSDCard()方法使用输入流,将SD卡中的文件内容读入到应用程序中。需要注意的是,存储到SD卡中的文件可以被其他任何应用程序使用。在使用SD卡之前,需要使用android.os.Environment.getExternalStorageState()方法判断SD卡的状态。
1.3.1.获取SD卡信息
SD卡作为手机的扩展存储设备,可以让我们手机存放更多的数据和多媒体等大体积文件,在Android开发中,我们经常需要查看SD卡的可用空间。要获取SD卡上面的信息,必须先对SD卡有访问的权限,因此第一件事就是需要在AndroidManifest.xml中添加访问扩展设备的权限,如下所示:
<!-- 在SD卡中创建与删除文件权限 -->
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
<!-- 往SD卡写入数据权限 -->
<uses-permissionandroid:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
Android提供Environment工具类用于访问环境变量,StatFs类用于获取文件系统的状态、SD卡的大小和剩余空间、系统内部空间等。通过这两个类就可以判断手机上面SD卡是否插好、获取SD卡的文件路径,SD卡的总空间大小和空闲空间大小。下面就将详细地介绍这两个类。
Environment类
Environment类常用的判断SD卡状态的常量和获取SD卡目录的方法如下表13-1所示:
表13-1 Environment方法
常量和方法 | 解释 |
MEDIA_BAD_REMOVAL | 表明SD卡被卸载前己被移除。 |
MEDIA_CHECKING | 表明对象正在磁盘检查。 |
MEDIA_MOUNTED | 表明对象是否存在并具有读/写权限。 |
MEDIA_MOUNTED_READ_ONLY | 表明对象权限为只读。 |
MEDIA_NOFS | 表明对象为空白或正在使用不受支持的文件系统。 |
MEDIA_REMOVED | 如果不存在SD卡返回。 |
MEDIA_SHARED | 如果SD卡未安装,并通过USB和计算机连接返回。 |
MEDIA_UNMOUNTABLE | 有SD卡,但无法装载。 |
MEDIA_UNMOUNTED | 有SD卡,但未挂载。 |
getDataDirectory() | 获取 Android 数据目录。 |
getDownloadCacheDirectory() | 获取 Android 下载/缓存内容目录。 |
getExternalStorageDirectory() | 获取外部存储目录。 |
getExternalStoragePublicDirectory(String type) | 取一个高端的公用的外部存储器目录来摆放某些类型的文件。 |
getExternalStorageState() | 获取外部存储设备的当前状态。 |
getRootDirectory() | 获取 Android 的根目录。 |
StatFs类
StatFs是一个模拟linux检查文件系统的磁盘空间占用情况命令的类,可以获得SD卡和手机内存的使用情况。StatFs获取的都是以block为单位的,一般SD卡都是fat32的文件系统,blocksize是4096字节。StatFs主要方法如下表13-2所示:
表13-2 StatFs方法
方法 | 解释 |
getAvailableBlocks() | 获取当前可用的存储空间。 |
getBlockCount() | 获取该区域可用的文件系统数。 |
getBlockSize() | 获取SD卡上每个block的SIZE。 |
getFreeBlocks() | 块区域剩余的空间。 |
restat(String path) | 执行一个由该对象所引用的文件系统。 |
1.3.2.SD卡文件操作
对SD卡文件的读取操作和内部存储器的文件操作其实是相似的,不同的是需要修改的输入输出的路径。这主要得力于Java输入输出流的设计。SD卡路径在2.2以前是“/sdcard”,在2.2以上版本中是“/mnt/sdcard”,采用Environment.getExternalStorageDirectory()方式,适用于所有版本。如图13-4所示,程序将应用中的数据存放在SD卡下的zhangfei.txt文件中。
图13-4 SD卡存储位置
对于SD卡文件夹的permissions描述是“d---rwxr-x”, d代表文件夹。拥有者的权限是“---”,组群权限是“rwx”,其他人权限是“r-x”(r:读w:写x:执行)。
张飞:想要遍历SD卡目录,查找文件,可以使用Environment.getExternalStorageDirectory()方法结合File类的listFiles()方法实现噢,我小飞飞是不是很厉害噢? |
1.4. 资源File读取
一个优秀的Android应用程序会用到各种资源文件,例如,音乐、图片、字符串等。平时接触最多的资源目录一般为以下三个:/res/drawable、/res/values、/res/layout。细心的你可能已经注意文件夹不同,所放的资源也不相同。其实在建立Android工程时,系统就为我们自动规划了两个目录文件,一个叫res,另一个叫assets。res目录下存放的资源文件,可以通过Android自动产生的索引文件类R直接访问。在res目录下通常又可细分为anim(动画)、drawable(图片)、layout(布局文件)、menu(菜单)、raw(原生文件)、values(常量)和xml(xml文件)。assets中保存的一般是原生文件,如MP3文件、字体文件,Android程序不能直接访问,需要通过AssetManage类以二进制形式进行读取。
1.4.1. raw文件读取
/res/drawable、/res/values、/res/layout中的文件会被映射到R.java文件中,同样/res/raw中的文件也会被映射到R.java中。虽然/res/raw中的文件不会被aapt(aapt是标准的Android辅助打包工具)处理成为二进制文件,但是它的文件还是被映射到R.java中,方便以资源id形式来访问/res/raw目录下的文件。
张飞:/res/中除了SDK支持的folder外,不能再有子目录,虽不会有编译错误,但是子目录会被完全忽略,如在/res/layout下在建一个子目录activity(/res/layout/acitivity/,那么你在生成的R.java中将看不到activity和其目录下的内容。 |
两种常见访问/res/raw的关键代码如下所示:
// 设置使用R文件访问
imageView.setImageResource(R.raw.zhangfei);
// 使用getResources()获取到Resources类,使用openRawResource方法获取到输入流
InputStream is=getResources().openRawResource(R.raw.filename);
第一种直接使用R文件下的资源id来对资源进行引用。第二种方式通过openRawResource()方法获取/res/raw目录下资源的输入流。
关羽:三弟这么努力,我也不能落后呀。res定义或提供资源时,需要注意同一个类型或同一文件夹下面的资源不可以使用相同的文件名,也就是说不能用文件扩展名来区别不同的文件。因为R.java中只保留资源的文件名而不管扩展名,所以如果有二个图片一个是icon.png另一个是icon.jpg,那么在R.java中只会有一个R.drawable.icon,另外一个则会无法访问到。 |
1.4.2. assets文件读取
不像/res/raw目录,assets目录可以有子目录。assets目录中的文件不会被编译成二进制形式,可以通过文件名获取输入输出流来进行读写操作,而不是资源id。一般将比较大的文件放入到assets中,进行比较耗时的资源操作。常见操作assets文件夹资源的代码如下:
AssetManager am= getResources().getAssets();
InputStream is= am.open("zhangfei2.jpg");
AssetManager提供访问应用程序的assets文件的一种方式。AssetManager允许打开和读取资源作为一个输入输出流。常见方法有AssetManager.open(StringfileName):打开assets目录下的文件;AssetManager.list(String path):获取assets目录下所有文件夹和文件的名称。
1.4.3. xml文件读取
/res/xml可以用来存储xml格式的文件。和其他资源文件一样,该目录下的xml资源会被编译成二进制格式的文件并放到最终的APK安装包里。可以通过R类来访问xml资源。常见的xml解析方式有三种:DOM、SAX、Pull。Android系统中推荐使用Pull,Pull解析器是一个开源的Java项目,Android系统内部解析xml文件均为此种方式。Android SDK中已经集成了Pull解析器,无需添加任何jar文件。Android中一般使用XmlResourceParser类解析xml资源,代码如下:
XmlResourceParserxml= getResources().getXml(R.xml.data);
XmlResourceParser继承了2个接口:XmlPullParser和AttributeSet。其中XmlPullParser定义了Android xml解析框架。它可以建立xml文档的结构模型,并根据需要添加一个事件监听器。AttributeSet接口实际上是一个抽象的对象,它并没有定义实例变量或字段,只定义了一系列的访问xml元素属性的方法。使用XmlResourceParser读取xml的一般的步骤为:
1. Xml.newPullParser() 获得解析器。
2. parser.setInput(in,"UTF-8") 设置输入流以及编码。
3. parser.next() 获取下一个解析事件,得到一个事件代码。
4. 利用XmlPullParser中定义的常量来标识各种解析事件。
1.4.4. File读取实例
本节将通过一个实例介绍如何操作/res/xml、/res/raw和/assets中的资源文件。首先在/res/raw文件加下放入图片文件zhangfei.jpg,/assets资源文件下放入图片文件zhangfei2.jpg,/res/xml目录下放入data.xml文件,如图13-5所示:
图13-5 添加资源文件
然后添加按钮、增加按钮点击事件、点击按钮并读取对应资源,运行结果如图13-6所示:
图13-6 读取资源结果
这回,我小飞飞可出尽了风头。在大哥二哥的苦苦哀求之下,我就告诉他们实现上面13-6的源码ResFileActivity是怎么写的吧。
ResFileActivity.java代码清单13-4-4:
/**
*@author张飞:西游记告诉我们:凡是有后台的妖怪都被接走了,凡是没后台的都被一棒子打
死了。
*/
public class ResFileActivity extendsActivity {
// 显示资源文件的图像
private ImageView imageView;
// 引导到raw数据显示按钮的监听器
private OnClickListener rawButtonListener = null;
// raw图片资源显示的按钮
private Button rawButton;
// 引导assets数据显示的按钮的监听器
private OnClickListener assetsButtonListener = null;
// assets数据显示的按钮
private Button assetsButton;
// 引导到xml文件数据的按钮监听器
private OnClickListener xmlButtonListener = null;
// 引导到xml数据读取的按钮
private Button xmlButton;
// 显示xml数据
private TextView xmlTextView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.resfile);
setListener();
findView();
}
/**
* @author张飞:娃哈哈,把控件初始化放一个方法里,我好厉害呀
*/
private void findView() {
imageView =(ImageView)findViewById(R.id.ImageView);
rawButton =(Button)findViewById(R.id.rawButton);
assetsButton =(Button)findViewById(R.id.assetsButton);
xmlButton =(Button)findViewById(R.id.xmlButton);
rawButton.setOnClickListener(rawButtonListener);
assetsButton.setOnClickListener(assetsButtonListener);
xmlButton.setOnClickListener(xmlButtonListener);
xmlTextView =(TextView)findViewById(R.id.xmlTextView);
}
/**
* @author张飞:嘿咻咻,把按钮监听都放在一个方法里,我不得了的厉害了呀
*/
private void setListener() {
rawButtonListener = new OnClickListener() {
@Override
public void onClick(View v) {
// 保存信息方法
loadFromRaw();
}
};
assetsButtonListener = new OnClickListener() {
@Override
public void onClick(View v) {
// 读取信息方法
loadFromAssets();
}
};
xmlButtonListener = new OnClickListener() {
@Override
public void onClick(View v) {
// 读取信息方法
loadFromXml();
}
};
}
/**
* @author张飞:从xml文件里面读取数据
*/
protected void loadFromXml() {
try {
XmlResourceParser xml =getResources().getXml(R.xml.data);
xml.next();
// 产生第一个事件
inteventType =xml.getEventType();
booleanisData = false;
// XmlPullParser.END_DOCUMENT是否是结束事件
while (eventType != XmlPullParser.END_DOCUMENT) {
// 到达data节点时标记一下
if (eventType == XmlPullParser.START_TAG) {
if (xml.getName().equals("data")) {
isData = true;
}
}
// 如过到达标记的节点则取出内容
if (eventType == XmlPullParser.TEXT && isData) {
xmlTextView.setText(xml.getText());
}
xml.next();
// 产生下一个事件
eventType = xml.getEventType();
}
} catch (XmlPullParserException e) {
e.printStackTrace();
} catch (IOException e){
e.printStackTrace();
}
}
/**
* @author张飞:从Assets文件里面读取图片
*/
protected void loadFromAssets(){
Bitmap image = null;
// 获取Assets管理器
AssetManager am = getResources().getAssets();
try {
// 获取Assets输入流
InputStream is = am.open("zhangfei2.jpg");
image = BitmapFactory.decodeStream(is);
is.close();
} catch (IOException e){
e.printStackTrace();
}
imageView.setImageBitmap(image);
}
/**
* @author张飞:从Raw文件里面读取图片
*/
protected void loadFromRaw() {
// 设置raw为imageView的背景图片
imageView.setImageResource(R.raw.zhangfei);
// InputStreamis=getResources().openRawResource(R.raw.zhangfei2);
}
}
该代码的loadFromRaw()方法通过R类获取R.raw.zhangfei图片,也可以通过openRawResource()方法获得R.raw.zhangfei2图片的输入流。loadFromXml()方法通过XmlResourceParser获取了xml文件里面的“我是张飞,我帅气handsome!”字符串资源。loadFromAssets()方法使用了AssetManager.open()方法获取到了zhangfei2.jpg图片的输入流。
1.5.Preferences存取
Preperences 是一种应用程序内部轻量级的数据存储方案,主要用于存储和查询简单的数据类型,包括boolean、int、float、long以及 String 等。存储方式以键值对的形式存放在应用程序 /data/data/package_name /shared_prefs目录下。Preferences 一般用来存储应用程序的设置信息,如色彩方案、文字字体等。在应用程序中获取Preferences有如下两种方式:
第一,调用Context的getSharedPreferences(Stringname, int mode)方法获得SharedPreferences 对象。其中name表示SharedPreferences的名称,如果不存在则创建一个以name为名的新的SharedPreferences文件。mode表示打开的模式,主要有 PRIVATE(私有)、 MODE_WORLD_READABLE (可读)和 MODE_WORLD_WRITEABLE(可写)。
张飞:如果应用程序想支持外部应用程序对其SharedPreferences既可读又可写,需要加入MODE_WORLD_READABLE|MODE_WORLD_WRITEABLE。 |
第二,调用Activity对象的getPreferences(intmode)方法获得 SharedPreferences对象。mode参数和上一方法getSharedPreferences(String name, intmode)的mode参数一样。
两种获得SharedPreferences的最大的不同是,通过 Context的getSharedPreferences方法获得的SharedPreferences可以被同一应用程序下的其他组件共享。而用Activity的getPreferences 方法获得的SharedPreferences只能被该方法所在的Activity使用。
SharedPreferences 对象提供了一系列的 get 方法用于接收键返回对应的值。如果需要对Preferences 文件中存储的键值进行修改,首先需要调用 SharedPreferences的 edit 方法获得一个Editor 对象,该对象可以用来修改 Preferences 文件中存储的内容。
下面通过一个小例子来说明 Preferences 的用法,运行结果如图13-7。从运行结果上可以看出第一次运行程序没有任何数据,然后再次启用应用程序的时候就会发现上次输入的值显示在文本框上。
图13-7Preperences存储
大哥、二哥看我现在Android水平提高这么快,各种羡慕嫉妒恨!那想想我小飞飞的智商那也是到达了222的程度,必须提高的快呀。看在多年兄弟的情分上,就告诉他们PreferenceActivity是怎么写的吧。
PreferenceActivity.java代码清单13-5-0:
/**
*@author张飞:念了十几年书,想起来还是幼儿园比较好混!
*/
public class PreferenceActivity extends Activity {
// 存储文件按钮
private Button saveButton = null;
//存储文件按钮的监听
private OnClickListener saveButtonListener = null;
//读取文件内容按钮
private Button loadButton = null;
//读取文件内容按钮的监听
private OnClickListener loadButtonListener = null;
//输入姓名存储数据
private EditText nameEditText = null;
//输入智商存储数据
private EditText iqEditText = null;
//Preferences对象
private SharedPreferences settings;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.preference);
setListener();
findView();
// 装载数据,取得活动的Preferences对象
settings =getPreferences(Activity.MODE_PRIVATE);
// 检查SharedPreferences是否已经存储了数据
init();
}
/**
* @author张飞:如果有数据,我就取出来,真是棒
*/
private void init() {
String name =settings.getString("name", "");
String iq =settings.getString("IQ", "");
nameEditText.setText(name);
iqEditText.setText(iq);
}
/**
* @author张飞:娃哈哈,把控件初始化放一个方法里,我好厉害呀
*/
private void findView() {
saveButton= (Button)findViewById(R.id.saveButton);
saveButton.setOnClickListener(saveButtonListener);
loadButton= (Button)findViewById(R.id.loadButton);
loadButton.setOnClickListener(loadButtonListener);
nameEditText= (EditText)findViewById(R.id.nameEditText);
iqEditText= (EditText)findViewById(R.id.iqEditText);
}
/**
* @author张飞:嘿咻咻,把按钮监听都放在一个方法里,我不得了的厉害了呀
*/
private void setListener() {
saveButtonListener = newOnClickListener() {
@Override
public void onClick(View v) {
// 保存信息方法
save();
}
};
loadButtonListener = newOnClickListener() {
@Override
public void onClick(View v) {
// 读取信息方法
load();
}
};
}
/**
* @author张飞:把常用配置数据存入SharedPreferences.
*/
private void save() {
String name =nameEditText.getText().toString();
// 检查是否有数据
if (name == null|| name.length() == 0) {
Toast.makeText(PreferenceActivity.this, "没有姓名数据不要瞎点,会崩溃的!",Toast.LENGTH_LONG).show();
return;
}
String iq = iqEditText.getText().toString();
// 检查是否有数据
if (iq == null|| iq.length() == 0) {
Toast.makeText(PreferenceActivity.this, "没有智商数据不要瞎点,会崩溃的!",Toast.LENGTH_LONG).show();
return;
}
settings.edit().putString("name",nameEditText.getText().toString()).commit();
settings.edit().putString("IQ", iqEditText.getText().toString()).commit();
Toast.makeText(PreferenceActivity.this, "存储成功了!",Toast.LENGTH_LONG).show();
}
/**
* @author张飞:把常用配置数据从SharedPreferences读出
*/
private void load() {
String name =settings.getString("name", "");
String iq =settings.getString("IQ", "");
nameEditText.setText(name);
iqEditText.setText(iq);
}
}
该代码中,在save()方法里使用了SharedPreferences类对name和IQ进行了存储,存储方式是Activity.MODE_PRIVATE(私有)。需要注意的是,在使用SharedPreferences.edit()进行数据填充之后,要调用commit()方法将数据提交。load()方法使用了name和IQ键获取到了存储在SharedPreferences当中的值。
1.6. Content Provider
Android中不同的应用程序间的数据是不能直接被相互访问和操作的,如果不同应用程序之间需要共享数据,就需要用到ContentProvider。Content Provider属于Android应用程序的组件之一,作为应用程序之间唯一的共享数据的途径。它的主要功能就是存储并检索数据以及向其他应用程序提供数据访问接口。
Android系统已经提供了许多ContentProvider,例如Contacts、Browser、CallLog、Settings等。在应用程序中,不仅可以调用Android系统提供的ContentProvider,另外还可以公开我们自己应用程序的数据。本节重点介绍与ContentProvider紧密相关的Uri类、UriMatcher类和ContentUris类的使用方法。最后通过两个实例介绍如何调用系统的ContentProvider数据和共享数据。
Uri类
在Content Provider中使用Uri来进行统一资源定位,Uri在ContentProvider中代表了要操作的数据,以下是一些示例Uri:
content://media/internal/images:返回设备上存储的所有图片;
content://contacts/people/:返回设备上的所有联系人信息;
content://contacts/people/45:返回联系人信息中ID为45的联系人记录。
Uri和Url很相似,但在Android中广泛应用Uri,而不是Url。Url标识资源的物理位置,相当于文件的路径;而Uri则是标识资源的逻辑位置,并不提供资源的具体位置。比如说电话薄中的数据,如果用Url来标识的话,可能会是一个很复杂的文件结构,而且一旦文件的存储路径改变,Url也必须得改动。但是若是Uri,则可以用诸如content://contract /people这样容易记录的逻辑地址来标识,而且并不需要关心文件的具体位置,即使文件位置改动也不需要做变化。
在Android中,Uri主要分为三个部分:scheme、authority、path,其中authority又分为host和port。格式如下:scheme://host:port/path。在程序中一般是不直接用Uri来标识资源,而是用常量来标识,例如用Contacts.People.CONTENT_URI来标识Contacts中的People表。
如果要标识某个联系人的具体数据,可以用路径来表示需要操作的数据,如下所示:
要操作xxx表中的记录,可以构建这样的路径:/xxx;
要操作People表中的所有记录,可以构建这样的路径:/People;
要操作People表中id为10的记录,可以构建这样的路径:/People/10;
要操作People表中id为10的记录的name字段,可以构建这样的路径:People/10/name。
UriMatcher类
因为Uri代表了要操作的数据,所以经常需要匹配Uri,根据不同的Uri获取不同的数据。UriMatcher类就是为了匹配Uri,它事先设定好Uri的匹配码,根据不同的Uri,返回对应的匹配码,得到当前所要操作的数据。具体用法如下:
首先第一步把需要匹配Uri路径全部给注册上,代码如下所示:
//常量UriMatcher.NO_MATCH表示不匹配任何路径返回的匹配码
UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
//添加需要匹配uri,如果uri等于content://com.firstpeople.database/heros就会返回匹配码1。
sMatcher.addURI("com.firstpeople.database ", "heros#", 1);
第二步使用sMatcher.match(uri)方法对输入的Uri进行匹配,如果匹配成功就返回匹配码,代码如下:
//*指代任意,#指代数字
//type返回sMatcher.addURI("com.firstpeople.database","person#", 1)注册时的值1
inttype=sMatcher.match(Uri.parse("content://com.firstpeople.database/heros/10"));
ContentUris类
ContentUris类用于添加和获取Uri路径后的id。它有两个比较实用的方法:withAppendedId(uri,id)用于为路径加上id;parseId(uri)用于从路径中获取id。代码如下所示:
Uri uri = Uri.parse("content://com.firstpeople.database/heros");
Uri resultUri = ContentUris.withAppendedId(uri, 10);
//生成后的Uri为:content://com.firstpeople.database/heros /10
Uri uri = Uri.parse("content://com.firstpeople.database/heros/10");
//获取的结果为:10
longpersonid = ContentUris.parseId(uri);
1.6.1.Content Provider共享数据
在Android中,ContentProvider是数据对外的接口,程序通过Content Provider访问数据而不需要关心数据具体的存储及访问过程,这样既提高了数据的访问效率,同时也保护了数据。Activity类中有一个继承自ContentWapper的getContentResolver()无参数方法,该方法返回一个ContentResolver对象,通过ContentResolver访问一个ContentProvider。ContentResolver类提供了四个方法:
l publicUri insert(Uri uri, ContentValues values):用于往Content Provider添加数据。
l publicint delete(Uri uri, String selection, String[] selectionArgs):用于从ContentProvider删除数据。
l publicint update(Uri uri, ContentValues values, String selection, String[]selectionArgs):用于更新Content Provider中的数据。
l publicCursor query(Uri uri, String[] projection, String selection, String[]selectionArgs, String sortOrder):用于从Content Provider中获取数据。
张飞:如果Content Provider访问者需要知道Content Provider中的数据发生变化,可以事先使用ContentObserver对数据Uri进行监听。利用getContentResolver().registerContentObserver函数监听注册,在Content Provider发生数据变化时调用getContentResolver().notifyChange(uri, null)通知注册此Uri上的访问者。系统监听到数据发生变化的通知,就会调用ContentObserver的onChange()方法。 |
下面通过一个访问Android联系人的例子展示如何通过ContentProvider共享数据,运行结果如图13-8所示:
图13-8 ContentProvide共享联系人数据
值得注意的是,需要在AndroidManifest.xml里添加访问联系人权限,如下所示:
<uses-permissionandroid:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
这回我们讲解的是高深的联系人共享的数据存储代码ContentProviderShareActivity,大哥和二哥学了好久也不知道学会了没。军师说我在数据存储方面有天赋,不知道是不是跟我能吃有点关系。
ContentProviderShareActivity.java代码清单13-6-1:
/**
*@author张飞:学问之美,在于使人一头雾水;诗歌之美,在于煽动男女出轨;女人之美,在
于蠢得无怨无悔;男人之美,在于说谎说得白日见鬼。
*/
public class ContentProviderShareActivity extends Activity {
// 显示所有联系人信息
private TextView textView = null;
// 输入姓名存储数据
private EditText nameEditText = null;
// 输入电话号码存储数据
private EditText iphoneEditText = null;
// 显示联系人信息按钮
private Button showButton = null;
// 显示联系人信息按钮的监听
private OnClickListener showButtonListener = null;
// 插入联系人信息按钮
private Button insertButton = null;
// 插入联系人信息按钮的监听
private OnClickListener insertButtonListener = null;
// 删除联系人信息按钮
private Button deleteButton = null;
// 删除联系人信息按钮的监听
private OnClickListener deleteButtonListener = null;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.contentprovidershare);
setListener();
findView();
}
/**
* @author张飞:哇哈哈,把控件初始化放一个方法里,我好厉害呀
*/
private void findView() {
textView =(TextView)findViewById(R.id.textView);
showButton= (Button)findViewById(R.id.showButton);
showButton.setOnClickListener(showButtonListener);
insertButton = (Button)findViewById(R.id.insertButton);
insertButton.setOnClickListener(insertButtonListener);
deleteButton= (Button)findViewById(R.id.deleteButton);
deleteButton.setOnClickListener(deleteButtonListener);
nameEditText = (EditText)findViewById(R.id.nameEditText);
iphoneEditText = (EditText)findViewById(R.id.iphoneEditText);
}
/**
* @author张飞:嘿咻咻,把按钮监听都放在一个方法里,我不得了的厉害了呀
*/
private void setListener() {
showButtonListener = newOnClickListener() {
@Override
public void onClick(View v) {
// 显示所有联系人信息
showRecords();
}
};
insertButtonListener = newOnClickListener() {
@Override
public void onClick(View v) {
// 插入联系人信息
insertRecords();
}
};
deleteButtonListener = newOnClickListener() {
@Override
public void onClick(View v) {
// 删除所有联系人
deleteRecords();
}
};
}
/**
* @author张飞:显示所有联系人数据
*/
private void showRecords() {
// 该数组中包含了所有要返回的字段
String columns[] = new String[] {
People.NAME, People.NUMBER
};
String item = "";
/**
* Android1.6版本之后,对联系人进行了改进,修改了表的结构,增加了更多联系人存储信息,例如可以添加多个联系电话、添加邮箱。因此不再推荐使用XX获取联系人的名称和电话,而使用ContactsContract.Contacts.DISPLAY_NAME获取联系人姓名,使用ContactsContract.CommonDataKinds.Phone.CONTENT_URI获取联系人联系电话列表。
* 实例为了简化代码的复杂性,让读者更好的理解ContentProvider数据读取,采用1.6版本。
*/
Uri mContacts = People.CONTENT_URI;
Cursor cur = managedQuery(mContacts,columns, null, null, null);
if (cur.moveToFirst()) {
String name = null;
String phoneNo = null;
// 循环获取联系人
do {
// 获取字段的值
name = cur.getString(cur.getColumnIndex(People.NAME));
phoneNo = cur.getString(cur.getColumnIndex(People.NUMBER));
item = item + name + ":" + phoneNo + "\n";
} while (cur.moveToNext());
}
textView.setText(item);
}
/**
* @author张飞:插入联系人数据
*/
private void insertRecords() {
String name =nameEditText.getText().toString();
String phoneNo =iphoneEditText.getText().toString();
if (name == null|| name.length() == 0) {
Toast.makeText(ContentProviderShareActivity.this, "没有姓名数据不要瞎点,会崩溃的!",Toast.LENGTH_LONG)
.show();
return;
}
if (phoneNo == null|| phoneNo.length() == 0) {
Toast.makeText(ContentProviderShareActivity.this, "没有电话数据不要瞎点,会崩溃的!",Toast.LENGTH_LONG)
.show();
return;
}
ContentValues values = new ContentValues();
values.put(People.NAME, name);
// 插入联系人名称
Uri uri =getContentResolver().insert(People.CONTENT_URI, values);
Uri numberUri = Uri.withAppendedPath(uri,People.Phones.CONTENT_DIRECTORY);
values.clear();
values.put(Contacts.Phones.TYPE, People.Phones.TYPE_MOBILE);
values.put(People.NUMBER,phoneNo);
// 插入联系人电话
getContentResolver().insert(numberUri, values);
getContentResolver().notifyChange(uri, null);
getContentResolver().notifyChange(numberUri, null);
showRecords();
Toast.makeText(ContentProviderShareActivity.this, "存储成功了!",Toast.LENGTH_LONG).show();
}
/**
* @author张飞:删除所有联系人数据
*/
private void deleteRecords() {
Uri uri = People.CONTENT_URI;
// 删除联系人
getContentResolver().delete(uri, null,null);
showRecords();
Toast.makeText(ContentProviderShareActivity.this, "删除成功了!",Toast.LENGTH_LONG).show();
}
}
该代码的showRecords()通过设置Uri mContacts =People.CONTENT_URI来查找联系人表。在insertRecords()方法中,使用getContentResolver().insert(numberUri, values)插入一条联系人数据,使用getContentResolver().notifyChange(uri,null)和getContentResolver().notifyChange(numberUri,null)两个方法通知监听联系人和电话信息的应用程序联系人数据已经发生了变化。在deleteRecords()方法中,使用getContentResolver().delete(uri, null, null)方法删除指定Uri的联系人信息。
1.6.2.创建自己的ContentProvider
一个程序可以通过实现一个Content Provider的抽象接口将自己的数据完全暴露出去。Content Providers是以类似数据库中表的方式将数据暴露出去的,因此外界获取数据的操作类似数据库中获取数据的基本操作。只不过是采用Uri来表示外界需要访问的“数据库”。至于如何从Uri中识别出外界需要用到的是哪个“数据库”,是由Android底层负责。
下面通过一个实例简要分析Content Provider如何向外界提供数据操作的接口。实例新建了一个ehero.db数据库,生成了一个HeroStore表,通过自定义ContentProvider的CONTENT_URI将数据表暴露给其他应用程序,结果如图所示13-9所示:
图13-9 创建ContentProvide
首先定义一个名为HeroStores的类用于存储publicstatic final的Uri类型CONTENT_URI变量。CONTENT_URI变量必须为其指定一个唯一的字符串值,最好的方案是以包名加表名的方式进行命名,防止有相同名字冲突。如content://com.firstpeople.database/heros就是由包名com.firstpeople.database和heros表名(此表用于向外界提供数据操作的接口)组成。代码如下所示:
HeroStores.java 代码清单13-6-2:
public class HeroStores {
//类中定义了ContentProvider的CONTENT_URI
public static final String AUTHORITY = "com.firstpeople.database";
// BaseColumns类中已经包含了_id字段
public interface HeroStore extendsBaseColumns {
// 定义URI
public static final Uri CONTENT_URI = Uri.parse("content://com.firstpeople.database/heros");
// 表数据列
public static final String NAME = "name";
}
}
接着,创建了我们自己的Content Provider,继承自ContentProvider父类并实现其接口,通过重写了Content Provider的6个接口,实现了外部应用对数据heros表的增删查改等操作,从而实现了数据的共享。具体代码如下所示:
MyContentProvider.java代码清单13-6-2:
/**
* @author张飞:少年不胡作妄为,大胆放肆,试问老年时哪来的题材话当年。
*/
public class MyContentProvider extendsContentProvider {
public static final String PROVIDER_NAME ="com.firstpeople.database";
private staticfinalint HEROS = 1;
private staticfinalint HERO_ID = 2;
// UriMatcher类用于匹配Uri
private staticfinal UriMatcher uriMatcher;
static {
// 常量UriMatcher.NO_MATCH表示不匹配任何路径的返回码
uriMatcher= newUriMatcher(UriMatcher.NO_MATCH);
// 注册Uri路径
uriMatcher.addURI(PROVIDER_NAME, "heros", HEROS);
uriMatcher.addURI(PROVIDER_NAME, "heros/#", HERO_ID);
}
// 创建操作数据库类
private SQLiteDatabase db;
private DatabaseHelper dbHelper;
private staticfinal String DATABASE_NAME = "ehero.db";
private staticfinal String TABLE_NAME = "HeroStore";
private staticfinalint DATABASE_VERSION = 1;
@Override
public boolean onCreate() {
Context context = getContext();
// 实例化数据库操作类
dbHelper = new DatabaseHelper(context);
db =dbHelper.getWritableDatabase();
return (db == null)? false : true;
}
private static class DatabaseHelper extendsSQLiteOpenHelper {
DatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
// 创建用于存储数据的表
String DATABASE_CREATE= "create table " + TABLE_NAME + " (" +HeroStores.HeroStore._ID
+ " INTEGER PRIMARY KEY AUTOINCREMENT, " +HeroStores.HeroStore.NAME
+ " text notnull);";
db.execSQL(DATABASE_CREATE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, intnewVersion) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
onCreate(db);
}
}
@Override
public String getType(Uri uri) {
// 根据uri匹配结果返回不同的查找结果
switch (uriMatcher.match(uri)) {
// 返回所以的英雄
case HEROS:
return "vnd.android.cursor.dir/vnd.learn2develop.heros";
// 返回指定的英雄
case HERO_ID:
return "vnd.android.cursor.item/vnd.learn2develop.heros";
default:
throw newIllegalArgumentException("Unsupported URI: " + uri);
}
}
@Override
public Cursor query(Uri uri, String[] projection, Stringselection, String[] selectionArgs,
String sortOrder) {
//SQLiteQueryBuilder 是一个构造SQL查询语句的辅助类。
SQLiteQueryBuilder sqlBuilder= new SQLiteQueryBuilder();
sqlBuilder.setTables(TABLE_NAME);
if (uriMatcher.match(uri) == HERO_ID)
// 如果是查找特定的英雄,添加条件语句
sqlBuilder.appendWhere(HeroStores.HeroStore._ID+ " = " + uri.getPathSegments().get(1));
if (sortOrder== null || sortOrder =="")
sortOrder =HeroStores.HeroStore.NAME;
Cursorc = sqlBuilder
.query(db, projection,selection, selectionArgs, null, null, sortOrder);
// 注册一个监听观察Uri的变换
c.setNotificationUri(getContext().getContentResolver(),uri);
return c;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
// 插入一个英雄
dbHelper = new DatabaseHelper(getContext());
db =dbHelper.getWritableDatabase();
longrowID = db.insert(TABLE_NAME, null,values);
//插入成功
if (rowID > 0) {
Uri _uri = ContentUris.withAppendedId(HeroStores.HeroStore.CONTENT_URI,rowID);
getContext().getContentResolver().notifyChange(_uri, null);
return _uri;
}else {
return null;
}
}
@Override
public int delete(Uri arg0, String arg1, String[] arg2) {
intcount = 0;
switch (uriMatcher.match(arg0)) {
case HEROS:
count = db.delete(TABLE_NAME, arg1,arg2);
break;
case HERO_ID:
String id =arg0.getPathSegments().get(1);
count = db.delete(TABLE_NAME,
HeroStores.HeroStore._ID + " = " + id
+ (!TextUtils.isEmpty(arg1) ? " AND (" +arg1 + ')' : ""), arg2);
break;
default:
throw new IllegalArgumentException("Unknown URI " +arg0);
}
getContext().getContentResolver().notifyChange(arg0, null);
return count;
}
@Override
public int update(Uri uri, ContentValues values, String selection,String[] selectionArgs){
intcount = 0;
switch (uriMatcher.match(uri)) {
case HEROS:
count = db.update(TABLE_NAME, values, selection, selectionArgs);
break;
case HERO_ID:
count = db.update(TABLE_NAME, values, HeroStores.HeroStore._ID + "= "
+ uri.getPathSegments().get(1)
+ (!TextUtils.isEmpty(selection) ? " AND(" + selection + ')' : ""),
selectionArgs);
break;
default:
throw new IllegalArgumentException("Unknown URI " +uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return count;
}
}
这里采用SQLite数据库来保存应用程序的数据,也可以根据需求用其他的方式来存储数据。在MyContentProvider的onCreate()方法被第一次调用时,会创建应用所需的数据库ehero.db和heros表。当外界需要操作heros表时,就会通过ContentProvider的接口实现对heros表数据的操作。例如,当外界需要修改数据库heros表时,就会调用MyContentProvider的update()方法,该方法根据外界传入的Uri,通过Matcher.match(Uri)方法获得匹配码,从而选择性的修改整张表或对应的单个id行的数据。ContentProvider类主要有以下6个方法:
表13-3 Content Provider的方法
方法 | 解释 |
public boolean onCreate() | 该方法在Content Provider创建后就会被调用,Android系统运行后,Content Provider只有在被第一次使用它时才会被创建。 |
public Uri insert(Uri uri, ContentValues values) | 外部应用程序通过这个方法向 Content Provider添加数据。Uri:标识操作数据的Uri,values:需要添加数据的键值对。 |
public int delete(Uri uri, String selection, String[] selectionArgs) | 外部应用程序通过这个方法从 Content Provider中删除数据。Uri:标识操作数据的Uri;selection:构成筛选添加的语句,如“id=1”或者“id=?”;selectionArgs:对应selection的两种情况可以传入null或者new String[]{"1"}。 |
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) | 外部应用程序通过这个方法对 Content Provider中的数据进行更新。Values:对应需要更新的键值对,键为对应共享数据中的字段,值为对应的修改值,其余参数同delete方法。 |
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) | 外部应用程序通过这个方法从Content Provider中获取数据,并返回一个Cursor对象。Projection:需要从Content Provider中选择的字段,如果为空,则返回的Cursor将包含所有的字段;sortOrder:默认的排序规则,其余参数同delete方法。 |
public String getType(Uri uri) | 该方法用于返回当前Uri所代表数据的MIME类型。 |
自己定义ContentProvider需要在AndroidMenifest.xml中使用<provider>标签来设置ContentProvider。代码如下:
<provider android:name="com.firstpeople.database.MyContentProvider"android:authorities="com.firstpeople.database"/>
张飞:如果你要处理的数据类型是一种比较新的类型,你就必须先定义一个新的MIME类型,以供ContentProvider.geType(url)方法来返回。MIME类型有两种形式:一种是为指定单个记录的,还有一种是为指定多条记录的。 |
ContentProviderBuildActivity.java代码清单13-6-2:
/**
*@author孔明:写代码三定律:要么忍!要么狠!要么滚!
*/
public class ContentProviderBuildActivity extends Activity {
// 显示所有英雄信息
private TextView textView = null;
//输入英雄姓名存储数据
private EditText nameEditText = null;
//显示英雄信息按钮
private Button showButton = null;
//显示英雄信息按钮的监听
private OnClickListener showButtonListener = null;
//插入英雄信息按钮
private Button insertButton = null;
//插入英雄信息按钮的监听
private OnClickListener insertButtonListener = null;
//删除英雄信息按钮
private Button deleteButton = null;
//删除英雄信息按钮的监听
private OnClickListener deleteButtonListener = null;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.contentproviderbuild);
setListener();
findView();
}
/**
* @author张飞:哇哈哈,把控件初始化放一个方法里,我好厉害呀
*/
private void findView() {
textView =(TextView)findViewById(R.id.textView);
showButton= (Button)findViewById(R.id.showButton);
showButton.setOnClickListener(showButtonListener);
insertButton = (Button)findViewById(R.id.insertButton);
insertButton.setOnClickListener(insertButtonListener);
deleteButton = (Button)findViewById(R.id.deleteButton);
deleteButton.setOnClickListener(deleteButtonListener);
nameEditText = (EditText)findViewById(R.id.nameEditText);
}
/**
* @author张飞:嘿咻咻,把按钮监听都放在一个方法里,我不得了的厉害了呀
*/
private void setListener() {
showButtonListener = newOnClickListener() {
@Override
public void onClick(View v) {
// 显示所有英雄姓名
show();
}
};
insertButtonListener = newOnClickListener() {
@Override
public void onClick(View v) {
// 插入英雄姓名
insert();
}
};
deleteButtonListener = newOnClickListener() {
@Override
public void onClick(View v) {
// 删除英雄
delete();
}
};
}
// 利用ContentProvider插入数据
public void insert() {
String name =nameEditText.getText().toString();
if (name == null|| name.length() == 0) {
Toast.makeText(ContentProviderBuildActivity.this, "没有姓名数据不要瞎点,会崩溃的!", Toast.LENGTH_LONG).show();
return;
}
ContentValues values = new ContentValues();
new MyContentProvider();
// 存放数据
values.put(HeroStores.HeroStore.NAME, name);
// 获取到自己定义的ContentProvider,调用接口
getContentResolver().insert(HeroStores.HeroStore.CONTENT_URI, values);
show();
Toast.makeText(ContentProviderBuildActivity.this, "存储成功了!",Toast.LENGTH_LONG).show();
}
// 利用ContentProvider显示数据
public void show() {
String item = "";
String columns[] = new String[] {
HeroStores.HeroStore._ID, HeroStores.HeroStore.NAME
};
// 要操作的数据
Uri myUri =HeroStores.HeroStore.CONTENT_URI;
// 获取到要操作数据的指向
/*
* 查询返回一个Cursor类型的对象。所有执行写操作的方法如insert(), update() 以及delete()可以将被监听。
* 可以通过使用ContentResover().notifyChange()方法来通知监听器关于数据更新的信息。
*/
Cursor cur = managedQuery(myUri, columns, null, null, null);
if (cur == null)
return;
if (cur.moveToFirst()) {
String id = null;
String name = null;
do {
// 根据 name的名称获得它的列索引
id= cur.getString(cur.getColumnIndex(HeroStores.HeroStore._ID));
name = cur.getString(cur.getColumnIndex(HeroStores.HeroStore.NAME));
item = item + id + " " + name + ",";
} while (cur.moveToNext());
}
textView.setText(item);
}
// 利用ContentProvider删除数据
public void delete() {
Uri uri =HeroStores.HeroStore.CONTENT_URI;
// 删除英雄
getContentResolver().delete(uri, null,null);
show();
Toast.makeText(ContentProviderBuildActivity.this, "删除成功了!",Toast.LENGTH_LONG).show();
}
}
该代码的insert ()、show()和delete()方法,分别调用了MyContentProvider的insert、query和delete接口,实现了外部程序对heros表数据的插入、查找和删除操作,最终实现了数据的共享。
1.7. 玄德有话说
张飞:大哥,properties这是什么文件,好像蛮狠的样子。
刘备:小飞飞,在我们平时写应用的时候,有些参数是经常改变的,而这种改变不是我们预知的。比如说张飞,一会住在平原,一会住在新野,要使得这个地址信息具有通用性,那么以上信息就不能写死在程序里。通常我们的做法是用配置文件来解决。properties是属性文件,也用来配置应用程序的一些信息,采用键值对的方式存储,有利于你以后的代码重构,方便维护。
张飞:大哥,你能正常点说话吗?小飞飞叫的人家好肉麻……那话说回来这样的文件我存哪里啊?
刘备:小飞飞,难倒你要存你的小秘密?如果是你的小秘密的话,或者是应用程序不可或缺的配置信息,那当然是存在内部存储器啦,这样就没有人发现你小飞飞暗恋如花的事情了。不然,将配置文件存在外部存储器也是可以的啦。
张飞:原来如此!大哥的比喻果然深邃!
关羽:大哥,为何我苦练Android存储,至今无法顿悟呀?
刘备:二弟,你是否没有打通任督二脉?
关羽:何为任督二脉。
刘备:就是Java File类和输入输出流呀!
关羽:大哥,快教我,何为File类和输入输出流。
刘备:小羽羽,存储当然难免要接触文件系统。Java中的File类不仅代表文件而且代表目录。创建一个File类的实例,就意味着可以管理文件夹命名、查询文件属性和处理目录等到操作。学好它,才能随意操作文件啊,小羽羽。
关羽:大哥,你今天这是犯什么病了……不过听大哥这么一说,我有点悟到了。
刘备:小羽羽,不要心急。光操作文件是不行的,还需要将内容和文件建立起输入输出呢。java中的输入输出流,就好像水管,将两个容器连接起来。将数据从外存中读取到内存中的称为输入流,将数据从内存写入外存中的称为输出流。数据源可以包括键盘、文件、网络等。学好输入输出流,才是你练就数据存储的关键。
关羽:大哥,事不宜迟,我要闭关打通任督二脉,有什么小花、小丽、小翠的找我,一律说我不在。
刘备:老弟,啥小花、小丽、小翠,不就是隔壁王大妈家的小鸭、小鸡和小狗,它们明明都是公的,你非得起个女性化的名字。
刘备:军师啊,我的资源文件怎么一放进工程中就出错呀?快帮帮我!
孔明:老大,资源文件的名字必须符合Java变量的命名规则,且不能有大写,只能是'[a-z][0-9]._',否则会有编译错误。
刘备:soga!Apk能够在运行时更改资源文件吗?
孔明:主公,你越来越会问问题了。所有资源文件都是只读的,运行时无法更改。因为,程序运行时是把Apk动态解析加载到内存中,也就是说,Apk是不会有变化的,它是无法被改变的。所有的资源文件夹/res和/assets在运行时都是只读的,不可写入,Apk在编译后是无法再改变的。
张飞:老诸!我要用Content Provider存储字节型数据,怎么办?
孔明:如果你要存储字节型数据,比如位图文件等,数据列其实是一个表示实际保存文件的Uri字符串,通过它来读取对应的文件数据。处理这种数据类型的ContentProvider需要实现一个名为_data的字段,_data字段列出了该文件在Android文件系统上的精确路径。这个字段不仅可以供客户端使用,而且也可以供ContentResolver使用。客户端可以调用ContentResolver.openOutputStream()方法来处理该URI指向的文件资源;如果是ContentResolver本身的话,由于其持有的权限比客户端要高,所以它能直接访问该数据文件。