Android aidl项目中服务端与客户端aidl文件不一致引起的问题

Android aidl项目中服务端与客户端aidl文件不一致引起的问题

 

前几天和林工联调的时候发现远程调用我的aidl接口时候,有的接口总是调用不正确,本来是想调用我的A接口,却实际调用出来却显示调用的我B接口。仔细检查了,没问题啊,不可能会把名称写错的啊。Aidl文件在最开始是固定的,但是接口在后面有增有减,就导致了我这边的aidl接口与他的aidl接口数量和顺序上都有差异了。后面怀疑是aidl文件引起的,后面让他传他的demo给我看看,没问题,仔细看了还是aidl文件有些差异,而且只是顺序不一样了。修改顺序后,效果就出来了。就是顺序引起的错误。

在网上找了一篇文章贴下来:http://www.myexception.cn/ai/603931.html

 

记录一下方便以后查看

使用OPhone平台Service机制时,如果客户端所用的aidl文件和已安装的Service所使用的aidl文件不一致时会导致接口调用的错误,甚至会导致程序错误退出。比如Service升级时,会在aidl文件里增加或修改接口,如果客户端不更新所使用的aidl文件,这就会出现上述不一致的情况。本文主要分析这个问题的原因和解决方案。
Service
        下面的TestService类里的stub实现了3个方法,test1, test2, test3, 分别返回一个整数。这个service是在com.aidl.service这个包里的。
view plaincopy to clipboardprint?
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了。
ClientActivity
       有时候,你实现的service给不同的Activity用,而且并不都是和service在一个包里的,甚至不是一个apk里的,这时要使用service的接口就需要把aidl文件复制到自己的src目录下。
view plaincopy to clipboardprint?
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);  
    }  
    ...  
}  
      上面的TestActivitycom.aidl.test包里,使用的是TestService的接口,这时需要在这个apk的代码目录里面有一份ITestService.aidl的拷贝。这个TestActivity3个按钮,分别是test1, test2, test3。预期的情况是,点testX按钮时,调用TestServicetestX接口,然后显示一个Toast,内容是TextX方法被调用了。如下图所示

问题出现
       这时,如果TestServiceaidl有改变,比如增加或减少接口,别的使用旧的aidl的应用就会有问题。哪怕是aidl里面的接口顺序变化也会带来问题。(注:一般来讲service的接口一旦发布,是不好轻易改动的。但是在团队协作开发时,这个情况就会出现。)
     下面是TestService的新aidl
view plaincopy to clipboardprint?
package com.aidl.service;  
interface ITestService{  
    int test2 ();  
    int test1 ();  
    int test3 ();  
}  
     而TestActivity还使用旧的aidl,这时还点test1按钮:
     预期应该调用servicetest1接口,但是却调了test2接口...
客户端与service的通讯
         为什么会出现上述的问题呢?客户端与service的通讯是通过binder进行的,在build的时候,客户端与service两边都会根据aidl文件生成具体的ITestService类。
客户端的ITestService
TestActivity是通过调用ITestService.Stub.asInterface(service)来得到Service在本进程的代理。下面是客户端生成的ITestServiceasInterface函数,返回一个ITestService.Stub.Proxy的对象。
view plaincopy to clipboardprint?
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_test1TRANSACTION code。当TestActivitymServicetest1时,就调用了Proxytest1方法,而test1是调用transact方法进行进程间通讯,把TRANSACTION code通过binder发送到service进程。
view plaincopy to clipboardprint?
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);  
}  
serviceITestService
         service生成的ITestServiceTRANSACTION code如下
view plaincopy to clipboardprint?
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 codeTRANSACTION_test1就会调TestService里面ITestService.Stub对象binder里面实现的test1函数然后返回。再通过ipc机制,把返回值发送给客户端进程。
view plaincopy to clipboardprint?
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);  
}  
问题所在
        如果客户端和serviceaidl文件是不一致的,就会出现问题了。
TestService使用新的aidl
view plaincopy to clipboardprint?
package com.aidl.service;  
interface ITestService{  
    int test2 ();  
    int test1 ();  
    int test3 ();  
}  
  生成的ITestService里面定义的TRANSACTION code如下:
view plaincopy to clipboardprint?
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如下:
view plaincopy to clipboardprint?
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! serviceonTransact函数里的switch,当收到的code1的时候,认为是应该调用TRANSACTION_test2对应的test2方法了。所以就出现上面的例子中,诡异的错乱现象了。

       所以当aidl里面函数的声明顺序改变,或者新加,删除函数,都会造成TRANSACTION code的值会不同。这样使用旧aidl文件的应用就可能出现问题!

解决方案
       当service升级时,为了避免出现上面的问题,应该保证aidl的变化不影响到旧有接口的TRANSACTION code。所以新的aidl的编写有以下几个注意点。
新加函数接口应该在旧有函数的后面。
尽量避免删除旧有函数,如果真的要删的话,可以保留函数名字作为占位,返回一个错误码之类的来解决。
不能改变原来的接口声明顺序。
       当然如果改变原来函数接口,导致函数签名都变了的话,肯定会出错了,不过不是上面的错乱了。如果你非要这样改的话,会被别的工程师骂精神错乱的!

       如果是多人协作开发,使用同一个版本库的时候,所有使用service的应用程序,不是把aidl代码cp到自己的目录里,而是建一个文件链接linkservice目录里面的aidl。这样在service aidl文件有变化的时候,客户端不需要手动更新aidl文件。

      比如上面例子中,TestActivityclient/src/com/aidl/service/目录里面ITestService.aidl就是service/src/com/aidl/service/ITestService.aidllink
ln -s service/src/com/aidl/service/ITestService.aidl client/src/com/aidl/service/ITestService.aidl

建议
       其实这个问题我觉得属于service机制设计上的一个缺陷。如果客户端和service都是以函数签名而不是code来标志aidl里的接口,在onTransact()里使用函数签名进行判断具体调用哪个接口的话,就能根本上解决这个问题。而字符串的比较和int的比较的开销即使有性能差别,也是可以接受的。这样设计的话对于开发人员来说使用起来和原来一样方便,而且不会出现上述问题。

 

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值