目录
方式三:startService和bindService混合双打
需求定义
假设一个设备具备装备了湿度传感器,可通过监测真实环境的湿度值来工作(如市面上的除湿设备),用户要求:除湿工作全自动化,无需人工干预,除非断电关机,否则永远在后台运行;有一个用户界面,打开可以查看当前环境的湿度值以及设备的工作状态(是否在进行除湿工作)。针对这些需求,得到写代码的思路:
- 无需人工干预全自动化永久运行 --> 定义service(前台服务)获得湿度传感器的值,当湿度值达到某个阈值,开启除湿功能,否则关闭除湿功能
- 用户界面呈现设备状态 --> 定义Activity,通过和service交互得到service的数据
这样就引出了文章的主题:Activity和Service交互的方式。
注:这个需求比较简单,实现方式也非常的多,可能有人会说Service可以拿到湿度传感器的值,Activity一样可以拿到啊,还交互个啥?确实是这样的,但是这里是为了探讨Activity和Service的交互行为,所以需要假定Activity的数据只能通过Service获取到。
方式一:广播(Broadcast)
public class HumidityService extends Service {
private static final String BROADCAST_KEY = "humidity_broadcast";
private SensorManager sm;
private Sensor mHumiditySensor;
/**
* 省略注册为前台服务的相关代码流程....
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// 省略...
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onCreate() {
super.onCreate();
sm = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
mHumiditySensor = sm.getDefaultSensor(Sensor.TYPE_RELATIVE_HUMIDITY);
sm.registerListener(humidityEventListener, mHumiditySensor, SensorManager.SENSOR_DELAY_NORMAL);
}
// 监听湿度传感器,获取湿度值用广播发送出去
SensorEventListener humidityEventListener = new SensorEventListener() {
@Override
public void onSensorChanged(SensorEvent sensorEvent) {
float[] values = sensorEvent.values;
sendBroadcast(values[0]);
}
@Override
public void onAccuracyChanged(Sensor sensor, int i) {
}
};
// 发送广播
private void sendBroadcast(float data) {
Intent intent = new Intent(BROADCAST_KEY);
intent.putExtra("data", data);
sendBroadcast(intent);
}
@Override
public void onDestroy() {
super.onDestroy();
sm.unregisterListener(humidityEventListener);
}
}
public class HumidityActivity extends Activity {
private static final String BROADCAST_KEY = "humidity_broadcast";
private IntentFilter mFilter;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 省略相关代码...
mFilter = new IntentFilter();
mFilter.addAction(BROADCAST_KEY);
}
// 接收Service发过来的湿度值,界面显示等代码省略...
BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Log.d("test", "data: " + intent.getFloatExtra("data", 0));
}
};
@Override
protected void onResume() {
super.onResume();
registerReceiver(mReceiver, mFilter);
}
@Override
protected void onPause() {
super.onPause();
unregisterReceiver(mReceiver);
}
}
这个方法肯定是可以实现功能的,在界面显示的时候动态注册广播,接收service发送的数据,并呈现给用户。但是有一个问题,sensor的数据change是毫秒级的,也就是说一秒钟之内会发送多次广播,并且Broadcast的传输是通过binder间通信,本质上是用来方便跨进程通信的,这么频繁的发送广播必然是更耗性能的,不太友好,所以对于大量数据传输频繁的场景,我都不建议采用广播(不管是进程内通信还是跨进程通信)。尤其是对于那些对实时性要求高的数据,广播就显得有点不靠谱,因为发送广播的行为是提交到系统的,Android系统本身有很多系统级广播要发送,在framework层的实现是一个队列,将所有广播按照优先级等条件排排站,一个个发送的,所以,广播从发出去到接收到的时间是不固定的,快慢取决于系统当前的负载。
方式二:bindService-接口回调
定义一个回调接口
package com.xxx...
public interface SensorCallback {
void callback(float data);
}
public class HumidityService extends Service {
private static final String BROADCAST_KEY = "humidity_broadcast";
private SensorManager sm;
private Sensor mHumiditySensor;
private SensorCallback mSensorCallback;
public class MyBinder extends Binder {
public HeaterService getService() {
return HeaterService.this;
}
}
private MyBinder binder = new MyBinder();
@Nullable
@Override
public IBinder onBind(Intent intent) {
Log.d(TAG, "onBind");
return binder;
}
// 设置回调接口
public void setCallback(SensorCallback callback) {
this.mSensorCallback= callback;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// 省略...
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onCreate() {
super.onCreate();
sm = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
mHumiditySensor = sm.getDefaultSensor(Sensor.TYPE_RELATIVE_HUMIDITY);
sm.registerListener(humidityEventListener, mHumiditySensor, SensorManager.SENSOR_DELAY_NORMAL);
}
// 监听湿度传感器,获取湿度值调用回调接口
SensorEventListener humidityEventListener = new SensorEventListener() {
@Override
public void onSensorChanged(SensorEvent sensorEvent) {
float[] values = sensorEvent.values;
if (mSensorCallback!= null) {
mSensorCallback.callback(values[0]);
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int i) {
}
};
@Override
public void onDestroy() {
super.onDestroy();
sm.unregisterListener(humidityEventListener);
}
}
public class HumidityActivity extends Activity implements SensorCallback {
private HumidityService mService;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 省略部分代码...
bind();
}
// 通过bind方式可以得到Service的实例,这个实例的public方法都可以调用,当然包含设置回调函数的方法
ServiceConnection conn = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
HumidityService.MyBinder myBinder = (HumidityService.MyBinder) iBinder;
mService = myBinder.getService();
mService.setCallback(HumidityActivity.this);
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
}
};
// 回调湿度传感器数据
@Override
public void callback(float data) {
Log.d(TAG, "callback data: " + data);
// 省略显示UI代码...
}
private void bind() {
Intent intent = new Intent(HumidityActivity.this, HumidityService.class);
bindService(intent, conn, Context.BIND_AUTO_CREATE);
}
@Override
protected void onDestroy() {
super.onDestroy();
unbindService(conn);
}
}
从代码流程可以看到,当打开了用户界面,会bindSerivce,并获取Service实例,设置回调函数获得传感器数据。但是通过bindService接口启动的服务有一个特点:Service的生命周期和UI界面会绑定在一起,也就是当用户退出界面,UI Destory后服务也会Destory。这样设备就停止工作了。前面的项目需求里要求设备24小时不停的工作的,这显然是不符合要求的。
方式三:startService和bindService混合双打
bindService和startService的显著区别:bindService启动的服务生命周期和UI绑定,同命运共存亡;startService启动的服务不会和其它东西绑定,如果声明为前台服务,除非在特殊情况下(用户主动杀掉进程或者系统内存不足查杀)会结束掉生命周期,否则会一直在后台运行。
public class BootCompleteReceiver extends BroadcastReceiver {
private static final String BOOT_COMPLETED = "android.intent.action.BOOT_COMPLETED";
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (TextUtils.isEmpty(action)) return;
if (action.equals(BOOT_COMPLETED)) {
Intent i = new Intent(context, HumidityService.class);
context.startService(i);
}
}
}
其它代码和前面的流程大致相同,如何声明前台服务等也不再细说。先通过startService启动服务,使后台服务持续运行;当用户打开界面的时候通过bindService绑定服务,设置回调函数获得传感器数据更新UI,当用户退出界面则Unbind,但是后台服务不会消亡,还是在后台持续运行。通过startService和bindService的混合使用方式,可以改变Service的生命周期,得到预期的结果。
方式四:单例传值
public class DataTransManager {
private static volatile DataTransManager mInstance = null;
private SensorCallback callback;
private DataTransManager() {
// nothing...
}
public static DataTransManager getInstance() {
if (mInstance == null) {
synchronized (DataTransManager.class) {
if (mInstance == null) {
mInstance = new DataTransManager();
}
}
}
return mInstance;
}
public SensorCallback getCallback() {
return callback;
}
public void setCallback(SensorCallback callback) {
this.callback = callback;
}
public void dataTrans(float data) {
if (callback != null) {
callback.callback(data);
}
}
}
上面定义了一个单例类,SensorCallback 回调接口还是前面定义的那个。
public class HumidityActivity extends Activity implements SensorCallback {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 省略部分代码...
}
// 回调湿度传感器数据
@Override
public void callback(float data) {
Log.d(TAG, "callback data: " + data);
// 省略显示UI代码...
}
@Override
protected void onResume() {
super.onResume();
// 通过单例设置回调接口
if (DataTransManager.getInstance().getCallback() == null) {
DataTransManager.getInstance().setCallback(this);
}
}
@Override
protected void onPause() {
super.onPause();
// 反注册回调接口
DataTransManager.getInstance().setCallback(null);
}
}
public class HumidityService extends Service {
private SensorManager sm;
private Sensor mHumiditySensor;
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// 省略...
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onCreate() {
super.onCreate();
sm = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
mHumiditySensor = sm.getDefaultSensor(Sensor.TYPE_RELATIVE_HUMIDITY);
sm.registerListener(humidityEventListener, mHumiditySensor, SensorManager.SENSOR_DELAY_NORMAL);
}
// 监听湿度传感器,获取湿度值通过单例的方式调用回调接口
SensorEventListener humidityEventListener = new SensorEventListener() {
@Override
public void onSensorChanged(SensorEvent sensorEvent) {
float[] values = sensorEvent.values;
DataTransManager.getInstance().dataTrans(values[0]);
}
@Override
public void onAccuracyChanged(Sensor sensor, int i) {
}
};
@Override
public void onDestroy() {
super.onDestroy();
sm.unregisterListener(humidityEventListener);
}
}
通过单例传值的方式可以很方便的实现预定义的功能,后台服务一直在运行监听环境湿度,并传递到单例中,如果有监听者,则把数据发送给它。当用户打开了前台界面,立即将自己作为监听者传递给单例,当用户离开前台界面,则解除监听者。
方式五:配合系统级别服务
这种方式只适合有源代码的方案公司。我们可以添加一个系统级服务,在后台监听环境湿度,如何添加系统服务参考我之前的文章:Android原生系统开发如何优雅的提供系统级的API供第三方程序调用?,或者 AIDL在android系统中的作用。
值得一提的是,使用这种方式开发的服务会变的及其稳定,因其实集成在framework中的,所以生命周期是和Android系统共存亡的,用户无法终止其运行,除非断电关机,否则会一直运行。并且可以很方便的交互,只要定义好对应的xxxManager类,设置回调接口或者存取值都是非常的便捷的。当然也是有缺点的,用户界面是一个独立的apk,它显示的数据全部来自于framework层的系统级服务,这样一来本来一个进程能完成的事情现在变成了两个进程。不过好在这种设计方式耦合性不大,因为这套服务一旦设计完成,就和系统其它服务一样,例如WifiManager等,都是对外接口,任何上层应用进程都可以调用,不存在和谁耦合的问题。
因这个方案涉及到源代码定制开发,具体代码不详细赘述,有兴趣的参考上面两篇文章链接。
其它方式
就这个简单的功能来说,可实现的方式还有很多,比如共享文件、Handler+Message等方式,对于我们定义的这个需求来说,这些方式都不太适合,或多或少都有各自的缺陷,这里也不在展开。个人觉得最强大的实现方式是方式五,当然开发条件也最严苛,代码量最大。最方便的是方式四单例传值。
本文旨在大家一起探讨,如果有更好的性能更佳的实现方式,欢迎留言评论。