9.Android中的IPC方式——Bundle、文件共享、Messenger、AIDL、Content-Provider

来源:互联网 发布:linux查看日志命令tail 编辑:程序博客网 时间:2024/04/30 04:04
    Android中的IPC方式有很多,比如可以通过Intent中附加extras(Bundle类型)来传递信息,或者通过共享文件的方式来共享数据,还可以采用前面的文章说的Binder方式来跨进程通信,另外ContentProvider天生就支持跨进程访问的(ContentProvider底层使用的就是Binder机制),所以我们也可以使用ContengProvider来进行IPC。另外,我们也可以通过网络通信的方式来实现IPC,所以,我们也可以使用Socket来实现IPC。下面将介绍几种IPC的方式。
    
    9.1 使用Bundle
    Android中的四大Component都支持在Intent中传递Bunlde数据。其中我们要知道,只有可序列化的类才能在进程间传递,Bundle正是实现了Parcelable接口,我们常用的方式比如,打开一个Service、Activity,或者是发送一个广播,这时候我们都可以在intent中setExtras来放入Bundle数据,使得进程之间可以互相通信。(广播其实是非常常用的一种IPC方式,它本质也是使用Bundle来实现)
    
     9.2 使用文件共享
    两个进程通过读/写同一个文件来交接数据,比如A进程把数据写入文件,B进程通过读取这个文件来获取数据。Android是基于Linux的,所以并发读、写文件可以没有限制地进行,甚至多个线程对一个文件进行写操作都是允许的,所以这样也很容易造成脏数据等问题。所以说,文件共享的方式适合在数据同步要求不高的进程之间进行通信,并且要妥善处理并发读写的问题。我们知道Sharepreferences也是以文件形式的存储数据,所以,我们也可以用Sharepreferences  来实现进程通信。
    
     9.3 使用Messenger
    Messenger意思是信差。因此,它其实就是用来传递Message数据的。在Messager中加入我们需要传递的数据,就可以轻松地实现数据的进程间的通信了。(底层还是使用Binder实现的),在Message中放入我们需要传递的数据,就可以轻松地实现数据的进程间传递了。Messenger是一种轻量级的IPC方案,它的底层实现是AIDL。
     /**     * Create a Messenger from a raw IBinder, which had previously been     * retrieved with {@link #getBinder}.     *      * @param target The IBinder this Messenger should communicate with.     */    public Messenger(IBinder target) {        mTarget = IMessenger.Stub.asInterface(target);    }


    从这个函数中,就可以知道,Messenger其实就是AIDL。

    Messenger的使用是比较简单的,它对AIDL做了封装,所以我们可以更简便地进行进程间的通信。并且,Messenger一次只处理一个请求,因此在服务端我们不需要考虑线程同步的问题(已经为我们处理好了,那么普通的AIDL有这个问题吗?)。实现一个Messenger有如下几个步骤,可以分为服务端和客户端。
    
    1.服务端进程    
    下面看下服务端的代码实现:
package com.example.messengerserverapp;import android.app.Service;import android.content.Intent;import android.os.Bundle;import android.os.Handler;import android.os.IBinder;import android.os.Message;import android.os.Messenger;import android.os.RemoteException;import android.util.Log;public class MyMessengerService extends Service {    public static final String TAG  = "MyMessengerService";    public static final int MSG_FROM_CLIENT = 1;    public static final int MSG_FROM_SERVICE  = 2;    Handler handler = new Handler()    {        public void handleMessage(android.os.Message msg) {            switch (msg.what) {            case MSG_FROM_CLIENT:                Log.i( TAG,msg.getData().getString( "msg") );                Messenger messenger = msg.replyTo;                Message message = Message.obtain(null, MSG_FROM_SERVICE);                Bundle bundle = new Bundle();                bundle.putString( "reply", "服务端已经接受到了您的请求,已经处理完毕");                message.setData(bundle);                try {                    messenger.send(message);                } catch (RemoteException e) {                    // TODO: handle exception                }                break;            default:                break;            }        };    };    Messenger messenger = new Messenger(handler);    @Override    public IBinder onBind(Intent arg0) {         return messenger.getBinder();    }}


从上面的代码中可以看到,在服务端,我们可以给Messenger指定一个Handler来处理客户端发送的数据。服务端在onBind方法中返回这个Messenger的Binder对象,然后,客户端就可以获取到这个Binder的对象,在前面的文章:http://blog.csdn.net/savelove911/article/details/51288577中,我们学过了Binder了,这里就不再赘述了。接下来我们开始看客户端的代码。

2.客户端进程
    下面是客户端的代码实现:
package com.example.messengerclientapp;import android.content.ComponentName;import android.content.Context;import android.content.Intent;import android.content.ServiceConnection;import android.os.Bundle;import android.os.Handler;import android.os.IBinder;import android.os.Message;import android.os.Messenger;import android.os.RemoteException;import android.support.v4.app.Fragment;import android.support.v4.widget.SearchViewCompatIcs.MySearchView;import android.support.v7.app.ActionBarActivity;import android.util.Log;import android.view.LayoutInflater;import android.view.Menu;import android.view.MenuItem;import android.view.View;import android.view.ViewGroup;public class MainActivity extends Activity {    public static final String TAG = "MessengerActivity";    public static final int MSG_FROM_CLIENT = 1;    public static final int MSG_FROM_SERVICE = 2;    private Messenger mService;    private Messenger mGetReplyMessenger = new Messenger( new Handler() {        public void handleMessage(Message msg) {            switch (msg.what) {            case MSG_FROM_SERVICE:                Log.i( TAG,"receiver msg from services:" + msg.getData().getString( "reply"));                break;            default:                break;            }        };    });    private ServiceConnection mConnection = new ServiceConnection(){        @Override        public void onServiceConnected(ComponentName arg0, IBinder arg1) {            mService = new Messenger(arg1);            Message msg = Message.obtain( null , MSG_FROM_CLIENT );            Bundle data = new Bundle();            data.putString( "msg", "这个消息来自于客户端");            msg.setData(data);            msg.replyto = mGetReplyMessenger;            try {                mService.send(msg);            } catch (RemoteException e) {             }        }        @Override        public void onServiceDisconnected(ComponentName arg0) {        }    };    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        Intent startService = new Intent( );        startService.setAction( "com.zhenfei.messengertest");        bindService(startService, mConnection, Context.BIND_AUTO_CREATE);         }    @Override    protected void onDestroy() {         super.onDestroy();        unbindService(mConnection);    } }


接下来我们主要看下面这几行代码中:
  mService = new Messenger(arg1);
   在这行代码中,我们使用了服务端的Binder对象来创建Messenger对象,这个Messenger对象可以根据Binder对象发送Message给服务端。实现细节这边就不进行讨论了。然后,服务端就会接收到客户端的消息了,并且,在本例中,服务端可以在接收到消息以后返回数据给客户端,因为我们在客户端设置了replyTo,看到如下代码:
     msg.replyto = mGetReplyMessenger;
   在这边,我们把客户端的Messenger对象放在了Message当中传递给了服务端,所以服务端那边可以通过Message来获取客户端的Messenger,使得他们之间可以互相通信。
    
    本例只是Messenger的简单应用,在实际应用的时候,我们可以在Message中放入一些客户端进程的信息,然后,服务端那边可以用一个List或者是一个Map来维护客户端的发送过来的客户端信息和Message的replyTo属性,这样服务端就能和客户端进行通信了。
    那么,客户端不发送信息,服务端能主动给客户端发送信息吗?答案是,NO。不行.简单地理解,你不写信给别人,别人如何给你回信呢?所以,使用Messenger在连接服务端后,只能客户端先发送Message给服务端。

 9.4 使用AIDL进行进程通信。
   9.4.1 在AIDL中设置回调
    
在前面的文章中:http://blog.csdn.net/savelove911/article/details/51288577 我们已经简单地使用了AIDL,接下来,我们进一步丰富AIDL的使用。其中包括了服务端主动回调客户端的接口,实现客户端监听服务端,比如在前面的例子中,当服务端添加了一本书以后,回调给客户端,通知客户端。下面我们就开始继续完善上面的代码,代码从上一篇文章开始修改。
    首先,在这边先实现客户端添加书到服务端上,代码如下:
    
public void onClick( View view )    {        switch (view.getId()) {        case R.id.addNewBook:            if( iBookManager != null )            {                try {                    iBookManager.addBook( new Book( "客户端的第"+cilentCount+"本书" ));                } catch (RemoteException e) {                    // TODO Auto-generated catch block                    e.printStackTrace();                }            }            break;        default:            break;        }    }      


    其实就是调用IBookManager的addBook方法,接下来我们重点放在符合让服务端回调客户端。在AIDL中,我们是无法使用普通的接口的,只能使用AIDL接口(进程不同,所以无法互相访问对方的内存,所以找不到对方的接口对象,自然无法使用普通的接口)
    那我们这边先做一个aidl接口,如下:
 
   package com.zhenfei.aidl;    import com.zhenfei.aidl.Book;    interface AddNewBookListener{    void onAddNewBook( in Book book );    }

    在这边,我们继续来讨论这个方法的声明,在上一篇文章,我们说过这样的一段话:

    如果client不需要传输数据给server,client只需要处理经过server处理过后的数据,

    那么 client 和 server 都为 out 

    如果client只需要传输数据给server,而不需要处理返回的数据,

    那么client和server都为 in


    那么,为什么上面我们这个方法声明是in,这个方法应该是Service调用,然后客户端App接收才对吧?
    那这边应该是out才对吧?
    错,这边必须是in。那上面的内容错了吗?也不是,我们这边先要搞清楚,什么样才叫服务端,什么样又叫客户端。
    在AIDL中,服务端并非固定是Service,并没有这样定义过,对于AIDL来说,服务端是指:
    创建了Binder对象那一方
    客户端是指:
    从Binder驱动获取Binder对象的那一方。

    所以,因为这个AddNewBookListener的Binder对象是在“客户端”创建的,所以,调用回调方法的时候,客户端充当了服务端的角色,因此,在此处,我们需要使用in 作为参数的方向。

    接下来,我们需要继续完善IBookManager代码:
    
package com.zhenfei.aidl;import com.zhenfei.aidl.Book;import com.zhenfei.aidl.AddNewBookListener;interface IBookManager{    List<Book> getBookList();    void addBook( in Book book);    void registerListener( AddNewBookListener listener);    void unregisterListener( AddNewBookListener listener);}这边看到,我们在AddNewBookListener 上,并不需要使用方向,因为他们并不是数据类型,而是接口类型的参数。然后,我们先完善客户端的代码,也就是在客户端上,加上添加监听器和注销监听器的代码: 如下:private Stub stub = new Stub() {        @Override        public List<Book> getBookList() throws RemoteException {             return books;        }        @Override        public void addBook(Book book) throws RemoteException {            books.add(book);            for( AddNewBookListener addNewBookListener : list )            {                addNewBookListener.onAddNewBook(book);            }        }        @Override        public void registerListener(AddNewBookListener listener)                throws RemoteException {            Log.i(TAG, "添加监听器成功");            list.add(listener);        }        @Override        public boolean unregisterListener(AddNewBookListener listener)                throws RemoteException {             for( AddNewBookListener addNewBookListener : list )             {                 if( addNewBookListener == listener )                 {                      list.remove(listener);                      return true;                 }             }            Log.i(TAG, "移除监听器失败,没有找到对应的Listener对象");             return false;        }    };

然后是客户端的监听器的代码:
private class MyAddNewBookListener extends AddNewBookListener.Stub    {       int pos;        private  MyAddNewBookListener( int pos )        {            this.pos = pos;        }        @Override        public void onAddNewBook(Book book) throws RemoteException {            Log.i(TAG, "第"+pos+"个监听器接收到回调,添加了一本新书:"+book.bookName);            List<Book> books;            try {                books = iBookManager.getBookList();                String formatStr = getResources().getString(R.string.tv_book_info);                formatStr = String.format(formatStr,books.size() + "");                placeholderFragment.tvBookInfo.setText( formatStr);            } catch (RemoteException e) {                 e.printStackTrace();            }        }    }

下面做添加监听器 并且注销监听器的操作,打印的日志如下:

 

我们会发现,在服务端是找不到这个Listener的,因为,客户端传过来的Listener,每次从服务端接口中获取,都是一个全新的,尽管看似都是一个对象,但是很显然他们的地址是不一样的。所以,直接匹配监听器对象来卸载是不现实的,那我们应该怎么做才能成功地注销监听器呢?
   
     一种方法是使用RemoteCallbackList,这个是系统专门提供的用于删除跨进程listener的接口,RemoteCallbackList是一个泛型,支持管理任意的AIDL接口。
    它的声明如下:
   
 public class RemoteCallbackList<E extends IInterface> 

    好了,在这边问一下,什么样的接口才能叫AIDL接口?我想,应该是实现了IInterface的接口。
    RemoteCallbackList 的工作原理很简单,在它的内部中有一个Map结构专门用来保存所有的AIDL接口,这个Map的key是IBinder类型,value是Callback类型,所以,其中key和value分别通过下面的方式来获取数得
 
   IBinder key = listener.asBinder();    Callback value = new Callback( listener , cookie);    

    下面,我们来使用RemoteCallbackList,首先把监听器列表类型改为RemoteCallbackList
    
private RemoteCallbackList<AddNewBookListener> mListenerList = new RemoteCallbackList<AddNewBookListener>();    

    然后修改resgisterListener 和unregisterListener 这两个接口是实现,如下:
    
    
public void registerListener( AddNewBookListener listener )    {        mListenerList.register( listener );    }    public void unregisterListener( AddNewBookListener listener)    {       mListenerList.unregister( listener );    }

    
    最后要修改addBook方法,有新书的时候就用纸所有已经注册的listener
    
 public void addBook( Book book ) throws RemoteException{       books.add( book );        final int N = mListenerList.beginBroadcast();        for( int i = 0 ; i < N ; i ++ )            {            AddNewBookListener listener = mListenerList.getBroadcastItem( i );            if( listener != null )            {                try{                        listener.onAddNewBook( book );                }catch( RemoteException exception)                {                    exception.printStackTrace();                }            }        }       mListener.finishBroadcast();    }


   这边需要注意,RemoteCallBackList它其实不是一个List所以,我们不能够像操作普通的List一样去操作它,所以我们只能够通过它的getBroadcastItem方法来获取其中的元素,而不能直接使用for进行遍历,而且beginBroadcast和finishBroadcast这两个方法,必须成对地使用。
    那上面就是系统提供给我们的监听器方法了,那还有别的方法可以做到注销监听器吗?其实是有的,比如我们给AddNewBookListener这个接口中,添加一个getId方法,然后把每个listener对象和一个id绑定,就可以实现Listener和id绑定。只会,我们就可以根据id匹配listener,这样就可以正常地注销listener对象了。
    在这边,我们考虑这样一种情况,客户端想要获取服务端有多少本书,服务端查询的时候,加入需要花费一些时间,那么,会发生什么样的情况呢?我们在服务端的getBookList方法改成如下:
    
<span style="white-space:pre"></span>@Override        public List<Book> getBookList() throws RemoteException {            SystemClock.sleep(3000);             return books;        }

    然后,我们点击按钮添加书本,并且获取几次获取图书列表,就会发现出现ANR了。这是因为,客户端执行AIDL接口方法的时候,会挂起当前的线程,然后去调用服务端的Binder线程,所以,如果在服务端的接口方法中做了耗时操作,就会造成无响应,ANR。
    所以我们应该尽量避免在服务端的接口方法上做耗时操作,如果这个接口方法是耗时的,我们就应该是用子线程去调用。
    同理,对于listener方法也是,服务端应该避免在主线程调用客户端的耗时方法。可以的话,可以在子线程中调用。
    
    9.4.2 处理Binder意外死亡事件
    
    为了程序的健壮性,我们接下来还要继续完善这个AIDL代码。比如当服务端进程挂掉了,那么就会导致Binder死亡,这时候,我们需要做一些相应的操作,比如重新连接远程服务端、或者是在客户端提醒远程服务可不用。
    一般来说,有两种方法来处理,第一种是设置Binder的死亡代理。
    
    这就需要使用到Binder很重要的两个方法linkToDeath和unlinkToDeath,在linkToDeath方法中,我们可以为Binder设置一个死亡代理,当Binder死亡的时候,我们就能够收到通知,这样我们就可以重新连接请求从而恢复连接。
    第一种是为Binder设置一个死亡代理,首先我们需要声明一个DeathRecipient对象,DeathRecipient是一个接口,这个接口只有一个方法就是binderDied,很明显在,在Binder死亡的时候,系统会回调这个方法。然后,我们就可以移除之前绑定的binder代理并且,重新绑定远程服务。如下:
 
       private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {        @Override        public void binderDied() {            if( iBookManager == null )                return ;            iBookManager.asBinder().unlinkToDeath(mDeathRecipient, 0 );            iBookManager = null;        }    };

     其次,在客户端绑定了远程服务成功以后,给binder设置死亡代理:
    
<span style="white-space:pre"></span>@Override        public void onServiceConnected(ComponentName service, IBinder binder) {             Log.i(TAG, "连接远程服务成功:"+service.getClassName());            iBookManager = IBookManager.Stub.asInterface(binder);            List<Book> books;            try {                //远程调用获取书本数量的方法                books = iBookManager.getBookList();                String formatStr = getResources().getString(R.string.tv_book_info);                formatStr = String.format(formatStr,books.size() + "");                placeholderFragment.tvBookInfo.setText( formatStr);                //绑定死亡代理                binder.linkToDeath(mDeathRecipient, 0);            } catch (RemoteException e) {                 e.printStackTrace();            }        } 

    其中第二参数是个标记位,一般设置为0就可以了。这样子,我们就成功地为客户端绑定了远程的Binder的死亡代理,这样Binder死亡的时候我们就可以接收到通知了,另外,通过Binder的方法,isBinderAlive也可以判断Binder是否死亡。
    
    第二种方法,是在客户端的onServiceDisconnected中重连远程服务,这两种方法,我们可以随便选择一种来使用,它们的区别在于,onServiceDisconnected在客户端的UI线程中被回调,而binderDied在客户端的Binder线程池中被回调,也就是说,在binderDied方法中,我们是不能访问UI的,这就是它们的区别。
    
    9.4.3 设置AIDL服务端的权限
    默认情况下,我们的远程服务,是允许任何人都可以连接的,但是这样会带来一些风险,因此,我们必须在服务上加入权限验证的功能,验证的方法很多。下面介绍两种常用的方法:
    第一种,可以使用权限来验证。
    首先,在服务端的AndroidManifest.xml中加入权限定义:
 <!-- 加入自定义的权限 -->
    <permission android:name="com.zhenfei.aidl.ACCESS_SERVER_SERVICE" android:protectionLevel="normal"></permission>
    然后,在Service中声明所需要的权限
 
<span style="white-space:pre"></span><service             android:name="com.zhenfei.aidl.ServerService"            android:permission="com.zhenfei.aidl.ACCESS_SERVER_SERVICE"            >            <intent-filter                 >                <action android:name="com.zhenfei.myaidl"/>            </intent-filter>        </service>

    之后,在客户端就可以使用这个权限来绑定服务,在客户端Androidmanifest.xml中加入如下权限:
        <uses-permission android:name="com.zhenfei.aidl.ACCESS_SERVER_SERVICE"/>

    这样就可以了。
    第二种,可以在服务端的onTransact方法中进行权限验证,如果验证失败,返回false,这样服务端就不会执行AIDL中的方法,验证的方法可以使用getCallingUid和getCallingPid来获取客户端所属应用的Uid和Pid,然后在通过这两个参数做一些验证工作,比如验证包名什么的。
    如下:
    public boolean onTransact( int code, Parcel data,Parcel reply , int flags) throws RemoteException    {        int check = checkCallingOrSelfPermission( "com.zhenfei.aild.ACCESS_SERVR_SERVICE");        if( check == PackageManager.PERMISSION_DENIED)        {            return false;        }        String packageName = null;        String[] packaghes = getPackageManager().getPackagesForUid( getCallingUid());        if( package != null && packages.length>0)            packageName = packages[0];                if( !packageName.startWith( "com.zhenfei")){            return false;        }        return super.onTransact( code, data , reply ,flags);    }


    上面的代码,服务端在new一个Stub对象的时候,覆盖重写即可。

    好了,关于AIDL的介绍就到这为止,IPC方式还有一种很常用的,那就是四大组件之一:ContentProvider。
    

9.5 Content Provider简介
    ContentProvider是Android提供的专门用于不同应用之间进行数据共享的方式,因此,ContentProvider天生就支持进程之间通信。和Messenger一样,ContentProvider的底层也是用Binder来实现的,只是由于系统为我们封装了许多,因此,使用起来,比AIDL简单多了。在这边就先不重点介绍了。
    
9.6 使用Socket进行IPC
    Socket也就是我们常说的套接字,我们经常使用Socket来实现网络编程,分为流式套接字和用户数据报套接字。分别对应网络传输协议的TCP协议和UDP协议。TCP协议是面对连接的协议,而UDP协议是面对无连接的协议。Socket编程是属于Java基础范畴的,这里就不进行详细的描述,大致就是服务端打开个端口,让客户端访问。
2 0