1、activity
1.1、activity是用户操作的可视化界面,它为用户提供了一个完成操作指令的窗口。当我们创建完毕Activity之后,需要调用setContentView()方法来完成界面的显示,以此来为用户提供交互的入口。
1.2、activity的4个生命周期状态,7个生命周期方法,2个其他方法。
4个生命周期状态:
在Activity运行之前,会通过onCreate(),onStart(),onResume();当运行完onResume()之后,Activity就处于上面说的Running/Active 状态了。
当Activity处于暂停状态时,会调用onPause(),这个时候Activity还是见的。当在这个时候Activity恢复到运行状态时,会重新调用onResume()。
当Activity处理停止状态时,会调用onStop(),这个时候如果要恢复到运行状态就会调用一个新的方法onRestart(),然后去调用onStart(),onResume()。
当Activity被销毁时,就会调用onDestroy(),那么如果要恢复Activity的显示就需要重新创建这个Activity;重新去走onCreate(),onStart(),onResume()这三个方法。
7个生命周期方法:
onCreate: 当Activity第一次被创建时调用。是生命周期开始的第一个方法。在这里我们可以做一些初始化的操作,比如:调用setContentView()方法去加载界面,绑定布局里的一些控件,初始化一些Activity需要用到的数据。之后会调用onStart方法.
onStart:当Activity正在变为可见时调用。这个时候Activity已经可见了,但是还没有出现在前台还不能跟用户交互。可以简单理解为Actvity已经可见但是还没有出现在前台。之后会调用onResume.
onResume:当Activity可以跟用户交互时调用,这个时候,这个Activity位于栈的顶部。跟onStart相比,它们都是表示Activity已经可见,但是onStart调用时Activity还在后台,而调用onResume时,Activity已经进入了前台,可以跟用户交互了。之后会调用 onPause.
onPause:当Activity暂停时调用这个方法;在这里我们可以用来保存数据,关闭动画和其它比较耗费CPU的操作;但是在这里做的操作绝对不能耗时,因为如果当前Activity要启动一个新的Activity,这个新的Activity会在当前Activity执行完毕onPause之后才能进入可见状态。这个方法之后一般会调用的方法有onStop或者onResume.注意:在Android3.0之前,调用这个方法之后,Activity可能会在系统内存紧张时被系统回收。
onStop:当Activity进入后台,并且不会被用户看到时调用。当别的Activity出现在前台时,或者Activity会被销毁时,调用此方法;在这个方法调用之后,系统可能会在内存不够的情况下回收Activity;在这个方法之后一般会调用onRestart或者onDestroy.
onDestroy:这个方法是Activity生命周期中调用的最后一个方法。它会在Activity被销毁之前调用;Activity销毁原因一般是我们调用Activity的finish方法手动销毁,另一个就是系统在内存紧张的情况下去销毁Activity,以用来节省空间。我们可以通过方法 isFinishing 来判断Activity是否正在被销毁。
onRestart:这个方法是在Activity处于停止状态后,又回到可视状态时调用。之后会调用onResume.
注:onCreate 跟 onDestroy 是相对的,一个创建一个销毁,并且其只可能被调用一次。onStart跟onStop 是配对的,这两个方法可以被多次调用。onResume 和 onPause 也是配对的,它们一个获取焦点和用户交互,一个正好相反。onStart和onStop 是Activity是否可见的标志,而onResume和onPause是从Activity是否位于前台的标志,它们针对的角度不同。在onPause里不能做耗时操作,因为如果要启动一个新的Activity,新的Activity必须要在前一个Activity的onPause 方法执行完毕之后才会启动的新的Activity。
2个其他方法:
onSaveInstanceState:它不属于Activity的生命周期方法,不一定会被触发,但是当应用遇到意外情况,比如:内存紧张,用户点击了Home键或者用户按下电源键关闭屏幕等这些情况,这时系统可能会去销毁Activity,这时这个方法就会被调用。这里强调的是系统去回收Activity,而不是我们去手动的销毁Activity。这个方法提供了一个Bundle对象,我们可以通过它来保存一些临时性的状态或者数据。通常这个方法只适合保存临时数据,如果需要数据的持久化保存等操作,还是要在onPause方法里执行才好。当 Activity再次被创建时,Activity会通过onCreate(Bundle)或者onRestoreInstanceState(Bundle)执行的时候,会将提供一个的Bundle对象来恢复这些数据。
onRestoreInstanceState:这个方法调用的前提是,Activity必须是被系统销毁了,在Activity被再次创建时它会在onStart()方法之后被调用。
生命周期的一些实用场景:
当前Activity产生事件弹出Toast和AlertDialog的时候Activity的生命周期不会有改变。
在系统内存不够时会根据优先级杀死Activity,就会按照优先级去销毁Activity。最高的优先级:在前台显示并且跟用户交互的Activity,优先级最高;暂停状态的Activity优先级次之:如果Activity没有在前台,但是可见,不可与用户交互,比如弹出一个对话框等;处于后台Activity优先级最低:执行了onStop方法的Activity优先级最低。它不可见,并且无法跟用户交互。在销毁Activity时会额外的在onPause和onStop之间调用onSaveInstanceState;当要重新创建这个Activity 时,会在onStart方法之后调用onRestoreInstanceState方法。
对于一个正常的Activity,第一次启动,会依次回调以下方法:onCreate->onStart->onResume;当我们打开一个新的Activity或者点击Home键回到桌面后,会依次回调以下方法:onPause->onStop;上面提到过,如果新的Activity是透明的(采用的透明主题),当前的Activity不会回调onStop。当我们再次回到原Activity,会依次回调以下方法:onRestart->onStart->onResume;当我们点击返回键后,会依次回调以下方法:onPause->onStop->onDestroy;当Activity被系统回收后,再次被打开,会跟第一次启动的时回调生命周期方法一样(不包含 onSaveInstanceState 和 onRestoreInstanceState)。
横竖屏切换,不设置Activity的android:configChanges时,切屏会重新调用各个生命周期,切横屏时会执行一次,切竖屏时会执行两次;设置Activity的android:configChanges="orientation"时,切屏还是会重新调用各个生命周期,切横、竖屏时只会执行一次(onCreate-->onStart-->onResume->onSaveInstanceState-->onPause-->onStop-->onDestroy-->onCreate-->onStart-->onRestoreInstanceState-->onResume-->onSaveInstanceState-->onPause-->onStop-->onDestroy-->onCreate-->onStart-->onRestoreInstanceState-->onResume-->onConfigurationChanged-->);设置Activity的android:configChanges="orientation|keyboardHidden"时,切屏不会重新调用各个生命周期,只会执行onConfigurationChanged方法。
注:Android 提供了一个属性:android:screenOrientation,用它来限制Activity的朝向。它提供很多设定值,但是我们比较常用的是,横向(landscape)和竖向(portrait),设置两个值中的一个,屏幕就会按照设定值显示,不再会变化。这个属性需要在清单文件(AndroidMenifest.xml)的<activity></activity>节点下设置。或是可以在代码中使用setRequestedOrientation(int),来设置屏幕旋转,它的参数定义在ActivityInfo类中。再者,Activity 提供了一个onConfigurationChanged,当屏幕的朝向放生变化时,获取当前屏幕方向。
1.3、Activity的启动模式
Task就是用户为了某一个特定的工作时进行交互的Activity的集合。Activity统一放到了一个栈里,这个栈就叫做回退栈,也可以叫做任务栈,调用栈等,这样我们就可以通过回退栈来管理这些的Activity。当我们点击手机桌面的应用图标进入应用后,应用的Task就进入了前台。如果没有Task存在(这个应用最近没有被使用过),Android就会新建一个Task,并且把主Activity作为当前任务栈的根Activity(栈底Actvitiy)。默认情况下,我们每启动一个的Activity,就会放到Activity的回退栈中,就像是子弹压入弹夹,在栈顶的Activity才会显示到界面上。如果我们需要finish掉所有的Activity,需要从栈顶一个一个把Activity移除。
Task是一个有粘性的组合,可以回到后台,当我们开启一个新的Task或者点击Home键回到桌面,原有的Task就会进入后台,当前Task的回退栈里所有的Activity都出与Stopped状态,但是这个回退栈还是完整存在的,这个Task的位置被新的Task占据,只是失去了焦点而已。当然既然Task回到后台也能重新回到前台。
standard 模式(默认模式):
默认的模式,如果我们在AndroidManifest不设置launchMode属性,就是默认模式,当然我们也可以显式设置launchMode=”standard”。这模式下的Activity可以被实例化多次,也可以属于不同的Task。我们每启动一次Activity,它都会实例化一次;其中的生命周期也会重写走一遍。当我们从别的App启动这个Activity时,它就业属于另外的Task了。
singleTop 模式 (栈顶复用):
这个模式名称中含有一个”Top”,当然它的特点也与这个单词有关。如果一个Activity已经存在于当前的Task了,并且在栈的顶端,当我们通过Intent开启Activity时,不会在实例化一个新的Activity对象,而是复用了栈顶的Activity。并且会在Activity内部回调 onNewIntent方法,而不会再重走它的的生命周期方法。但是如果我们能点击返回键,则不会回到调用onNewIntent方法之前的状态。虽然设置了singleTop模式,当Activity不在栈顶时,还是会创建新的实例;而且它还可以属于多个Task,并且同一个Task可以含有多个设置了singleTop模式的Activity实例。
singleTask 模式:
又叫做 单任务模式和栈内复用模式。既然它的名字中带有一个Task,它必定跟Task有关。它会重新创建一个Task,把设置launchMode为singleTask的Activity单独放到这个Task里。如果启动的Activity已经在新建Task的回退栈中,那么它不会重新实例化这个Activity,而是调用它的onNewIntent方法。不管多少次启动这个Activity,都不会重新创建实例。singleTask还有一个非常重要的一个特性,再需要重新开启已有的Activity,就会把它上面所有的Activity移除该栈。
singleInstance 模式:
字面翻译过来就是 单例模式,这个模式表现非常接近于singleTask,系统中只允许一个Activity的实例存在。区别是,如果一个Activity设置了singleInstance模式,那么系统就会新建一个Task,并且这个Task只能有这一个Activity。如果重复启动这个Activity,均不会重建这个Activity,它会实行栈内复用,跟singleTask一样。
启动模式的两种设置方式:
启动模式可以在清单文件中设置,也可以通过Intent设置类似的启动模式,我们可以设置Flag来实现这个目的。Intent中定义了很多的flag,其中就有能设置Activity启动模式的flag。使用Intent设置这些flag跟在AndroidManifest中设置launchMode功能是一样的。FLAG_ACTIVITY_NEW_TASK:官方文档说相当于在xml里设置 singleTask,但不尽然,具体的大家可以试验一下;FLAG_ACTIVITY_SINGLE_TOP:相当于在xml里设置 singleTop;FLAG_ACTIVITY_CLEAR_TOP:设置这个flag之后,当它启动时,在同一个任务栈中的所有位于它上面的Activity都会被销毁。它一般要跟FLAG_ACTIVITY_NEW_TASK一块用。
关于启动模式还牵扯到两个Activity的另两个参数:
taskAffinity用于指定当前Activity(activity1)所关联的Task,allowTaskReparenting用于配置是否允许该activity可以更换从属task,通常情况二者连在一起使用,用于实现把一个应用程序的Activity移到另一个应用程序的Task中。
allowTaskReparenting用来标记Activity能否从启动的Task移动到taskAffinity指定的Task,默认是继承至application中的allowTaskReparenting=false,如果为true,则表示可以更换;false表示不可以。
1.4、Activity的启动方式
显式跳转,是在已知包名和类名的情况下常用的跳转方法:
第一种方式
Intent mIntent = new Intent();
mIntent.setClassName("com.android.settings","com.android.settings.Settings
mContext.startActivity(mIntent);
第二种方式
Intent intent = new Intent(mContext, XXActivity.class);
startActivity(intent);
隐式跳转:
第一种方式,隐式跳转之Action跳转
清单文件:
<activity android:name=".ActionActivity";
<intent-filter
action android:name="customer_action_here"
</intent-filter>
</activity>
跳转:
//创建一个隐式的 Intent 对象:Action 动作
Intent intent = new Intent();
//设置 Intent 的动作为清单中指定的action
intent.setAction("customer_action_here");
startActivity(intent);
第二种方式,隐式跳转之Category跳转
清单文件:
<activity android:name=".CategoryActivity" >
<intent-filter>
<action android:name="customer_action_here" />
<category android:name="customer_category_here" />
</intent-filter>
</activity>
跳转:
//创建一个隐式的 Intent 对象:Category 类别
Intent intent = new Intent();
intent.setAction("customer_action_here");
//添加与清单中相同的自定义category
intent.addCategory("customer_category_here");
startActivity(intent);
第三种方式,隐式跳转之Data跳转
清单文件:
<activity android:name=".DataActivity">
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<data
android:scheme="content"
android:host="com.example.intentdemo"
android:port="8080"
android:pathPattern=".*pdf"
android:mimeType="text/plain"/>
</intent-filter>
</activity>
跳转:
//创建一个隐式的 Intent 对象,方法四:Date 数据
Intent intent = new Intent();
Uri uri = Uri.parse("content://com.example.intentdemo:8080/abc.pdf");
//注:setData、setDataAndType、setType 这三种方法只能单独使用,不可共用
//单独以 setData 方法设置 URI
//intent.setData(uri);
//单独以 seType 方法设置 Type
//intent.setType("text/plain");
//上面分步骤设置是错误的,要么以 setDataAndType 方法设置 URI 及 mime type
intent.setDataAndType(uri, "text/plain");
startActivity(intent);
第四种方式,隐式跳转之调用系统应用
使用浏览器浏览网页
//web浏览器
Uri uri= Uri.parse("http://www.baidu.com");
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
startActivity(intent);
调用地图
//打开地图查看经纬度
Uri uri = Uri.parse("geo:38.899533,-77.036476");
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
startActivity(intent);
调用电话拨号(不需要拨号权限)
Uri uri = Uri.parse("tel:10086");
Intent intent = new Intent(Intent.ACTION_DIAL, uri);//注意区别于下面4.4的action
startActivity(intent);
调用电话直接拨号(需要拨号权限)
Uri uri = Uri.parse("tel:15980665805");
Intent intent = new Intent(Intent.ACTION_CALL, uri);//注意区别于上面4.3的aciton
startActivity(intent);
调用短信程序(无需发送短信权限,接收者自填)
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.putExtra("sms_body", "这里写短信内容");
intent.setType("vnd.android-dir/mms-sms");
startActivity(intent);
调用短信程序(无需发送短信权限)
Uri uri = Uri.parse("smsto:10086");//指定接收者
Intent intent = new Intent(Intent.ACTION_SENDTO, uri);
intent.putExtra("sms_body", "你这个黑心运营商");
startActivity(intent);
调用邮件程序
Intent intent = new Intent(Intent.ACTION_SENDTO);
intent.setData(Uri.parse("mailto:xxx@gmail.com"));
intent.putExtra(Intent.EXTRA_SUBJECT, "这是标题");
intent.putExtra(Intent.EXTRA_TEXT, "这是内容");
startActivity(intent);
调用音乐播放器
Intent intent = new Intent(Intent.ACTION_VIEW);
//Uri uri = Uri.parse("file:///sdcard/xiong_it.mp4");
Uri uri = Uri.parse("file:///sdcard/xiong_it.mp3");
intent.setDataAndType(uri, "audio/mp3");
startActivity(intent);
调用视频播放器
Intent intent = new Intent(Intent.ACTION_VIEW);
//Uri uri = Uri.parse("file:///sdcard/xiong_it.mp3");
Uri uri = Uri.parse("file:///sdcard/xiong_it.mp4");
intent.setDataAndType(uri, "video/mp4");
startActivity(intent);
注:调用视频播放器和音乐播放器的区别在setDataAndType()时一个是audio类型,一个是video类型
调用搜索
Intent intent = new Intent();
intent.setAction(Intent.ACTION_WEB_SEARCH);
intent.putExtra(SearchManager.QUERY, "android");
startActivity(intent);
2、service
2.1、定义
概念,Service(服务)是一个一种可以在后台执行长时间运行操作而没有用户界面的应用组件。服务可由其他应用组件启动(如Activity),服务一旦被启动将在后台一直运行,即使启动服务的组件(Activity)已销毁也不受影响。 此外,组件可以绑定到服务,以与之进行交互,甚至是执行进程间通信 (IPC)。服务位于主线程,并不会创建自己的子线程。
应用场景,服务可以处理网络事务、播放音乐,执行文件 I/O 或与内容提供程序交互。
启动服务,当应用组件(如 Activity)通过调用 startService() 启动服务时,服务即处于“启动”状态。一旦启动,服务即可在后台无限期运行,即使启动服务的组件已被销毁也不受影响,除非手动调用才能停止服务, 已启动的服务通常是执行单一操作,而且不会将结果返回给调用方。
绑定服务,当应用组件通过调用 bindService() 绑定到服务时,服务即处于“绑定”状态。绑定服务提供了一个客户端-服务器接口,允许组件与服务进行交互、发送请求、获取结果,甚至是利用进程间通信 (IPC) 跨进程执行这些操作。 仅当与另一个应用组件绑定时,绑定服务才会运行。 多个组件可以同时绑定到该服务,但全部取消绑定后,该服务即会被销毁。
2.2、两种服务使用的具体流程
大概介绍:
启动服务使用startService(Intent intent)方法,仅需要传递一个Intent对象即可,在Intent对象中指定需要启动的服务。由于每次启动服务(调用startService)时,onStartCommand方法都会被调用,因此我们可以通过该方法使用Intent给Service传递所需要的参数,然后在onStartCommand方法中处理的事件,最后根据需求选择不同的Flag返回值,以达到对程序更友好的控制。而使用startService()方法启动的服务,在服务的外部,必须使用stopService()方法停止,在服务的内部可以调用stopSelf()方法停止当前服务。如果使用stopService()或者stopSelf()方法请求停止服务,系统就会尽快销毁这个服务。值得注意的是对于启动服务,一旦启动将与访问它的组件无任何关联,即使访问它的组件被销毁了,这个服务也一直运行下去,直到手动调用停止服务才被销毁。启动状态下Service的执行周期,第一次调用startService方法时,onCreate方法、onStartCommand方法将依次被调用,而多次调用startService时,只有onStartCommand方法被调用,最后我们调用stopService方法停止服务时onDestory方法被回调。
绑定服务是Service的另一种变形,当Service处于绑定状态时,其代表着客户端-服务器接口中的服务器。当其他组件(如 Activity)绑定到服务时(有时我们可能需要从Activity组建中去调用Service中的方法,此时Activity以绑定的方式挂靠到Service后,我们就可以轻松地方法到Service中的指定方法),组件(如Activity)可以向Service(也就是服务端)发送请求,或者调用Service(服务端)的方法,此时被绑定的Service(服务端)会接收信息并响应。也可以通过绑定服务进行执行进程间通信。与启动服务不同的是绑定服务的生命周期通常只在为其他应用组件(如Activity)服务时处于活动状态,不会无限期在后台运行,也就是说宿主(如Activity)解除绑定后,绑定服务就会被销毁。我们必须提供一个 IBinder接口的实现类,该类用以提供客户端用来与服务进行交互的编程接口。
注:通常情况下我们应该在客户端生命周期(如Activity的生命周期)的引入 (bring-up) 和退出 (tear-down) 时刻设置绑定和取消绑定操作,以便控制绑定状态下的Service,一般有以下两种情况:1、如果只需要在 Activity 可见时与服务交互,则应在 onStart() 期间绑定,在 onStop() 期间取消绑定。2、如果希望 Activity 在后台停止运行状态下仍可接收响应,则可在 onCreate() 期间绑定,在 onDestroy() 期间取消绑定。需要注意的是,这意味着 Activity 在其整个运行过程中(甚至包括后台运行期间)都需要使用服务,因此如果服务位于其他进程内,那么当提高该进程的权重时,系统很可能会终止该进程。
切勿在 Activity 的 onResume() 和 onPause() 期间绑定和取消绑定。
应该始终捕获 DeadObjectException DeadObjectException 异常,该异常是在连接中断时引发的,表示调用的对象已死亡,也就是Service对象已销毁,这是远程方法引发的唯一异常。
应用组件(客户端)可通过调用 bindService() 绑定到服务,Android 系统随后调用服务的 onBind() 方法,该方法返回用于与服务交互的 IBinder,而该绑定是异步执行的。
具体流程:
首先要创建服务,必须创建 Service 的子类(或使用它的一个现有子类如IntentService),服务中有4个方法。
onBind(Intent intent)(只有绑定服务返回)
当另一个组件想通过调用 bindService() 与服务绑定(例如执行 RPC)时,系统将调用此方法。在此方法的实现中,必须返回 一个IBinder 接口的实现类,供客户端用来与服务进行通信。无论是启动状态还是绑定状态,此方法必须重写,但在启动状态的情况下直接返回 null。
返回类型IBinder接口的实现类,IBinder可以通过三种方法定义,扩展 Binder 类,如果服务是提供给自有应用专用的,并且Service(服务端)与客户端相同的进程中运行(常见情况),则应通过扩展 Binder 类并从 onBind() 返回它的一个实例来创建接口。客户端收到 Binder 后,可利用它直接访问 Binder 实现中以及Service 中可用的公共方法。如果我们的服务只是自有应用的后台工作线程,则优先采用这种方法。 不采用该方式创建接口的唯一原因是,服务被其他应用或不同的进程调用。使用 Messenger,Messenger可以翻译为信使,通过它可以在不同的进程中共传递Message对象(Handler中的Messager,因此 Handler 是 Messenger 的基础),在Message中可以存放我们需要传递的数据,然后在进程间传递。如果需要让接口跨不同的进程工作,则可使用 Messenger 为服务创建接口,客户端就可利用 Message 对象向服务发送命令。同时客户端也可定义自有 Messenger,以便服务回传消息。这是执行进程间通信 (IPC) 的最简单方法,因为 Messenger 会在单一线程中创建包含所有请求的队列,也就是说Messenger是以串行的方式处理客户端发来的消息,这样我们就不必对服务进行线程安全设计了。使用 AIDL,由于Messenger是以串行的方式处理客户端发来的消息,如果当前有大量消息同时发送到Service(服务端),Service仍然只能一个个处理,这也就是Messenger跨进程通信的缺点了,因此如果有大量并发请求,Messenger就会显得力不从心了,这时AIDL(Android 接口定义语言)就派上用场了, 但实际上Messenger 的跨进程方式其底层实现 就是AIDL,只不过android系统帮我们封装成透明的Messenger罢了 。因此,如果我们想让服务同时处理多个请求,则应该使用 AIDL。 在此情况下,服务必须具备多线程处理能力,并采用线程安全式设计。使用AIDL必须创建一个定义编程接口的 .aidl 文件。Android SDK 工具利用该文件生成一个实现接口并处理 IPC 的抽象类,随后可在服务内对其进行扩展。但它可能要求具备多线程处理能力,并可能导致实现的复杂性增加,所以一般不使用该方法。
扩展Binder 类和Messenger的使用进行分析:
扩展Binder 类,如果我们的服务仅供本地应用使用,不需要跨进程工作,则可以实现自有 Binder 类,让客户端通过该类直接访问服务中的公共方法。其使用开发步骤如下:
1.创建BindService服务端,继承自Service并在类中,创建一个实现IBinder 接口的实例对象并提供公共方法给客户端调用
2.从 onBind() 回调方法返回此 Binder 实例。
3.在客户端中,从 onServiceConnected() 回调方法接收 Binder,并使用提供的方法调用绑定服务。
注意:此方式只有在客户端和服务位于同一应用和进程内才有效,如对于需要将 Activity 绑定到在后台播放音乐的自有服务的音乐应用,此方式非常有效。另一点之所以要求服务和客户端必须在同一应用内,是为了便于客户端转换返回的对象和正确调用其 API。服务和客户端还必须在同一进程内,因为此方式不执行任何跨进程编组。
Messenger,如果服务与远程进程(即不同进程间)通信,而不同进程间的通信,最简单的方式就是使用 Messenger 服务提供通信接口,利用此方式,我们无需使用 AIDL 便可执行进程间通信 (IPC)。以下是 Messenger 使用的主要步骤:
1.服务实现一个 Handler,由其接收来自客户端的每个调用的回调
2.Handler 用于创建 Messenger 对象(对 Handler 的引用)
3.Messenger 创建一个 IBinder,服务通过 onBind() 使其返回客户端
4.客户端使用 IBinder 将 Messenger(引用服务的 Handler)实例化,然后使用Messenger将 Message 对象发送给服务
5.服务在其 Handler 中(在 handleMessage() 方法中)接收每个 Message
onCreate()
首次创建服务时,系统将调用此方法来执行一次性设置程序(在调用 onStartCommand() 或 onBind() 之前)。 如果服务已在运行,则不会调用此方法。该方法只被调用一次
onStartCommand(Intent intent, int flags, int startId)(只有启动服务实现)
当另一个组件(如 Activity)通过调用 startService() 请求启动服务时,系统将调用此方法。一旦执行此方法,服务即会启动并可在后台无限期运行。 如果自己实现此方法,则需要在服务工作完成后,通过调用 stopSelf() 或 stopService() 来停止服务。(在绑定状态下,无需实现此方法。)
参数含义,intent :启动时,启动组件传递过来的Intent,如Activity可利用Intent封装所需要的参数并传递给Service。flags:表示启动请求时是否有额外数据,可选值有 0,START_FLAG_REDELIVERY,START_FLAG_RETRY。0,这个值代表没有。START_FLAG_REDELIVERY,这个值代表了onStartCommand方法的返回值为START_REDELIVER_INTENT,而且在上一次服务被杀死前会去调用stopSelf方法停止服务。其中START_REDELIVER_INTENT意味着当Service因内存不足而被系统kill后,则会重建服务,并通过传递给服务的最后一个 Intent 调用 onStartCommand(),此时Intent时有值的。START_FLAG_RETRY,这个值代表当onStartCommand调用后一直没有返回值时,会尝试重新去调用onStartCommand()。startId : 指明当前服务的唯一ID,与stopSelfResult (int startId)配合使用,stopSelfResult 可以更安全地根据ID停止服务。
返回值的类型,它有三种可选值,START_STICKY,START_NOT_STICKY,START_REDELIVER_INTENT。START_STICKY,当Service因内存不足而被系统kill后,一段时间后内存再次空闲时,系统将会尝试重新创建此Service,一旦创建成功后将回调onStartCommand方法,但其中的Intent将是null,除非有挂起的Intent,如pendingintent,这个状态下比较适用于不执行命令、但无限期运行并等待作业的媒体播放器或类似服务。START_NOT_STICKY,当Service因内存不足而被系统kill后,即使系统内存再次空闲时,系统也不会尝试重新创建此Service。除非程序中再次调用startService启动此Service,这是最安全的选项,可以避免在不必要时以及应用能够轻松重启所有未完成的作业时运行服务。START_REDELIVER_INTENT,当Service因内存不足而被系统kill后,则会重建服务,并通过传递给服务的最后一个 Intent 调用 onStartCommand(),任何挂起 Intent均依次传递。与START_STICKY不同的是,其中的传递的Intent将是非空,是最后一次调用startService中的intent。这个值适用于主动执行应该立即恢复的作业(例如下载文件)的服务。
onDestroy()
服务销毁时的回调,服务应该实现此方法来清理所有资源,如线程、注册的侦听器、接收器等,这是服务接收的最后一个调用。
其次是在客户端中(如Activity)的操作。
绑定服务,先把当前Activity绑定到服务LocalService上,绑定服务是通过通过bindService()方法,解绑服务则使用unbindService()方法,这两个方法解析如下:bindService(Intent service, ServiceConnection conn, int flags)该方法执行绑定服务操作,其中Intent是我们要绑定的服务(也就是LocalService)的意图,而ServiceConnection代表与服务的连接,它只有两个方法,前面已分析过,flags则是指定绑定时是否自动创建Service。0代表不自动创建、BIND_AUTO_CREATE则代表自动创建。unbindService(ServiceConnection conn)
该方法执行解除绑定的操作,其中ServiceConnection代表与服务的连接,它只有两个方法,前面已分析过。之后我们创建了一个ServiceConnection对象,该代表与服务的连接,它只有两个方法, onServiceConnected和onServiceDisconnected。onServiceConnected(ComponentName name, IBinder service)系统会调用该方法以传递服务的 onBind() 方法返回的 IBinder。其中service便是服务端返回的IBinder实现类对象,通过该对象我们便可以调用获取LocalService实例对象,进而调用服务端的公共方法。而ComponentName是一个封装了组件(Activity, Service, BroadcastReceiver, or ContentProvider)信息的类,如包名,组件描述等信息,较少使用该参数。
onServiceDisconnected(ComponentName name)Android 系统会在与服务的连接意外中断时(例如当服务崩溃或被终止时)调用该方法。注意:当客户端取消绑定时,系统“绝对不会”调用该方法。Activity通过bindService()绑定到LocalService后ServiceConnection#onServiceConnected()便会被回调并可以获取到LocalService实例对象mService,之后我们就可以调用LocalService服务端的公共方法了。
当我们第一次点击绑定服务时,LocalService服务端的onCreate()、onBind方法会依次被调用,此时客户端的ServiceConnection#onServiceConnected()被调用并返回LocalBinder对象,接着调用LocalBinder#getService方法返回LocalService实例对象,此时客户端便持有了LocalService的实例对象,也就可以任意调用LocalService类中的声明公共方法了。更值得注意的是,我们多次调用bindService方法绑定LocalService服务端,而LocalService得onBind方法只调用了一次,那就是在第一次调用bindService时才会回调onBind方法。接着我们点击获取服务端的数据,从Log中看出我们点击了3次通过getCount()获取了服务端的3个不同数据,最后点击解除绑定,此时LocalService的onUnBind、onDestroy方法依次被回调,并且多次绑定只需一次解绑即可。此情景也就说明了绑定状态下的Service生命周期方法的调用依次为onCreate()、onBind、onUnBind、onDestroy。
启动服务,在Activity中调用startService(intent),startService(intent)中的intent就是唯一的向service交互的方式;service中通过广播来向外发布回调。如果多次启动服务,会导致onStartCommand()被多次调用。
最后是清单文件声明,Service分为启动状态和绑定状态两种,但无论哪种具体的Service启动类型,都是通过继承Service基类自定义而来,也都需要在AndroidManifest.xml中声明。
<service android:enabled=["true" | "false"] android:exported=["true" | "false"] android:icon="drawable resource" android:isolatedProcess=["true" | "false"] android:label="string resource" android:name="string" android:permission="string" android:process="string" > </service>
android:exported:代表是否能被其他应用隐式调用,其默认值是由service中有无intent-filter决定的,如果有intent-filter,默认值为true,否则为false。为false的情况下,即使有intent-filter匹配,也无法打开,即无法被其他应用隐式调用。
android:name:对应Service类名
android:permission:是权限声明
android:process:是否需要在单独的进程中运行,当设置为android:process=”:remote”时,代表Service在单独的进程中运行。注意“:”很重要,它的意思是指要在当前进程名称前面附加上当前的包名,所以“remote”和”:remote”不是同一个意思,前者的进程名称为:remote,而后者的进程名称为:App-packageName:remote。
android:isolatedProcess :设置 true 意味着,服务会在一个特殊的进程下运行,这个进程与系统其他进程分开且没有自己的权限。与其通信的唯一途径是通过服务的API(bind and start)。
android:enabled:是否可以被系统实例化,默认为 true因为父标签 也有 enable 属性,所以必须两个都为默认值 true 的情况下服务才会被激活,否则不会激活。
2.3、两种服务之间的转换
虽然服务的状态有启动和绑定两种,但实际上一个服务可以同时是这两种状态,也就是说,它既可以是启动服务(以无限期运行),也可以是绑定服务。有点需要注意的是Android系统仅会为一个Service创建一个实例对象,所以不管是启动服务还是绑定服务,操作的是同一个Service实例,而且由于绑定服务或者启动服务执行顺序问题将会出现以下两种情况:
先绑定服务后启动服务,如果当前Service实例先以绑定状态运行,然后再以启动状态运行,那么绑定服务将会转为启动服务运行,这时如果之前绑定的宿主(Activity)被销毁了,也不会影响服务的运行,服务还是会一直运行下去,指定收到调用停止服务或者内存不足时才会销毁该服务。
先启动服务后绑定服务,如果当前Service实例先以启动状态运行,然后再以绑定状态运行,当前启动服务并不会转为绑定服务,但是还是会与宿主绑定,只是即使宿主解除绑定后,服务依然按启动服务的生命周期在后台运行,直到有Context调用了stopService()或是服务本身调用了stopSelf()方法抑或内存不足时才会销毁服务。
注:不过无论Service是处于启动状态还是绑定状态,或处于启动并且绑定状态,我们都可以像使用Activity那样通过调用 Intent 来使用服务(即使此服务来自另一应用)。 当然,我们也可以通过清单文件将服务声明为私有服务,阻止其他应用访问。最后这里有点需要特殊说明一下的,由于服务在其托管进程的主线程中运行(UI线程),它既不创建自己的线程,也不在单独的进程中运行(除非另行指定)。 这意味着,如果服务将执行任何耗时事件或阻止性操作(例如 MP3 播放或联网)时,则应在服务内创建新线程来完成这项工作,简而言之,耗时操作应该另起线程执行。只有通过使用单独的线程,才可以降低发生“应用无响应”(ANR) 错误的风险,这样应用的主线程才能专注于用户与 Activity 之间的交互, 以达到更好的用户体验。
2.4、其他
设置服务运行于前台
Android官方给我们提供了两个方法,分别是startForeground()和stopForeground(),这两个方式解析如下:
startForeground(int id, Notification notification)
该方法的作用是把当前服务设置为前台服务,其中id参数代表唯一标识通知的整型数,需要注意的是提供给 startForeground() 的整型 ID 不得为 0,而notification是一个状态栏的通知。
stopForeground(boolean removeNotification)
该方法是用来从前台删除服务,此方法传入一个布尔值,指示是否也删除状态栏通知,true为删除。 注意该方法并不会停止服务。 但是,如果在服务正在前台运行时将其停止,则通知也会被删除。
服务Service与线程Thread的区别
两者概念的迥异,Thread 是程序执行的最小单元,它是分配CPU的基本单位,android系统中UI线程也是线程的一种,当然Thread还可以用于执行一些耗时异步的操作。
Service是Android的一种机制,服务是运行在主线程上的,它是由系统进程托管。它与其他组件之间的通信类似于client和server,是一种轻量级的IPC通信,这种通信的载体是binder,它是在linux层交换信息的一种IPC,而所谓的Service后台任务只不过是指没有UI的组件罢了。
两者的执行任务迥异,在android系统中,线程一般指的是工作线程(即后台线程),而主线程是一种特殊的工作线程,它负责将事件分派给相应的用户界面小工具,如绘图事件及事件响应,因此为了保证应用 UI 的响应能力主线程上不可执行耗时操作。如果执行的操作不能很快完成,则应确保它们在单独的工作线程执行。
Service 则是android系统中的组件,一般情况下它运行于主线程中,因此在Service中是不可以执行耗时操作的,否则系统会报ANR异常,之所以称Service为后台服务,大部分原因是它本身没有UI,用户无法感知(当然也可以利用某些手段让用户知道),但如果需要让Service执行耗时任务,可在Service中开启单独线程去执行。
两者使用场景,当要执行耗时的网络或者数据库查询以及其他阻塞UI线程或密集使用CPU的任务时,都应该使用工作线程(Thread),这样才能保证UI线程不被占用而影响用户体验。
在应用程序中,如果需要长时间的在后台运行,而且不需要交互的情况下,使用服务。比如播放音乐,通过Service+Notification方式在后台执行同时在通知栏显示着。
两者的最佳使用方式,在大部分情况下,Thread和Service都会结合着使用,比如下载文件,一般会通过Service在后台执行+Notification在通知栏显示+Thread异步下载,再如应用程序会维持一个Service来从网络中获取推送服务。在Android官方看来也是如此,所以官网提供了一个Thread与Service的结合来方便我们执行后台耗时任务,它就是IntentService,当然 IntentService并不适用于所有的场景,但它的优点是使用方便、代码简洁,不需要我们创建Service实例并同时也创建线程,某些场景下还是非常赞的!由于IntentService是单个worker thread,所以任务需要排队,因此不适合大多数的多任务情况。
Android 5.0以上的隐式启动问题
将隐式启动转换为显示启动
public static Intent getExplicitIntent(Context context, Intent implicitIntent) { // Retrieve all services that can match the given intent PackageManager pm = context.getPackageManager(); List<ResolveInfo> resolveInfo = pm.queryIntentServices(implicitIntent, 0); // Make sure only one match was found if (resolveInfo == null || resolveInfo.size() != 1) { return null; } // Get component info and create ComponentName ResolveInfo serviceInfo = resolveInfo.get(0); String packageName = serviceInfo.serviceInfo.packageName; String className = serviceInfo.serviceInfo.name; ComponentName component = new ComponentName(packageName, className); // Create a new intent. Use the old one for extras and such reuse Intent explicitIntent = new Intent(implicitIntent); // Set the component to be explicit explicitIntent.setComponent(component); return explicitIntent; }
调用方式如下:
Intent mIntent=new Intent();//辅助Intent mIntent.setAction("com.android.ForegroundService"); final Intent serviceIntent=new Intent(getExplicitIntent(this,mIntent)); startService(serviceIntent);
如何保证服务不被杀死
因内存资源不足而杀死Service,这种情况比较容易处理,可将onStartCommand() 方法的返回值设为 START_STICKY或START_REDELIVER_INTENT ,该值表示服务在内存资源紧张时被杀死后,在内存资源足够时再恢复。也可将Service设置为前台服务,这样就有比较高的优先级,在内存资源紧张时也不会被杀掉。
用户通过 settings -> Apps -> Running -> Stop 方式杀死Service,这种情况是用户手动干预的,不过幸运的是这个过程会执行Service的生命周期,也就是onDestory方法会被调用,这时便可以在 onDestory() 中发送广播重新启动。这样杀死服务后会立即启动。这种方案是行得通的,但为程序更健全,我们可开启两个服务,相互监听,相互启动。服务A监听B的广播来启动B,服务B监听A的广播来启动A。
用户通过 settings -> Apps -> Downloaded -> Force Stop 方式强制性杀死Service
这种方式就比较悲剧了,因为是直接kill运行程序的,不会走生命周期的过程,前面两种情况只要是执行Force Stop ,也就废了。也就是说这种情况下无法让服务重启,或者只能去设置Force Stop 无法操作了。
3、broadcast receiver(广播接收者)
3.1、定义
广播,是一个全局的监听器。Android 广播分为两个角色:广播发送者、广播接收者。用于监听 / 接收应用 App 发出的广播消息,并做出响应。他可以使Android不同组件间的通信(含 :应用内 / 不同应用之间);多线程通信;以及与 Android 系统在特定情况下的通信(如:电话呼入时、网络可用时)。
3.2、原理
Android中的广播使用了设计模式中的观察者模式:基于消息的发布 / 订阅事件模型。因此,Android将广播的发送者 和 接收者 解耦,使得系统方便集成,更易扩展。
3.3、广播使用的具体流程
a、自定义广播接收者BroadcastReceiver,继承BroadcastReceivre基类,必须复写抽象方法onReceive()方法,广播接收器接收到相应广播后,会自动回调 onReceive() 方法;一般情况下,onReceive方法会涉及 与 其他组件之间的交互,如发送Notification、启动Service等;默认情况下,广播接收器运行在 UI 线程,因此,onReceive()方法不能执行耗时操作,否则将导致ANR。
b、广播接收器注册的方式分为两种:静态注册、动态注册:
静态注册,注册方式:在AndroidManifest.xml里通过<receive>标签声明。
属性说明:
<receiver android:enabled=["true" | "false"] //此broadcastReceiver能否接收其他App的发出的广播 //默认值是由receiver中有无intent-filter决定的:如果有intent-filter,默认值为true,否则为false android:exported=["true" | "false"] android:icon="drawable resource" android:label="string resource" //继承BroadcastReceiver子类的类名 android:name=".mBroadcastReceiver" //具有相应权限的广播发送者发送的广播才能被此BroadcastReceiver所接收; android:permission="string" //BroadcastReceiver运行所处的进程 //默认为app的进程,可以指定独立的进程 //注:Android四大基本组件都可以通过此属性指定自己的独立进程 android:process="string" > //用于指定此广播接收器将接收的广播类型 //本示例中给出的是用于接收网络状态改变时发出的广播 <intent-filter> <action android:name="android.net.conn.CONNECTIVITY_CHANGE" /> </intent-filter> </receiver>
动态注册,注册方式:在代码中调用Context.registerReceiver()方法
具体代码如下:
// 选择在Activity生命周期方法中的onResume()中注册
@Override
protected void onResume(){
super.onResume();
// 1. 实例化BroadcastReceiver子类 & IntentFilter mBroadcastReceiver mBroadcastReceiver = new mBroadcastReceiver();
IntentFilter intentFilter = new IntentFilter();
// 2. 设置接收广播的类型 intentFilter.addAction(android.net.conn.CONNECTIVITY_CHANGE);
// 3. 动态注册:调用Context的registerReceiver()方法 registerReceiver(mBroadcastReceiver, intentFilter);
}
// 注册广播后,要在相应位置记得销毁广播
// 即在onPause() 中unregisterReceiver(mBroadcastReceiver)
// 当此Activity实例化时,会动态将MyBroadcastReceiver注册到系统中
// 当此Activity销毁时,动态注册的MyBroadcastReceiver将不再接收到相应的广播。 @Override
protected void onPause() {
super.onPause();
//销毁在onResume()方法中的广播
unregisterReceiver(mBroadcastReceiver);
}
}
注:动态广播最好在Activity 的 onResume()注册、onPause()注销。原因:对于动态广播,有注册就必然得有注销,否则会导致内存泄露(因为onPause()在App死亡前一定会被执行,从而保证广播在App死亡前一定会被注销,从而防止内存泄露)。并且重复注册、重复注销也不允许。
两种注册的区别:在AndroidManifest中进行注册后,不管该应用程序是否处于活动状态,都会进行监听。(如对内存使用的监听)
在代码中进行注册后,当应用程序关闭后,就不再进行监听。(可使APP省电)
c、广播的发送,广播 是 用“意图(Intent)”标识,定义广播的本质就是定义广播所具备的“意图(Intent)”。所以广播发送就是广播发送者将此广播的“意图(Intent)”通过sendBroadcast()方法发送出去。
广播有普通广播(Normal Broadcast)、系统广播(System Broadcast)、有序广播(Ordered Broadcast)、粘性广播(Sticky Broadcast)、App应用内广播(Local Broadcast)五种。或者有序广播和无序广播两种。
普通广播(Normal Broadcast)
即开发者自身定义 intent的广播(最常用)。
发送广播使用如下:
Intent intent = new Intent(); //对应BroadcastReceiver中intentFilter的action intent.setAction(BROADCAST_ACTION); //发送广播 sendBroadcast(intent);
若被注册了的广播接收者中注册时intentFilter的action与上述匹配,则会接收此广播(即进行回调onReceive())。
系统广播(System Broadcast)
Android中内置了多个系统广播:只要涉及到手机的基本操作(如开机、网络状态变化、拍照等等),都会发出相应的广播,每个广播都有特定的Intent - Filter(包括具体的action)。如下:
监听网络变化
android.net.conn.CONNECTIVITY_CHANGE
关闭或打开飞行模式
Intent.ACTION_AIRPLANE_MODE_CHANGED
充电时或电量发生变化
Intent.ACTION_BATTERY_CHANGED
电池电量低
Intent.ACTION_BATTERY_LOW
电池电量充足(即从电量低变化到饱满时会发出广播)
Intent.ACTION_BATTERY_OKAY
系统启动完成后(仅广播一次)
Intent.ACTION_BOOT_COMPLETED
按下照相时的拍照按键(硬件按键)时
Intent.ACTION_CAMERA_BUTTON
屏幕锁屏
Intent.ACTION_CLOSE_SYSTEM_DIALOGS
设备当前设置被改变时(界面语言、设备方向等) Intent.ACTION_CONFIGURATION_CHANGED
插入耳机时
Intent.ACTION_HEADSET_PLUG
未正确移除SD卡但已取出来时(正确移除方法:设置--SD卡和设备内存--卸载SD卡)
Intent.ACTION_MEDIA_BAD_REMOVAL
插入外部储存装置(如SD卡)
Intent.ACTION_MEDIA_CHECKING
成功安装 APK
Intent.ACTION_PACKAGE_ADDED
成功删除 APK
Intent.ACTION_PACKAGE_REMOVED
重启设备
Intent.ACTION_REBOOT
屏幕被关闭
Intent.ACTION_SCREEN_OFF
屏幕被打开
Intent.ACTION_SCREEN_ON
关闭系统时
Intent.ACTION_SHUTDOWN
重启设备
Intent.ACTION_REBOOT
注:当使用系统广播时,只需要在注册广播接收者时定义相关的action即可,并不需要手动发送广播,当系统有相关操作时会自动进行系统广播。
有序广播(Ordered Broadcast)
发送出去的广播被广播接收者按照先后顺序接收,有序是针对广播接收者而言的。接收广播按顺序接收;先接收的广播接收者可以对广播进行截断,即后接收的广播接收者不再接收到此广播;先接收的广播接收者可以对广播进行修改,那么后接收的广播接收者将接收到被修改后的广播。广播接受者接收广播的顺序规则(同时面向静态和动态注册的广播接受者),按照Priority属性值从大-小排序;Priority属性相同者,动态注册的广播优先。
发送广播如下:
有序广播的使用过程与普通广播非常类似,差异仅在于广播的发送方式:sendOrderedBroadcast(intent);
App应用内广播(Local Broadcast)
App应用内广播可理解为一种局部广播,广播的发送者和接收者都同属于一个App。相比于全局广播(普通广播),App应用内广播优势体现在:安全性高 & 效率高。
发送广播方式1----将全局广播设置成局部广播
注册广播时将exported属性设置为false,使得非本App内部发出的此广播不被接收;
在广播发送和接收时,增设相应权限permission,用于权限验证;
发送广播时指定该广播接收器所在的包名,此广播将只会发送到此包中的App内与之相匹配的有效广播接收器中。
通过intent.setPackage(packageName)指定报名
发送广播方式2----使用封装好的LocalBroadcastManager类
使用方式上与全局广播几乎相同,只是注册/取消注册广播接收器和发送广播时将参数的context变成了LocalBroadcastManager的单一实例。对于LocalBroadcastManager方式发送的应用内广播,只能通过LocalBroadcastManager动态注册,不能静态注册。
代码如下:
//步骤1:实例化BroadcastReceiver子类 & IntentFilter
mBroadcastReceiver mBroadcastReceiver = new mBroadcastReceiver();
IntentFilter intentFilter = new IntentFilter();
//步骤2:实例化LocalBroadcastManager的实例
localBroadcastManager = LocalBroadcastManager.getInstance(this);
//步骤3:设置接收广播的类型
intentFilter.addAction(android.net.conn.CONNECTIVITY_CHANGE);
//步骤4:调用LocalBroadcastManager单一实例的registerReceiver()方法进行动态注册
localBroadcastManager.registerReceiver(mBroadcastReceiver, intentFilter);
//取消注册应用内广播接收器
localBroadcastManager.unregisterReceiver(mBroadcastReceiver);
//发送应用内广播
Intent intent = new Intent();
intent.setAction(BROADCAST_ACTION);
localBroadcastManager.sendBroadcast(intent);
粘性广播(Sticky Broadcast)
在Android5.0 & API 21中已经失效
3.4、其他
对于不同注册方式的广播接收器回调OnReceive(Context context,Intent intent)中的context返回值是不一样的:
对于静态注册(全局+应用内广播),回调onReceive(context, intent)中的context返回值是:ReceiverRestrictedContext;
对于全局广播的动态注册,回调onReceive(context, intent)中的context返回值是:Activity Context;
对于应用内广播的动态注册(LocalBroadcastManager方式),回调onReceive(context, intent)中的context返回值是:Application Context。
对于应用内广播的动态注册(非LocalBroadcastManager方式),回调onReceive(context, intent)中的context返回值是:Activity Context。
无序广播
通过Context.sendBroadcast()方法来发送,它是完全异步的。
所有的receivers(接收器)的执行顺序不确定,因此所有的receivers(接收器)接收broadcast的顺序不确定。这种方式效率更高,但是BroadcastReceiver无法使用setResult系列、getResult系列及abort(中止)系列API
有序广播
是通过Context.sendOrderedBroadcast来发送,所有的receiver依次执行。
BroadcastReceiver可以使用setResult系列函数来结果传给下一个BroadcastReceiver,通过getResult系列函数来取得上个BroadcastReceiver返回的结果,并可以abort系列函数来让系统丢弃该广播,使用该广播不再传送到别的BroadcastReceiver。可以通过在intent-filter中设置android:priority属性来设置receiver的优先级,优先级相同的receiver其执行顺序不确定。如果BroadcastReceiver是代码中注册的话,且其intent-filter拥有相同android:priority属性的话,先注册的将先收到广播。
有序广播,即从优先级别最高的广播接收器开始接收,接收完了如果没有丢弃,就下传给下一个次高优先级别的广播接收器进行处理,依次类推,直到最后。如果多个应用程序设置的优先级别相同,则谁先注册的广播,谁就可以优先接收到广播。
4、content provider
4.1、定义
ContentProvider为存储和获取数据提供统一的接口,可以在不同的应用程序之间共享数据。(Android内置的许多数据都是使用ContentProvider形式,供开发者调用的 (如视频,音频,图片,通讯录等))使用表的形式来组织数据,无论数据来源是什么,ContentProvider都会认为是一种表(把数据组织成表格)。提供的方法:query:查询、insert:插入、update:更新、delete:删除、getType:得到数据类型、onCreate:创建数据时调用的回调函数。每个ContentProvider都有一个公共的URI,这个URI用于表示这个ContentProvider所提供的数据(Android所提供的ContentProvider都存放在android.provider包当中),ContentProvider分为系统的和自定义的。
注:虽然使用其他方法也可以对外共享数据,但数据访问方式会因数据存储的方式而不同。采用文件方式对外共享数据,需要进行文件操作读写数据;采用sharedpreferences共享数据,需要使用sharedpreferences API读写数据。使用ContentProvider共享数据的好处是统一了数据访问方式。
4.2、Uri、UriMatcher、ContentUris
Uri:
代表了要操作的数据,Uri 主要包含了两部分信息:需要操作的ContentProvider;对ContentProvider中的什么数据进行操作。Uri由三部分组成,ContentProvider的scheme已经由Android所规定,scheme为content://。主机名(authorities)用于唯一标识这个ContentProvider,外部调用者可以根据它找到对应的内容提供者(ContentProvider)。路径(Path)可以用来表示我们要操作的数据,路径的构建应该根据业务而定(如要操作Person表中ID 为10的记录,可以构建这样的路径:/person/id/10,也可以为/prson/10,构建什么样的路径需要与UriMatcher中注册的匹配Uri相一致)。
注:如果要把一个字符串转换成Uri,可以使用Uri类中的parse()方法。如Uri uri = Uri.parse("content://com.ljq.provider.personprovider/person"
UriMatcher:
操作Uri的工具类,UriMatcher类用于匹配Uri。用法如下,
首先第一步把你需要匹配Uri路径全部给注册上,如下:
//常量UriMatcher.NO_MATCH表示不匹配任何路径的返回码
UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
//添加需要匹配uri,如果匹配就会返回匹配码
//如果match()方法匹配content://com.ljq.provider.personprovider/person路径返回
//匹配码为1,匹配Uri注册如下:
sMatcher.addURI("com.ljq.provider.personprovider", person", 1);
//如果match()方法匹配content://com.ljq.provider.personprovider/person/230
//路径,返回匹配码为2,配Uri注册如下:
sMatcher.addURI("com.ljq.provider.personprovider", "person/#", 2);
//#号为通配符
//传入Uri,进行匹配
switch(sMatcher.match(Uri.parse("content://com.ljq.provider.personprovider/person/10"))) {
case 1
break;
case 2
break;
default://不匹配
break;
}
注册完需要匹配的Uri后,就可以使用sMatcher.match(uri)对输入的uri进
匹配,如果匹配正确就返回匹配码,匹配码是addUri()方法传入的第三个
数,假设匹配content://com.ljq.provider.personprovider/person 路径,返回
匹配码为1。
ContentUris:
操作Uri的工具类,ContentUris用于操作Uri后面的ID 部分。用法如下:
Uri uri = Uri.parse("content://com.ljq.provider.personprovider/person")
Uri resultUri = ContentUris.withAppendedId(uri, 10);
//生成后的Uri为content://com.ljq.provider.personprovider/person/10
//parseId(uri)用于从路径中获取Id,如下:
Uri uri = Uri.parse("content://com.ljq.provider.personprovider/person/10"long personid = ContentUris.parseId(uri);
4.3、系统ContentProvider的实现步骤(系统中数据就是一些联系人,信息等)
以查询和插入联系人为例:
/*查询*/
private void select() {
ContentResolver cr=getContentResolver();
/*第一个参数为Uri,第二个参数为查询哪些列,第三个参数为查询条件,第五个参数为排序方式*/
/*查询id和姓名*/
Cursor c=cr.query(Contacts.CONTENT_URI,new String[] {Contacts._ID,Contacts.DISPLAY_NAME},null,null,null);
if(c!=null){
while(c.moveToNext()){
int id=c.getInt(c.getColumnIndex("_ID"));
String name=c.getString(c.getColumnIndex("DISPLAY_NAME"));
Log.i("MainActivity","_ID "+id);
Log.i("MainActivity","DISPLAY_NAME "+name);
/*根据id查询联系人的电话号码*/
Cursor everyName=cr.query(Phone.CONTENT_URI,new String[]
{Phone.NUMBER,Phone.TYPE},Phone.CONTACT_ID+"="+id,null,null);
if(everyName!=null){
while(everyName.moveToNext()){
/*查询电话号码类型,type为0表示座机电话,type为1表示移动电话*/
int type=everyName.getInt(everyName.getColumnIndex(Phone.TYPE));
if(type==Phone.TYPE_HOME){
Log.i("MainActivity","座机电话"+everyName.getString(everyName.getColumnIndex(Phone.NUMBER)));
}else if(type==Phone.TYPE_MOBILE){
Log.i("MainActivity","移动电话"+everyName.getString(everyName.getColumnIndex(Phone.NUMBER)));
} }
everyName.close(); }
/*根据id查询联系人的邮箱地址*/
Cursor everyEmail=cr.query(Email.CONTENT_URI,new String[]{Email.DATA,Email.TYPE},Email.CONTACT_ID+"="+id,null,null);
if(everyEmail!=null){
while(everyEmail.moveToNext()){
int type=everyEmail.getInt(everyEmail.getColumnIndex(Email.TYPE)); if(type==Email.TYPE_WORK){
Log.i("MainActivity","工作邮 箱"+everyEmail.getString(everyEmail.getColumnIndex(Email.DATA)));
} }
everyEmail.close(); } }
c.close(); } }
/*插入*/
private void insert() {
ContentResolver cr=getContentResolver();
/*向联系人中插入一行数据*/
ContentValues values=new ContentValues();
Uri uri=cr.insert(RawContacts.CONTENT_URI, values);
Long raw_contact_id=ContentUris.parseId(uri);
values.clear();
//插入姓名
values.put(StructuredName.RAW_CONTACT_ID,raw_contact_id);
values.put(StructuredName.DISPLAY_NAME,"张三");
values.put(StructuredName.MIMETYPE,StructuredName.CONTENT_ITEM_TYPE);
uri=cr.insert(Data.CONTENT_URI,values);
//插入电话信息
values.clear();
values.put(Phone.RAW_CONTACT_ID,raw_contact_id);
values.put(Phone.NUMBER,"13333333333");//添加号码
values.put(Phone.MIMETYPE,Phone.CONTENT_ITEM_TYPE);
values.put(Phone.TYPE,Phone.TYPE_MOBILE);//添加号码类型
uri=cr.insert(Data.CONTENT_URI, values);
}
4.4、自定义ContentProvider的实现步骤
a. 定义一个 CONTENT_URI 常量,并且是public static final的Uri类型的类变量,最好的方案是以类的全名称,实例如下:
// CONTENT_URI 的字符串必须是唯一 public static final Uri CONTENT_URI = Uri.parse("content://com.WangWeiDa.MyContentprovider"); // 如果有子表,URI为: public static final Uri CONTENT_URI = Uri.parse("content://com.WangWeiDa.MyContentProvider/users");
b. 定义一个继承自ContentProvider的类,实例如下:
public class MyContentProvider extends ContentProvider { ... }
c. 实现MyContentProvider的所有方法,(大多数Content Provider使用 Android文件系统或SQLite数据库来保持数据,但是你也可以以任何你想要的方式来存储。)实例如下(SQLite数据库):
先为ContentProvider提供一个常量类 MyContentProviderMetaData.java
public class MyContentProviderMetaData { //URI的指定,此处的字符串必须和声明的authorities一致 public static final String AUTHORITIES = "com.zhuanghongji.app.MyContentProvider"; //数据库名称 public static final String DATABASE_NAME = "myContentProvider.db"; //数据库的版本 public static final int DATABASE_VERSION = 1; //表名 public static final String USERS_TABLE_NAME = "user"; public static final class UserTableMetaData implements BaseColumns{ //表名 public static final String TABLE_NAME = "user"; //访问该ContentProvider的URI public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITIES + "/user"); //该ContentProvider所返回的数据类型的定义 public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.myprovider.user"; public static final String CONTENT_TYPE_ITEM = "vnd.android.cursor.item/vnd.myprovider.user"; //列名 public static final String USER_NAME = "name"; //默认的排序方法 public static final String DEFAULT_SORT_ORDER = "_id desc"; } }
再具体实现,里面的方法
public class MyContentProvider extends ContentProvider { //访问表的所有列 public static final int INCOMING_USER_COLLECTION = 1; //访问单独的列 public static final int INCOMING_USER_SINGLE = 2; //操作URI的类 public static final UriMatcher uriMatcher; //为UriMatcher添加自定义的URI static{ uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); uriMatcher.addURI(MyContentProviderMetaData.AUTHORITIES,"/user", INCOMING_USER_COLLECTION); uriMatcher.addURI(MyContentProviderMetaData.AUTHORITIES,"/user/#", INCOMING_USER_SINGLE); } private DatabaseHelper dh; //为数据库表字段起别名 public static HashMap userProjectionMap; static { userProjectionMap = new HashMap(); userProjectionMap.put(UserTableMetaData._ID,UserTableMetaData._ID); userProjectionMap.put(UserTableMetaData.USER_NAME, UserTableMetaData.USER_NAME); } /** * 删除表数据 */ @Override public int delete(Uri uri, String selection, String[] selectionArgs) { System.out.println("delete"); //得到一个可写的数据库 SQLiteDatabase db = dh.getWritableDatabase(); //执行删除,得到删除的行数 int count = db.delete(UserTableMetaData.TABLE_NAME, selection, selectionArgs); return count; } /** * 数据库访问类型 */ @Override public String getType(Uri uri) { System.out.println("getType"); //根据用户请求,得到数据类型 switch (uriMatcher.match(uri)) { case INCOMING_USER_COLLECTION: return MyContentProviderMetaData.UserTableMetaData.CONTENT_TYPE; case INCOMING_USER_SINGLE: return MyContentProviderMetaData.UserTableMetaData.CONTENT_TYPE_ITEM; default: throw new IllegalArgumentException("UnKnown URI"+uri); } } /** * 插入数据 */ @Override public Uri insert(Uri uri, ContentValues values) { //得到一个可写的数据库 SQLiteDatabase db = dh.getWritableDatabase(); //向指定的表插入数据,得到返回的Id long rowId = db.insert(UserTableMetaData.TABLE_NAME, null, values); if(rowId > 0){ // 判断插入是否执行成功 //如果添加成功,利用新添加的Id和 Uri insertedUserUri = ContentUris.withAppendedId(UserTableMetaData.CONTENT_URI, rowId); //通知监听器,数据已经改变 getContext().getContentResolver().notifyChange(insertedUserUri, null); return insertedUserUri; } return uri; } /** * 创建ContentProvider时调用的回调函数 */ @Override public boolean onCreate() { System.out.println("onCreate"); //得到数据库帮助类 dh = new DatabaseHelper(getContext(),MyContentProviderMetaData.DATABASE_NAME); return false; } /** * 查询数据库 */ @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { //创建一个执行查询的Sqlite SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); //判断用户请求,查询所有还是单个 switch(uriMatcher.match(uri)){ case INCOMING_USER_COLLECTION: //设置要查询的表名 qb.setTables(UserTableMetaData.TABLE_NAME); //设置表字段的别名 qb.setProjectionMap(userProjectionMap); break; case INCOMING_USER_SINGLE: qb.setTables(UserTableMetaData.TABLE_NAME); qb.setProjectionMap(userProjectionMap); // 追加条件,getPathSegments()得到用户请求的Uri地址截取的数组, // get(1)得到去掉地址中/以后的第二个元素 qb.appendWhere(UserTableMetaData._ID + "=" + uri.getPathSegments().get(1)); break; } //设置排序 String orderBy; if(TextUtils.isEmpty(sortOrder)){ orderBy = UserTableMetaData.DEFAULT_SORT_ORDER; } else { orderBy = sortOrder; } //得到一个可读的数据库 SQLiteDatabase db = dh.getReadableDatabase(); //执行查询,把输入传入 Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, orderBy); //设置监听 c.setNotificationUri(getContext().getContentResolver(), uri); return c; } /** * 更新数据库 */ @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { System.out.println("update"); //得到一个可写的数据库 SQLiteDatabase db = dh.getWritableDatabase(); //执行更新语句,得到更新的条数 int count = db.update(UserTableMetaData.TABLE_NAME, values, selection, selectionArgs); return count; } }
d. 在AndroidMinifest.xml中进行声明,实例如下:
<provider android:name=".MyContentProvider" android:authorities="com.zhuanghongji.app.MyContentProvider" />
e.涉及到的类
ContentProvider类主要方法的介绍:
public boolean onCreate(),在ContentProvider创建后就会被调用,而ContentProvider是在其它应用第一次访问它时被创建;
public Uri insert(Uri uri, ContentValues values),供外部应用向ContentProvider添加数据;
public int delete(Uri uri, String selection, String[] selectionArgs),供外部应用从ContentProvider删除数据;
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs),供外部应用更新ContentProvider中的数据;
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder),供外部应用从ContentProvider中获取数据;
public String getType(Uri uri),返回当前Uri所代表数据的MIME类型;
如果操作的数据属于集合类型,那么MIME类型字符串应该以vnd.android.cursor.dir/开头,如要得到所有tablename记录的Uri为content://com.wang.provider.myprovider/tablename,那么返回的MIME类型字符串应该为:vnd.android.cursor.dir/table。
如果要操作的数据属于非集合类型数据,那么MIME类型字符串应该以vnd.android.cursor.item/开头,如得到id为10的tablename记录,Uri为content://com.wang.provider.myprovider/tablename/10,那么返回的MIME类型字符串为:vnd.android.cursor.item/tablename 。
注:如果要处理的数据类型是一种比较新的类型,你就必须先定义一个新的MIME类型,以供ContentProvider.geType(url)来返回。MIME类型有两种形式:一种是为指定的单个记录的,另一种是为多条记录的。
这里给出一种常用的格式:
第一种:
vnd.android.cursor.item/vnd.yourcompanyname.contenttype // 单个记录的MIME类型
比如, 一个请求列车信息的URI
content://com.example.transportationprovider/trains/122
可能就会返回
typevnd.android.cursor.item/vnd.example.rail
这样一个MIME类型
第二种:
vnd.android.cursor.dir/vnd.yourcompanyname.contenttype // 多个记录的MIME类型
比如, 一个请求所有列车信息的URI
content://com.example.transportationprovider/trains
可能就会返回
vnd.android.cursor.dir/vnd.example.rail
这样一个MIME 类型
ContentResolver操作数据:
当外部应用需要对ContentProvider中的数据进行添加、删除、修改及查询操作时,可以使用ContentResolver 类来完成。而要获取ContentResolver 对象,可以使用Activity提供的getContentResolver()方法。
ContentResolver 类提供了与ContentProvider类相同签名的四个方法:
public Uri insert(Uri uri, ContentValues values),往ContentProvider添加数据;
public int delete(Uri uri, String selection, String[] selectionArgs),从ContentProvider删除数据;
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs),更新ContentProvider中的数据;
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder),从ContentProvider中获取数据;
这些方法的第一个参数为Uri,代表要操作的ContentProvider和对其中的什么数据进行操作,其实和ContentProvider里面的方法是一样的。他们所对应的数据,最终会被传到我们在之前程序里面定义的那个ContentProvider类的方法,假设给定的是:Uri.parse("content://com.wang.provider.myprovider/tablename/10"),那么将会对主机名为com.wang.provider.myprovider的ContentProvider进行操作,操作的数据为tablename表中id为10的记录。
使用ContentResolver对ContentProvider中的数据进行操作的代码如下:
ContentResolver resolver = getContentResolver(); Uri uri = Uri.parse("content://com.wang.provider.myprovider/tablename"); //添加一条记录 ContentValues values = new ContentValues(); values.put("name", "wang1"); values.put("age", 28); resolver.insert(uri, values); //获取tablename表中所有记录 Cursor cursor = resolver.query(uri, null, null, null, "tablename data"); while(cursor.moveToNext()){ Log.i("ContentTest", "tablename_id="+ cursor.getInt(0)+ ", name="+ cursor.getString(1));} //把id为1的记录的name字段值更改新为zhang1 ContentValues updateValues = new ContentValues(); updateValues.put("name", "zhang1"); Uri updateIdUri = ContentUris.withAppendedId(uri, 2); resolver.update(updateIdUri, updateValues, null, null); //删除id为2的记录,即字段age Uri deleteIdUri = ContentUris.withAppendedId(uri, 2); resolver.delete(deleteIdUri, null, null);
监听数据变化:
如果ContentProvider的访问者需要知道数据发生的变化,可以在ContentProvider发生数据变化时调用getContentResolver().notifyChange(uri, null)来通知注册在此URI上的访问者。只给出类中监听部分的代码:
public class MyProvider extends ContentProvider { public Uri insert(Uri uri, ContentValues values) { db.insert("tablename", "tablenameid", values); getContext().getContentResolver().notifyChange(uri, null); } }
而访问者必须使用ContentObserver对数据(数据采用uri描述)进行监听,当监听到数据变化通知时,系统就会调用ContentObserver的onChange()方法:
getContentResolver().registerContentObserver(Uri.parse("content://com.ljq.providers.personprovider/person"), true, new PersonObserver(new Handler())); public class PersonObserver extends ContentObserver{ public PersonObserver(Handler handler) { super(handler); } public void onChange(boolean selfChange) { //to do something } }
f.具体的调用步骤
为应用程序添加ContentProvider的访问权限;
通过getContentResolver()方法得到ContentResolver对象;
调用ContentResolver类的query()方法查询数据,该方法会返回一个Cursor对象;
对得到的Cursor对象进行分析,得到需要的数据;
调用Cursor类的close()方法将Cursor对象关闭。