3G,全称为3rd Generation,中文含义就是指第三代数字通信。
所谓3G,是指将无线通信与国际互联网等多媒体通信结合的新一代移动通信系统。 3G只是一种通信技术标准,符合这个标准的技术有WCDMA、CDMA2000、TD-SCDMA三种制式。中国联通使用的是WCDMA(世界上大部分 3G网络都采用的是该标准) ;中国电信使用的是CDMA2000 (日、韩和北美使用);中国移动使用的是具有自主知识产权的TD-SCDMA(只有中国才使用) 。相对第一代模拟制式手机(1G)和第二代GSM、 CDMA等数字手机(2G),3G网络能处理图像、音乐、视频等多种媒体形式,提供包括网页浏览、电话会议、电子商务等多种信息服务。第三代与前两代的主要区别是在传输声音和数据的速度上有很大的提升。
由于3G商用需要相当浩大的工程,要从目前的2G迈向3G不可能一下就衔接得上,因此前几年2.5G的手机就出现了。符合2.5G标准的技术有 CDMA2000 1X和GPRS,中国联通使用的是CDMA2000 1X标准,中国移动使用的是GPRS标准。目前,我们可以把2.5G移动通信技术看作是2G迈向3G的衔接性技术,在2.5G网络下出现了如WAP、蓝牙 (Bluetoot) 等技术。
Android
应用程序架构
src/ java原代码存放目录
gen/ 自动生成目录
gen 目录中存放所有由Android开发工具自动生成的文件。目录中最重要的就是R.java文件。 这个文件由Android开发工具自动产生的。Android开发工具会自动根据你放入res目录的xml界面文件、图标与常量,同步更新修改 R.java文件。正因为R.java文件是由开发工具自动生成的,所以我们应避免手工修改R.java。R.java在应用中起到了字典的作用,它包含了界面、图标、常量等各种资源的id,通过R.java,应用可以很方便地找到对应资源。另外编绎器也会检查R.java列表中的资源是否被使用到,没有被使用到的资源不会编绎进软件中,这样可以减少应用在手机占用的空间。
res/ 资源(Resource)目录
在这个目录中我们可以存放应用使用到的各种资源,如xml界面文件,图片或数据。具体请看ppt下方备注栏。
AndroidManifest.xml 功能清单文件
这个文件列出了应用程序所提供的功能,在这个文件中,你可以指定应用程序使用到的服务(如电话服务、互联网服务、短信服务、GPS服务等等)。另外当你新添加一个Activity的时候,也需要在这个文件中进行相应配置,只有配置好后,才能调用此Activity。
default.properties 项目环境信息,一般是不需要修改此文件
res/drawable 专门存放png、jpg等图标文件。在代码中使用getResources().getDrawable(resourceId)获取该目录下的资源。
res/layout 专门存放xml界面文件,xml界面文件和HTML文件一样,主要用于显示用户操作界面。
res/values 专门存放应用使用到的各种类型数据。不同类型的数据存放在不同的文件中,如下:
· strings.xml 定义字符串和数值,在Activity中使用getResources().getString(resourceId) 或getResources().getText(resourceId)取得资源。它的作用和 struts中的国际化资源文件一样。
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="android"> android</string>
</resources>
· arrays.xml 定义数组。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="colors">
<item>red</item>
<item>yellow</item>
<item>green</item>
<item>blue</item>
</string-array>
</resources>
· colors.xml 定义颜色和颜色字串数值,你可以在Activity中使用getResources().getDrawable(resourceId) 以及getResources().getColor(resourceId)取得这些资源。例子如下:
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<color name="contents_text">#ff000000</color>
</resources>
· dimens.xml 定义尺寸数据,在Activity中使用getResources().getDimension(resourceId) 取得这些资源
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<dimen name="key_height">50dip</dimen>
</resources>
· styles.xml 定义样式。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="androidText" parent="@style/Text">
<item name="android:textSize">18px</item>
<item name="android:textColor">#008</item>
</style>
</resources>
res/anim/ 编译成帧动画的XML文件。
res/xml/ 在Activity中使用getResources().getXML()读取该目录下的XML资源文件。
res/raw/ 该目录下的文件将直接被复制到设备上。编译软件时,这些数据不会被编译,它们被直接加入到程序安装包里。 为了在程序中使用这些资源,你可以调用getResources().openRawResource(ID) , 参数ID形式:R.raw.
somefilename。
电话拔号器
因为应用要使用手机的电话服务,所以要在清单文件AndroidManifest.xml中添加电话服务权限:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="cn.android.action"
android:versionCode="1"
android:versionName="1.0">
略....
<uses-sdk android:minSdkVersion=“6" />
<uses-permission android:name="android.permission.CALL_PHONE"/>
</manifest>
界面布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<TextView
android:layout_width="fill_parent" android:layout_height="wrap_content"
android:text="@string/inputmobile"/>
<EditText android:layout_width="fill_parent" android:layout_height="wrap_content"
android:id="@+id/mobile"/>
<Button android:layout_width="wrap_content" android:layout_height="wrap_content"
android:text="@string/button"
android:id="@+id/button"/>
</LinearLayout>
LinearLayout (线性布局)、AbsoluteLayout(绝对布局)、RelativeLayout(相对布局)、TableLayout(表格布局)、FrameLayout(帧布局)
Activity:
public class DialerAction extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
Button button = (Button)findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener(){
public void onClick(View v) {
EditText editText = (EditText)findViewById(R.id.mobile);
Intent intent = new Intent(Intent.ACTION_CALL, Uri.parse("tel:"+ editText.getText()));
DialerAction.this.startActivity(intent);
}
});
}
}
测试步骤:
1>在Eclipse中运行此应用
2>在Dos窗口中进入android SDK安装路径的tools目录,输入以下命令再开启一个Android模拟器:
emulator -data android
注:android为用户数据存取文件,如果该文件不存在,默认在tools目录创建该文件
3>在电话扰号器中输入上图现显的电话号码
“
尚未注册网络
”
错误信息的解决办法
打开Android模拟器时,出现无信号,拔打电话或发短信时,提示“尚未注册网络”错误信息的解决方案如下。
l 场景一:你的电脑没有连接上互联网,同时也没有在局域网。
解决办法:右键点击网上邻居,选择"属性",在网络连接窗口中右键点击"本地连接",选择"属性",设置TCP/IP属性如下:
IP地址:192.168.1.100
子网掩码:255.255.255.0
默认网关:192.168.1.100
首选DNS 服务器:192.168.1.100
l 场景二:你的电脑没有连接上互联网,但在局域网。
解决办法:右键点击网上邻居,选择"属性",在网络连接窗口中右键点击"本地连接",选择"属性",设置TCP/IP属性如下:
IP地址:设置成你所在局域网的IP,如:192.168.1.100
子网掩码:设置成你所在局域网的掩码,如:255.255.255.0
默认网关:设置成你所在局域网的网关,一般网关的IP格式为:*.*.*.1,如:192.168.1.1
首选DNS 服务器:设置成你所在局域网的路由器IP,一般路由器的IP格式为:*.*.*.1,如:192.168.1.1
最后一种解决方案是:让你的电脑连接上互联网。
短信发送器
因为应用要使用手机的短信服务,所以要在清单文件AndroidManifest.xml中添加短信服务权限:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="cn.android.sms"
android:versionCode="1"
android:versionName="1.0">
略....
<uses-sdk android:minSdkVersion=“4" />
<uses-permission android:name="android.permission.SEND_SMS"/>
</manifest>
界面布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical“ android:layout_width="fill_parent“ android:layout_height="fill_parent" >
<TextView android:layout_width="fill_parent" android:layout_height="wrap_content"
android:text="@string/inputmobile"/>
<EditText android:layout_width="fill_parent" android:layout_height="wrap_content"
android:id="@+id/mobile"/>
<TextView android:layout_width="fill_parent" android:layout_height="wrap_content"
android:text="@string/content"/>
<EditText android:layout_width="fill_parent" android:layout_height="wrap_content"
android:minLines="3"
android:id="@+id/content"/>
<Button android:layout_width="wrap_content" android:layout_height="wrap_content"
android:text="@string/button"
android:id="@+id/button"/>
</LinearLayout>
Activity主要代码:
String mobile = mobileView.getText().toString();
String content = contentView.getText().toString();
SmsManager smsManager = SmsManager.getDefault();
PendingIntent sentIntent = PendingIntent.getBroadcast(SMSSender.this, 0, new Intent(), 0);
if(content.length()>70){//如果字数超过70,需拆分成多条短信发送
List<String> msgs = smsManager.divideMessage(content);
for(String msg : msgs){
smsManager.sendTextMessage(mobile, null, msg, sentIntent, null);
//最后二个参数为短信已发送的广播意图,最后一个参数为短信对方已收到短信的广播意图
}
}else{
smsManager.sendTextMessage(mobile, null, content, sentIntent, null);
}
Toast.makeText(SMSSender.this, "短信发送完成", Toast.LENGTH_LONG).show();
对应用进行单元测试
第一步:首先在AndroidManifest.xml中加入下面红色代码:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="cn.android.action
“ android:versionCode="1“ android:versionName="1.0">
<application android:icon="@drawable/icon" android:label="@string/app_name">
<uses-library android:name="android.test.runner" />
....
</application>
<uses-sdk android:minSdkVersion="6" />
<instrumentation android:name="android.test.InstrumentationTestRunner"
android:targetPackage="cn.android.action" android:label="Tests for My App" />
</manifest>
上面targetPackage指定的包要和应用的package相同。
第二步:编写单元测试代码(选择要测试的方法,右键点击
“
Run As
”
--
“
Android Junit Test
”
):
import android.test.AndroidTestCase;
import android.util.Log;
public class XMLTest extends AndroidTestCase {
public void testSomething() throws Throwable {
Assert.assertTrue(1 + 1 == 3);
}
}
使用文件进行数据存储
首先给大家介绍使用文件如何对数据进行存储,Activity提供了openFileOutput()方法可以用于把数据输出到文件中,具体的实现过程与在J2SE环境中保存数据到文件中是一样的。
public class FileActivity extends Activity {
@Override public void onCreate(Bundle savedInstanceState) {
...
FileOutputStream outStream = this.openFileOutput("android.txt", Context.MODE_PRIVATE);
outStream.write("安桌".getBytes());
outStream.close();
}
}
openFileOutput()方法的第一参数用于指定文件名称,不能包含路径分隔符“/” ,如果文件不存在,Android 会自动创建它。创建的文件保存在/data/data/<package name>/files目录,如: /data/data/cn.android.action/files/android.txt ,通过点击Eclipse菜单“Window”-“Show View”-“Other”,在对话窗口中展开android文件夹,选择下面的File Explorer视图,然后在File Explorer视图中展开/data/data/<package name>/files目录就可以看到该文件。
openFileOutput()方法的第二参数用于指定操作模式,有四种模式,分别为: Context.MODE_PRIVATE = 0
Context.MODE_APPEND = 32768
Context.MODE_WORLD_READABLE = 1
Context.MODE_WORLD_WRITEABLE = 2
Context.MODE_PRIVATE:为默认操作模式,代表该文件是私有数据,只能被应用本身访问,在该模式下,写入的内容会覆盖原文件的内容,如果想把新写入的内容追加到原文件中。可以使用Context.MODE_APPEND
Context.MODE_APPEND:模式会检查文件是否存在,存在就往文件追加内容,否则就创建新文件。
Context.MODE_WORLD_READABLE和Context.MODE_WORLD_WRITEABLE用来控制其他应用是否有权限读写该文件。
MODE_WORLD_READABLE:表示当前文件可以被其他应用读取;
MODE_WORLD_WRITEABLE:表示当前文件可以被其他应用写入。
如果希望文件被其他应用读和写,可以传入:
openFileOutput("android.txt", Context.MODE_WORLD_READABLE + Context.MODE_WORLD_WRITEABLE);
android有一套自己的安全模型,当应用程序(.apk)在安装时系统就会分配给他一个userid,当该应用要去访问其他资源比如文件的时候,就需要userid匹配。默认情况下,任何应用创建的文件,sharedpreferences,数据库都应该是私有的(位于/data/data /<package name>/files),其他程序无法访问。除非在创建时指定了Context.MODE_WORLD_READABLE或者 Context.MODE_WORLD_WRITEABLE ,只有这样其他程序才能正确访问。
读取文件内容
如果要打开存放在/data/data/<package name>/files目录应用私有的文件,可以使用Activity提供openFileInput()方法。
FileInputStream inStream = this.getContext().openFileInput("android.txt");
Log.i("FileTest", readInStream(inStream));
readInStream()的方法请看本页下面备注。
或者直接使用文件的绝对路径:
File file = new File("/data/data/cn.android.action/files/android.txt");
FileInputStream inStream = new FileInputStream(file);
Log.i("FileTest", readInStream(inStream));
注意:上面文件路径中的“cn.android.action”为应用所在包,当你在编写代码时应替换为你自己应用使用的包。
对于私有文件只能被创建该文件的应用访问,如果希望文件能被其他应用读和写,可以在创建文件时,指定Context.MODE_WORLD_READABLE和Context.MODE_WORLD_WRITEABLE权限。
Activity还提供了getCacheDir()和getFilesDir()方法:
getCacheDir()方法用于获取/data/data/<package name>/cache目录
getFilesDir()方法用于获取/data/data/<package name>/files目录
public static String readInStream(FileInputStream inStream){
try {
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length = -1;
while((length = inStream.read(buffer)) != -1 ){
outStream.write(buffer, 0, length);
}
outStream.close();
inStream.close();
return outStream.toString();
} catch (IOException e) {
Log.i("FileTest", e.getMessage());
}
return null;
}
把文件存放在
SDCard
使用Activity的openFileOutput()方法保存文件,文件是存放在手机空间上,一般手机的存储空间不是很大,存放些小文件还行,如果要存放像视频这样的大文件,是不可行的。对于像视频这样的大文件,我们可以把它存放在SDCard。 SDCard是干什么的?你可以把它看作是移动硬盘或U盘。
在模拟器中使用SDCard,你需要先创建一张SDCard卡(当然不是真的SDCard,只是镜像文件)。创建SDCard可以在Eclipse创建模拟器时随同创建,也可以使用DOS命令进行创建,如下:
在Dos窗口中进入android SDK安装路径的tools目录,输入以下命令创建一张容量为2G的SDCard,文件后缀可以随便取,建议使用.img:
mksdcard 2048M D:\AndroidTool\sdcard.img
在程序中访问SDCard,你需要申请访问SDCard的权限。
在AndroidManifest.xml中加入访问SDCard的权限如下:
<!-- 在SDCard中创建与删除文件权限 -->
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
<!-- 往SDCard写入数据权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
要往SDCard存放文件,程序必须先判断手机是否装有SDCard,并且可以进行读写。
注意:访问SDCard必须在AndroidManifest.xml中加入访问SDCard的权限
if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
File sdCardDir = Environment.getExternalStorageDirectory();//获取SDCard目录
File saveFile = new File(sdCardDir, “android.txt”);
FileOutputStream outStream = new FileOutputStream(saveFile);
outStream.write("安桌".getBytes());
outStream.close();
}
Environment.getExternalStorageState()方法用于获取SDCard的状态,如果手机装有SDCard,并且可以进行读写,那么方法返回的状态等于Environment.MEDIA_MOUNTED。
Environment.getExternalStorageDirectory()方法用于获取SDCard的目录,当然要获取SDCard的目录,你也可以这样写:
File sdCardDir = new File("/sdcard"); //获取SDCard目录
File saveFile = new File(sdCardDir, "android.txt");
//上面两句代码可以合成一句: File saveFile = new File("/sdcard/android.txt");
FileOutputStream outStream = new FileOutputStream(saveFile);
outStream.write("安桌test".getBytes());
outStream.close();
cuihai |
2011-03-03 12:10 |
使用SAX或者DOM或者pull解析XML文件 在Android平台上可以使用Simple API for XML(SAX) 、 Document Object Model(DOM)和Android附带的pull解析器解析XML文件。 下面是本例子要解析的XML文件: 文件名称:android.xml <?xml version="1.0" encoding="UTF-8"?> <persons> <person id="23"> <name>李明</name> <age>30</age> </person> <person id="20"> <name>李向梅</name> <age>25</age> </person> </persons> 使用SAX读取XML文件 SAX是一个解析速度快并且占用内存少的xml解析器,非常适合用于Android等移动设备。 SAX解析XML文件采用的是事件驱动,也就是说,它并不需要解析完整个文档,在按内容顺序解析文档的过程中,SAX会判断当前读到的字符是否合法XML 语法中的某部分,如果符合就会触发事件。所谓事件,其实就是一些回调(callback)方法,这些方法(事件)定义在ContentHandler接口。下面是一些ContentHandler接口常用的方法: startDocument() 当遇到文档的开头的时候,调用这个方法,可以在其中做一些预处理的工作。 endDocument() 和上面的方法相对应,当文档结束的时候,调用这个方法,可以在其中做一些善后的工作。 startElement(String namespaceURI, String localName, String qName, Attributes atts) 当读到一个开始标签的时候,会触发这个方法。namespaceURI就是命名空间,localName是不带命名空间前缀的标签名,qName是带命名空间前缀的标签名。通过atts可以得到所有的属性名和相应的值。要注意的是SAX中一个重要的特点就是它的流式处理,当遇到一个标签的时候,它并不会纪录下以前所碰到的标签,也就是说,在startElement()方法中,所有你所知道的信息,就是标签的名字和属性,至于标签的嵌套结构,上层标签的名字,是否有子元属等等其它与结构相关的信息,都是不得而知的,都需要你的程序来完成。这使得SAX在编程处理上没有DOM来得那么方便。 endElement(String uri, String localName, String name) 这个方法和上面的方法相对应,在遇到结束标签的时候,调用这个方法。 characters(char[] ch, int start, int length) 这个方法用来处理在XML文件中读到的内容,第一个参数为文件的字符串内容,后面两个参数是读到的字符串在这个数组中的起始位置和长度,使用new String(ch,start,length)就可以获取内容。 只要为SAX提供实现ContentHandler接口的类,那么该类就可以得到通知事件(实际上就是SAX调用了该类中的回调方法)。因为 ContentHandler是一个接口,在使用的时候可能会有些不方便,因此,SAX还为其制定了一个Helper类:DefaultHandler,它实现了ContentHandler接口,但是其所有的方法体都为空,在实现的时候,你只需要继承这个类,然后重载相应的方法即可。使用SAX解析 android.xml的代码如下: public static List<Person> readXML(InputStream inStream) { try { SAXParserFactory spf = SAXParserFactory.newInstance(); SAXParser saxParser = spf.newSAXParser(); //创建解析器 //设置解析器的相关特性,http://xml.org/sax/features/namespaces = true 表示开启命名空间特性 //saxParser.setProperty("http://xml.org/sax/features/namespaces",true); XMLContentHandler handler = new XMLContentHandler(); saxParser.parse(inStream, handler); inStream.close(); return handler.getPersons(); } catch (Exception e) { e.printStackTrace(); } return null; } import java.util.ArrayList; import java.util.List; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; import cn.android.xml.domain.Person; public class XMLContentHandler extends DefaultHandler { private List<Person> persons = null; private Person currentPerson; private String tagName = null;//当前解析的元素标签 public List<Person> getPersons() { return persons; } /* 接收文档的开始的通知。 */ @Override public void startDocument() throws SAXException { persons = new ArrayList<Person>(); } /* 接收字符数据的通知。 */ @Override public void characters(char[] ch, int start, int length) throws SAXException { if(tagName!=null){ String data = new String(ch, start, length); if(tagName.equals("name")){ this.currentPerson.setName(data); }else if(tagName.equals("age")){ this.currentPerson.setAge(Short.parseShort(data)); } } } /* * 接收元素开始的通知。 * 参数意义如下: * namespaceURI:元素的命名空间 * localName :元素的本地名称(不带前缀) * qName :元素的限定名(带前缀) * atts :元素的属性集合 */ @Override public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException { if(localName.equals("person")){ currentPerson = new Person(); currentPerson.setId(Integer.parseInt(atts.getValue("id"))); } this.tagName = localName; } /* * 接收文档的结尾的通知。 * 参数意义如下: * uri :元素的命名空间 * localName :元素的本地名称(不带前缀) * name :元素的限定名(带前缀) */ @Override public void endElement(String uri, String localName, String name) throws SAXException { if(localName.equals("person")){ persons.add(currentPerson); currentPerson = null; } this.tagName = null; } } 使用DOM读取XML文件 除了可以使用 SAX解析XML文件,大家也可以使用熟悉的DOM来解析XML文件。 DOM解析XML文件时,会将XML文件的所有内容读取到内存中,然后允许您使用DOM API遍历XML树、检索所需的数据。使用DOM操作XML的代码看起来比较直观,并且,在某些方面比基于SAX的实现更加简单。但是,因为DOM需要将 XML文件的所有内容读取到内存中,所以内存的消耗比较大,特别对于运行Android的移动设备来说,因为设备的资源比较宝贵,所以建议还是采用SAX 来解析XML文件,当然,如果XML文件的内容比较小采用DOM是可行的。 import cn.android.xml.domain.Person; /** 使用Dom解析xml文件 */ public class DomXMLReader { public static List<Person> readXML(InputStream inStream) { List<Person> persons = new ArrayList<Person>(); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); try { DocumentBuilder builder = factory.newDocumentBuilder(); Document dom = builder.parse(inStream); Element root = dom.getDocumentElement(); NodeList items = root.getElementsByTagName("person");//查找所有person节点 for (int i = 0; i < items.getLength(); i++) { Person person = new Person(); //得到第一个person节点 Element personNode = (Element) items.item(i); //获取person节点的id属性值 person.setId(new Integer(personNode.getAttribute("id"))); //获取person节点下的所有子节点(标签之间的空白节点和name/age元素) NodeList childsNodes = personNode.getChildNodes(); for (int j = 0; j < childsNodes.getLength(); j++) { Node node = (Node) childsNodes.item(j); //判断是否为元素类型 if(node.getNodeType() == Node.ELEMENT_NODE){ Element childNode = (Element) node; //判断是否name元素 if ("name".equals(childNode.getNodeName())) { //获取name元素下Text节点,然后从Text节点获取数据 person.setName(childNode.getFirstChild().getNodeValue()); } else if (“age”.equals(childNode.getNodeName())) { person.setAge(new Short(childNode.getFirstChild().getNodeValue())); } } } persons.add(person); } inStream.close(); } catch (Exception e) { e.printStackTrace(); } return persons; } 使用Pull解析器读取XML文件 除了可以使用 SAX和DOM解析XML文件,大家也可以使用Android内置的Pull解析器解析XML文件。 Pull解析器的运行方式与 SAX 解析器相似。它提供了类似的事件,如:开始元素和结束元素事件,使用parser.next()可以进入下一个元素并触发相应事件。事件将作为数值代码被发送,因此可以使用一个switch对感兴趣的事件进行处理。当元素开始解析时,调用parser.nextText()方法可以获取下一个Text类型元素的值。 public class PullXMLReader { public static List<Person> readXML(InputStream inStream) { XmlPullParser parser = Xml.newPullParser(); try { parser.setInput(inStream, "UTF-8"); int eventType = parser.getEventType(); Person currentPerson = null; List<Person> persons = null; while (eventType != XmlPullParser.END_DOCUMENT) { switch (eventType) { case XmlPullParser.START_DOCUMENT://文档开始事件,可以进行数据初始化处理 persons = new ArrayList<Person>(); break; case XmlPullParser.START_TAG://开始元素事件 String name = parser.getName(); if (name.equalsIgnoreCase("person")) { currentPerson = new Person(); currentPerson.setId(new Integer(parser.getAttributeValue(null, "id"))); } else if (currentPerson != null) { if (name.equalsIgnoreCase("name")) { currentPerson.setName(parser.nextText());// 如果后面是Text元素,即返回它的值 } else if (name.equalsIgnoreCase("age")) { currentPerson.setAge(new Short(parser.nextText())); } } break; case XmlPullParser.END_TAG://结束元素事件 if (parser.getName().equalsIgnoreCase("person") && currentPerson != null) { persons.add(currentPerson); currentPerson = null; } break; } eventType = parser.next(); } inStream.close(); return persons; } catch (Exception e) { e.printStackTrace(); } return null; } } 使用Pull解析器生成XML文件 有些时候,我们需要生成一个XML文件,生成XML文件的方法有很多,如:可以只使用一个StringBuilder组拼XML内容,然后把内容写入到文件中;或者使用DOM API生成XML文件,或者也可以使用pull解析器生成XML文件,这里推荐大家使用Pull解析器。 使用Pull解析器生成一个与android.xml文件内容相同的myandroid.xml文件,代码在本页下方备注 使用代码如下(生成XML文件): File xmlFile = new File("myandroid.xml"); FileOutputStream outStream = new FileOutputStream(xmlFile); OutputStreamWriter outStreamWriter = new OutputStreamWriter(outStream, "UTF-8"); BufferedWriter writer = new BufferedWriter(outStreamWriter); writeXML(persons, writer); writer.flush(); writer.close(); 如果只想得到生成的xml字符串内容,可以使用StringWriter: StringWriter writer = new StringWriter(); writeXML(persons, writer); String content = writer.toString(); public static String writeXML(List<Person> persons, Writer writer){ XmlSerializer serializer = Xml.newSerializer(); try { serializer.setOutput(writer); serializer.startDocument("UTF-8", true); //第一个参数为命名空间,如果不使用命名空间,可以设置为null serializer.startTag("", "persons"); for (Person person : persons){ serializer.startTag("", "person"); serializer.attribute("", "id", person.getId().toString()); serializer.startTag("", "name"); serializer.text(person.getName()); serializer.endTag("", "name"); serializer.startTag("", "age"); serializer.text(person.getAge().toString()); serializer.endTag("", "age"); serializer.endTag("", "person"); } serializer.endTag("", "persons"); serializer.endDocument(); return writer.toString(); } catch (Exception e) { e.printStackTrace(); } return null; } 使用SharedPreferences进行数据存储 很多时候我们开发的软件需要向用户提供软件参数设置功能,例如我们常用的QQ,用户可以设置是否允许陌生人添加自己为好友。对于软件配置参数的保存,如果是window软件通常我们会采用ini文件进行保存,如果是j2se应用,我们会采用properties属性文件进行保存。如果是 Android应用,我们最适合采用什么方式保存软件配置参数呢?Android平台给我们提供了一个SharedPreferences类,它是一个轻量级的存储类,特别适合用于保存软件配置参数。使用SharedPreferences保存数据,其背后是用xml文件存放数据,文件存放在/data /data/<package name>/shared_prefs目录下: SharedPreferences sharedPreferences=getSharedPreferences("android", Context.MODE_PRIVATE); Editor editor = sharedPreferences.edit();//获取编辑器 editor.putString("name", "安桌"); editor.putInt("age", 4); editor.commit();//提交修改 生成的android.xml文件内容如下: <?xml version='1.0' encoding='utf-8' standalone='yes' ?> <map> <string name="name">安桌</string> <int name="age" value="4" /> </map> 因为SharedPreferences背后是使用xml文件保存数据,getSharedPreferences(name,mode)方法的第一个参数用于指定该文件的名称,名称不用带后缀,后缀会由Android自动加上。方法的第二个参数指定文件的操作模式,共有四种操作模式,这四种模式前面介绍使用文件方式保存数据时已经讲解过。如果希望SharedPreferences背后使用的xml文件能被其他应用读和写,可以指定 Context.MODE_WORLD_READABLE和Context.MODE_WORLD_WRITEABLE权限。 另外Activity还提供了另一个getPreferences(mode)方法操作SharedPreferences,这个方法默认使用当前类不带包名的类名作为文件的名称。 访问SharedPreferences中的数据 访问SharedPreferences中的数据代码如下: SharedPreferences sharedPreferences=getSharedPreferences("android", Context.MODE_PRIVATE); //getString()第二个参数为缺省值,如果preference中不存在该key,将返回缺省值 String name = sharedPreferences.getString("name", ""); int age = sharedPreferences.getInt("age", 1); 如果访问其他应用中的Preference,前提条件是:该preference创建时指定了 Context.MODE_WORLD_READABLE或者Context.MODE_WORLD_WRITEABLE权限。如:有个<package name>为cn.android.action的应用使用下面语句创建了preference。 getSharedPreferences("android", Context.MODE_WORLD_READABLE); 其他应用要访问上面应用的preference,首先需要创建上面应用的Context,然后通过Context 访问preference ,访问preference时会在应用所在包下的shared_prefs目录找到preference : Context otherAppsContext = createPackageContext("cn.android.action", Context.CONTEXT_IGNORE_SECURITY); SharedPreferences sharedPreferences = otherAppsContext.getSharedPreferences("android", Context.MODE_WORLD_READABLE); String name = sharedPreferences.getString("name", ""); int age = sharedPreferences.getInt("age", 0); 如果不通过创建Context访问其他应用的preference,可以以读取xml文件方式直接访问其他应用preference对应的xml文件,如: File xmlFile = new File(“/data/data/<package name>/shared_prefs/android.xml”);//<package name>应替换成应用的包名 使用嵌入式关系型SQLite数据库存储数据 除了可以使用文件或SharedPreferences存储数据,还可以选择使用SQLite数据库存储数据。 在Android平台上,集成了一个嵌入式关系型数据库—SQLite,SQLite3支持 NULL、INTEGER、REAL(浮点数字)、TEXT(字符串文本)和BLOB(二进制对象)数据类型,虽然它支持的类型只有五种,但实际上 sqlite3也接受varchar(n)、char(n)、decimal(p,s) 等数据类型,只不过在运算或保存时会转成对应的五种数据类型。 SQLite最大的特点是你可以保存任何类型的数据到任何字段中,无论这列声明的数据类型是什么。例如:可以在Integer类型的字段中存放字符串,或者在布尔型字段中存放浮点数,或者在字符型字段中存放日期型值。 但有一种情况例外:定义为INTEGER PRIMARY KEY的字段只能存储64位整数, 当向这种字段中保存除整数以外的数据时,将会产生错误。 另外, SQLite 在解析CREATE TABLE 语句时,会忽略 CREATE TABLE 语句中跟在字段名后面的数据类型信息,如下面语句会忽略 name字段的类型信息: CREATE TABLE person (personid integer primary key autoincrement, name varchar(20)) SQLite可以解析大部分标准SQL语句,如: 查询语句:select * from 表名 where 条件子句 group by 分组字句 having ... order by 排序子句 如:select * from person select * from person order by id desc select name from person group by name having count(*)>1 分页SQL与mysql类似,下面SQL语句获取5条记录,跳过前面3条记录 select * from Account limit 5 offset 3 或者 select * from Account limit 3,5 插入语句:insert into 表名(字段列表) values(值列表)。如: insert into person(name, age) values(‘传智’,3) 更新语句:update 表名 set 字段名=值 where 条件子句。如: update person set name=‘传智‘ where id=10 删除语句:delete from 表名 where 条件子句。如:delete from person where id=10 使用SQLiteOpenHelper对数据库进行版本管理 因为我们开发的软件可能会安装在成百上千个用户的手机上,如果应用使用到了SQLite数据库,我们必须在用户初次使用软件时创建出应用使用到的数据库表结构及添加一些初始化记录,另外在软件升级的时候,也需要对数据表结构进行更新。那么,我们如何才能实现在用户初次使用或升级软件时自动在用户的手机上创建出应用需要的数据库表呢?总不能在每个需要安装此软件的手机上通过手工方式创建数据库表吧?因为这种需求是每个数据库应用都要面临的,所以在 Android系统,为我们提供了一个名为SQLiteOpenHelper的抽象类,必须继承它才能使用,它是通过对数据库版本进行管理来实现前面提出的需求。 为了实现对数据库版本进行管理,SQLiteOpenHelper类提供了两个重要的方法,分别是 onCreate(SQLiteDatabase db)和onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion),前者用于初次使用软件时生成数据库表,后者用于升级软件时更新数据库表结构。当调用SQLiteOpenHelper的 getWritableDatabase()或者getReadableDatabase()方法获取用于操作数据库的SQLiteDatabase实例的时候,如果数据库不存在,Android系统会自动生成一个数据库,接着调用onCreate()方法,onCreate()方法在初次生成数据库时才会被调用,在onCreate()方法里可以生成数据库表结构及添加一些应用使用到的初始化数据。onUpgrade()方法在数据库的版本发生变化时会被调用,一般在软件升级时才需改变版本号,而数据库的版本是由程序员控制的,假设数据库现在的版本是1,由于业务的变更,修改了数据库表结构,这时候就需要升级软件,升级软件时希望更新用户手机里的数据库表结构,为了实现这一目的,可以把原来的数据库版本设置为2(有同学问设置为3行不行?当然可以,如果你愿意,设置为100也行),并且在onUpgrade()方法里面实现表结构的更新。当软件的版本升级次数比较多,这时在onUpgrade()方法里面可以根据原版号和目标版本号进行判断,然后作出相应的表结构及数据更新。 getWritableDatabase()和getReadableDatabase()方法都可以获取一个用于操作数据库的 SQLiteDatabase实例。但getWritableDatabase() 方法以读写方式打开数据库,一旦数据库的磁盘空间满了,数据库就只能读而不能写,倘若使用的是getWritableDatabase() 方法就会出错。getReadableDatabase()方法先以读写方式打开数据库,如果数据库的磁盘空间满了,就会打开失败,当打开失败后会继续尝试以只读方式打开数据库。 public class DatabaseHelper extends SQLiteOpenHelper { //类没有实例化,是不能用作父类构造器的参数,必须声明为静态 private static final String name = "android"; //数据库名称 private static final int version = 1; //数据库版本 public DatabaseHelper(Context context) { //第三个参数CursorFactory指定在执行查询时获得一个游标实例的工厂类,设置为null,代表使用系统默认的工厂类 super(context, name, null, version); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL("CREATE TABLE IF NOT EXISTS person (personid integer primary key autoincrement, name varchar(20), age INTEGER)"); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL("DROP TABLE IF EXISTS person"); onCreate(db); } } 上面onUpgrade()方法在数据库版本每次发生变化时都会把用户手机上的数据库表删除,然后再重新创建。一般在实际项目中是不能这样做的,正确的做法是在更新数据库表结构时,还要考虑用户存放于数据库中的数据不会丢失。 使用SQLiteDatabase操作SQLite数据库 Android提供了一个名为SQLiteDatabase的类,该类封装了一些操作数据库的API,使用该类可以完成对数据进行添加 (Create)、查询(Retrieve)、更新(Update)和删除(Delete)操作(这些操作简称为CRUD)。对 SQLiteDatabase的学习,我们应该重点掌握execSQL()和rawQuery()方法。 execSQL()方法可以执行insert、delete、update和CREATE TABLE之类有更改行为的SQL语句; rawQuery()方法可以执行select语句。 execSQL()方法的使用例子: SQLiteDatabase db = ....; db.execSQL("insert into person(name, age) values('安桌', 4)"); db.close(); 执行上面SQL语句会往person表中添加进一条记录,在实际应用中, 语句中的“安桌”这些参数值会由用户输入界面提供,如果把用户输入的内容原样组拼到上面的insert语句, 当用户输入的内容含有单引号时,组拼出来的SQL语句就会存在语法错误。要解决这个问题需要对单引号进行转义,也就是把单引号转换成两个单引号。有些时候用户往往还会输入像“ & ”这些特殊SQL符号,为保证组拼好的SQL语句语法正确,必须对SQL语句中的这些特殊SQL符号都进行转义,显然,对每条SQL语句都做这样的处理工作是比较烦琐的。 SQLiteDatabase类提供了一个重载后的execSQL(String sql, Object[] bindArgs)方法,使用这个方法可以解决前面提到的问题,因为这个方法支持使用占位符参数(?)。使用例子如下: SQLiteDatabase db = ....; db.execSQL("insert into person(name, age) values(?,?)", new Object[]{"安桌", 4}); db.close(); execSQL(String sql, Object[] bindArgs)方法的第一个参数为SQL语句,第二个参数为SQL语句中占位符参数的值,参数值在数组中的顺序要和占位符的位置对应。 SQLiteDatabase的rawQuery() 用于执行select语句,使用例子如下: SQLiteDatabase db = ....; Cursor cursor = db.rawQuery(“select * from person”, null); while (cursor.moveToNext()) { int personid = cursor.getInt(0); //获取第一列的值,第一列的索引从0开始 String name = cursor.getString(1);//获取第二列的值 int age = cursor.getInt(2);//获取第三列的值 } cursor.close(); db.close(); rawQuery()方法的第一个参数为select语句;第二个参数为select语句中占位符参数的值,如果select语句没有使用占位符,该参数可以设置为null。带占位符参数的select语句使用例子如下: Cursor cursor = db.rawQuery("select * from person where name like ? and age=?", new String[]{"%传智%", "4"}); Cursor是结果集游标,用于对结果集进行随机访问,如果大家熟悉jdbc, 其实Cursor与JDBC中的ResultSet作用很相似。使用moveToNext()方法可以将游标从当前行移动到下一行,如果已经移过了结果集的最后一行,返回结果为false,否则为true。另外Cursor 还有常用的moveToPrevious()方法(用于将游标从当前行移动到上一行,如果已经移过了结果集的第一行,返回值为false,否则为true )、moveToFirst()方法(用于将游标移动到结果集的第一行,如果结果集为空,返回值为false,否则为true )和moveToLast()方法(用于将游标移动到结果集的最后一行,如果结果集为空,返回值为false,否则为true ) 。 除了前面给大家介绍的execSQL()和rawQuery()方法, SQLiteDatabase还专门提供了对应于添加、删除、更新、查询的操作方法: insert()、delete()、update()和query() 。这些方法实际上是给那些不太了解SQL语法的菜鸟使用的,对于熟悉SQL语法的程序员而言,直接使用execSQL()和rawQuery()方法执行 SQL语句就能完成数据的添加、删除、更新、查询操作。 Insert()方法用于添加数据,各个字段的数据使用ContentValues进行存放。 ContentValues类似于MAP,相对于MAP,它提供了存取数据对应的put(String key, Xxx value)和getAsXxx(String key)方法, key为字段名称,value为字段值,Xxx指的是各种常用的数据类型,如:String、Integer等。 SQLiteDatabase db = databaseHelper.getWritableDatabase(); ContentValues values = new ContentValues(); values.put("name", "安桌"); values.put("age", 4); long rowid = db.insert(“person”, null, values);//返回新添记录的行号,与主键id无关 不管第三个参数是否包含数据,执行Insert()方法必然会添加一条记录,如果第三个参数为空,会添加一条除主键之外其他字段值为Null的记录。Insert()方法内部实际上通过构造insert语句完成数据的添加,Insert()方法的第二个参数用于指定空值字段的名称,相信大家对此参数会感到疑惑,此参数的作用是干嘛的?是这样的:如果第三个参数values 为Null或者元素个数为0, Insert()方法必然要添加一条除了主键之外其它字段为Null值的记录,为了满足这条insert语句的语法, insert语句必须给定一个字段名,如:insert into person(name) values(NULL),倘若不给定字段名 , insert语句就成了这样: insert into person() values(),显然这不满足标准SQL的语法。对于字段名,建议使用主键之外的字段,如果使用了INTEGER类型的主键字段,执行类似insert into person(personid) values(NULL)的insert语句后,该主键字段值也不会为NULL。如果第三个参数values 不为Null并且元素的个数大于0 ,可以把第二个参数设置为null。 delete()方法的使用: SQLiteDatabase db = databaseHelper.getWritableDatabase(); db.delete("person", "personid<?", new String[]{"2"}); db.close(); 上面代码用于从person表中删除personid小于2的记录。 update()方法的使用: SQLiteDatabase db = databaseHelper.getWritableDatabase(); ContentValues values = new ContentValues(); values.put(“name”, “安桌”);//key为字段名,value为值 db.update("person", values, "personid=?", new String[]{"1"}); db.close(); 上面代码用于把person表中personid等于1的记录的name字段的值改为“安桌”。 query()方法实际上是把select语句拆分成了若干个组成部分,然后作为方法的输入参数: SQLiteDatabase db = databaseHelper.getWritableDatabase(); Cursor cursor = db.query("person", new String[]{"personid,name,age"}, "name like ?", new String[]{"%传智%"}, null, null, "personid desc", "1,2"); while (cursor.moveToNext()) { int personid = cursor.getInt(0); //获取第一列的值,第一列的索引从0开始 String name = cursor.getString(1);//获取第二列的值 int age = cursor.getInt(2);//获取第三列的值 } cursor.close(); db.close(); 上面代码用于从person表中查找name字段含有“传智”的记录,匹配的记录按personid降序排序,对排序后的结果略过第一条记录,只获取2条记录。 query(table, columns, selection, selectionArgs, groupBy, having, orderBy, limit)方法各参数的含义: table:表名。相当于select语句from关键字后面的部分。如果是多表联合查询,可以用逗号将两个表名分开。 columns:要查询出来的列名。相当于select语句select关键字后面的部分。 selection:查询条件子句,相当于select语句where关键字后面的部分,在条件子句允许使用占位符“?” selectionArgs:对应于selection语句中占位符的值,值在数组中的位置与占位符在语句中的位置必须一致,否则就会有异常。 groupBy:相当于select语句group by关键字后面的部分 having:相当于select语句having关键字后面的部分 orderBy:相当于select语句order by关键字后面的部分,如:personid desc, age asc; limit:指定偏移量和获取的记录数,相当于select语句limit关键字后面的部分。 public class DatabaseHelper extends SQLiteOpenHelper { private static final String name = "android"; //数据库名称 private static final int version = 1; //数据库版本 ......略 } public class HelloActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { ...... Button button =(Button) this.findViewById(R.id.button); button.setOnClickListener(new View.OnClickListener(){ public void onClick(View v) { DatabaseHelper databaseHelper = new DatabaseHelper(HelloActivity.this); SQLiteDatabase db = databaseHelper.getWritableDatabase(); db.execSQL("insert into person(name, age) values(?,?)", new Object[]{"安桌", 4}); db.close(); }}); } } 第一次调用getWritableDatabase()或getReadableDatabase()方法后,SQLiteOpenHelper会缓存当前的SQLiteDatabase实例,SQLiteDatabase实例正常情况下会维持数据库的打开状态,所以在你不再需要SQLiteDatabase实例时,请及时调用close()方法释放资源。一旦SQLiteDatabase实例被缓存,多次调用getWritableDatabase()或getReadableDatabase()方法得到的都是同一实例。 |
|
cuihai |
2011-03-03 12:11 |
使用事务操作SQLite数据库 使用SQLiteDatabase的beginTransaction()方法可以开启一个事务,程序执行到endTransaction() 方法时会检查事务的标志是否为成功,如果程序执行到endTransaction()之前调用了setTransactionSuccessful() 方法设置事务的标志为成功则提交事务,如果没有调用setTransactionSuccessful() 方法则回滚事务。使用例子如下: SQLiteDatabase db = ....; db.beginTransaction();//开始事务 try { db.execSQL("insert into person(name, age) values(?,?)", new Object[]{"安桌", 4}); db.execSQL("update person set name=? where personid=?", new Object[]{"传智", 1}); db.setTransactionSuccessful();//调用此方法会在执行到endTransaction() 时提交当前事务,如果不调用此方法会回滚事务 } finally { db.endTransaction();//由事务的标志决定是提交事务,还是回滚事务 } db.close(); 上面两条SQL语句在同一个事务中执行。 使用ContentProvider共享数据 当应用继承ContentProvider类,并重写该类用于提供数据和存储数据的方法,就可以向其他应用共享其数据。虽然使用其他方法也可以对外共享数据,但数据访问方式会因数据存储的方式而不同,如:采用文件方式对外共享数据,需要进行文件操作读写数据;采用 sharedpreferences共享数据,需要使用sharedpreferences API读写数据。而使用ContentProvider共享数据的好处是统一了数据访问方式。 当应用需要通过ContentProvider对外共享数据时,第一步需要继承ContentProvider并重写下面方法: public class PersonContentProvider extends ContentProvider{ public boolean onCreate() public Uri insert(Uri uri, ContentValues values) public int delete(Uri uri, String selection, String[] selectionArgs) public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) public String getType(Uri uri)} 第二步需要在AndroidManifest.xml使用<provider>对该ContentProvider进行配置,为了能让其他应用找到该ContentProvider , ContentProvider 采用了authorities(主机名/域名)对它进行唯一标识,你可以把 ContentProvider看作是一个网站(想想,网站也是提供数据者),authorities 就是他的域名: <manifest .... > <application android:icon="@drawable/icon" android:label="@string/app_name"> <provider android:name=".PersonContentProvider" android:authorities="cn.android.provider.personprovider"/> </application> </manifest> 注意:一旦应用继承了ContentProvider类,后面我们就会把这个应用称为ContentProvider(内容提供者)。 public class PersonContentProvider extends ContentProvider{ //数据集的MIME类型字符串则应该以vnd.android.cursor.dir/开头 public static final String PERSONS_TYPE = "vnd.android.cursor.dir/person"; //单一数据的MIME类型字符串应该以vnd.android.cursor.item/开头 public static final String PERSONS_ITEM_TYPE = "vnd.android.cursor.item/person"; public static final String AUTHORITY = "cn.android.provider.personprovider";//主机名 /*自定义匹配码*/ public static final int PERSONS = 1; /*自定义匹配码*/ public static final int PERSON = 2; public static final Uri PERSONS_URI = Uri.parse("content://" + AUTHORITY + "/person"); /*这里UriMatcher是用来匹配Uri的类,使用match()方法匹配路径时返回匹配码*/ private static final UriMatcher sMatcher; static { sMatcher = new UriMatcher(UriMatcher.NO_MATCH);//常量UriMatcher.NO_MATCH表示不匹配任何路径的返回码 //如果match()方法匹配content://cn.android.provider.personprovider/person路径,返回匹配码为PERSONS sMatcher.addURI(AUTHORITY, "person", PERSONS); //如果match()方法匹配content://cn.android.provider.personprovider/person/230路径,返回匹配码为PERSON sMatcher.addURI(AUTHORITY, "person/#", PERSON); } private DatabaseHelper databaseHelper; @Override public boolean onCreate() { databaseHelper = new DatabaseHelper(this.getContext()); return true; } @Override public Uri insert(Uri uri, ContentValues values) { SQLiteDatabase db = databaseHelper.getWritableDatabase(); if (sMatcher.match(uri) != PERSONS) { throw new IllegalArgumentException("Unknown URI " + uri); } long rowId = db.insert("person", "personid", values);//往person表添加一条记录 db.close(); if(rowId>0){//如果添加成功 //ContentUris是content URI的一个辅助类。下面方法负责把rowId和PERSONS_URI连接成一个新的Uri, //生成的Uri如:content://cn.android.provider.personprovider/person/10 return ContentUris.withAppendedId(PERSONS_URI, rowId); } throw new SQLException("Failed to insert row into " + uri);//抛出添加失败信息 } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { SQLiteDatabase db = databaseHelper.getWritableDatabase(); int count = 0; switch (sMatcher.match(uri)) { case PERSONS: count = db.delete("person", selection, selectionArgs); break; case PERSON: //下面的方法用于从URI中解析出id,对这样的路径content://cn.android.provider.personprovider/person/10 进行解析,返回值为10 long personid = ContentUris.parseId(uri); String where = "personid="+ personid;//删除指定id的记录 where += !TextUtils.isEmpty(selection) ? " and ("+ selection +")" : "";//把其它条件附加上 count = db.delete("person", where, selectionArgs); break; default: throw new IllegalArgumentException("Unknown URI " + uri); } db.close(); return count; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { SQLiteDatabase db = databaseHelper.getWritableDatabase(); int count = 0; switch (sMatcher.match(uri)) { case PERSONS: count = db.update("person", values, selection, selectionArgs); break; case PERSON: //下面的方法用于从URI中解析出id,对这样的路径content://cn.android.provider.personprovider/person/10 进行解析,返回值为10 long personid = ContentUris.parseId(uri); String where = "personid="+ personid;//删除指定id的记录 where += !TextUtils.isEmpty(selection) ? " and ("+ selection +")" : "";//把其它条件附加上 count = db.update("person", values, where, selectionArgs); break; default: throw new IllegalArgumentException("Unknown URI " + uri); } db.close(); return count; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { SQLiteDatabase db = databaseHelper.getReadableDatabase(); Cursor cursor; switch (sMatcher.match(uri)) { case PERSONS: cursor = db.query("person", projection, selection, selectionArgs, null, null, sortOrder); break; case PERSON: //下面的方法用于从URI中解析出id,对这样的路径content://cn.android.provider.personprovider/person/10 进行解析,返回值为10 long personid = ContentUris.parseId(uri); String where = "personid="+ personid;//删除指定id的记录 where += !TextUtils.isEmpty(selection) ? " and ("+ selection +")" : "";//把其它条件附加上 cursor = db.query("person", projection, where, selectionArgs, null, null, sortOrder); break; default: throw new IllegalArgumentException("Unknown URI " + uri); } cursor.getCount();//在数据库关闭前获取所有数据 db.close(); return cursor; } @Override public String getType(Uri uri) { switch (sMatcher.match(uri)) { case PERSONS: return PERSONS_TYPE; case PERSON: return PERSONS_ITEM_TYPE; default: throw new IllegalArgumentException("Unknown URI " + uri); } } } Uri介绍 Uri代表了要操作的数据,Uri主要包含了两部分信息:1》需要操作的ContentProvider ,2》对ContentProvider中的什么数据进行操作,一个Uri由以下几部分组成: ContentProvider(内容提供者)的scheme已经由Android所规定, scheme为:content:// 主机名(或叫Authority)用于唯一标识这个ContentProvider,外部调用者可以根据这个标识来找到它。 路径(path)可以用来表示我们要操作的数据,路径的构建应根据业务而定,如下: 要操作person表中id为10的记录,可以构建这样的路径:/person/10 要操作person表中id为10的记录的name字段, person/10/name 要操作person表中的所有记录,可以构建这样的路径:/person 要操作xxx表中的记录,可以构建这样的路径:/xxx 当然要操作的数据不一定来自数据库,也可以是文件等他存储方式,如下: 要操作xml文件中person节点下的name节点,可以构建这样的路径:/person/name 如果要把一个字符串转换成Uri,可以使用Uri类中的parse()方法,如下: Uri uri = Uri.parse("content://cn.android.provider.personprovider/person") |
|
cuihai |
2011-03-03 12:11 |
UriMatcher类使用介绍 因为Uri代表了要操作的数据,所以我们经常需要解析Uri,并从Uri中获取数据。Android系统提供了两个用于操作Uri的工具类,分别为UriMatcher 和ContentUris 。掌握它们的使用,会便于我们的开发工作。 UriMatcher类用于匹配Uri,它的用法如下: 首先第一步把你需要匹配Uri路径全部给注册上,如下: //常量UriMatcher.NO_MATCH表示不匹配任何路径的返回码 UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH); //如果match()方法匹配content://cn.android.provider.personprovider/person路径,返回匹配码为1 sMatcher.addURI(“cn.android.provider.personprovider”, “person”, 1);//添加需要匹配uri,如果匹配就会返回匹配码 //如果match()方法匹配content://cn.android.provider.personprovider/person/230路径,返回匹配码为2 sMatcher.addURI(“cn.android.provider.personprovider”, “person/#”, 2);//#号为通配符 switch (sMatcher.match(Uri.parse("content://cn.android.provider.personprovider/person/10"))) { case 1 break; case 2 break; default://不匹配 break; } 注册完需要匹配的Uri后,就可以使用sMatcher.match(uri)方法对输入的Uri进行匹配,如果匹配就返回匹配码,匹配码是调用addURI()方法传入的第三个参数,假设匹配content://cn.android.provider.personprovider /person路径,返回的匹配码为1 ContentUris类使用介绍 ContentUris类用于获取Uri路径后面的ID部分,它有两个比较实用的方法: withAppendedId(uri, id)用于为路径加上ID部分: Uri uri = Uri.parse("content://cn.android.provider.personprovider/person") Uri resultUri = ContentUris.withAppendedId(uri, 10); //生成后的Uri为:content://cn.android.provider.personprovider/person/10 parseId(uri)方法用于从路径中获取ID部分: Uri uri = Uri.parse("content://cn.android.provider.personprovider/person/10") long personid = ContentUris.parseId(uri);//获取的结果为:10 使用ContentProvider共享数据 ContentProvider类主要方法的作用: public boolean onCreate() 该方法在ContentProvider创建后就会被调用, Android开机后, ContentProvider在其它应用第一次访问它时才会被创建。 public Uri insert(Uri uri, ContentValues values) 该方法用于供外部应用往ContentProvider添加数据。 public int delete(Uri uri, String selection, String[] selectionArgs) 该方法用于供外部应用从ContentProvider删除数据。 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) 该方法用于供外部应用更新ContentProvider中的数据。 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) 该方法用于供外部应用从ContentProvider中获取数据。 public String getType(Uri uri) 该方法用于返回当前Url所代表数据的MIME类型。如果操作的数据属于集合类型,那么MIME类型字符串应该以 vnd.android.cursor.dir/开头,例如:要得到所有person记录的Uri为content: //cn.android.provider.personprovider/person,那么返回的MIME类型字符串应该为:“vnd.android.cursor.dir/person”。如果要操作的数据属于非集合类型数据,那么MIME类型字符串应该以 vnd.android.cursor.item/开头,例如:得到id为10的person记录,Uri为content: //cn.android.provider.personprovider/person/10,那么返回的MIME类型字符串应该为:“vnd.android.cursor.item/person”。 使用ContentResolver操作ContentProvider中的数据 当外部应用需要对ContentProvider中的数据进行添加、删除、修改和查询操作时,可以使用ContentResolver 类来完成,要获取ContentResolver 对象,可以使用Activity提供的getContentResolver()方法。 ContentResolver 类提供了与ContentProvider类相同签名的四个方法: public Uri insert(Uri uri, ContentValues values) 该方法用于往ContentProvider添加数据。 public int delete(Uri uri, String selection, String[] selectionArgs) 该方法用于从ContentProvider删除数据。 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) 该方法用于更新ContentProvider中的数据。 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) 该方法用于从ContentProvider中获取数据。 这些方法的第一个参数为Uri,代表要操作的是哪个ContentProvider和对其中的什么数据进行操作,假设给定的是: Uri.parse(“content://cn.android.provider.personprovider/person/10”),那么将会对主机名为cn.android.provider.personprovider的ContentProvider进行操作,操作的数据为person 表中id为10的记录。 使用ContentResolver对ContentProvider中的数据进行添加、删除、修改和查询操作: ContentResolver resolver = getContentResolver(); Uri uri = Uri.parse("content://cn.android.provider.personprovider/person"); //添加一条记录 ContentValues values = new ContentValues(); values.put("name", "android"); values.put("age", 25); resolver.insert(uri, values); //获取person表中所有记录 Cursor cursor = resolver.query(uri, null, null, null, "personid desc"); while(cursor.moveToNext()){ Log.i("ContentTest", "personid="+ cursor.getInt(0)+ ",name="+ cursor.getString(1)); } //把id为1的记录的name字段值更改新为liming ContentValues updateValues = new ContentValues(); updateValues.put("name", "liming"); Uri updateIdUri = ContentUris.withAppendedId(uri, 2); resolver.update(updateIdUri, updateValues, null, null); //删除id为2的记录 Uri deleteIdUri = ContentUris.withAppendedId(uri, 2); resolver.delete(deleteIdUri, null, null); 从Internet获取数据 利用HttpURLConnection对象,我们可以从网络中获取网页数据. URL url = new URL("http://www.sohu.com"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setConnectTimeout(6* 1000);//设置连接超时 if (conn.getResponseCode() != 200) throw new RuntimeException("请求url失败"); InputStream is = conn.getInputStream();//得到网络返回的输入流 String result = readData(is, "GBK"); conn.disconnect(); System.out.println(result); //第一个参数为输入流,第二个参数为字符集编码 public static String readData(InputStream inSream, String charsetName) throws Exception{ ByteArrayOutputStream outStream = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int len = -1; while( (len = inSream.read(buffer)) != -1 ){ outStream.write(buffer, 0, len); } byte[] data = outStream.toByteArray(); outStream.close(); inSream.close(); return new String(data, charsetName); <!-- 访问internet权限 --> <uses-permission android:name="android.permission.INTERNET"/> 利用HttpURLConnection对象,我们可以从网络中获取文件数据. URL url = new URL("http://photocdn.sohu.com/20100125/Img269812337.jpg"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setConnectTimeout(6* 1000); if (conn.getResponseCode() != 200) throw new RuntimeException("请求url失败"); InputStream is = conn.getInputStream(); readAsFile(is, "Img269812337.jpg"); public static void readAsFile(InputStream inSream, File file) throws Exception{ FileOutputStream outStream = new FileOutputStream(file); byte[] buffer = new byte[1024]; int len = -1; while( (len = inSream.read(buffer)) != -1 ){ outStream.write(buffer, 0, len); } outStream.close(); inSream.close(); } 利用HttpURLConnection对象,我们可以向网络发送请求参数. String requestUrl = "http://localhost:8080/android/contanctmanage.do"; Map<String, String> requestParams = new HashMap<String, String>(); requestParams.put("age", "12"); requestParams.put("name", "中国"); StringBuilder params = new StringBuilder(); for(Map.Entry<String, String> entry : requestParams.entrySet()){ params.append(entry.getKey()); params.append("="); params.append(URLEncoder.encode(entry.getValue(), "UTF-8")); params.append("&"); } if (params.length() > 0) params.deleteCharAt(params.length() - 1); byte[] data = params.toString().getBytes(); URL realUrl = new URL(requestUrl); HttpURLConnection conn = (HttpURLConnection) realUrl.openConnection(); conn.setDoOutput(true);//发送POST请求必须设置允许输出 conn.setUseCaches(false);//不使用Cache conn.setRequestMethod("POST"); conn.setRequestProperty("Connection", "Keep-Alive");//维持长连接 conn.setRequestProperty("Charset", "UTF-8"); conn.setRequestProperty("Content-Length", String.valueOf(data.length)); conn.setRequestProperty("Content-Type","application/x-www-form-urlencoded"); DataOutputStream outStream = new DataOutputStream(conn.getOutputStream()); outStream.write(data); outStream.flush(); if( conn.getResponseCode() == 200 ){ String result = readAsString(conn.getInputStream(), "UTF-8"); 利用HttpURLConnection对象,我们可以向网络发送xml数据. StringBuilder xml = new StringBuilder(); xml.append("<?xml version=\"1.0\" encoding=\"utf-8\" ?>"); xml.append("<M1 V=10000>"); xml.append("<U I=1 D=\"N73\">中国</U>"); xml.append("</M1>"); byte[] xmlbyte = xml.toString().getBytes("UTF-8"); URL url = new URL("http://localhost:8080/android/contanctmanage.do?method=readxml"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setConnectTimeout(6* 1000); conn.setDoOutput(true);//允许输出 conn.setUseCaches(false);//不使用Cache conn.setRequestMethod("POST"); conn.setRequestProperty("Connection", "Keep-Alive");//维持长连接 conn.setRequestProperty("Charset", "UTF-8"); conn.setRequestProperty("Content-Length", String.valueOf(xmlbyte.length)); conn.setRequestProperty("Content-Type", "text/xml; charset=UTF-8"); DataOutputStream outStream = new DataOutputStream(conn.getOutputStream()); outStream.write(xmlbyte);//发送xml数据 outStream.flush(); if (conn.getResponseCode() != 200) throw new RuntimeException("请求url失败"); InputStream is = conn.getInputStream();//获取返回数据 String result = readAsString(is, "UTF-8"); outStream.close(); |
|
cuihai |
2011-03-03 12:12 |
应用程序组件 Android的核心功能之一就是一个应用程序可以使用其它应用程序的元素(如果那个应用程序允许的话)。比如说,如果你的应用程序需要一个图片卷动列表,而另一个应用程序已经开发了一个合用的而又允许别人使用的话,你可以直接调用那个卷动列表来完成工作,而不用自己再开发一个。你的应用程序并没有吸纳或链接其它应用程序的代码,它只是在有需求的时候启动了其它应用程序的那个功能部分。 为达到这个目的,系统必须在一个应用程序的一部分被需要时启动这个应用程序,并将那个部分的Java对象实例化。与在其它系统上的应用程序不同,Android应用程序没有为应用准备一个单独的程序入口(比如说,没有main()方法),而是为系统依照需求实例化提供了基本的组件。共有四种组件类型: Activity Activity是为用户操作而展示的可视化用户界面。比如说,一个activity可以展示一个菜单项列表供用户选择,或者显示一些包含说明的照片。一个短消息应用程序可以包括一个用于显示做为发送对象的联系人的列表的activity,一个给选定的联系人写短信的activity以及翻阅以前的短信和改变设置的activity。尽管它们一起组成了一个内聚的用户界面,但其中每个activity都与其它的保持独立。每个都是以Activity类为基类的子类实现。 一个应用程序可以只有一个activity,或者,如刚才提到的短信应用程序那样,包含很多个。每个activity的作用,以及其数目,自然取决于应用程序及其设计。一般情况下,总有一个应用程序被标记为用户在应用程序启动的时候第一个看到的。从一个activity转向另一个的方式是靠当前的activity启动下一个。 每个activity都被给予一个默认的窗口以进行绘制。一般情况下,这个窗口是满屏的,但它也可以是一个小的位于其它窗口之上的浮动窗口。一个activity也可以使用超过一个的窗口──比如,在activity运行过程中弹出的一个供用户反应的小对话框,或是当用户选择了屏幕上特定项目后显示的必要信息。 窗口显示的可视内容是由一系列视图构成的,这些视图均继承自View基类。每个视图均控制着窗口中一块特定的矩形空间。父级视图包含并组织它子视图的布局。叶节点视图(位于视图层次最底端)在它们控制的矩形中进行绘制,并对用户对其直接操作做出响应。所以,视图是activity与用户进行交互的界面。比如说,视图可以显示一个小图片,并在用户指点它的时候产生动作。Android有很多既定的视图供用户直接使用,包括按钮、文本域、卷轴、菜单项、复选框等等。 视图层次是由Activity.setContentView()方法放入activity的窗口之中的。上下文视图是位于视图层次根位置的视图对象。(参见用户界面章节获取关于视图及层次的更多信息。) 服务 服务没有可视化的用户界面,而是在一段时间内在后台运行。比如说,一个服务可以在用户做其它事情的时候在后台播放背景音乐、从网络上获取一些数据或者计算一些东西并提供给需要这个运算结果的activity使用。每个服务都继承自Service基类。 一个媒体播放器播放播放列表中的曲目是一个不错的例子。播放器应用程序可能有一个或多个activity来给用户选择歌曲并进行播放。然而,音乐播放这个任务本身不应该为任何activity所处理,因为用户期望在他们离开播放器应用程序而开始做别的事情时,音乐仍在继续播放。为达到这个目的,媒体播放器activity应该启用一个运行于后台的服务。而系统将在这个activity不再显示于屏幕之后,仍维持音乐播放服务的运行。 你可以连接至(绑定)一个正在运行的服务(如果服务没有运行,则启动之)。连接之后,你可以通过那个服务暴露出来的接口与服务进行通讯。对于音乐服务来说,这个接口可以允许用户暂停、回退、停止以及重新开始播放。 如同activity和其它组件一样,服务运行于应用程序进程的主线程内。所以它不会对其它组件或用户界面有任何干扰,它们一般会派生一个新线程来进行一些耗时任务(比如音乐回放)。参见下述进程和线程。 广播接收器 广播接收器是一个专注于接收广播通知信息,并做出对应处理的组件。很多广播是源自于系统代码的──比如,通知时区改变、电池电量低、拍摄了一张照片或者用户改变了语言选项。应用程序也可以进行广播──比如说,通知其它应用程序一些数据下载完成并处于可用状态。 应用程序可以拥有任意数量的广播接收器以对所有它感兴趣的通知信息予以响应。所有的接收器均继承自BroadcastReceiver基类。 广播接收器没有用户界面。然而,它们可以启动一个activity来响应它们收到的信息,或者用NotificationManager来通知用户。通知可以用很多种方式来吸引用户的注意力──闪动背灯、震动、播放声音等等。一般来说是在状态栏上放一个持久的图标,用户可以打开它并获取消息。 内容提供者 内容提供者将一些特定的应用程序数据供给其它应用程序使用。数据可以存储于文件系统、SQLite数据库或其它方式。内容提供者继承于ContentProvider基类,为其它应用程序取用和存储它管理的数据实现了一套标准方法。然而,应用程序并不直接调用这些方法,而是使用一个ContentResolver对象,调用它的方法作为替代。ContentResolver可以与任意内容提供者进行会话,与其合作来对所有相关交互通讯进行管理。 参阅独立的内容提供者章节获得更多关于使用内容提供者的内容。 每当出现一个需要被特定组件处理的请求时,Android会确保那个组件的应用程序进程处于运行状态,或在必要的时候启动它。并确保那个相应组件的实例的存在,必要时会创建那个实例。 激活组件:intent 当接收到ContentResolver发出的请求后,内容提供者被激活。而其它三种组件──activity、服务和广播接收器被一种叫做intent的异步消息所激活。intent是一个保存着消息内容的Intent对象。对于activity和服务来说,它指明了请求的操作名称以及作为操作对象的数据的URI和其它一些信息。比如说,它可以承载对一个activity的请求,让它为用户显示一张图片,或者让用户编辑一些文本。而对于广播接收器而言,Intent对象指明了声明的行为。比如,它可以对所有感兴趣的对象声明照相按钮被按下。 对于每种组件来说,激活的方法是不同的: * 通过传递一个Intent对象至Context.startActivity()或Activity.startActivityForResult()以载入(或指定新工作给)一个activity。相应的activity可以通过调用getIntent()方法来查看激活它的intent。Android通过调用activity的onNewIntent()方法来传递给它继发的intent。 一个activity经常启动了下一个。如果它期望它所启动的那个activity返回一个结果,它会以调用startActivityForResult()来取代startActivity()。比如说,如果它启动了另外一个activity以使用户挑选一张照片,它也许想知道哪张照片被选中了。结果将会被封装在一个Intent对象中,并传递给发出调用的activity的onActivityResult()方法。 * 通过传递一个Intent对象至Context.startService()将启动一个服务(或给予正在运行的服务以一个新的指令)。Android调用服务的onStart()方法并将Intent对象传递给它。 与此类似,一个Intent可以被调用组件传递给Context.bindService()以获取一个正在运行的目标服务的连接。这个服务会经由onBind()方法的调用获取这个Intent对象(如果服务尚未启动,bindService()会先启动它)。比如说,一个activity可以连接至前述的音乐回放服务,并提供给用户一个可操作的(用户界面)以对回放进行控制。这个activity可以调用bindService()来建立连接,然后调用服务中定义的对象来影响回放。 后面一节:远程方法调用将更详细的阐明如何绑定至服务。 * 应用程序可以凭借将Intent对象传递给Context.sendBroadcast(),Context.sendOrderedBroadcast(),以及Context.sendStickyBroadcast()和其它类似方法来产生一个广播。Android会调用所有对此广播有兴趣的广播接收器的onReceive()方法,将intent传递给它们。 欲了解更多intent消息的信息,请参阅独立章节Intent和Intent滤过器。 关闭组件 内容提供者仅在响应ContentResolver提出请求的时候激活。而一个广播接收器仅在响应广播信息的时候激活。所以,没有必要去显式的关闭这些组件。 而activity则不同,它提供了用户界面,并与用户进行会话。所以只要会话依然持续,哪怕对话过程暂时停顿,它都会一直保持激活状态。与此相似,服务也会在很长一段时间内保持运行。所以Android为关闭activity和服务提供了一系列的方法。 * 可以通过调用它的finish()方法来关闭一个activity。一个activity可以通过调用另外一个activity(它用startActivityForResult() 启动的)的finishActivity()方法来关闭它。 * 服务可以通过调用它的stopSelf()方法来停止,或者调用Context.stopService()。 系统也会在组件不再被使用的时候或者Android需要为活动组件声明更多内存的时候关闭它。后面的组件的生命周期一节,将对这种可能及附属情况进行更详细的讨论。 manifest文件 当Android启动一个应用程序组件之前,它必须知道那个组件是存在的。所以,应用程序会在一个manifest文件中声明它的组件,这个文件会被打包到Android包中。这个.apk文件还将涵括应用程序的代码、文件以及其它资源。 这个manifest文件以XML作为结构格式,而且对于所有应用程序,都叫做AndroidManifest.xml。为声明一个应用程序组件,它还会做很多额外工作,比如指明应用程序所需链接到的库的名称(除了默认的Android库之外)以及声明应用程序期望获得的各种权限。 但manifest文件的主要功能仍然是向Android声明应用程序的组件。举例说明,一个activity可以如下声明: <?xml version="1.0" encoding="utf-8"?> <manifest . . . > <application . . . > <activity android:name="com.example.project.FreneticActivity" android:icon="@drawable/small_pic.png" android:label="@string/freneticLabel" . . . > </activity> . . . </application> </manifest> <activity>元素的name属性指定了实现了这个activity的Activity的子类。icon和label属性指向了包含展示给用户的此activity的图标和标签的资源文件。 其它组件也以类似的方法声明──<service>元素用于声明服务,<receiver>元素用于声明广播接收器,而<provider>元素用于声明内容提供者。 manifest文件中未进行声明的activity、服务以及内容提供者将不为系统所见,从而也就不会被运行。然而,广播接收器既可以在manifest文件中声明,也可以在代码中进行动态的创建,并以调用Context.registerReceiver()的方式注册至系统。 欲更多了解如何为你的应用程序构建manifest文件,请参阅AndroidManifest.xml文件一章。 Intent过滤器[yl1] Intent对象可以被显式的指定目标组件。如果进行了这种指定,Android会找到这个组件(依据manifest文件中的声明)并激活它。但如果Intent没有进行显式的指定,Android就必须为它找到对于intent来说最合适的组件。这个过程是通过比较Intent对象和所有可能对象的intent过滤器完成的。组件的intent过滤器会告知Android它所能处理的intent类型。如同其它相对于组件很重要的信息一样,这些是在manifest文件中进行声明的。这里是上面实例的一个扩展,其中加入了针对activity的两个intent过滤器声明: <?xml version="1.0" encoding="utf-8"?> <manifest . . . > <application . . . > <activity android:name="com.example.project.FreneticActivity" android:icon="@drawable/small_pic.png" android:label="@string/freneticLabel" . . . > <intent-filter . . . > <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <intent-filter . . . > <action android:name="com.example.project.BOUNCE" /> <data android:type="image/jpeg" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity> . . . </application> </manifest> 示例中的第一个过滤器──action “android.intent.action.MAIN”和类别“android.intent.category.LAUNCHER”的组合──是通常具有的。它标明了这个activity将在应用程序加载器中显示,就是用户在设备上看到的可供加载的应用程序列表。换句话说,这个activity是应用程序的入口,是用户选择运行这个应用程序后所见到的第一个activity。 第二个过滤器声明了这个activity能被赋予一种特定类型的数据。 组件可以拥有任意数量的intent过滤器,每个都会声明一系列不同的能力。如果它没有包含任何过滤器,它将只能被显式声明了目标组件名称的intent激活。 对于在代码中创建并注册的广播接收器来说,intent过滤器将被直接以IntentFilter对象实例化。其它过滤器则在manifest文件中设置。 欲获得更多intent过滤器的信息,请参阅独立章节:Intent和Intent过滤器。 Activity和任务 如前所述,一个activity可以启动另外一个,甚至包括与它不处于同一应用程序之中的。举个例子说,假设你想让用户看到某个地方的街道地图。而已经存在一个具有此功能的activity了,那么你的activity所需要做的工作就是把请求信息放到一个Intent对象里面,并把它传递给startActivity()。于是地图浏览器就会显示那个地图。而当用户按下BACK键的时候,你的activity又会再一次的显示在屏幕上。 对于用户来说,这看起来就像是地图浏览器是你activity所在的应用程序中的一个组成部分,其实它是在另外一个应用程序中定义,并运行在那个应用程序的进程之中的。Android将这两个activity放在同一个任务中来维持一个完整的用户体验。简单的说,任务就是用户所体验到的“应用程序”。它是安排在一个堆栈中的一组相关的activity。堆栈中的根activity就是启动了这整个任务的那个──一般情况下,它就是用户在应用程序加载器中所选择的。而堆栈最上方的activity则是当前运行的──用户直接对其进行操作的。当一个activity启动另外一个的时候,新的activity就被压入堆栈,并成为当前运行的activity。而前一个activity仍保持在堆栈之中。当用户按下BACK键的时候,当前activity出栈,而前一个恢复为当前运行的activity。 堆栈中保存的其实是对象,所以如果发生了诸如需要多个地图浏览器的情况,就会使得一个任务中出现多个同一Activity子类的实例同时存在,堆栈会为每个实例单独开辟一个入口。堆栈中的Activity永远不会重排,只会压入或弹出。 任务其实就是activity的堆栈,而不是manifest文件中的一个类或者元素。所以你无法撇开activity而为一个任务设置一个值。而事实上整个任务使用的值是在根activity中设置的。比如说,下一节我们会谈及“任务的affinity”,从affinity中读出的值将会设置到任务的根activity之中。 任务中的所有activity是作为一个整体进行移动的。整个的任务(即activity堆栈)可以移到前台,或退至后台。举个例子说,比如当前任务在堆栈中存有四个activity──三个在当前activity之下。当用户按下HOME键的时候,回到了应用程序加载器,然后选择了一个新的应用程序(也就是一个新任务)。则当前任务遁入后台,而新任务的根activity显示出来。然后,过了一小会儿,用户再次回到了应用程序加载器而又选择了前一个应用程序(上一个任务)。于是那个任务,带着它堆栈中所有的四个activity,再一次的到了前台。当用户按下BACK键的时候,屏幕不会显示出用户刚才离开的activity(上一个任务的根activity)。取而代之,当前任务的堆栈中最上面的activity被弹出,而同一任务中的上一个activity显示了出来。 上述的种种即是activity和任务的默认行为模式。但是有一些方法可以改变所有这一切。activity和任务的联系、任务中activity的行为方式都被启动那个activity的Intent对象中设置的一系列标记和manifest文件中那个activity中的<activity>元素的系列属性之间的交互所控制。无论是请求发出者和回应者在这里都拥有话语权。 我们刚才所说的这些关键Intent标记如下: FLAG_ACTIVITY_NEW_TASK FLAG_ACTIVITY_CLEAR_TOP FLAG_ACTIVITY_RESET_TASK_IF_NEEDED FLAG_ACTIVITY_SINGLE_TOP 而关键的<activity>属性是: taskAffinity launchMode allowTaskReparenting clearTaskOnLaunch alwaysRetainTaskState finishOnTaskLaunch 接下来的一节会描述这些标记以及属性的作用,它们是如何互相影响的,以及控制它们的使用时必须考虑到的因素。 Affinity(吸引力)和新任务 默认情况下,一个应用程序中的activity相互之间会有一种Affinity──也就是说,它们首选都归属于一个任务。然而,可以在<activity>元素中把每个activity的taskAffinity属性设置为一个独立的affinity。于是在不同的应用程序中定义的activity可以享有同一个affinity,或者在同一个应用程序中定义的activity有着不同的affinity。affinity在两种情况下生效:当加载activity的Intent对象包含了FLAG_ACTIVITY_NEW_TASK标记,或者当activity的allowTaskReparenting属性设置为“true”。 FLAG_ACTIVITY_NEW_TASK标记 如前所述,在默认情况下,一个新activity被另外一个调用了startActivity()方法的activity载入了任务之中。并压入了调用者所在的堆栈。然而,如果传递给startActivity()的Intent对象包含了FLAG_ACTIVITY_NEW_TASK标记,系统会为新activity安排另外一个任务。一般情况下,如同标记所暗示的那样,这会是一个新任务。然而,这并不是必然的。如果已经存在了一个与新activity有着同样affinity的任务,则activity会载入那个任务之中。如果没有,则启用新任务。 allowTaskReparenting属性 如果一个activity将allowTaskReparenting属性设置为“true”。它就可以从初始的任务中转移到与其拥有同一个affinity并转向前台的任务之中。比如说,一个旅行应用程序中包含的预报所选城市的天气情况的activity。它与这个应用程序中其它的activity拥有同样的affinity(默认的affinity)而且允许重定父级。你的另一个activity启动了天气预报,于是它就会与这个activity共处与同一任务之中。然而,当那个旅行应用程序再次回到前台的时候,这个天气预报activity就会被再次安排到原先的任务之中并显示出来。 如果在用户的角度看来,一个.apk文件中包含了多于一个的“应用程序”,你可能会想要为它们所辖的activity安排不一样的affinity。 加载模式 <activity>元素的launchMode属性可以设置四种不同的加载模式: "standard" (默认值) "singleTop" "singleTask" "singleInstance" 这些模式之间的差异主要体现在四个方面: * 哪个任务会把持对intent做出响应的activity。对“standard”和“singleTop”模式而言,是产生intent(并调用startActivity())的任务──除非Intent对象包含FLAG_ACTIVITY_NEW_TASK标记。而在这种情况下,如同上面Affinitie和新任务一节所述,会是另外一个任务。 相反,对“singleTask”和“singleInstance”模式而言,activity总是位于任务的根部。正是它们定义了一个任务,所以它们绝不会被载入到其它任务之中。 * activity是否可以存在多个实例。一个“standard”或“singleTop”的activity可以被多次初始化。它们可以归属于多个任务,而一个任务也可以拥有同一activity的多个实例。 相反,对“singleTask”和“singleInstance”的activity被限定于只能有一个实例。因为这些activity都是任务的起源,这种限制意味着在一个设备中同一时间只允许存在一个任务的实例。 * 在实例所在的任务中是否会有别的activity。一个“singleInstance”模式的activity将会是它所在的任务中唯一的activity。如果它启动了别的activity,那个activity将会依据它自己的加载模式加载到其它的任务中去──如同在intent中设置了FLAG_ACTIVITY_NEW_TASK标记一样的效果。在其它方面,“singleInstance”模式的效果与“singleTask”是一样的。 剩下的三种模式允许一个任务中出现多个activity。“singleTask”模式的activity将是任务的根activity,但它可以启动别的activity并将它们置入所在的任务中。“standard”和“singleTop”activity则可以在堆栈的任意位置出现。 * 是否要载入新的类实例以处理新的intent。对默认的"standard"模式来说,对于每个新intent都会创建一个新的实例以进行响应,每个实例仅处理一个intent。“singleTop”模式下,如果activity位于目的任务堆栈的最上面,则重用目前现存的activity来处理新的intent。如果它不是在堆栈顶部,则不会发生重用。而是创建一个新实例来处理新的intent并将其推入堆栈。 举例来说,假设一个任务的堆栈由根activityA和activity B、C和位于堆栈顶部的D组成,即堆栈A-B-C-D。一个针对D类型的activity的intent抵达的时候,如果D是默认的“standard”加载模式,则创建并加载一个新的类实例,于是堆栈变为A-B-C-D-D。然而,如果D的载入模式为“singleTop”,则现有的实例会对新intent进行处理(因为它位于堆栈顶部)而堆栈保持A-B-C-D的形态。 换言之,如果新抵达的intent是针对B类型的activity,则无论B的模式是“standard”还是“singleTop” ,都会加载一个新的B的实例(因为B不位于堆栈的顶部),而堆栈的顺序变为A-B-C-D-B。 如前所述,“singleTask”或“singleInstance”模式的activity永远不会存在多于一个实例。所以实例将处理所有新的intent。一个“singleInstance”模式的activity永远保持在堆栈的顶部(因为它是那个堆栈中唯一的一个activity),所以它一直坚守在处理intent的岗位上。然而,对一个“singleTask”模式的activity来说,它上面可能有,也可能没有别的activity和它处于同一堆栈。在有的情况下,它就不在能够处理intent的位置上,则那个intent将被舍弃。(即便在intent被舍弃的情况下,它的抵达仍将使这个任务切换至前台,并一直保留) 当一个现存的activity被要求处理一个新的intent的时候,会调用onNewIntent()方法来将intent对象传递至activity。(启动activity的原始intent对象可以通过调用getIntent()方法获得。) 请注意,当一个新的activity实例被创建以处理新的intent的时候,用户总可以按下BACK键来回到前面的状态(回到前一个activity)。但当使用现存的activity来处理新intent的时候,用户是不能靠按下BACK键回到当这个新intent抵达之前的状态的。 想获得更多关于加载模式的内容,请参阅<activity>元素的描述。 清理堆栈 如果用户离开一个任务很长一段时间,系统会清理该任务中除了根activity之外的所有activity。当用户再次回到这个任务的时候,除了只剩下初始化activity尚存之外,其余都跟用户上次离开它的时候一样。这样做的原因是:在一段时间之后,用户再次回到一个任务的时候,他们更期望放弃他们之前的所作所为,做些新的事情。 这些属于默认行为,另外,也存在一些activity的属性用以控制并改变这些行为: alwaysRetainTaskState属性 如果一个任务的根activity中此属性设置为“true”,则上述默认行为不会发生。任务将在很长的一段时间内保留它堆栈内的所有activity。 clearTaskOnLaunch属性 如果一个任务的根activity中此属性设置为“true”,则每当用户离开这个任务和返回它的时候,堆栈都会被清空至只留下rootactivity。换句话说,这是alwaysRetainTaskState的另一个极端。哪怕仅是过了一小会儿,用户回到任务时,也是见到它的初始状态。 finishOnTaskLaunch属性 这个属性与clearTaskOnLaunch属性相似,但它仅作用于单个的activity,而不是整个的task。而且它可以使任意activity都被清理,甚至根activity也不例外。当它设置为“true”的时候,此activity仅做为任务的一部分存在于当前回话中,一旦用户离开并再次回到这个任务,此activity将不复存在。 此外,还有别的方式从堆栈中移除一个activity。如果一个intent对象包含FLAG_ACTIVITY_CLEAR_TOP标记,而且目标任务的堆栈中已经存在了一个能够响应此intent的activity类型的实例。则这个实例之上的所有activity都将被清理以使它位于堆栈的顶部来对intent做出响应。如果此时指定的activity的加载模式为“standard”,则它本身也会从堆栈中移除,并加载一个新的实例来处理到来的intent。这是因为加载模式为“standard”的activity总会创建一个新实例来处理新的intent。 FLAG_ACTIVITY_CLEAR_TOP与FLAG_ACTIVITY_NEW_TASK经常合并使用。这时,这些标记提供了一种定位其它任务中现存的activity并将它们置于可以对intent做出响应的位置的方法。 启动任务 当一个activity被指定一个“android.intent.action.MAIN”做为动作,以及“android.intent.category.LAUNCHER”做为类别的intent过滤器之后(在前述intent过滤器一节中已经有了这个示例),它就被设置为一个任务的入口点。这样的过滤器设置会在应用程序加载器中为此activity显示一个图标和标签,以供用户加载任务或加载之后在任意时间回到这个任务。 第二个能力相当重要:用户必须可以离开一个任务,并在一段时间后返回它。出于这个考虑,加载模式被设定为“singleTask”和“singleInstance”的activity总是会初始化一个新任务,这样的activity仅能用于指定了一个MAIN和LAUNCHER过滤器的情况之下。我们来举例说明如果没指定过滤器的情况下会发生的事情:一个intent加载了一个“singleTask”的activity,初始化了一个新任务,用户在这个任务中花费了一些时间来完成工作。然后用户按下了HOME键。于是任务被要求转至后台并被主屏幕所掩盖。因为它并没有在应用程序加载器中显示图标,这将导致用户无法再返回它。 类似的困境也可由FLAG_ACTIVITY_NEW_TASK标记引起。如果此标记使一个activity启动了一个新任务继而用户按下了HOME键离开了它,则用户必须要有一些方法再次回到这个任务。一些实体(诸如通知管理器)总是在另外的任务中启动新activity,而不是做为它们自己的一部分,所以它们总是将FLAG_ACTIVITY_NEW_TASK标记包含在intent里面并传递给startActivity()。如果你写了一个能被外部实体使用这个标记调用的activity,你必须注意要给用户留一个返回这个被外部实体启动的任务的方法。 当你不想让用户再次返回一个activity的情况下,可以将<activity>元素的finishOnTaskLaunch设置为“true”。参见前述清理堆栈。. 进程和线程 当一个应用程序开始运行它的第一个组件时,Android会为它启动一个Linux进程,并在其中执行一个单一的线程。默认情况下,应用程序所有的组件均在这个进程的这个线程中运行。 然而,你也可以安排组件在其他进程中运行,而且可以为任意进程衍生出其它线程。 进程 组件运行所在的进程由manifest文件所控制。组件元素——<activity>,<service>,<receiver>和<provider>——都有一个process属性来指定组件应当运行于哪个进程之内。这些属性可以设置为使每个组件运行于它自己的进程之内,或一些组件共享一个进程而其余的组件不这么做。它们也可以设置为令不同应用程序的组件在一个进程中运行——使应用程序的组成部分共享同一个Linux用户ID并赋以同样的权限。<application>元素也有一个process属性,以设定所有组件的默认值。 所有的组件实例都位于特定进程的主线程内,而对这些组件的系统调用也将由那个线程进行分发。一般不会为每个实例创建线程。因此,某些方法总是运行在进程的主线程内,这些方法包括诸如View.onKeyDown()这样报告用户动作以及后面组件生命周期一节所要讨论的生命周期通告的。这意味着组件在被系统调用的时候,不应该施行长时间的抑或阻塞的操作(诸如网络相关操作或是循环计算),因为这将阻塞同样位于这个进程的其它组件的运行。你应该如同下面线程一节所叙述的那样,为这些长时间操作衍生出一个单独的线程进行处理。 在可用内存不足而又有一个正在为用户进行服务的进程需要更多内存的时候,Android有时候可能会关闭一个进程。而在这个进程中运行着的应用程序也因此被销毁。当再次出现需要它们进行处理的工作的时候,会为这些组件重新创建进程。 在决定结束哪个进程的时候,Android会衡量它们对于用户的相对重要性。比如说,相对于一个仍有用户可见的activity的进程,它更有可能去关闭一个其activity已经不为用户所见的进程。也可以说,决定是否关闭一个进程主要依据在那个进程中运行的组件的状态。这些状态将在后续的一节组件生命周期中予以说明。 线程 尽管你可以把你的应用程序限制于一个单独的进程中,有时,你仍然需要衍生出一个线程以处理后台任务。因为用户界面必须非常及时的对用户操作做出响应,所以,控管activity的线程不应用于处理一些诸如网络下载之类的耗时操作。所有不能在瞬间完成的任务都应安排到不同的线程中去。 线程在代码中是以标准Java Thread对象创建的。Android提供了很多便于管理线程的类:Looper用于在一个线程中运行一个消息循环,Handler用于处理消息,HandlerThread用于使用一个消息循环启用一个线程。 远程过程调用 Android有一个轻量级的远程过程调用(RPC)机制:即在本地调用一个方法,但在远程(其它的进程中)进行处理,然后将结果返回调用者。这将方法调用及其附属的数据以系统可以理解的方式进行分离,并将其从本地进程和本地地址空间传送至远程过程和远程地址空间,并在那里重新装配并对调用做出反应。返回的结果将以相反的方向进行传递。Android提供了完成这些工作所需的所有的代码,以使你可以集中精力来实现RPC接口本身。 RPC接口可以只包括方法。即便没有返回值,所有方法仍以同步的方式执行(本地方法阻塞直至远程方法结束)。 简单的说,这套机制是这样工作的:一开始,你用简单的IDL(界面描绘语言)声明一个你想要实现的RPC接口。然后用aidl工具为这个声明生成一个Java接口定义,这个定义必须对本地和远程进程都可见。它包含两个内部类,如下图所示: 内部类中有管理实现了你用IDL声明的接口的远程方法调用所需要的所有代码。两个内部类均实现了IBinder接口。一个用于系统在本地内部使用,你些的代码可以忽略它;另外一个,我们称为Stub,扩展了Binder类。除了实现了IPC调用的内部代码之外,它还包括了你声明的RPC接口中的方法的声明。你应该如上图所示的那样写一个Stub的子类来实现这些方法。 一般情况下,远程过程是被一个服务所管理的(因为服务可以通知系统关于进程以及它连接到别的进程的信息)。它包含着aidl工具产生的接口文件和实现了RPC方法的Stub的子类。而客户端只需要包括aidl工具产生的接口文件。 下面将说明服务与其客户端之间的连接是如何建立的: * 服务的客户端(位于本地)应该实现onServiceConnected()和onServiceDisconnected()方法。这样,当至远程服务的连接成功建立或者断开的时候,它们会收到通知。这样它们就可以调用bindService()来设置连接。 * 而服务则应该实现onBind()方法以接受或拒绝连接。这取决于它收到的intent(intent将传递给bindService())。如果接受了连接,它会返回一个Stub的子类的实例。 * 如果服务接受了连接,Android将会调用客户端的onServiceConnected()方法,并传递给它一个IBinder对象,它是由服务所管理的Stub的子类的代理。通过这个代理,客户端可以对远程服务进行调用。 线程安全方法 在一些情况下,你所实现的方法有可能会被多于一个的线程所调用,所以它们必须被写成线程安全的。 对于我们上一节所讨论的RPC机制中的可以被远程调用的方法来说,这是必须首先考虑的。如果针对一个IBinder对象中实现的方法的调用源自这个IBinder对象所在的进程时,这个方法将会在调用者的线程中执行。然而,如果这个调用源自其它的进程,则这个方法将会在一个线程池中选出的线程中运行,这个线程池由Android加以管理,并与IBinder存在于同一进程内;这个方法不会在进程的主线程内执行。反过来说,一个服务的onBind()方法应为服务进程的主线程所调用,而实现了由onBind()返回的对象(比如说,一个实现了RPC方法的Stub的子类)的方法将为池中的线程所调用。因为服务可以拥有多于一个的客户端,而同一时间,也会有多个池中的线程调用同一个IBinder方法。因此IBinder方法必须实现为线程安全的。 类似的,一个内容提供者能接受源自其它进程的请求数据。尽管ContentResolver和ContentProvider类隐藏了交互沟通过程的管理细节,ContentProvider会由query(),insert(),delete(),update()和getType()方法来相应这些请求,而这些方法也都是由那个内容提供者的进程中所包涵的线程池提供的,而不是进程的主线程本身。所以这些有可能在同一时间被很多线程调用的方法也必须被实现为线程安全的。 组件生命周期 应用程序组件有其生命周期──由Android初始化它们以相应intent直到这个实例被摧毁。在此之间,它们有时是激活的有时则相反。或者,如果它是一个activity,则是可为用户所见或者不能。这一节讨论了activity、服务以及广播接收器的生命周期,包括它们在生命周期中的状态、在状态之间转变时通知你的方法、以及当这些进程被关闭或实例被摧毁时,这些状态产生的效果。 Activity生命周期 一个activity主要有三个状态: * 当在屏幕前台时(位于当前任务堆栈的顶部),它是活跃或运行的状态。它就是相应用户操作的activity。 * 当它失去焦点但仍然对用户可见时,它处于暂停状态。即是:在它之上有另外一个activity。这个activity也许是透明的,或者未能完全遮蔽全屏,所以被暂停的activity仍对用户可见。暂停的activity仍然是存活状态(它保留着所有的状态和成员信息并连接至窗口管理器),但当系统处于极低内存的情况下,仍然可以杀死这个activity。 * 如果它完全被另一个activity覆盖是,它处于停止状态。它仍然保留所有的状态和成员信息。然而它不在为用户可见,所以它的窗口将被隐藏,如果其它地方需要内存,则系统经常会杀死这个activity。 如果一个activity处于暂停或停止状态,系统可以通过要求它结束(调用它的finish()方法)或直接杀死它的进程来将它驱出内存。当它再次为用户可见的时候,它只能完全重新启动并恢复至以前的状态。 当一个activity从这个状态转变到另一个状态时,它被以下列protected方法所通知: void onCreate(Bundle savedInstanceState) void onStart() void onRestart() void onResume() void onPause() void onStop() void onDestroy() 你可以重载所有这些方法以在状态改变时进行合适的工作。所有的activity都必须实现onCreate()用以当对象第一次实例化时进行初始化设置。很多activity会实现onPause()以提交数据变化或准备停止与用户的交互。 调用父类 所有activity生命周期方法的实现都必须先调用其父类的版本。比如说: protected void onPause() { super.onPause(); . . . } 总得来说,这七个方法定义了一个activity完整的生命周期。实现这些方法可以帮助你监察三个嵌套的生命周期循环: * 一个activity 完整的生命周期自第一次调用onCreate()开始,直至调用onDestroy()为止。activity在onCreate()中设置所有“全局”状态以完成初始化,而在onDestroy()中释放所有系统资源。比如说,如果activity有一个线程在后台运行以从网络上下载数据,它会以onCreate()创建那个线程,而以onDestroy()销毁那个线程。 * 一个activity的可视生命周期自onStart()调用开始直到相应的onStop()调用。在此期间,用户可以在屏幕上看到此activity,尽管它也许并不是位于前台或者正在与用户做交互。在这两个方法中,你可以管控用来向用户显示这个activity的资源。比如说,你可以在onStart()中注册一个BroadcastReceiver来监控会影响到你UI的改变,而在onStop()中来取消注册,这时用户是无法看到你的程序显示的内容的。onStart()和onStop()方法可以随着应用程序是否为用户可见而被多次调用。 * 一个activity的前台生命周期自onResume()调用起,至相应的onPause()调用为止。在此期间,activity位于前台最上面并与用户进行交互。activity会经常在暂停和恢复之间进行状态转换──比如说当设备转入休眠状态或有新的activity启动时,将调用onPause()方法。当activity获得结果或者接收到新的intent的时候会调用onResume()方法。因此,在这两个方法中的代码应当是轻量级的。 下图展示了上述循环过程以及activity在这个过程之中历经的状态改变。着色的椭圆是activity可以经历的主要状态。矩形框代表了当activity在状态间发生改变的时候,你进行操作所要实现的回调方法。 下表详细描述了这些方法,并在activity的整个生命周期中定位了它们。 方法 描述 可被杀死 下一个 onCreate() 在activity第一次被创建的时候调用。这里是你做所有初始化设置的地方──创建视图、绑定数据至列表等。如果曾经有状态记录(参阅后述Saving Activity State。),则调用此方法时会传入一个包含着此activity以前状态的包对象做为参数。 总继之以onStart()。 否 onStart() onRestart() 在activity停止后,在再次启动之前被调用。 总继之以onStart()。 否 onStart() onStart() 当activity正要变得为用户所见时被调用。 当activity转向前台时继以onResume(),在activity变为隐藏时继以onStop()。 否 onResume() or onStop() onResume() 在activity开始与用户进行交互之前被调用。此时activity位于堆栈顶部,并接受用户输入。 继之以onPause()。 否 onPause() onPause() 当系统将要启动另一个activity时调用。此方法主要用来将未保存的变化进行持久化,停止类似动画这样耗费CPU的动作等。这一切动作应该在短时间内完成,因为下一个activity必须等到此方法返回后才会继续。 当activity重新回到前台是继以onResume()。当activity变为用户不可见时继以onStop()。 是 onResume() or onStop() onStop() 当activity不再为用户可见时调用此方法。这可能发生在它被销毁或者另一个activity(可能是现存的或者是新的)回到运行状态并覆盖了它。 如果activity再次回到前台跟用户交互则继以onRestart(),如果关闭activity则继以onDestroy()。 是 onRestart() or onDestroy() onDestroy() 在activity销毁前调用。这是activity接收的最后一个调用。这可能发生在activity结束(调用了它的finish()方法)或者因为系统需要空间所以临时的销毁了此acitivity的实例时。你可以用isFinishing()方法来区分这两种情况。 是 nothing 请注意上表中可被杀死一列。它标示了在方法返回后,还没执行activity的其余代码的任意时间里,系统是否可以杀死包含此activity的进程。三个方法(onPause()、onStop()和onDestroy())被标记为“是”。onPause()是三个中的第一个,它也是唯一一个在进程被杀死之前必然会调用的方法──onStop()和onDestroy()有可能不被执行。因此你应该用onPause()来将所有持久性数据(比如用户的编辑结果)写入存储之中。 在可被杀死一列中标记为“否”的方法在它们被调用时将保护activity所在的进程不会被杀死。所以只有在onPause()方法返回后到onResume()方法被调用时,一个activity才处于可被杀死的状态。在onPause()再次被调用并返回之前,它不会被系统杀死。 如后面一节进程和生命周期所述,即使是在这里技术上没有被定义为“可杀死”的activity仍然有可能被系统杀死──但这仅会发生在实在没有其它方法的极端情况之下。 保存activity状态 当系统而不是用户自己出于回收内存的考虑,关闭了一个activity之后。用户会期望当他再次回到那个activity的时候,它仍保持着上次离开时的样子。 为了获取activity被杀死前的状态,你应该为activity实现onSaveInstanceState()方法。Android在activity有可能被销毁之前(即onPause()调用之前)会调用此方法。它会将一个以名称-值对方式记录了activity动态状态的Bundle对象传递给该方法。当activity再次启动时,这个Bundle会传递给onCreate()方法和随着onStart()方法调用的onRestoreInstanceState(),所以它们两个都可以恢复捕获的状态。 与onPause()或先前讨论的其它方法不同,onSaveInstanceState()和onRestoreInstanceState()并不是生命周期方法。它们并不是总会被调用。比如说,Android会在activity易于被系统销毁之前调用onSaveInstanceState(),但用户动作(比如按下了BACK键)造成的销毁则不调用。在这种情况下,用户没打算再次回到这个activity,所以没有保存状态的必要。 因为onSaveInstanceState()不是总被调用,所以你应该只用它来为activity保存一些临时的状态,而不能用来保存持久性数据。而是应该用onPause()来达到这个目的。 协调activity 当一个activity启动了另外一个的时候,它们都会经历生命周期变化。一个会暂停乃至停止,而另一个则启动。这种情况下,你可能需要协调好这些activity: 生命周期回调顺序是已经定义好的,尤其是在两个activity在同一个进程内的情况下: 1. 调用当前activity的onPause()方法。 2. 接着,顺序调用新启动activity的onCreate()、onStart()和onResume()方法。 3. 然后,如果启动的activity不再于屏幕上可见,则调用它的onStop()方法。 服务生命周期 服务以两种方式使用: * 它可以启动并运行,直至有人停止了它或它自己停止。在这种方式下,它以调用Context.startService()启动,而以调用Context.stopService()结束。它可以调用Service.stopSelf()或Service.stopSelfResult()来自己停止。不论调用了多少次startService()方法,你只需要调用一次stopService()来停止服务。 * 它可以通过自己定义并暴露出来的接口进行程序操作。客户端建立一个到服务对象的连接,并通过那个连接来调用服务。连接以调用Context.bindService()方法建立,以调用Context.unbindService()关闭。多个客户端可以绑定至同一个服务。如果服务此时还没有加载,bindService()会先加载它。 这两种模式并不是完全分离的。你可以绑定至一个用startService()启动的服务。比如说,一个后台音乐播放服务可以调用startService()并传递给它一个包含欲播放的音乐列表的Intent对象来启动。不久,当用户想要对播放器进行控制或者查看当前播放曲目的详情时,会启用一个activity,调用bindService()连接到服务来完成操作。在这种情况下,直到绑定连接关闭stopService()才会真正停止一个服务。 与activity一样,服务也有一系列你可以实现以用于监控其状态变化的生命周期方法。但相对于activity要少一些,只有三个,而且,它们是public属性,并非protected: void onCreate() void onStart(Intent intent) void onDestroy() 倚仗实现这些方法,你监控服务的两个嵌套的生命周期循环: * 服务的完整生命周期始于调用onCreate()而终于onDestroy()方法返回。如同activity一样,服务在onCreate()里面进行它自己的初始化,而在onDestroy()里面释放所有资源。比如说,一个音乐回放服务可以在onCreate()中创建播放音乐的线程,而在onDestroy()中停止这个线程。 * 服务的活跃生命周期始于调用onStart()。这个方法用于处理传递给startService()的Intent对象。音乐服务会打开Intent来探明将要播放哪首音乐,并开始播放。 服务停止时没有相应的回调方法──不存在onStop()方法。 onCreate()和onDestroy()方法在所有服务中都会被调用,不论它们是由Context.startService()还是由Context.bindService()所启动的。而onStart()仅会被startService()所启用的服务调用。 如果一个服务允许别的进程绑定,则它还会有以下额外的回调方法以供实现: IBinder onBind(Intent intent) boolean onUnbind(Intent intent) void onRebind(Intent intent) 传递给bindService的Intent的对象也会传递给onBind()回调方法,而传递给unbindService()的Intent对象同样传递给onUnbind()。如果服务允许绑定,onBind()将返回一个供客户端与服务进行交互的通讯渠道。如果有新的客户端连接至服务,则onUnbind()方法可以要求调用onRebind()。 下图描绘了服务的回调方法。尽管图中对由startService和startService方法启动的服务做了区分,但要记住,不论一个服务是怎么启动的,它都可能允许客户端的连接,所以任何服务都可以接受onBind()和onUnbind()调用。 |
|
cuihai |
2011-03-03 12:13 |
广播接收器生命周期 广播接收器只有一个回调方法: void onReceive(Context curContext, Intent broadcastMsg) 当广播消息抵达接收器时,Android调用它的onReceive()方法并将包含消息的Intent对象传递给它。广播接收器仅在它执行这个方法时处于活跃状态。当onReceive()返回后,它即为失活状态。 拥有一个活跃状态的广播接收器的进程被保护起来而不会被杀死。但仅拥有失活状态组件的进程则会在其它进程需要它所占有的内存的时候随时被杀掉。 这种方式引出了一个问题:如果响应一个广播信息需要很长的一段时间,我们一般会将其纳入一个衍生的线程中去完成,而不是在主线程内完成它,从而保证用户交互过程的流畅。如果onReceive()衍生了一个线程并且返回,则包涵新线程在内的整个进程都被会判为失活状态(除非进程内的其它应用程序组件仍处于活跃状态),于是它就有可能被杀掉。这个问题的解决方法是令onReceive()启动一个新服务,并用其完成任务,于是系统就会知道进程中仍然在处理着工作。 下一节中,我们会讨论更多进程易误杀的问题。 进程与生命周期 Android系统会尽可能长的延续一个应用程序进程,但在内存过低的时候,仍然会不可避免需要移除旧的进程。为决定保留或移除一个进程,Android将每个进程都放入一个“重要性层次”中,依据则是它其中运行着的组件及其状态。重要性最低的进程首先被消灭,然后是较低的,依此类推。重要性共分五层,依据重要性列表如下: 1. 前台进程是用户操作所必须的。当满足如下任一条件时,进程被认为是处于前台的: o 它运行着正在与用户交互的activity(Activity对象的onResume()方法已被调用)。 o 一个正在与用户交互的activity使用着它提供的一个服务。 o 它包含着一个正在执行生命周期回调方法(onCreate()、onStart()或onDestroy())的Service对象。 o 它包含着一个正在执行onReceive()方法的BroadcastReceiver对象。 任一时间下,仅有少数进程会处于前台,仅当内存实在无法供给它们维持同时运行时才会被杀死。一般来说,在这种情况下,设备已然处于使用虚拟内存的状态,必须要杀死一些前台进程以用户界面保持响应。 2. 可视进程没有前台组件,但仍可被用户在屏幕上所见。当满足如下任一条件时,进程被认为是可视的:、 o 它包含着一个不在前台,但仍然为用户可见的activity(它的onPause()方法被调用)。这种情况可能出现在以下情况:比如说,前台activity是一个对话框,而之前的activity位于其下并可以看到。 o 它包含了一个绑定至一个可视的activity的服务。 可视进程依然被视为是很重要的,非到不杀死它们便无法维持前台进程运行时,才会被杀死。 3. 服务进程是由startService()方法启动的服务,它不会变成上述两类。尽管服务进程不会直接为用户所见,但它们一般都在做着用户所关心的事情(比如在后台播放mp3或者从网上下载东西)。所以系统会尽量维持它们的运行,除非系统内存不足以维持前台进程和可视进程的运行需要。 4. 背景进程包含目前不为用户所见的activity(Activity对象的onStop()方法已被调用)。这些进程与用户体验没有直接的联系,可以在任意时间被杀死以回收内存供前台进程、可视进程以及服务进程使用。一般来说,会有很多背景进程运行,所以它们一般存放于一个LRU(最后使用)列表中以确保最后被用户使用的activity最后被杀死。如果一个activity正确的实现了生命周期方法,并捕获了正确的状态,则杀死它的进程对用户体验不会有任何不良影响。 5. 空进程不包含任何活动应用程序组件。这种进程存在的唯一原因是做为缓存以改善组件再次于其中运行时的启动时间。系统经常会杀死这种进程以保持进程缓存和系统内核缓存之间的平衡。 Android会依据进程中当前活跃组件的重要程度来尽可能高的估量一个进程的级别。比如说,如果一个进程中同时有一个服务和一个可视的activity,则进程会被判定为可视进程,而不是服务进程。 此外,一个进程的级别可能会由于其它进程依赖于它而升高。一个为其它进程提供服务的进程级别永远高于使用它服务的进程。比如说,如果A进程中的内容提供者为进程B中的客户端提供服务,或进程A中的服务为进程B中的组件所绑定,则A进程最低也会被视为与进程B拥有同样的重要性。 因为运行着一个服务的进程重要级别总高于一个背景activity。所以一个activity以启动一个服务的方式启动一个长时间运行过程比简单的衍生一个线程来进行处理要好。尤其是当处理过程比activity本身存在时间要长的情况之下。我们以背景音乐播放和上传一个相机拍摄的图片至网站上为例。使用服务则不论activity发生何事,都至少可以保证操作拥有“服务进程”的权限。如上一节广播接收器生命周期所提到的,这也正是广播接收器使用服务,而不是使用线程来处理耗时任务的原因。 用户界面User Interface 在一个Android应用中,用户界面是由View和ViewGroup对象构建的。View与ViewGroup都有很多种类,而它们都是View类的子类。 View对象是Android平台中用户界面体现的基础单位。View类是它称为“widgets(工具)”的子类的基础,它们提供了诸如文本输入框和按钮之类的UI对象的完整实现。ViewGroup类同样为其被称为“Layouts(布局)”的子类奠定了基础,它们提供了象流式布局、表格布局以及相对布局之类的布局架构。 View对象是一个数据体,它的属性存储了用于屏幕上一块矩形区域的布局参数及内容。并负责这块它所辖的这个矩形区域之中所有测量、布局、焦点转换、卷动以及按键/触摸手势的处理。作为一个用户界面对象,View同时也担任着用户交互关键点以及交互事件接受者的角色。 视图层次View Hierarchy 在Android平台上,你可以用下图所示的View和ViewGroup层次图来定义一个Activity的UI。这个层次树可随你所愿的简单或者复杂化,你能使用Android预定义的一套工具和布局来创建它,或者使用你自己定义的Views来创建。 为了把一个视图层次树展现到屏幕上,你的Activity必须调用setContentView()方法,并传给它一个根节点对象的引用。Android系统将接受此引用,并用来进行界面的废止、测量并绘制这棵树。层次的根结点会要求它的子节点进行自我绘制──进而,每个视图组节点也负责调用它的子视图进行自我绘制。子节点将向父节点申请绘制的位置以及大小,而其父类享有子节点绘制的位置及大小的最终决定权。Android依次(自层次树顶层开始)解析你布局中的元素,实例化View并将它们添加到它们的父节点中。因为这个过程是依次进行的,所以如果出现了元素重叠的情况,最后一个绘制的元素将位于所有重叠元素之上显现。 如欲获得更多关于视图层次如何测算以及绘制细节的讨论,情参阅Android如何绘制视图。 布局Layout 定义并展现你的视图层次的最常用的方法是使用XML布局文件。如同HTML一样,XML为布局提供了一种可读的结构。XML中的每个元素都是View或ViewGroup对象(抑或它们的子类)。View对象是树的叶节点,而ViewGroup对象是树的分支(参阅楼上的视图层次图)。 XML元素的名称与它体现的Java类相对应。所以一个<TextView>元素将在你的UI中生成一个TextView,而<LinearLayout>则创建一个LinearLayout视图组。当你载入一个布局资源时,Android系统会根据你布局中的元素初始化这些运行时对象。 举例来说,一个包含文本视图和一个按钮的简单垂直布局如下: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <TextView android:id="@+id/text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello, I am a TextView" /> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello, I am a Button" /> </LinearLayout> 请注意:LinearLayout 元素包含了TextView 和Button 对象。你可以在其中另外安置一个LinearLayout (或其它类型的视图组),以延展这个视图层次,构建更复杂的布局。 欲获知更多如何构建UI布局的内容,请参阅声明布局。 提示:您也可以用Java代码来绘制View和ViewGroup对象,并用addView(View)方法动态的插入新的View和ViewGroup对象。 您有相当多的方法来对视图进行布局。使用大量不同种类的视图组,您可以有近乎无穷的方式来构建子视图和视图组。Android提供了一些预定义的视图组,其中包括LinearLayout, RelativeLayout, AbsoluteLayout, TableLayout, GridLayout以及其它的一些。每个都为定义子视图和布局结构提供了一套独特的布局参数。 欲了解其它用于布局的不同种类的视图组,请参阅普通布局对象。 部件Widgets 部件是为用户交互界面提供服务的视图对象。Android提供了一套完整的部件实现,包括按钮、复选框、文本输入框等,以助于你快速的构建UI。Android还提供了一些更高级的部件,比如日期选择、时钟以及缩放控制。但您并没有被局限于Android平台提供的这些部件上。如果您想创建一些您自己的定制动作元素,您可以这么做,只要定义自己的视图对象或者扩展或合并现有的部件就行。 更多信息参阅构建自定义组件。 您可以在android.widget包中找到Android提供的部件列表。 用户界面事件UI Events 当你在用户界面中加入了一些视图和工具之后,你可能想要知道如何让它们与用户交互,进而实现你的动作。如欲获得用户界面事件通知,你需要做以下两件事情之一: * 定义一个事件侦听器并将其注册至视图。通常情况下,这是你侦听事件的主要方式。View类包含了一大堆命名类似 On<什么什么>Listener的接口,每个都带有一个叫做On<什么什么>()的回调方法。比如:View.OnClickListener (用以处理视图中的点击),View.OnTouchListener(用以处理视图中的触屏事件),以及View.OnKeyListener (用以处理视图中的设备按键事件)。所以,如果你希望你的视图在它被”点击”(比如选择了一个按钮)的时候获得通知,你就要实现OnClickListener,定义它的onClick()回调方法(在其中进行相应处理),并将它用setOnClickListener()方法注册到视图上。 * 为视图覆写一个现有的回调方法。这种方法主要用于你自己实现了一个View类,并想侦听其上发生的特定事件。比如说当屏幕被触摸(onTouchEvent()),当轨迹球发生了移动(onTrackballEvent())或者是设备上的按键被按下(onKeyDown())。这种方式允许你为自己定制的视图中发生的每个事件定义默认的行为,并决定是否需要将事件传递给其它的子视图。再说一次,这些是View类相关的回调方法,所以你只能在你构建自定义组件时定义它们。 如何在视图中处理用户交互请参见处理用户界面事件文档。 菜单Menus 应用程序菜单是应用程序用户界面中另外一个重要的组成部分。菜单为展现应用程序功能和设置提供了一个可靠的界面。按下设备上的MENU键会调出最普通的应用程序菜单。然而,你也可以加入当用户长按一个项目时调出的上下文菜单。 菜单也是用视图层次进行构架的,但你不必自己定义这个架构。你只要为你的Activity定义onCreateOptionsMenu()和onCreateContextMenu()回调方法,并声明你想要包含在菜单中的项目就行了。Android将为你的菜单自动创建视图层次,并在其中绘入你的菜单项。 菜单会自行处理它们的事件,所以你不必为你菜单中的项目注册事件侦听器。当你菜单中的一项被选定时,框架将自动调用onOptionsItemSelected()或onContextItemSelected()方法。 如同应用程序布局一样。你也可以在一个XML文件中定义你菜单中的项目。 更多信息,请参阅创建菜单。 高级话题Advanced Topics 一旦你对创建用户界面的基础了如指掌,你就可以尝试着用一些高级功能来创建更加复杂的应用程序界面。 适配器Adapter 有时候你会想要用一些无法硬编码的信息来填充视图组。你想将源于外部的数据绑定到你的视图中。为达到这个目的,你可以使用AdapterView作为你的视图组,并用Adapter传来的数据初始化每个子视图并填入其中。 AdapterView对象是一个用给定的Adapter对象为基础构建它的子视图的ViewGroup实现。而Adapter在你的数据源(可能是一个外部字符串数组)和显示这些数据的AdapterView之间扮演着一个信使的角色。针对特定的任务有着很多不同的Adapter类实现,比如CursorAdapter依据Cursor读出一个数据库的数据,而一个ArrayAdapter则从任一个数组进行读取。 想要了解如何运用Adapter填充你的视图,请参见用AdapterView绑定至数据。 风格与主题Styles and Themes 或许你对标准工具的外表不是那么满意。为了解决这个问题,你可以创建你自己的风格和主题。 * 风格是一套包含一个或多个格式化属性的整体,你可以把它们加诸于你布局中的单个元素之上。比如,你可以定义一个包含特定文本字体大小和颜色的风格,并将它单独施用于特定的视图元素。 * 主题也是一套包含一个或多个格式化属性的整体,但却应用于一个应用程序中的所有Activity,或单独一个Activity。比如说,你可以定义一个包含了特定窗口边框颜色和版面背景、以及一套字体大小和菜单颜色的主题。这个主题可以施用于特定的Activity抑或整个应用程序。 风格与主题隶属于资源。Android提供了一些默认的风格和主题供你使用,你也可以定制你自己的风格和主题资源。 想了解更多关于使用风格和主题的内容,请参阅使用风格和主题文档。 资源和资产Resources and Assets 资源是Android应用程序不可或缺的部分。总体而言,资源是你想包含和引入到应用程序里面的一些外部元素,比如图片、音频、视频、文本字符串、布局、主题等。每个Android应用程序包含一个资源目录(res/)和资产目录(assets/),资产不经常被使用,因为它们的应用程序很少。你仅在需要读取原始字节流时才需要保存数据为资产。资源和资产目录均驻留在Android项目树的顶端,和源代码目录(src/)处在同一级上。 资源和资产从表面上看没多大区别,不过总体上,在存储外部内容时资源用得更多。真正的区别在于任何放置在资源目录里的内容可以通过您的应用程序的R类访问,这是被Android编译过的。而任何存放在资产目录里的内容会保持它的原始文件格式,为了读取它,你必须使用AssetManager来以字节流的方式读取文件。所以保持文件和数据在资源中(res/)中会更方便访问。 在这篇文章中,你将获取关于Android应用程序经常使用的标准资源类型以及如何在代码中引用方面的信息。资源和国际化(Resources and Internationalization)是第一步,可以知道Android如何利用项目资源。然后,可用资源类型(Available Resource Types)汇总描述了各种资源类型及其规格引用。 资源和国际化Resources and Internationalization 资源是外部文件(即非源代码文件),它们被你的代码使用,并且在编译时被编译到你的应用程序中。Android支持很多不同类型的资源文件,包括XML、PNG和JPEG文件。XML文件会由于其所描述的内容不同而形式不同。该文档描述了所有支持的文件类型及每种类型的语法或格式。 资源从源代码中被抽取出来,基于效率考虑,XML文件被编译成二进制、可以快速加载的形式。字符串,同样被压缩为一种更富效率的存储形式。由于这些原因,在Android平台中我们就有了这些不同的资源类型。 这是一篇纯粹的技术性文档,它和可用资源(Available Resources)一起覆盖了有关资源的众多信息。在使用Android时并不需要记住这篇文档,但是当你需要它时你应该知道来这里寻找信息。 介绍Introduction 这个话题包含了与之相应的术语列表,和一系列在代码中使用资源的实例。关于Android支持的所有资源类型的完整指南,请查阅可用资源(Available Resources)。 Android资源系统记录应用程序中所有非代码资产。你可以使用Resources类来访问应用程序中的资源;一般可以通过Context.getResources()获得这个Resources实例。 一个应用程序的资源在生成(build)时被编译器编译到应用程序的二进制文件中。要使用一个资源,你必须把它放置到源代码树中的正确位置,并且生成(build)到你的应用程序中。作为编译过程的一部分,每个资源的标记都会被生成,在你的源代码中可以使用这些标记-这允许编译器验证你的应用程序代码是否和你定义的资源相匹配。 本部分的其余内容以一个在应用程序中如何使用资源的指南的形式组织。 创建资源Creating Resources Android支持字符串、位图以及其他很多种类型的资源。每一种资源的语法、格式以及存放的位置,都会根据其类型的不同而不同。通常,你创建的资源一般来自于三种文件:XML文件(除位图和raw之外的任何文件)、位图文件(图像)以及Raw文件(除前面以外的其他东西,如声音文件,等等)。事实上,XML文件也有两种不同的类型:被原封不动地编译进包内的文件和被aapt用来产生资源的文件。这里有一个每种资源类型的列表,包括文件格式、文件描述以及XML文件类型的细节。 你可以在你的项目中的res/目录的适当的子目录中创建和保存资源文件。Android有一个资源编译器(aapt),它依照资源所在的子目录及其格式对其进行编译。这里有一个每种资源的文件类型的列表,关于每种类型的描述、语法、格式以及其包含文件的格式或语法见资源参考。 表一 目录Directory 资源类型Resource Types res/anim/ XML文件,它们被编译进逐帧动画(frame by frame animation)或补间动画(tweened animation)对象 res/drawable/ .png、.9.png、.jpg文件,它们被编译进以下的Drawable资源子类型中: 要获得这种类型的一个资源,可以使用Resource.getDrawable(id) 位图文件 9-patches(可变尺寸的位图) 为了获取资源类型,使用mContext.getResources().getDrawable(R.drawable.imageId) 注意:放在这里的图像资源可能会被aapt工具自动地进行无损压缩优化。比如,一个真彩色但并不需要256色的PNG可能会被转换为一个带调色板的8位PNG。这使得同等质量的图片占用更少的资源。所以我们得意识到这些放在该目录下的二进制图像在生成时可能会发生变化。如果你想读取一个图像位流并转换成一个位图(bitmap),请把图像文件放在res/raw/目录下,这样可以避免被自动优化。 res/layout/ 被编译为屏幕布局(或屏幕的一部分)的XML文件。参见布局声明(Declaring Layout) res/values/ 可以被编译成很多种类型的资源的XML文件。 注意:不像其他的res/文件夹,它可以保存任意数量的文件,这些文件保存了要创建资源的描述,而不是资源本身。XML元素类型控制这些资源应该放在R类的什么地方。 尽管这个文件夹里的文件可以任意命名,不过下面使一些比较典型的文件(文件命名的惯例是将元素类型包含在该名称之中): * array.xml定义数据 * colors.xml定义color drawable和颜色的字符串值(color string values)。使用Resource.getDrawable()和Resources.getColor()分别获得这些资源。 * dimens.xml定义尺寸值(dimension value)。使用Resources.getDimension()获得这些资源。 * strings.xml定义字符串(string)值(使用Resources.getString()或者Resources.getText()获取这些资源。getText()会保留在UI字符串上应用的丰富的文本样式)。 · styles.xml定义样式(style)对象。 res/xml/ 任意的XML文件,在运行时可以通过调用Resources.getXML()读取。 res/raw/ 直接复制到设备中的任意文件。它们无需编译,添加到你的应用程序编译产生的压缩文件中。要使用这些资源,可以调用Resources.openRawResource(),参数是资源的ID,即R.raw.somefilename。 资源被编进最终的APK文件中。Android创建了一个封装类,叫做R,在代码中你可以使用它来引用这些资源。R包含了根据资源文件的路径和名称命名的子类。 全局资源说明Global Resource Notes 一些资源允许你定义颜色值。Android接受的颜色值可以使用多种web样式的形式--以下几种包含十六进制常数的形式:#RGB、#ARGB、#RRGGBB、#AARRGGBB。 所有颜色值支持设置透明度(alpha channel value),前两位的十六进制数指定了透明了。0在透明度值是全透明。默认值是不透明。 使用资源Using Resources 这一部分描述如何使用你创建的资源。它包含以下主题: l 代码中使用资源 - 如何在你的代码中调用资源进行实例化。 l 从其他资源中引用资源 - 你可以从其他资源中引用资源。这就使得你可以重用资源中公共资源值。../../Docs/android_dev_guide/android_dev_guide/developer.android.com/guide/topics/resources/resources-i18n.html - ReferencesToResources l 支持针对交替配置的交替资源 - 你可以根据主机硬件的语言或显示配置指定加载不同的资源。 在编译时,Android产生一个名为R的类,它包含了你的程序中所有资源的资源标识符。这个类包含了一些子类,每一个子类针对一种Android支持的资源类型,或者你提供的一个资源文件。每一个类都包含了已编译资源的一个或多个资源标识符,你可以在代码中使用它们来加载资源。下面是一个小的资源文件,包含了字符串、布局(屏幕或屏幕的一部分)和图像资源。 注意:R类是一个自动产生的文件,并没有设计为可以手动编辑。当资源更新时,它会根据需要重新产生。 package com.google.android.samples; public final class R { public static final class string { public static final int greeting = 0x0204000e; public static final int start_button_text = 0x02040001; public static final int submit_button_text = 0x02040008; public static final main_screen_title = 0x0204000a; }; public static final class layout { public static final int start_screen = 0x02070000; public static final int new_user_pane = 0x02070001; public static final int select_user_list = 0x02070002; }; public static final class drawable { public static final int company_logo = 0x02020005; public static final int smiling_cat = 0x02020006; public static final int yellow_fade_background = 0x02020007; public static final int stretch_button_1 = 0x02020008; }; }; 在代码中使用资源Using Resources in Code 在代码中使用资源,只是要知道所有资源ID和你的被编译的资源是什么类型。下面是一个引用资源的语法: R.resource_type.resource_name 或者 android.R.resource_type.resource_name 其中resource_type是R的子类,保存资源的一个特定类型。resource_name时在XML文件定义的资源的name属性,或者有其他文件类型为资源定义的文件名(不包含扩展名)。每一种资源类型都会根据其类型加为一个特定的R子类;要了解R的哪一个子类是关于你的资源类型的,请参考资源参考(resource reference)文档。被你的应用程序编译的资源可以不加包名引用(就像R.resource_type.resource_name这样简单)。Android包含了很多标准资源,如屏幕样式和按钮背景。要在代码中引用这些资源,你必须使用android进行限定,如android.R.drawable.button_background。 这里有一些在代码中使用已编译资源的正确和错误用法的例子。 // Load a background for the current screen from a drawable resource. this.getWindow().setBackgroundDrawableResource(R.drawable.my_background_image); // WRONG Sending a string resource reference into a // method that expects a string. this.getWindow().setTitle(R.string.main_title); // RIGHT Need to get the title from the Resources wrapper. this.getWindow().setTitle(Resources.getText(R.string.main_title)); // Load a custom layout for the current screen. setContentView(R.layout.main_screen); // Set a slide in animation for a ViewFlipper object. mFlipper.setInAnimation(AnimationUtils.loadAnimation(this, R.anim.hyperspace_in)); // Set the text on a TextView object. TextView msgTextView = (TextView)findViewByID(R.id.msg); msgTextView.setText(R.string.hello_message); 引用资源References to Resources 在属性(或资源)中提供的值也可以作为资源的引用。这种情况经常使用在布局文件中,以提供字符串(因此它们可以被本地化<将UI上的字符串放在一个单独的文件中,在做国际化时只需要将它们翻译成相应的语言版本,然后应用程序根据locale信息加载相应的字符串文件——译者注>)和图像(它们存在于另外的文件中),虽然引用可以是任何资源类型,包括颜色和整数。 例如,如果我们有颜色资源(color resources),我们可以编写一个布局文件,将文本的颜色设为那些资源中包含的值: <?xml version="1.0" encoding="utf-8"?> <EditText id="text" xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:textColor="@color/opaque_red" android:text="Hello, World!" /> 注意,这里使用“@”前缀引入对一个资源的引用——在@[package:]type/name形式中后面的文本是资源的名称。在这种情况下,我们不需要指定包名,因为我们引用的是我们自己包中的资源。要引用系统资源,你应该这样写: <?xml version="1.0" encoding="utf-8"?> <EditText id="text" xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:textColor="@android:color/opaque_red" android:text="Hello, World!" /> 另外一个例子,当在布局文件中提供字符串以便于本地化时,你应该一直使用资源引用。 <?xml version="1.0" encoding="utf-8"?> <EditText id="text" xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:textColor="@android:color/opaque_red" android:text="@string/hello_world" /> 这种技巧还可以用来创建资源之间的引用。例如,我们可以创建新的drawable资源作为已存在资源的别名。 <?xml version="1.0" encoding="utf-8"?> <resources> <drawable id="my_background">@android:drawable/theme2_background</drawable> </resources> 引用主题属性References to Theme Attributes 另外一种资源值允许你引用当前主题中的属性的值。这个属性值只能在样式资源和XML属性中使用;它允许你通过将它们改变为当前主题提供的标准变化来改变UI元素的外观,而不是提供具体的值。 如例中所示,我们在布局资源中使用这个特性将文本颜色设定为标准颜色的一种,这些标准的颜色都是定义在基本系统主题中。 <?xml version="1.0" encoding="utf-8"?> <EditText id="text" xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:textColor="?android:textDisabledColor" android:text="@string/hello_world" /> 注意,这和资源引用非常类似,除了我们使用一个“?”前缀代替了“@”。当你使用这个标记时,你就提供了属性资源的名称,它将会在主题中被查找——因为资源工具知道需要的属性资源,所以你不需要显示声明这个类型(如果声明,其形式就是?android:attr/android:textDisabledColor)。 除了使用这个资源的标识符来查询主题中的值代替原始的资源,其命名语法和“@”形式一致:?[namespace:]type/name,这里类型可选。 使用系统资源Using System Resources 在系统中的包含了很多应用程序可以使用的资源。所有的这些资源都在“android.R”类下定义。例如,使用下面的代码你可以在屏幕上显示标准应用程序的图标: public class MyActivity extends Activity { public void onStart() { requestScreenFeatures(FEATURE_BADGE_IMAGE); super.onStart(); setBadgeResource(android.R.drawable.sym_def_app_icon); } } 以相似的方式,下面的代码将对你的屏幕应用系统定义的标准“绿色背景”视觉处理。 public class MyActivity extends Activity public void onStart() { super.onStart(); setTheme(android.R.style.Theme_Black); } } 替换资源(为了可替换的资源和配置)Alternate Resources 你可以根据UI语言或者设备上的硬件配置,为你的产品提供不同的资源。注意,尽管你可以包含不同的字符串、布局和其他资源,然而SDK没有方法供你指定加载哪一个替换资源。Android检测关于硬件和未知的适当配置,然后适当加载。用户可以使用设备上的设置面板选择替换语言设置。 为了包含替换资源,需要创建平行的资源文件夹,而文件夹的名字后面要使用限定符表示它要应用的配置(语言、屏幕方向等等)。例如,下面的工程包含了字符串资源,一个用于英语,而另外一个用于法语: MyApp/ res/ values-en/ strings.xml values-fr/ strings.xml Android支持几种类型的限定符,每一个都有不同的值。把它们连接在资源文件夹名称的后面,使用短横线隔开。你可以为每一个文件夹名称添加多个限定符,但是它们必须按照这里列出的顺序排列。例如,一个包含drawable资源的文件夹,对于一个完整详细的配置,可能看起来像: MyApp/ res/ values-en/ drawable-en-rUS-port-160dpi-finger-qwerty-dpad-480x320/ 更典型的是,你只需指定一些特定的要定义资源的配置选项。你可以放弃完整列表中的任何值,但同时要保证剩下的值仍然保持列表中的顺序。 MyApp/ res/ drawable-en-rUS-finger/ drawable-port/ drawable-port-160dpi/ drawable-qwerty/ 表2 列举了合法的限定符目录名称,按优先级排序。下表中列举在上面的限定符比下面的具有更高的优先级,如同Android如何查找最匹配的目录中所描述的那样。 表2 限定符Qualifier 值Values 移动国家码MCC和移动网络码MNC 手机设备SIM卡上的移动国家码和移动网络码。比如mcc310-mnc004 (美国,Verizon品牌); mcc208-mnc00 (法国, Orange品牌); mcc234-mnc00 (英国,BT品牌). 如果这个设备使用一个无线连接(GSM电话),则MCC来自SIM卡,而MNC来自该设备将要附着的网络。你有时会仅使用MCC,例如包含特定国家合法资源在您的应用程序中。如果您的应用程序指定了MCC/MNC组合的资源,这些资源仅在MCC和MNC都匹配的时候才能使用。 语言和区域Language and region 两个字母的ISO 639-1语言码和ISO 3166-1-alpha-2区域码 (以"r"为前缀)。比如en-rUS, fr-rFR, es-rES. 这个代码是大小写敏感的:语言码是小写字母,国家码是大写字母。你不能单独指定一个区域,但是你可以单独指定一个语言,比如en, fr, es, zh. 屏幕方向Screen orientation 纵向,横向,正方形(port, land, square) 屏幕像素密度Screen pixel density 92dpi, 108dpi等. 当Android选择使用哪个资源时,它对屏幕像素密度的处理和其它限定符不同。在文章后面描述的步骤1Android如何查找最匹配的目录中,屏幕密度总被认为是匹配的。在步骤4中,如果被考虑的限定符是屏幕密度,Android将选择在那个位置的最佳匹配,而无需继续步骤5。 触摸屏类型Touchscreen type 非触摸式,触摸笔,手指(notouch, stylus, finger) 键盘可用方式Whether the keyboard is available to the user 外在键盘,隐藏键盘,软键盘(keysexposed, keyshidden, keyssoft) 如果你的应用程序有一个特定的资源只能通过软件盘使用,则使用keyssoft值,如果没有keyssoft资源可用(只有keysexposed和 keyshidden)并且该设备显示了一个软键盘,那么系统将使用keysexposed资源。 首选文本输入方法Primary text input method 不支持按键,标准键盘,12键(nokeys, qwerty, 12key) 首选非触摸式导航方法Primary non-touchscreen navigation method 不支持导航,滑板,跟踪球,滚轮(nonav, dpad, trackball, wheel) 屏幕分辨率Screen dimensions 320x240, 640x480, 等. 更大的分辨率必须先被指定。 SDK版本SDK version 设备支持的SDK版本,比如v3。Android1.0 SDK是v1,1.1SDK是v2,1.5SDK是v3。 小版本(Minor version) 你目前还不能指定小版本,它总是被设置为0。 这个列表不包含设备特有的参数比如载波,品牌,设备/硬件,或者制造商。所有应用程序需要知道的设备信息均通过上表中的资源限定符编码。 所有资源目录,许可的和未经许可的,都存放在res/目录下。下面是一些关于许可的资源目录名称的指导原则: l 你可以指定多个限定符,用破折号分开。比如,drawable-en-rUS-land会被应用在美国英语的横向手机设备中。 l 限定符必须符合表2中列举的顺序。比如: * 正确的:values-mcc460-nokeys/ * 错误的:values-nokeys-mcc460/ l 限定符的值大小写敏感。比如一个纵向特定的drawable目录必须命名为drawable-port,不可以是drawable-PORT或drawable-Port。 l 每个限定符类型仅支持一个值。比如,如果你想使用为法国和西班牙使用相同的drawable文件,你得需要两个资源目录,如drawable-rES/和drawable-rFR/,包含相同的文件。你不能使用一个名为drawable-rES-rFR的目录。 l 限定符不能嵌套使用。比如,你不能使用res/drawable/drawable-en。 资源怎么在代码中使用How resources are referenced in code 所有的资源均通过它们简单未经修饰的名字在代码或资源引用语法中引用。所以如果一个资源命名如下: MyApp/res/drawable-port-92dpi/myimage.png 它会被这样引用: R.drawable.myimage (code) @drawable/myimage (XML) 如果有多个drawable目录可用, Android将会选择其一(如下所述)并从中加载myimage .png。 AndroidHow Android finds the best matching directory如何查找最匹配的目录 Android将从各种潜在的资源中挑选出哪个应该在运行时使用,这取决于设备的当前配置。这里的例子假定使用了如下的设备配置: 区域Locale = en-GB 屏幕方向Screen orientation = port 屏幕像素密度Screen pixel density = 108dpi 触摸屏类型Touchscreen type = notouch 首选文本输入方式Primary text input method = 12key 下面说明了Android如何作出选择: 1. 排除和设备配置冲突的资源文件。比如,假定如下的drawables资源目录可用。那么drawable-fr-rCA/会被排除,因为它和设备的区域配置冲突。 MyApp/res/drawable/ MyApp/res/drawable-en/ MyApp/res/drawable-fr-rCA/ MyApp/res/drawable-en-port/ MyApp/res/drawable-en-notouch-12key/ MyApp/res/drawable-port-92dpi/ 例外:屏幕像素密度是唯一不用来排除文件的限定符。即使设备屏幕密度是108dpi,drawable-port-92dpi/也不会被从列表中排除,因为在这里所有的屏幕密度都被视为匹配。 2. 从表2中选取最高优先级的限定符(从MCC开始,然后自该列表依次往下)。 3. 有没有哪个可用的资源目录包含了这个限定符? ² 如果没有,回到步骤2然后查看表2中所列的下一个限定符。在我们的例子中,答案是“没有”直到我们到达语言这一级。If No, return to step 2 and look at the next qualifier listed in Table 2. In our example, the answer is "no" until we reach Language; ² 如果有,则跳转到步骤4。 4. 排除不包含这个限定符的资源目录,在我们的例子中,我们排除所有不包含语言的目录。 MyApp/res/drawable/ MyApp/res/drawable-en/ MyApp/res/drawable-en-port/ MyApp/res/drawable-en-notouch-12key/ MyApp/res/drawable-port-92dpi/ MyApp/res/drawable-port-notouch-12key 例外:如果询问中的限定符是屏幕像素密度,Android会选择最接近匹配于设备的选项,而且选择过程将会完成。一般而言,Android会倾向于缩小一个大图片而不是放大一个小图片。 5. 回头重复步骤2,3,4直到只剩下一个选择。在本例中,屏幕方向是下一个要比较的限定符,我们排除没有指定屏幕方向的资源。现在只剩下一个选择,那就是它了。当drawables被这个应用程序调用时,Android系统会从下面这个目录中加载资源:MyApp/res/drawable-en-port/ 提示Tip:限定符的优先权比匹配的数目要重要得多。比如,在上面的步骤4中,列表中最后的选项包含三个限定符和设备匹配(方向,触摸屏类型,和输入法),而drawable-en只有一个参数匹配(语言)。但是,语言拥有更高的优先权,所以drawable-port-notouch-12key 被排除出局。 下面的流程图总结了Android如何选择资源目录来加载的过程: 术语Terminology 资源系统将一系列分散内容集合在一起形成最终的完整的资源功能,去帮助我们了解整个系统。这里有一些核心概念以及组件的概要说明,你在开发中将可能使用到: 资产Asset:应用程序的独立的数据块。这包含所有从java程序编译成的目标文件,图像 (例如PNG图片), XML文件等等。这些文件以一种特定的方式组织在一起,在程序最后打包时,它们被捆绑进一个单独的ZIP文件里。 aapt::Android最终文件打包工具。这个工具产生最终程序的ZIP文件。除了将资产元数据文件收集在一起,它也把资源定义解析到最终的二进制数据里。 资源表Resource Table: aapt工具产生的特殊的文件,描述了所有在程序/包里的资源。这个文件可以通过资源类来访问;它不能直接和应用程序接触。 资源Resource: 资源表里一条记录描述的是单一的命名值。大体上, 资源分成两种:元资源和包资源. 资源标识符Resource Identifier: 在资源表里所有的资源都被唯一的整数标识着。所有的代码中(资源描述,XML 文件,Java源代码)你可以直接使用符号名代替真实的整数数值。 元资源Primitive Resource: 所有元资源都可以被写成一个简单的字串,使用一定的格式可以描述资源系统里各种不同的基本类型:整数,颜色,字串,其他资源的引用,等等。像图片以及XML描述文件这些复杂资源,被以元字串资源储存,它们的值就是相关最终数据文件的路径。 包资源Bag Resource: 一种特殊类型的资源,不是简单的字符串,而是一个容纳名字/数值对的任意列表。每个名字本身就是资源标识,每个值可以容纳相同类型的字符串格式的数据作为一个普通资源。包资源支持继承:一个包里的数据能从其他包里继承,有选择地替换或者扩展能产生它自己的内容。 种类Kind: 资源种类是对于不同需求的资源标识符而言的。例如,绘制资源类常常实例化绘制类的对象,所以这些包含颜色以及指向图片或XML文件的字符串路径数据是原始数据。其它常见资源类型是字符串(本地化字符串),颜色(基本颜色),布局(一个指向XML文件的字串路径,它描述的是一个用户界面)以及风格(一个描述用户接口属性的包装资源)。还有一个标准的“attr”资源类型,它定义了命名包装数据以及XML属性的资源标识符。 风格Style: 包含包装资源类型的名字常常用来描述一系列用户接口属性。例如,一个TextView的类可能会有一个描述界面风格的类来定义文本大小,颜色以及对齐方式。在一个界面布局的XML文件中,可以使用“风格” 属性来确定整体界面风格,它的值就是风格资源的名字。 风格类Style Class: 这里将详述一些属性资源类。其实数据不会被放在资源表本身,通常在源代码里它以常量的形式出现,这也可以使你在风格类或者XML的标签属性里方便找到它的值。例如,Android平台里定义了一个“视图”的风格类,它包含所有标准视图的属性:画图区域,可视区域,背景等。这个视图被使用时,它就会借助风格类去从XML文件取得数据并将其载入到实例中。 配置Configuration: 对许多特殊的资源标识符,根据当前的配置,可以有多种不同的值。配置包括地区(语言和国家),屏幕方向,屏幕分辨率,等等。当前的配置用来选择当资源表载入时哪个资源值生效。 主题Theme: 一个标准类型的资源能为一个特殊的上下文提供全局的属性值。例如,当应用工程师写一个活动时,他能选择一个标准的主题去使用,白色的或者黑色的;这个类型能提供很多信息,如屏幕背景图片/颜色,默认文本颜色,按钮类型,文本编辑框类型,文本大小,等。当布置一个资源布局时,控件(文本颜色,选中后颜色,背景)的大部分设置值取自当前主题;如果需要,布局中的风格以及属性也可以从主题的属性中获得。 覆盖层Overlay: 资源表不能定义新类型的资源,但是你可以在其他表里替换资源值。就像配置值,这可以在装载时候进行;它能加入新的配置值(例如,改变字串到新的位置),替换现有值(例如,将标准的白色背景替换成"Hello Kitty"的背景图片),修改资源包(例如修改主题的字体大小。白色主题字体大小为18pt)。这实际上允许用户选择设备不同的外表,或者下载新的外表文件。 资源引用Resource Reference 可用资源Available Resources文档提供了一个各种类型资源的详细列表,并描述了如何在Java代码中或其他引用中使用它们。 国际化和本地化Internationalization and Localization 即将完成:国际化和本地化是非常关键的,但现在的SDK还没有完全准备好。当SDK成熟时,这个章节会包含Android平台国际化和本地化的相关信息。在此期间,让我们先从把资源外部化以及练习以好的结构创建和使用资源开始做起吧。 |
|
cuihai |
2011-03-03 12:14 |
意图和意图过滤器Intents and Intent Filters 一个应用程序的三个核心组件-活动,服务和广播接收器是通过消息即意图(Intents)来激活的。Intent息传送是相同或不同应用中组件运行时晚绑定的一种机制。意图本身,一个意图对象,是一个包含被执行操作抽象描述的被动的数据结构-或者,对于广播而言,是某件已经发生并被声明的事情的描述。存在不同的机制来传送意图到每种组件中: * 一个意图对象是传递给Context.startActivity()或者Activity.startActivityForResult()来启动一个活动或者让一个存在的活动去做某些新的事情。 * 一个意图对象是传递给Context.startService()来发起一个服务或者递交新的指令给运行中的服务。类似的,一个意图能被传递给Context.bindService() 来在调用组件和一个目标服务之间建立连接。作为一个可选项,它可以发起这个服务如果还没运行的话。 * 传递给任意广播方法(例如Context.sendBroadcast(),Context.sendOrderedBroadcast(), 或者Context.sendStickyBroadcast())的意图对象被传递给所有感兴趣的广播接收者。许多种广播产生于系统代码。 在每个例子里,Android系统找到合适的活动,服务,或者一组广播接收者来回应这个意图,必要时实例化它们。这些消息传送系统没有重叠:广播意图仅被传递给广播接收者,永远不会给活动或者服务。一个传送给startActivity()的意图是只会被传递给一个活动,永远不会给一个服务或广播接收者,如此类推。 这篇文档以意图对象的描述开始,然后描述Android映射意图到组件的规则-如何解决哪个组件应该接收一个意图消息。对于没有显式命名一个目标组件的意图,这个过程包括对照与潜在目标相关联的意图过滤器来测试这个意图对象。 意图对象Intent Objects 一个意图Intent对象是一堆信息。它包含接收这个意图的组件感兴趣的信息(例如将要采取的动作和操作的数据)再加上Android系统感兴趣的信息(例如应该处理这个意图的组件类别和如何启动一个目标活动的指令): 组件名称Component name 应该处理这个意图的组件名字. 这个字段是一个ComponentName对象- 一个组合物:目标组件的完全合格的类名 (比如"com.example.project.app.FreneticActivity") 以及应用程序描述文件中设置的组件所在包的名字(比如, "com.example.project"). 这个组件名字的包部分和描述文件中设置的包名字不一定要匹配。 组件名字是可选的。如果被设置了,这个意图对象将被传递到指定的类。如果没有, Android使用另外的意图对象中的信息去定位一个合适的目标- 请看本文稍后描述的意图解析Intent Resolution。 组件名字通过如下方法:setComponent(),setClass(), 或者setClassName()设置并通过getComponent()读取。 动作Action 一个将被执行的动作的字符串命名-或者, 对于广播意图而言, 是发生并被报告的动作。这个意图类定义了一些动作常量, 包含下面这些: 常量 目标组件 Action ACTION_CALL 活动 开始一个电话呼叫 ACTION_EDIT 活动 显示数据以给用户编辑 ACTION_MAIN 活动 开始任务的初始活动,没有输入数据也没有输出返回 ACTION_SYNC 活动 同步服务器与移动设备之间的数据 ACTION_BATTERY_LOW 广播接收器 电池低电量警告 ACTION_HEADSET_PLUG 广播接收器 耳机插拔 ACTION_SCREEN_ON 广播接收器 屏幕开启 ACTION_TIMEZONE_CHANGED 广播接收器 时区变化 通过查看Intent类描述可获得一个通用动作的预定义常量列表。其他动作被定义在Android API的其他地方。你也可以自定义动作字符串来激活应用程序中的组件。那些你所创建的动作字符串应该以应用程序包名作为前缀-例如: "com.example.project.SHOW_COLOR". 动作很大程度上决定了意图其他部分如何被组织-尤其是数据data和附加字段extras-很像一个方法名决定了一些参数和返回值. 因此, 一个好的想法是使用尽可能具体的动作名并和意图的其他字段紧密联系起来。换句话说,为您的组件能处理的意图对象定义一个整体的协议而不是定义一个孤立的动作。 一个意图对象里的动作可以通过setAction()方法设置和通过getAction()方法读取. 数据Data 想要操作的数据统一资源标识符(URI)和那种数据的多用途互联网邮件扩展(MIME). 不同的动作伴随着不同种类的数据规格。例如,如果动作是ACTION_EDIT,数据字段会包含可编辑文档的URI;如果动作是ACTION_CALL,数据字段会是一个电话号码:含呼叫电话号码的URI;类似的,如果动作是ACTION_VIEW而且数据字段是一个http:URI, 那么接收到的活动将会是下载并显示URI所引用数据的请求。当匹配一个意图到一个能处理数据的组件时,除了它的URI外,通常需要知道数据类型(它的MIME类型)。 比如,一个能显示图片的组件不应该被要求去播放一个声音文件。 在很多情况下,这个数据类型可以从URI里推断出来-尤其是content:URIs, 这意味着数据被存放在设备上而且由一个内容提供者控制着。(参阅separate discussion on content providers). 但类型可以在意图对象里显示的设置。setData()方法指定数据只能为一个URI,setType()指定它只能是一个MIME类型, 而setDataAndType()指定它同时为URI和MIME类型。URI通过getData()读取,类型则通过getType(). 目录Category 一个包含关于应该处理这个意图的组件的附加信息的字符串。任意数目的类别描述可以被放到一个意图对象里。和动作一样,意图类定义若干类别常量,包含如下这些: 常量 含义 CATEGORY_BROWSABLE 目标活动可以被浏览器安全的唤起来显示被一个链接所引用的数据-比如,一张图片或一条e-mail消息。 CATEGORY_GADGET 这个活动可以被嵌入到充当配件宿主的另外的活动里面。 CATEGORY_HOME 这个活动将显示桌面,也就是用户开机后看到的第一个屏幕或者按HOME键时看到的屏幕。 CATEGORY_LAUNCHER 这个活动可以是一个任务的初始活动并被列在应用程序启动器的顶层。 CATEGORY_PREFERENCE 目标活动是一个选择面板。 查阅Intent类描述可获取类别的完整列表。 addCategory()方法在一个意图对象中添加了一个目录,removeCategory()删除之前添加的目录,而getCategories()可以获取当前对象的所有类别。 附加信息Extras 应该递交给意图处理组件的附加信息键-值对。就像一些动作伴随着特定的数据URIs类型,一些动作则伴随着特定的附加信息。比如,一个ACTION_TIMEZONE_CHANGED意图有一个“时区”附加信息用来区别新的时区,而ACTION_HEADSET_PLUG有一个“状态”附加字段表明耳机有没有插着,以及一个“名字”附加信息来表示耳机的类型。如果你想要创建一个SHOW_COLOR动作,颜色的值将被设置在一个附加的键-值对中。 意图对象有一系列的put...()方法来插入各种不同的附加数据和一个类似的用来读取数据的get...()方法系列。这些方法与Bundle对象的方法相似。事实上,附加信息可以被当作一个Bundle通过使用putExtras()和getExtras()方法安装和读取。 标志Flags 各种类型的标志. 许多标志用来指示Android系统如何去加载一个活动(例如,哪个是这个活动应该归属的任务)和启动后如何对待它(比如,它是否属于当前活动列表),所有这些列表都在意图类中定义了。 Android系统以及这个平台上的应用程序利用意图对象来发送源于系统的广播以及激活系统定义的组件。要查阅如何组织一个意图去激活一个系统组件,请咨询引用中的意图列表list of intents。 意图解析Intent Resolution 意图可以被分成两组: * 显式意图 通过名字指明目标组件(这个组件名字字段component name field, 前面提到过, 有一个数值集)。既然组件名称通常不为其他应用程序的开发者所了解,显式意图典型的被用作应用程序的内部消息-例如一个活动启动一个附属服务或姊妹活动。 * 隐式意图 不命名目标组件(组件名称字段为空)。隐式意图经常用来激活其他应用程序的组件。 Android递交一个显式的意图给一个指定目标类的实例。意图对象中的组件名称唯一的确定哪个组件应该获取这个意图。隐式意图需要一个不同的策略。在没有指定目标的情况下,Android系统必须找到最合适的组件来处理这个意图-单个活动或者服务来执行这个请求动作或者一系列的广播接收器来应对广播通告。 这是通过比较意图对象的内容和意图过滤器,有可能接收意图的组件相关结构。过滤器公布一个组件具备的能力以及限定它能处理的意图。他们使组件接收该公布类型的隐式意图成为可能。如果一个组件没有任何的意图过滤器,那它只能接收显式意图。一个带过滤器的组件可以同时接收显式和隐式意图。 当一个意图对象被一个意图过滤器测试时,只有三个方面会被参考到: 动作 数据(URI以及数据类型) 类别 附加信息和标志并不参与解析哪个组件接收一个意图。 意图过滤器Intent filters 为了通知系统它们可以处理哪些意图,活动、服务和广播接收器可以有一个或多个意图过滤器。每个过滤器描述组件的一个能力,一系列组件想要接收的意图。它实际上按照一个期望的类型来进行意图滤入,同时滤出不想要的意图-但是只有不想要的隐式意图会被滤出(那些没有命名目标的对象类)。一个显式意图总能够被递交给它的目标,而无论它包含什么。这种情况下过滤器不起作用。但是一个显式意图仅当它能通过组件的一个过滤器时才可以被递交到这个组件。 组件为它能做的每项工作,每个呈现给用户的不同方面分有不同的过滤器。比如,范例记事本应用程序中的主要活动有三个过滤器-一个是空白板,另一个是用户可以查看、编辑、或选择的一个指定的记事目录,第三是在没有初始目录说明的情况下查找一个特定的记录。一个意图过滤器是IntentFilter类的一个实例。但是,由于Android系统在启动一个组件前必须知道这个组件的能力,意图过滤器通常不会用Java代码来设置,而是在应用程序清单文件(AndroidManifest.xml)中设置<intent-filter>元素。(有一个例外,通过调用Context.registerReceiver()来注册的广播接收器的过滤器;它们是作为意图过滤器对象而被直接创建的。 过滤器与安全Filters and security 不能信赖一个意图过滤器的安全性。当它打开一个组件来接收某些特定类型的隐式意图,它并不能阻止以这个组件为目标的显式意图。即使过滤器对组件要处理的意图限制某些动作和数据源,总有人能把一个显式意图和一个不同的动作及数据源组合在一起,然后命名该组件为目标。 一个过滤器和意图对象有同样的动作、数据以及类别字段。一个隐式意图在过滤器的所有三个方面都被测试。为了递交到拥有这个过滤器的组件,它必须通过所有这三项测试。即便只有一个不通过,Android系统都不会把它递交给这个组件-至少以那个过滤器的标准而言。不过,由于一个组件可以包含多个意图过滤器,一个不能通过其中一个组件过滤器的意图可能在另外的过滤器上获得通过。 三个测试详细描述如下: 动作测试Action test 清单文件中的意图过滤器元素里列举了动作元素,比如: <intent-filter . . . > <action android:name="com.example.project.SHOW_CURRENT" /> <action android:name="com.example.project.SHOW_RECENT" /> <action android:name="com.example.project.SHOW_PENDING" /> . . . </intent-filter> 如同例子所示,一个意图对象只对单个动作命名,而一个过滤器可能列举多个。列表不能为空;一个过滤器必须包含至少一个动作元素,否则它将阻塞所有的意图。 为了通过这个测试,在意图对象中指定的动作必须匹配过滤器中所列举的动作之一。如果意图对象或过滤器不指定一个动作,结果将如下: · 如果这个过滤器没有列出任何动作,那意图就没有什么可匹配的,因此所有的意图都会测试失败。没有意图能够通过这个过滤器。 · 另一方面,一个未指定动作的意图对象自动通过这个测试-只要过滤器包含至少一个动作。 类别测试Category test 一个意图过滤器<intent-filter>元素也列举了类别作为子元素。比如: <intent-filter . . . > <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> . . . </intent-filter> 注意前面描述的动作和类别常量没有在清单文件中使用。相反使用了完整的字符串。比如,对应于前述CATEGORY_BROWSABLE常量,上面的例子里使用了"android.intent.category.BROWSABLE"字符串。类似的,字符串"android.intent.action.EDIT" 对应于ACTION_EDIT常量。 对一个通过类别测试的意图,每个意图对象中的类别必须匹配一个过滤器中的类别。这个过滤器可以列举另外的类别,但它不能遗漏任何在这个意图中的类别。 因此,原则上一个没有类别的意图对象应该总能够通过测试,而不管过滤器里有什么。绝大部分情况下这个是对的。但有一个例外,Android把所有传给startActivity()的隐式意图当作他们包含至少一个类别:"android.intent.category.DEFAULT" (CATEGORY_DEFAULT常量)。因此,想要接收隐式意图的活动必须在它们的意图过滤器中包含"android.intent.category.DEFAULT"。(带"android.intent.action.MAIN"和"android.intent.category.LAUNCHER"设置的过滤器是例外)。它们标记那些启动新任务和呈现在启动屏幕的活动。它们可以在类别列表中包含"android.intent.category.DEFAULT",但不是必要的。) 可查阅后面的使用意图匹配(Using intent matching)以获得更多关于过滤器的信息。 数据测试Data test 就像动作和类别,一个意图过滤器的数据规格被包含在一个子元素中。而且这个子元素可以出现多次或一次都不出现。例如: <intent-filter . . . > <data android:type="video/mpeg" android:scheme="http" . . . /> <data android:type="audio/mpeg" android:scheme="http" . . . /> . . . </intent-filter> 每个数据<data>元素可以指定一个URI和一个数据类型(MIME媒体类型)。有一些单独的属性-模式,主机,端口和路径-URI的每个部分: scheme://host:port/path 比如,在下面的URI里面, content://com.example.project:200/folder/subfolder/etc 模式是"内容",主机是"com.example.project",端口是"200",路经是"folder/subfolder/etc"。主机和端口一起组成URI鉴权(authority);如果未指定主机,端口会被忽略。 这些属性都是可选的,但彼此有依赖关系:一个授权要有意义,必须指定一个模式。一个路经要有意义,必须同时指定模式和鉴权。 当一个意图对象中的URI被用来和一个过滤器中的URI规格比较时,它实际上比较的是上面提到的URI的各个部分。比如,如果过滤器仅指定了一个模式,所有那个模式的URIs和这个过滤器相匹配;如果过滤器指定了一个模式、鉴权但没有路经,所有相同模式和鉴权的URIs可以匹配上,而不管它们的路经;如果过滤器指定了一个模式、鉴权和路经,只有相同模式、鉴权和路经的URIs可以匹配上。当然,一个过滤器中的路径规格可以包含通配符,这样只需要部分匹配即可。 数据<data>元素的类型属性指定了数据的MIME类型。这在过滤器里比在URI里更为常见。意图对象和过滤器都可以使用一个"*"通配符指定子类型字段-比如,"text/*"或者"audio/*"-指示任何匹配的子类型。 数据测试同时比较意图对象和过滤器中指定的URI和数据类型。规则如下: a. 一个既不包含URI也不包含数据类型的意图对象仅在过滤器也同样没有指定任何URIs和数据类型的情况下才能通过测试。 b. 一个包含URI但没有数据类型的意图对象仅在它的URI和一个同样没有指定数据类型的过滤器里的URI匹配时才能通过测试。这通常发生在类似于mailto:和tel:这样的URIs上:它们并不引用实际数据。 c. 一个包含数据类型但不包含URI的意图对象仅在这个过滤器列举了同样的数据类型而且也没有指定一个URI的情况下才能通过测试。 d. 一个同时包含URI和数据类型(或者可从URI推断出数据类型)的意图对象可以通过测试,如果它的类型和过滤器中列举的类型相匹配的话。如果它的URI和这个过滤器中的一个URI相匹配或者它有一个内容content:或者文件file: URI而且这个过滤器没有指定一个URI,那么它也能通过测试。换句话说,一个组件被假定为支持content:和file: 数据如果它的过滤器仅列举了一个数据类型。 如果一个意图可以通过不止一个活动或服务的过滤器,用户可能会被询问要激活那个组件。如果没有发现目标对象将会出现异常。 通常情况Common cases 上面描述的数据测试的最后一个规则(d),表达了这样一个期望即组件能够从文件或内容提供者中获取本地数据。因此,它们的过滤器可以只列举一个数据类型而不需要显式的命名content:和file:模式。这是一个典型情况。比如,一个如下的数据<data>元素,告诉Android这个组件能从内容提供者获取图片数据并显示: <data android:type="image/*" /> 既然大多数可用数据是通过内容提供者来分发,那么过滤器最通常的配置就是指定一个数据类型而不指定URI。另外一个通用的配置是带有一个模式和数据类型的过滤器。比如,一个如下的数据<data>元素告诉Android可以从网络获取视频数据并显示: <data android:scheme="http" android:type="video/*" /> 比如,想一下,当用户点击网页上的一个链接时浏览器做了什么。它首先试图去显示这个数据(如果这个链接指向一个HTML页面)。如果它不能显示这个数据,它会把一个显式意图和一个模式、数据类型组成整体然后尝试启动一个可以处理这个工作的活动。如果没有接受者,它将要求下载管理器来下载数据。这让它处于内容提供者的控制下,以便一个潜在的更大的活动池可以做出反应。 大多数应用程序同样有一个方法去启动刷新,而不包含任何特定数据的引用。能初始化应用程序的活动拥有指定动作为"android.intent.action.MAIN"的过滤器。如果它们表述在应用程序启动器中,那它们同样指定了"android.intent.category.LAUNCHER"类别: <intent-filter . . . > <action android:name="code android.intent.action.MAIN" /> <category android:name="code android.intent.category.LAUNCHER" /> </intent-filter> 使用意图匹配Using intent matching 通过意图过滤器匹配的意图不仅是为了发现要激活的目标组件,而且为了发现这个设备上的一系列组件的某些东西。比如,Android系统通过查找符合条件的所有活动(需要包含指定了动作"android.intent.action.MAIN"和"android.intent.category.LAUNCHER"类别的意图过滤器,如前面章节所述)来产生应用程序启动器,也就是用户可用程序的前置屏幕。然后它显示在这个启动器里的这些活动的图标和标签。类似的,它通过查找其过滤器配有"android.intent.category.HOME"元素的活动来发现桌面。 你的应用程序可以用类似的方式使用意图匹配。PackageManager有一系列的查询query…()方法可以接收一个特定的意图,以及相似的一个解析resolve…()方法系列可以确定应答意图的最佳组件。比如,queryIntentActivities()返回一个所有活动的列表,而queryIntentServices()返回一个类似的服务列表。两个方法都不会激活组件;它们仅仅列举能应答的。对于广播接收者,有一个类似的方法queryBroadcastReceivers()。 数据存储Data Storage 概览Storage quickview ² 系统偏好:快速,轻量级存储 ² 文件:存储到设备内部或可移动闪存 ² 数据库:任意的结构化存储 ² 支持基于网络的存储 一个典型的桌面操作系统提供了一个通用文件系统使得任何应用程序能够使用它来存储文件,这些文件可以被其它应用程序读取(可能有访问权限的设置)。Android使用一个不同的系统:在Android上,所有应用程序数据(包括文件)都是该应用程序私有的。 不过,Android同样提供了一个应用程序向其它应用程序暴露其私有数据的基本方式-通过内容提供器。内容提供器是应用程序的可选组件,用来暴露该应用程序数据的读写接口,且遵循任何可能引入的约定。内容提供器实现了一个用来请求和修改数据的基本语法,一个读取返回数据的基本机制。Android为基础数据类型如图像,音频和视频文件以及个人联系人信息提供了许多内容提供器。想要了解更多如何使用内容提供器的信息,请参见一篇单独的文章:内容提供器(Content Providers)。 无论你是否想把应用程序数据输出给别人,你总需要有一个方法来保存它。Android提供了下面4种机制来保存和获取数据:系统偏好Preferences,文件Files,数据库Databases和网络Network。 系统偏好Preferences 系统偏好是一个用来存放和提取元数据类型键-值对的轻量级机制。它通常用来存放应用程序偏好,例如一个应用程序启动时所使用的默认问候或文本字体。通过调用Context.getSharedPreferences()来读写数值。如果你想分享给应用程序中的其它组件,可以为你的偏好集分配一个名字,或者使用没有名字的Activity.getPreferences()方法来保持对于该调用程序的私有性。你不能跨应用程序共享偏好(除了使用一个内容提供器)。 下面是一个为计算器设置按键静音模式的例子: import android.app.Activity; import android.content.SharedPreferences; public class Calc extends Activity { public static final String PREFS_NAME = "MyPrefsFile"; . . . @Override protected void onCreate(Bundle state){ super.onCreate(state); . . . // Restore preferences SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0); boolean silent = settings.getBoolean("silentMode", false); setSilent(silent); } @Override protected void onStop(){ super.onStop(); // Save user preferences. We need an Editor object to // make changes. All objects are from android.context.Context SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0); SharedPreferences.Editor editor = settings.edit(); editor.putBoolean("silentMode", mSilentMode); // Don't forget to commit your edits!!! editor.commit(); } } 文件Files 你可以直接在移动设备或可移动存储媒介里存放文件。缺省情况下,其它应用程序不能访问这些文件。 为了从文件中读取数据,可调用Context.openFileInput()方法并传递本地文件名和文件路径给它。该方法返回一个标准的Java FileInputStream对象。为了写一个文件,可调用Context.openFileOutput()并传递文件名和路径,这个方法也返回FileOutputStream对象。从另外的应用程序中调用这些方法将不起作用,你只能访问本地文件。 如果你有一个静态文件需要在编译时打包进应用程序,你可以保存该文件在你项目中res/raw/myDataFile,然后使用Resources.openRawResource (R.raw.myDataFile)打开它。该方法返回一个InputStream对象,你可以使用它读取文件数据。 数据库Databases Android API包含对创建和使用SQLite数据库的支持。每个数据库都是创建它的应用程序所私有的。 这个SQLiteDatabase对象代表了一个数据库并包含与之交互的方法-生成查询和管理数据。为了创建数据库,调用SQLiteDatabase.create()并同时子类化SQLiteOpenHelper。 作为支持SQLite数据库的一部分,Android暴露了数据库管理函数,这让你可以存储复杂的数据集合,这些数据被包装到有用的对象里。比如,Android为联系人信息定义了一个数据类型;它由很多字段组成,其中包括姓,名(字符串),地址信息和电话号码(也是字符串),照片(位图图像),以及更多其它个人信息。 Android装载了sqlite3数据工具,利用这些工具你可以浏览表内容,运行SQL命令,并执行SQLite数据库上的其它有用的函数。请查阅检查数据库(Examine databases (sqlite3))得知如何运行这个程序。 所有的数据库,SQLite以及其它,都被保存在设备如下目录里: /data/data/package_name/databases. 讨论创建多少表格,包含哪些字段以及它们之间如何连接超出了本文的范围,不过Android并没有引入任何在标准SQLite概念之外的限制。我们确实推荐包含一个自增长数值的关键域,作为一个唯一ID用来快速查找一个记录。这对于私有数据并不必要,但如果你实现了一个内容提供器,你必须包含这样一个唯一ID字段。请参见Content Providers文档以获取关于该字段的更多信息,以及NotePadProvider类(在NotePad例子代码里)中创建和组装一个新数据库的方法。你创建的任何数据库都将可以通过名字被应用程序中其它的类访问,但不能从应用程序外部访问。 网络Network 你也可以使用网络来存放和获取数据(当它可用时)。要进行网络操作,可使用如下程序包中的类: · java.net.* · android.net.* 内容提供器Content Providers 内容提供器用来存放和获取数据并使这些数据可以被所有的应用程序访问。它们是应用程序之间共享数据的唯一方法;不存在所有Android软件包都能访问的公共储存区域。 Android为常见数据类型(音频,视频,图像,个人联系人信息,等等)装载了很多内容提供器。你可以看到在android.provider包里列举了一些。你还能查询这些提供器包含了什么数据(尽管,对某些提供器,你必须获取合适的权限来读取数据)。 如果你想公开你自己的数据,你有两个选择:你可以创建你自己的内容提供器(一个ContentProvider子类)或者你可以给已有的提供器添加数据-如果存在一个控制同样类型数据的内容提供器且你拥有写的权限。 这篇文档是一篇关于如何使用内容提供器的简介。先是一个简短的基础知识讨论,然后探究如何查询一个内容提供器,如何修改内容提供器控制的数据,以及如何创建你自己的内容提供器。 内容提供器的基础知识Content Provider Basics 内容提供器究竟如何在表层下保存它的数据依赖于它的设计者。但是所有的内容提供器实现了一个公共的接口来查询这个提供器和返回结果-增加,替换,和删除数据也是一样。 这是一个客户端直接使用的接口,一般是通过ContentResolver对象。你可以通过getContentResolver()从一个活动或其它应用程序组件的实现里获取一个ContentResolver: ContentResolver cr = getContentResolver(); 然后你可以使用这个ContentResolver的方法来和你感兴趣的任何内容提供器交互。 当初始化一个查询时,Android系统识别查询目标的内容提供器并确保它正在运行。系统实例化所有的ContentProvider对象;你从来不需要自己做。事实上,你从来不会直接处理ContentProvider对象。通常,对于每个类型的ContentProvider只有一个简单的实例。但它能够和不同应用程序和进程中的多个ContentProvider对象通讯。进程间的交互通过ContentResolver和ContentProvider类处理。 数据模型The data model 内容提供器以数据库模型上的一个简单表格形式暴露它们的数据,这里每一个行是一个记录,每一列是特别类型和含义的数据。比如,关于个人信息以及他们的电话号码可能会以下面的方式展示: _ID NUMBER NUMBER_KEY LABEL NAME TYPE 13 (425) 555 6677 425 555 6677 Kirkland office Bully Pulpit TYPE_WORK 44 (212) 555-1234 212 555 1234 NY apartment Alan Vain TYPE_HOME 45 (212) 555-6657 212 555 6657 Downtown office Alan Vain TYPE_MOBILE 53 201.555.4433 201 555 4433 Love Nest Rex Cars TYPE_HOME 每个记录包含一个数字的_ID字段用来唯一标识这个表格里的记录。IDs可以用来匹配相关表格中的记录-比如,用来在一张表格中查找个人电话号码并在另外一张表格中查找这个人的照片。 一个查询返回一个Cursor对象可在表格和列中移动来读取每个字段的内容。它有特定的方法来读取每个数据类型。所以,为了读取一个字段,你必须了解这个字段包含了什么数据类型。(后面会更多的讨论查询结果和游标Cursor对象)。 唯一资源标识符URIs 每个内容提供器暴露一个公开的URI(以一个Uri对象包装)来唯一的标识它的数据集。一个控制多个数据集(多个表)的内容提供器为每一个数据集暴露一个单独的URI。所有提供器的URIs以字符串"content://"开始。这个content:形式表明了这个数据正被一个内容提供器控制着。 如果你正准备定义一个内容提供器,为了简化客户端代码和使将来的升级更清楚,最好也为它的URI定义一个常量。Android为这个平台所有的提供器定义了CONTENT_URI常量。比如,匹配个人电话号码的表的URI和包含个人照片的表的URI是:(均由联系人Contacts内容提供器控制) android.provider.Contacts.Phones.CONTENT_URI android.provider.Contacts.Photos.CONTENT_URI 类似的,最近电话呼叫的表和日程表条目的URI如下:Similarly, the URIs for the table of recent phone calls and the table of calendar entries are: android.provider.CallLog.Calls.CONTENT_URI android.provider.Calendar.CONTENT_URI 这个URI常量被使用在和这个内容提供器所有的交互中。每个ContentResolver方法采用这个URI作为它的第一个参数。正是它标识了ContentResolver应该和哪个内容提供器对话以及这个内容提供器的哪张表格是其目标。 查询一个内容提供器Querying a Content Provider 你需要三方面的信息来查询一个内容提供器: · 用来标识内容提供器的URI · 你想获取的数据字段的名字 · 这些字段的数据类型 如果你想查询某一条记录,你同样需要那条记录的ID。 生成查询Making the query 你可以使用ContentResolver.query()方法或者Activity.managedQuery()方法来查询一个内容提供器。两种方法使用相同的参数序列,而且都返回一个Cursor对象。不过,managedQuery()使得活动需要管理这个游标的生命周期。一个被管理的游标处理所有的细节,比如当活动暂停时卸载自身,而活动重新启动时重新查询它自己。你可以让一个活动开始管理一个尚未被管理的游标对象,通过如下调用:Activity.startManagingCursor()。 无论query()还是managedQuery(),它们的第一个参数都是内容提供器的URI-CONTENT_URI常量用来标识某个特定的ContentProvider和数据集(参见前面的URIs)。 为了限制只对一个记录进行查询,你可以在URI后面扩展这个记录的_ID值-也就是,在URI路径部分的最后加上匹配这个ID的字符串。比如,如果ID是23,那么URI会是: content://. . . ./23 有一些辅助方法,特别是ContentUris.withAppendedId()和Uri.withAppendedPath(),使得为URI扩展一个ID变得简单。所以,比如,如果你想在联系人数据库中查找记录23,你可能需要构造如下的查询语句: import android.provider.Contacts.People; import android.content.ContentUris; import android.net.Uri; import android.database.Cursor; // Use the ContentUris method to produce the base URI for the contact with _ID == 23. Uri myPerson = ContentUris.withAppendedId(People.CONTENT_URI, 23); // Alternatively, use the Uri method to produce the base URI. // It takes a string rather than an integer. Uri myPerson = Uri.withAppendedPath(People.CONTENT_URI, "23"); // Then query for this specific record: Cursor cur = managedQuery(myPerson, null, null, null, null); query()和managedQuery()方法的其它参数限定了更多的查询细节。如下: · 应该返回的数据列的名字。null值返回所有列。否则只有列出名字的列被返回。所有这个平台的内容提供器为它们的列定义了常量。比如,android.provider.Contacts.Phones类对前面说明过的通讯录中各个列的名字定义了常量ID, NUMBER, NUMBER_KEY, NAME,等等。 · 指明返回行的过滤器,以一个SQL WHERE语句格式化。null值返回所有行。(除非这个URI限定只查询一个单独的记录)。 · 选择参数 · 返回行的排列顺序,以一个SQL ORDER BY语句格式化(不包含ORDER BY本身)。null值表示以该表格的默认顺序返回,有可能是无序的。 让我们看一个查询的例子吧,这个查询获取一个联系人名字和首选电话号码列表: import android.provider.Contacts.People; import android.database.Cursor; // Form an array specifying which columns to return. String[] projection = new String[] { People._ID, People._COUNT, People.NAME, People.NUMBER }; // Get the base URI for the People table in the Contacts content provider. Uri contacts = People.CONTENT_URI; // Make the query. Cursor managedCursor = managedQuery(contacts, projection, // Which columns to return null, // Which rows to return (all rows) null, // Selection arguments (none) // Put the results in ascending order by name People.NAME + " ASC"); 这个查询从联系人内容提供器中获取了数据。它得到名字,首选电话号码,以及每个联系人的唯一记录ID。同时它在每个记录的_COUNT字段告知返回的记录数目。 列名的常量被定义在不同的接口中-_ID和_COUNT定义在BaseColumns里, NAME在PeopleColumns里,NUMBER在PhoneColumns里。Contacts.People类已经实现了这些接口,这就是为什么上面的代码实例只需要使用类名就可以引用它们的原因。 查询的返回结果What a query returns 一个查询返回零个或更多数据库记录的集合。列名,默认顺序,以及它们的数据类型是特定于每个内容提供器的。但所有提供器都有一个_ID列,包含了每个记录的唯一ID。另外所有的提供器都可以通过返回_COUNT列告知记录数目。它的数值对于所有的行而言都是一样的。 下面是前述查询的返回结果的一个例子: _ID _COUNT NAME NUMBER 44 3 Alan Vain 212 555 1234 13 3 Bully Pulpit 425 555 6677 53 3 Rex Cars 201 555 4433 获取到的数据通过一个游标Cursor对象暴露出来,通过游标你可以在结果集中前后浏览。你只能用这个对象来读取数据。如果想增加,修改和删除数据,你必须使用一个ContentResolver对象。 读取查询所获数据Reading retrieved data 查询返回的游标对象可以用来访问结果记录集。如果你通过指定的一个ID来查询,这个集合将只有一个值。否则,它可以包含多个数值。(如果没有匹配结果,那还可能是空的。)你可以从表格中的特定字段读取数据,但你必须知道这个字段的数据类型,因为这个游标对象对于每种数据类型都有一个单独的读取方法-比如getString(), getInt(), 和getFloat()。(不过,对于大多数类型,如果你调用读取字符串的方法,游标对象将返回给你这个数据的字符串表示。)游标可以让你按列索引请求列名,或者按列名请求列索引。 下面的代码片断演示了如何从前述查询结果中读取名字和电话号码: import android.provider.Contacts.People; private void getColumnData(Cursor cur){ if (cur.moveToFirst()) { String name; String phoneNumber; int nameColumn = cur.getColumnIndex(People.NAME); int phoneColumn = cur.getColumnIndex(People.NUMBER); String imagePath; do { // Get the field values name = cur.getString(nameColumn); phoneNumber = cur.getString(phoneColumn); // Do something with the values. ... } while (cur.moveToNext()); } } 如果一个查询可能返回二进制数据,比如一个图像或声音,这个数据可能直接被输入到表格或表格条目中也可能是一个content: URI的字符串可用来获取这个数据,一般而言,较小的数据(例如,20到50K或更小)最可能被直接存放到表格中,可以通过调用Cursor.getBlob()来获取。它返回一个字节数组。 如果这个表格条目是一个content: URI,你不该试图直接打开和读取该文件(会因为权限问题而失败)。相反,你应该调用ContentResolver.openInputStream()来得到一个InputStream对象,你可以使用它来读取数据。 修改数据Modifying Data 保存在内容提供器中的数据可以通过下面的方法修改: · 增加新的记录 · 为已有的记录添加新的数据 · 批量更新已有记录 · 删除记录 所有的数据修改操作都通过使用ContentResolver方法来完成。一些内容提供器对写数据需要一个比读数据更强的权限约束。如果你没有一个内容提供器的写权限,这个ContentResolver方法会失败。 增加记录Adding records 想要给一个内容提供器增加一个新的记录,第一步是在ContentValues对象里构建一个键-值对映射,这里每个键和内容提供器的一个列名匹配而值是新记录中那个列期望的值。然后调用ContentResolver.insert()并传递给它提供器的URI和这个ContentValues映射图。这个方法返回新记录的URI全名-也就是,内容提供器的URI加上该新记录的扩展ID。你可以使用这个URI来查询并得到这个新记录上的一个游标,然后进一步修改这个记录。下面是一个例子: import android.provider.Contacts.People; import android.content.ContentResolver; import android.content.ContentValues; ContentValues values = new ContentValues(); // Add Abraham Lincoln to contacts and make him a favorite. values.put(People.NAME, "Abraham Lincoln"); // 1 = the new contact is added to favorites // 0 = the new contact is not added to favorites values.put(People.STARRED, 1); Uri uri = getContentResolver().insert(People.CONTENT_URI, values); |
|
cuihai |
2011-03-03 12:14 |
增加新值Adding new values 一旦记录已经存在,你就可以添加新的信息或修改已有信息。比如,上例中的下一步就是添加联系人信息-如一个电话号码或一个即时通讯IM或电子邮箱地址-到新的条目中。 在联系人数据库中增加一条记录的最佳途径是在该记录URI后扩展表名,然后使用这个修正的URI来添加新的数据值。为此,每个联系人表暴露一个CONTENT_DIRECTORY常量的表名。下面的代码继续之前的例子,为上面刚刚创建的记录添加一个电话号码和电子邮件地址: Uri phoneUri = null; Uri emailUri = null; // Add a phone number for Abraham Lincoln. Begin with the URI for // the new record just returned by insert(); it ends with the _ID // of the new record, so we don't have to add the ID ourselves. // Then append the designation for the phone table to this URI, // and use the resulting URI to insert the phone number. phoneUri = Uri.withAppendedPath(uri, People.Phones.CONTENT_DIRECTORY); values.clear(); values.put(People.Phones.TYPE, People.Phones.TYPE_MOBILE); values.put(People.Phones.NUMBER, "1233214567"); getContentResolver().insert(phoneUri, values); // Now add an email address in the same way. emailUri = Uri.withAppendedPath(uri, People.ContactMethods.CONTENT_DIRECTORY); values.clear(); // ContactMethods.KIND is used to distinguish different kinds of // contact methods, such as email, IM, etc. values.put(People.ContactMethods.KIND, Contacts.KIND_EMAIL); values.put(People.ContactMethods.DATA, "[email protected]"); values.put(People.ContactMethods.TYPE, People.ContactMethods.TYPE_HOME); getContentResolver().insert(emailUri, values); 你可以通过调用接收字节流的ContentValues.put()版本来把少量的二进制数据放到一张表格里去。这对于像小图标或短小的音频片断这样的数据是可行的。但是,如果你有大量二进制数据需要添加,比如一张相片或一首完整的歌曲,则需要把该数据的content:URI放到表里然后以该文件的URI调用ContentResolver.openOutputStream()方法。(这导致内容提供器把数据保存在一个文件里并且记录文件路径在这个记录的一个隐藏字段中。) 考虑到这一点,MediaStore内容提供器,这个用来分发图像,音频和视频数据的主内容提供器,利用了一个特殊的约定:用来获取关于这个二进制数据的元信息的query()或managedQuery()方法使用的URI,同样可以被openInputStream()方法用来数据本身。类似的,用来把元信息放进一个MediaStore记录里的insert()方法使用的URI,同样可以被openOutputStream()方法用来在那里存放二进制数据。下面的代码片断说明了这个约定: import android.provider.MediaStore.Images.Media; import android.content.ContentValues; import java.io.OutputStream; // Save the name and description of an image in a ContentValues map. ContentValues values = new ContentValues(3); values.put(Media.DISPLAY_NAME, "road_trip_1"); values.put(Media.DESCRIPTION, "Day 1, trip to Los Angeles"); values.put(Media.MIME_TYPE, "image/jpeg"); // Add a new record without the bitmap, but with the values just set. // insert() returns the URI of the new record. Uri uri = getContentResolver().insert(Media.EXTERNAL_CONTENT_URI, values); // Now get a handle to the file for that record, and save the data into it. // Here, sourceBitmap is a Bitmap object representing the file to save to the database. try { OutputStream outStream = getContentResolver().openOutputStream(uri); sourceBitmap.compress(Bitmap.CompressFormat.JPEG, 50, outStream); outStream.close(); } catch (Exception e) { Log.e(TAG, "exception while writing image", e); } 批量更新记录Batch updating records 要批量更新一组记录(例如,把所有字段中的"NY"改为"New York"),可以传以需要改变的列和值参数来调用ContentResolver.update()方法。 删除一个记录Deleting a record 要删除单个记录,可以传以一个特定行的URI参数来调用ContentResolver.delete()方法。 要删除多行记录,可以传以需要被删除的记录类型的URI参数来调用ContentResolver.delete()方法(例如,android.provider.Contacts.People.CONTENT_URI)以及一个SQL WHERE语句来定义哪些行要被删除。(小心:如果你想删除一个通用类型,你得确保包含一个合法的WHERE语句,否则你可能删除比设想的多得多的记录!) 创建一个内容提供器Creating a Content Provider 要创建一个内容提供器,你必须: · 建立一个保存数据的系统。大多数内容提供器使用Android的文件储存方法或SQLite数据库来存放它们的数据,但是你可以用任何你想要的方式来存放数据。Android提供SQLiteOpenHelper类来帮助你创建一个数据库以及SQLiteDatabase类来管理它。 · 扩展ContentProvider类来提供数据访问接口。 · 在清单manifest文件中为你的应用程序声明这个内容提供器(AndroidManifest.xml)。 下面的章节对后来两项任务有一些标注。 扩展ContentProvider类Extending the ContentProvider class 你可以定义一个ContentProvider子类来暴露你的数据给其它使用符合ContentResolver和游标Cursor对象约定的应用程序。理论上,这意味需要实现6个ContentProvider类的抽象方法: query() insert() update() delete() getType() onCreate() query()方法必须返回一个游标Cursor对象可以用来遍历请求数据,游标本身是一个接口,但Android提供了一些现成的Cursor对象给你使用。例如,SQLiteCursor可以用来遍历SQLite数据库。你可以通过调用任意的SQLiteDatabase类的query()方法得到它。还有一些其它的游标实现-比如MatrixCursor用来访问没有存放在数据库中的数据。- 因为这些内容提供器的方法可以从不同的进程和线程的各个ContentResolver对象中调用,所以它们必须以线程安全的方式来实现。 周到起见,当数据被修改时,你可能还需要调用ContentResolver.notifyChange()方法来通知侦听者。 除了定义子类以外,你应该还需要采取其它一些步骤来简化客户端的工作和让这个类更容易被访问: · 定义一个public static final Uri命名为CONTENT_URI。这是你的内容提供器处理的整个content: URI的字符串。你必须为它定义一个唯一的字符串。最佳方案是使用这个内容提供器的全称(fully qualified)类名(小写)。因此,例如,一个TransportationProvider类可以定义如下: public static final Uri CONTENT_URI = Uri.parse("content://com.example.codelab.transporationprovider"); 如果这个内容提供器有子表,那么为每个子表也都定义CONTENT_URI常量。这些URIs应该全部拥有相同的权限(既然这用来识别内容提供器),只能通过它们的路径加以区分。例如: content://com.example.codelab.transporationprovider/train content://com.example.codelab.transporationprovider/air/domestic content://com.example.codelab.transporationprovider/air/international 请查阅本文最后部分的Content URI Summary以对content: URIs有一个总体的了解。 · 定义内容提供器返回给客户端的列名。如果你正在使用一个底层数据库,这些列名通常和SQL数据库列名一致。同样还需要定义公共的静态字符串常量用来指定查询语句以及其它指令中的列。 确保包含一个名为"_id"(常量_ID)的整数列来返回记录的IDs。你应该有这个字段而不管有没有其它字段(比如URL),这个字段在所有的记录中是唯一的。如果你在使用SQLite数据库,这个_ID 字段应该是下面的类型: INTEGER PRIMARY KEY AUTOINCREMENT 其中AUTOINCREMENT描述符是可选的。但是没有它,SQLite的ID数值字段会在列中已存在的最大数值的基础上增加到下一个数字。如果你删除了最后的行,那么下一个新加的行会和这个删除的行有相同的ID。AUTOINCREMENT可以避免这种情况,它让SQLite总是增加到下一个最大的值而不管有没有删除。 · 在文档中谨慎的描述每个列的数据类型。客户端需要这些信息来读取数据。 · 如果你正在处理一个新的数据类型,你必须定义一个新的MIME类型在你的ContentProvider.getType()实现里返回。这个类型部分依赖于提交给getType()的content: URI参数是否对这个请求限制了特定的记录。有一个MIME类型是给单个记录用的,另外一个给多记录用。使用Uri方法来帮助判断哪个是正在被请求的。下面是每个类型的一般格式: ² 对于单个记录: vnd.android.cursor.item/vnd.yourcompanyname.contenttype 比如,一个火车记录122的请求,URI如下 content://com.example.transportationprovider/trains/122 可能会返回这个MIME类型: vnd.android.cursor.item/vnd.example.rail ² 对于多个记录: vnd.android.cursor.dir/vnd.yourcompanyname.contenttype 比如, 一个所有火车记录的请求,URI如下 content://com.example.transportationprovider/trains 可能会返回这个MIME类型: vnd.android.cursor.dir/vnd.example.rail · 如果你想暴露过于庞大而无法放在表格里的字节数据-比如一个大的位图文件-这个给客户端暴露数据的字段事实上应该包含一个content: URI字符串。这个字段给了客户端数据访问接口。这个记录应该有另外的一个字段,名为"_data",列出了这个文件在设备上的准确路径。这个字段不能被客户端读取,而要通过ContentResolver。客户端将在这个包含URI的用户侧字段上调用ContentResolver.openInputStream() 方法。ContentResolver会请求那个记录的"_data"字段,而且因为它有比客户端更高的许可权,它应该能够直接访问那个文件并返回给客户端一个包装的文件读取接口。 自定义内容提供器的实现的一个例子,参见SDK附带的Notepad例程中的NodePadProvider 类。 声明内容提供器Declaring the content provider 为了让Android系统知道你开发的内容提供器,可以用在应用程序的AndroidManifest.xml文件中以<provider>元素声明它。未经声明的内容提供器对Android系统不可见。 名字属性是ContentProvider子类的全称名(fully qualified name)。权限属性是标识提供器的content: URI的权限认证部分。例如如果ContentProvider子类是AutoInfoProvider,那么<provider>元素可能如下: <provider name="com.example.autos.AutoInfoProvider" authorities="com.example.autos.autoinfoprovider" . . . /> </provider> 请注意到这个权限属性忽略了content: URI的路径部分。例如,如果AutoInfoProvider为各种不同的汽车或制造商控制着各个子表,Note that the authorities attribute omits the path part of a content: URI. For example, if AutoInfoProvider controlled subtables for different types of autos or different manufacturers, content://com.example.autos.autoinfoprovider/honda content://com.example.autos.autoinfoprovider/gm/compact content://com.example.autos.autoinfoprovider/gm/suv 这些路径将不会在manifest里声明。权限是用来识别提供器的,而不是路径;你的提供器能以任何你选择的方式来解释URI中的路径部分。 其它<provider>属性可以设置数据读写许可,提供可以显示给用户的图标和文本,启用或禁用这个提供器,等等。如果数据不需要在多个内容提供器的运行版本中同步则可以把multiprocess属性设置成"true"。这使得在每个客户进程中都有一个提供器实例被创建,而无需执行IPC调用。 Content URI 总结 这里回顾一下content URI的重要内容: A. 标准前缀表明这个数据被一个内容提供器所控制。它不会被修改。 B. URI的权限部分;它标识这个内容提供器。对于第三方应用程序,这应该是一个全称类名(小写)以确保唯一性。权限在<provider>元素的权限属性中进行声明: <provider name=".TransportationProvider" authorities="com.example.transportationprovider" . . . > C. 用来判断请求数据类型的路径。这可以是0或多个段长。如果内容提供器只暴露了一种数据类型(比如,只有火车),这个分段可以没有。如果提供器暴露若干类型,包括子类型,那它可以是多个分段长-例如,提供"land/bus", "land/train", "sea/ship", 和"sea/submarine"这4个可能的值。 D. 被请求的特定记录的ID,如果有的话。这是被请求记录的_ID数值。如果这个请求不局限于单个记录,这个分段和尾部的斜线会被忽略: content://com.example.transportationprovider/trains 清单文件The AndroidManifest.xml File 每个应用程序都有一个AndroidManifest.xml文件(一定是这个名字)在它的根目录里。这个清单文件给Android系统提供了关于这个应用程序的基本信息,系统在能运行任何程序代码之前必须知道这些信息。AndroidManifest.xml主要包含以下功能: * 命名应用程序的Java包,这个包名用来唯一标识应用程序; * 描述应用程序的组件-活动,服务,广播接收者,以及组成应用程序的内容提供器;对实现每个组件和公布其能力(比如,能处理哪些意图消息)的类进行命名。这些声明使得Android系统了解这些组件以及在什么条件下可以被启动; * 决定应用程序组件运行在哪个进程里面; * 声明应用程序所必须具备的权限,用以访问受保护的部分API,以及和其它应用程序交互; * 声明应用程序其他的必备权限,用以组件之间的交互; * 列举测试设备Instrumentation类,用来提供应用程序运行时所需的环境配置及其他信息,这些声明只在程序开发和测试阶段存在,发布前将被删除; * 声明应用程序所要求的Android API的最低版本级别; * 列举application所需要链接的库; 清单文件结构Structure of the Manifest File 下面的图表显示了清单文件的基本结构以及它能包含的所有元素。每个元素,和它所有的属性,在一个单独的文件中完整描述。要查看任何元素的细节信息,可在图表下方的以字符序排列的元素列表中点击其元素名称。 <?xml version="1.0" encoding="utf-8"?> <manifest> <uses-permission /> <permission /> <permission-tree /> <permission-group /> <instrumentation /> <uses-sdk /> <application> <activity> <intent-filter> <action /> <category /> <data /> </intent-filter> <meta-data /> </activity> <activity-alias> <intent-filter> . . . </intent-filter> <meta-data /> </activity-alias> <service> <intent-filter> . . . </intent-filter> <meta-data/> </service> <receiver> <intent-filter> . . . </intent-filter> <meta-data /> </receiver> <provider> <grant-uri-permission /> <meta-data /> </provider> <uses-library /> <uses-configuration /> </application> </manifest> 所有清单文件中可能出现的元素按字符序排列如下。只有这些元素是合法的,你不能添加自己的元素或属性: <action> <activity> <activity-alias> <application> <category> <data> <grant-uri-permission> <instrumentation> <intent-filter> <manifest> <meta-data> <permission> <permission-group> <permission-tree> <provider> <receiver> <service> <uses-configuration> <uses-library> <uses-permission> <uses-sdk> 文件约定File Conventions 下面是一些清单文件中适用于所有元素和属性的约定和规则: 元素Elements: 在所有的元素中只有<manifest>和<application>是必需的,且只能出现一次。很多其他元素可以出现多次甚或一次都没有-尽管如果清单文件想要完成一些有意义的工作,必须设置至少其中的一些。如果一个元素包含点什么,那就是包含其他元素。所有的值必须通过属性来设置,而不是元素中的字符数据。同一级别的元素一般是没有顺序的。比如,<activity>, <provider>, 和<service>元素可以以任意顺序混合使用。(<activity-alias>元素是个例外:它必须跟在该别名所指的<activity>后面。) 属性Attributes: 正规意义上,所有的属性都是可选的,但实际上有些属性是必须为一个元素指定来完成其目标。把这篇文档当作一个指南。对于那些真正可选的属性,即使不存在一个规格,也会有默认的数值或状态。 除了根元素<manifest>的一些属性,所有其他元素属性的名字都是以android:作为前缀的-比如,android:alwaysRetainTaskState。因为这个前缀是通用的,这篇文档提及属性名称时一般会忽略它。 声明类名Declaring class names: 很多对应于Java对象的元素,包括应用程序自己(<application>元素)以及它的基础组件-活动(<activity>),服务(<service>),广播接收器(<receiver>),以及内容提供器(<provider>)。 如果你定义一个子类,就像你将经常为组件类(Activity, Service, BroadcastReceiver, 和ContentProvider)所做的那样,这个子类通过一个名字属性来声明。这个名字必须包含完整的包名称。比如,一个服务Service子类可能会声明如下: <manifest . . . > <application . . . > <service android:name="com.example.project.SecretService" . . . > . . . </service> . . . </application> </manifest> 不过,作为类名的简写,如果这个字符串的第一个字符是一个点号“.”,那么这个字符串将被扩展到应用程序包名的后面(正如<manifest>元素的package属性所指明的那样)。下面这个赋值和上面效果一样: <manifest package="com.example.project" . . . > <application . . . > <service android:name=".SecretService" . . . > . . . </service> . . . </application> </manifest> 当启动一个组件时,Android创建了一个命名子类的实例。如果没有指定一个子类,它创建基类的一个实例。 多数值项Multiple values: 如果某个元素有超过一个数值,这个元素几乎总是需要被重复声明,而不能将多个数值项列举在一个属性中。比如,一个意图过滤器可以列举多个动作: <intent-filter . . . > <action android:name="android.intent.action.EDIT" /> <action android:name="android.intent.action.INSERT" /> <action android:name="android.intent.action.DELETE" /> . . . </intent-filter> 资源项Resource values: 一些属性有能显示给用户的数值-比如,活动(activity)的一个标签和图标。这些属性的值应该被本地化,从一个资源或主题中设置。当需要引用某个资源时,采用如下的表述格式: @[package:]type:name 这里package名称可以被忽略,要是资源和应用程序在同一个包里的话;type是资源的类型-如"string"或"drawable"-而且name是用来标识特定资源的名字。例如 <activity android:icon="@drawable/smallPic" . . . > 从主题获取的数据以类似的方式表述,不过以'?'而不是'@'开头。 ?[package:]type:name 字符串值String values: 如果属性值是一个字符串,则必须使用双反斜杠('\\')来表示escape('\')字符;第一个反斜杠起转义字符的作用)。比如,'\\n'表示换行或'\\uxxxx'表示一个Unicode字符。 文件特性File Features 下面的章节描述了一些Android特性如何被映射到清单(manifest)文件中。 意图过滤器Intent Filters 应用程序的核心组件(活动,服务和广播接收器)通过意图被激活。意图是描述期望动作的信息包(一个Intent对象)-包括要操作的数据,执行该动作的组件类别,以及其他有关指令。Android寻找一个合适的组件来响应这个意图,如果需要会启动这个组件一个新的实例,并传递给这个意图对象。 组件通过意图过滤器(intent filters)通告它们所具备的能力-能响应的意图类型。由于Android系统在启动一个组件前必须知道该组件能够处理哪些意图,那么意图过滤器需要在manifest中以<intent-filter>元素指定。一个组件可以拥有多个过滤器,每一个描述不同的能力。 一个显式命名目标组件的意图将会激活那个组件;过滤器不起作用。但是一个没有指定目标的意图只在它能够通过组件过滤器任一过滤器时才能激活该组件。 请查看关于意图和意图过滤器的文档以获取更多信息:Intents and Intent Filters. 图标和标签Icons and Labels 许多元素有图标(icon)和标签(label)属性。其中一些还有一个描述(description)属性,可以用更长的解释性文字呈现给用户。比如,<permission>元素有所有这三个属性,因此当用户被询问是否授予一个应用程序请求的权限许可时,一个代表权限的图标,权限的名称和必定伴有的权限描述会全部被显示给用户。 所有的情况中,设置在一个包含元素里的图标和标签会成为该容器所有子元素的缺省设置。这样,在<application>元素中设置的图标和标签就是该应用程序每个组件的缺省图标和标签。类似的,为一个组件设置的图标和标签-比如,一个<activity>元素-是这个组件<intent-filter>元素的缺省值。如果一个<application>元素设置了一个图标,但活动及其意图过滤器没有,那么程序标签被当作活动和意图过滤器的标签。 当呈现给用户的组件实现一个意图过滤器公告的函数时,为这个过滤器设置的图标和标签将被用来代表这个组件。比如,一个设置了"android.intent.action.MAIN"和"android.intent.category.LAUNCHER"的过滤器公告了一个活动来初始化应用程序-也就是,会被显示在应用程序启动器中。因此设置在过滤器中的图标和标签也就是显示在启动器里的那些图标和标签。 许可Permissions 一个许可(permission)是代码对设备上数据的访问限制。这个限制被引入来保护可能会被误用而曲解或破坏用户体验的关键数据和代码。 每个许可被一个唯一的标签所标识。这个标签常常指出了受限的动作。例如,下面是一些Android定义的许可: android.permission.CALL_EMERGENCY_NUMBERS android.permission.READ_OWNER_DATA android.permission.SET_WALLPAPER android.permission.DEVICE_POWER 一个功能(feature)最多只能被一个权限许可保护。 如果一个应用程序需要访问一个需要特定权限的功能,它必须在manifest文件中使用<uses-permission>元素来声明这一点。这样,当应用程序安装到设备上时,安装器可以通过检查签署应用程序认证的机构来决定是否授予请求的权限,在某些情况下,会询问用户。如果权限已被授予,那应用程序就能够访问受保护的功能特性。如果没有,访问将失败,但不会给用户任何通知。 应用程序还可以通过权限许可来保护它自己的组件(活动,服务,广播接收器,和内容提供器)。它可以利用Android已经定义(列在android.Manifest.permission里)或其他应用程序已声明的权限许可。或者定义自己的许可。一个新的许可通过<permission>元素声明。比如,一个活动可以用下面的方式保护: <manifest . . . > <permission android:name="com.example.project.DEBIT_ACCT" . . . /> . . . <application . . .> <activity android:name="com.example.project.FreneticActivity" . . . > android:permission="com.example.project.DEBIT_ACCT" . . . > . . . </activity> </application> . . . <uses-permission android:name="com.example.project.DEBIT_ACCT" /> . . . </manifest> 注意,在这个例子里,这个DEBIT_ACCT许可并非仅仅在<permission>元素中声明,它同样声明在<uses-permission>元素里。为了应用程序的其他组件可以启动这个受保护的活动,必须请求它的使用(use),即使这个保护是应用程序自己引入的。 如果,就在这个例子里,这个permission属性被设置为在其他地方声明的权限许可(例如android.permission.CALL_EMERGENCY_NUMBERS,它将不需要再次声明它,但是,它仍然需要通过<uses-permission>来请求它的使用。 这个<permission-tree>元素为一组想在代码中定义的权限许可声明了一个命名空间。而<permission-group>元素为一系列许可定义了一个标签(用<permission>元素定义在manifest中的以及其他地方声明的)。它仅仅影响这些权限许可在显示给用户时如何分组。<permission-group>元素并不指明哪个权限属于这个分组;它只是给这个组命名。一个权限许可通过给<permission>元素的permissionGroup属性赋予这个组名来放置到这个权限组中。 库Libraries 每个应用程序都链接到缺省的Android库,这个库包含了基础应用程序开发包(实现了基础类如活动,服务,意图,视图,按钮,应用程序,内容提供器,等等) 然而,一些包处于它们自己的库中。如果你的应用程序使用了其他开发包中的代码,它必须显式的请求链接到它们。这个manifest必须包含一个单独的<uses-library>元素来命名每一个库。 什么是 Android? Android 是一个专门针对移动设备的软件集,它包括一个操作系统,中间件和一些重要的应用程序。Beta 版的 Android SDK 提供了在Android 平台上使用JaVa 语言进行Android 应用开发必须的工具和API 接口。 特性 · 应用程序框架 支持组件的重用与替换 · Dalvik 虚拟机专为移动设备优化 · 集成的浏览器 基于开源的WebKit 引擎 · 优化的图形库 包括定制的2D 图形库,3D 图形库基于OpenGL ES 1.0 (硬件加速可选) · SQLite 用作结构化的数据存储 · 多媒体支持 包括常见的音频、视频和静态图像格式(如 MPEG4, H.264, MP3,AAC, AMR, JPG, PNG, GIF) · GSM 电话技术(依赖于硬件) · 蓝牙Bluetooth, EDGE, 3G, 和 WiFi (依赖于硬件) · 照相机,GPS,指南针,和加速度计(accelerometer) (依赖于硬件) · 丰富的开发环境 包括设备模拟器,调试工具,内存及性能分析图表,和Eclipse 集成开发环境插件 应用程序 Android 会同一系列核心应用程序包一起发布,该应用程序包包括email 客户端,SMS短消息程序,日历,地图,浏览器,联系人管理程序等。所有的应用程序都是使用JAVA语言编写的。 应用程序框架 开发人员也可以完全访问核心应用程序所使用的API 框架。该应用程序的架构设计简化了组件的重用;任何一个应用程序都可以发布它的功能块并且任何其它的应用程序都可以使用其所发布的功能块(不过得遵循框架的安全性限制)。同样,该应用程序重用机制也使用户可以方便的替换程序组件。 隐藏在每个应用后面的是一系列的服务和系统, 其中包括; · 丰富而又可扩展的视图(Views),可以用来构建应用程序, 它包括列表(lists), 网格(grids),文本框(text boxes),按钮(buttons), 甚至可嵌入的web 浏览器。 · 内容提供器(Content Providers)使得应用程序可以访问另一个应用程序的数据(如联系人数据库), 或者共享它们自己的数据 · 资源管理器(Resource Manager)提供 非代码资源的访问,如本地字符串,图形,和布局文件( layout files )。 · 通知管理器 (Notification Manager) 使得应用程序可以在状态栏中显示自定义的提示信息。 · 活动管理器( Activity Manager) 用来管理应用程序生命周期并提供常用的导 航回退功能。 程序库 Android 包含一些C/C++库,这些库能被Android 系统中不同的组件使用。它们通过Android 应用程序框架为开发者提供服务。以下是一些核心库: · 系统 C 库 - 一个从 BSD 继承来的标准 C 系统函数库( libc ), 它是专门为基于 embedded linux 的设备定制的。 · 媒体库 - 基于 PacketVideo OpenCORE;该库支持多种常用的音频、视频格式回放和录制,同时支持静态图像文件。编码格式包括MPEG4, H.264, MP3, AAC,AMR, JPG, PNG 。 · Surface Manager - 对显示子系统的管理,并且为多个应用程序提 供了2D 和 3D 图层的无缝融合。 · LibWebCore - 一个最新的web 浏览器引擎用,支持Android 浏览器和一个可嵌入的web 视图。 · SGL - 底层的2D 图形引擎 · 3D libraries - 基于OpenGL ES 1.0 APIs 实现;该库可以使用硬件 3D 加速(如果可用)或者使用高度优化的3D 软加速。 · FreeType -位图(bitmap)和矢量(vector)字体显示。 · SQLite - 一个对于所有应用程序可用,功能强劲的轻型关系型数据库引擎。 Android 运行库 Android 包括了一个核心库,该核心库提供了JAVA 编程语言核心库的大多数功能。每一个Android 应用程序都在它自己的进程中运行,都拥有一个独立的Dalvik 虚拟 机实例。Dalvik 被设计成一个设备可以同时高效地运行多个虚拟系统。 Dalvik 虚拟机执行(.dex)的Dalvik 可执行文件,该格式文件针对小内存使用做了优化。同时虚拟机是基于寄存器的,所有的类都经由JAVA 编译器编译,然后通过SDK 中 的 "dx" 工具转化成.dex 格式由虚拟机执行。 Dalvik 虚拟机依赖于linux 内核的一些功能,比如线程机制和底层内存管理机制。 Linux 内核 Android 的核心系统服务依赖于 Linux 2.6 内核,如安全性,内存管理,进程管理,网络协议栈和驱动模型。 Linux 内核也同时作为硬件和软件栈之间的抽象层。 建立应用程序签名 所有应用程序在安装它们之前都必须被签名。ADT 插件和ant 为基础的开发工具都支持这一要求,它们通过带一个调试KEY 瘿apk 文件来签发编译。为了做到这一点,编译工具使用包括在JDK 的Keytool 去创造一个keystore 和带着一个已知的别名和密码一个key 带着一个已知的别名和密码。如需详细信息,请查阅 签名你的应用程序. 为了支持签签名,你应该首先确认Keytool 对于SDK 的编译工具是有效的。在大多数情况下,你可以告诉的SDK 编译工具如何找到Keytool,通过设置你的J AVA_HOME环境变量设置和一个合适的JDK。另外,您也可以添加keytool 的JDK 版本到您的PATH变量如果你正在开发Linux 的一个版本,那原本使用的是GNU 的JAVA 编译嗿,请确保该系统正在使用的Keytool 的JDK 版本,而不是使用gcj,如果keyt ool 已经在您的路径,它可能是指向在一个符号链接是/usr/bin/keytool 。在这种情况下,检查符号链接的目标,以确保它指向正确的Keytool.如果您使用的ant 编译你的.apk 文件ض而不是ADT,你必须重新产生你的build.xml文件。为了做到这一点,请执行下列步骤: 1. 在您的android 应用程序工程目录中,找到并删除目前的build.xml 文件 2. 2. 运行activitycreator ,直接输出到包含您的应用程序项目的文件夹 3. - exec activitycreator --out your.activity.YourActivity 运行这种方式ضactivityCreator不会擦出或创建新的Java 文件(或manifest 文件ض,对于那些已经存在的activity 和package。重要的是,package 和activity 是真实存在的。该工具创建一个新的build.xml 文件,以及一个新的目录称libs"中,这个目录将放置第三方jar 文件,这是你就能够使用ant 脚本自动处理。 移植您的应用程序 更新过您的SDK 以后 ,您可能会遇到破损的代码,由于框架和API 的变化。您需要更新您的代码以匹配变化的Andriod 的API。 一种方法是用Eclipse 打开您的项目和查看你的应用程序中ADT 的标记错误。从这里,你可以查找对应的变势 变化预览 and API 变化报告. 如果您更新您的代码有其他麻烦,请访问 android 小组讨论 寻求帮助或者求助于其他android 开发人员.如果已经修改了一个ApiDemos 应用程序,并希望移植到新的SDK 的,请注意您将需要卸载模拟器中预装的ApiDemos 版本。了解更多信息,或(运行或安装A piDemos)遇到一个重新安装"的错误,见疑难解答论頿 因为签名错误,我不能在我的IDE 中安装ApiDemos 应用程序 来获得解决这个问题的信息。 为程序附加调试器 这一节我们介绍如何在屏幕上显示调试信息(例如CPU 使用率),以及如何将IDE 和模拟器上运行的程序关联起来。 使用eclipse 插件可以自动的生成调试器。但你也可以通过配置IDES 来监听调试端口得到调试信息。 1. 启动Dalvik Debug Monitor Server (DDMS) 工具 ,它在IDE 和模拟器之间扮演着端口转换服务的角色。? 2. 设置模拟器调试配置选项。例如,等到调试信息被加载后才启动应用程序。注意,很多调试选项无需DDMS 也可以使用,例如模拟器上显示CPU 的使用效率,或者屏幕的刷新频率。 3. 配置IDE,使得调试时IDE 与8700 端口关联 .how to set up Eclipse to debug your project. 包含以下信息。 配置IDE 附加调试端口 DDMS 将为每一个虚拟机分配一个特殊的调试端口,这个端口在模拟器上可以找到。你必须将你的IDE 与此端口(虚拟机上信息栏中有列出这些端口)关联或者是默认的端口8700。这样可以使IDE 连接到模拟器上程序列表中的任一个程序。你的IDE 需要能够关联模拟器上正在运行的程序,显示它的线程,并允许你挂起它,检查它的状态,设置断点。如果你在开发设置面板选择了“等待调试”,应用程序将等到Eclipse 连接后才运行,所以你需要在连接之前设置断点。 修改正在调试的程序,或者在当前程序运行时选择“等待调试”将引起系统杀死这个应用程序。如果你的程序处于一种坏的状态,你可以使用方式杀死它,方法很简单,只需要设置和钩掉复选框。 应用程序签名 Android 系统要求所有的程序经过数字签名才能安装,如果没有可用的数字签名,系统将不许安装运行此程序。不管是模拟器还是真实设备,只要是android 系统,这都适用。鉴于此原因,在设备或者是模拟器上运行调试程序之前,你必须为你的应用程序设置数字签名。 理解android 程序签名的重要几点:: · 所有的程序都必须签名,没有被签名的程序,系统将不能安装。 · 你可使用自签署证书签署你的应用程序,必须是无凭证授权是的。 · 系统仅仅会在安装的时候测试签名证书的有效期,如果应用程序的签名是在安装之后才到期,那么应用程序仍然可以正常启用。 · 你可以使用标准工具-Keytool and Jarsigner-生成密钥,来签名应用程序的.apk 文件。 Android SDK 工具可以帮助你在调试时给应用程序签名。ADT 插件和Ant 编译工具都提供了两种签名模式-debug 模式和release 模式 · debug 模式下,编译工具使用JDK 中的通用程序Keytool 通过已知方法和密码创建秘锁和密钥。每次编译的时候,工具使用debug 密钥签名应用程序的.apk 文件。因为密码是已知的,工具不需要在每次编译的时候提示你输入密锁和密钥。 · 当你的应用程序已经准备release 了,你可以在release 模式下编译。release 模式下,工具编译时不会将.apk 文件签名。你需要用Keytool 生成密钥和密锁, 再用JDK 中的Jarsigner 工具给.apk 文件签名。 签名基本设置 为了支持生成密锁和密钥,你首先要确定Keytool 在SDK 编译工具中是有效的。在很多情况下,你可以设置JAVA_HOME 环境变量,告诉SDK 如何找到Keytool,或者你可以在PATH 变量中添加Keytool 的JDK 版本。 如果你是在linux 版本中开发,原本是来自Java Gnu 编译器,请确定系统用的是Keytool版本的JDK,而不是gcj 版本的。如果Keytool 已经在PATH 中,它将指向符号连接/usr/bin/keytool。这种情况下,核实符号连接的目标是指向JDK 下的Keytool Eclipse/ADT 中的签名 如果你是在Eclipse 下开发,并已经按照上面所介绍的安装了Keytool,默认情况下是可以在debug 模式下签名的。当你运行调试程序的时候ADK 将给.apk 文件签名,并安装到模拟器上。这部分不需要特殊的动作,ADT 已经进入Keytool 在release 模式下编译程序,在Package 面版上按project 右键,选择Android Tools>Export Application Package.或者你可以点击Manifest Editor, overview 页面上的“Exporting the unsigned .apk”连接 ,导出未签名apk 文件。保存.apk 文件之后,用Jarsigner 及你自己的密钥给apk 文件签名,如果没有密钥, 你可以用Keystore 创建密钥和密锁。如果已经有一个密钥了,如公共密钥,就可以给.apk 文件签名了。 Ant 签名 如果用Ant 编译.apk 文件,假设你使用最新版的SDK 中包含的activitycreator 工具生成build.xml 文件,默认情况下可以使用debug 签名模式。当你运行Ant 对build.xml编译程序,build 脚本将生成密锁和密钥并签名.apk 文件。这部分不需要做其它特殊的动作。 release 模式下编译程序,你需要做的是在Ant 命令中指定编译目标“release”。例如,如果是在bulid.xml 所在目录下运行ant,输入以下命令: ant release build 脚本编译程序时并没有签名。编译完.apk 文件后,你需要用Jarsigner 和你自己的密钥给.apk 文件签名。如果没有密钥,你可以用Keystore 创建密钥和密锁。如果已经有一个密钥了,如公共密钥,你就可以给.apk 文件签名了。 调试证书期限 自签名证书用于程序的debug 模式下(默认情况下是Eclipse/ADT 和Ant builds),自它创建时间起有一年的期限。 当证书到期时,将会有编译错误。 And 下错误显示如下: debug: [echo] Packaging bin/samples-debug.apk, and signing it with a debug key... [exec] Debug Certificate expired on 8/4/08 3:43 PM 在Eclipse/ADT 下,你可以看到类似的错误。 解决这个问题的简单方法是删除debug.keystore 文件。Linux/Mac OSX 下这个文件保存在~/.android 下,windows XP 下,文件保存在 C:\Documents and Settings\<user>\Local Settings\Application Data\Android。windows Vista 下文件保存在 C:\Users\<user>\AppData\Local\Android。 下次编译时,编译工具将生成新的密锁和密钥。 使用ApiDemo 示例应用程序 Android SDK 包含了一套示例程序,他们验证了许多功能以及API 的用法。ApiDemos软件包被提前安装在模拟器中,所以你可以启动模拟器,在主画面的应用程序抽屉里打开它。你也可以在<SDK>/samples/ApiDemos 中找到源码,可用看看它,学习Demo 的实现方法。 如果你愿意,你还可以将ApiDemo 的示例程序作为一个工程加载进来,修改并在模拟器上运行。然而,在这之前你首先要卸载之前已经安装的ApiDemos。如果你没有移除之前安装的版本而直接在开发环境中运行或修改ApiDemos,将会有安装错误。 关于如何卸载和重装ApiDemo,可以参考I can't install ApiDemos apps in my IDE because of a signing error.这样你就可以在你的开发环境中工作了。 调试 Android 有相当广泛的一套工具帮助你调试你的应用程序: · DDMS -一个生动的程序,它支持端口转换(因此你可以在IDE 中给你的代码下端点),支持抓取模拟器屏幕,线程和堆栈信息,以及许多其他功能。你还可以运行logcat 重新获得Log 信息。点击此连接查看更多信息。 · logcat- 转储系统信息,这些信息包括,模拟器抛出错误时堆栈的运行过程以及日志信息。运行logcat,点击此连接。 · ... · I/MemoryDealer( 763): MemoryDealer (this=0x54bda0): Creating 2621440 bytes heap at 0x438db000 · I/Logger( 1858): getView() requesting item number 0 · I/Logger( 1858): getView() requesting item number 1 · I/Logger( 1858): getView() requesting item number 2 · D/ActivityManager( 763): Stopping: HistoryRecord{409dbb20 com.android.home.AllApps} ... · Android Log - 输出模拟器上log 文件信息日志类。如果你在DDMS 上运行了 logcat,你可以实时阅读这些信息。在你的代码中添加logging 方法的调用。使 用log 类,你可以根据你想获得信息的重要程度不同调用 Log.v(verbose),Log.d()(debug),Log.i()(information),Log.w()(warning)或者 Log.e(error).来分派log 信息Log.i("MyActivity", "MyClass.getView() — Requesting item number " + position) 你可以用logcat 阅读这些信息。 · Traceview - Android 可以将函数的调用情况以及调用时间保存到一个log 文件中,你可以用图形阅读器Traceview 查看详细内容。更多信息查看这个连接下的主题 · Eclipse plugin -Eclipse 插件整合了相当数量的工具(ADB,DDMS,logcat output, 以及其它功能),点击此连接查看更多信息。 · Debug and Test Device Settings -Android 揭示了很多有用的设定,例如CPU使用率和帧速率,参看下面的 Debug and Test Settings on the Emulator Also, see the Troubleshooting section of the doc to figure out why your application isn't appearing on the emulator, or why it's not starting. 此外,参看疑难解答这一节文档,以找出您的应用程序为什么没有出现在模拟器上,或为什么不开始。 设备上的调试和测试设置 Android 允许你设置多个设定以便你测试和调试程序。获得模拟器的开发设置,可以选择Dev Tools>Development Settings。 按照以下选项将打开开发设置页(或其中之一): · Debug app 选择要被调试的程序,你不需要设置这个来关联调试器,但是这个变量有两个作用: o 防止Android 在调试的断点处长时间停留时抛出错误。 o 允许你选择Wait for Debugger 选项来暂停程序启动,直到调试器被关联 上(如下介绍) · Wait for debugger 阻塞程序加载直到关联上调试器。这样你可以在 onCreate()中设置端点,这在调试Activity 的启动进程时很重要。当你改变这个 选项,任何当前运行的程序实例将被杀死。为选中此框,你必须如上面介绍的选 择一个调试程序。这和在代码中添加waitForDebugger()是一样的。 · Immediately destroy activities 告诉系统只要activity 停止了就销毁它。 (犹 如 Android 必须回收内存). 这个在测试 onSaveInstanceState(Bundle) / onCreate(android.os.Bundle)代码路径 时非常有用, 否则将难以生效.选择这个 选项可能带来很多问题,因为他们没有保存程序的状态。 · Show screen updates 选中这个选项时,屏幕上任何被重绘的矩形区域会闪现粉红色。这对于发现屏幕不必要的绘图很有用。 · Show CPU usage 在屏幕顶端显示一个CPU 进度,显示CPU 的使用情况。上面红色栏显示总的CPU 使用率,下方绿色栏显示目前画面的CPU 使用时间。注意:一旦打开次功能就不能关掉,除非重新启动模拟器。 · Show background 没有activity 屏幕显示时显示背景面板,这个通常在调试的时候才会发生。 模拟器重起后这些设置仍被记忆。 顶端调试技巧 快速堆栈转储从模拟器上获得堆转储,你可以登录adb shell,用"ps"命令找到你想要的进程,然后用"kill-3",堆栈使用轨迹将显示在log 文件中。 在模拟器屏幕上显示有用信息设备可以显示一些有用信息,例如CPU 使用率,以及高亮显示重绘区域。可以在开发设定窗口打开和关闭这些功能。Setting debug and test configurations onthe emulator.中有详细介绍。 你可以通过Dalvik Debug Monitor Service 工具获得转储状态信息。请参考adb 中介绍的dumpsys and dumpstate获得模拟器中应用程序状态信息(dumpsys) 你可以通过Dalvik Debug Monitor Service 工具获得dumpsys 信息。参考adb 中介绍的dumpsys and dumpstate 。 获得无线连接信息你可以通过Dalvik Debug Monitor Service 工具获得无线连接信息。在Device菜单中选择"Dump radio state"记录跟踪数据你可以在activity 中通过调用android.os.Debug.startMethodTracing()来记录函数的调用以及其它跟踪数据。详细的参考Running the Traceview DebuggingProgram 。 记录无线数据默认情况下系统不记录无线数据(数据很多)。然而,你可以用下面的命令记录无线数据:adb shell logcat -b radio 运行adb Android 有adb 工具,他提供了许多功能,包括移动和同步文件到模拟器上,改变端口,在模拟器上运行 UNIX shell。获得模拟器屏幕截图Dalvik Debug Monitor Server (DDMS)可以抓取模拟器屏幕截图。使用调试帮助类Android 为方便使用提供了调试帮助类,例如util.Log 和Debug 编译安装Anroid 应用程序 Android 要求专门的编译工具可以正确的编译资源文件和应用程序的其他部分,因此,你必须为你的应用程序建立一个专门的编译环境。专门Android 编译器编译步骤包括,编译XML 和其他资源文件并创建合适的输出格式。编译好的Android 应用程序是一个.apk 压缩文件,它含有.dex 文件,资源文件,原data文件,以及其他文件。你可以通过scratch,或者源文件构造一个合适的Android 工程。Android 目前不支持的在本地代码上开发第三方应用程序。比较推荐的Andriod 应用程序开发方法是use Eclipse with the Android plugin,它支持编译,运行,调试Android 应用程序。如果你还有其他IDE,Android provides tools for other IDEs 可以编译运行Android 应用程序,但是他们不是很完整。 移出一个Android 应用程序 移出一个安装在模拟器上的应用程序,你需要执行adbrun adb 删除.apk 文件。.apk 文件是在安装的时候发送到模拟器上的。使用adb shell 进入设备的shell,切换到data/app目录下,用rm 命令删除apk 文件 :rm your_app.apk。用法在连接中介绍。 Eclipse 技巧 在Eclipse 上执行任意java 代码 在Eclipse 上,当程序停在断点处时你可以执行任意代码。例如,在一个含有“zip”字符串参数的函数中,你可以获得软件包信息,调用类方法。你也可以执行任意静态方法:如,输入 android.os.Debug.startMethodTracing() ,启动 dmTrace。 打开代码执行窗口,主菜单中选择Window>Show View>Display,打开显示窗口,一个简单的文本编辑器。输入你的代码,高亮显示文字,单击'J'图标(或者CTRL + SHIFT+ D)运行代码。代码在被选线程的上下文中运行, 而这个线程必须是停在断点处或者单步停止点。(如果你手动挂去线程,你必须单步执行。线程停在Object.wait()是没有用的)。 如果你目前是停在断点,你可以简单的按下(CTRL + SHIFT + D)高亮并执行一段代码。 你可以高亮同一选中区域的文字,通过按下 ALT +SHIFT + 向上/向下箭头来改变所选区域的大小下面是一些例子,输入内容和eclipse 显示窗口的回应信息。 手动运行DDMS 虽然推荐用ADT 插件调试程序,但是你也可以手动运行DDMS,配置Eclipse 以便在8700 端口上调试程序(注意:首先确定你启动了DDMS)。 |
|
cuihai |
2011-03-03 12:15 |
小提示: 如果你忘记引入 TextView 的包,可以尝试 Ctrl-Shift-O (如果是Mac 系统Cmd-Shift-O)。 这是Eclipse 管理应用的快捷方式-它会显示没有找到的包然后自动为你加上。在Android 里,用户接口由一些称之为视图的不同层次的类组成。一个视图就是一个简单的对象。如单选框,动画控件,一个文本框(我们的例子里的),我们称处理文本的这样一个子视图就叫TextView。这里教你如何创建 TextView。 这里教你如何创建TextView: TextView tv = new TextView(this); TextView 构造器就是Android 上下文的实例,这个上下文仅仅是指向系统的一个句柄,它提供像资源处理之类的服务。包含一些进入资料库以及参数选择的入口。这个活动也是继承上下文。 HelloAndroid 类是活动的一个子类,它也是一个上下文,我们能通过this 操作TextView。 创建TextView 后,加入需要显示的内容: tv.setText("Hello, Android"); 这里很正常。我们创建一个TextView,然后告诉它显示的内容。最后一步就是让TextView 在屏幕上显示出来,像这样: setContentView(tv); 活动里setContentView()的方法表明哪个视图需要在当前UI 上被操作。如果一个活动不能调用这个方法,那么当前就没有界面系统显示为一个空白屏幕。我们仅仅想显示一些文本,所以我们将刚才创建的TextView 连接上 这就是Android 平台里的“Hello,World”,当然,我们可以看下运行情况。 不使用Eclipse 创建工程 如果你不使用Eclipse(比如你喜欢其他IDE 工具,或者仅仅使用文本编辑器或者命令行工具),那么Eclipse 的插件对你没有作用。别担心-它不会因为你不使用Eclipse 而使用失去任何功能。 Android 对Eclipse 的插件仅仅是Android SDK 外围的一个工具。(这些工具如:模拟器,aapt, adb, ddms 等等都在 其他文档) 因此,将这些工具和其他工具如“ant”编译文件结合也是有可能的。 Android SDK里包含一个Python 的脚本“activitycreator.py”,它可以为你的项目创建所有的代码以及目录。就像ant 中的“build.xml”文件。这允许你使用命令行编译你的工程或者使用你自己的IDE 工具集成。 例如,就像我们刚使用Eclipse 创建的HelloAndroid 项目,你可以使用命令: activitycreator.py --out HelloAndroid com.android.hello.HelloAndroid 编译工程的时候,你运行“ant”命令,当命令成功执行后,将在“bin”目录里产生一个“HelloAndroid.apk”的文件,“apk”文件是Android 的一个包,你可以使用“adb”工具安装或者执行。 如果想获得更多信息,请阅读网站提供的替他文档。 Android 应用程序构成 一般情况Android 应用程序是由以下四种组件构造而成的: · 活动 · 广播接收器 · 服务 · 内容提供器 需要注意的是,并不是每个Andorid 应用程序都必须构建这4 个组件,有些可能由这些组件的组合而成。 一旦你确定了你的应用程序中需要的组件,那么你就应该在AndroidManifest.xml 中列出他们。 这是一个XML 配置文件,它用于定义应用程序中需要的组件、组件的功能及必要条件等。这个文件是必须的。详情参见Android manifest file documentation 四种组件说明如下: 活动 活动是最基本的Andorid 应用程序组件,应用程序中,一个活动通常就是一个单独的屏幕。每一个活动都被实现为一个独立的类,并且从活动基类中继承而来, 活动类将会显示由视图控件组成的用户接口,并对事件做出响应。 大多数的应用是由多屏幕显示组成。例如,一个文本信息的应用也许有一个显示发送消息的联系人列表屏幕,第二个屏幕用来写文本消息和选择收件人, 再来一个屏幕查看消息历史或者消息设置操作等。 这里每一个这样的屏幕就是一个活动,很容易实现从一个屏幕到一个新的屏幕并且完成新的活动。 在某些情况下当前的屏幕也许需要向上一个屏幕动提供返回值--比如让用户从手机中挑选一张照片返回通讯录做为电话拨入者的头像。 当打开一个新的屏幕时,之前一个屏幕会被置为暂停状态并且压入历史堆栈中。用户可以通过回退操回到以前打开过的屏幕。我们可以选择性的移除一些没有必要保留的屏幕,因为Android 会把每个从桌面打开的程序保留在堆栈中。 Intent 和 Intent Filters 调用Android 专有类 Intent 进行构屏幕之间的切换。 Intent 是描述应用想要做什么。Intent 数据结构两最重要的部分是动作和动作对应的数据。典型的动作类型有:MAIN(活动的门户)、VIEW、PICK、EDIT 等。而动作对应的数据则以URI 的形式进行表示。例如:要查看某一个人的联系方式,你需要创建一个动作类型为VIEW 的intent,以及一个表示这个人的URI。 与之有关系的一个类叫IntentFilter。当intent 被要求做某事的时候,intent filter 用于描述一个活动(或者BroadcastReceiver,看下面)能够操作哪些intent。一个活动如果要显示一个人的联系方式时,需要声明一个IntentFilter,这个IntentFilter 要知道怎么去处理VIEW 动作和表示一个人的URI。 IntentFilter 需要在AndroidManifest.xml 中定义。 通过解析各种intent,从一个屏幕切换到另一个屏幕是很简单的。当向前导航时,活动将会调用startActivity(myIntent)方法。然后,系统会在所有安装的应用程序定义的IntentFilter 中查找,找到最匹配myIntent 的Intent 对应的活动。新的活动接收到myIntent 的通知后,开始运行。当start 活动方法被调用将触发解析myIntent 的动作,这个机制提供了两个关键好处: · 活动能够重复利用从其它组件中以Intent 的形式产生的一个请求 · 活动可以在任何时候被一个具有相同IntentFilter 的新的活动取代 广播接收器 你可以使用BroadcastReceiver 来让你的应用对一个外部的事件做出响应。比如:当电话呼入时,数据网络可用时,或者到了晚上时。BroadcastReceivers 不能显示UI,它只能通过 NotificationManager 来通知用户这些有趣的事情发生了。 BroadcastReceivers 既可以在AndroidManifest.xml 中注册,也可以在代码中使用Context.registerReceiver()进行注册。但这些有趣的事情发生时,你的应用不必对请求调用BroadcastReceivers,系统会在需要的时候启动你的应用,并在必要情况下触发BroadcastReceivers。各种应用还可以通过使用Context.sendBroadcast()将它们自己的intent broadcasts 广播给其它应用程序。 服务 一个服务是具有一段较长生命周期且没有用户界面的程序。比较好的一个例子就是一个正在从播放列表中播放歌曲的媒体播放器。在一个媒体播放器的应用中,应该会有多个活动,让使用者可以选择歌曲并播放歌曲。然而,音乐重放这个功能并没有对应的活动,因为使用者当然会认为在导航到其它屏幕时音乐应该还在播放的。在这个例子中,媒体播放器这个活动会使用Context.startService() 来启动一个服务,从而可以在后台保持音乐的播放。同时,系统也将保持这个服务一直执行,直到这个service 运行结束。(你可以通过阅读Life Cycle of an Android Application 获取更多关于服务的介绍). 另外,我们还可以通过使用Context.bindService() 方法,连接到一个服务上(如果这个服务还没有运行将启动它)。当连接到一个服务之后,我们还可以通过服务提供的接口与它进行通讯。拿媒体播放器这个例子来说,我们还可以进行暂停、重播等操作。 教程:一个记事本应用程序范例 本教程通过手把手教你的方式,讲解如何利用Android 框架和诸多工具建立自己的手机应用。从一个预先配置好的工程文件开始,该教程通过一个简单记事本应用程序完整的开发过程,并辅以贯穿始终的详尽例子,指导你如何搭建工程、组织应用逻辑以及UI,乃至接下来的编译及运行可执行程序等等。 该教程将这个记事本应用的开发过程视作一组练习(见如下),每一个练习都由若干步骤组成。你可以亦步亦趋地完成每个练习步骤,逐步建立并完善自己的应用程序。这些练习提供了你实现此应用所需的——细到每一步骤的——具体范例代码。 当你完成此教程后,一个具有实际功能的Android 应用就从你手上诞生了,并且你对Android 应用开发中的一些极为重要的概念也会有更加深刻的理解。若你想为你这个简单的记事本应用添加更多复杂功能的话,你可以用另一方式实现的记事本程序比照你的练习代码,具体可参看 Sample Code 文档部分。 本教程目标读者 该教程主要是面向有一定经验,尤其是那些具备一定Java 编程语言知识的开发者。如果你之前从未写过一个Java 应用程序的话,仍可以使用此教程,只是学习进度稍稍慢一点罢了。 本教程假定你已熟悉了一些基本的Android 应用概念和术语。如果你对这些还不够熟稔的话,你得将 Overview of an Android Application 好好温故一下,才能继续下面的学习。 同时需注意的时,该教程的集成开发环境是预装Android 插件的Eclipse。如果你不用Eclipse,仍可做下面的这些练习和建立应用,但你届时将不得不面对一些涉及Eclipse的步骤在非Eclipse IDE 中如何实现的问题。 Android 应用程序模块: 应用, 任务, 进程, 和线程 在大多数操作系统里,存在独立的一个1 对1 的可执行文件(如Windows 里的exe 文件),它可以产生进程,并能和界面图标、应用进行用户交互。但在Android 里,这是不固定的,理解将这些分散的部分如何进行组合是非常重要的。 由于Android 这种可灵活变通的,在实现一个应用不同部分时你需要理解一些基础技术: · 一个android 包 (简称 .apk ) ,里面包含应用程序的代码以及资源。这是一个应用发布,用户能下载并安装他们设备上的文件。 · 一个 任务 ,通常用户能当它为一个“应用程序”来启动:通常在桌面上会有一个图标可以来启动任务,这是一个上层的应用,可以将你的任务切换到前台来。 · 一个 进程 是一个底层的代码运行级别的核心进程。通常.apk 包里所有代码运行在一个进程里,一个进程对于一个.apk 包;然而, 进程 标签常用来改变代码运行的位置,可以是 全部的.apk 包 或者是独立的 活动, 接收器, 服务, 或者 提供器组件。 任务 记住关键的一点:当用户看到的“应用”,无论实际是如何处理的,它都是一个任务。如果你仅仅通过一些活动来创建一个.apk 包,其中有一个肯定是上层入口(通过动作的intent-filter 以及分android.intent.category.LAUNCHER),然后你的.apk 包就创建了一个单独任务,无论你启动哪个活动都会是这个任务的一部分。 一个任务,从使用者的观点,他是一个应用程序;对开发者来讲,它是贯穿活动着任务的一个或者多个视图,或者一个活动栈。当设置Intent.FLAG_ACTIVITY_NEW_TASK标志启动一个活动意图时,任务就被创建了;这个意图被用作任务的根用途,定义区分哪个任务。如果活动启动时没有这个标记将被运行在同一个任务里(除非你的活动以特殊模式被启动,这个后面会讨论)。如果你使用 FLAG_ACTIVITY_NEW_TASK 标记并且这个意图的任务已经启动,任务将被切换到前台而不是重新加载。 FLAG_ACTIVITY_NEW_TASK 必须小心使用:在用户看来,一个新的应用程序由此启动。如果这不是你期望的,你想要创建一个新的任务。另外,如果用户需要从桌面退出到他原来的地方然后使用同样的意图打开一个新的任务,你需要使用新的任务标记。否则,如果用户在你刚启动的任务里按桌面(HOME)键,而不是退出(BACK)键,你的任务以及任务的活动将被放在桌面程序的后面,没有办法再切换过去。 任务亲和力(Affinities) 一些情况下Android 需要知道哪个任务的活动附属于一个特殊的任务,即使该任务还没有被启动。这通过任务亲和力来完成,它为任务中一个或多个可能要运行的活动提供一个独一无二的静态名字。默认为活动命名的任务亲和力的名字,就是实现该活动.apk 包的名字。这提供一种通用的特性,对用户来说,所有在.apk 包里的活动都是单一应用的一部分。 当不带 Intent.FLAG_ACTIVITY_NEW_TASK 标记启动一个新的活动,任务亲和力对新启动的活动将没有影响作用:它将一直运行在它启动的那个任务里。然而,如果使用NEW_TASK 标记,亲和力会检测已经存在的任务是否具有相同的亲和力。如果是,该任务会被切换到前台,新的活动会在任务的最上面被启动。 你可以在你的表现文件里的应用程序标签里为.apk 包里所有的活动设置你自己的任务亲和力,当然也可以为单独的活动设置标签。这里有些例子演示如何使用: · 如果你的.apk 包里包含多个用户可启动的上层应用程序,那么你可能想要为每个活动分配不同的亲和力。这里有一个不错的协定,你可以将不同的名字字串加上冒号附加在.apk 包名字的后面。 例如,"com.android.contacts"的亲和力命名可以是"com.android.contacts:Dialer"and"com.android.contacts:ContactsList"。 · 如果你想替换一个通知,快捷键,或者其它能从外部启动的应用程序的内部活动,你需要在你想替换的活动里明确的设置任务亲和力(taskAffinity)。例如,如果你想替换联系人详细信息浏览界面(用户可以直接操作或者通过快捷方式调用),你需要设置任务亲和力(taskAffinity)为“com.android.contacts”。 启动模式以及启动标记 你控制活动和任务通信的最主要的方法是通过设置启动模式的属性以及意图相应的标记。这两个参数能以不同的组合来共同控制活动的启动结果,这在相应的文档里有描述。 这里我们只描述一些通用的用法以及几种不同的组合方式。 你最通常使用的模式是singleTop(除了默认为standard 模式)。这不会对任务产生什么影响;仅仅是防止在栈顶多次启动同一个活动。 singleTask 模式对任务有一些影响:它能使得活动总是在新的任务里被打开(或者将已经打开的任务切换到前台来)。使用这个模式需要加倍小心该进程是如何和系统其他部分交互的,它可能影响所有的活动。这个模式最好被用于应用程序入口活动的标记中。 (支持MAIN 活动和LAUNCHER 分类)。 singleInstance 启动模式更加特殊,该模式只能当整个应用只有一个活动时使用。有一种情况你会经常遇到,其它实体(如搜索管理器SearchManager 或者 通知管理器NotificationManager)会启动你的活动。这种情况下,你需要使用Intent.FLAG_ACTIVITY_NEW_TASK 标记,因为活动在任务(这个应用/任务还没有被启动)之外被启动。就像之前描述的一样,这种情况下标准特性就是当前和任务和新的活动的亲和性匹配的任务将会切换到前台,然后在最顶端启动一个新的活动。当然,你也可以实现其它类型的特性。 一个常用的做法就是将Intent.FLAG_ACTIVITY_CLEAR_TOP 和NEW_TASK 一起使用。这样做,如果你的任务已经处于运行中,任务将会被切换到前台来, 在栈里的所有的活动除了根活动,都将被清空,根活动的onNewIntent(Intent) 方法传入意图参数后被调用。当使用这种方法的时候 singleTop 或者 singleTask 启动模式经常被使用,这样当前实例会被置入一个新的意图,而不是销毁原先的任务然后启动一个新的实例。 另外你可以使用的一个方法是设置活动的任务亲和力为空字串(表示没有亲和力),然后设置finishOnBackground 属性。 如果你想让用户给你提供一个单独的活动描述的通知,倒不如返回到应用的任务里,这个比较管用。要指定这个属性,不管用户使用BACK还是HOME,活动都会结束;如果这个属性没有指定,按HOME 键将会导致活动以及任务还留在系统里,并且没有办法返回到该任务里。 请确保阅读过文档启动模式属性(launchMode attribute) 以及 意图标记(Intent flags) ,关注这些选项的详细信息。 进程 在Android 中,进程是应用程序的完整实现,而不是用户通常了解的那样。他们主要用途很简单: · 提高稳定性和安全性,将不信任或者不稳定的代码移动到其他进程。 · 可将多个.apk 包运行在同一个进程里减少系统开销。 · 帮助系统管理资源,将重要的代码放在一个单独的进程里,这样就可以单独销毁应用程序的其他部分。 像前面描述的一样,进程的属性被用来控制那些有特殊应用组件运行的进程。注意这个属性不能违反系统安全: 如果两个.apk 包不能共享同一个用户ID,却试图运行在通一个进程里,这种情况是不被允许的,事实上系统将会创建两个不同的进程。 请查看安全相关文档以获取更多关于安全限制方面的信息。 线程 每个进程包含一个或多个线程。多数情况下,Android 避免在进程里创建多余的线程,除非它创建它自己的线程,我们应保持应用程序的单线程性。一个重要的结论就是所有呼叫实例, 广播接收器, 以及 服务的实例都是由这个进程里运行的主线程创建的。 注意新的线程不是为活动,广播接收器,服务或者内容提供器实例创建:这些应用程序的组件在进程里被实例化(除非另有说明,都在同一个进程处理),实际上是进程的主线程。这说明当系统调用时这些组件(包括服务)不需要进程远距离或者封锁操作(就像网络呼叫或者计算循环),因为这将阻止进程中的所有其他组件。你可以使用标准的线程 类或者Android 的HandlerThread 类去对其它线程执行远程操作。 这里有一些关于创建线程规则的例外: · 呼叫IBinder 或者IBinder 实现的接口,如果该呼叫来自其他进程,你可以通过 线程发送的IBinder 或者本地进程中的线程池呼叫它们,从进程的主线程呼叫是不可以的。特殊情况下,,呼叫一个服务 的IBinder 可以这样处理。(虽然在服务里呼叫方法在主线程里已经完成。)这意味着IBinder 接口的实现必须要有一种线程安全的方法,这样任意线程才能同时访问它。 · 呼叫由正在被调用的线程或者主线程以及IBinder 派发的内容提供器 的主方法。 被指定的方法在内容提供器的类里有记录。这意味着实现这些方法必须要有一种 线程安全的模式,这样任意其它线程同时可以访问它。 · 呼叫视图以及由视图里正在运行的线程组成的子类。通常情况下,这会被作为进程的主线程,如果你创建一个线程并显示一个窗口,那么继承的窗口视图将从那个线程里启动。 Android 应用程序的生命周期 在大多数情况下,每个Android 应用程序都运行在自己的Linux 进程中。当应用程序的某些代码需要运行时,这个进程就被创建并一直运行下去,直到系统认为该进程不再有用为止。然后系统将回收进程占用的内存以便分配给其它的应用程序。应用程序的开发人员必须理解不同的应用程序组件(尤其是Activity, Service, 和BroadcastReceiver)是如何影响应用程序进程生命周期的,这是很重要的一件事情。不正确地使用这些组件可能会导致系统杀死正在执行重要任务的应用程序进程。一个常见的进程生命周期bug 的例子是BroadcastReceiver, 当BroadcastReceiver在BroadcastReceiver.onReceive()方法中接收到一个Intent 时,它会启动一个线程,然后返回。一旦它返回,系统将认为BroadcastReceiver 不再处于活动状态,因而BroadcastReceiver 所在的进程也就不再有用了(除非该进程中还有其它的组件处于活动状态)。因此,系统可能会在任意时刻杀死进程以回收内存。这样做的话,进程中创建(spawned)出的那个线程也将被终止。对这个问题的解决方法是从BroadcastReceiver 启动一个服务,让系统知道进程中还有处于活动状态的工作。为了决定在内存不足时让系统杀死哪个进程,Android 根据每个进程中运行的组件以及组件的状态把进程放入一个”重要性分级(importance hierarchy)”中。进程的类型包括(按重要程度排序): 1. 前台(foreground)进程,与用户当前正在做的事情密切相关。不同的应用程序组件能够通过不同的方法使它的宿主进程移到前台。当下面任何一个条件满足时,可以考虑将进程移到前台: 1. 进程正在屏幕的最前端运行一个与用户交互的Activity (它的onResume() 方法被调用) 2. 进程有一正在运行的BroadcastReceiver (它的 BroadcastReceiver.onReceive()方法正在执行) 3. 进程有一个Service,并且在Service 的某个回调函数(Service.onCreate(), Service.onStart(), 或 Service.onDestroy())内有正在执行的代码。 1. 可见(visible)进程,它有一个可以被用户从屏幕上看到的Activity,但不在前台(它的onPause()方法被调用)。举例来说,如果前台的Activity 是一个对话框,以前的Activity 隐藏在对话框之后,就可能出现这种进程。这样的进程特别重要,一般不允许被杀死,除非为了保证前台进程的运行不得不这样做。 2. 服务(service)进程,有一个已经用startService() 方法启动的Service。虽然这些进程用户无法直接看到,但它们做的事情却是用户所关心的(例如后台MP3回放或后台网络数据的上传下载)。因此,系统将一直运行这些进程除非内存不足以维持所有的前台进程和可见进程。 3. 后台(background)进程, 拥有一个当前用户看不到的Activity(它的onStop() 方法被调用)。这些进程对用户体验没有直接的影响。如果它们正确执行了Activity生命期(详细信息可参考Activity),系统可以在任意时刻杀死进程来回收内存,并提供给前面三种类型的进程使用。系统中通常有很多个这样的进程在运行,因此要将这些进程保存在LRU 列表中,以确保当内存不足时用户最近看到的进程最后一个被杀掉。 4. 空(empty)进程,不包含任何处于活动状态的应用程序组件。保留这种进程的唯一原因是,当下次应用程序的某个组件需要运行时,不需要重新创建进程,这样可以提高启动速度。 系统将以进程中当前处于活动状态组件的重要程度为基础对进程进行分类。请参考Activity, Service 和 BroadcastReceiver 文档来获得有关这些组件在进程整个生命期中是如何起作用的详细信息。每个进程类别的文档详细描述了它们是怎样影响应用程序整个生命周期的。进程的优先级可能也会根据该进程与其它进程的依赖关系而增长。例如,如果进程A 通过在进程B 中设置Context.BIND_AUTO_CREATE 标记或使用ContentProvider 被绑定到一个服务(Service),那么进程B 在分类时至少要被看成与进程A 同等重要。 二、开发应用程序 Android 应用构成 Android 应用是由各种各样的组件来构成。 这些组件大部分都是松散连接的,准确 的说你可以把它们看成组件的联合而非是一个单一的应用。 通常,这些组件运行在同一个系统进程里面。你也可以在这个进程里面创建多个线程(这是很常见的),如果必要你也可以创建独立的子进程。不过这种情况是非常少见的,因为Android 尽力使代码进程间透明。 以下部分是很重要的Android APIs: AndroidManifest.xml AndroidManifest.xml 是系统的控制文件,它告诉系统如何处理你所创建的所有 顶层组件(尤其是activities,服务,Intent 接收器和后面描述的内容管理器)。举例 来说,控制文件就是把你的活动(Activities)要接收的Intents 连接在一起的“胶 水”。 活动(Activities) 活动(Activity)就是一个有生命周期的对象。 一个Activity 就是完成某些工作 的代码块, 如必要的话,这部分工作还可能包括对用户UI 界面的显示。不过 这不是必须的,有些活 动从不显示UI 界面。典型地,你将会指定你的应用程 序中的一个活动为整个程序的入口点。 视图(Views) 视图(Views)可以将其自身绘制到屏幕上。Android 的用户界面由一系列的视图 树 (trees of views)构成。接口都是由一组以树的形式出现的视图组成的。开 发者可 以通过创建一个新的视图的方法来使用自定义的图形处理技术(比如开 发游戏,或者是 使用了不常用的用户图形(UI)窗口界面(widget))。 Intents Intents 是一个简单的消息对象,它表示程序想做某事的“意图”(intention)。比 如如果你的应用程序想要显示一个网页,那么它通过创建一个Intent 实例并将其 传递给 系统来表示意图浏览这个URI。系统将定位于知道如何能处理这一 Intent 的代码(在当 前情况下就是浏览器),并运行之。Intents 也可以用于广 播系统范围内的有效事件 (例如通知事件)。 服务(Services) 服务是运行在后台的一段代码。它可以运行在它自己的进程,也可以运行在其他 应用程 序进程的上下文(context)里面,这取决于自身的需要.。其它的组件 可以绑定到一个服 务(Service)上面,通过远程过程调用(RPC)来调用这 个方法。例如媒体播放器的服务, 当用户退出媒体选择用户界面,她仍然希望 音乐依然可以继续播放,这时就是由服务 (service)来保证当用户界面关闭时 音乐继续播放的。 通知(Notifications) 通知将以小图标的形式呈现在状态栏里,用户通过与图标的交互式操来接收消 息。最常见 的通知包括短信息,通话记录,语音邮件,但是应用程序也可以创 建它们自己的通知事件。 我们推荐采用通知事件实现提醒用户的注意。 内容管理器(ContentProviders) 内容管理器(ContentProvider)提供对设备上数据进行访问的数据仓库。典型 的例子就 是使用内容管理器来访问联系人列表。你的应用程序也可以使用其它 程序通过内容管理器提 供的数据,同时你也可以定义你自己的内容管理器来向 其它应用提供数据访问服务。 存、取、提供数据 典型的桌面操作系统一般能提供一种通用的文件系统,所有应用程序都能储存和读文件,并且其他应用程序也能访问该文件(可能需要一些访问控制设置). Android 使用不同的方式:在平台上,所有应用程序的数据(包括文件),对该应用程序是私有的。当然,Android 也提供一种标准方法将自己的私有数据提供给其他应用程序访问。这一章节讲了很多方法,描述应用如何存取数据,以及将数据提供给其他程序访问,当然,你也可以向其他应用程序请求并获得它的数据。 Android 提供下面的方式来存取数据: 参数选择 使用一个轻量级机制来存取基本数据类型的数据对,这是典型应用程序参数的 存储模式。 文件 你可以将你的文件存储在设备上或者其他移动媒介上,默认情况下,其他应用 程序是不能访问这些文件的。 数据库 Android 有直接SQLite 数据库的API。应用程序可以创建以及使用SQLite 数据 库。 每个包创建的数据库都是私有的。 数据提供 数据提供是应用程序的一个可选组件,它可以提供读/写应用程序私有数据的方 法。内容提供组件实现了一种标准请求数据的语法,和一种标准处理返回值的 机制。Android 提供很多标准数据的提供方式,例如私有联系人。 网络 不要忘记,我们还可以使用网络存取数据。 Android 的安全与权限 Android 是一个多进程系统,每一个应用程序(和系统的组成部分)都运行在自己的进程中。在应用程序和系统间的安全通过标准的Linux 设备在进程级被执行,例如被分配给应用程序的用户和组ID。额外的细粒度安全特性通过“许可”机制来提供,该机制能够对一个指定进程可实现的特定操作进行约束。 安全结构 Android 安全学中的一个重要的设计点是在默认情况下应用程序没有权限执行对其它应用程序、操作系统或用户有害的操作。这些操作包括读/写用户的隐私数据(例如联系方式或e-mail),读/写其它应用程序的文件,执行网络访问,保持设备活动,等等。 应用程序的进程是一个安全的沙箱。它不能干扰其它应用程序,除非在它需要添加原有沙箱不能提供的功能时明确声明权限。这些权限请求能够被不同方式的操作所处理,特别的要基于证书和用户的提示被自动的允许或禁止。权限的请求在那个应用程序中通过一个应用程序被声明为静态的,所以在此之后在安装时或没有改变时它们会预先知道。 应用程序签名 所有的Android 应用程序(.apk 文件)必须通过一个证书的签名,此证书的私钥必须被开发者所掌握。这个证书的标识是应用程序的作者。这个证书不需要通过证书组织的签署:Android 应用程序对于使用自签署的证书是完全允许的和特别的。这个证书仅仅被用于与应用程序建立信任关系,不是为了大规模的控制应用程序可否被安装。最重要的方面是通过确定能够访问原始签名权限和能够共享用户ID 的签名来影响安全。 用户标识和文件访问 安装在设备中的每一个Android包文件(.apk)都会被分配给一个属于自己的统一的Linux用户ID,并且为它创建一个沙箱以防止影响其它应用程序(或者其它应用程序影响它)。 用户ID 在应用程序安装到设备中时被分配,并且在这个设备中保持它的永久性。 因为安全执行发生在进程级,所以一些不同包中的代码在相同进程中不能正常的运行,自从他们需要以不同Linux 用户身份运行时。你可以使用每一个包中的 AndroidManifest.xml 文件中的manifest 标签属性sharedUserId 拥有它们分配的相同用户ID。通过这样做,两个包被视为相同的应用程序的安全问题被解决了,注意为了保持安全,仅有相同签名(和请求相同sharedUserId 标签)的两个应用程序签名将会给相同的用户ID。 应用创建的任何文件都会被赋予应用的用户标识,并且,正常情况下不能被其它包访问。当你通过getSharedPreferences(String, int), openFileOutput(String, int) 或者openOrCreateDatabase(String, int, SQLiteDatabase.CursorFactory)创建一个新文件时, 你可以同时或分别使用 MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE 标志允许其它包读/写此文件。当设置了这些标志时,这个文件仍然属于你的应用程序,但是它的全局读、写和读写权限已经设置所以其它任何应用程序可以看到它。 权限命名 一个基本的Android 应用程序没有与其相关联的权限,意味着它不能做任何影响用户体验或设备中的数据的有害操作。要利用这个设备的保护特性,在你的应用程序需要时,你必须在AndroidManifest.xml 文件中包含一个或更多的<uses-permission> 标签来声明此权限。 例如:需要监听来自SMS 消息的应用程序将要指定如下内容: <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.app.myapp" > <uses-permission android:name="android.permission.RECEIVE_SMS" /> </manifest> 在安装应用程序时,通过包安装器应用程序要通过权限请求的许可,使建立在与应用程序签名的核对下声明对于用户的那些权限和影响。在应用运行期间对用户不做检查:它要么在安装时被授予特定的许可,并且使用想用的特性;要么不被授予许可,并且使得一切使用特性的尝试失败而不提示用户。 例如,sendBroadcast(Intent) 方法就是当数据被发送给到每个接收器时检查许可的, 在方法调用返回之后,因此当许可失败时你不会收到一个异常。然而,几乎在所有例子中,许可失败都会被打印到系统日志中。通常,多次的许可错误会产生抛回至应用程序的SecurityException 异常。 Android 系统提供的许可可以在Manifest.permission 中找到。每个引用也可以定义和Enforce 它自己的许可,因此这不是全面的所有可能的列表。 在程序操作期间,个别权限在一些地方可能被强制: · 在系统接到呼叫的时候,预防一个应用程序去执行特定的函数。 · 在启动Activity 时,防止一个应用启动其它应用程序的Activities。 · 发送和接收Intent 广播时,控制谁能接收你的广播或者谁能发送广播给你。 · 在一个内容提供器上访问和操作时。 · 绑定或开始一个服务时。 权限的声明和支持 为了执行你自己的权限,你必须首先在你的AndroidManifest.xml 中使用一个或多个<permission> 标签声明它们。 <protectionLevel> 属性是必需的,告诉系统用户应如何处理应用程序接到请求此权限的通知,或者在这个文档中对这个权限的许可的描述。 <permissionGroup> 属性是可选的,仅仅用于帮助系统为用户显示权限。通常,你要设置这些,向一个标准的系统组(列在android.Manifest.permission_group 中),或者在更多的情况下要自定义。它更偏向于使用一个已经存在的组,做为简化的权限用户界面显示给用户。 注意:应该为每个权限提供标签(label) 和描述(description)。当用户浏览权限列表时,它们可以为用户展示字符资源,如(android:label) 或者一个许可的详细信息( android:description) 。标签(label)比较短,用几个词来描述该权限保护的关键功能。描述(description)应该是一组句子,用于描述获得权限的用户可以做什么。我们写描述的习惯是两句话,第一句声明权限,第二句警告用户如果应用许可该权限时,会发生什么不好的事情。 你可以在系统中通过shell 命令 adb shell pm list permissions 查看权限当前 在AndroidManifest.xml 文件中支持权限 通过 AndroidManifest.xml 文件可以设置高级权限,以限制访问系统的所有组件或者使用应用程序。所有的这些请求都包含在你所需要的组件中的 android:permission属性,命名这个权限可以控制访问此组件。 Activity 权限 (使用 <activity> 标签) 限制能够启动与 Activity 权限相关联的组件或应用程序。此权限在 Context.startActivity() 和 Activity.startActivityForResult() 期间要经过检查;如果调用者没有请求权限,那么会为调用抛出一个安全异常 ( SecurityException )。 Service 权限(应用 <service> 标签)限制启动、绑定或启动和绑定关联服务的组件或应用程序。此权限在 Context.startService(), Context.stopService() 和 Context.bindService() 期间要经过检查;如果调用者没有请求权限,那么会为调用抛出一个安全异常( SecurityException )。 BroadcastReceiver 权限(应用 <receiver> 标签)限制能够为相关联的接收者发送广播的组件或应用程序。在 Context.sendBroadcast() 返回后此权限将被检查,同时系统设法将广播递送至相关接收者。因此,权限失败将会导致抛回给调用者一个异常;它将不能递送到目的地。在相同方式下,可以使 Context.registerReceiver() 支持一个权限,使其控制能够递送广播至已登记节目接收者的组件或应用程序。其它的,当调用 Context.sendBroadcast() 以限制能够被允许接收广播的广播接收者对象一个权限(见下文)。 ContentProvider 权限(使用 <provider> 标签)用于限制能够访问 ContentProvider中的数据的组件或应用程序。(Content providers 有一个重要的附加安全设施可用于它们调用被描述后的URI 权限。) 不同于其它组件,它有两个不相连系的权限属性要设置:android:readPermission 用于限制能够读取提供器的组件或应用程序,android:writePermission 用于限制能够写入提供器的组件或应用程序。注意,如果一个提供者的读写权限受保护,意思是你只能从提供器中读,而没有写权限。当你首次收回提供者(如果你没有任何权限,将会抛出一个SecurityException 异常),那么权限要被检查,并且做为你在这个提供者上的执行操作。 使用 ContentResolver.query() 请求获取读权限; 使用 ContentResolver.insert(), ContentResolver.update() 和ContentResolver.delete() 请求获取写权限。在所有这些情况下,一个SecurityException异常从一个调用者那里抛出时不会存储请求权限结果。 发送广播时支持权限 当发送一个广播时你能总指定一个请求权限,此权限除了权限执行外,其它能发送Intent到一个已注册的BroadcastReceiver 的权限均可以。 通过调用 Context.sendBroadcast() 及一些权限字符串, 为了接收你的广播,你请求一个接收器应用程序必须持有那个权限。 注意,接收者和广播者都能够请求一个权限。当这样的事发生了,对于Intent 来说,这两个权限检查都必须通过,为了交付到共同的目的地。 其它权限支持 任意一个好的粒度权限都能够在一些调用者的一个服务中被执行。 和 Context.checkCallingPermission() method. 方法一起被完成。调用并产生一个需要的权限字符串,它将返回一个整型,以确定当前调用进程是否被许可。注意,仅仅当你执行的调用进入到其它进程的时候这些才会被使用,通常,通过IDL 接口从一个服务发布,或从一些其它方式通知其它进程。 这有许多其它有益的方式去检查权限。如果你有一个其它进程的PID,你可以使用上下文的方法 Context.checkPermission(String, int, int) 检查权限违反PID。 如果你有一个其它应用程序的包名, 你可以使用直接的包管理器方法 PackageManager.checkPermission(String, String) 去查看特定的包是否被指定的权限所许可。 URI 权限 迄今为止,在与内容提供器共同使用时,标准权限系统描述通常是不充分的。一个内容提供器要保护它自己及读和写权限,当为了它们产生作用,它的直接客户端总是需要手动的对其它应用程序指定URI。一个典型的例子是邮件应用程序中的附件。访问邮件的权限应该被保护,因为这是敏感用户数据。可以,如果一个网址图片附件提供给一个图片查看器,那个图片查看器将没有权限打开这个附件,因为它没有原因去拥有一个权限从而不能访问所有的电子邮件。 对于这个问题的解决是通过网址权限:当开始一个活动或对一个活动返回一个结果,调用者可通过设置Intent.FLAG_GRANT_READ_URI_PERMISSION 和 Intent.FLAG_GRANT_WRITE_URI_PERMISSION 中的一个或者两个。允许接收活动权限访问在Intent 中特定的数据地址,不论它有权限访问数据在内容提供器相应的Intent 中。 这种机制允许一个公用的功能性模型使用户相互交互(打开一个附件,从一个列表中选择一个联系人,等等)驱动ad-hoc 在优粒度权限的许可下。这可能是一个主要设备,应用程序为了减少这个权限而需要,仅仅直接关系到它们的行为。 这优粒度URI 权限的许可工作,然而,请求一些协作和内容提供者保持那些URI。强烈推荐内容提供者实现这种设备,并且通过android:grantUriPermissions 属性或者<grant-uri-permissions> 标签声明支持它。 更多信息可以在Context.grantUriPermission(), Context.revokeUriPermission(), 和Context.checkUriPermission() 方法中找到。 资源管理和多国版本 资源是外部文件(不含代码的文件),它被代码使用并在编译时编入应用程序。Android支持不同类型的资源文件,包括XML,PNG 以及JPEG 文件XML 文件根据描述的不同有不同格式。这份文档描述可以支持什么样的文件,语法,以及各种格式.源代码以及XML 文件将资源打包并编译进二进制文件,这种模式能使得资源更快得被加载。字符串也同样被压缩成更高效的模式。由于这些原因, Android 平台上存在不同的资源类型. 资源 Android 资源系统能跟踪所有非代码相关的应用程序。你可以使用 资源 类来访问应用程序的资源,资源的实例通常和应用程序联系在一起,你可以通过Context.getResources()来访问。 应用程序的资源在编译时就被编译到应用程序二进制代码里。为了使用某个资源,你需要将它在代码目录结构里放正确,然后编译。作为编译过程的一部分,产生的资源代号你可以在源代码里使用 -- 这允许编译器验证你的程序代码和你定义的资源是否相符。 创建资源 Android 支持字符串,图片以及很多其他类型的资源。每个对象语法、格式以及它们存储位置的支持,都是取决于不同类型的对象? 通常,你可以通过三种类型的文件来创建资源:XML 文件(除位图以及原数据文件),位图文件(对于图片)以及原始数据(其它类型,例如声音文件,等等。)。事实上,有两种不同类型的XML 文件,一种是编译到包里的,另外一种是通过aapt 来产生的资源文件,这里有一张包含所有资源类型, 文件格式,文件描述以及所有XML 文件的详细信息的列表。 在项目里,你可以在子目录res/下创建和存储资源文件。Android 有一个资源编译工具(aapt),它可以编译在这个目录下所有的子目录中的资源,这里有个各种资源的列表。你可以从 资源引用 这里看到各种类型的对象,包含其语法以及格式。 路径 资源类型res/anim/ XML 文件被编译进 逐帧动画 或 补间动画 的对象res/drawable/.png, .9.png, .jpg files 这些类型的文件被编译进下列这些图表资源列表为了获得这些资源的类型,使用Resource.getDrawable(id) · 位图文件 · 9-patches (可改变尺寸的图像) res/layout/ 可编译成屏幕布局的XML 文件 (或者屏幕的一部分). 查看 布局 res/values/ 可编译成多种类型资源的文件 注意: 不像其他 res/ 文件夹,它能容纳任何数量的文件,但只是描述其创建而不是资源本身. XML 的元素类型可以决定这些资源在R.class 里什么位置被替换 .文件可以被命名为任何名字,文件夹里有一些典型的文件(一般约定文件以定义的元素类型后面部分为文件名):: · arrays.xml 定义数组 · colors.xml 定义 颜色 和 颜色字串数值. 你可以使用 Resources.getDrawable() 以及Resources.getColor(), respectively, 取得这些资源.· dimens.xml 定义 尺寸数据 . 使用Resources.getDimension() 取得这些资源。· strings.xml 定义字符串数值 (使用Resources.getString 或Resources.getText()取得资源,(后者更好一点)getText() 能取到在用户界面上显示的文本框里的文本。· styles.xml 定义类型 对象。res/xml/ 任何XML 文件可以进行编译,并能在运行时调用Resources.getXML() 显示XML 原文件。 res/raw/ 这里的任何文件都将直接被复制到设备上。编译产品时,这些数 据不会被编译,它们被直接加入到程序包里。 为了在程序中使用这些资源,你可以调用Resources.openRawResource() , 参数为 ID: R.raw.somefilename. 资源最终会被编译成APK 文件,Android 创建一个包装类,命名为R,这样你能做你的代码里使用这些资源类。根据资源路径和文件名的不同,R 包含很多子类。 全局资源 · 一些资源类允许你定义颜色。它能接受多种网络类型的值 -- 你可以写成 #RGB, #ARGB, #RRGGBB, #AARRGGBB 这样16 进制常数都可以。 · 所有的颜色都可以设置一个阿尔法值,开始的两个16 进制数指定为透明。 0 在阿尔法值里意味着透明。当然,默认值是不透明的。 使用资源 编译时,Android 产生一个叫R 的类,它指向你程序中所有的资源。这个类包含很多子类。每一种都是Android 支持的,同时,编译后会产生一个资源文件。每个类提供一个或多个编译后资源的标识符,你可以在代码中使用。下面是个源代码的文件,里面包含了字符串,布局文件(全屏或者部分屏幕),以及图像资源。 注意: 这个R 类是自动产生的,你不能手动编写。当资源变化的时候它会自动更新。 在代码中使用资源 只要知道资源的ID 以及你编译进目标文件的资源类型就可以在代码里使用它来。下面是一些语法: R.resource_type.resource_name 或者 android.R.resource_type.resource_name resource_type 是R 子类的一种类型。 resource_name 是定义在XML 文件里的资源名或者为其他文件类型定义的资源文件(没有后缀)名。每种类型的资源会被加入到一个特定的R 的子类中;为了学习哪种R 的子类里有你编译的资源类型,参考资源引用 文档。被编译进应用程序的资源不需要包的名字就可以直接被访问到(像这样: R.resource_type.resource_name). Android 包含一些标准资源,如屏幕的类型,按钮的背景。要使用这些代码,你需要包含 android, 如 android.R.drawable.button_background. 这里有一些好的和糟糕的例子说明如何在代码里使用编译后的资源: // 从画图资源类里装载一个当前屏幕背景。 this.getWindow().setBackgroundDrawableResource(R.drawable.my_background_image); // 错误! 将一个资源ID 装入一个需要字符串的方法中 this.getWindow().setTitle(R.string.main_title); //正确!需要从资源封装类里取得标题。 this.getWindow().setTitle(Resources.getText(R.string.main_title)); // 从当前屏幕中装载布局数据。 setContentView(R.layout.main_screen); //从ViewFlipper 对象中设置动画中一帧 。 mFlipper.setInAnimation(AnimationUtils.loadAnimation(this, R.anim.hyperspace_in)); // 在TextView 对象中设置文本内容。 TextView msgTextView = (TextView)findViewByID(R.id.msg); msgTextView.setText(R.string.hello_message); 资源引用 一个在属性(或者资源)里提供的数值可以被指向一个具体的资源。这常常被使用在布局文件中用于字符串(可以被本地化) 以及图片(存在于其他文件中的),通过一个引用可以是包括颜色和整数的任何资源类型。 例如,如果有 颜色资源, 我们可以将文本的颜色值写在布局文件中,颜色值可以从资源文件里取得: <?xml version="1.0" encoding="utf-8"?> <EditText id="text" xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:textColor="@color/opaque_red" android:text="Hello, World!" /> 注意这里使用‘@’的前缀是说明资源引用 -- 后面的文本是资源的名字 @[package:]type/name. 这里我们不需要指定包,因为我们在我们自己的包里引用资源。为了指定一个系统资源,你需要这样写: <?xml version="1.0" encoding="utf-8"?> <EditText id="text" xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:textColor="@android:color/opaque_red" android:text="Hello, World!" /> 另外一个例子,当你在布局文件里使用字符串,你必须做资源引用,这样字符串才能被使用: <?xml version="1.0" encoding="utf-8"?> <EditText id="text" xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:textColor="@android:color/opaque_red" android:text="@string/hello_world" /> 这段代码也能被用来创建资源间引用。例如,我们能这样创建图像资源: <?xml version="1.0" encoding="utf-8"?> <resources> <drawable id="my_background">@android:drawable/theme2_background</drawab le> </resources> 主题属性引用 另一种资源数值允许你引用当前主题属性值。这种属性引用只能被用于特殊的资源类以及XML 属性中;它允许你根据现在主题风格将你定制的UI 变得更标准化,而不用使用大量的具体数值。 这里有个例子,我们能在布局文件中将文本颜色设置为基本系统主题中定义好的标准颜色: <?xml version="1.0" encoding="utf-8"?> <EditText id="text" xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:textColor="?android:textDisabledColor" android:text="@string/hello_world" /> 注意除来我们将前缀'?'代替了'@',其他非常像资源引用。当你使用这个标记,系统会自动查找你提供的属性的名字 -- 资源工具知道肯定会有资源属性相符合,你不需要详细指定(?android:attr/android:textDisabledColor). 使用资源标识符到主题里去寻找相应的数据而不是直接使用原数据,其语法和'@'模式是一样的: ?[namespace:]type/name 这里的type 是可选择的. 使用系统资源 许多系统资源应用程序是可以使用的。这样的资源定义在"android.R"的类里。 例如,你可以使用下面的代码在屏幕上显示一个标准的应用程序图标: public class MyActivity extends Activity { public void onStart() { requestScreenFeatures(FEATURE_BADGE_IMAGE); super.onStart(); setBadgeResource(android.R.drawable.sym_def_app_icon); } } 用相似的方法,这段代码能将你的屏幕变成系统定义的标准的“绿色背景”: public class MyActivity extends Activity { public void onStart() { super.onStart(); setTheme(android.R.style.Theme_Black); } } 对于不同的语言和设置支持不同的资源 你可以根据产品界面语言以及硬件配置设置不同的资源。注意,虽然你可以包含不同的字串,布局以及其他资源,但开发包(SDK)不会给你显式的方法去指定不同的资源去加载。Android 检测你的硬件以及位置信息选择合适的设置去加载。用户可以到设备上的设置界面去选择不同的语言。 要包含不同的资源,在同一目录下创建并行的文件夹,在每个文件夹后加上合适的名字,这个名字能表明一些配置信息(如语言,原始屏幕等等)。例如,这里的项目字符串文件一个是英文版的,另一个是法文版的: MyApp/ res/ values-en/ strings.xml values-fr/ strings.xml Android 支持不同类型的修饰语,并可以加多条在文件夹名的后面, 修饰语之间以破折号分开。例如:一个绘图资源类指定全部配置名称命名会像这样: MyApp/ res/ drawable-en-rUS-port-160dpi-finger-keysexposed-qwerty-dpad-480 x320/ 更典型的,你可以仅仅指定部分特定的配置选项。只要保证所有的数值都是按顺序排列: MyApp/ res/ drawable-en-rUS-finger/ drawable-port/ drawable-port-160dpi/ drawable-qwerty/ 修饰语 值 语言 两个小写字母 ISO 639-1。例如: en, fr, es 地区 两个大写字母加上一个小写字母'r' ISO 3166-1-alpha-2。 例如: rUS, rFR, rES 屏幕方向 port, land, square 屏幕像素 92dpi, 108dpi, 等等。 触摸屏类型 notouch, stylus, finger 键盘是否有效 keysexposed, keyshidden 基本文本输入模式 nokeys, qwerty, 12key 无触摸屏的主要导航模式 notouch, dpad, trackball, wheel 屏幕分辨率 320x240, 640x480, 等等。大分辨率需要开始指定。 这个列表不包含一些特殊的参数,如载体,商标,设备/硬件,制造商。任何应用程序需要知道的信息都在资源修饰语里有说明。 这里有一些通用的关于资源目录的命名指导: · 各个变量用破折号分开 (每个基本的目录名后跟一个破折号) · 变量大小写敏感(其大小写法必须始终一致)例如, o 一个drawable 的目录必须命名为 drawable-port, 而不是drawable-PORT。 o 你不能有两个目录命名为 drawable-port 以及 drawable-PORT, 甚至故意将"port" 和 "PORT"指为不同的参数也不可以。 · 一个式子里同一个类型修饰语中只有一个值是有效的(你不能指定像这样 drawable-rEN-rFR/) · 你可以指定多个参数去定义不同的配置,但是参数必须是上面表格里的。例如, drawable-en-rUS-land 意思在US-English 的机器里载入风景视图。 · Android 会寻找最适合当前配置的目录,这会在下面描述 · 表格里所列的参数是用来打破平衡以防止多重路径限制。 (看下面的例子) · 所有目录,无论是限制的,还是不限制的,只要在 res/ 目录下.一些目录是不能嵌套的(这样 res/drawable/drawable-en 是不可以的) · 所有的资源在被代码引用中最好都使用简单的、不加修饰的名字,如果一个资源这样命名: MyApp/res/drawable-port-92dp/myimage.png 它将这样被引用: R.drawable.myimage (code) @drawable/myimage (XML) Android 如何找到最合适的目录 Android 将会挑出哪些基本资源文件在运行时会被使用,这依靠当前的配置。 选择过程如下: 1. 删去一些和当前设备配置不符合的资源。例如,如果屏幕的像素是108dpi,这可以删除 MyApp/res/drawable-port-92dpi/. 2. MyApp/res/drawable/myimage.png 3. MyApp/res/drawable-en/myimage.png 4. MyApp/res/drawable-port/myimage.png 5. MyApp/res/drawable-port-92dpi/myimage.png 6. 挑出一些最经常符合配置的资源。例如,如果我们的地区是 en-GB, 方向是 port,那我们有两个符合配置的选项: MyApp/res/drawable-en/ 和 MyApp/res/drawable-port/. 这个目录 MyApp/res/drawable/ 可以被 删除了,因为当另外一个有一次匹配正确,而它没有。 7. MyApp/res/drawable/myimage.png 8. MyApp/res/drawable-en/myimage.png 9. MyApp/res/drawable-port/myimage.png 10. 根据配置的优先级选取最终适合的文件,它们按顺利被排列在上面的表格里。更确切得说,语言匹配比方位匹配更重要, 所以我们可以通过选择语言文件来平衡,MyApp/res/drawable-en/. 11. MyApp/res/drawable-en/myimage.png 12. MyApp/res/drawable-port/myimage.png 术语 资源系统将一系列分散内容集合在一起形成最终的完整的资源功能,去帮助我们了解整个系统。这里有一些核心概念以及组件的概要说明,你在开发中将可能使用到: 最终文件: 应用程序的独立的数据包。这包含所有从java 程序编译成的目标文件,图像(例如PNG 图片), XML 文件等等。这些文件以一种特定的方式组织在一起,在程序打包最后时,它们被打包成一个独立的ZIP 文件。 aapt: Android 最终文件打包工具。这个工具产生最终程序的ZIP 文件。除了将最终的元数据文件打包在一起,它也解析资源定义到最终的二进制数据里。 资源表:aapt 工具产生的特殊的文件,描述了所有在程序/包里的资源。这个文件可以通过资源类来访问;它不能直接和应用程序接触。 资源: 资源表里一条记录描述的是单一的命名值。大体上, 资源分成两种:基本的和包装的.资源标识符: 在资源表里所有的资源都被唯一的整数标识着。所有的代码中(资源描述,XML 文件,Java 源代码)你可以直接使用符号名代替真实的整数数值。 基本资源: 所有基本资源都可以被写成一个简单的字串,使用一定的格式可以描述资源系统里各种不同的基本类型:整数,颜色,字串,其他资源的引用,等等。像图片以及XML 描述文件这些复杂资源,被以基本字串资源储存,它们的值就是相关最终数据文件的路径。 包装资源: 有一种特殊类型的资源,不是简单的字符串,而是有一个随意的名字/数值配对列表。每个数值可以对应它本身的资源标识,每个值可以持相同类型的字符串格式的数据作为一个正常的资源。包装资源支持继承:一个包里的数据能从其他包里继承,有选择地替换或者扩展能产生你自己需要的内容。 种类: 资源种类是对于不同需求的资源标识符而言的。例如,绘制资源类常常实例化绘制类的对象,所以这些包含颜色以及指向图片或XML 文件的字符串路径数据是原始数据。其它常见资源类型是字符串(本地化字符串),颜色(基本颜色),布局(一个指向XML 文件的字串路径,它描述的是一个用户界面)以及风格(一个描述用户接口属性的包装资源)。还有一个标准的“attr”资源类型,它定义了命名包装数据以及XML 属性的资源标识符。 风格: 包含包装资源类型的名字常常用来描述一系列用户接口属性。例如,一个 TextView 的类可能会有一个描述界面风格的类来定义文本大小,颜色以及对齐方式。 在一个界面布局的XML 文件中,可以使用“风格” 属性来确定整体界面风格,它的值就是风格资源的名字。 风格类: 这里将详述一些属性资源类。其实数据不会被放在资源表本身,通常在源代码里它以常量的形式出现,这也可以使你在风格类或者XML 的标签属性里方便找到它的值。例如,Android 平台里定义了一个“视图”的风格类,它包含所有标准视图的属性: 画图区域,可视区域,背景等。这个视图被使用时,它就会借助风格类去从XML 文件取得数据并将其载入到实例中。 配置: 对许多特殊的资源标识符,根据当前的配置,可以有多种不同的值。配置包括地区(语言和国家),屏幕方向,屏幕分辨率,等等。当前的配置用来选择当资源表载入时哪个资源值生效。 主题: 一个标准类型的资源能为一个特殊的上下文提供全局的属性值。例如,当应用工程师写一个活动时,他能选择一个标准的主题去使用,白色的或者黑色的;这个类型能提供很多信息,如屏幕背景图片/颜色,默认文本颜色,按钮类型,文本编辑框类型,文本大小,等。当布置一个资源布局时,控件(文本颜色,选中后颜色,背景)的大部分设置值取自当前主题;如果需要,布局中的风格以及属性也可以从主题的属性中获得。 覆盖层: 资源表不能定义新类型的资源,但是你可以在其他表里替换资源值。就像配置值,这可以在装载时候进行;它能加入新的配置值(例如,改变字串到新的位置),替换现有值(例如,将标准的白色背景替换成"Hello Kitty"的背景图片),修改资源包(例如修改主题的字体大小。白色主题字体大小为18pt)。这实际上允许用户选择设备不同的外表,或者下载新的外表文件。 资源引用 资源引用 这份文档提供了不同类型资源的详细列表,并提供了如何在Java 代码中使用资源以及如何引用资源的描述。 国际化和本地化 即将完成: 国际化和本地化是非常关键的,但现在的SDK 还没有完全支持好。当SDK成熟时,这个章节会包含Android 平台国际化和本地化的相关信息。 那时,外部字串以及良好的结构将会使得创建和使用资源变得更省事。 |
|
cuihai |
2011-03-03 12:15 |
三、开发工具箱 Android 设计哲学 即使平台之间有很大的不同,但是如何利用API 创建应用程序的学习过程是大同小异的。一般来说,有两个步骤:首先,应该知道怎么用API 实现你的功能。其次,要了解平台间的细微差别。换句话说,首先你应该学会如何创建应用程序(了解应用程序的基本结构等),然后就要学会根据具体情况实现这个应用程序。 相比而言,第二阶段(学习使用正确的方法来实现应用程序)通常需要很长一段时间,在这个过程中你会不断地写代码,犯错误,然后从错误中吸取教训。显然,这不是一个有效的学习方法,本小节和下面的一些连接针对这向你伸出援助之手,教你怎么学习创建你的应用程序。 在此之前,先讲一个要点:成功的应用程序往往提供一个突出的用户体验。当Android团队构建了一个有着健壮核心的系统时,大多数的用户体验将来源于用户和应用程序之间的的交互。因此,我们鼓励你们花时间去构建应用程序优秀的用户体验。 显著的用户体验体现在三个核心特征上:1、快速;2、响应;3、无缝。当然,自从计算机出现以后,每一个平台都曾经有过类似的三种性质。尽管如此,每个平台实现这些特性的方式也有所不同;下面将会简单的介绍在Android 平台下面你的应用程序将如何达到这些要求。 快速(Fast) Android 程序执行应该是很快的。当然,准确来说它的程序应该执行的很有效率(有效率才会快)。在目前的计算机世界里哟这样一个倾向:假设摩尔定律能够最终解决我们所有的问题。当这种倾向遇到嵌入式应用程序的时候,摩尔定律就变得有些复杂了。 与桌面和服务应用程序不一样,摩尔定律在移动设备应用程序上面不是真正适用的。摩尔定律实际上是关于电子晶体管集成密度的,它原本的含义是:随着时间的流逝,在给定的电路尺寸上面可以集成更多的电路。对于桌面和服务应用陈旭来说,它的含义是: 你可以打包更多的“速度”在一个大小差不多的芯片上面,速度的提高,其他相应的一些性能也会显著的提高。对以像手机这样的嵌入式应用程序来说,相反的,使用摩尔定律是为了使芯片变得更小。这样,随着芯片密度的提高,相同功能的芯片会变得越来越小,功耗会越来越低,从而使手机做的越来越小,电池的持续的时间越来越长。这样就导致手持嵌入式设备相对于桌面系统来说正在以一种比较慢二实际的速度在增涨。因而,对于嵌入式设备来说,摩尔定律就意味着更多的功能和更少的功耗,速度只是次要的。这就是我们要写出高效代码的原因:你不能天真的认为电话在速度上面的增涨速度和桌面、服务应用程序是一样的。一般来说,高效的代码意味着最小的内存占用,意味着紧凑的风格,意味着避免了因为某种语言和编码习惯对性能的影响。我们用面向对象这个概念来理解,大部分这样的工作都是在方法层面上,与实际的代码,循环等等相类似。 在 “编写高效Android” 一文中,我们会对此做详细的介绍。 响应(Responsive) 我们有可能能够编写赢得世界上所有的性能测试的代码,但是用户使用起来会感到很恼火,这是因为应用程序没有足够的响应性——让人感觉反映迟钝,在关键的时刻失灵,或者处理输入太慢。在Android 平台下,那些响应性不够的应用程序会经常弹出"Application Not Responding" (ANR)这样的致命消息。 通常,这会在应用程序不能响应用户的输入的情况下发生。例如,你的应用程序在一些I/O 操作上(如网络接口调用)阻塞,这是主线程将不会处理用户的输入事件,一段时间之后应用系统就会认为你的程序挂起了,就会给一个选项给用户询问是否结束它。同样的,如果你的应用程序花费很多时间去构建内存中的一个结构、或者计算游戏的下一步,这时系统同样会认为程序已经挂起。当碰到上面情况的时候,要确保计算的高效性,但是即使是最高效的代码也需要花费时间。 在上面两个例子中,问题的解决方案是建立一个子线程来处理大部分的工作,这样就能保证你的主线程(响应用户界面事件)一直运行,这样防止系统认为你的程序已经僵化。因为这种线程的实现一般在“类”(CLASS)这个层次上,你可以把响应当成是类的问题来处理(这里,可以和方法层次描述的基本性能相比较)。 这里只是一个简单的介绍,在 “构建响应Android 应用程序” 一文中对于应用程序的响应性有详细的介绍。 无缝性(Seamless) 即使是你的应用程序执行很快,并且具有很高的响应性,它仍然有可能让用户苦恼。一个常见的例子是后台进程(比如Android 的 Service 和 BroadcastReceiver)对某些事件可能会突然弹出一个UI 响应。这似乎是无关紧要的,并且一般开发者会认为这这是正常的,因为他们花费了戴亮时间去测试和使用自己的应用程序。可是,Android 应用程序模型的构建是能够允许用户在不同的应用程序之间进行流畅的切换。这就意味着,当你的后台进程实际上弹出那个UI 的时候,用户可能正在系统的其他部分中,做一些其他的事情,如在接电话。想像一下,如果SMS 服务每次都会在文本消息传入时弹出一个对话框,这很快就会使用户崩溃。这就是为什么Android 标准对于这些事件使用的是通知(Notifications)机制;这使用户能够自己控制。 这仅仅是一个例子,相似的例子数不胜数。比如,如果Activities 没有正确的实现onPause()方法和其他生命周期方法,这将会导致数据丢失。或者如果你的应用程序有意的暴露数据给其他应用程序使用,你应该使用一个ContentProvider,而不是用一个路人皆可见的未加工过的文件或者数据库。 这些例子有一个共同的特点,他们都涉及到程序与程序或则程序与系统之间的交互。系统被设计为将多个应用程序视为一种松耦合组件的联合,而不是大块的代码黑盒。这就允许作为开发人员的你将整个系统看作是一个这些组件的大联合。这允许你干净地封装,无缝地和其他应用程序结合,因而你能设计自己喜欢的程序。 这使一种组件层次的概念(与性能和响应的类层次和方法层次相对应)。至于怎样编写无缝性能很高的代码, “与系统相结合” 一文中将会对此做出介绍,提供代码提示和最佳实例。 构建自定义组件 Android 中,你的应用程序程序与View 类组件有着一种固定的联系,例如按钮(Button)、文本框(TextView), 可编辑文本框(EditText), 列表框(ListView), 复选框(CheckBox),单选框(RadioButton), 滚动条(Gallery), 微调器(Spinner), 等等,还有一些比较先进的有着特殊用途的View 组件,例如 AutoCompleteTextView, ImageSwitcher 和TextSwitcher。除此之外,种类繁多的像 线性布局(LinearLayout), 框架布局(FrameLayout), 这样的布局组件(Layout)也被认为是View 组件,他们是从View类派生过来的。 你的应用程序就是这些控制组件和布局组件以某种方式结合显示在屏幕上,一般来说这些组件对你来说基本够用,但是你也应该知道你是可以通过类继承创建属于自己的组件,一般可以继承像View、Layouts(布局组件)这样的组件,甚至可以是一些比较高级的控制类组件。下面我们说一下为什么要继承: · 你可以为实现某种功能创建一个完全自定义风格的组件,例如用二维的图形创建控制组件实现声音的控制,就像电子控制一样。 · 你可以把几种组件结合形成一个新的组件,你的组件可能同时包含ComboBox (一个能输入的文本列表)和dual-pane selector control(左右两个List 窗口, 你可以分配窗口每一项的从属关系)等等。 · 你可以创建自己的布局组件(Layout)。SDK 中的布局组件已经提供了一系列的选项让你打造属于自己的应用程序,但是高级的开发人员会发现根据现有的 Layout 组件开发新的Layout 组件是很有必要的,甚至是完全从底层开发新的组 件。 · 你可以覆盖一个现有组件的显示或功能。例如,改变EditText(可编辑文本)组件在屏幕上的显示方式(可以参考Notepad 的例子,里面教你如何创建一个下划线的显示页面)。 · 你可以捕获像按键按下这样的事件,以一些通用的方法来处理这些事件(一个游戏的例子)。 为了实现某种目标你可能很有必要扩展一个已经存在的View 组件,下面我们结合一些例子教你如何去做。 基本方法(The Basic Approach ) 下面的一些步骤都比较概括,教你如何创建自己的组件: 1. 让你的类(Class)继承一个现有的View 类或View 的子类。 2. 重载父类的一些方法:需要重载的父类方法一般以‘on’开头,如onDraw(), onMeasure()和 onKeyDown()等等。 o 这个在Activity 或则 ListActivity 派生中同样适用,你需要重载一些生命 周期函数和一些其他功能性的HOOK 函数。 3. 使用你的继承类:一旦你的继承类创建完成,你可以在基类能够使用的地方使用你的继承类,但完成功能就是你自己编写的了。 继承类能够定义在activities 里面,这样你能够方便的调用,但是这并不是必要的(或许在你的应用程序中你希望创建一个所有人都可以使用的组件)。 完全自定义组件(Fully Customized Components) 完全自定义组件的方法可以创建一些用于显示的图形组件(graphical components),也许是一个像电压表的图形计量器,或者想卡拉OK 里面显示歌词的小球随着音乐滚动。 无论那种方式,你也不能单纯的利用组件的结合完成,无论你怎么结合这些现有的组件。 幸运的是,你可以以你自己的要求轻松地创建完全属于自己的组件,你会发现不够用的只是你的想象力、屏幕的尺寸和处理器的性能(记住你的应用程序最后只会在那些性能低于桌面电脑的平台上面运行)。 下面简单介绍如何打造完全自定义的组件: 1. 最为通用的VIEW 类的父类毫无疑问是View 类,因此,最开始你要创建一个基于此类的一个子类。 2. 你可以写一个构造函数从XML 文件中提取属性和参数,当然你也可以自己定义这些属性和参数(也许是图形计量器的颜色和尺寸,或者是指针的宽度和幅度等等) 3. 你可能有必要写自己的事件监听器,属性的访问和修改函数和一些组件本身的功能上的代码。 4. 如果你希望组件能够显示什么东西,你很有可能会重载 onMeasure() 函数,因而你就不得不重载 onDraw() 函数。当两个函数都用默认的,那么 onDraw()函数将不会做任何事情,并且默认的 onMeasure() 函数自动的设置了一个100x100 —的尺寸,这个尺寸可能并不是你想要的。 5. 其他有必要重载的on... 系列函数都需要重新写一次。 onDraw()和onMeasure() onDraw()函数将会传给你一个 Canvas 对象,通过它你可以在二维图形上做任何事情,包括其他的一些标准和通用的组件、文本的格式,任何你可以想到的东西都可以通过它实现。 注意: 这里不包括三维图形如果你想使用三维的图形,你应该把你的父类由View 改为SurfaceView 类,并且用一个单独的线程。可以参考GLSurfaceViewActivity 的例子。 onMeasure() 函数有点棘手,因为这个函数是体现组件和容器交互的关键部分,onMeasure()应该重载,让它能够有效而准确的表现它所包含部分的测量值。这就有点复杂了,因为我们不但要考虑父类的限制(通过onMeasure()传过来的),同时我们应该知道一旦测量宽度和高度出来后,就要立即调用setMeasuredDimension() 方法。 概括的来讲,执行onMeasure()函数分为一下几个阶段: 1. 重载的onMeasure()方法会被调用,高度和宽度参数同时也会涉及到 (widthMeasureSpec 和heighMeasureSpec 两个参数都是整数类型),同时你应该考虑你产品的尺寸限制。这里详细的内容可以参考View.onMeasure(int,int) (这个连接内容详细的解释了整个measurement 操作)。 2. 你的组件要通过onMeasure()计算得到必要的measurement 长度和宽度从而来显示你的组件,它应该与规格保持一致,尽管它可以实现一些规格以外的功能(在这个例子里,父类能够选择做什么,包括剪切、滑动、提交异常或者用不同的参数又一次调用onMeasure()函数)。 3. 一旦高度和宽度计算出来之后,必须调用setMeasuredDimension(int width, int height),否则就会导致异常。 一个自定义组件的例子(A Customized Component Example) 在 API Demos 中的CustomView 提供了以一个自定义组件的例子,这个自定义组件在LabelView 类中定义。 LabelView 例子涉及到了自定义组件的方方面面: · 首先让自定义组件从View 类中派生出来。 · 编写带参数的构造函数(参数可以来源于XML 文件)。这里面的一些处理都已经在View 父类中完成,但是任然有些Labelview 使用的自定义组件特有的新的参数需要处理。 · 一些标准的Public 函数,例如setText(), setTextSize(), setTextColor() · 重载onMeasure()方法来确定组件的尺寸(注意:在LabelView 中是通过一个私有函数measureWidth()来实现的) · 重载onDraw()函数把Lable 显示在提供的canvas 上。 在例子中,你可以通过custom_view_1.xml 看到自定义组件LabelView 的用法。在XML文件中特别要注意的是android:和app:两个参数的混合运用,app:参数表示应用程序中被认为是LabelView 组件的个体,这些也会作为资源在R 类中定义。 组件混合技术Compound Components (or Compound Controls) 如果你不想创建一个完全自定义的组件,而是由几个现有组件的组合产生的新的组件,那么混合组件技术就更加适合。简单的来说,这样把几个现有的组件融合到一个逻辑组合里面可以封装成一个新的组件。例如,一个Combo Box 组件可以看作是是一个EditText 和一个带有弹出列表的Button 组件的混合体。如果你点击按钮为列表选择一项, 在Android 中,其实还有其他的两个View 类可以做到类似的效果: Spinner 和 AutoCompleteTextView,,但是Combo Box 作为一个例子更容易让人理解。 下面简单的介绍如何创建组合组件: 1. 一般从Layout 类开始,创建一个Layout 类的派生类。也许在Combo box 我们会选择水平方向的LinearLayout 作为父类。记住,其他的Layout 类是可以嵌套到里面的,因此混合组件可以是任何组件的混合。注意,正如Activity 一样,你既可以使用外部XML 文件来声明你的组件,也可以嵌套在代码中。 2. 在新的混合组件的构造函数中,首先,调用所有的父类的构造函数,传入对应的参数。然后可以设置你的混合组件的其他的一些方面,在哪创建EditText 组件,又在哪创建PopupList 组件。注意:你同时也可以在XML 文件中引入一些自己的属性和参数,这些属性和参数也可以被你的混合组件所使用。 3. 你也可以创建时间监听器去监听新组件中View 类触发的事件,例如,对List 选项单击事件的监听,你必须在此时间发生后更新你EditText 的值。 4. 你可能创建自己的一些属性,带有访问和修改方法。例如,允许设置EditText 初始值并且提供访问它的方法。 5. 在Layout 的派生类中,你没有必要去重载onDraw()和onMeasure()方法,因为Layout 会有比较好的默认处理。但是,如果你觉得有必要你也可以重载它。 6. 你也可能重载一些on 系列函数,例如通过onKeyDown()的重载,你可以通过按某个键去选择列表中的对应的值。 总之,把Layout 类作为基类有下面几个优点: · 正如activity 一样,你也可以通过XML 文件去声明你的新组件,或者你也可以在代码中嵌套。 · onDraw()函数和onMeasure()函数是没有必要重载的,两个函数已经做得 很好了。 · 你可以很快的创建你的混合组件,并且可以像单一组件那样使用。 混合组件的例子(Examples of Compound Controls) In the API Demos project 在API Demos 工程中,有两个List 类的例子——Example 4 和Example 6,里面的SpeechView 组件是从LinearLayout 类派生过来,实现显示演讲显示功能,对应的原代码是List4.java 和List6.java。 调整现有组件(Tweaking an Existing Component) 在某些情况下,你可能有更简单的方法去创建你的组件。如果你应经有了一个非常类似的组件,你所要做的只是简单的从这个组件派生出你的组件,重在其中一些有必要修改的方法。通过完全自定义组件的方法你也可以同样的实现,但通过冲View 派生产生新的组件,你可以简单获取一些已经存在的处理机制,这些很可能是你所想要的,而没有必要从头开始。 例如,在SDK 中有一个NotePad 的例子(NotePad application )。该例子演示了很多Android 平台实用的细节,例如你会学到从EditView 派生出能够自动换行的记事本。这还不是一个完美的例子,因为相比早期的版本来说,这些API 已经感变了很多,但它确实说明了一些问题。 如果你还未查看该程序,现在你就可以在Eclipse 中导入记事本例程(或仅通过提供的链接查看相应的源代码)。特别是查看NoteEditor.java 中的MyEditText 的定义。 下面有几点要注意的地方: 1. 声明(The Definition) 这个类是通过下面一行代码来定义的: public static class MyEditText extends EditText o 它是定义在NoteEditor activity 类里面的,但是它是共有的 (public),因此如果有必要,它可以通过NoteEditor.MyEditText 从 NoteEditor 外面来调用。 o 它是static 类(静态类),意味着不会出现所谓的通过父类访问数据的 “虚态方法”, 这样就使该类成为一个可以不严重依赖NoteEditor 的单独 类。对于不需要从外部类访问的内联类的创建,这是一个很清晰地思路,保 证所产生的类很小,并且允许它可以被其他的类方便的调用。 o 它是EditText 类的扩展,它是我们选择的用来自定义的父类。当我们 完成以后,新的类就可以作为一个普通的EditText 来使用。 2. 类的初始化 一般来说,父类是首先调用的。进一步来说,这不是一个默认的构造函数,而是 一个带参数的构造函数。因为EditText 是使用从XML 布局文件提取出来的参 数进行创建,因此我们的构造函数也要取出参数并且将这些参数传递给父类。 3. 方法重载 在本例中,仅对onDraw()一个方法进行重载。但你可以很容易地为你的定制组 件重载其他需要的方法。 对于记事本例子来说,通过重载onDraw()方法我们可以在EidtView 的画布 (canvas)上绘制蓝色的线条(canvas 类是通过重写的onDraw()方法传递)。 该函数快要结束时要调用super.onDraw()函数。父类的方法是应该调用,但 是在这个例子里面,我们是在我们划好了蓝线之后调用的。 4. 使用定制组件 现在,我们已经有自己定制的组件了,但是应该怎样使用它呢?在记事本例子中, 定制的组件直接在预定义的布局文件中使用,让我们看一看res/layout 目录 中的note_editor.xml 文件。 <view xmlns:android="http://schemas.android.com/apk/res/android" class="com.android.notepad.NoteEditor$MyEditText" id="@+id/note" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="@android:drawable/empty" android:padding="10dip" android:scrollbars="vertical" android:fadingEdge="vertical" /> o 该自定义组件在XML 中是作为一个一般的View 类来创建的,并且是通过 全路径包来描述的。注意这里内联类是通过NoteEditor$MyEditText 来 表示的,这是Java 编程中引用内联类的标准方法。 o 在定义中的其他属性和参数将传递给定制组件的构造函数,然后才传到 EditText 构造函数中,因此这些参数也是你使用EditText 组件的参数。注意, 这里你也可以增加你自己的参数,我们将在下面讨论这个问题。 这就是你全部需要做的,诚然这是一个简单的例子。但问题的关键是:你的需求有多复杂,那么你的自定义组件就有多么复杂。 一个更为复杂的组件可能需要重载更多的on 系列函数,并且还要很多特有的函数来充分实现自定义组件的功能。唯一的限制就是你的想象力和你需要组件去执行什么工作。 现在开始你的组件化之旅吧 如你所见,Android 提供了一种精巧而又强大的组件模型,让你尽可能的完成你的工作。从简单的组件调整到组件混合,甚至完全自定义组件,灵活的运用这些技术,你应该可以得到一个完全符合你外观要求的的Android 程序 Android 平台的可选API Android 适用于各种各样的手机,从最低端直到最高端的智能手机。核心的Android API在每部手机上都可使用,但任然有一些API 接口有一些特别的适用范围:这就是所谓的“可选API”。这些API 之所以是“可选的”,主要是因为一个手持设备并不一定要完全支持这类API,甚至于完全不支持。例如,一个手持设备可能没有GPS 或Wi-FI 的硬件。在这个条件下,这类功能的API 任然存在,但不会以相同的方式来工作。例如Location API 任然在没有GPS 的设备上存在,但极有可能完全没有安装功能提供者,意味着这类API 就不能有效地使用。 你的应用应该无障碍地运行或连接在一个可能不支持你API 的设备,因为你的设备上有这些上层接口(the classes)。当然执行起来可能什么也不会做,或者抛出一个异常。 每个API 会做些什么我们可以参考这些API 的说明文档,你应该编写你的程序来适当的处理这类问题。 Wi-Fi API Wi-Fi API 为应用程序提供了一种与那些带有Wi-FI 网络接口的底层无线堆栈相互交流的手段。几乎所有的请求设备信息都是可利用的,包括网络的连接速度、IP 地址、当前状态等等,还有一些其他可用网络的信息。一些可用的交互操作包括扫描、添加、保存、结束和发起连接。 Wi-Fi API 在 android.net.wifi 包中。 定位服务(Location-Based Services) 定位服务允许软件获取手机当前的位置信息。这包括从全球定位系统卫星上获取地理位置,但相关信息不限于此。例如,未来其他定位系统可能会运营,届时,对其相应的API 接口也会加入到系统中。定位服务的API 在android.location 包中。 多媒体API(Media APIs) 多媒体API 主要用于播放媒体文件。这同时包括对音频(如播放MP3 或其他音乐文件以及游戏声音效果等)和视频(如播放从网上下载的视频)的支持,并支持"播放URI地址"(Note:URI 即是统一资源识别地址)模式-在网络上直接播放的流媒体。技术上来说,多媒体API 并不是“可选的”,因为它总是要用到。但是不同的硬件环境上面可能有不同的编解码的硬件机制,因而它又是“可选的”。 多媒体API 在 android.media 包中。 基于OpenGL 的3D 图形(3D Graphics with OpenGL) Android 的主要用户接口框架是一个典型的面向控件的类继承系统。但不要让表面的情况迷惑了你,因为在它下面是一种非常快的2D 和3D 组合的图形引擎,并且支持硬件加速。用来访问平台3D 功能的API 接口是OpenGL ES API。和多媒体API 一样,OpenGL 也不是严格意义上的“可选”,因为这些API 会总是存在并且实现那些固定的功能。但是,一些设备可能有硬件加速环节,使用它的时候就会影响你的应用程序的表现。 l 用于HTTP请求中的常用头 • Accept: text/html,image/* • Accept-Charset: ISO-8859-1 • Accept-Encoding: gzip,compress • Accept-Language: en-us,zh-cn • Host: www.it315.org:80 • If-Modified-Since: Tue, 11 Jul 2000 18:23:51 GMT • Referer: http://www.it315.org/index.jsp • User-Agent: Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 5.0) • Cookie • Connection: close/Keep-Alive • Date: Tue, 11 Jul 2000 18:23:51 GMT l HTTP请求中的常用响应头 • Location: http://www.it315.org/index.jsp • Server:apache tomcat • Content-Encoding: gzip • Content-Length: 80 • Content-Language: zh-cn • Content-Type: text/html; charset=GB2312 • Last-Modified: Tue, 11 Jul 2000 18:23:51 GMT • Refresh: 1;url=http://www.it315.org • Content-Disposition: attachment; filename=aaa.zip • Transfer-Encoding: chunked • Set-Cookie:SS=Q0=5Lb_nQ; path=/search • Expires: -1 • Cache-Control: no-cache • Pragma: no-cache • Connection: close/Keep-Alive • Date: Tue, 11 Jul 2000 18:23:51 GMT l 通用信息头指既能用于请求,又能用于响应的一些消息头。 • Cache-Control: no-cache • Pragma: no-cache • Connection: close/Keep-Alive • Date: Tue, 11 Jul 2000 18:23:51 GMT 数据的存储与访问 接口编程会对软件有影响,少用接口编程。 多用内部类,少创建类。 Androd的文件输出流:context.openFileOutput(“文件名称(不能带有路径)”,操作模式); 操作模式:context.MODE_PRIVATE:如果有新的文件内容会覆盖原来的文件内容,而且只能被创建这个文件的应用所访问。其它应用是不能被访问,因为私有了。而且默认也是这种私有的操作模式。如果文件不存在,会自动创建。如果这个文件已经存在,这个模式会把新的几个问题覆盖旧的内容。 openFileOutput()方法的第一参数用于指定文件名称,不能包含路径分隔符“/” ,如果文件不存在,Android 会自动创建它。创建的文件保存在/data/data/<package name>/files目录,如: /data/data/cn.itcast.action/files/itcast.txt ,通过点击Eclipse菜单“Window”-“Show View”-“Other”,在对话窗口中展开android文件夹,选择下面的File Explorer视图,然后在File Explorer视图中展开/data/data/<package name>/files目录就可以看到该文件。 SAX解析xml文档: //创 建SAX工厂 SAXParserFactory factory = SAXParserFactory.newInstance(); //创建解析器 SAXParser saxParser = factory.newSAXParser(); 创建一个类myDefaultHandler,继承DefaultHandler saxParser.parse(inputStream,handler); 在myDefaultHandler类中重写charactors()方法、startDocument()方法、startElement()方法、endElement()方法。 在startDocument方法中创建一个存xml文件数据的集合对象。 在startElement方法中判断开始标签是否为我们要的数据的开始标签,如果是就创建对象来存取。还要用一个变量记住所经过的标签。 在charactors方法中,判断startElement元素的标签是否为内容字符串的开始标签,如果是就可以new String();获得里面的数据。 在endElement方法中,把记住标签的变量置空。最后还要判断结束标签是否为一个完整对象的结束标签,如果是就可以把这个对象加到集合中,再把这个对象置空。 DOM解析xml文档 先用获得读取xml文件的流。 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); Document dom = builder.parser(inputStream); Pull解析xml文档 Pull解析器也是基于事件驱动解析。 思路:首先获得pull解析器,然后设置读入的流setIput(inputStream,”UTF-8”);就开始触发第一个事件。得到一个int常量,然后就是用while循环,判断是否为结束文档事件,只要不是结束文档事件,就不要结束。在这个循环中完成数据的获得。 代码如下: public static List<Person> readXML(InputStream inStream) { XmlPullParser parser = Xml.newPullParser(); try { parser.setInput(inStream, "UTF-8"); int eventType = parser.getEventType(); Person currentPerson = null; List<Person> persons = null; while (eventType != XmlPullParser.END_DOCUMENT) { switch (eventType) { case XmlPullParser.START_DOCUMENT://文档开始事件,可以进行数据初始化处理 persons = new ArrayList<Person>(); break; case XmlPullParser.START_TAG://开始元素事件 String name = parser.getName(); if (name.equalsIgnoreCase("person")) { currentPerson = new Person(); currentPerson.setId(new Integer(parser.getAttributeValue(null, "id"))); } else if (currentPerson != null) { if (name.equalsIgnoreCase("name")) { currentPerson.setName(parser.nextText());// 如果后面是Text元素,即返回它的值 } else if (name.equalsIgnoreCase("age")) { currentPerson.setAge(new Short(parser.nextText())); } } break; case XmlPullParser.END_TAG://结束元素事件 if (parser.getName().equalsIgnoreCase("person") && currentPerson != null) { persons.add(currentPerson); currentPerson = null; }break; } eventType = parser.next(); } inStream.close(); return persons; } catch (Exception e) { e.printStackTrace(); } return null; } 生成xml文件 代码如下: public static String writeXML(List<Person> persons, Writer writer){ XmlSerializer serializer = Xml.newSerializer(); try { //也可以输出要指定的SD卡文件 File file = new File(Enviroment.getExternalStorageDirectory(),”XXX.xml”); FileOutputStream out = new FileOutputStream(file); serializer.setOutput(writer); serializer.startDocument("UTF-8", true); //第一个参数为命名空间,如果不使用命名空间,可以设置为null serializer.startTag("", "persons"); for (Person person : persons){ serializer.startTag("", "person"); serializer.attribute("", "id", person.getId().toString()); serializer.startTag("", "name"); serializer.text(person.getName()); serializer.endTag("", "name"); serializer.startTag("", "age"); serializer.text(person.getAge().toString()); serializer.endTag("", "age"); serializer.endTag("", "person"); } serializer.endTag("", "persons"); serializer.endDocument(); return writer.toString(); } catch (Exception e) { e.printStackTrace(); } return null; } 第六天 第三个视频: 使用广播: 要继承广播接收者类:BroadcastReceiver,如: ,只要继承这个类,就能完成对短信的窃听。 订阅广播接收者有两种方式:其一: SmsMessage能把PDU(移动的数据格式)数据,转换成短能被阅读的信息。代码如: 短信接收权限: 广播被分为两种不同的类型:“普通广播(Normal broadcasts)”和“有序广播(Ordered broadcasts)”。前者是完全异步的,所有接收者(逻辑上)都在同一时刻运行,对消息传递的效率而言这是很好的做法,但缺点是:接收者不能传递处理结果给下一个接收者,并且无法终止广播Intent的传播;然而后者是逐个执行接收者——系统会按照接收者声明的优先级别(声明在intent-filter元素的android:priority属性中,数越大优先级别越高,取值范围:-1000到1000。也可以调用IntentFilter对象的setPriority()进行设置),按顺序逐次执行。 Context.sendBroadcast() 发送的是普通广播,所有订阅者都有机会获得并进行处理。 Context.sendOrderedBroadcast() 发送的是有序广播,系统会根据接收者声明的优先级别按顺序逐个执行接收者,前面的接收者有权终止广播,如果广播被前面的接收者终止,后面的接收者就再也无法获取到广播。对于有序广播,前面的接收者可以将处理结果存放进广播Intent,然后传给下一个接收者。 在Android中,程序的响应(Responsive)被活动管理器(Activity Manager)和窗口管理器(Window Manager)这两个系统服务所监视。当BroadcastReceiver在10秒内没有执行完毕,Android会认为该程序无响应。所以在BroadcastReceiver里不能做一些比较耗时的操作,否侧会弹出ANR(Application No Response)的对话框。如果需要完成一项比较耗时的工作,应该通过发送Intent给Service,由Service来完成。而不是使用子线程的方法来解决,因为BroadcastReceiver的生命周期很短(在onReceive() 执行后BroadcastReceiver 的实例就会被销毁),子线程可能还没有结束它就先结束了。当然如果BroadcastReceiver结束了,它的宿主进程还在运行,子线程还会继续执行。但宿主进程此时很容易在系统需要内存时被优先杀死,因为它属于空进程(没有任何活动组件的进程)。 每次广播消息到来时都会创建BroadcastReceiver实例并执行onReceive() 方法。 开发调用者跟服务进行通信的应用: 开发在线音乐播放器,与服务器进行播放和停止的操作,就可以用到。 比如:多线程上传下载。 |