Android - Service

Android中的服务与windows中的服务非常类似——没有用户操作界面、长时间在后台运行而不易被用户所知。

应用程序可以开启某些服务在后台运行,即使用户切换到其他应用程序也不会被停止。同时,应用程序可以bind一个服务与之交互,甚至可以执行IPC,例如:网络处理、音乐播放、文件输入输出或者是与内容提供者交互,都可以放在后台执行。

服务的两个运行方式

应用的组件(如Activity)可以通过下面两个方式运行服务:

  1. startService():开启服务(started)。
    即使应用被关闭,服务也不会被停止。这种开启方式,没有返回值,不能和应用交互,如下载和上传文件到网络,注意的是:服务应该在业务执行完毕后自我停止。
  2. bindService():绑定服务(bound)。
    服务被绑定后可以与应用交互数据。当应用被关闭后,服务应立即被解绑(unbind)。一个服务可以被多个应用绑定,当这些应用都解绑了服务,服务将被销毁(destoryed)。

Tips:服务可以同时被这两个方式开启。但要注意的是:如果被绑定,则服务只能在解绑后才能被停止。
一般两者混用的调用顺序为:startService()->bindService()->unbindService()->stopService()。

Service的生命周期

  1. onStartCommand():当其他组件startService()来启动服务时,系统调用这个方法。一经执行,服务开始运行在后台。当服务业务执行完毕后应调用stopSelf()自我停止或者其他组件来调用stopService()停止服务。如果这个服务只是提供内容绑定,则不应该去实现这个方法。
  2. onBind():服务通过bindService()方式启动,系统调用此方法。应该提供一个接口给应用,使之能回调服务中的方法(接口其实就服务的代理),方法应返回一个实现了该接口的IBinder对象。如果不想让应用绑定则应该返回null。
  3. onCreate():系统仅在服务第一次被创建时调用。之前会执行上面两个方法
  4. onUnbind():在被解绑时调用。如果服务可以被重新绑定(onRebind()),应返回true。
  5. onRebind():在被解绑(onUnbind()返回true)后又被重新bindService()启动,则会调用此方法。
  6. onDestory():系统会在服务许久不使用或在销毁时调用。可以实现此方法,在服务销毁前清除使用过的资源。

Tips:

  1. 服务的生命周期方法不需要像Activity的生命周期方法那样,在实现方法体中需要显式调用其父类的方法来维护生命周期。
  2. 多次startService()开启服务不会多次调用onCreate()但会多次调用onStart()
  3. 服务在所有绑定组件unbindService()解绑时会被系统销毁,需要自我停止。

UriMatcher和ContentUris

Android提供了两个用于操作Uri的工具类,分别为UriMatcher 和ContentUris 。

UriMatcher类

用于匹配Uri,它的用法如下

  • 首先第一步注册需要匹配Uri路径:

eg.

1
2
3
4
5
6
//常量UriMatcher.NO_MATCH表示不匹配任何路径的返回码
UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
//如果match()方法匹配content://net.yrom.provider.noteprovider/note路径,返回匹配码为1
sMatcher.addURI( "net.yrom.provider.noteprovider", "note", 1); //添加需要匹配 uri,如果匹配就会返回匹配码
//如果match()方法匹配content://net.yrom.provider/note/230路径,返回匹配码为2
sMatcher.addURI( "net.yrom.provider", "note/#", 2); //#号为通配符
  • 注册完需要匹配的Uri后,就可以使用sMatcher.match(uri)方法对输入的Uri进行匹配
    如果匹配就返回匹配码,匹配码是调用addURI()方法传入的第三个参数,可以针对各个匹配码进行switch-case操作:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    switch (sMatcher.match(Uri.parse( "content://net.yrom.provider.noteprovider/note/10"))) {
    case 1:
    //匹配1的操作
    break;
    case 2:
    //匹配2的操作
    break;
    default: //不匹配
    break;
    }

ContentUris类

用于处理Uri路径后面的ID部分

  • 方法withAppendedId(uri, id)用于为路径加上ID部分:

    1
    2
    Uri uri1 = Uri.parse("content://net.yrom.provider.noteprovider/note");
    Uri resultUri = ContentUris.withAppendedId (uri1, 10);

    生成后的Uri为:content://net.yrom.provider.noteprovider/note/10

  • 方法parseId(uri)方法用于从路径中获取ID部分:

    1
    2
    Uri uri2 = Uri.parse("content://net.yrom.provider.noteprovider/note/10");
    long personid = ContentUris.parseId(uri2);

    获取的结果为:10

Android - Content Provider

ContentProvider在android中的作用是对外共享应用的私有数据,也就是说可以通过ContentProvider把应用中的数据共享给其他应用访问,其他应用可以通过ContentProvider对应用中的数据进行添删改查。

关于数据共享,前面的文件操作模式中知道通过指定文件的操作模式为Context.MODE_WORLD_READABLE或Context.MODE_WORLD_WRITEABLE同样也可以对外共享数据。

那么,为何还要使用ContentProvider 对外共享数据呢?
如果采用文件操作模式对外共享数据,数据的访问方式会因数据存储的方式不同而不同,导致数据的访问方式无法统一,
如:采用xml文件对外共享数据,需要进行xml解析才能读取数据;采用sharedpreferences共享数据,需要使用sharedpreferences API读取数据等等。

使用ContentProvider对外共享数据的好处是统一了数据的访问方式

成为ContentProvider

当应用需要通过ContentProvider对外共享数据时,第一步需要继承ContentProvider并重写下面方法:

ContentProvider - PersonContentProvider.java
1
2
3
4
5
6
7
8
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 采用了authority(主机名/域名)对它进行唯一标识(详见Content URIs)

Manifest - AndroidManifest.xml
1
2
3
4
5
<manifest .... >
<application android:icon="@drawable/icon" android:label="@string/app_name">
<provider android:name=".PersonContentProvider" android:authorities="net.yrom.providers.personprovider"/>
</application>
</manifest>

Tips:Content Provider在<provider>节点中也可以声明一些访问的权限,当访问者没有申请到权限是不能访问provider的。

ContentProvider类主要方法

  • boolean onCreate()
    该方法在ContentProvider创建后就会被调用, Android开机后, ContentProvider在其它应用第一次访问它时才会被创建。
  • Uri insert(Uri uri, ContentValues values)
    该方法用于供外部应用往ContentProvider添加数据。
  • int delete(Uri uri, String selection, String[] selectionArgs)
    该方法用于供外部应用从ContentProvider删除数据。
  • int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)
    该方法用于供外部应用更新ContentProvider中的数据。
  • Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
    该方法用于供外部应用从ContentProvider中获取数据。
  • String getType(Uri uri)
    该方法用于返回当前Url所代表数据的MIME类型。

如果操作的数据属于集合类型,那么MIME类型字符串应该以vnd.android.cursor.dir/开头。
例如:要得到所有person记录的Uri为content://net.yrom.provider.personprovider/person,那么返回的MIME类型字符串应该为:“vnd.android.cursor.dir/person”。

如果要操作的数据属于非集合类型数据,那么MIME类型字符串应该以vnd.android.cursor.item/开头。
例如:得到id为10的person记录,Uri为content://net.yrom.provider.personprovider/person/10,那么返回的MIME类型字符串应该为:“vnd.android.cursor.item/person”。

Android - 控件的样式(&主题)配置

Android中的样式(Style)和CSS样式作用相似,都是用于为界面元素定义显示风格,它是一个包含一个或者多个view控件属性的集合。
如:需要定义字体的颜色和大小。

定义样式

第一步:在配置文件中定义样式

在应用的资源文件res/values/styles.xml文件中添加如下内容

style - styles.xml
1
2
3
4
5
6
7
8
<resources>
<style name="text"> <!-- 为样式定义一个全局唯一的名字-->
<item name="android:textSize">20sp</item> <!-- name属性的值为使用了该样式的View控件的属性 -->
<item name="android:textColor">#0000CC</item>
<itemname="android:layout_width">wrap_content</item>
<itemname="android:layout_height">wrap_content</item>
</style>
</resources>

第二步:在布局文件中引用样式

在layout文件中可以像下面这样使用上面的android样式:

layout - layout.xml
1
2
3
4
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" ....>
<TextView style="@style/text"
..... />
</LinearLayout>

<style>节点还可以指定parent属性。这个属性可以让当前样式完全继承一个父样式,并且可以增删改一些新的样式。
如:

1
2
3
 <style name=“text_title” parent="text"> <!-- 继承text样式-->
<item name="android:textSize">28sp</item> <!-- 修改文字大小的样式-->
</style>

Activity的主题配置

主题(Theme)的定义和样式的定义是一样的,在资源文件res/values/styles.xml中添加<style>节点:

1
<style name="AppBaseTheme" parent="android:Theme.Light"></style>

Android - 数据的存储

Android中的数据存储有以下几种方式:

  1. 文件存储
  2. SharedPreferences
  3. SQLite数据库
  4. Content Provider

文件存储

Activity中的openFileOutput()方法可以用于把数据输出到文件中,具体的实现过程与在JavaSE 环境中保存数据到文件中是一样的。
openFileInput()方法则用于读取当前应用的保存的数据

openFileOutpupt详解

openFileOutput(String name, int mode)
name - 指定文件名称,不能包含路径分隔符“/” ,如果文件不存在,Android 会自动创建它。创建的文件保存在/data/data//files目录,如: /data/data/net.yrom.xxx/files/xxx.txt
mode - 文件操作模式,即访问权限

0 或者MODE_PRIVATE 默认的模式,文件为私有的,只能本应用程序才能访问;
MODE_APPEND 添加默认,数据将追加到文件末尾;
MODE_WORLD_READABLE 全局可读;!危险
MODE_WORLD_WRITEABLE 全局可写。!危险

eg.

1
2
3
4
5
6
7
8
9
public class FileActivity extends Activity {
//...
public void save2File(byte[] data, String filename) {
//...
FileOutputStream out = this.openFileOutput(filename, Context.MODE_PRIVATE);
out.write( data);
out.close();
}
}

获得应用的文件存储路径

getFileDir() - /data/data/<当前应用包名>/files/
getCacheDir() - /data/data/<当前应用包名>/cache/
Environment.getExternalStorageDirectory() - 用于获取SDCard的目录,
注意:

  1. 写数据应在程序清单文件中加入sdcard的访问权限:
    android.permission.WRITE_EXTERNAL_STORAGE
  2. 先判断sdcard是否挂载:
    Environment.getExternalStorageState() 应返回 Environment.MEDIA_MOUNTED
    eg.获取sdcard的可用大小:
    1
    2
    3
    4
    5
    6
    7
    8
    if(Environment.getExternalStorageState().equals(Environment. MEDIA_MOUNTED)){
    File sd = Environment. getExternalStorageDirectory();
    StatFs stat = new StatFs(sd.getPath());
    long availableBlocks = stat.getAvailableBlocks();
    long blockSize = stat.getBlockSize();
    long availableSize = availableBlocks * blockSize;
    String totalAvailableSize = Formatter.formatFileSize(getApplicationContext(), availableSize);
    }

    SharedPreferences

    Android应用一般采用SharedPreferences来存储于应用相关的配置参数
    其实就是 /data/data/<package name>/shared_prefs/ 目录下的xml文件

XML Pull解析

android系统本身使用到的各种xml文件,其内部也是采用Pull解析器进行解析的。
Pull解析器的运行方式与 SAX 解析器相似。
它提供了类似的事件,如:开始元素和结束元素事件,使用parser.next()可以进入下一个元素并触发相应事件。
跟SAX不同的是, Pull解析器产生的事件是一个数字,而非方法,因此可以使用一个switch对感兴趣的事件进行处理。
当元素开始解析时,调用parser.nextText()方法可以获取下一个Text类型节点的值。

pull解析的事件类型有

  • START_DOCUMENT文档开始,getEventType()的初始值
    下面的为next()返回值
  • START_TAG ,开始标签
  • TEXT,文本标签
  • END_TAG,结束标签
  • END_DOCUMENT,文档结束

    eg.

    pullparser - XmlUtil.java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    package net.yrom.xmlparser.util;

    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.OutputStream;
    import java.util.ArrayList;
    import java.util.List;

    import net.yrom.xmlparser.bean.SmsInfo;

    import org.xmlpull.v1.XmlPullParser;
    import org.xmlpull.v1.XmlPullParserException;
    import org.xmlpull.v1.XmlSerializer;

    import android.util.Xml;

    public class XmlUtil {
    /**
    生成XML文件
    */
    public static void generateXML(File file , List<SmsInfo> smss) throws IOException{
    OutputStream out = new FileOutputStream(file);
    // 序列化器
    XmlSerializer serializer = Xml.newSerializer();
    // 初始化输出
    serializer.setOutput(out, "UTF-8");
    // 创建xml
    String namespace = null;
    serializer.startDocument("UTF-8", true);
    serializer.startTag(null, "smss");

    // 循环遍历
    for(SmsInfo sms : smss){
    serializer.startTag(namespace, "sms");
    serializer.attribute(namespace, "id", String.valueOf(sms.getId()));

    serializer.startTag(namespace, "body");
    serializer.text(sms.getBody());
    serializer.endTag(namespace, "body");

    serializer.startTag(namespace, "num");
    serializer.text(sms.getNum());
    serializer.endTag(namespace, "num");

    serializer.startTag(namespace, "type");
    serializer.text(String.valueOf(sms.getType()));
    serializer.endTag(namespace, "type");

    serializer.endTag(namespace, "sms");
    }

    serializer.endTag(namespace, "smss");
    serializer.endDocument();
    }
    /**
    解析XML文件
    */
    public static List<SmsInfo> parseXML(InputStream in,String encoding) throws Exception{
    XmlPullParser parser = Xml.newPullParser();
    parser.setInput(in, encoding);
    List<SmsInfo> smss = null;
    SmsInfo sms = null;
    int eventType = parser.getEventType();
    // EventType不为文档结束时,循环解析
    while(eventType!=XmlPullParser.END_DOCUMENT){
    switch (eventType) {
    // 开始标签
    case XmlPullParser.START_TAG:
    if("smss".equals(parser.getName())){
    smss = new ArrayList<SmsInfo>();
    } else if ("sms".equals(parser.getName())){
    sms = new SmsInfo();
    int id = Integer.parseInt(parser.getAttributeValue(0));
    sms.setId(id);
    } else if("body".equals(parser.getName())){
    String body = parser.nextText();
    sms.setBody(body);
    } else if("num".equals(parser.getName())){
    String num = parser.nextText();
    sms.setNum(num);
    } else if("type".equals(parser.getName())){
    sms.setType(Integer.parseInt(parser.nextText()));
    }
    break;
    case XmlPullParser.END_TAG:
    if("sms".equals(parser.getName())){
    smss.add(sms);
    }
    break;
    }
    eventType = parser.next();
    }
    return smss;
    }
    public static List<SmsInfo> parseXML(File file, String encoding) throws Exception{
    return parseXML(new FileInputStream(file), encoding);
    }
    }

Android - Activity

Android组件:Activity、Service、Broadcast Receiver、Content Provider

Activity

Android中的Activity有四个基本状态:

  1. Actived/Runing 一个新的Activity被启动,处于Activity任务栈栈顶,显示在屏幕最前端,此时它处于可见并可和用户交互的激活状态。
  2. Paused 被另一个透明或者 Dialog 样式的 Activity 覆盖时的状态。仍然可见,但已失去焦点,不能与用户交互
  3. Stoped 被另一个Activity覆盖、不可见、失去焦点的状态。
  4. Killed/Destoryed 被系统回收,Activity实例被销毁

这些状态之间转换都依赖于用户的操作。程序员可以决定一个Activity何时启动,但不能决定它何时被销毁。

Activity生命周期方法

在android.app.Activity类中定义了一系列生命周期相关的方法,在应用自定义的Activity中只要复写了这些方法中所需的,就可以确保被android系统调用到。
Activity生命周期

Android - Button(按钮)的响应点击事件的4种写法

Button控件setOnclickListener(View.OnClickListener listener)来接收一个点击事件的监听器

  • 自定义一个点击事件监听器类
    让其实现View.OnClickListener的onClick(View v)方法
    1
    2
    3
    4
    5
    class MyOnClickListener implements View.OnclickListener{
    public void onClick(View v){
    ...
    }
    }
    然后注册到按钮上
    1
    btn.setOnclickListener(new MyOnClickListener ());
  • 采用匿名内部类的形式
    当监听器只适用一个按钮时,可以将上面一个方法简化:
    1
    2
    3
    4
    5
    btn.setOnclickListener(new View.OnClickListener (){
    public void onClick(View v){
    ...
    }
    });
  • 将当前Activity去实现View.OnClickListener
    在Activity中实现其onClick()方法。这样可以使多个按钮复用一个监听器。
    注册监听时,只需将当前对象(this)给按钮即可:
    1
    btn.setOnclickListener(this);
    Tips:用switch-case针对各个Button的id来做相应的响应:
    eg.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public void onClick(View v){
    switch(v.getId()){
    case R.id.btn_login:
    login();
    break;
    case R.id.btn_regist:
    regist();
    break;
    default:
    doSomething();
    break;
    }
    }
  • 在布局文件中显式指定按钮的onClick属性
    对应Activity的布局文件加入形如:
    1
    2
    3
    4
    5
    <Button
    android:onClick="click"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/btn_text" />

这样按钮点击时会利用反射的方式调用对应Activity中的click()方法:

1
2
3
public void click(View v){
...
}

一个Button只能有一个onClick属性值,一个click()方法可以赋给多个Button。
在click()方法中可以针对Button的Id来做响应,参见上面的Tips。

Android - 应用源程序目录结构

/src/ java原代码存放目录
/gen/ 自动生成目录
gen目录中存放所有由Android开发工具自动生成的文件。
目录中最重要的就是R.java文件。 这个文件由Android开发工具(ADT)自动产生的。
Android开发工具会自动根据放入res目录的资源,同步更新修改R.java文件。正因为R.java文件是由开发工具自动生成的,所以我们应避免手工修改R.java。R.java在应用中起到了字典的作用,它包含了各种资源的id,通过R.java,应用可以很方便地找到对应资源。
另外编绎器也会检查R.java列表中的资源是否被使用到,没有被使用到的资源不会编绎进软件中,这样可以减少应用在手机占用的空间。
/res/ 资源(Resource)目录
在这个目录中我们可以存放应用使用到的各种资源,如xml界面文件,图片或数据。

/libs/ 支持库目录
程序开发时需要的一些三方的jar包可以放在这个目录,系统会自动把里面的jar包,添加到环境变量。
/assets/ 资源目录
Android除了提供/res目录存放资源文件外,在/assets目录也可以存放资源文件,而且/assets目录下的资源文件不会在R.java自动生成ID,所以读取/assets目录下的文件必须指定文件的路径,形如:file:///android_asset/xxx.html

/AndroidManifest.xml项目清单文件
这个文件列出了应用程序所提供的功能,以后你开发好的各种组件需要在该文件中进行配置,如果应用使用到了系统内置的应用(如电话服务、互联网服务、短信服务、GPS服务等等),你还需在该文件中声明使用权限。

/project.properties项目环境信息,一般是不需要修改