OPhone平台aidl文件不一致导致的问题及解决
OPhone平台开发, 2010-01-04 17:53:06
使用OPhone平台Service机制时,如果客户端所用的aidl文件和已安装的Service所使用的aidl文件不一致时会导致接口调用的错误,甚至会导致程序错误退出。比如Service升级时,会在aidl文件里增加或修改接口,如果客户端不更新所使用的aidl文件,这就会出现上述不一致的情况。本文主要分析这个问题的原因和解决方案。
Service
下面的
TestService类里的stub实现了3个方法,test1, test2, test3, 分别返回一个整数。这个service是在com.aidl.service这个包里的。
- package com.aidl.service;
- public class TestService extends Service
- {
- public IBinder onBind(Intent intent) {
- return binder;
- }
-
- private final ITestService.Stub binder = new ITestService.Stub(){
- public int test1(){
- return 1;
- }
- public int test2(){
- return 2;
- }
- public int test3(){
- return 3;
- }
- };
- }
- 定义aidl: ITestService.aidl
- package com.aidl.service;
- interface ITestService{
- int test1 ();
- int test2 ();
- int test3 ();
- }
- manifest定义
- <application android:label="@string/app_name">
- <service android:name="com.aidl.service.TestService">
- <intent-filter>
- <action android:name="com.aidl.intent.TEST_SERVICE" />
- </intent-filter>
- </service>
- </application>
package com.aidl.service; public class TestService extends Service { public IBinder onBind(Intent intent) { return binder; } private final ITestService.Stub binder = new ITestService.Stub(){ public int test1(){ return 1; } public int test2(){ return 2; } public int test3(){ return 3; } }; } 定义aidl: ITestService.aidl package com.aidl.service; interface ITestService{ int test1 (); int test2 (); int test3 (); } manifest定义 <application android:label="@string/app_name"> <service android:name="com.aidl.service.TestService"> <intent-filter> <action android:name="com.aidl.intent.TEST_SERVICE" /> </intent-filter> </service> </application>
这样
TestService作为一个apk,就是提供了一个service。而别的应用,只需要拿到ITestService.aidl文件就可以用这个service了。
Client的Activity
有时候,你实现的
service给不同的Activity用,而且并不都是和service在一个包里的,甚至不是一个apk里的,这时要使用service的接口就需要把aidl文件复制到自己的src目录下。
- package com.aidl.client;
- import com.aidl.service.ITestService;
- public class TestActivity extends Activity
- {
- ITestService mService;
-
- Button testButton1, testButton2, testButton3;
-
- private Intent intent = new Intent("com.aidl.intent.TEST_SERVICE");
-
- private ServiceConnection mConnection = new ServiceConnection() {
- public void onServiceConnected(ComponentName className, IBinder service) {
- mService = ITestService.Stub.asInterface(service);
- }
- public void onServiceDisconnected(ComponentName className) {
- mService = null;
- }
- };
-
- public void onCreate(Bundle savedInstanceState)
- {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.main);
- startService(intent);
- }
-
- public void onResume(){
- super.onResume();
- bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
-
- testButton1 = (Button)findViewById(R.id.test1);
- testButton1.setOnClickListener(new Button.OnClickListener(){
- public void onClick(View v){
- try {
- showToast(mService.test1());
- }catch(RemoteException e){
- }
- }
- });
- ...
- }
-
- public void onDestroy(){
- super.onDestroy();
- unbindService(mConnection);
- }
-
- ...
- }
package com.aidl.client; import com.aidl.service.ITestService; public class TestActivity extends Activity { ITestService mService; Button testButton1, testButton2, testButton3; private Intent intent = new Intent("com.aidl.intent.TEST_SERVICE"); private ServiceConnection mConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder service) { mService = ITestService.Stub.asInterface(service); } public void onServiceDisconnected(ComponentName className) { mService = null; } }; public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); startService(intent); } public void onResume(){ super.onResume(); bindService(intent, mConnection, Context.BIND_AUTO_CREATE); testButton1 = (Button)findViewById(R.id.test1); testButton1.setOnClickListener(new Button.OnClickListener(){ public void onClick(View v){ try { showToast(mService.test1()); }catch(RemoteException e){ } } }); ... } public void onDestroy(){ super.onDestroy(); unbindService(mConnection); } ... }
上面的
TestActivity在com.aidl.test包里,使用的是TestService的接口,这时需要在这个apk的代码目录里面有一份ITestService.aidl的拷贝。这个TestActivity有3个按钮,分别是test1, test2, test3。预期的情况是,点testX按钮时,调用TestService的testX接口,然后显示一个Toast,内容是TextX方法被调用了。如下图所示
问题出现
这时,如果
TestService的aidl有改变,比如增加或减少接口,别的使用旧的aidl的应用就会有问题。哪怕是aidl里面的接口顺序变化也会带来问题。(注:一般来讲service的接口一旦发布,是不好轻易改动的。但是在团队协作开发时,这个情况就会出现。)
下面是
TestService的新aidl:
- package com.aidl.service;
- interface ITestService{
- int test2 ();
- int test1 ();
- int test3 ();
- }
package com.aidl.service; interface ITestService{ int test2 (); int test1 (); int test3 (); }
而
TestActivity还使用旧的aidl,这时还点test1按钮:
预期应该调用
service的test1接口,但是却调了test2接口...
客户端与service的通讯
为什么会出现上述的问题呢?客户端与
service的通讯是通过binder进行的,在build的时候,客户端与service两边都会根据aidl文件生成具体的ITestService类。
客户端的ITestService
TestActivity是通过调用ITestService.Stub.asInterface(service)来得到Service在本进程的代理。下面是客户端生成的ITestService的asInterface函数,返回一个ITestService.Stub.Proxy的对象。
-
- public static com.aidl.service.ITestService asInterface(android.os.IBinder obj)
- {
- if ((obj==null)) {
- return null;
- }
- android.os.IInterface iin = (android.os.IInterface)obj.queryLocalInterface(DESCRIPTOR);
- if (((iin!=null)&&(iin instanceof com.aidl.service.ITestService))) {
- return ((com.aidl.service.ITestService)iin);
- }
- return new com.aidl.service.ITestService.Stub.Proxy(obj);
- }
public static com.aidl.service.ITestService asInterface(android.os.IBinder obj) { if ((obj==null)) { return null; } android.os.IInterface iin = (android.os.IInterface)obj.queryLocalInterface(DESCRIPTOR); if (((iin!=null)&&(iin instanceof com.aidl.service.ITestService))) { return ((com.aidl.service.ITestService)iin); } return new com.aidl.service.ITestService.Stub.Proxy(obj); }
Proxy里面实现了
test1等方法,而且定义了对应的TRANSACTION_test1等TRANSACTION code。当TestActivity调mService的test1时,就调用了Proxy的test1方法,而test1是调用transact方法进行进程间通讯,把TRANSACTION code通过binder发送到service进程。
- private static class Proxy implements com.aidl.service.ITestService{
- private android.os.IBinder mRemote;
- Proxy(android.os.IBinder remote){
- mRemote = remote;
- }
- public android.os.IBinder asBinder(){
- return mRemote;
- }
- public java.lang.String getInterfaceDescriptor(){
- return DESCRIPTOR;
- }
- public int test1() throws android.os.RemoteException{
- android.os.Parcel _data = android.os.Parcel.obtain();
- android.os.Parcel _reply = android.os.Parcel.obtain();
- int _result;
- try {
- _data.writeInterfaceToken(DESCRIPTOR);
- mRemote.transact(Stub.TRANSACTION_test1, _data, _reply, 0);
- _reply.readException();
- _result = _reply.readInt();
- }finally {
- _reply.recycle();
- _data.recycle();
- }
- return _result;
- }
- ...
-
- static final int TRANSACTION_test1 = (IBinder.FIRST_CALL_TRANSACTION + 0);
- static final int TRANSACTION_test2 = (IBinder.FIRST_CALL_TRANSACTION + 1);
- static final int TRANSACTION_test3 = (IBinder.FIRST_CALL_TRANSACTION + 2);
- }
private static class Proxy implements com.aidl.service.ITestService{ private android.os.IBinder mRemote; Proxy(android.os.IBinder remote){ mRemote = remote; } public android.os.IBinder asBinder(){ return mRemote; } public java.lang.String getInterfaceDescriptor(){ return DESCRIPTOR; } public int test1() throws android.os.RemoteException{ android.os.Parcel _data = android.os.Parcel.obtain(); android.os.Parcel _reply = android.os.Parcel.obtain(); int _result; try { _data.writeInterfaceToken(DESCRIPTOR); mRemote.transact(Stub.TRANSACTION_test1, _data, _reply, 0); _reply.readException(); _result = _reply.readInt(); }finally { _reply.recycle(); _data.recycle(); } return _result; } ... //接口的TRANSACTION code static final int TRANSACTION_test1 = (IBinder.FIRST_CALL_TRANSACTION + 0); static final int TRANSACTION_test2 = (IBinder.FIRST_CALL_TRANSACTION + 1); static final int TRANSACTION_test3 = (IBinder.FIRST_CALL_TRANSACTION + 2); }
service的ITestService
service生成的
ITestService的TRANSACTION code如下
- static final int TRANSACTION_test1 = (IBinder.FIRST_CALL_TRANSACTION + 0);
- static final int TRANSACTION_test2 = (IBinder.FIRST_CALL_TRANSACTION + 1);
- static final int TRANSACTION_test3 = (IBinder.FIRST_CALL_TRANSACTION + 2);
static final int TRANSACTION_test1 = (IBinder.FIRST_CALL_TRANSACTION + 0); static final int TRANSACTION_test2 = (IBinder.FIRST_CALL_TRANSACTION + 1); static final int TRANSACTION_test3 = (IBinder.FIRST_CALL_TRANSACTION + 2);
这时,
service进程的binder对象会调用onTransact函数,而这个函数是在service端生成的ITestService类里。在这里面,根据收到的TRANSACTION code是TRANSACTION_test1就会调TestService里面ITestService.Stub对象binder里面实现的test1函数然后返回。再通过ipc机制,把返回值发送给客户端进程。
- public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
- switch (code) {
- case INTERFACE_TRANSACTION:
- {
- reply.writeString(DESCRIPTOR);
- return true;
- }
- case TRANSACTION_test1:
- {
- data.enforceInterface(DESCRIPTOR);
- int _result = this.test1();
- reply.writeNoException();
- reply.writeInt(_result);
- return true;
- }
- case TRANSACTION_test2:
- {
- data.enforceInterface(DESCRIPTOR);
- int _result = this.test2();
- reply.writeNoException();
- reply.writeInt(_result);
- return true;
- }
- case TRANSACTION_test3:
- {
- data.enforceInterface(DESCRIPTOR);
- int _result = this.test3();
- reply.writeNoException();
- reply.writeInt(_result);
- return true;
- }
- }
- return super.onTransact(code, data, reply, flags);
- }
public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException { switch (code) { case INTERFACE_TRANSACTION: { reply.writeString(DESCRIPTOR); return true; } case TRANSACTION_test1: { data.enforceInterface(DESCRIPTOR); int _result = this.test1(); reply.writeNoException(); reply.writeInt(_result); return true; } case TRANSACTION_test2: { data.enforceInterface(DESCRIPTOR); int _result = this.test2(); reply.writeNoException(); reply.writeInt(_result); return true; } case TRANSACTION_test3: { data.enforceInterface(DESCRIPTOR); int _result = this.test3(); reply.writeNoException(); reply.writeInt(_result); return true; } } return super.onTransact(code, data, reply, flags); }
问题所在
如果客户端和
service的aidl文件是不一致的,就会出现问题了。
当
TestService使用新的aidl时
- package com.aidl.service;
- interface ITestService{
- int test2 ();
- int test1 ();
- int test3 ();
- }
package com.aidl.service; interface ITestService{ int test2 (); int test1 (); int test3 (); }
生成的
ITestService里面定义的TRANSACTION code如下:
- static final int TRANSACTION_test2 = (IBinder.FIRST_CALL_TRANSACTION + 0);
- static final int TRANSACTION_test1 = (IBinder.FIRST_CALL_TRANSACTION + 1);
- static final int TRANSACTION_test3 = (IBinder.FIRST_CALL_TRANSACTION + 2);
static final int TRANSACTION_test2 = (IBinder.FIRST_CALL_TRANSACTION + 0); static final int TRANSACTION_test1 = (IBinder.FIRST_CALL_TRANSACTION + 1); static final int TRANSACTION_test3 = (IBinder.FIRST_CALL_TRANSACTION + 2);
客户端
TestActivity还使用旧的aidl,生成的ITestService里面定义的TRANSACTION code如下:
- static final int TRANSACTION_test1 = (IBinder.FIRST_CALL_TRANSACTION + 0);
- static final int TRANSACTION_test2 = (IBinder.FIRST_CALL_TRANSACTION + 1);
- static final int TRANSACTION_test3 = (IBinder.FIRST_CALL_TRANSACTION + 2);
static final int TRANSACTION_test1 = (IBinder.FIRST_CALL_TRANSACTION + 0); static final int TRANSACTION_test2 = (IBinder.FIRST_CALL_TRANSACTION + 1); static final int TRANSACTION_test3 = (IBinder.FIRST_CALL_TRANSACTION + 2);
从上面的两组
TRANSACTION code可以看出,TRANSACTION code是根据aidl里接口声明的顺序生成的。IBinder.FIRST_CALL_TRANSACTION的值是1,也就是说TRANSACTION_test1的值在客户端里是1,而在service端是2! 而service端onTransact函数里的switch,当收到的code是1的时候,认为是应该调用TRANSACTION_test2对应的test2方法了。所以就出现上面的例子中,诡异的错乱现象了。
所以当
aidl里面函数的声明顺序改变,或者新加,删除函数,都会造成TRANSACTION code的值会不同。这样使用旧aidl文件的应用就可能出现问题!
解决方案
当
service升级时,为了避免出现上面的问题,应该保证aidl的变化不影响到旧有接口的TRANSACTION code。所以新的aidl的编写有以下几个注意点。
- 新加函数接口应该在旧有函数的后面。
- 尽量避免删除旧有函数,如果真的要删的话,可以保留函数名字作为占位,返回一个错误码之类的来解决。
- 不能改变原来的接口声明顺序。
当然如果改变原来函数接口,导致函数签名都变了的话,肯定会出错了,不过不是上面的错乱了。如果你非要这样改的话,会被别的工程师骂精神错乱的
!
如果是多人协作开发,使用同一个版本库的时候,所有使用
service的应用程序,不是把aidl代码cp到自己的目录里,而是建一个文件链接link到service目录里面的aidl。这样在service aidl文件有变化的时候,客户端不需要手动更新aidl文件。
比如上面例子中,
TestActivity的client/src/com/aidl/service/目录里面ITestService.aidl就是service/src/com/aidl/service/ITestService.aidl的link。
ln -s service/src/com/aidl/service/ITestService.aidl client/src/com/aidl/service/ITestService.aidl
建议
其实这个问题我觉得属于
service机制设计上的一个缺陷。如果客户端和service都是以函数签名而不是code来标志aidl里的接口,在onTransact()里使用函数签名进行判断具体调用哪个接口的话,就能根本上解决这个问题。而字符串的比较和int的比较的开销即使有性能差别,也是可以接受的。这样设计的话对于开发人员来说使用起来和原来一样方便,而且不会出现上述问题。
作者简介
Bear,豆瓣网开发工程师,曾在新浪网,雅虎中国工作,有3年的互联网开发经验,热衷开源项目,通过豆瓣电台手机客户端项目接触Android/OPhone平台,对手机上的应用开发很感兴趣,希望和大家多做交流。
[email protected]
(声明:本网的新闻及文章版权均属OPhone SDN网站所有,如需转载请与我们编辑团队联系。任何媒体、网站或个人未经本网书面协议授权,不得进行任何形式的转载。已经取得本网协议授权的媒体、网站,在转载使用时请注明稿件来源。)