Service与Android系统设计(五)
特别声明:本系列文章LiAnLab.org著作权所有,转载请注明出处。作者系LiAnLab.org资深Android技术顾问吴赫老师。本系列文章交流与讨论:@宋宝华Barry
1.1 双向Remote Service
在AIDL编程环境里实际上是支持反向调用的,原因跟我们实现一个Remote Service一样,就是通过把Proxy与Stub反过来,就得到了这样的回调式的aidl编程。唯一的区别是,当我们的Stub在Activity时实现时,我们实际上跟后台线程执行也没有区别,Callback并非是在主线程里执行的,于是不能进行重绘界面的工作。于是,我们必须像后台线程编程一样,使用Handler来处理界面显示处理。
前面我们说过aidl是可以互相引用的,于是我们可以借用这样的机制,通过引用另一个新增的aidl文件来加强我们前面的单向的TaskService版本。我们先增加一个新的ITaskServiceCallback.aidl文件,与ITaskService保持同一目录:
package org.lianlab.services;
onewayinterface ITaskServiceCallback {
void valueCounted(int value);
}
在这一定义里,我们新增加了一个ITaskServiceCallback的接口类,基本上与我们前面的ITaskService.aidl一样,在这个接口类里,我们新加了一个valueCounted()方法,这一方法将会被Service所使用。在这个文件里,唯一与ITaskService.aidl不同之处在于,我们使用了一个oneway的标识符,oneway可以使aidl调用具有异步调用的效果。在默认情况下,基于aidl的调用都会等待远端调用完成之后再继续往下执行,但有时我们可能希望在跨进程调用会有异步执行的能力,我们在发出调用请求后会立即返回继续执行,调用请求的结果会通过其他的callback返回,或是我们干脆并不在乎成功与否,此时就可以使用oneway。当然,从我们前面分析aidl底层进行的工作,我们可以知道,所谓的远程调用,只不过是通过Binder发送出去一个命令而已,所以在aidl里面如果使用了oneway限定符,也就是发送了命令就收工。
然后,我们修改一下我们的ITaskService.aidl,使我们可以使用上这个新加入的回调接口:
package org.lianlab.services;
importorg.lianlab.services.ITaskServiceCallback;
interface ITaskService {
intgetPid (ITaskServiceCallback callback);
}
我们会引用前面定义好的ITaskServiceCallback.aidl文件,通过包名+接口的方式进行引用。为了省事,我们直接在原来的getPid()方法里进行修改,将新定义的ITaskServiceCallback接口类作为参数传递给getPid()接口。于是,在Service端Stub对象里实现的getPid()方法,将可以使用这一回调对象:
package org.lianlab.services;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.Process;
import android.os.RemoteException;
public class TaskService extends Service {
static private int mCount = 0;
@Override
public IBinder onBind(Intent intent) {
if (ITaskService.class.getName().equals(intent.getAction())) {
return mTaskServiceBinder;
}
return null;
}
private final ITaskService.Stub mTaskServiceBinder = newITaskService.Stub() {
public int getPid(ITaskServiceCallback callback) { 1
mCount ++ ; 2
try { 3
callback.valueCounted(mCount); 4
} catch (RemoteException e) {
e.printStackTrace();
}
return Process.myPid();
}
};
}
加入了回调之后的代码结构并没有大变,只增加了3部分的内容,通过这三部分的内容,我们此时便可以记录我们的getPid()总共被调用了多少次。
1 getPid()方法,是通过aidl定义来实现的,否则会报错。所以我们这里新的getPid()会按照aidl里的定义加入ITaskServiceCallback对象作为参数,与ITaskService对象相反,这一对象实际上是由客户端提供给Service端调用的。
2 为了记录下getPid()被调用了多少次,我们使用了一个mCount来进行计数,这一int为static类型,于是在Service生存周期里会始终有效。但这部分的改动与我们的回调改进并无直接关系。
3 在使用回调接口ITaskServiceCall之前,因为这是一个远程引用,我们会需要捕捉Remote Exception,由客户端抛出的异常将在这里被捕获处理。
4 调用ITaskServiceCall里定义的回调方法,将处理发送给客户端。此时,因为是oneway,这时很多就会从回调方法里返回,继续执行原来的getPid(),再将处理结果以返回值的形式发送回客户端。
加入了异常调用之后,对Service端的实现并没有增加多大的工作量,因为作为回调,实现是放在客户端上来完成的。因为我们的aidl接口已经发生了变动,于是需要将新加的ITaskServiceCall.aidl与改变过的ITaskService.aidl文件拷贝到应用程序工程里。我们再来看一下客户端实现代码需要作怎样的调整:
package org.lianlab.services;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.TextView;
import org.lianlab.services.R;
public class MainActivity extends Activity {
ITaskService mTaskService = null;
private TextView mCallbackText;
private Handler mHandler = new Handler();
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
try {
bindService(newIntent(ITaskService.class.getName()), mTaskConnection,
Context.BIND_AUTO_CREATE);
} catch (SecurityException e) {
e.printStackTrace();
}
setContentView(R.layout.activity_main);
((TextView)findViewById(R.id.textView1)).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mTaskService != null) {
try {
int mPid = -1;
mPid =mTaskService.getPid(mCounter); 1
((TextView)findViewById(R.id.textView1))
.setText("Service pid is " + mPid);
} catch (RemoteException e){
e.printStackTrace();
}
} else {
((TextView)findViewById(R.id.textView1)).setText("No service connected");
}
}
});
mCallbackText = (TextView) findViewById(R.id.callbackView); 2
mCallbackText.setText("Clicked 0 times");
}
@Override
public void onDestroy() {
super.onDestroy();
if (mTaskService != null) {
unbindService(mTaskConnection);
}
}
private ServiceConnection mTaskConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service){
mTaskService = ITaskService.Stub.asInterface(service);
}
public void onServiceDisconnected(ComponentName className) {
mTaskService = null;
}
};
private ITaskServiceCallback.Stub mCounter = newITaskServiceCallback.Stub() { 3
public void valueCounted(final int n) {
mHandler.post(new Runnable() { 4
public void run() {
mCallbackText.setText("Clicked " + String.valueOf(n) + " times");
}
});
}
};
}
新加入的回调接口对象,也没给我们带来多大的麻烦,我们最重要是提供一个实例化的ITaskServiceCallback.Stub对象,然后通过getPid()将这一远程对象的引用发送给Service端。之后,Service处理后的回调请求,则会通过Binder会回到客户端,调用在Stub对象里实现的回调方法。所以我们实际增加的工作量,也仅是写一个ITaskServiceCallback.Stub接口类的实现而已:
1 我们需要使用通过getPid(),将ITaskServiceCallback.Stub传递给Service。因为这一对象是用于Service使用的,于是我们必须在使用前先创建,然后再以引用的方式进行传递,像我们代码例子里的mCounter对象。
2 这一部分的改动,只是为了让我们检查效果时更方便,我们通过一个新加的id为callbackView的textView,来显示getPid()被调多次的效果。
3 这是我们真正所需的改动,通过新建一个ITaskServiceCallback.Stub对象,于是当前进程便有了一个Stub实体,用于实现aidl里定义的valueCounted()接口方法,对Binder过来这一接口方法的调用请求作响应。
4 valueCounted()是一个回调方法,从编程模型上我们可以类似的看成是由Service进程所执行的代码,于是我们需要通过Handler()来处理显示。当然,在实现上不可能如此神奇,我们可以把一个方法搬运到另一个进程空间里运行,但valueCounted()既然也不是在主线程环境里执行,而是通过线程池来响应Binder请求的,于是跟后台线程的编程方式一样,我们使用Handler来处理回显。Handler本身是一种很神奇的实现机制,它可以弱化编程环境里的有限状态机的硬性限制,也可以使代码在拓展上变得更灵活,我们会在后续内容里加以说明。
从上面的代码来看,我们通过aidl创建回调方法好像比我们直接通过aidl写一个Remote Service还要简单。事实上,并非回调创建方便,在原则上,我们本只需要一个Stub对象便可以得到我们想要的RPC能力了,只不过出于管理存活周期的需要,才融入到了Service管理框架里,因为这种Service使用上的需求才带来了一些编程上的开销。