安卓中不同APP之间的消息通信

昨天在腾讯实习生招聘初试面试时面试官问道我关于两个APP之间相互通信的方式,当时自己回道到了contentProvider与BroadcastReceiver。但他接着问还有没有其它的方式,我跟他说可以使用AIDL,但是当时没说清楚,所以最后我说目前只知道这两种方式,然后他说可以使用文件的方式或云端存储的方式共享。面试回来后自己上网查了一下相关知识,根据自己的理解将安卓中不同APP之间消息通信总结如下:

首先要明白消息通信一般包括两种,一种是简单的数据访问,如ContentProvider,使用文件或云端方式共享,一种是消息的传递(传递的任然是数据,但不再是单纯的数据访问,而是组件之间的相互通信),如AIDL,BroadcastReceiver,Messenger。

一使用ContentProvider

ContentProvider(内容提供者)是Android中的四大组件之一,内容提供者将一些特定的应用程序数据供给其它应用程序使用,它主要作用是用来在多个APP之间共享数据,如腾讯QQ中的QQ电话功能需要获取用户手机上的联系人的手机号码,还算不上标准的多个APP之间的消息通信(因为共享数据只是消息通信中很小的一部分)。共享的数据可以存储于文件系统、SQLite数据库或其它方式。内容提供者继承于ContentProvider 基类,为其它应用程序取用和存储它管理的数据实现了一套标准方法。然而,应用程序并不直接调用这些方法,而是使用一个
ContentResolver 对象,调用它的方法作为替代。ContentResolver可以与任意内容提供者进行会话,与其合作来对所有相关交互通讯进行管理。

无论数据的来源是什么,ContentProvider都会认为是一种表,然后把数据组织成表格形式,因此该类提供给我们的方法类似于sqlite数据库中的增删改查的操作。

要使用ContentProvider我们需要先来了解一个概念URI。

1 URI(统一资源标识符(Uniform Resource Identifier))用来唯一的标识一个资源。在上述我说过ContentProvider是用来在多个APP之间共享数据的,那么首先我们得找到这个数据,这个资源(数据属于资源的一种)是采用URI来标识的。它包括三个部分:scheme authority and path,其中在安卓*ContentProvider访问的scheme固定值为:content://(就像Http协议固定值为http://),authority包括host和port。

scheme:标准前缀,用来说明一个Content Provider控制这些数据,无法改变的;"content://"

authority:URI 的标识,用于唯一标识这个ContentProvider,外部调用者可以根据这个标识来找到它。它定义了是哪个Content Provider提供这些数据。对于第三方应用程序,为了保证URI标识的唯一性,它必须是一个完整的、小写的类名。这个标识在 元素的 authorities属性中说明:一般是定义该ContentProvider的包.类的名称

path:路径(path),通俗的讲就是你要操作的数据库中表的名字,或者你也可以自己定义,记得在使用的时候保持一致就可以了;"content://com.bing.provider.myprovider/tablename"

如果URI中包含表示需要获取的记录的ID(数据以表的形式表示),则就返回该id对应的数据,如果没有ID,就表示返回全部

要操作的数据不一定来自数据库,也可以是文件、xml或网络等其他存储方式,如要操作xml文件中person节点下的name节点,可以构建这样的路径:/person/name

如果要把一个字符串转换成Uri,可以使用Uri类中的parse()方法,如下:Uri uri = Uri.parse("content://com.demo.provider.personprovider/person");

2ContentProvider共享数据

我们先来看一下ContentProvider(ContentProvider为一个抽象类)的重要方法:

  public abstract boolean onCreate();

 public abstract Cursor query(Uri uri, String[] projection,
String selection, String[] selectionArgs, String sortOrder); public Cursor query(Uri uri, String[] projection,
String selection, String[] selectionArgs, String sortOrder,
CancellationSignal cancellationSignal) {
return query(uri, projection, selection, selectionArgs, sortOrder);
} public abstract String getType(Uri uri);
public abstract Uri insert(Uri uri, ContentValues values);
public abstract int delete(Uri uri, String selection, String[] selectionArgs);
public abstract int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs);

可以看到ContentProvider中的增删改查这些重要的方法都是抽象的,因此当我们继承自该类时需要重写其抽象方法。

3ContentResolver来操作数据

当外部应用需要对ContentProvider中的数据进行添加、删除、修改和查询操作时,需要使用ContentResolver类来完成,要获取ContentResolver对象,可以使用Context提供的getContentResolver()方法(即该方法位于Context类中)。

ContentResolver cr = getContentResolver();

ContentProvider负责组织应用程序的数据,向其他应用程序提供数据,ContentResolver则负责获取ContentProvider提供的数据。因此可以知道ContentResolver中也因该存在增删改查的接口。

 public final Cursor query(Uri uri, String[] projection,
String selection, String[] selectionArgs, String sortOrder) {
return query(uri, projection, selection, selectionArgs, sortOrder, null);
}
public final Uri insert(Uri url, ContentValues values)
public final int delete(Uri url, String where, String[] selectionArgs) public final int update(Uri uri, ContentValues values, String where,
String[] selectionArgs) public final String getType(Uri url)

可以看到在ContentResolver中存在于ContentProvider相对应的增删改查的方法接口,而且这些方法都是final修饰的,另外这些方法的第一个参数为Uri,代表要操作的ContentProvider(通常用包名+类名表示)和对其中的什么数据(通常是以表形式存储的)进行操作。代码如下:

ContentResolver resolver =  getContentResolver();//首先获取ContentResolver对象
Uri uri = Uri.parse("content://com.demo.provider.personprovider/person");//定义要访问的ContentProvider的RUI
//添加一条记录
ContentValues values = new ContentValues();
values.put("name", "htq");
values.put("age", 20);
resolver.insert(uri, values); //获取person表中所有记录
Cursor cursor = resolver.query(uri, null, null, null, "personid desc");
while(cursor.moveToNext()){
Log.i("ContentTest", "personid="+ cursor.getInt(0)+ ",name="+ cursor.getString(1));
} //把id为1的记录的name字段值更改新为zhangsan
ContentValues updateValues = new ContentValues();
updateValues.put("name", "zhangsan");
Uri updateIdUri = ContentUris.withAppendedId(uri, 2);
resolver.update(updateIdUri, updateValues, null, null); //删除id为2的记录
Uri deleteIdUri = ContentUris.withAppendedId(uri, 2);
resolver.delete(deleteIdUri, null, null);

二使用文件或云端方式共享

使用文件就是把数据以文件的形式保存,然后提供一定的访问权限供其它APP访问,云端共享与此类似只不过存储位置位于云端而已,云端共享最典型的莫过于搜索引擎与云盘共享。



三使用BroadcastReceiver

上述介绍的几种方式算不上完完全全的APP之间的消息通信,因为上述仅仅是多个APP之间共享数据而已,而在安卓中广播机制就是用来在多个APP之间通信的较好的方式,如多个APP可以响应系统网络监听的广播。一般广播的工作流程如下:

1.广播接收者BroadcastReceiver通过Binder机制向AMS(Activity Manager Service)进行注册;



2.广播发送者通过binder机制向AMS发送广播;



3.AMS查找符合相应条件(IntentFilter/Permission等)的BroadcastReceiver,将广播发送到BroadcastReceiver(一般情况下是Activity)相应的消息循环队列中;



4.消息循环执行拿到此广播,回调BroadcastReceiver中的onReceive()方法。

一般广播的使用流程如下:

1定义一个子类继承自抽象的BroadcastReceiver类,重写其抽象的onReceive(Context context, Intent intent)方法,当广播接收者收到广播会自动回调该方法。

2注册/注销该广播:通过intentFilter.addAction(Constants.ACTION_MSG);来指定注册的广播对哪种广播消息进行响应

3在另一个组件中使用intent.setAction(Constants.ACTION_MSG); sendBroadcast(intent);来指定发送的广播类型,如果要传递数据可以使用intent.putExtra(Constants.MSG, msg);

如在BaseActivity中响应ACTION_MSG,在getMsgService中发送ACTION_MSG的广播,代码如下:

//接受广播的BaseActivity类
public abstract class BaseActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
}
protected void onStart() {
// TODO Auto-generated method stub
super.onStart();
IntentFilter intentFilter=new IntentFilter();
intentFilter.addAction(Constants.ACTION_MSG);//指定响应<span style="font-family: Arial, Helvetica, sans-serif;">Constants.ACTION_MSG)的广播</span> registerReceiver(MsgReceiver, intentFilter);//注册广播
} @Override
protected void onStop() {
// TODO Auto-generated method stub
super.onStop(); unregisterReceiver(MsgReceiver);
}
BroadcastReceiver MsgReceiver=new BroadcastReceiver()//定义一个类继承自抽象的BroadcastReceiver类,此处采用的是匿名类的方式
{ @Override
public void onReceive(Context context, Intent intent) {//重写抽象的onReceive(Context context, Intent intent)方法
// TODO Auto-generated method stub
TransportObject msg=(TransportObject)intent.getSerializableExtra(Constants.MSG);
getMessage(msg); }};
protected abstract void getMessage(TransportObject msg); } //发送广播的getMsgService类
public class GetMsgService extends Service {
... @Override
public void onStart(Intent intent, int startId) {
new Thread(){
public void run()
{
...
if(isStart)
{
cit=client.getClientInputThread();
if(cit!=null)
{
cit.setMessageListener(new MessageListener() { public void getMessage(TransportObject msg) { if(msg!=null&&msg instanceof TransportObject)
{
//通过广播向Activity传递消息
Intent intent=new Intent();
intent.setAction(Constants.ACTION_MSG);
intent.putExtra(Constants.MSG, msg);//通过广播传递数据
sendBroadcast(intent);
}
}
});
}
else {
Log.i("GetMsgService","服务器端连接暂时出错");
// Toast.makeText(getApplicationContext(), "服务器端连接暂时出错,请稍后重试!",0).show();
}
} }
}.start();
} ... }

虽然上述的代码是在同一个APP中响应的该广播,但是如果多个APP中的广播注册时使用Constants.ACTION_MSG字符串指定的广播则getMsgService类中sendBroadcast时多个APP的广播可以响应。

四使用AIDL让多个APP与同一个Service通信

AIDL(Android Interface definition language),在Android中,每个应用运行在属于自己的进程中,无法直接调用到其他应用的资源,那么当多个APP之间相互通信的话,那么自然就转化为IPC机制了,而AIDL就是安卓系统中用来实现IPC机制的。那么哪些场合需要使用AIDL呢,安卓官方文档上说的很清楚:

Using AIDL is necessary only if you allow clients from different applications to access your service for IPC and want to handle multithreading in your service. If you do not need to perform concurrent IPC across different applications, you should create your interface by implementing a Binder or, if you want to perform IPC, but do not need to handle multithreading, implement your interface using a Messenger. Regardless, be sure that you understand Bound Services before implementing an AIDL.

通过上述文档叙述可以看到,当需要在不同APP之间访问同一个服务且处理多线程的时候需要用到AIDL,如果不是多个APP之间的IPC只需使用Binder机制,如果需要处理不同APP之间但是只是单线程的话只需使用Messager机制,即下面将介绍的一类情况。所以可以知道AIDL最佳使用情况是在不同APP之间访问同一个服务且处理多线程,因此可以知道AIDL可以用来处理不同APP之间的通信。

AIDL的使用:

一服务端:

1定义AIDL文件(该文件是一个接口,文件中的方法全部为抽象方法,如果格式正确,IDE会自动在gen目录下生成对应的java文件)

interface MyAIDL {
int plus(int a, int b);
}

2定义服务类(AIDL就是用来在多个APP之间访问同一个service的),在该服务类中定义对应的stub对象,在该stub对象中实现上述AIDL文件中定义的抽象方法,在服务的onBind(Intent intent)中返回该stub对象。AndroidManifest.xml配置相关属性。

public class MyService extends Service {

	......

	@Override
public IBinder onBind(Intent intent) {
return mBinder;//在onBind中返回该stub对象
} DemoAIDL.Stub mBinder = new Stub() {//在服务类中定义对应的stub对象,实现aidl中定义的抽象方法 @Override
public int plus(int a, int b) throws RemoteException {
return a + b;
}
}; }

AndroidManifest.xml配置相关属性如下:

<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

    package="com.example.servicetest"

    android:versionCode="1"

    android:versionName="1.0" >

    ......



    <service

        android:name="com.example.servicetest.MyService"

        android:process=":remote" >

        <intent-filter>

            <action android:name="com.example.servicetest.MyAIDLService"/>//注意action的android:name属性,该属性在客户端bindService中将会用到

        </intent-filter>

    </service>





</manifest>

上述定义服务的APP相当于服务端。

二客户端:

1我们只需要把服务端aidl文件拷到相应的目录中即可,IDE会自动生成相对应的java文件,这一部分和服务端相同,这样服务端和客户端就在通信协议上达到了统一。

2在客户端的Activity中与Service通信,在客户端的Activity中定义ServiceConnection类,在该类的onServiceConnected(ComponentName name, IBinder service)方法中通过xxx.Stub.asInterface(service);获取定义的AIDL文件生成的java类,(xxx为aidl文件自动生成的对应的java文件类名),使用bindService(intent, conn,Context.BIND_AUTO_CREATE);来绑定远程服务,注意此时的intent需要指定为我们在服务端创建的service的name属性。

<pre name="code" class="java">public class MainActivity extends Activity implements OnClickListener {

	...
private MyAIDL myAIDL; private ServiceConnection connection = new ServiceConnection() { @Override
public void onServiceDisconnected(ComponentName name) {
} @Override
public void onServiceConnected(ComponentName name, IBinder service) {
myAIDL = MyAIDL.Stub.asInterface(service);//在onServiceConnected中将IBinder转换为aidl对应的java类
try {
int result = myAIDL.plus(3, 5);
Log.d("TAG", "result is " + result);
} catch (RemoteException e) {
e.printStackTrace();
}
}
};
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button bindService = (Button) findViewById(R.id.bind_service);
bindService.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent("com.example.servicetest.MyAIDLService");//intent指定为我们在服务端创建的service的intent-filter中action的android:name属性。
bindService(intent, connection, BIND_AUTO_CREATE);
}
});
} }

五使用Messager

Messager实现IPC通信,底层也是使用了AIDL方式。和AIDL方式不同的是,Messager方式是利用Handler形式处理,因此,它是线程安全的,这也表示它不支持多线程处理;而AIDL方式是非线程安全的,支持多线程处理,因此,我们使用AIDL方式时需要保证代码的线程安全。

首先我们来看一下其构造函数:

    public Messenger(Handler target) {
        mTarget = target.getIMessenger();  
    }

可以看到Messenger的构造函数中的参数为Handler对象,Messager本质上就是跨进程使用Handler。

Messenger使用步骤:

客户端绑定服务端,在ServiceConnection类的onServiceConnection方法中将远程服务端传过来的binder对象转换为Messenger对象,调用Messenger的send函数,就可以把Message发送至服务端的Handler。同时,如果需要服务端回调客户端(往客户端的Handler发消息),则可以在send的Message中设置replyTo,服务端就可以往客户端发送消息了。

客户端代码:

public class MainActivity extends Activity {
  
    protected static final String TAG = "MainActivity";
    Messenger messenger;
    Messenger reply;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        reply = new Messenger(handler);
        Intent intent = new Intent("<span style="line-height: 25.2px; font-family: Verdana, Arial, Helvetica, sans-serif;">test.messenger.MessengerTestService</span><span style="line-height: 25.2px; font-family: Verdana, Arial, Helvetica, sans-serif;">");</span>
        // 绑定服务
        bindService(intent, new ServiceConnection() {
            @Override
            public void onServiceDisconnected(ComponentName name) {
            }
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                Toast.makeText(MainActivity.this, "bind success", 0).show();
                messenger = new Messenger(service);//将远程服务端中返回的IBinder对象转换为Messenger对象
            }
        }, Context.BIND_AUTO_CREATE);      
    }
    public void sendMessage(View v) {
        Message msg = Message.obtain(null, 1);
        // 设置回调用的Messenger
        msg.replyTo = reply;//<span style="font-family: Verdana, Arial, Helvetica, sans-serif;font-size:12px; line-height: 25.2px;">如果需要服务端回调客户端,<span style="font-family: Verdana, Arial, Helvetica, sans-serif;font-size:12px; line-height: 25.2px;">则可以在send的Message中设置replyTo,将客户端的Messenger传递给服务端</span></span>
        try {
            messenger.send(msg);
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }
    private Handler handler = new Handler() {//回调Messenger处理的Handler
        @Override
        public void handleMessage(Message msg) {
            Log.d(TAG, "回调成功");
        }
    };
}

服务端通过Message的replyTo取出客户端传递过来的Messenger,这样就可以通过该Messenger与客户端通信。

服务端通过Messenger的getBinder方法将IBinder对象返给客户端,用于共享服务端的Messenger。

服务端代码:

public class MessengerTestService extends Service {
    protected static final String TAG = "MessengerTestService";
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case 1:
                Log.d(TAG, "收到消息");
                 //获取客户端message中的Messenger,用于回调
                final Messenger callback = msg.replyTo;
                try {
                    // 回调
                    callback.send(Message.obtain(null, 0));
                } catch (RemoteException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                break;
            }
        }
    };
    @Override
    public IBinder onBind(Intent intent) {
        return new Messenger(mHandler).getBinder();//在onBind(Intent intent)方法中返回Messenger对应的binder对象。
    }
}

可以看到该方式与用AIDL方式整体大的框架基本相同,都是在远程服务端的Service中的onBind(Intent intent)中返回Ibinder对象,在客户端的ServiceConnection类的onServiceConnectioned(ComponentName name, IBinder service)中奖Ibinder转换为对应的对象,在AIDL中通过xxx.Stub.asInterface(service);转换为对应的aidl的java类,在Messenger中通过messenger
= new Messenger(service);转换为Messenger对象,然后利用这个对象就可相互通信。

上一篇:arm linux串口蓝牙工具移植及使用【转】


下一篇:望岳物业App开发过程记录