Android开发之调用系统的ContentProvider——短信的备份和恢复
最后更新于:2022-04-01 09:46:45
转载请注明出处:[http://blog.csdn.net/dmk877/article/details/50518464](http://blog.csdn.net/dmk877/article/details/50518464)
相关文章:[Android开发之内容提供者——创建自己的ContentProvider(详解)](http://blog.csdn.net/dmk877/article/details/50387741)
忍耐和坚持虽是痛苦的事情,但却能渐渐地为你带来好处.——奥维德。
可能在坚持一件事情一段时间后,我们脑海中会有很多放弃的念头,可能在放弃之后的几年后,我们会想如果当时坚持下来会怎么怎么样。。。,但是可惜的是我们没有坚持。最近比较懒,也在这里提醒自己,不要迷失自己,坚持学习。
在上一篇我们讲到了如何创建自己的ContentProvider,如果你掌握了上一篇所讲的内容,那么相信今天这一篇,你会很轻松的掌握。这一篇的主要内容就是调用谷歌工程师给我们提供好的ContentProvider,也就是说谷歌定义好一个ContentProvider后会给我们一个Uri,我们拿着这个Uri就可以得到相应的数据。如果你没调用过系统的Uri,没有关系,今天我们会通过一个案例来详细讲解怎么调用。废话不多说进入正题,如有谬误欢迎批评指正,如有疑问欢迎留言。
通过本篇博客你将学到以下知识点
①如何调用系统的ContentProvider
②如何通过谷歌给我们的Uri获得短信的数据
③一个案例将手机中的短信进行备份和恢复
**1、如何调用系统的ContentProvider**
其实阅读了上一篇文章之后,这个问题会很好的理解,谷歌工程师在将ContentProvider写好之后,肯定会给我们一个Uri,只要知道这个Uri,我们就可以拿到我们需要的数据,比方说你想获得手机短信的信息,那么必定有和其对应的Uri,你想获得图库、联系人信息,也必定有相应的Uri与之对应。知道对应的Uri后,就可以过ContentResolver这个对象,根据Uri进行数据访问。更多的内容请参考:[Android开发之内容提供者——创建自己的ContentProvider(详解)](http://blog.csdn.net/dmk877/article/details/50387741),今天的主要任务就是完成一个案例获取系统的短信数据。
**2、案例(手机短信数据的获取,以及备份和恢复)**
接下来我们就来看一个案例,这个案例的主要功能就是根据Uri获取手机短信的信息,并将其备份和恢复,它的效果图如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e41db07.jpg)
这个图片演示了这样一种功能,首先在DDMS中向模拟器中发几条短信,然后运行我们的程序,点击备份,提示备份成功后,将所有的短信删除,然后在我们的程序中点击恢复,打开短信界面发现刚才删除的短信已经恢复,这就是我们要实现的功能
首先来分析一下怎么实现上述效果,如果想备份短信,首先要做的就是获取短信的列表,这一步比较简单因为谷歌已经将其封装好,我们所要做的就是用Uri去查询短信库,就O了,然后拿到数据后需要将数据以XML的形式保存到SD卡里面,当然你用其它的方式也可以,只要能将其恢复就行。最后恢复的时候将指定路径的XML文件解析,根据Uri将解析的短信数据插入到系统的短信列表中。思路就是这样一个思路。没有看懂没关系,下面会有源码以及对它们的分析。
了解了大概思路后,另一个重要的任务就是看看短信的表结构在模拟器中它的路径是data->data->com.android.providers.telephony->databases下,如下图
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e5c6252.jpg)
将其导出然后用Sqlite数据打开可以看到数据的结构如下,这里只关心threads表和sms表就够了
threads表的结构如下
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e5d6b82.jpg)
其中
_id:用于区分不同的电话号码,系统会为不同的电话号码分配不同的_id。
date:收到信息的时间(如果收到来自同一个phone number多条信息,并且有对于一条信息未读,那么date表示收到的最后一条信息时的时间)
message_count:收到的信息的数目
read: 0. 代表未读。 1.代表 已读
对于其它字段一般很少用到,这里就不多做介绍了,
sms表的结构如下
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e5e9241.jpg)
其中
_id:用于区分不同的短信
date: 该条短信接收的时间
read: 0表未读,1表已读
body: 表示具体的短信内容
到这里准备工作还差一步就可以进行代码的书写了,哪一步呢?就是访问这个数据库的Uri,对于访问手机短信的Uri主要有以下这么几个
content://sms/ 所有短信
content://sms/inbox 收件箱
content://sms/sent 已发送
content://sms/draft 草稿
content://sms/outbox 发件箱
content://sms/failed 发送失败
content://sms/queued 待发送列表
在这个案例中我们用content://sms/,因为是备份肯定是备份所有的短信,好了,下面一起来看看代码吧。首先要做的就是根据Uri获取短信的列表,这里新建一个SmsManage类,将备份和恢复的方法放到这个类中,获取短信列表的代码如下
~~~
/**
* 获取短信列表
* @return
*/
public List<SmsData> getSmsList() {
//获取所有短信的 Uri
Uri uri = Uri. parse( "content://sms/");
//获取ContentResolver对象
ContentResolver contentResolver = mContext.getContentResolver();
//根据Uri 查询短信数据
Cursor cursor = contentResolver.query(uri, null, null, null, null);
if ( null != cursor) {
Log. i( TAG, "cursor.getCount():" + cursor.getCount());
//根据得到的Cursor一条一条的添加到smsList(短信列表)中
while (cursor.moveToNext()) {
int _id = cursor.getInt(cursor.getColumnIndex("_id" ));
int type = cursor.getInt(cursor.getColumnIndex("type" ));
String address = cursor.getString(cursor.getColumnIndex( "address"));
String body = cursor.getString(cursor.getColumnIndex("body" ));
String date = cursor.getString(cursor.getColumnIndex("date" ));
SmsData smsData = new SmsData(_id, type, address, body, date);
smsList.add(smsData);
}
cursor.close();
}
return smsList;
}
~~~
可以看到上述代码就是根据content://sms/这个Uri去查询手机中短信的数据库,得到一个Cursor这个Cursor就包含了一条一条的短信。然后去遍历这个Cursor将我们需要的数据添加到smsList中。这样短信数据就拿到了,因为我们做的功能是短信备份,所以接下来需要将smsList这个集合中的数据保存到本地,以方便短信恢复的时候去读取保存的这个文件,那么问题来了,怎样将smsList这个集合以文件的形式保存到本地呢?当然方法有很多,这里我们采用的是使用XmlSerializer将其序列化,待我们需要恢复的时候使用XmlPullParser 将其反序列化,就可以拿到备份的数据,听起来感觉挺高大上的,其实很简单就是对xml的操作。下面来看看序列化的代码即将上面得到的集合smsList中的数据生成一个xml文件,并保存到本地,代码如下
~~~
/**
* 将短信数据保存到 sd卡中
*/
public void saveSmsToSdCard(){
smsList=getSmsList();
//获得一个序列化对象
XmlSerializer xmlSerializer=Xml. newSerializer();
//将生成的 xml文件保存到sd 卡中名字为"sms.xml"
File file= new File(Environment.getExternalStorageDirectory(), "sms.xml");
FileOutputStream fos;
try {
fos = new FileOutputStream(file);
xmlSerializer.setOutput(fos, "utf-8");
xmlSerializer.startDocument( "utf-8", true);
xmlSerializer.startTag( null, "smss");
xmlSerializer.startTag( null, "count");
xmlSerializer.text( smsList.size()+ "");
xmlSerializer.endTag( null, "count");
for(SmsData smsData: smsList){
xmlSerializer.startTag( null, "sms");
xmlSerializer.startTag( null, "_id");
xmlSerializer.text(smsData.get_id()+ "");
System. out.println( "smsData.get_id()=" +smsData.get_id());
xmlSerializer.endTag( null, "_id");
xmlSerializer.startTag( null, "type");
xmlSerializer.text(smsData.getType()+ "");
System. out.println( "smsData.getType=" +smsData.getType());
xmlSerializer.endTag( null, "type");
xmlSerializer.startTag( null, "address");
xmlSerializer.text(smsData.getAddress()+ "");
System. out.println( "smsData.getAddress()=" +smsData.getAddress());
xmlSerializer.endTag( null, "address");
xmlSerializer.startTag( null, "body");
xmlSerializer.text(smsData.getBody()+ "");
System. out.println( "smsData.getBody()=" +smsData.getBody());
xmlSerializer.endTag( null, "body");
xmlSerializer.startTag( null, "date");
xmlSerializer.text(smsData.getDate()+ "");
System. out.println( "smsData.getDate()=" +smsData.getDate());
xmlSerializer.endTag( null, "date");
xmlSerializer.endTag( null, "sms");
}
xmlSerializer.endTag( null, "smss");
xmlSerializer.endDocument();
fos.flush();
fos.close();
Toast. makeText( mContext, "备份完成", Toast.LENGTH_SHORT ).show();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalStateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
~~~
通过调用以上方法就将短信以xml的形式保存到了本地。运行这个方法后可以再sd卡中看到sms.xml文件,将其导出来可以看到它的格式如下
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e608b1f.jpg)
注:实际上xml文件中它是一行这里为了让大家更清楚的看清结构,我手动将其改成上述格式了。
保存到本地后,工作就剩下最后一步了,那就是将这个xml文件反序列化,并将其中的数据一条一条插入到短信的数据库中,与其对应的代码如下
~~~
/**
* 将指定路径的 xml文件中的数据插入到短信数据库中
* @param path
*/
public void restoreSms(String path) {
File file = new File(path);
//得到一个解析 xml的对象
XmlPullParser parser = Xml. newPullParser();
try {
fis = new FileInputStream(file);
parser.setInput( fis, "utf-8");
ContentValues values = null;
int type = parser.getEventType();
while (type != XmlPullParser. END_DOCUMENT) {
switch (type) {
case XmlPullParser. START_TAG:
if ( "count".equals(parser.getName())) {
} else if ("sms" .equals(parser.getName())) {
values = new ContentValues();
} else if ("type" .equals(parser.getName())) {
values.put( "type", parser.nextText());
} else if ("address" .equals(parser.getName())) {
values.put( "address", parser.nextText());
} else if ("body" .equals(parser.getName())) {
values.put( "body", parser.nextText());
} else if ("date" .equals(parser.getName())) {
values.put( "date", parser.nextText());
}
break;
case XmlPullParser. END_TAG:
if ( "sms".equals(parser.getName())) {// 如果节点是 sms
Uri uri = Uri.parse( "content://sms/");
ContentResolver resolver = mContext.getContentResolver();
resolver.insert(uri, values);//向数据库中插入数据
System. out.println( "插入成功" );
values = null; //
}
break;
}
type=parser.next();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (XmlPullParserException e) {
e.printStackTrace();
} catch (NumberFormatException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
~~~
可以看到在上述方法中判断xml的END_TAG是不是"sms"如果是的话说明一条短信的"type"、"address"、"body"、"date"这些信息已经拿到,然后就根据Uri将这条数据插入到数据库中,就完成一条短信的恢复,待读到 *END_DOCUMENT*说明xml文件已经读完,此时所备份的短信就全部恢复了。
好了今天这篇文章就到这里了,如果发现文章中的错误,欢迎指出,如果有疑问请留言,谢谢。如果你觉的本篇文章对你有帮助,就赞一下,顶一个呗,谢谢您的支持。
[源码下载点这里](http://download.csdn.net/detail/dmk877/9404958)
转载请注明出处:[http://blog.csdn.net/dmk877/article/details/50518464](http://blog.csdn.net/dmk877/article/details/50518464)
Android开发之内容提供者——创建自己的ContentProvider(详解)
最后更新于:2022-04-01 09:46:43
转载请注明出处:[http://blog.csdn.net/dmk877/article/details/50387741](http://blog.csdn.net/dmk877/article/details/50387741)
苦心人天不负卧薪尝胆三千越甲可吞吴,有志者天不负釜底抽薪百二秦川终属楚。这是一对非常励志的名言,每当读这句话都会被震撼一下,然后接着颓废,哈哈,最近的工作比较忙,也在这里提醒自己,一定要坚持下去,一定要坚持一件对自己有益的事情。
装逼到此进入正题,今天要讨论的主要内容是ContentProvider(内容提供者),ContentProvider也是android的四大组件之一,可见其在android中的重要性,可能大家用ContentProvider比其他三个组件用的少一点,但是ContentProvider同样的非常重要,有的人知道怎么使用ContentProvider,但是对于ContentProvider的原理等,并没有搞清楚,没关系,通过本篇博客相信你会对ContentProvider有一个全新的认识。
通过本篇博客你将学到以下知识
①什么是内容提供者
②为什么会有内容提供者
③怎样使用内容提供者
④ContentProvider中的Uri的详细介绍
⑤ContentResolver讲解
⑥UriMatch用法介绍
⑦ContentObserver用法详解
⑧通过一个案例来讲解自定义ContentProvider的执行过程(下一篇将给大家带来调用系统的ContentProvider)
**1.什么是内容提供者?**
首先我们必须要明白的是ContentProvider(内容提供者)是android中的四大组件之一,但是在一般的开发中,可能使用比较少。ContentProvider为不同的软件之间数据共享,提供统一的接口。而且ContentProvider是以类似数据库中表的方式将数据暴露,也就是说ContentProvider就像一个“数据库”。那么外界获取其提供的数据,也就应该与从数据库中获取数据的操作基本一样,只不过是采用URI来表示外界需要访问的“数据库”。至于如何从URI中识别出外界需要的是哪个“数据库”这就是Android底层需要做的事情了,也就是说,如果我们想让其他的应用使用我们自己程序内的数据,就可以使用ContentProvider定义一个对外开放的接口,从而使得其他的应用可以使用我们自己应用中的文件、数据库内存储的信息。当然,自己开发的应用需要给其他应用共享信息的需求可能比较少见,但是在Android系统中,很多数据如:联系人信息、短信信息、图片库、音频库等,这些信息在开发中还是经常用到的,这些信息谷歌工程师已经帮我们封装好了,我们可以使用谷歌给我的Uri去直接访问这些数据。所以对于ContentProvider我们还是需要认真的学习的,在遇到获取联系人信息,图片库,音视频库等需求的时候,才能更好的实现功能。
**2.为什么会有内容提供者?**
当应用继承ContentProvider类,并重写该类用于提供数据和存储数据的方法,就可以向其他应用共享其数据。虽然使用其他方法也可以对外共享数据,但数据访问方式会因数据存储的方式而不同,如:采用文件方式对外共享数据,需要进行文件操作读写数据;采用sharedpreferences共享数据,需要使用sharedpreferences API读写数据。而使用ContentProvider共享数据的好处是统一了数据访问方式,这也是为什么会有内容提供者的原因。
**3.怎么使用内容提供者?**
在理解了什么是内容提供者,为什么会有内容提供者之后,我想在大家脑海中的浮现的一个问题就是怎么使用内容提供者,这也是今天我们要讨论的重点内容。在前面的博客我也说到学习这种东西的最好方法是看谷歌给出的官方文档,那么好我们先来翻译一段谷歌给出的介绍(注:这是本地的文档,我采用的是脱机工作地址(file:///D:/adt-bundle-windows-x86_64_20140101/sdk/docs/reference/android/content/ContentProvider.html))。
翻译:
内容提供者是android应用程序的基本构建块之一,它们封装数据并将封装的数据通过单一的ContentResolver接口提供给应用程序。当你需要在多个应用之间共享数据的时候就需要用到内容提供者。例如,手机中的联系人数据会被多个应用所用到所以必须要用内容提供者存储起来。如果你不需要在多个应用之间共享数据,你可以使用一个数据库,直接通过SQLite数据库。 当通过content resolver发送一个请求时,系统会检查给定的URI并把请求传给有注册授权的Contentprovider。 UriMatcher类有助于解析uri。
需要实现的主要方法是:
public boolean onCreate() 在创建ContentProvider时调用
public Cursor query(Uri, String[], String, String[], String) 用于查询指定Uri的ContentProvider,返回一个Cursor
public Uri insert(Uri, ContentValues) 用于添加数据到指定Uri的ContentProvider中,(外部应用向ContentProvider中添加数据)
public int update(Uri, ContentValues, String, String[]) 用于更新指定Uri的ContentProvider中的数据
public int delete(Uri, String, String[]) 用于从指定Uri的ContentProvider中删除数据
public String getType(Uri) 用于返回指定的Uri中的数据的MIME类型
数据访问的方法(如:insert(Uri, ContentValues) and update(Uri, ContentValues, String, String[]))可能被多个线程同时调用,此时必须是线程安全的。其他方法(如: onCreate())只能被应用的主线程调用,它应当避免冗长的操作。ContentResolver(内容解析者)请求被自动转发到合适的内容提供者实例,所以子类不需要担心跨进程调用的细节。
唉,每次最头疼的就是看全英文的文档,发现自己的英语水平太差,最近也在空闲时间学习学习英语,但是对于英语从来就没感兴趣过,希望自己可以坚持一段时间吧,其实在开发过程中遇到那些比较难解决的问题,在国外的有名的网站中基本都是可以查找到的,但是对于我这样一个英语水平差的人来说,说多了其实都是眼泪。。。,我先哭一会。。。
关于怎样使用ContentProvider后面有实例帮助大家理解。
**4.Uri详解**
在上面的翻译中如果你认真看的话你会发现在谷歌的官方文档中提到了ContentResolver(内容解析者),外界可以通过ContentResolver接口来访问ContentProvider(内容提供者)中的数据。但是在详细了解ContentResolver之前有一项工作是必须要做的,那就是先理解Uri,在谷歌文档中也有介绍,接下来我们就来详细的学习下Uri这个类
Uri 通用资源标志符(Universal Resource Identifier)Uri代表要操作的数据,Android中可用的每种资源 - 图像、视频片段等都可以用Uri来表示。Uri的结构由以下几个部分组成
scheme、authority、path、query和fragment组成。其中authority又分为host和port。它的格式根据划分的详细程度可以分为三种
如下:
~~~
[scheme:][scheme-specific-part][#fragment]
[scheme:][//authority][path][?query][#fragment]
[scheme:][//host:port][path][?query][#fragment]——最详细的划分形式
~~~
看到这里肯定有人糊里糊涂的,接着我们就来举一个例子来帮助大家详细的理解Uri这个类的结构
假如有这么一个Uri:[http://www.baidu.com:8080/](http://www.baidu.com:8080/)yourpath/fileName.html?id=15&name=du#dmk
你能将上述Uri进行提取吗?接着我们就比着标准的格式[scheme:][//host:port][path][?query][#fragment]来将这个Uri各个部分提取出来
scheme:根据标准格式可以看出这里的scheme就是Uri前面//前面的部分这里也就是**http:。**
fragment:dmk这个也是比较容易找到的,在#后面
query:id=15&name=du#dmk。从标准格式可以看到在"#"之前"?"之后的部分是query,在这里当然就是id=15&name=du#dmk了。
authority:从格式二中可以看到authority是在//后的部分,它的终点就是在path之前所以这里的authority就是[www.baidu.com:8080](http://www.baidu.com:8080/)
path:path就是?之前,主机之后的部分那就是yourpath/fileName.html
host和port:因为主机可以分为host和port所以这里的host和port分别为:[www.baidu.com](http://www.baidu.com:8080/)和8080
这里要提醒大家注意的是:在Uri中并不是上述所有的字段都必须有的除了scheme、authority是必须要有的,其它的几个path、query、fragment,它们每一个可以选择性的要或不要,但顺序不能变,比方说在上述Uri中没有path那它的格式就为:[http://www.baidu.com:8080/](http://www.baidu.com:8080/)?id=15&name=du#dmk。
在理解了Uri的格式之后,有的人可能会说Uri的各个字段能否用代码获取?答案是肯定的
这里我们同样以[http://www.baidu.com:8080/](http://www.baidu.com:8080/)yourpath/fileName.html?id=15&name=du#dmk为例
- **getScheme() :**获取Uri中的scheme字符串部分,在这里是http
- **getSchemeSpecificPart():**获取Uri中的scheme-specific-part:部分,这里是:[http://www.baidu.com:8080/](http://www.baidu.com:8080/)yourpath/fileName.html?
- **getFragment():**获取Uri中的fragment部分,即dmk
- **getAuthority():**获取Uri中Authority部分,即[www.baidu.com:8080](http://www.baidu.com:8080/)
- **getPath():**获取Uri中path部分,即/yourpath/fileName.html
- **getQuery():**获取Uri中的query部分,即id=15&name=du
- **getHost():**获取Authority中的Host字符串,即[www.baidu.com](http://www.baidu.com:8080/)
- **getPost():**获取Authority中的Port字符串,即8080
到这里关于Uri的介绍就完了(这里的关于Uri的介绍的内容主要来自:[Uri详解之——Uri结构与代码提取](http://blog.csdn.net/harvic880925/article/details/44679239) 和 [Java魔法堂:URI、URL(含URL Protocol Handler](http://www.cnblogs.com/fsjohnhuang/p/4280369.html))
**5.ContentResolver讲解**
在了解了Uri之后就可以来学习学习ContentResolver了,前面我们说到ContentProvider共享数据是通过定义一个对外开放的统一的接口来实现的。然而,应用程序并不直接调用这些方法,而是使用一个 ContentResolver 对象,调用它的方法作为替代。ContentResolver可以与任意内容提供者进行会话,与其合作来对所有相关交互通讯进行管理。当外部应用需要对ContentProvider中的数据进行添加、删除、修改和查询操作时,可以使用ContentResolver类来完成,要获取ContentResolver对象,可以使用Context提供的getContentResolver()方法。ContentResolver cr = getContentResolver();在上面我们提到ContentProvider可以向其他应用程序提供数据,与之对应的ContentResolver则负责获取ContentProvider提供的数据,修改、添加、删除更新数据等;
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://com.qstingda.provider.personprovider/contact/15”),那么将会对主机名为com.qstingda.provider.personprovider的ContentProvider进行操作,path为contact/15的数据,看到这如果你之前没有接触过ContentProvider肯定一头雾水,没有关系,这很正常,等我们把理论知识讲完后会有实例,相信你看过实例后就会很明白了。
**6.UriMatch**
UriMatcher 类主要用于匹配Uri.这里的匹配是发生在ContentProvider中的,假如我们向ContentProvider中插入一条数据,不可能为所欲为的想怎么干就怎么干,在ContentProvider肯定要做一个判断,只有在符合条件下才会去执行你想要执行的操作,这里的判断就是用UriMatch进行匹配,假如是系统的ContentProvider如联系人、图库、视频库等,这些系统都提供了Uri我们可以根据系统提供的Uri来操作相应的数据。其实UriMatch的用法非常简单,查阅谷歌官方文档你会发现有这么几个方法
①publicUriMatcher(int code) 它的作用就是创建一个UriMatch对象
②public voidaddURI([String](#) authority,[String](#) path, int code)
它的作用是在ContentProvider添加一个用于匹配的Uri,当匹配成功时返回code。Uri可以是精确的字符串,Uri中带有*表示可匹配任意text,#表示只能匹配数字。
③public int match([Uri](#) uri) 这里的Uri就是传过来的要进行验证,匹配的Uri假如传过来的是:content://com.example.test/student/#,则content://com.example.test/student/10可以匹配成功,这里的10可以使任意的数字。
**7.ContentObserver用法**
ContentObserver——内容观察者,从其名字我们可以看出它的作用就是观察,观察什么?观察指定的Uri引起的数据库的变化,然后通知主线程,根据需求做我们想要做的处理。这样说大家可能理解的不是特别透彻,这样跟大家说它可以实现类似于Adapter的notifyDataSetChanged()这个方法的作用,比方说当观察到ContentProvider的数据变化时会自动调用谷歌工程师给我们提供的好的方法,可以在此方法中通知主线程数据改变等等。那么问题来了,应该怎样实现这样的功能呢?首先要做的就是注册这个观察者,这里的注册是在需要监测ContentProvider的应用中进行注册并不是在ContentProvider中而在ContentProvider中要做的就是当数据变化时进行通知,这里的通知的方法谷歌已经帮我们写好,直接调用就行了,查看谷歌文档你会发现在ContentResolver中有这样的介绍:
public final void registerContentObserver (Uri uri, boolean notifyForDescendents, ContentObserver observer)
注册一个观察者实例,当指定的Uri发生改变时,这个实例会回调实例对象做相应处理。
参数:uri:需要观察的Uri
notifyForDescendents:如果为true表示以这个Uri为开头的所有Uri都会被匹配到,
如果为false表示精确匹配,即只会匹配这个给定的Uri。
举个例子,假如有这么几个Uri:
~~~
①content://com.example.studentProvider/student
②content://com.example.studentProvider/student/#
③content://com.example.studentProvider/student/10
④content://com.example.studentProvider/student/teacher
~~~
假如观察的Uri为content://com.example.studentProvider/student,当notifyForDescendents为true时则以这个Uri开头的Uri的数据变化时都会被捕捉到,在这里也就是①②③④的Uri的数据的变化都能被捕捉到,当notifyForDescendents为false时则只有①中Uri变化时才能被捕捉到。
看到registerContentObserver 这个方法,根据语言基础我想大家能够想到ContentResolver中的另一个方法
public final voidunregisterContentObserver([ContentObserver](#)observer)它的作用就是取消对注册的那个Uri的观察,这里传进去的就是在registerContentObserver中传递进去的ContentObserver对象。到这关于注册和解除注册的ContentObserver可能大家都比较清楚了,那么问题来了,怎么去写一个ContentObserver呢?其实它的实现很简单,直接创建一个类继承ContentObserver需要注意的是这里必须要实现它的构造方法
`public ContentObserver([Handler](#)handler)`
这里传进去的是一个Handler对象,这个Handler对象的作用一般要依赖于ContentObserver的另一个方法即
`public void onChange(boolean selfChange)`
这个方法的作用就是当指定的Uri的数据发生变化时会回调该方法,此时可以借助构造方法中的Handler对象将这个变化的消息发送给主线程,当主线程接收到这个消息之后就可以按照我们的需求来完成相应的操作,比如上面提到的类似于Adapter的notifyDataSetChanged()的作用,下面的案例也是完成了这个功能,准备工作完成之后来看一个案例,相信这个案例会让你对以上知识了解的更加深入。
**8.案例(自定义ContentProvider)**
在真正的开发中我们很少去自定义一个ContentProvider因为ContentProvider是为了更好的去共享数据,我们在开发中很少会遇到这种情况,而遇到更多的则是访问系统的ContentProvider,系统的ContentProvider谷歌工程师已经帮我们写好了,我们直接使用就可以了,这里为了让大家能够理解ContentProvider更加彻底,我们自定义一个ContentProvider然后在其它应用中来访问自定义的ContentProvider的数据这个案例的运行效果如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e2e59fe.jpg)
这里的插入数据,是在一个项目中向另一个项目中的ContentProvider中插入一条数据,其他的操作也是,接下来就来看看怎么实现上述的效果。
在上面我们提到在自定义ContentProvider时需要继承ContentProvider并实现3中所述的那几个方法(系统会自动帮你将要复写的方法罗列出来),那么我们自定义的PeopleContentProvider的代码如下
~~~
package com.example.contentproviderpractice;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
public class PeopleContentProvider extends ContentProvider {
//这里的AUTHORITY就是我们在AndroidManifest.xml中配置的authorities,这里的authorities可以随便写
private static final String AUTHORITY = "com.example.studentProvider";
//匹配成功后的匹配码
private static final int MATCH_ALL_CODE = 100;
private static final int MATCH_ONE_CODE = 101;
private static UriMatcher uriMatcher;
private SQLiteDatabase db;
private DBOpenHelper openHelper;
private Cursor cursor = null;
//数据改变后指定通知的Uri
private static final Uri NOTIFY_URI = Uri.parse("content://" + AUTHORITY + "/student");
//在静态代码块中添加要匹配的 Uri
static {
//匹配不成功返回NO_MATCH(-1)
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
/**
* uriMatcher.addURI(authority, path, code); 其中
* authority:主机名(用于唯一标示一个ContentProvider,这个需要和清单文件中的authorities属性相同)
* path:路径路径(可以用来表示我们要操作的数据,路径的构建应根据业务而定)
* code:返回值(用于匹配uri的时候,作为匹配成功的返回值)
*/
uriMatcher.addURI(AUTHORITY, "student", MATCH_ALL_CODE);// 匹配记录集合
uriMatcher.addURI(AUTHORITY, "student/#", MATCH_ONE_CODE);// 匹配单条记录
}
@Override
public boolean onCreate() {
openHelper = new DBOpenHelper(getContext());
db = openHelper.getWritableDatabase();
return false;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
switch (uriMatcher.match(uri)) {
/**
* 这里如果匹配是uriMatcher.addURI(AUTHORITY, "student",
* MATCH_SUCCESS_CODE);中的Uri,则我们可以在这里对这个ContentProvider中的数据库
* 进行删除等操作。这里如果匹配成功,我们将删除所有的数据
*/
case MATCH_ALL_CODE:
int count=db.delete("personData", null, null);
if(count>0){
notifyDataChanged();
return count;
}
break;
/**
* 这里如果匹配是uriMatcher.addURI(AUTHORITY,
* "student/#",MATCH_ONE_CODE);中的Uri,则说明我们要操作单条记录
*/
case MATCH_ONE_CODE:
// 这里可以做删除单条数据的操作。
break;
default:
throw new IllegalArgumentException("Unkwon Uri:" + uri.toString());
}
return 0;
}
@Override
public String getType(Uri uri) {
return null;
}
/**
* 插入 使用UriMatch的实例中的match方法对传过来的 Uri进行匹配。 这里通过ContentResolver传过来一个Uri,
* 用这个传过来的Uri跟在ContentProvider中静态代码块中uriMatcher.addURI加入的Uri进行匹配
* 根据匹配的是否成功会返回相应的值,在上述静态代码块中调用uriMatcher.addURI(AUTHORITY,
* "student",MATCH_CODE)这里的MATCH_CODE
* 就是匹配成功的返回值,也就是说假如返回了MATCH_CODE就表示这个Uri匹配成功了
* ,我们就可以按照我们的需求就行操作了,这里uriMatcher.addURI(AUTHORITY,
* "person/data",MATCH_CODE)加入的Uri为:
* content://com.example.studentProvider/student
* ,如果传过来的Uri跟这个Uri能够匹配成功,就会按照我们设定的步骤去执行相应的操作
*/
@Override
public Uri insert(Uri uri, ContentValues values) {
int match=uriMatcher.match(uri);
if(match!=MATCH_ALL_CODE){
throw new IllegalArgumentException("Unkwon Uri:" + uri.toString());
}
long rawId = db.insert("personData", null, values);
Uri insertUri = ContentUris.withAppendedId(uri, rawId);
if(rawId>0){
notifyDataChanged();
return insertUri;
}
return null;
}
/**
* 查询 如果uri为
* content://com.example.studentProvider/student则能匹配成功,然后我们可以按照需求执行匹配成功的操作
*/
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
switch (uriMatcher.match(uri)) {
/**
* 如果匹配成功,就根据条件查询数据并将查询出的cursor返回
*/
case MATCH_ALL_CODE:
cursor = db.query("personData", null, null, null, null, null, null);
break;
case MATCH_ONE_CODE:
// 根据条件查询一条数据。。。。
break;
default:
throw new IllegalArgumentException("Unkwon Uri:" + uri.toString());
}
return cursor;
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
switch (uriMatcher.match(uri)) {
case MATCH_ONE_CODE:
long age = ContentUris.parseId(uri);
selection = "age = ?";
selectionArgs = new String[] { String.valueOf(age) };
int count = db.update("personData", values, selection,selectionArgs);
if(count>0){
notifyDataChanged();
}
break;
case MATCH_ALL_CODE:
// 如果有需求的话,可以对整个表进行操作
break;
default:
throw new IllegalArgumentException("Unkwon Uri:" + uri.toString());
}
return 0;
}
//通知指定URI数据已改变
private void notifyDataChanged() {
getContext().getContentResolver().notifyChange(NOTIFY_URI, null);
}
}
~~~
可以看到在onCreate()方法中创建了一个数据库,关于数据库的操作大家可以看此博客[http://blog.csdn.net/dmk877/article/details/44876805](http://blog.csdn.net/dmk877/article/details/44876805)。这里就不多做介绍了。注意这个案例牵扯到两个项目,一个是包含我们自定义的ContentProvider,另一个项目是访问这个包含ContentProvider项目中的数据。
写好PeopleContentProvider之后千万不要忘了在清单文件中注册
~~~
<provider
android:name="com.example.contentproviderpractice.PeopleContentProvider"
android:authorities="com.example.student"
android:exported="true" >
</provider>
~~~
这里的authorities就是它是唯一标识内容提供者的,为内容提供者指定一个唯一的标识,这样别的应用才可以唯一获取此provider,exported的值为[flase|true]当为true时:当前提供者可以被其它应用使用。任何应用可以使用Provider通过URI 来获得它,也可以通过相应的权限来使用Provider。当为false时:当前提供者不能被其它应用使用,默认为true。注册好之后运行到手机上,此时其它的应用就可以通过ContentResolver来访问这个PeopleContentProvider了,具体怎么操作呢?我们再新建一个项目在MainActivity的代码如下:
~~~
package com.example.otherapplication;
import java.util.ArrayList;
import com.example.otherapplication.adapter.MyAdapter;
import com.example.otherapplication.bean.Student;
import com.example.otherapplication.observer.PersonOberserver;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.database.Cursor;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.ListView;
public class MainActivity extends Activity implements OnClickListener {
private ContentResolver contentResolver;
private ListView lvShowInfo;
private MyAdapter adapter;
private Button btnInit;
private Button btnInsert;
private Button btnDelete;
private Button btnUpdate;
private Button btnQuery;
private Cursor cursor;
private static final String AUTHORITY = "com.example.studentProvider";
private static final Uri STUDENT_ALL_URI = Uri.parse("content://" + AUTHORITY + "/student");
protected static final String TAG = "MainActivity";
private Handler handler=new Handler(){
public void handleMessage(android.os.Message msg) {
//在此我们可以针对数据改变后做一些操作,比方说Adapter.notifyDataSetChanged()等,根据业务需求来定。。
cursor = contentResolver.query(STUDENT_ALL_URI, null, null, null,null);
adapter.changeCursor(cursor);
};
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
lvShowInfo=(ListView) findViewById(R.id.lv_show_info);
initData();
}
private void initData() {
btnInit=(Button) findViewById(R.id.btn_init);
btnInsert=(Button) findViewById(R.id.btn_insert);
btnDelete=(Button) findViewById(R.id.btn_delete);
btnUpdate=(Button) findViewById(R.id.btn_update);
btnQuery=(Button) findViewById(R.id.btn_query);
btnInit.setOnClickListener(this);
btnInsert.setOnClickListener(this);
btnDelete.setOnClickListener(this);
btnUpdate.setOnClickListener(this);
btnQuery.setOnClickListener(this);
contentResolver = getContentResolver();
//注册内容观察者
contentResolver.registerContentObserver(STUDENT_ALL_URI,true,new PersonOberserver(handler));
adapter=new MyAdapter(MainActivity.this,cursor);
lvShowInfo.setAdapter(adapter);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
//初始化
case R.id.btn_init:
ArrayList<Student> students = new ArrayList<Student>();
Student student1 = new Student("苍老师",25,"一个会教学的好老师");
Student student2 = new Student("柳岩",26,"大方");
Student student3 = new Student("杨幂",27,"漂亮");
Student student4 = new Student("张馨予",28,"不知道怎么评价");
Student student5 = new Student("范冰冰",29,"。。。");
students.add(student1);
students.add(student2);
students.add(student3);
students.add(student4);
students.add(student5);
for (Student Student : students) {
ContentValues values = new ContentValues();
values.put("name", Student.getName());
values.put("age", Student.getAge());
values.put("introduce", Student.getIntroduce());
contentResolver.insert(STUDENT_ALL_URI, values);
}
break;
//增
case R.id.btn_insert:
Student student = new Student("小明", 26, "帅气男人");
//实例化一个ContentValues对象
ContentValues insertContentValues = new ContentValues();
insertContentValues.put("name",student.getName());
insertContentValues.put("age",student.getAge());
insertContentValues.put("introduce",student.getIntroduce());
//这里的uri和ContentValues对象经过一系列处理之后会传到ContentProvider中的insert方法中,
//在我们自定义的ContentProvider中进行匹配操作
contentResolver.insert(STUDENT_ALL_URI,insertContentValues);
break;
//删
case R.id.btn_delete:
//删除所有条目
contentResolver.delete(STUDENT_ALL_URI, null, null);
//删除_id为1的记录
Uri delUri = ContentUris.withAppendedId(STUDENT_ALL_URI,1);
contentResolver.delete(delUri, null, null);
break;
//改
case R.id.btn_update:
ContentValues contentValues = new ContentValues();
contentValues.put("introduce","性感");
//更新数据,将age=26的条目的introduce更新为"性感",原来age=26的introduce为"大方".
//生成的Uri为:content://com.example.studentProvider/student/26
Uri updateUri = ContentUris.withAppendedId(STUDENT_ALL_URI,26);
contentResolver.update(updateUri,contentValues, null, null);
break;
//查
case R.id.btn_query:
//通过ContentResolver获得一个调用ContentProvider对象
Cursor cursor = contentResolver.query(STUDENT_ALL_URI, null, null, null,null);
//CursorAdapter的用法,参考此博客:http://blog.csdn.net/dmk877/article/details/44983491
adapter=new MyAdapter(MainActivity.this,cursor);
lvShowInfo.setAdapter(adapter);
cursor = contentResolver.query(STUDENT_ALL_URI, null, null, null,null);
adapter.changeCursor(cursor);
break;
}
}
}
~~~
可以看出若想操作我们想操作的ContentProvider,必须要知道内容提供者的Uri,再正确得到Uri之后,就可以通过ContentResolver对象来操作ContentProvider中的数据了,假如你需要插入数据只需要调用contentResolver.insert(uri, contentValues);把正确的uri和ContentValues键值对传过去就行了。执行这句话系统就会根据我们提供的uri找到对应的ContentProvider,因为我们的uri中包含了authority(主机等各种信息),得到对应的ContentProvider后将调用ContentResolver的与之对应的增删改查方法,并将参数通过ContentResolver的增删改查方法传递到ContentProvider中。在上面用到了CursorAdapter关于CursorAdapter的用法可以参考此博客[http://blog.csdn.net/dmk877/article/details/44983491](http://blog.csdn.net/dmk877/article/details/44983491)
PersonObserver的代码如下
~~~
package com.example.otherapplication.observer;
import android.database.ContentObserver;
import android.os.Handler;
import android.os.Message;
public class PersonOberserver extends ContentObserver {
private Handler handler;
public PersonOberserver(Handler handler) {
super(handler);
this.handler=handler;
}
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
//向handler发送消息,更新查询记录
Message msg = new Message();
handler.sendMessage(msg);
}
}
~~~
可以看到,在构造方法中接收了Handler然后当监听到指定的Uri的数据变化时就会通过Handler消息机制发送一条消息,然后的操作就由我们自行完成了。
到这里我们来理一理整个操作的运行流程:首先有两个项目,一个是有ContentProvider的,在这个ContentProvider中初始化了一个数据库,我们的目的就是在另一个项目中来操作这个项目中ContentProvider中的数据,例如插入一条数据,查询等。对于怎么在另一个项目中操作ContentProvider中的数据,是通过ContentResolver(内容解析者)对象来操作的,假如我们要进行insert操作,那么需要调用ContentResolver的insert(uri, ContentValues);将Uri和ContentValues对象经过一系列操作传递到ContentProvider的中,然后在ContentProvider会对这个Uri进行匹配,如果匹配成功则按照我们的需求去执行相应的操作,如:插入数据、查询数据等。如果想进一步理解ContentProvider和ContentResolver之间的关系[http://blog.csdn.net/u010961631/article/details/14227421](http://blog.csdn.net/u010961631/article/details/14227421)(对这个过程从源码进行了解析,不建议初学者阅读)。下面我们来画一张图再来说一下这个过程
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e3a56fa.jpg)
从图中可以看出在OtherApplication中注册了ContentObserver之后,当Application1中的数据库发生了变化时,只需要在ContentProvider中调用ContentResolver的notifyChange(Uri,[ContentObserver]() observer),由于在OtherApplication中注册了ContentObserver(注册时用的Uri和ContentProvider中发生变化的Uri一样)因此在ContentObserver中会收到这个变化信息,它就可以将这个消息通过Handler发送给OtherApplication。
好了,本篇关于ContentProvider的介绍主要是ContentProvider的基础知识,以及自定义一个ContentProvider并操作它的执行过程,如果大家发现博客中有错误的地方,欢迎批评指正,如有疑问欢迎留言,谢谢
如果你觉着本篇博客对你有帮助,就赞一个,顶一下呗。。。。
[案例源码地址](http://download.csdn.net/detail/dmk877/9381338) (注意案例应先运行含有ContentProvider的项目。)
转载请注明出处:[http://blog.csdn.net/dmk877/article/details/50387741](http://blog.csdn.net/dmk877/article/details/50387741)
参考链接:
[http://blog.csdn.net/harvic880925/article/details/44679239](http://blog.csdn.net/harvic880925/article/details/44679239)(Uri详解)
[http://www.cnblogs.com/fsjohnhuang/p/4280369.html](http://www.cnblogs.com/fsjohnhuang/p/4280369.html)
[http://blog.csdn.net/whyrjj3/article/details/7852800](http://blog.csdn.net/whyrjj3/article/details/7852800)
[http://www.androiddesignpatterns.com/2012/06/content-resolvers-and-content-providers.html](http://www.androiddesignpatterns.com/2012/06/content-resolvers-and-content-providers.html)
[http://blog.csdn.net/liuhe688/article/details/7050868](http://blog.csdn.net/liuhe688/article/details/7050868)
[http://blog.csdn.net/qinjuning/article/details/7047607](http://blog.csdn.net/qinjuning/article/details/7047607)(ContentObserver用法详解)
[http://blog.csdn.net/u010961631/article/details/14227421](http://blog.csdn.net/u010961631/article/details/14227421)(对ContentProvider源码解析)
Android开发之低调的Service
最后更新于:2022-04-01 09:46:40
锲而舍之,朽木不折;锲而不舍,金石可镂。——荀况
今天学习了一下Service的用法就和大家一起来讨论Android中Service的相关知识点,如有谬误,欢迎批评指正,如有疑问欢迎留言。
**一、Service用途**
Service在Android中和Activity是属于同一级别上的组件,Android中的Service,其意思是“服务”,它在后台运行不可交互。Service自己不能运行,需要通过某一个Activity或者其它Context对象来调用,Context.startService()和Context.bindService()两种方式启动 Service 。 Service在Android中和Activity是属于同一级别上的组件,Android 中的Service ,其意思是“服务”,它是在后台运行,不可交互的。Service自己不能运行,需要通过某一个Activity或者其它Context对象来调用,Context.startService()和Context.bindService()两种方式启动 Service 。
Android 中的服务,它与 Activity不同,它是不能与用户交互的,不能自己启动的,运行在后台的程序(干着重的工作,却连个界面也没有,因此我说它低调),如果我们退出应用时, Service进程并没有结束,它仍然在后台运行,那我们什么时候会用到Service呢?比如我们播放音乐的时候,有可能想边听音乐边干些其他事情,当我们退出播放音乐的应用,如果不用 Service,我们就听不到歌了,所以这时候就得用到Service了,又比如当我们一个应用的数据是通过网络获取的,不同时间(一段时间)的数据是不同的,这时候我们可以用 Service在后台定时更新,而不用每打开应用的时候在去获取。如果在 Service的 onCreate或者 onStart方法中做一些很耗时的动作,最好是启动一个新线程来运行这个 Service,因为,如果 Service运行在主线程中,会影响到程序的 UI操作或者阻塞主线程中的其它事情。
**二、Service的生命周期**
首先来看官网给出的Service的生命周期图
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e2040ca.jpg)
1.onCreate() 创建Service
2.onStart(Intent intent, int startId) 启动Service
3.onStartCommand(Intent intent, **int** flags, **int** startId)启动Service
4.onDestroy() 销毁Service
5.onBind() 返回一个IBinder接口对象给Service
从这个生命周期图中我们可以看到有两种启动Service的方法Context.startService和Context.bindService,对应的生命周期也分为两种情况:
(1)startService启动模式
从图中可以看出当我们采用Context.startService(intent)这种方式时,系统会实例化一个服务,依次调用onCreate()和onStartCommand()方法,之后服务就进入了运行状态,如果服务已经运行了我们再次启动时不会重新创建服务,系统会自动调用刚才我们启动的服务,并调用其onStart()方法,如果我们想销毁一个服务可以使用stopService(intent)方法,使用stopService()方法会调用onDestroy()方法此时服务就被销毁了,
(2)bindService启动模式
在这种模式下,调用bindService(Intent service, ServiceConnection conn, int flags)来绑定一个Service,这时Service会调用自身的onCreate()方法(前提是该Service未创建),系统会实例化一个服务,接着调用onBind(intent)方法调用onBind方法后调用者就可以和服务进行交互了,当我们采用bindService这种方法创建服务时,如果已经创建好了一个如果再进行创建,系统不会创建新的Service实例,也不会调用onBind方法,这种方式启动的服务的销毁方法是使用unbindService方法,此时onUnbind方法和onDestroy方法都会被调用。
关于bindService(Intent service, ServiceConnection conn, int flags)参数的说明
参数①service: Intent 对象
参数②conn: ServiceConnection对象,实现其onServiceConnected()和onServiceDisconnected()在连接成功和断开连接时处理。后面有实例来进行说明
参数③flags:Service创建的方式,一般用Service.BIND_AUTO_CREATE表示绑定时自动创建。
可能有的人会问这两种启动模式有什么不同?
strarService和bindService的不同之处:startService模式下调用者与服务无必然联系,即使调用者结束了自己的生命周期,只要没有使用stopService方法停止这个服务,服务仍会运行;然而通常情况下,bindService模式下服务是与调用者同生共死的,在绑定结束之后,一旦调用者被销毁,服务就会终止,我们通常用一句话来形容bindService:不求同生,但求同死。
另外需要注意的是在Android2.0之前我们使用startService启动服务时都是习惯重写onStart方法,在Android2.0时系统引进了onStartCommand方法取代了onStart方法,但是为了兼容以前的程序,在onStartCommand方法中其实是调用了onStart方法,我们之后会做验证,不过我们最好还是重写onStartCommand方法。
小平同志曾经说过,实践是检验真理的唯一标准,下面我们就结合实例对上面的理论来做验证,以便加深印象
布局如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e21da87.jpg)
代码如下:
1.Service的代码
~~~
package com.example.servicepractice;
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;
public class MyService extends Service {
private static final String TAG = "MyService";
@Override
public void onCreate() {
super.onCreate();
Log. i(TAG,"onCreate called" );
}
@Override
public void onStart(Intent intent, int startId) {
super. onStart(intent, startId);
Log. i(TAG,"onStart called" );
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log. i(TAG,"onStartCommand called" );
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
super.onDestroy();
Log. i(TAG,"onDestroy called" );
}
@Override
public IBinder onBind(Intent intent) {
Log. i(TAG,"onBind called" );
return null;
}
@Override
public boolean onUnbind(Intent intent) {
Log. i(TAG,"onUnbind called" );
return super.onUnbind(intent);
}
}
~~~
2.MainActivity的代码
~~~
package com.example.servicepractice;
import android.os.Bundle;
import android.os.IBinder;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.util.Log;
import android.view.Menu;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup.LayoutParams;
import android.widget.Button;
public class MainActivity extends Activity {
protected static final String TAG = "MyService";
private Button btn_start;
private Button btn_stop;
private Button btn_bind;
private Button btn_unbind;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout. activity_main);
findViews();
setClick();
}
private void findViews() {
btn_start=(Button) findViewById(R.id. btn_start);
btn_stop=(Button) findViewById(R.id. btn_stop);
btn_bind=(Button) findViewById(R.id. btn_bind);
btn_unbind=(Button) findViewById(R.id. btn_unbind);
}
private void setClick() {
//采用startService启动服务
btn_start.setOnClickListener( new OnClickListener() {
public void onClick(View v) {
Intent intent= new Intent(MainActivity.this,MyService.class );
startService(intent);
}
});
//销毁服务
btn_stop.setOnClickListener( new OnClickListener() {
public void onClick(View v) {
Intent intent= new Intent(MainActivity.this,MyService.class );
stopService(intent);
}
});
//绑定服务
btn_bind.setOnClickListener( new OnClickListener() {
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this,MyService.class );
bindService(intent, conn,Context. BIND_AUTO_CREATE);
}
});
//解除绑定
btn_unbind.setOnClickListener( new OnClickListener() {
public void onClick(View v) {
unbindService( conn);
}
});
}
private ServiceConnection conn=new ServiceConnection() {
public void onServiceConnected(ComponentName name, IBinder service) {
//connected
Log. i(TAG,"onServiceConnection called." );
}
public void onServiceDisconnected(ComponentName name) {
}
};
}
~~~
因为服务是四大组件之一所以我们要在清单文件中配置,注意:如果启动的服务没有在清单文件中配置并不会报错(这种错误很难发现,切记要配置),只是启动时不启动这个服务,所以在开发时一定要注意养成一个好的习惯--->对于四大组件一定要声明好之后就去配置否则如果忘记了还得花好长时间去寻找错误
配置代码如下:
~~~
<service
android:name="com.example.servicepractice.MyService" >
<intent-filter>
<action android:name="com.example.service" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</service>
~~~
如果我们的服务只在本应用中调用可以去掉<intent-filter>这个属性是其他应用调用本应用中的服务时所配置的属性
从布局中我们可以看到一共有四个按钮我们首先来看前面两个按钮
1、"启动服务"和"销毁服务"学习
~~~
//采用startService启动服务
btn_start.setOnClickListener( new OnClickListener() {
public void onClick(View v) {
Intent intent= new Intent(MainActivity.this,MyService.class );
startService(intent);
}
});
//销毁服务
btn_stop.setOnClickListener( new OnClickListener() {
public void onClick(View v) {
Intent intent=new Intent(MainActivity.this,MyService. class);
stopService(intent);
}
});
~~~
首先我们点击启动按钮打印日志如下
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e22b770.jpg)
我们发现它调用的方法的顺序是onCreate->onStartCommand->onStart
然后我们接着点击启动服务按钮打印结果如下
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e23b47f.jpg)
我们发现再次启动时系统并没有重新实例化这个Service,因为系统发现这个服务已经启动了,此时它会直接调用onStartCommand方法,在onStartCommand方法中会调用onStart方法
onStartCommand方法的源码:
~~~
/* @param intent The Intent supplied to {@link android.content.Context#startService},
* as given. This may be null if the service is being restarted after
* its process has gone away, and it had previously returned anything
* except {@link #START_STICKY_COMPATIBILITY}.
* @param flags Additional data about this start request. Currently either
* 0, {@link #START_FLAG_REDELIVERY}, or {@link #START_FLAG_RETRY}.
* @param startId A unique integer representing this specific request to
* start. Use with {@link #stopSelfResult(int)}.
*
* @return The return value indicates what semantics the system should
* use for the service's current started state. It may be one of the
* constants associated with the {@link #START_CONTINUATION_MASK} bits.
*
* @see #stopSelfResult(int)
*/
public int onStartCommand(Intent intent, int flags, int startId) {
onStart(intent , startId);
return mStartCompatibility ? START_STICKY_COMPATIBILITY : START_STICKY;
}
~~~
操作完启动服务按钮后我们接着点击销毁服务按钮打印日志如下
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e24b16c.jpg)
发现当调用 stopService(intent)这个方法是会调用Service的onDestroy方法从而销毁服务。
2."绑定服务"和"解除绑定"的学习
~~~
//绑定服务
btn_bind.setOnClickListener( new OnClickListener() {
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this,MyService.class );
bindService(intent, conn,Context. BIND_AUTO_CREATE);
}
});
//解除绑定
btn_unbind.setOnClickListener( new OnClickListener() {
public void onClick(View v) {
unbindService( conn);
}
});
~~~
通过上面的 bindService(intent,conn,Context. *BIND_AUTO_CREATE*)这个方法我们发现通过需要一个ServiceConnection对象,ServiceConnection代表与服务的连接,它只有两个方法,onServiceConnected和onServiceDisconnected,前者是在操作者在连接一个服务成功时被调用,而后者是在服务崩溃或被杀死导致的连接中断时被调用,而如果我们自己解除绑定时则不会被调用,所以我们这里只研究onServiceConnected这个方法。
我们首先来看下ServiceConnection的代码
~~~
private ServiceConnection conn= new ServiceConnection() {
/**
* Called when a connection to the Service has been established, with
* the {@link android.os.IBinder} of the communication channel to the
* Service.
*
* @param name The concrete component name of the service that has
* been connected.
*
* @param service The IBinder of the Service's communication channel,
* which you can now make calls on.
*/
public void onServiceConnected(ComponentName name, IBinder service) {
//connected
Log. i( TAG,"onServiceConnection called." );
}
/**
* Called when a connection to the Service has been lost. This typically
* happens when the process hosting the service has crashed or been killed.
* This does <em>not </em> remove the ServiceConnection itself -- this
* binding to the service will remain active, and you will receive a call
* to {@link #onServiceConnected} when the Service is next running.
*
* @param name The concrete component name of the service whose
* connection has been lost.
*/
public void onServiceDisconnected(ComponentName name) {
}
};
~~~
到这我们还差一步就可以绑定服务了,因为在前面服务中的onBind方法返回值为null,这样是不行的,要想实现绑定操作,必须返回一个实现了IBinder接口类型的实例,该接口描述了与远程对象进行交互的抽象协议,有了它我们才能与服务进行交互。所以我们要修改代码
~~~
@Override
public IBinder onBind(Intent intent) {
Log. i(TAG,"onBind called" );
return new Binder(){};
}
~~~
上面的代码中返回了一个Binder的实例,而这个Binder恰恰是实现了IBinder接口,所以这样就可以实现绑定服务的操作了
首先我们点击一下”绑定服务“按钮打印日志如下
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e258e48.jpg)
我们发现onCreate方法、onBind方法和onServiceConnection方法被调用了,此时服务已经进入运行状态,如果此时我们再次点击”绑定服务“按钮,这三个方法都不会被调用然后点击”解除绑定“按钮打印日志如下
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e26aafb.jpg)
可以看到onUnbind方法和onDestroy方法被调用了,此时Service已经被销毁,整个生命周期结束。
由于bindService启动的服务是和应用绑定到一起的所以当MainActivity退出程序时,服务也会随之结束。
下面我们来说一种情况当我们点击"绑定服务"按钮之后我们点击两次“解除绑定”按钮会发现程序崩溃了,日志如下
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e279865.jpg)
还有一种情况,当我们点击“绑定服务”按钮后此时服务已经运行了,此时我们直接按“返回”键程序退出了,我们看到日志会报一个错
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e28932d.jpg)
我们要解决这两种问题,我们可以定义一个变量,然后在Activity的onDestroy方法中解除绑定就ok了,也就是做如下修改
~~~
//定义一个变量,标识这个服务是否处于绑定状态
private boolean binded;
//定义一个绑定服务的方法
private void unbindService(){
if( binded){
unbindService( conn);
binded= false;
}
}
//解除绑定
btn_unbind.setOnClickListener( new OnClickListener() {
public void onClick(View v) {
unbindService();
}
});
//在onDestroy方法中调用解除绑定的方法
protected void onDestroy() {
unbindService();
};
~~~
~~~
~~~
这样上面两种错误就解决了,也体现了我们写代码的严谨性。
3.以上两种启动方法混合使用的学习
在上面我们讨论的都是一对相匹配的启动和销毁方式,可能有的人会问,那我这四个按钮混合着点击会有什么效果呢,接着我们来验证一下
首先我们点击"启动服务"按钮打印日志如下
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e296a07.jpg)
这个和我们前面讨论的匹配没有问题,接着我们按下绑定服务按钮打印日志如下
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e2a6ab4.jpg)
调用了onBind方法接着我们点击解除绑定按钮日志如下
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e2b5d64.jpg)
此时调用了onUbind方法,值得注意的是此时服务虽然解除了但是没有终止,而是继续运行,这时我们再次点击绑定服务按钮和解除服务按钮发现onbind和onUnbind方法都不会被调用,那么是不是没有绑定成功呢?答案是虽然没有调用onBind方法但是还是绑定成功了,我们可以从如下日志验证
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e2c2de5.jpg)
但是我们怎么销毁掉这个服务呢?答案是:如果我们同时使用startService与bindService,Service终止需要unbindService与stopService同时调用,才能终止Service,不管startService与bindServicede的调用顺序,如果先调用unbindService此时服务不会自动终止,再调用stopService之后服务才会停止,如果先调用stopService此时服务也不会终止,而再调用unbindService或者之前调用bindService的context不在了(如Activity被finish的时候)之后服务才会自动停止;
总结+特别注意:
1、在调用 bindService 绑定到Service的时候,你就应当保证在某处调用 unbindService 解除绑定(尽管 Activity 被 finish 的时候绑定会自动解除,并且Service会自动停止);
2、在使用 startService 启动服务之后,一定要使用 stopService停止服务,不管你是否使用bindService;
3、同时使用 startService 与 bindService 要注意到,Service 的终止,需要unbindService与stopService同时调用,才能终止 Service,不管 startService 与 bindService 的调用顺序,如果先调用 unbindService 此时服务不会自动终止,再调用 stopService 之后服务才会停止,如果先调用 stopService 此时服务也不会终止,而再调用 unbindService 或者 之前调用 bindService 的 Context 不存在了(如Activity 被 finish 的时候)之后服务才会自动停止;
4、当在旋转手机屏幕的时候,当手机屏幕在“横”“竖”变换时,此时如果你的Activity 如果会自动旋转的话,旋转其实是 Activity 的重新创建,因此旋转之前的使用 bindService 建立的连接便会断开(Context 不存在了),对应服务的生命周期与上述相同。
5、在 sdk 2.0 及其以后的版本中,对应的 onStart 已经被否决变为了 onStartCommand,不过之前的 onStart 任然有效。这意味着,如果你开发的应用程序用的 sdk 为 2.0 及其以后的版本,那么你应当使用 onStartCommand 而不是 onStart。
6、startService 启动服务想要用startService启动服务,不管Local (本地)还是 Remote(远程) 我们需要做的工作都是一样简单。当然要记得在Androidmanifest.xml 中注册 service。
好了这一篇就到这里了,欢迎大家留言交流,批评指正。
Android开发之一起来学BroadcastReceiver
最后更新于:2022-04-01 09:46:38
业精于勤,荒于嬉;行成于思,毁于随。---韩愈
在Android开发中BroadcastReceiver的使用是非常广泛的它也是Android的四大组件之一,翻译成汉语就是:"广播接收者",那么什么是广播呢?举个日常生活的例子,在农村大队里村干部这里会有一个大喇叭,用来发送广播通知各种事情,假如由于小花超生通知小花和小明来开会村干部就会通过大喇叭说:”喂,咱庄上的人注意了,小花和小明请以最快的速度来开会"。这个广播发出之后,首先这个庄上的所有人都听到了广播,村民们根据广播的内容选择去还是不去,小花和小明听到广播后开始分析:"小花和小明去开会,我是小花,我应该去开会"小明也是如此,村民们虽然听到了广播,但是分析后发现跟我没关系啊,于是继续干自己的事情。这是我们生活中的广播的形式,那么在android中广播是怎么一回事呢?一起来揭开它的面纱吧。
Android中的BroadcastReceiver的设计不得不让人拍案叫绝,它大大减少了开发者的工作量,假如现在要实现功能检测电池的电量少于10%进入省电模式,如果没有广播,我们要设监听,注册监听器,然后处理。这样是不是很麻烦?用广播就很爽啊,当电池的电量改变时系统会发出一条广播我们只需要接收这条广播然后判断它是否低于10%就行了。这样工作量大大的减少了!这只是广播的一个小的应用,除此之外当手机的网络变化、手机开机时等系统也会发出一条广播,我们可以收到此广播做自己想要的处理
下面我们结合实例来对BroadcastReceiver做详细的阐述,如有疑问欢迎提问,如有谬误欢迎批评指正,谢谢
**一、 广播分类**
1.普通广播
普通广播是完全异步的,所谓异步就是说这个广播可以在同一时刻(逻辑上)被所有广播接收者接收到,消息传递的效率比较高,但缺点是:接收者不能将接收到的数据处理后将处理结果传递给下一个接收者,并且无法终止广播Intent的传播;
这种广播的发送方式是:Context.SendBroadcast();
2.有序广播
然而有序广播是按照接收者声明的优先级别声明方式如下
~~~
<receiver
~~~
~~~
android:name=".broadcastreceiver.FirstBroadcastReceiver">
<intent-filter android:priority="Integer" >
<action android:name="com.example.BroadcastReceiver" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
~~~
在intent-filter元素的android:priority属性中配置优先级,数越大优先级别越高,取值范围:-1000到1000。也可以调 用IntentFilter对象的setPriority()进行设置,配置好优先级后当广播过来时被接收者依次接收广播。假如现在现在有三个接收者Receiver1、Receiver2、Receiver3
如:Receiver1的级别高于Receiver2,Receiver2的级别高于Receiver3,那么当发送一个有序广播的时候,广播会先发给Receiver1,然后再发给Receiver2,最后传给Receiver3。Receiver1收到广播后可以对广播进行处理:
(1)可以往广播里存入数据,当广播传给Receiver2时,Receiver2可以从广播中得到Receiver1存入的数据。
(2)中断广播调用BroadcastReceiver.abortBroadcast()这个方法进行广播的中断,中断后Receiver2、Receiver3将收不到此条广播
这种广播的发送的方式是:Context.sendOrderedBroadcast();
按来源分(这是我自己的想的,哈哈)
1.自己定义的广播
2.系统广播就是系统里面自带的广播
**二、广播的注册方式**
广播的注册方式可以分为两种
1.静态注册
这种方式是在AndroidManifest.xml清单文件中注册的注册方式如下
~~~
<receiver android:name = ".broadcastreceiver.FirstBroadcastReceiver">
<intent-filter>
< action android:name ="com.example.BroadcastReceiver" />
< category android:name ="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
~~~
配置了以上信息之后,只要是com.example.BroadcastReceiver这个地址的广播,广播接收者都能够接收的到。注意,这种方式的注册是常驻型的,也就是说当应用关闭后,如果有广播信息传来,广播接受者也会被系统调用而自动运行。
2.动态注册
动态注册需要在代码中动态的指定广播地址并注册,通常我们是在Activity或Service(在服务中监听网络的状态,电量的变化,然后在服务中发出一个广播)注册一个广播,
注:动态注册的广播是非常驻型广播,当应用程序结束了,广播自然就没有了,比如你在activity中的onCreate或者onResume中注册广播,同时你必须在onDestory或者onPause中取消广播订阅。不然会报异常。
注册广播的代码如下
~~~
FirstBroadcastReceiver firstBroadcastReceiver= new FirstBroadcastReceiver();
IntentFilter intentFilter= new IntentFilter();
intentFilter.addAction( "com.example.BroadcastReceiver" );
registerReceiver(firstBroadcastReceiver,intentFilter);
~~~
可能有的朋友会问,为什么registerReceiver和SendBroadcastReceiver在Activity和Service中可以直接用,我们看看它们的继承关系图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e1242a3.jpg)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e13457a.jpg)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e144df7.jpg)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e15a22d.jpg)
通过这四张图看明白了吧,registerReceiver和sendBroadcast是android.content.ContextWrapper类中的方法,Activity和Service都继承了ContextWrapper,所以可以直接调用,但是如果我们在其它的类中的话是不可以直接调用的可以通过Conext.sendBroadcast的方式进行调用。
还有一点需要注意,当我们的在Activity或Service中执行了销毁操作但是没有解除注册的话会抛异常,虽然不影响程序的运行,但是作为一名有经验的开发人员,我们要保证代码的严谨性(说这话没别的意思,就是装个B,哈哈)抛出的异常提示是否忘记调用unregisterReceiver()方法了,异常如下
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e16d6c2.jpg)
所以我们要根据需要在合适的地方解除注册假如我们在onDestroy中解除注册代码如下
~~~
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver( firstBroadcastReceiver );
}
~~~
下面我们通过实例来对上面的理论进行测试,来进一步的加深印象,看看广播是怎么发出的,是怎么被接收的
**三、BroadCastReceiver实例详解**
1.关于静态和动态注册广播的讨论
首先创建BroadcastReceiver
在创建BroadcastReceiver时需要声明一个类来继承android.content.BroadcastReceiver包下的BroadcastReceiver并且复写其中的onReceive方法示例代码
~~~
package com.example.broadcastreceiverpractice.broadcastreceiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
public class FirstBroadcastReceiver extends BroadcastReceiver {
private static final String TAG = "FirstBroadcastReceiver" ;
@Override
public void onReceive(Context context, Intent intent) {
String getFromBroadcast = intent.getStringExtra("key" );
Log. i( TAG,getFromBroadcast);
}
}
~~~
在onReceive方法中我们通过获得广播传过来的数据来进行测试是否收到了我们所需要的广播,进行到这儿还有更重要的一步,那就是注册广播假如我们不注册广播的话,我们想向这个广播接收者发消息的话就无法发送,因为找不到和这个广播对应的东西,注册广播可以分为两种上面已经提到了我们再来通过实例说一遍
~~~
<receiver android:name =".broadcastreceiver.FirstBroadcastReceiver" >
<intent-filter>
<action android:name ="com.example.BroadcastReceiver" />
<category android:name ="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
~~~
配置以上内容的作用就是说从当前Receiver只接收从com.example.BroadcastReceiver发过来的广播,这样广播发送者和接收者就建立了联系了。当从com.example.BroadcastReceiver发出广播时系统就会自动调用这个广播接收者来进行相关的操作
说了这么多我们在Activity中测试一下吧
~~~
package com.example.broadcastreceiverpractice;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
public class MainActivity extends Activity {
private Button btn_send;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout. activity_main);
btn_send=(Button) findViewById(R.id. btn_send);
btn_send.setOnClickListener( new OnClickListener() {
public void onClick(View v) {
//发送一条广播com.example.BroadcastReceiver要和广播接收者在清单文件中配置的相同
Intent intent = new Intent("com.example.BroadcastReceiver" );
intent.putExtra( "key", "MainActivity中发了一条广播" );
sendBroadcast(intent);
}
});
}
}
~~~
我们点击几次发送按钮在广播接收者中打印内容如下
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e1812cc.jpg)
说明我们已经接收到了广播
(2)动态注册
在原来的基础上我们把AndroidManifest.xml清单文件中的静态的注册的代码去掉
只去掉<intent-filter></intent-filter>之间的内容即可,广播接收者是四大组件我们必须注册,大家要把广播接收者是四大组件之一的注册和为了接收自己设定的地址发过来的广播而进行的注册区分开。
~~~
package com.example.broadcastreceiverpractice;
import com.example.broadcastreceiverpractice.broadcastreceiver.FirstBroadcastReceiver;
import android.app.Activity;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
public class MainActivity extends Activity {
private Button btn_send;
private FirstBroadcastReceiver firstBroadcastReceiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout. activity_main);
firstBroadcastReceiver = new FirstBroadcastReceiver();
//声明过滤器
IntentFilter intentFilter= new IntentFilter();
intentFilter.addAction( "com.example.BroadcastReceiver" );
//将添加的动作和声明的广播接收者绑定到一起,和我们在清单文件中配置的作用是一样的
registerReceiver( firstBroadcastReceiver,intentFilter);
btn_send=(Button) findViewById(R.id. btn_send);
btn_send.setOnClickListener( new OnClickListener() {
public void onClick(View v) {
//发送一条广播com.example.BroadcastReceiver要和广播接收者在清单文件中配置的相同
Intent intent = new Intent("com.example.BroadcastReceiver" );
intent.putExtra( "key", "MainActivity中发了一条广播" );
sendBroadcast(intent);
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
//解除注册
unregisterReceiver( firstBroadcastReceiver);
}
}
~~~
我们点击几次按钮发送广播打印内容如下
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e1919dc.jpg)
关于动态注册广播的特点以及注意事项在"二"中已经说了,这里就不再重复说了唯一要提的一点就是别忘了解除注册。
但是问题来了,上面只是一个广播,假如我们我们有多个广播呢,谁先接收呢,能同时接收吗等等问题,要想弄清这个问题必须掌握普通广播和有序广播
2.有序广播和普通广播
(1)普通广播
要讨论这个问题你我们首先新建三个广播
分别如下
~~~
package com.example.broadcastreceiverpractice.broadcastreceiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
public class FirstBroadcastReceiver extends BroadcastReceiver {
private static final String TAG = "FirstBroadcastReceiver" ;
@Override
public void onReceive(Context context, Intent intent) {
String getFromBroadcast = intent.getStringExtra("key" );
Log. i( TAG,getFromBroadcast);
}
}
~~~
~~~
package com.example.broadcastreceiverpractice.broadcastreceiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
public class SecondBroadcastReceiver extends BroadcastReceiver {
private static final String TAG = "SecondBroadcastReceiver" ;
@Override
public void onReceive(Context context, Intent intent) {
String getFromBroadcast = intent.getStringExtra("key" );
Log. i( TAG,getFromBroadcast);
}
}
~~~
~~~
package com.example.broadcastreceiverpractice.broadcastreceiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
public class ThirdBroadcastReceiver extends BroadcastReceiver {
private static final String TAG = "ThirdBroadcastReceiver" ;
@Override
public void onReceive(Context context, Intent intent) {
String getFromBroadcast = intent.getStringExtra("key" );
Log. i( TAG,getFromBroadcast);
}
}
~~~
以上三个广播接收者都采用的是静态注册在清单文件中进行注册,然后在MainActivity中点击发送广播按钮打印日志如下
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e1a020e.jpg)
我们看到都收到了,我们再看下时间差那么几毫秒,这个接收顺序跟我们在清单文件中配置的顺序是一致的,假如我们把SecondBroadcastReceiver方法最上面FirstBroadcastReceiver放在第二那么打印日志如下
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e1b0c0f.jpg)
说明这三个接收者都接收到了广播。
接着我们试着来中断这个广播在每个接收者的的onReceive()方法中来中断广播添加如下代码
~~~
abortBroadcast();
~~~
然后点击发送按钮发现每个接收者都能接收到广播,说明这种广播是不能被终止的
(2)有序广播
有序广播比较特殊当我们调用Context.sendOrderedBroadcast()发送有序广播时它只发给优先级最高的那个,然后由优先级高的接收者传到优先级低的那里,并且优先级高的接收者有能力中断广播,广播被中断后优先级低的广播将收不到广播。
修改清单文件增加android:priority属性
~~~
<receiver
android:name =".broadcastreceiver.FirstBroadcastReceiver" >
<intent-filter android:priority ="10" >
<action android:name ="com.example.BroadcastReceiver" />
<category android:name ="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<receiver
android:name =".broadcastreceiver.SecondBroadcastReceiver" >
<intent-filter android:priority ="9" >
<action android:name ="com.example.BroadcastReceiver" />
<category android:name ="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<receiver
android:name =".broadcastreceiver.ThirdBroadcastReceiver" >
<intent-filter android:priority ="8" >
<action android:name ="com.example.BroadcastReceiver" />
<category android:name ="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
~~~
在上面一中的2提到当优先级高的广播收到广播后一般做两种处理:(1)是把广播中断。(2)接收广播中的数据并按照需求对此数据做更改首先我们来测一下中断广播,我们修改MainActivity的发送广播的方式
~~~
//发送一条广播com.example.BroadcastReceiver要和广播接收者在清单文件中配置的相同
Intent intent =new Intent("com.example.BroadcastReceiver" );
intent.putExtra("key", "MainActivity中发了一条广播" );
sendOrderedBroadcast(intent,null);
~~~
注意,使用sendOrderedBroadcast方法发送有序广播时,需要一个权限参数,如果为null则表示不要求接收者声明指定的权限,如果不为null,则表示接收者若要接收此广播,需声明指定权限。这样做是从安全角度考虑的,例如系统的短信就是有序广播的形式,一个应用可能是具有拦截垃圾短信的功能,当短信到来时它可以先接受到短信广播,必要时终止广播传递,这样的软件就必须声明接收短信的权限。
所以我们在AndroidMainfest.xml中定义一个权限:
~~~
<permission android:protectionLevel="normal"
android:name="com.tsingda.broadcast_permission"/>
~~~
然后添加此权限
~~~
<uses-permission android:name="com.tsingda.broadcast_permission" />
~~~
然后在SecondBroadcastReceiver 中调用
~~~
abortBroadcast();
~~~
这个方法然后点击发送按钮打印日志如下
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e1c4d9b.jpg)
发现ThirdBroadcastReceiver没有接收到广播,说明中断广播成功。
然后再来看更改广播中的数据,在讲解更改之前我们需要了解setResultExtras(Bundle)这个方法,在进行广播的传输时优先接收到Broadcast的接收者可以通过setResultExtras(Bundle)方法将处理结果存入Broadcast中,然后传给下一个接收者,下一个接收者通过Bundle bundle = new getResultExtras(true)可以获取上一个接收者存入的数据。
setResultExtras(Bundle)方法的详细介绍如下图
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e1d42dd.jpg)
我们把三个接收者做如下更改
~~~
package com.example.broadcastreceiverpractice.broadcastreceiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
public class FirstBroadcastReceiver extends BroadcastReceiver {
private static final String TAG = "FirstBroadcastReceiver" ;
@Override
public void onReceive(Context context, Intent intent) {
String getFromBroadcast = intent.getStringExtra("key" );
Log. i( TAG,getFromBroadcast);
Bundle bundle = new Bundle();
bundle.putString( "key", getFromBroadcast+ "@First");
setResultExtras(bundle);
}
}
~~~
~~~
package com.example.broadcastreceiverpractice.broadcastreceiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
public class SecondBroadcastReceiver extends BroadcastReceiver {
private static final String TAG = "SecondBroadcastReceiver" ;
@Override
public void onReceive(Context context, Intent intent) {
String getFromBroadcast = getResultExtras(true ).getString("key" );
Log. i( TAG,getFromBroadcast);
Bundle bundle = new Bundle();
bundle.putString( "key", getFromBroadcast+ "@Second");
setResultExtras(bundle);
}
}
~~~
~~~
package com.example.broadcastreceiverpractice.broadcastreceiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
public class ThirdBroadcastReceiver extends BroadcastReceiver {
private static final String TAG = "ThirdBroadcastReceiver" ;
@Override
public void onReceive(Context context, Intent intent) {
String getFromBroadcast = getResultExtras(true ).getString("key" );
Log. i( TAG,getFromBroadcast);
}
}
~~~
点击发送广播按钮打印日志如下
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e1e684c.jpg)
说明我们接收到了广播并进行了修改然后传到了下一级。好了关于广播接收者的讨论就到这了,如果错误欢迎批评指正,谢谢
Android开发之Activity的启动模式
最后更新于:2022-04-01 09:46:36
黑发不知勤学早,白首方悔读书迟。——《劝学》
今天花了整个下午+晚上的的时间学习了Activity的启动模式,本来以为这个知识点很简单,但是在学习的过程中发现,Activity的启动模式并没有自己想象的那么简单,下面我们一起来看看这Activity的四种启动模式吧,如有疑问欢迎留言,如有谬误欢迎大家批评指正,谢谢
Activity的启动模式共有四种
1.standard
2.singleTop
3.singleTask
4.singleInstance
如图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500df49edc.jpg)
LaunchMode在多个Activity跳转的过程中扮演着重要的角色,它可以决定是否生成新的Activity实例,是否重用已存在的Activity实例,是否和其他Activity实例公用一个task里。这里简单介绍一下task的概念,task是一个具有栈结构的对象,一个task可以管理多个Activity,启动一个应用,也就创建一个与之对应的task。
下面我们就依次来说说这几种启动模式
1.standard
standard模式是Activity默认的启动模式,当我们在没有配置activity的launchMode时它就会按照standard方式去启动,
下面通过一个实例来解释下这种启动模式
FirstActivity代码如下:
~~~
package com.example.activitylauchmodepractice;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
public class FirstActivity extends Activity {
private Button btn_jumpToSecondActivity;
private TextView tv_showViewClass;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_first);
tv_showViewClass=(TextView) findViewById(R.id.tv_showViewClass);
tv_showViewClass.setText(FirstActivity.this.toString());
btn_jumpToSecondActivity=(Button) findViewById(R.id.btn_jumpToSecondActivity);
btn_jumpToSecondActivity.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent=new Intent(FirstActivity.this,FirstActivity.class);
startActivity(intent);
}
});
}
}
~~~
启动后的界面
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500df5aaa9.jpg)
此时它所对应的任务栈如下
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500df694ec.jpg)
在此基础上我们点击按钮再次启动Activity此时的界面如下
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500df7a352.jpg)
此时的任务栈变化过程如下
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500df8fc8e.jpg)
我们再次点击按钮跳转到FirstActivity界面如下
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500dfa439d.jpg)
此时的任务栈的变化过程如下
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500dfb105b.jpg)
好了到这我们就可以分析一下了,在上述过程中我们点击了三次按钮它实例化了三个FirstActivity
这就是standard模式的特点:不管任务栈中有没有实例存在它都会实例化一个Activity
当我们点击返回按钮时它会依次把最上面的Activity出栈,上面的过程中一共实例化了三个Activity因此我们需要点击三次返回按钮应用才能退出。
2.singleTop
还用上面那个例子,此时我们给FirstActivity的属性指定为:android:launchMode="singleTop"
启动后的界面
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500dfc13ac.jpg)
此时的任务栈如下
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500dfd1941.jpg)
我们接着点击按钮发现无论点击几次界面都没变说明它只实例化一次,此时的任务站始终是一个Activity此时点击一次返回键便可退出应用。
这是只有一个Activity的情况,下面我们说说多个Activity的情况
再来一个SecondActivity代码如下:
~~~
package com.example.activitylauchmodepractice;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
public class SecondActivity extends Activity {
private Button btn_jumpToFirstActivity_;
private TextView tv_showViewClass;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
tv_showViewClass=(TextView) findViewById(R.id.tv_showViewClass);
tv_showViewClass.setText(SecondActivity.this.toString());
btn_jumpToFirstActivity_=(Button) findViewById(R.id.btn_jumpToFirstActivity);
btn_jumpToFirstActivity_.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent=new Intent(SecondActivity.this, FirstActivity.class);
startActivity(intent);
}
});
}
}
~~~
把FirstActivity的代码稍作修改
~~~
btn_jumpToSecondActivity.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent=new Intent(FirstActivity.this,SecondActivity.class);
startActivity(intent);
}
});
~~~
在上面的Activity中FirstActivity的启动模式是singleTop,SecondActivity的启动模式是默认的standard,做好准备之后我们来做操作
启动后的界面如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500dfe13a7.jpg)
此时的任务栈如下
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500dff2cf1.jpg)
在此基础上我们点击一次按钮界面如下
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e00bbc3.jpg)
此时的任务栈的变化如下
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e01970f.jpg)
在SecondActivity中再次点击按钮的界面如下
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e02a7e3.jpg)
此时的任务栈的变化如下
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e037702.jpg)
从上面的过程中我们看到再次从SecondActivity跳转到FirstActivity时两次的FirstActivity的序列号不同说明又重新生成了一个FirstActivity
singleTop模式的特点:当从SecondActivity跳转到FirstActivity时,系统发现存在有FirstActivity实例,但不是位于栈顶,于是重新生成一个实例。这就是singleTop启动模式的特点,即如果发现有对应的Activity实例正位于栈顶,则重复利用,不再生成新的实例,如果栈顶没有对应的Activity则实例化一个。
该模式和standard模式基本一致,但有一点不同,当将要被启动的Activity已经位于Task栈顶时,系统不会重新创建目标Activity的实例,而是直接复用Task栈顶的Activity
3.singleTask(内单例模式)
我们还是建立在上面的基础上,把FirstActivity的启动模式改为android:launchMode="singleTask"
启动后我们点击三次跳转按钮界面如下图所示
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e04909b.jpg)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e0575a5.jpg)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e064e81.jpg)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e073755.jpg)
在上面的过程中,FirstActivity的序列号是不变的,SecondActivity的序列号是改变的,说明从SecondActivity跳转到FirstActivity时,没有生成新的实例,但是从FirstActivity跳转到SecondActivity时生成了新的实例。
在此过程中任务栈的变化过程如下
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e0910b3.jpg)
在上面的跳转过程中当从SecondActivity跳转到FirstActivity时发现SecondActivity消失了,这就是singleTask的特点在这个跳转过程中系统发现有存在的FirstActivity实例,于是不再生成新的实例,而是将FirstActivity之上的Activity实例统统出栈,将FirstActivity变为栈顶对象,显示到幕前。
singleTask模式的特点:如果发现有对应的Activity实例,则使此Activity实例之上的其他Activity实例统统出栈,使此Activity实例成为栈顶对象,显示到幕前。
Activity在同一个Task内只有一个实例. 当系统采用singleTask模式加载Activity时,又分为以下三种情况:
(1)如果将要启动的Activity不存在,那么系统将会创建该实例,并将其加入Task栈顶
(2)如果将要启动的Activity已存在,且存在栈顶,那么此时与singleTop模式的行为相同
(3)如果将要启动的Activity存在但是没有位于栈顶,那么 此时系统会把位于该Activity上面的所有其他Activity全部移出Task,从而使得该目标Activity位于栈顶
4.singleInstance(全局单例模式)
这种模式是四种模式中最难理解的一种模式,因为这种模式会重新创建一个新的任务栈,将Activity放置于这个栈中,并保证其它的Activity不再进入,由于这种模式比较复杂,我们首先来说说它的原理,然后再结合实例进一步的理解,假如现在用户打开了两个应用分别为应用1和应用2,应用1和应用2的任务栈假如如下图左边,此时在应用1中想打开Activity3,这时应用1和应用2就会共享Activity3的引用,
注意:之所以能公用Activity的引用是以应用2中的Activity设置了LaunchMode="singleInstance"为前提的。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e0a22c7.jpg)
由于这种模式比较复杂,我们举两个不同例子,来说明不同的问题
举例一、
还是上面的两个Activity。FirstActivity和SecondActivity在两个Activityt跳转的过程中我们打印两个Activity所在的任务栈的ID
对以上两个Activity做如下修改,并且把SecondActivity的启动模式改为singleInstance
~~~
tv_showViewClass=(TextView) findViewById(R.id.tv_showViewClass);
~~~
~~~
tv_showViewClass.setText("当前Activity:"+"\n"+this.toString()+"\n"+"当前TaskId:"+this.getTaskId());
~~~
启动后和点击跳转按钮后的界面如下
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e0b2798.jpg)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e0c42b8.jpg)
我们发现两个Activity的TaskId是不同的,说明这两个Activity是位于不同的任务栈中的,从而证实了为SecondActivity重新建立了一个任务栈,可能有的朋友会问,在这个时候如果点击返回按钮它们是怎么出栈的呢?假如现在我们点击返回按钮它的任务栈的变化如下图
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e0d88c2.jpg)
假如我们在SecondActivity中点击按钮跳转到FirstActivity然后会以怎样的方式退出应用呢?此时 它的任务栈的变化如下
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e0eae57.jpg)
图中下半部分显示的在SecondActivity中再次跳转到FirstActivity,这个时候系统会在原始栈结构中生成一个FirstActivity实例,然后回退两次,注意,并没有退出,而是回到了SecondActivity,为什么呢?是因为从SecondActivity跳转到FirstActivity的时候,我们的起点变成了SecondActivity实例所在的栈结构,这样一来,我们需要“回归”到这个栈结构。
由于singleInstance比较复杂些,我们再来举一个两个应用的例子为了和上面的例子混淆,我们重新写两个应用
第一个App中有两个Activity分别为Activity1和ShareActivity
第二个App中有一个Activity2我们在这个App中启动第一个App的ShareActivity
第一个App的Activity源码如下
~~~
package com.example.activitylauchmodepractice;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
public class Activity1 extends Activity {
private Button btn_jumpToSecondActivity;
private TextView tv_showViewClass;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_first);
tv_showViewClass=(TextView) findViewById(R.id.tv_showViewClass);
tv_showViewClass.setText("当前Activity:"+"\n"+this.toString()+"\n"+"当前TaskId:"+this.getTaskId());
btn_jumpToSecondActivity=(Button) findViewById(R.id.btn_jumpToSharedActivity);
btn_jumpToSecondActivity.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent=new Intent(Activity1.this,ShareActivity.class);
startActivity(intent);
}
});
}
}
~~~
ShareActivity源码
~~~
package com.example.activitylauchmodepractice;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
public class ShareActivity extends Activity {
private Button btn_jump;
private TextView tv_showViewClass;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
tv_showViewClass=(TextView) findViewById(R.id.tv_showViewClass);
tv_showViewClass.setText("当前Activity:"+"\n"+this.toString()+"\n"+"当前TaskId:"+this.getTaskId());
}
}
~~~
我们要特别注意ShareActivity在清单文件中的配置如下
~~~
<activity
android:name="com.example.activitylauchmodepractice.ShareActivity"
android:launchMode="singleInstance">
<intent-filter>
<action android:name="SecondActivity_action"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
~~~
我们需要配置ShareActivity的action在另一个应用中启动时会用到
第二个App中的Activity2的源码如下
~~~
package com.example.singleinstancepractice;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
public class MainActivity extends Activity {
private Button btn_jump;
private TextView tv_showTaskId;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv_showTaskId=(TextView) findViewById(R.id.tv_showTaskId);
tv_showTaskId.setText("当前Activity:"+"\n"+this.toString()+"\n"+"当前TaskId:"+this.getTaskId());
btn_jump=(Button) findViewById(R.id.btn_jump);
btn_jump.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent =new Intent();
intent.setAction("SecondActivity_action");
startActivity(intent);
}
});
}
}
~~~
当我们在第一个App中打开ShareActivity后再按后退键回到原来界面时,ShareActivity做为一个独立的个体存在,如果这时我们在第二个App中打开ShareActivity无需创建新的ShareActivity实例即可看到结果,因为系统会自动查找,存在则直接利用。原理图如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500e109216.jpg)
注意:上图是建立在第一个App运行到手机上时点击第二个App上的跳转按钮跳转到ShareActivity的情况的基础上的变化过程。
Android开发之Activity的生命周期
最后更新于:2022-04-01 09:46:33
路漫漫其修远兮,吾将上下而求索。---屈原《离骚》
可能很多人会感觉Activity很简单,但是经历了半年的android开发,我发现我对Activity的的理解还是比较浅显的,其实Activity并没有我们想象的那么简单今天花了一个下午学习了Activity的生命周期,为以后忘记后迅速回顾,做准备。
**一、首先看下官网给出activity的生命周期图**
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500de6b42c.jpg)
我们可以从图中分析它的执行过程:
1.启动Activity:系统会首先调用onCreate方法,然后调用onStart方法,最后调用onResume()方法。此时Activity进入运行状态
2.当前Activity被其他Activity覆盖或处于锁屏状态:系统会调用onPause方法,然后调用onStop方法,此时Activity处于停滞状态。
3.当前Activity由覆盖状态回到前台或解锁屏:系统会调用onRestart方法,然后调用onStart方法,最后调用onResume方法,再次进入运行状态。
4.当前Activity跳转到新的Activity界面或Home键回到主屏,退到后台:系统会先调用onPause方法,然后调用onStop方法进入停滞状态
5.用户后退回到此Activity:系统会先调用onRestart()方法,然后调用onStart方法,最后调用onResume方法,再次进入运行状态
6.当前Activity处于被覆盖状态或者后台不可见状态,即第二和第四步,系统内存不足,杀死当前Activity,而后用户退回当前Activity:再次调用onCreate方法、onStart方法、onResume方法,进入运行状态。
7.用户退出当前Activity:系统会调用onPause方法,然后调用onStop方法最后调用onDestroy方法,结束当前Activity
以上的Activity方法的生命周期是很简单的,但是任何简单的东西,只有我们通过实例验证才能理解的更加透彻,记忆更加深刻,
**二、实例分析**
实例代码:
~~~
package com.example.activitypractice;
import android.app.Activity;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
public class MainActivity extends Activity {
private static final String TAG = "MainActivity";
private Button btn_jump;
private String saveData= "存放的数据" ;
//Activity创建时调用
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout. activity_main);
btn_jump=(Button) findViewById(R.id. btn_jump);
btn_jump.setOnClickListener( new OnClickListener() {
public void onClick(View v) {
Intent intent= new Intent(MainActivity.this,JumpToActivity.class );
startActivity(intent);
}
});
Log. i(TAG,"执行了onCreate方法" );
}
//Activity创建或者从后台重新回到前台时被调用
@Override
protected void onStart() {
super.onStart();
Log. i(TAG,"执行了onStart方法" );
}
//Activity从后台重新回到前台时被调用
@Override
protected void onRestart() {
super.onRestart();
Log. i(TAG,"执行了onRestart方法" );
}
//Activity创建或者从被覆盖、后台重新回到前台时被调用
@Override
protected void onResume() {
super.onResume();
Log. i(TAG,"执行了onResume方法" );
}
//Activity被覆盖到下面或者锁屏时被调用
@Override
protected void onPause() {
super.onPause();
Log. i(TAG,"执行了onPause方法" );
}
//退出当前Activity或者跳转到新Activity时被调用
@Override
protected void onStop() {
super.onStop();
Log. i(TAG,"执行了onStop方法" );
}
//退出当前Activity时被调用,调用之后Activity就结束了
@Override
protected void onDestroy() {
super.onDestroy();
Log. i(TAG,"执行了onDestroy方法" );
}
//Activity窗口获得或失去焦点时被调用,在onResume之后或onPause之后
/* @Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
Log.i(TAG, "onWindowFocusChanged called.");
} */
/**
* Activity被系统杀死时被调用.
* 例如:屏幕方向改变时,Activity被销毁再重建;当前Activity处于后台,系统资源紧张将其杀死.
* 另外,当跳转到其他Activity或者按Home键回到主屏时该方法也会被调用,系统是为了保存当前View组件的状态.
* 在onPause之前被调用.
*/
@Override
protected void onSaveInstanceState(Bundle outState) {
outState.putString( "key", saveData);
Log. i(TAG, "onSaveInstanceState called.put saveData: " + saveData );
super.onSaveInstanceState(outState);
}
/**
* Activity被系统杀死后再重建时被调用.
* 例如:屏幕方向改变时,Activity被销毁再重建;当前Activity处于后台,系统资源紧张将其杀死,用户又启动该Activity.
* 这两种情况下onRestoreInstanceState都会被调用,在onStart之后.
*/
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
String getData = savedInstanceState.getString( "key");
Log. i(TAG, "onRestoreInstacedState called.getData: "+getData);
super.onRestoreInstanceState(savedInstanceState);
}
}
~~~
在上面的例子中我们可以看到还有三个不是特别常见的方法,onWindowFocusChanged、onSaveInstanceState、onRestoreInstanceState方法这三个方法我们用的比较少下面也来说说它们的用法:
(1)onWindowFocusChanaged方法
在Activity窗口获得或失去焦点时调用,例如创建时首次展示在用户面前、当Activity被其它Activity所覆盖、Activity退到后台、用户退出当前Activity,这几种条件都会调用onWindowFocusChanaged,并且当Activity被创建时是在onResume之后被调用,当Activity被覆盖或者退到后台或当前Activity退出时它是在onPause之后被调用的,如下图Activity启动之后被其它的activity覆盖(点击我们的跳转按钮)打印的日志
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500de82e66.jpg)
这个方法在很多场合下还是很有用的,例如程序启动时想要获取组件的尺寸大小,在onCreate中是无法取到的因为Window对象还没有创建完,这时候我们就需要在onWindwosFocusChanaged里获取,这也是我们常用的方法。
(2)onSaveInstanceState:
1.在Activity被覆盖或者退到后台之后,系统资源不足,将其杀死,此方法会被调用;
2.在用户改变屏幕方向时此方法会被调用;
3.在当前Activity跳转到其他Activity或者按Home键回到主屏,自身退到后台时,此方法会被调用。
第一种情况我们无法保证什么时候发生,系统根据资源的紧张程度去调度;第二种是屏幕翻转方向时,系统先销毁当前Activity,然后再重建一个新的,调用此方法时,我们可以保存一些临时数据,例如:当竖屏时加载一个进度条,切换到横屏时如果不保存当前加载到多少横屏时会重0开始,保存后就可以继续加载,第三种情况系统调用此方法是为了保存当前窗口各个View组件的状态。onSaveInstanceState的调用顺序是在onPause之后,如下图是在跳转到其它的Activity所执行的方法
我们从图中可以看出onSaveInstanceState方法是在onPause方法中执行的
(3)onRestoreInstanceState
1.在Activity被覆盖或是退居后台之后,系统资源不足将其杀死,然后又回到了此Activity,此方法会被调用
2.在用户改变屏幕方向时,重建的过程中,此方法会被调用。我们可以重写此方法,以便恢复一些临时数据。onRestoreInstanceState的调用顺序是在onStart方法之后。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500de95a75.jpg)
之后我们来结合上面的例子来一 一看看Activity的生命周期在各种的情况下的变化
1.启动Activity:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500deab1c4.jpg)
启动Activity:系统会首先调用onCreate方法,然后调用onStart方法,最后调用onResume()方法。此时Activity进入运行状态
2.在1跳转到其他的Activity,或按Home键
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500debc961.jpg)
系统会先调用onPause方法,然后调用 onSaveInstanceState方法,然后调用onStop方法进入停滞状态
3.从后台回到前台
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500dece7b7.jpg)
系统会先调用onRestart方法,然后调用onStart方法,然后调用onResume方法
4.修改在AndroidMainfest.xml中的配置,将android:theme属性设置为@android:style/Theme.Dialog,然后再点击跳转按钮,然后JumpToActivity覆盖到LifeCycleActivity之上了,
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500dee0cce.jpg)
此时调用的方法为:onPause和onSaveInstanceState,我们注意到,此时MainActivity的onPause方法被调用,并没有执行onStop方法,因为此时的MainActivity没有退居到后台只是被覆盖或被锁屏;onSaveInstanceState会在onPause之后被调用
5.在4的基础上按回退键MainActivity从被覆盖回到前面
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500def0d18.jpg)
此时执行onResume方法
6.退出
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500df0de8b.jpg)
系统调用onPause、onStop、onDestroy方法。
不知道大家有没有发现在上面的所有的日志中并没有执行onRestoreInstanceState方法,为什么没有执行它呢没有满足它执行的条件,从上面onRestoreInstanceState执行的条件我们发现第一种是不可预测的,但是第二种(即改变屏幕方向)的话我们可以模拟的
**三、Activity屏幕方向的知识**
我们可以为一个Activity指定一个特定的方向,指定之后即使转动屏幕方向,显示方向也不会跟着转动
1.指定为竖屏:在AndroidManifest.xml文件中对指定的Activity设置android:screenOrientation:="portrait",
或者在onCreate方法中指定代码为:
setRequestOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);就指定为了竖屏
2.指定为横屏:在AndroidManifest.xml文件中对指定的Activity设置android:screenOrientation:="landscape",
或者在onCreate方法中指定代码为:
setRequestOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);就指定为了竖屏
在开发应用的过程中,为应用中的Activity设置特定的方向是经常用到的方法,可以为我们省去很多麻烦,但是我为了让onRestoreInstanceState方法执行采用的是不固定屏幕的显示方向我们对上面的代码稍作修改加上onConfigurationChanged方法
现在源码如下:
~~~
package com.example.activitypractice;
import android.app.Activity;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
public class MainActivity extends Activity {
private static final String TAG = "MainActivity";
private Button btn_jump;
private String saveData= "存放的数据" ;
//Activity创建时调用
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout. activity_main);
btn_jump=(Button) findViewById(R.id. btn_jump);
btn_jump.setOnClickListener( new OnClickListener() {
public void onClick(View v) {
Intent intent= new Intent(MainActivity.this,JumpToActivity.class );
startActivity(intent);
}
});
Log. i(TAG,"执行了onCreate方法" );
}
//Activity创建或者从后台重新回到前台时被调用
@Override
protected void onStart() {
super.onStart();
Log. i(TAG,"执行了onStart方法" );
}
//Activity从后台重新回到前台时被调用
@Override
protected void onRestart() {
super.onRestart();
Log. i(TAG,"执行了onRestart方法" );
}
//Activity创建或者从被覆盖、后台重新回到前台时被调用
@Override
protected void onResume() {
super.onResume();
Log. i(TAG,"执行了onResume方法" );
}
//Activity被覆盖到下面或者锁屏时被调用
@Override
protected void onPause() {
super.onPause();
Log. i(TAG,"执行了onPause方法" );
}
//退出当前Activity或者跳转到新Activity时被调用
@Override
protected void onStop() {
super.onStop();
Log. i(TAG,"执行了onStop方法" );
}
//退出当前Activity时被调用,调用之后Activity就结束了
@Override
protected void onDestroy() {
super.onDestroy();
Log. i(TAG,"执行了onDestroy方法" );
}
/**
* Activity被系统杀死时被调用.
* 例如:屏幕方向改变时,Activity被销毁再重建;当前Activity处于后台,系统资源紧张将其杀死.
* 另外,当跳转到其他Activity或者按Home键回到主屏时该方法也会被调用,系统是为了保存当前View组件的状态.
* 在onPause之前被调用.
*/
@Override
protected void onSaveInstanceState(Bundle outState) {
outState.putString( "key", saveData);
Log. i(TAG, "onSaveInstanceState called.put saveData: " + saveData );
super.onSaveInstanceState(outState);
}
/**
* Activity被系统杀死后再重建时被调用.
* 例如:屏幕方向改变时,Activity被销毁再重建;当前Activity处于后台,系统资源紧张将其杀死,用户又启动该Activity.
* 这两种情况下onRestoreInstanceState都会被调用,在onStart之后.
*/
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
String getData = savedInstanceState.getString( "key");
Log. i(TAG, "onRestoreInstacedState called.getData: "+getData);
super.onRestoreInstanceState(savedInstanceState);
}
//当指定了android:configChanges="orientation"后,方向改变时onConfigurationChanged被调用,并且activity不再销毁重建
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
switch (newConfig. orientation) {
case Configuration. ORIENTATION_PORTRAIT://竖屏
Log. i(TAG,"竖屏" );
setContentView(R.layout. portrait);
break;
case Configuration. ORIENTATION_LANDSCAPE://横屏
Log. i(TAG,"横屏" );
setContentView(R.layout. landscap);
default:
break;
}
}
}
~~~
需要注意的是我们必须在手机上设置(注意:要在设置中设置自动旋转屏幕我用的三星的测试机是在设定-->我的设备-->显示 在"显示"里有"自动旋转屏幕"选项)自动旋转屏幕才可以
然后当我们旋转屏幕时,我们会看到Activity首先会销毁然后重建,系统会调用onSaveInstanceState方法并在Bundle对象中保存了一个临时的数据,当Activity销毁后再重建时调用onRestoreInstancedState方法我们从中可以把保存的数据取出来日志如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500df1c33f.jpg)
但是Activity销毁后再重建,这有时候并不是我们想要的,那么我们应该怎样避免这种情况呢?这是我们应该在清单文件中对应的Activity中配置android:configChanges="orientation"然后我们再旋转屏幕
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500df33734.jpg)
每次在旋转的时候都调用了onConfigurationChanged方法,并且Activity没有了销毁重建的过程
在上面的配置过程中我们需要注意几点:
1.如果在Activity中配置了android:screenOrientation属性,则会使android:configChanges="orientation"失效。
2.在配置android:configChanges时需要注意:如果是Android 4.0,则是"orientation|keyboardHidden|screenSize",如果是
4.0之前的版本android:configChanges="orientation|keyboardHidden"
[关于android:configChanges配置时需注意的事项见此链接](http://blog.csdn.net/dmk877/article/details/45056027)
Activity中使用configChanges属性讲解及需注意的问题
最后更新于:2022-04-01 09:46:31
不积跬步,无以至千里;不积小流,无以成江海。骐骥一跃,不能十步,驽马十驾,功在不舍。---------[荀子](http://baike.baidu.com/view/2776.htm)《[劝学篇](http://baike.baidu.com/view/247920.htm)》
今天遇到了一个关于orientation的问题查了点资料记录一下,只有点点滴滴的积累,才能让我们更加强壮,如有谬误欢迎大家批评指正
在Android默认情况,当“屏幕方向”或“键盘显示隐藏”变化时都会销毁当前Activity,创建新的Activity。如果不希望重新创建Activity实例,可以在AndroidManifest.xml中配置Activity:<activity android:name=".MainActivity" android:configChanges="keyboardHidden|orientation" >这样就不会销毁重建了在配置了这个属性后,android:configChanges 属性就会捕获“屏幕方向”和“键盘显示隐藏”变化,当捕获到这些变化后会调用Activity的onConfigurationChanged()方法。
我在手机上调试发现横竖屏切换时,并没有执行onConfigurationChanged()方法,但是Activity却执行了销毁重建的过程这是为什么呢?这是因为版本的问题上面的配置只在android4.o之前的版本起作用,在android 4.0 以上不起作用,必须要加上screenSize,也就是说android 4.0以后的版本必须这样配置android:configChanges="keyboardHidden|orientation|screenSize"
下面我们结合实例说orientation和onConfigurationChanaged方法
代码:
~~~
package com.example.activitypractice;
import android.app.Activity;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
public class MainActivity extends Activity {
private static final String TAG = "MainActivity";
//Activity创建时调用
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.i(TAG,"执行了onCreate方法");
}
//Activity创建或者从后台重新回到前台时被调用
@Override
protected void onStart() {
super.onStart();
Log.i(TAG,"执行了onStart方法");
}
//Activity从后台重新回到前台时被调用
@Override
protected void onRestart() {
super.onRestart();
Log.i(TAG,"执行了onRestart方法");
}
//Activity创建或者从被覆盖、后台重新回到前台时被调用
@Override
protected void onResume() {
super.onResume();
Log.i(TAG,"执行了onResume方法");
}
//Activity被覆盖到下面或者锁屏时被调用
@Override
protected void onPause() {
super.onPause();
Log.i(TAG,"执行了onPause方法");
}
//退出当前Activity或者跳转到新Activity时被调用
@Override
protected void onStop() {
super.onStop();
Log.i(TAG,"执行了onStop方法");
}
//退出当前Activity时被调用,调用之后Activity就结束了
@Override
protected void onDestroy() {
super.onDestroy();
Log.i(TAG,"执行了onDestroy方法");
}
//当指定了android:configChanges="orientation"后,方向改变时onConfigurationChanged被调用,并且activity不再销毁重建
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
switch (newConfig.orientation) {
case Configuration.ORIENTATION_PORTRAIT://竖屏
Log.i(TAG,"竖屏");
setContentView(R.layout.portrait);
break;
case Configuration.ORIENTATION_LANDSCAPE://横屏
Log.i(TAG,"横屏");
setContentView(R.layout.landscap);
default:
break;
}
}
}
~~~
当我们没有配置android:configChanges时进行横屏和竖屏的切换时(注意:要在设置中设置自动旋转屏幕我用的三星的测试机是在设定-->我的设备-->显示 在"显示"里有"自动旋转屏幕"选项)日志如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500de392ae.jpg)
从上面的日志中可以看出activity执行了销毁重建的过程,onConfigurationChanaged并没有被调用
如果我们加上android:configChanges="keyboardHidden|orientation|screenSize"再进行旋转日志如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-01_56d500de54fe8.jpg)
从上面的日志中可以看出activity并没有执行销毁重建的过程,而调用了onConfigurationChanaged方法
前言
最后更新于:2022-04-01 09:46:29
> 原文出处:[Android四大组件](http://blog.csdn.net/column/details/singwhiledrinking.html)
作者:[dmk877](http://blog.csdn.net/dmk877)
**本系列文章经作者授权在看云整理发布,未经作者允许,请勿转载!**
# Android四大组件
> 本专栏详细讲解了android的四大组件,Activity、BroadCastReceiver、Service、ContentProvider