安卓常用组件(启停活动页面、活动之间传递信息、收发应用广播、操作后台服务)

启停活动页面

Activity的启动和结束

页面跳转可以使用startActivity接口,具体格式为startActivity(new Intent(this, 目标页面.class));
关闭一个页面可以直接调用finish();方法即可退出页面。

Activity的生命周期

页面在安卓有个新的名字叫活动,因为每个页面安卓都为它划分了不同生命周期。下面是不同生命周期的介绍:

方法名称说明
onCreate创建页面。把页面布局加载进内存,进入了初始状态。
onStart开始活动。把活动页面显示在屏幕上,进入了就绪状态。
onResume恢复活动。活动页面进入活跃状态,能够与用户正常交互,例如允许响应用户点击动作、允许用户输入文字等。
onPause暂停活动。页面进入暂停状态,无法与用户正常交互。
onStop停止活动。页面将不在屏幕上显示。
onDestrory销毁活动。回收活动占用的系统资源,把页面从内存中清除。
onRestart重启活动。重新加载内存中的页面数据。
onNewItent重用已有的活动实例。

函数定义如下:

    @Override
    protected void onCreate(Bundle savedInstanceState) { // 创建活动
        super.onCreate(savedInstanceState);
    }

    @Override
    protected void onStart() { // 开始活动
        super.onStart();
    }

    @Override
    protected void onStop() { // 停止活动
        super.onStop();
    }

    @Override
    protected void onResume() { // 恢复活动
        super.onResume();
    }

    @Override
    protected void onPause() { // 暂停活动
        super.onPause();
    }

    @Override
    protected void onRestart() { // 重启活动
        super.onRestart();
    }

    @Override
    protected void onDestroy() { // 销毁活动
        super.onDestroy();
    }

    @Override
    protected void onNewIntent(Intent intent) { // 重用已有的活动实例
        super.onNewIntent(intent);
    }

Activity的启动模式

App如果依次打开两个活动,那么活动栈则会发生如下变化:
在这里插入图片描述
按下返回键,依次结束已经打开的两个活动,活动栈的变化情况如下:
在这里插入图片描述
如上所述的情况为页面打开的标准模式,Android允许在创建活动时指定活动的启动模式,通过启动模式控制活动的出入栈行为。
Android提供了两种方法用于设置活动页面的启动模式:一种是修改AndroidManifest.xml,在指定的activity节点添加属性android:launchMode,表示以哪种模式运行此活动;另一种是在代码中调用Intent#setFlags方法,表明后续打开的活动页面采用该启动标志。下面是两种打开方式的详细说明。

在配置文件中指定启动模式

打开AndroidManifest.xml,给activity的节点添加属性android:launchMode,属性值填入standard表示采取标准模式。实际使用实例如下:

<activity android:name=".JumpFirstActivity" android:launchMode="standard" android:exported="false" />

其中android:launchMode属性的取值说明见下表:

android:launchMode说明
standard标准模式,无论何时启动哪个模式,都是重新创建该页面的实例并放入栈顶。不指定android:launchMode属性,则默认为标准模式
singleTop启动新活动时,判断如果栈顶正好就是该活动的实例,则重用该实例;否则创建新的实例并放入栈顶,也就是按照standrad模式处理
singleTask启动新活动时,判断如果栈中存在该活动的实例,则重用该实例,并清除位于该实例上面的所有实例;否则按照standrad模式处理
singleInstance启动新活动时,将该活动的实例放入一个新栈中,原栈的实例列表保持不变

在代码里面设置启动标志

在java代码里,先调用Intent#setFlags方法设置启动标志,再将Intent对象传给startActivity方法。具体代码示例如下:

// 创建一个意图,准备跳转到指定的活动页面
Intent intent = new Intent(this, JumpSecondActivity.class);
startActivity(intent); // 跳转到意图对象指定的活动页面

由于在AndroidManifest.xml文件中对启动标志不能修改,因此通过代码设置的方式满足了不同情形下的启动模式。适用于Intent#setFlags方法的几种启动标志取值说明如下:

Intent类的启动标志说明
Intent.FLAG_ACTIVITY_NEW_TASK开辟一个新的任务栈,该值类似于android:launchMode="standard" ;不同的是,如果原来不存在活动栈,则会创建一个新栈
Intent.FLAG_ACTIVITY_SINGLE_TOP当栈顶为待跳转的活动实例之时,则重用栈顶的实例。该值等同于android:launchMode="singleTop"
Intent.FLAG_ACTIVITY_CLEAR_TOP当栈中存在跳转的活动实例时,则重新创建一个新实例,并清除原实例上方的所有实例。该值与android:launchMode="singleTask" 类似,但singleTask采取onNewIntent方法启用原任务,而FLAG_ACTIVITY_CLEAR_TOP 采取先调用onDestroy再调用onCreate来创建任务
Intent.FLAG_ACTIVITY_NO_HISTORY该标志与android:launchMode="standard" 情况类似,但栈中不保存新启动的活动实例,这样下次无论以何种方式再启动该实例,也要走standard模式的完整流程
Intent.FLAG_ACTIVITY_CLEAR_TASK该标志跳转到新界面时,栈中原有的实例都被清空。注意该标志需要结合FLAG_ACTIVITY_NEW_TASK使用,即setFlags方法的参数为Intent.FLAG_ACTIVITY_CLEAR_TASK|Intent.FLAG_ACTIVITY_NEW_TASK
在两个活动之间交替跳转

假设活动A有个按钮,点击就会跳转到活动B;而活动B也有个按钮,点击也会跳转到活动A。假设现在我们从首页打开活动A后进行两个页面间的轮流跳转,跳转流程为:活动A->活动B->活动A->活动B->活动A->活动B->… …多次跳转后想回到首页,那么按照默认的流程应该是这样的:… …活动B->活动A->活动B->活动A->活动B->活动A->首页。
按道理来说,无论跳转多少次,我们想要的效果是:活动B->活动A->首页,而不是没有意义地在活动A、B直接多次跳转。对于这种不希望重复返回地情况,可以设置启动标志FLAG_ACTIVITY_CLEAR_TOP ,即使活动栈里面存在待跳转的活动实例,也会重新创建该活动实例,并清除原实例上方的所有实例,保证栈中最多只有该活动的唯一实例,从而避免了无所谓的重复返回。
那么就可以将活动A内部的跳转代码改为如下:

// 创建一个意图,准备跳转到指定的活动页面
Intent intent = new Intent(this, JumpSecondActivity.class);
// 栈中存在待跳转的活动实例时,则重新创建该活动的实例,并清除原实例上方的所有实例
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent); // 跳转到意图对象指定的活动页面

而活动B也可以进行同样的修改:

// 创建一个意图,准备跳转到对应活动界面
Intent intent = new Intent(this, JumpFirstActivity.class);
// 当栈中存在待跳转的活动实例时,则重新创建该活动的实例,并清除原实例上方的所有实例
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); // 设置启动标志
// 打开界面
startActivity(intent);

这样当两个活动的跳转代码都设置FLAG_ACTIVITY_CLEAR_TOP ,每个活动就会只返回一次。

登录成功后不在返回登录页面

当我们登陆App后,退出软件时不必再回到登录界面,应该直接退出App。对于不退回登录页面的情况,可以设置启动标志Intent.FLAG_ACTIVITY_CLEAR_TASK,该标志会清空当前活动栈的所有实例。不过清空之后,就意味着当前栈没法用了,必须另外找个活动栈,可以同时设置启动标志Intent.FLAG_ACTIVITY_NEW_TASK,该标志用于开辟新的任务活动栈。那么离开登录页面的跳转代码就可以改动如下:

// 创建一个意图,跳转到对应界面
Intent intent = new Intent(this, LoginSuccessActivity.class);
// 设置启动标志:跳转到新的页面时,栈中原有的实例全部清除,同时开辟新的活动栈
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent); // 跳转到意图指定的活动页面

实测效果,登录成功进入首页后,在界面进行返回操作没回到登陆界面直接退出了App。

在活动之间传递信息

显示Intent和隐式Intent

Intent中文名意思是意图,简单点说就是传递消息。Intent是各个组件之间信息沟通的桥梁,既能在Activity之间沟通,也能在Activity与Service之间沟通,也能在Activity与Broadcast之间沟通。总之Intent用于Android各组件之间的通信,主要完成下列3部分工作:

  1. 标明本次通信请求从哪里来、到哪里去要怎么走。
  2. 发起方携带本次通信需要的数据内容,接收方从收到的意图中解析数据。
  3. 发起方若想判断接收方的处理结果,意图就要负责让接收方传回应答的数据内容。

为了做好以上工作,就要给意图配上必须的装备,Intent的组成部分见下表:

元素名称设置方法说明与用途
ComponentsetComponent组件,它指定意图的来源与目标
ActionsetAction动作,它指定意图的动作行为
DatasetData组件,它指定意图的来源与目标
CategoryaddCategory即Uri,它指定动作要操作的数据路径
TypesetType类别,它指定意图的操作类别
ExtrasputExtras扩展信息,它指定转载的包裹信息
FlagssetFlags标志位,它指定活动的启动标志

指定意图对象的目标有两种表达方式,一种是显示Intent,另一种是隐式Intent。

显示Intent,直接指定来源活动与目标活动,属于精准匹配

在构建一个意图对象时,需要指定两个参数,第一个参数表示跳转的来源页面,即"来源Activity.this";第二个参数表示待跳转的页面,即"目标Activity.class"。具体意图构建方式有如下3种:

  1. 在Intent的构造函数中指定,示例代码如下:
// 创建一个目标确定的意图
Intent intent = new Intent(this, ActNextActivity.class);
  1. 调用意图对象的setClass方法指定,示例代码如下:
Intent intent = new Intent(); // 创建一个意图
intent.setClass(this, ActNextActivity.class); // 设置意图要跳转的目标活动
  1. 调用意图对象的setComponent方法指定,示例代码如下:
Intent intent = new Intent(); // 创建一个意图
//创建包含目标活动在内的组件名称对象
ComponentName component = new ComponentName(this, ActNextActivity.class);
intent.setComponent(component); // 设置意图携带的组件信息

隐式Intent,没有明确指定要跳转的目标活动,只给出一个动作字符串让系统自动匹配,属于模糊匹配

通常App不希望向外部暴露活动名称,只给出一个实现定义好的标记串,大家都约定好每个标记串对应的活动,隐式Intent便起到了标记过滤的作用。这个动作名称标记串,可以是自已定义的动作,也可以是已有的系统动作。常见的系统动作的取值说明如下:

Intent类的系统动作常量名系统动作的常量值说明
ACTION_MAINandroid.intent.action.MAINApp启动时的入口
ACTION_VIEWandroid.intent.action.VIEW向用户显示数据
ACTION_SENDandroid.intent.action.SEND分享内容
ACTION_CALLandroid.intent.action.CALL直接拨号
ACTION_DIALandroid.intent.action.DIAL准备拨号
ACTION_SENDTOandroid.intent.action.SENDTO发送短信
ACTION_ANSWERandroid.intent.action.ANSWER接听电话

动作名称可以通过构造函数Intent(String action)直接生成意图对象,也可以通过setAction方法指定。对于模糊匹配,有时需要我们通过Uri和Category指定具体路径与门类信息。Uri数据可以通过构造函数Intent(String action, Uri uri)在生成对象时一起指定,也可以通过setData方法指定;Category可以通过addCategory方法指定一个或多个意图,方便一起过滤。
下面是一个用到了Uri调用系统拨号的程序代码例子:

String phoneNo = "12345";
Intent intent = new Intent(); 			// 创建一个新意图
intent.setAction(Intent.ACTION_DIAL); 	// 设置意图动作为准备拨号
Uri uri = Uri.parse("tel:" + phoneNo);	// 声明一个拨号的Uri
intent.setData(uri); 					//设置意图前往路径
startActivity(intent);					// 启动意图通往的活动页面

隐式Intent还可以通过过滤器把不符合匹配条件的过滤掉,剩下符合条件的按照优先顺序调用。在App的AndroidManifest.xml里的intent-filter就是配置文件的过滤器。例如最常见的首页活动MainActivity,它的activity节点下面便设置了action和category的过滤条件。其中android.intent.action.MAIN表示App的 入口动作,而android.intent.category.LAUNCHER表示在桌面上显示App图标,配置样例如下:

<activity
    android:name=".MainActivity"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

普通的活动数据交互

界面跳转所携带的信息参数存放在Extras中。Intent重载了很多种putExtras方法传递各种类型的参数,包括整型、双精度型、字符串型等基本数据类型以及Serializable这样的序列化结构。但实际使用中,一般都会将数据保存到一个Bundle里,再统一传递给下个界面。
Bundle内部用于存放消息的数据结构时Map映射,可以随意添加/删除文件,还可以判断元素是否存在。将Bundle数据全部打包好后,只需调用一次意图对象的putExtras方法;把Bundle数据取出,也只需要调用一次意图对象的getExtras方法。Bundle对象操作各类型数据的读写方法如下表:

数据类型读方法写方法
整型数getIntputInt
浮点数getFloatputFloat
双精度数getDoubleputDouble
布尔值getBooleanputBoolean
字符串getStringputString
字符串数组getStringArrayputStringArray
字符串列表getStringArrayListputStringArrayList
可序列化结构getSerializableputSerializable

下面是个数据传递的例子。在上一个活动界面中使用包裹封装好数据,把包裹交给意图对象,再调用startActivity方法跳转到意图指定的目标活动。完整代码如下:

// 创建一个意图对象
Intent intent  = new Intent(this, ActReceiveActivity.class);
Bundle bundle = new Bundle(); // 创建一个新包裹
// 往包裹存入名为request_time的字符串
bundle.putString("request_time", DateUtil.getNowTime());
// 往包裹存入名为request_content的字符串
bundle.putString("request_content", tv_send.getText().toString());
intent.putExtras(bundle); // 把包裹塞给意图
startActivity(intent); // 跳转到意图指定的界面

然后再下一个活动中获取意图携带的包裹,从包裹取出各种参数信息,并将传来的数据显示到文本视图中。下面时目标活动获取数据的例子:

// 从上一个页面传递来的意图中获取快递包裹
Bundle bundle = getIntent().getExtras();
// 从包裹中取出名为request_time的字符串
String request_time = bundle.getString("request_time");
// 从包裹中取出名为request_content的字符串
String request_content = bundle.getString("request_content");
String desc = String.format("收到消息:\n请求时间为%s\n请求内容为%s", request_time, request_content);
((TextView)findViewById(R.id.tv_receive)).setText(desc); //把传递过来的消息显示出来

数据传递有时是相互的,上一个页面传递数据给下一个页面后往往还要处理下一个页面返回的应答数据。详细步骤如下:

  1. 上一个活动打包好请求数据,调用startActivityForResult方法执行跳转动作,表示需要处理下一个活动的应答数据,该方法第二个参数表示请求代码,它用于表示每个跳转的唯一性。跳转代码如下:
String mRrequest = "你吃饭了吗?来我家吃吧";
// 创建一个意向,准备跳转指定活动页面
Intent intent = new Intent(this, ActResponseActivity.class);
Bundle bundle = new Bundle(); // 创建一个新包裹
// 往包裹存入一个名为request_time的字符串
bundle.putString("request_time", DateUtil.getNowTime());
// 往包裹存入一个名为request_content的字符串
bundle.putString("request_content", mRrequest);
intent.putExtras(bundle); // 把包裹塞入意图
// 期望接收下个页面返回数据,第二个参数为本次请求代码
startActivityForResult(intent, 0);
  1. 下一个活动接收并解析请求数据,进行相应处理。接收代码示例如下:
// 获取从上一个页面传递的包裹
Bundle bundle = getIntent().getExtras();
// 从包裹中获取名为request_time的字符串
String request_time = bundle.getString("request_time");
// 从包裹中获取名为request_content的字符串
String request_content = bundle.getString("request_content");
String desc = String.format("收到请求消息:\n请求时间为:%s\n请求内容为:%s", request_time, request_content);
((TextView)findViewById(R.id.tv_request)).setText(desc); // 把消息显示到文本视图上
  1. 下一个活动再返回上一个活动时,打包应答数据并调用setResult方法返回数据包裹。setResult方法的第一个参数表示应答代码(成功还是失败),第二个参数为携带包裹的意图对象。返回代码示例如下:
String mResponse = "我吃过了,还是你来我家吃";
Intent intent = new Intent(); // 创建一个意图
Bundle bundle = new Bundle(); // 创建一个包裹
// 往包裹中存入名为response_time的字符串
bundle.putString("response_time", DateUtil.getNowTime());
// 往包裹中存入名为response_content的字符串
bundle.putString("response_content", mResponse);
intent.putExtras(bundle); // 把包裹塞入intent
//携带意图返回上一个页面,RESULT_OK表示处理成功
setResult(Activity.RESULT_OK, intent);
finish();
  1. 上一个活动重写方法onActivityResult,该方法的参数包含请求代码和结果代码,其中请求代码用于判断这次返回对应哪个跳转,结果代码用于判断下一个活动是否处理成功。如果下一个活动处理成功,再对返回数据进行解包操作,处理返回数据的代码示例如下:
// 从下一个页面携带参数返回时触发。其中requestCode为请求代码
// resultCode为结果代码,intent为以下各页面返回的意图对象
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent intent) { // 接收返回数据
    super.onActivityResult(requestCode, resultCode, intent);
    // 意图非空,且请求代码为之前传的0,结果代码也为成功
    if (null != intent && 0 == requestCode && Activity.RESULT_OK == resultCode) {
        Bundle bundle = intent.getExtras(); // 从返回的意图中获取包裹
        // 从包裹中获取名为response_time的字符串
        String response_time = bundle.getString("response_time");
        // 从包裹中获取名为response_content的字符串
        String response_content = bundle.getString("response_content");
        String desc = String.format("收到返回消息:\n应答时间为:%s\n应答内容为:%s", response_time, response_content);
        tv_response.setText(desc);
    }
}

改进后的活动数据交互

从appcompat1.3.0开始,startActivityForResult方法被标记为已放弃,官方建议改用registerForActivityResult方法。具体使用步骤如下:

  1. 先声明一个活动结果启动对象ActivityResultLauncher,举例如下:
private ActivityResultLauncher mLauncher; // 声明一个活动结果启动器对象
  1. 调用registerForActivityResult方法注册一个善后工作的活动结果启动器,并指定对活动对象返回数据的处理过程,也就是第一个参数传入ActivityResultContracts.StartActivityForResult()对象,第二个参数填入onActivityResult要做的事情,示例如下:
// 注册一个善后工作的活动结果启动器
mLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
    if (result.getResultCode()==RESULT_OK && result.getData()!=null) {
        Bundle bundle = result.getData().getExtras(); // 从返回的意图中获取包裹
        // 从包裹中获取名为response_time的字符串
        String response_time = bundle.getString("response_time");
        // 从包裹中获取名为response_content的字符串
        String response_content = bundle.getString("response_content");
        String desc = String.format("收到返回消息:\n应答时间为:%s\n应答内容为:%s", response_time, response_content);
        tv_response.setText(desc); // 显示返回信息在文本视图上
    }
});
  1. 调用启动器对象的launch方法,传入封装了参数信息的意图对象,开始执行启动器的跳转与回调处理。代码如下:
String mRrequest = "你吃饭了吗?来我家吃吧";
// 创建一个意图对象,准备跳转到目标界面
Intent intent = new Intent(this, ActResponseActivity.class);
// 创建一个新包裹
Bundle bundle = new Bundle();
// 往包裹里存入名为request_time的字符串
bundle.putString("request_time", DateUtil.getNowTime());
// 往包裹里存入名为request_content的字符串
bundle.putString("request_content", mRrequest);
intent.putExtras(bundle); // 把包裹存入意图
mLauncher.launch(intent); // 活动启动器开动

以上使用活动结果启动器的操作,代码上并未简化多少,不过在一些特殊需求场合却能收到奇效。比如到系统相册挑选某张图片,调用startActivityForResult方法的话,活动跳转代码如下:

// 创建一个内容获取动作的意图(准备跳到系统相册)
Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
intent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*");
startActivityForResult(intent, CHOOSE_CODE);

重写onActivityResult方法,一次判断requestCode和resultCode,校验通过后再展示图片,回调代码如下:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
    super.onActivityResult(requestCode, resultCode, intent);
    if (RESULT_OK == resultCode && CHOOSE_CODE == requestCode && null != intent.getData()) {
        Uri uri = intent.getData(); // 获得已选择照片的路径对象
        // 根据指定图片的uri,获得自动缩小后的位图对象
        Bitmap bitmap = BitmapUtil.getAutoZoomImage(this, uri);
        iv_photo.setImageBitmap(bitmap); // 设置图像视图的位图对象
    }
}

利用活动启动器加以改造,可在调用registerForActivityResult方法时,第一个参数传入ActivityResultContracts.GetContent()对象,第二个参数填入图片展示过程。之后调用launch方法传入文件类型image/*"。即可完成从相册挑选并展示图片的功能。详细代码如下:

// 注册一个善后工作的活动结果启动器,获取指定类型内容
ActivityResultLauncher launcher = registerForActivityResult(new ActivityResultContracts.GetContent(), uri -> {
    if (null != uri) {
        // 根据指定图片的uri,获得自动缩小后的位图对象
        Bitmap bitmap = BitmapUtil.getAutoZoomImage(this, uri);
        iv_photo.setImageBitmap(bitmap); // 设置图像视图的位图对象
    }
});
// 点击按钮触发活动结果启动器,传入获取内容的文件类型
findViewById(R.id.btn_choose_register).setOnClickListener(v->{launcher.launch("image/*");});

收发应用广播

这个小节介绍应用广播的的几种收发形式,包括如何收发标准广播、如何收发有序广播、如何收发静态广播、如何监听定时器管理器发出的系统闹钟广播等。

收发标准广播

广播(Broadcast)也是Android四大组件之一,它用于Android各组件之间的灵活通信,与活动的区别在于:

  1. 活动只能一对一通信,而广播可以一对多,一人发送广播,多人接收处理。
  2. 对于发送方来说,广播不需要考虑接收方有没有工作,接收方在工作就接收广播,不在工作就丢弃广播。
  3. 对于接收方来说,因为可能会收到各式各样的广播,所以接收方要自行过滤符合条件的广播,之后再解包处理。

与广播有关的方法主要有三个:

  • sendBroadcast:发送广播。
  • registerReceiver:注册广播的接收器,可在onStart或onResume方法中注册接收器。
  • unregisterReceiver:注销广播的接收器,可在onStop或onPause方法中注销接收器。

编码实现上,广播的收发过程可分为3个步骤:发送标准广播、定义广播接收器、开关接收器,分别说明如下:

  1. 发送标准广播
    广播的发送操作分为两步:先创建意图对象,再调用sendBroadcast方法发送广播即可。不过要注意,意图对象需要指定广播的动作名称,这样接收方才能根据动作来判断来的是李逵还是李鬼。下面是在按键点击回调中发送广播的活动页面代码:
private final static String STANDARD_ACTION = "com.example.chapter04.standard";
Intent intent = new Intent(STANDARD_ACTION); // 创建指定动作的意图
sendBroadcast(intent); // 发送标准广播
  1. 定义广播接收器
    广播发送后还需要设备去接收广播。接收器主要规定两个事情:一个是接收什么样的广播,另一个是收到广播以后要做什么。Android提供了抽象之后的接收器基类BroadcastReceiver,开发者自定义的接收器都是从这个BroadcastReceiver派生而来。新定义的接收器需要重写onReceive方法,方法内部先判断当前广播是否符合接收的广播名称,校验通过后再开展后续的业务逻辑。下面是一个广播接收器的定义:
private String mDesc = "这里查看标准广播的收听信息";
private final static String STANDARD_ACTION = "com.example.chapter04.standard";
// 定义一个广播接收器
class StandardReceiver extends BroadcastReceiver {
    // 一旦接收到标准广播,马上触发接收器的onReceive方法
    @Override
    public void onReceive(Context context, Intent intent) {
        // 广播意图非空,且街头暗号正确
        if (null != intent && intent.getAction().equals(STANDARD_ACTION)) {
            mDesc = String.format("%s\n%s 收到一个标准广播", mDesc, DateUtil.getNowTime());
            tv_standard.setText(mDesc);
        }
    }
}
  1. 开关广播接收器
    广播接收器在活动页面启动之后才注册接收器,活动页面停止之际就要注销接收器。在注册接收器的时候,允许事先指定只接收某种类型的广播。即通过意图过滤器挑选动作名称一致的广播。接收器的注册与注销代码示例如下:
private final static String STANDARD_ACTION = "com.example.chapter04.standard";
private StandardReceiver standardReceiver; // 声明一个标准的广播接收器实例
@Override
protected void onStart() {
    super.onStart();
    standardReceiver = new StandardReceiver(); // 创建一个标准广播的接收器
    // 创建一个意图过滤器,只处理STANDARD_ACTION的广播
    IntentFilter filter = new IntentFilter(STANDARD_ACTION);
    registerReceiver(standardReceiver, filter, Context.RECEIVER_EXPORTED); // 注册接收器,注册之后才能正常接收广播
}

@Override
protected void onStop() {
    super.onStop();
    unregisterReceiver(standardReceiver); // 注销接收器,注销之后就不再接收广播
}

完成上述3个步骤后,就构建了广播从发送到接收的完整流程。

收发有序广播

有序广播的徐娅实现以下逻辑:

  1. 一个广播存在多个接收器,这些广播接收器需要排队收听广播,这意味着该广播是条有序广播。
  2. 先收到广播的接收器A,既可以让其他接收器继续收听广播,也可以中断广播不让其他接收器接听。

实现有序广播,需要完成以下3个编码步骤:

  1. 发送广播时要注明这是个有序广播
    之前发送标准广播用到了sendBroadcast方法,可是该方法发出来的广播是无序的。只有调用sendOrderedBroadcast方法才能发送有序广播,具体的发送代码示例如下:
Intent intent = new Intent(ORDER_ACTION); // 创建一个指定动作的意图
sendOrderedBroadcast(intent, null); // 发送有序广播
  1. 定义有序广播的接收器
    有序广播的接收器也要从BroadcastReceiver继承而来,唯一的区别是有序广播的接收器允许中断广播。如果在接收器的内部调用abortBroadcast方法,就会中断有序广播,使得后面的接收器不能再接收广播。下面是有序广播的两个接收器例子:
private OrderAReceiver orderAReceiver; // 声明有序广播接收器A的实例
// 定义一个有序广播接收器A
private class OrderAReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        if (null != intent && intent.getAction().equals(ORDER_ACTION)) {
            String desc = String.format("%s%s 接收器A收到一个有序广播\n", tv_order.getText().toString(), DateUtil.getNowTime());
            tv_order.setText(desc);
            if (ck_abort.isChecked()) {
                abortBroadcast(); // 中断广播,此时后面的接收器不能接收广播
            }
        }
    }
}

private OrderBReceiver orderBReceiver; // 声明有序广播接收器B的实例
// 定义一个有序广播B
private class OrderBReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        if (null != intent && intent.getAction().equals(ORDER_ACTION)) {
            String desc = String.format("%s%s 接收器B收到一个有序广播\n", tv_order.getText().toString(), DateUtil.getNowTime());
            tv_order.setText(desc);
            if (ck_abort.isChecked()) {
                abortBroadcast(); // 中断广播,此后的接收器不能接收广播
            }
        }
    }
}
  1. 注册有序广播的多个接收器
    接收器的注册操作同样调用registerReceiver方法,为了给接收器排队,还需要调用意图过滤器的setPriority设置优先级,优先级越大的接收器,越先收到有序广播。如果不设置优先级,或者两个接收器优先级相等,那么越早注册的接收器,越会先收到有序广播。如下列例子,尽管接收器A先注册,但接收器B的优先级更高,结果就是接收器B先收到广播。
orderAReceiver = new OrderAReceiver(); // 创建一个有序广播接收器A
// 创建一个意图过滤器A,只处理ORDER_ACTION的广播
IntentFilter filterA = new IntentFilter(ORDER_ACTION);
filterA.setPriority(8); // 设置过滤器A的优先级,数值越大优先级越高
registerReceiver(orderAReceiver, filterA, Context.RECEIVER_EXPORTED); // 注册接收器A,注册之后才能接收广播
orderBReceiver = new OrderBReceiver(); // 创建一个有序广播接收器B
// 创建一个意图过滤器,只处理ORDER_ACTION的广播
IntentFilter filterB = new IntentFilter(ORDER_ACTION);
filterB.setPriority(10); // 设置过滤器B的优先级,数值越大优先级越高
registerReceiver(orderBReceiver, filterB, Context.RECEIVER_EXPORTED); // 注册接收器B,注册之后才能正常接收广播

收发静态广播

广播(broadcast)与活动(activity)、服务(service)、内容提供器(provider)都能在AndroidManifest.xml注册,并且注册时名为receiver,一旦接收器在AndroidManifest.xml中注册了,就不用在代码里注册了。
AndroidManifest.xml中注册接收器,该方式称为静态注册;在代码中注册接收器,该方式被称为动态注册。静态注册容易导致安全问题,因此Android8.0之后废弃了大多数静态注册。但也没有彻底静止静态注册,只要满足特定条件的编码条件,那么依然能够通过静态方式注册接收器。具体步骤说明如下:

  • 右击包名,选择菜单的New->Other->Broadcast Receiver,弹出如下广播组件的对话框。
    在这里插入图片描述

  • 在组件创建对话框的Class Name一栏填写接收器的类名,比如ShockReceiver,再单击对话框右下角的Finished按钮。之后Android Studio自动在receiver包内创建代码文件ShockReceiver.java,且接收器的默认代码如下:

public class ShockReceiver extends BroadcastReceiver {
	@Override
	public void onReceive(Context context, Intent intent) {
	// TODO: This method is called when the BroadcastReceiver is receiving
	// an Intent broadcast.
	throw new UnsupportedOperationException("Not yet implemented");
	}
}

同时AndroidManifest.xml自动添加的接收器的节点配置,默认的receiver配置如下:

<receiver
    android:name=".receiver.ShockReceiver"
    android:enabled="true"
    android:exported="true"></receiver>

然而自动生成的接收器不仅什么都没干,还丢出一个异常UnsupportedOperationException。明显这个接收器没法用,为了感知到接收器正常工作,可以考虑在onReceive方法中记录日志,也可以在该方法中震动手机。实现手机震动,要调用getSystemService方法,先从系统服务Context.VIBRATOR_SERVICE获取震动管理器Vibrator,再调用震动管理器vibrate方法震动手机。包含手机震动功能的接收器代码如下:

public class ShockReceiver extends BroadcastReceiver {
    private static final String TAG = "ShockReceiver";
    // 静态注册时的action、发送广播时的action、接收广播时的action,三者要保持一致
    public static final String SHOCK_ACTION = "com.example.chapter04.shock";
    @Override
    public void onReceive(Context context, Intent intent) {
        Log.d(TAG, "onReceive");
        if (null != intent && intent.getAction().equals(SHOCK_ACTION)) {
            // 从系统服务中获取震动管理器
            Vibrator vb = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
            vb.vibrate(500); // 命令振动器震动一段时间
        }
    }
}

由于震动手机需要申请对应权限,因此打开AndroidManifest.xml添加以下权限申请配置:

<!-- 震动 -->
<uses-permission android:name="android.permission.VIBRATE" />

此外,接收器定义了一个动作名称,其值为com.example.chapter04.shock,表示onReceive方法只处理过滤该动作之后的广播,从而提高接收率。除了在代码过滤之外,还能修改AndroidManifest.xml,在receiver节点内部增加intent-filter标签加以过滤,添加过滤配置后的receiver节点信息如下:

<receiver
    android:name=".receiver.ShockReceiver"
    android:enabled="true"
    android:exported="true">
    <intent-filter>
        <action android:name="com.example.chapter04.shock" />
    </intent-filter>
</receiver>

由于Android8.0之后删除了大部分静态注册,防止App退出后仍在收听广播,因此为了让应用能够继续接收静态广播,需要给静态广播指定包名,也就是调用意图对象的setComponent方法设置组件路径。详细的静态广播发送代码示例如下:

// Android8.0之后删除了大部分静态注册,防止退出App后仍在接收广播
// 为了能让应用继续接收静态广播,需要给静态注册的广播指定包名
String receiverPath = "com.example.chapter04.receiver.ShockReceiver";
Intent intent = new Intent(ShockReceiver.SHOCK_ACTION); // 创建一个指定动作的意图
// 发送静态广播之时,需要通过setComponent方法指定接收器的完整路径
ComponentName componentName = new ComponentName(this, receiverPath);
intent.setComponent(componentName); // 设置意图组件信息
sendBroadcast(intent); // 发送静态广播

经过上述的编码以及配置工作,完成了静态广播的发送与接收过程。经过整改的静态注册只适用于接收App自身的广播,不能接收系统广播,也不能接受其他应用的广播。

定时管理器AlarmManager

Android提供了专门的定时管理器AlarmManager,它利用系统闹钟定时发送广播,能够让App实现定时功能,由于闹钟与振动同属于系统服务,且闹钟的服务名称为ALARM_SERVICE,因此依然调用getSystemService方法获取闹钟管理器的实例,下面是从系统服务中获取闹钟管理器的代码:

// 从系统服务中获取闹钟管理器
AlarmManager alarmMgr = (AlarmManager) getSystemService(ALARM_SERVICE);

得到闹钟实例后,即可调用它的各种方法设置闹钟规则了,AlarmManager的常见方法说明如下:

  • set:设置一次性定时器。第一个参数为定时器类型,通常填larmManager.RTC_WAKEUP;第二个参数为期望的执行时刻(单位:毫秒);第三个参数为待执行的延迟意图(PendingIntent类型)。

  • setAndAllowWhileIdle:设置一次性定时器,参数说明同set方法,不同之处在于:即使设备处于空闲,也会保证执行定时器。因为从Android6.0开始,set方法在暗屏时不保证发送广播,必须调用setAndAllowWhileIdl方法才能保证发送广播。

  • setRepeating:设置重复定时器。第一个参数为定时器类型;第二个参数为首次执行时间(单位为毫秒);第三个参数为下次执行的间隔时间(单位为毫秒);第四个参数为待执行的延迟意图(PendingIntent类型)。然而setRepeating方法不保证按时发送广播,只能通过setAndAllowWhileIdle方法间接实现重复定时功能。

  • cancel:取消指定延迟意图的定时器。

以上的方法说明出现了新名词–延迟意图,它是PendingIntent类型,顾名思义,延迟意图不是马上执行的意图,而是延迟若干时间才执行的意图。像之前的活动页面跳转,调用startActivity方法跳转到下个活动页面,此时跳转动作时立刻发生的,所以要传入Intent对象,由于定时器的广播不是立刻发送的,而是时刻到达了才发送广播,因此不能传Intent对象,只能传PendingIntent对象。当然意图与延迟意图不止这一处区别,它们的差异主要有以下3点:

  1. PendingIntent代表延迟意图,它指向的组件不会马上被激活;而Intent代表实时的意图,一旦被启动,它指向的组件马上被激活。
  2. PendingIntent对象是一类消息的组合,不但包括目标对象的Intent对象,还包含请求代码、请求方式等信息。
  3. PendingIntent对象在创建之时便已知晓将要用于活动还是广播,例如调用getActivity方法得到的是活动跳转的延迟意图,调用getBroadcast方法得到的是广播发送的延迟意图。

就闹钟广播的收发过程而言,需要实现3个编码步骤:定义定时器的广播接收器、开关定时器的广播接收器、设置定时器的播报规则,分别叙述如下。

  1. 定义定时器的广播接收器
    闹钟广播的接收器采用动态注册的方式,它的实现途径与标准广播类似,都要从BroadcastReceiver派生新的接收器,并重写onReceive方法,闹钟广播接收器的定义代码如下:
// 声明一个闹钟广播事件的标识串
private String ALARM_ACTION = "com.example.chapter04.alarm";
private String mDesc = ""; // 闹钟时间到达的描述

// 定义一个广播的接收器
public class AlarmReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (null != intent) {
            mDesc = String.format("%s\n%s 闹钟时间到达", mDesc, DateUtil.getNowTime());
            tv_alarm.setText(mDesc);
            // 从系统服务中获取震动管理器
            Vibrator vb = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
            vb.vibrate(500); // 震动500毫秒
        }
    }
}
  1. 开关定时器的广播接收器
    定时器接收器的开关流程参照标准广播,可以在活动页面的onStart方法中注册接收器,在活动页面的onStop方法注销接收器。相应代码如下:
private AlarmReceiver alarmReceiver; // 声明一个广播接收器
@Override
public void onStart() {
    super.onStart();
    alarmReceiver = new AlarmReceiver(); // 创建一个闹钟接收器
    // 创建一个意图过滤器,只处理指定事件来源广播
    IntentFilter filter = new IntentFilter(ALARM_ACTION);
    registerReceiver(alarmReceiver, filter, Context.RECEIVER_EXPORTED); // 注册接收器,注册之后才能正常接收广播
}
@Override
public void onStop() {
    super.onStop();
    unregisterReceiver(alarmReceiver);
}
  1. 设置定时器的播报规则
    首先从系统服务中获取闹钟管理器,然后调用管理器的set***方法,把事前创建的延迟意图填到播报规则中。下面是发送广播的代码例子:
// 发送闹钟广播
private void sendAlarm() {
    Intent intent = new Intent(ALARM_ACTION); // 创建一个用于广播事件的意图
    // 创建一个用于广播的延迟意图
    PendingIntent  pIntent = PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_IMMUTABLE);
    // 从系统服务中获取闹钟管理器
    AlarmManager alarmMgr = (AlarmManager) getSystemService(ALARM_SERVICE);
    long delayTime = System.currentTimeMillis() + mDelay*1000; // 给当前时间加上若干秒
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        // 允许在空闲时发送广播,Android6.0之后新增方法
        alarmMgr.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, delayTime, pIntent);
    } else {
        // 设置一次性闹钟,延迟若干秒后,携带延迟意图发送闹钟广播(但Android6.0之后,set方法在暗屏时不保证发送广播,必须调用setAndAllowWhileIdle方法)
        alarmMgr.set(AlarmManager.RTC_WAKEUP, delayTime, pIntent);
    }
    // 设置重复闹钟,每隔一定间隔就发送闹钟广播(但从Android4.4开始,setRepeating方法不保证按时发送广播)
//        alarmMgr.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), mDelay*1000, pIntent);
}

完成上述的3个步骤后,运行App,点击“设置闹钟”按钮,界面下方会回显闹钟的设置信息,同时手机会震动代表功能成功运行。

操作后台服务

这个小节介绍Android四大组件之一的Service的基本概念和常见用法。包括服务的生命周期以及两种启停方式–普通方式和绑定方式(含立即绑定和延迟绑定),还介绍了如何在活动和服务之间交互数据。

服务的启动和停止

Service与Activity相比没有对应的页面,但都有生命周期。要想用好服务,需要弄清楚它的生命周期。
Service与生命周期有关的方法说明如下:

  • onCreate:创建服务。
  • onStart:开始服务,Android2.0以下版本使用,现已废弃。
  • onStartCommand:开始服务,Android2.0及以上版本使用。该方法的返回值说明见下表:
返回值类型返回值说明
START_STICKY黏性的服务。如果服务进程被杀掉,就保留服务的状态为开始状态,但不保留传送的Intent对象。随后系统尝试重新创建服务,由于服务状态为开始状态,因此创建服务后一定会调用onStartCommand方法。如果在此期间没有任何启动命令传送给服务,参数Intent值就为空值
START_NOT_STICKY非黏性的服务。使用这个返回值,如果服务被异常杀掉,系统就不会自动重启该服务
START_REDELIVER_INTENT重传Intent服务。使用这个返回值时,如果服务被异常杀掉,系统就会重启该服务,并传入Intent的原值
START_STICKY_COMPATIBILITYSTART_STICKY 的兼容版本,但不保证服务被杀掉后一定能重启
  • onDestroy:销毁服务。
  • onBind:绑定服务。
  • onUnbind:解除绑定。返回值为true表示允许再次绑定,之后再绑定服务时,不会调用onBind方法;返回值为false表示只能绑定一次,不能再次绑定。
  • onRebind:重新绑定。只有上次的onUnbind方法返回true时,再次绑定服务才会调用onRebind方法。

在Java代码包下右击并在右键菜单中一次选择New->Service->Service,弹出如下创建服务对话框。
在这里插入图片描述
在创建服务对话框的Class Name一栏填写服务名称,比如NormalService,再单击对话框右下角的Finished按钮,Android Studio便自动在包下生成NormalService.java,同时在AndroidManifest.xmlapplication节点内部添加如下的服务注册配置:

<service
    android:name=".service.NormalService"
    android:enabled="true"
    android:exported="true"></service>

打开NormalService.java发现里面只有几行代码,为了便于观察服务的生命周期,需要重写该服务的所有周期方法,给每个方法都打印相应的运行日志,修改之后的服务代码如下:

public class NormalService extends Service {
    public NormalService() {
    }
    private static final String TAG = "NormalService";

    private void refresh(String text) {
        Log.d(TAG, text);
        ServiceNormalActivity.showText(text);
    }
    @Override
    public void onCreate() { // 创建服务
        refresh("onCreate");
    }
    @Override
    public int onStartCommand(Intent intent, int flags, int startid) { // 启动服务,Android2.0以上使用
        Log.d(TAG, "测试服务到此一游");
        refresh("onStartCommand.flag=" + flags);
        return START_STICKY;
    }
    @Override
    public void onDestroy() { // 销毁服务
        super.onDestroy();
        refresh("onDestroy");
    }
    @Override
    public IBinder onBind(Intent intent) { // 绑定服务。普通服务不存在绑定和解绑流程
        refresh("onBind");
        return null;
    }
    @Override
    public boolean onUnbind(Intent intent) { // 解绑服务
        refresh("onUnbind");
        return true; // 返回false表示只能绑定一次,返回true表示允许多次绑定
    }
}

启停普通服务很简单,只要创建一个指向服务的意图,然后调用startService方法即可启动服务,若要停止服务,调用stopService方法即可停止指定意图的服务。具体代码如下:

// 创建一个通往普通服务的意图
Intent intent = new Intent(this, NormalService.class);
startService(intent); // 启动指定意图的服务
//stopService(intent); // 停止指定意图的服务

运行App,点击“启动服务”按钮,监听器调用了startService方法,然后启动服务会依次调用onCreateonStartCommand方法。接着点击“停止服务”按钮,监听器调用了stopService方法,然后服务停止调用onDestroy方法。

服务的绑定与解绑

服务还有另外一种启停方式,那就是绑定服务和解绑服务。因为服务可能是由组件甲创建的却由组件乙使用,也可能服务由进程A创建却由进程B使用。既然所有者和使用者可以不同,那么就需要提供黏合剂Binder指定服务关系,同时黏合剂还负责在两个组件或者两个进程之间交流通信。增加黏合剂后的代码如下:

public class BindImmediateService extends Service {
    private final IBinder mBinder = new LocalBinder(); // 创建一个粘合剂

    // 定义一个当前服务的黏合计,用于将该服务黏到活动页面的进程中
    public class LocalBinder extends Binder {
        public BindImmediateService getService() {
            return BindImmediateService.this;
        }
    }

    private void refresh(String text) {
        BindImmediateActivity.showText(text);
    }

    @Override
    public void onCreate() { // 创建服务
        super.onCreate();
        refresh("onCreate");
    }

    @Override
    public void onDestroy() { // 销毁服务
        super.onDestroy();
        refresh("onDestroy");
    }

    @Override
    public IBinder onBind(Intent intent) { // 绑定服务。返回该服务的粘合剂对象
        refresh("onBind");
        return mBinder;
    }

    @Override
    public void onRebind(Intent intent) { // 重新绑定服务
        super.onRebind(intent);
        refresh("onRebind");
    }

    @Override
    public boolean onUnbind(Intent intent) { // 解绑服务
        refresh("onUnbind");
        return true; // 返回false表示只能绑定一次,返回true表示允许多次绑定
    }
}

对于绑定了黏合剂的服务,它的绑定和解绑操作与普通方式不同:首先要定义一个ServiceConnection的服务器连接对象,然后调用bindService方法绑定服务,绑定之后再择机调用unbindService方法解除服务,代码示例如下:

public class BindImmediateActivity extends AppCompatActivity implements View.OnClickListener{
    private final static String TAG = "BindImmediateActivity";
    private static TextView tv_immediate; // 声明一个文本视图
    private Intent mIntent; // 声明一个意图对象
    private static String mDesc = ""; // 日志描述
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_bind_immediate);
        tv_immediate = findViewById(R.id.tv_immediate);
        findViewById(R.id.btn_start_bind).setOnClickListener(this);
        findViewById(R.id.btn_unbind).setOnClickListener(this);
        // 创建一个通往立即绑定服务的意图
        mIntent = new Intent(this, BindImmediateService.class);
    }

    @Override
    public void onClick(View view) {
        if (view.getId() == R.id.btn_start_bind) { // 点击绑定服务按钮
            // 绑定服务。如果服务未启动,则系统先启动服务在进行绑定
            boolean bindFlag = bindService(mIntent, mServiceConn, Context.BIND_AUTO_CREATE);
            Log.d(TAG, "bindFlag=" + bindFlag);
        } else if (view.getId() == R.id.btn_unbind && null != mBindService) { // 点击解绑服务按钮
            // 解绑服务。如果先前服务立即绑定,则此时解绑之后自动停止服务
            unbindService(mServiceConn);
        }
    }

    public static void showText(String desc) {
        if (null != tv_immediate) {
            mDesc = String.format("%s%s %s\n", mDesc, DateUtil.getNowDateTime("HH:mm:ss"), desc);
            tv_immediate.setText(mDesc);
        }
    }

    // 声明一个服务对象
    private BindImmediateService mBindService;
    private ServiceConnection mServiceConn = new ServiceConnection() {
        // 获取服务对象时的操作
        @Override
        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
            // 如果服务运行于另一个进程,则不能直接强制转换类型,否则会报错
            mBindService = ((BindImmediateService.LocalBinder) iBinder).getService();
            Log.d(TAG, "onServiceConnected");
        }
        // 无法获取对象时的操作
        @Override
        public void onServiceDisconnected(ComponentName componentName) {
            mBindService = null;
            Log.d(TAG, "onServiceDisconnected");
        }
    };
}

运行App,点击“启动并绑定服务”按钮之后调用bindService方法,绑定服务依次调用onCreateonBind方法。然后点击“解绑并停止服务”调用unbindService方法,此时会依次调用onUnbindonDestroy方法。
如上述服务绑定与解绑操作,其实并不纯粹。因为调用bindService方法时先后触发了onCreateonBind方法,也就是创建服务后紧接着绑定服务;调用unbindService方法时先后触发onUnbindonDestroy,也就是解绑服务后紧接着销毁服务。既然服务的创建操作后面紧跟着绑定操作,它们的时空关系近似于普通启停,那么就体现不出绑定操作的特点,下面我们使用Android提供的延迟绑定来验证一下。
延迟绑定与立即绑定的区别在于:延迟绑定要先通过startService方法启动服务,再通过bindService方法绑定已存在的服务;同理,延迟解绑要通过unbindService方法解除绑定服务,再通过stopService方法停止服务。这样一来,因为启动操作在先、绑定操作在后,所以解绑操作只能撤销绑定操作,而不能撤销启动操作。由于解绑操作不能销毁服务,因此存在再次绑定服务的可能。以下为通过“启动服务”、“绑定服务”、“解绑服务”以及“停止服务”四个按钮演示startServicebindServiceunbindService以及stopService方法的例子:

@Override
public void onClick(View view) {
    if (view.getId() == R.id.btn_start) { // 点击了开始服务按钮
        startService(mIntent); // 启动服务
    } else if (view.getId() == R.id.btn_bind) { // 点击了绑定服务按钮
        boolean bindFlag = bindService(mIntent, mServiceConn, Context.BIND_AUTO_CREATE); // 绑定服务
        Log.d(TAG, "bindFlag=" + bindFlag);
    } else if (view.getId() == R.id.btn_unbind) { // 点击了解绑服务按钮
        if (mBindService != null) {
            unbindService(mServiceConn); // 解绑服务
        }
    } else if (view.getId() == R.id.btn_stop) { // 点击了停止服务按钮
        stopService(mIntent); // 停止服务
    }
}

延迟绑定与立即绑定两种方式的生命周期有以下两种区别:

  1. 延迟绑定的首次绑定操作只触发onBind方法,再次绑定操作只触发onRebind方法(是否允许再次绑定要看上次onUnbind方法的返回值)。
  2. 延迟绑定的解绑操作只触发onUnbind方法。

活动与服务之间的交互

不管是startService方法,还是bindService方法都支持将意图对象作为参数,这意味着在启动服务或绑定服务之时能够向服务传递信息。可是服务跑起来之后就一直在运行,活动代码怎么知道服务跑得快还是跑得慢,须知服务并不提供回调机制,活动守株待兔是等不到结果的。若想即时获取服务的运行情况,活动就得主动打探消息,此时需要有个信使承担消息传输的任务,这个信使便是绑定方式用到的服务黏合剂–IBinder。
注意看服务代码的onBind方法,它的返回值类型正是IBinder,表示绑定成功后返回服务的黏合剂对象。只要活动代码拿到了服务的黏合剂对象,就能通过黏合剂与服务进行数据交互。由于IBinder是个接口,它的实现类名叫Binder,因此每个服务的黏合剂都得从IBinder派生而来。除了定义getService方法返回当前对象之外,黏合剂还可以定义一般的数据交互方法,用于同活动代码往来通信。下面与黏合剂有关的服务定义代码片段:

private final IBinder mBinder = new LocalBinder(); // 创建一个黏合剂对象
// 定义一个当前服务的黏合剂,用于将该服务黏到活动页面的进程中
public class LocalBinder extends Binder {
    public DataService getService() { return DataService.this; }
    // 获取数字描述
    public String getNumber(int number) { return "我收到了数字"+number; }
}

@Override
public IBinder onBind(Intent intent) { // 绑定服务。返回该服务的黏合剂对象
    Log.d(TAG, "绑定服务");
    return mBinder;
}

活动代码在调用bindService方法时,第二个参数为ServiceConnection类型,表示绑定结果的连接对象。这个连接对象来自接口ServiceConnection,它的onServiceConnected方法在连接成功时回调,onServiceDisconnected方法在连接断开时回调。重写ServiceConnectiononServiceConnected方法,即可拿到已绑定服务的黏合剂对象。有了服务的黏合剂,才能通过黏合剂获取服务内部情况。下面代码演示如何通过黏合剂与服务通信:

private DataService.LocalBinder mBinder; // 声明一个黏合剂对象
private ServiceConnection mServiceConn = new ServiceConnection() {

    // 获取服务对象时的操作
    @Override
    public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
        // 如果服务运行于另外一个进程,则不能直接强制转换类型,否则会报错
        mBinder = (DataService.LocalBinder) iBinder;
        // 活动代码通过黏合剂与服务代码通信
        String response = mBinder.getNumber(new Random().nextInt(100));
        tv_result.setText(DateUtil.getNowTime()+" 绑定服务应答:"+response);
    }

    @Override
    public void onServiceDisconnected(ComponentName componentName) {
        mBinder = null;
    }
};

点击“启动并绑定服务”按钮,即可实现活动与服务之间的数据交互。

工程源码

可点击工程源码下载全文涉及的代码工程。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/575807.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【MySQL关系型数据库】基本命令、配置、连接池

目录 MySQL数据库 第一章 1、什么是数据库 2、数据库分类 3、不同数据库的特点 4、MySQL常见命令&#xff1a; 5、MySQL基本语法 第二章 1、MySQL的常见数据类型 1、数值类型 2、字符类型 3、时间日期类型 2、SQL语句分类 1、DDL&#xff08;数据定义语言&#x…

Relu激活函数

概念 神经网络中的每个神经元节点接受上一层神经元的输出值作为本神经元的输入值&#xff0c;并将输入值传递给下一层。在多层神经网络中&#xff0c;上层节点的输出和下层节点的输入之间具有一个函数关系&#xff0c;这个函数称为激活函数。 激活函数做的事情时把神经元的输…

STM32存储左右互搏 SDIO总线FATS文件读写SD/MicroSD/TF卡

STM32存储左右互搏 SDIO总线FATS文件读写SD/MicroSD/TF卡 SD/MicroSD/TF卡是基于FLASH的一种常见非易失存储单元&#xff0c;由接口协议电路和FLASH构成。市面上由不同尺寸和不同容量的卡&#xff0c;手机领域用的TF卡实际就是MicroSD卡&#xff0c;尺寸比SD卡小&#xff0c;而…

SN75107BDR 总线接收器 中文资料_PDF中文资料_参数_引脚图

SN75107BDR 规格信息&#xff1a; 制造商:Texas Instruments 产品种类:总线接收器 RoHS:是 接收机数量:2 Receiver 接收机信号类型:Differential 电源电压-最小:/- 4.75 V 电源电压-最大:/- 5.25 V 工作电源电流:30 mA 最小工作温度:0 C 最大工作温度: 70 C 封装 / 箱…

文旅IP孵化打造抖音宣传推广运营策划方案

【干货资料持续更新&#xff0c;以防走丢】 文旅IP孵化打造抖音宣传推广运营策划方案 部分资料预览 资料部分是网络整理&#xff0c;仅供学习参考。 PPT可编辑&#xff08;完整资料包含以下内容&#xff09; 目录 文旅IP抖音运营方案 1. 项目背景与目标 - 背景&#xff1a…

了解时间复杂度和空间复杂度

在学习数据结构前&#xff0c;我们需要了解时间复杂度和空间复杂度的概念&#xff0c;这能够帮助我们了解数据结构。 算法效率分为时间效率和空间效率 时间复杂度 一个算法的复杂度与其执行的次数成正比。算法中执行基础操作的次数&#xff0c;为算法的时间复杂度。 我们采…

Rust中的函数指针

什么是函数指针 通过函数指针允许我们使用函数作为另一个函数的参数。函数的类型是 fn &#xff08;使用小写的 ”f” &#xff09;以免与 Fn 闭包 trait 相混淆。fn 被称为 函数指针&#xff08;function pointer&#xff09;。指定参数为函数指针的语法类似于闭包。 函数指…

VIO外参标定方法总结

一、前言 VIO外参标定是指相机和IMU之间的转移矩阵的确定&#xff0c;包括33的旋转矩阵和3维平移向量。整体上分为离线标定和在线标定两类方法&#xff0c;这篇文章做一个总结&#xff0c;主要是经典的方法&#xff0c;记录其思想。 二、博文链接 1、离线标定方法 最基本的…

p0级故障-nptd和ntpdate用法

一、背景 绝对真实的大厂线上P0级故障经历分享。 某日凌晨3点&#xff0c;企业微信群变得热闹起来&#xff0c;想都不用想&#xff0c;作为互联网人&#xff0c;特别是运维相关的同学知道&#xff0c;肯定又是出故障了&#xff0c;并且这个故障还很大。 当前晚上我睡着了&#…

【Java EE】 文件IO的使用以及流操作

˃͈꒵˂͈꒱ write in front ꒰˃͈꒵˂͈꒱ ʕ̯•͡˔•̯᷅ʔ大家好&#xff0c;我是xiaoxie.希望你看完之后,有不足之处请多多谅解&#xff0c;让我们一起共同进步૮₍❀ᴗ͈ . ᴗ͈ აxiaoxieʕ̯•͡˔•̯᷅ʔ—CSDN博客 本文由xiaoxieʕ̯•͡˔•̯᷅ʔ 原创 CSDN 如…

【Qt】error LNK2001: 无法解析的外部符号 “__declspec(dllimport)

参考&#xff1a;Qt/VS LNK2019/LNK2001&#xff1a;无法解析的外部符号_qt lnk2001无法解析的外部符号-CSDN博客 微软官方报错文档-链接器工具错误 LNK2019 __declspec error LNK2001: 无法解析的外部符号 "__declspec(dllimport) 原因 以这种为前缀的基本上跟库相关…

Visual Studio安装MFC开发组件

MFC由于比较古老了&#xff0c;Visual Studio默认没有这个开发组件。最近由于一些原因&#xff0c;需要使用这个库&#xff0c;这就需要另外安装。 参考了网上的一些资料&#xff0c;根据实际使用&#xff0c;其实很多步骤不是必须的。 https://zhuanlan.zhihu.com/p/68117276…

HarmonyOS开发实战(黑马健康系列一:欢迎页)

系列文章目录 &#xff08;零&#xff09;鸿蒙HarmonyOS入门&#xff1a;如何配置环境&#xff0c;输出“Hello World“ &#xff08;一&#xff09;鸿蒙HarmonyOS开发基础 &#xff08;二&#xff09;鸿蒙HarmonyOS主力开发语言ArkTS-基本语法 &#xff08;三&#xff09;鸿蒙…

沐风老师3DMAX一键相框生成插件安装使用方法教程

3DMAX一键相框生成插件使用教程 3DMAX一键相框生成插件&#xff0c;用于根据导入的图像文件以正确的比例从选定的图像中快速创建相框。只需点击几下鼠标&#xff0c;它就可以同时创建多个相框&#xff0c;在尺寸、轮廓、颜色和玻璃方面有许多选项。 支持Corona、Vray和标准材质…

Java基础(运算符)

运算符 运算符和表达式 运算符&#xff1a;对字面量或者变量进行操作的符号 表达式&#xff1a;用运算符把字面量或者变量连接起来&#xff0c;符合java语法的式子就可以称为表达式&#xff1b;不同运算符连接的表达式体现的是不同类型的表达式。 算术运算符&#xff08;加…

Unity 按下Play键后,Scene View里面一切正常,但是Game View中什么都没有 -- Camera Clear Flags的设置

问题如下所示。 最先遇到这个问题是我想用Unity开发一个VR 360-degree Image Viewer。在Scene View中可以看到球体&#xff0c;但是Game View什么都看不到。最后找到的原因是&#xff0c;我使用的shader是Skybox/Panorama&#xff0c; 需要把Main Camera的Clear Flags设置成Do…

灌区信息化解决方案-大型灌区信息化改造

系统方案 灌区信息化解决方案主要对灌区的水情、渠道流量、土壤墒情、气象等信息进行监测&#xff0c;对重点区域进行视频监控&#xff0c;同时对泵站、闸门进行远程控制&#xff0c;实现信息的测量、统计、分析、控制、调度等功能。为灌区管理部门科学决策提供了依据&#xff…

多组学+机器学习+膀胱癌+分型+建模

这是一个基于多组学机器学习的分型建模文章&#xff0c;这里我们大概介绍一下&#xff0c;这篇文章做了啥 一、研究背景 1、尿路上皮癌是高度恶性的肿瘤&#xff0c;预后差&#xff0c;死亡率高 2、没有明显有效的治疗方法&#xff0c;多数患者在免疫治疗中无法受益&#xf…

Java混淆的重要性

在软件开发领域&#xff0c;安全性与代码保护一直是备受关注的问题。特别是在Java这样的跨平台语言中&#xff0c;保护源代码的机密性和完整性显得尤为重要。Java混淆作为一种代码保护技术&#xff0c;其在现代软件开发中的地位日益凸显。本文将详细探讨Java混淆的重要性&#…

【网络安全】网络安全协议和防火墙

目录 1、网络层的安全协议&#xff1a;IPsec 协议族 &#xff08;1&#xff09;IP 安全数据报格式 &#xff08;2&#xff09;互联网密钥交换 IKE (Internet Key Exchange) 协议 2、运输层的安全协议&#xff1a;TLS 协议 3、系统安全&#xff1a;防火墙与入侵检测 1、网络…