使用AIDL
前面说过,Messenger不能跨进程调用方法。所以如果想要在客户端调用服务端的方法,可以使用AIDL。
在下面的例子中,将继续使用用AIDL来分析Binder的工作机制这篇文章的创建的项目,并给这个项目添加简单的功能。
例子
客户端从服务端获取和添加图书
客户端要想从服务端获取图书列表,或者向服务端添加图书,可以通过调用服务端的方法来实现。
新建一个BookManagerService作为服务端,该Service运行在remote进程中。实现如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29public class BookManagerService extends Service {
//使用CopyOnWriteArrayList支持并发读写,因为可能又多个客户端同时访问AIDL方法,所以要进行同步
private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArrayList<>();
//服务端创建的Binder继承于IBookManager.Stub,并实现了它内部的AIDL方法
private Binder mBinder = new IBookManager.Stub() {
public List<Book> getBookList() throws RemoteException {
return mBookList;
}
public void addBook(Book book) throws RemoteException {
mBookList.add(book);
}
};
public void onCreate() {
mBookList.add(new Book(1, "Android开发艺术探索"));
mBookList.add(new Book(2, "第一行代码"));
}
public IBinder onBind(Intent intent) {
return mBinder;
}
}
服务端主要是创建一个Binder,该Binder继承于IBookManager.Stub,并实现了它内部的AIDL方法。然后通过onBind方法返回给绑定该服务的客户端。
在客户端(BookManagerActivity)中,先创建一个ServiceConnection:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23private ServiceConnection mServiceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName name, IBinder service) {
//绑定服务端的服务后,将服务端的Binder对象转换为客户端需要的AIDL接口类型对象
IBookManager bookManager = IBookManager.Stub.asInterface(service);
//通过得到的AIDL接口对象,从服务端得到图书信息,并向服务端添加一本书
try {
List<Book> list = bookManager.getBookList();
Log.d(TAG, "list type = " + list.getClass().getCanonicalName());
Log.d(TAG, "book list = " + list);
bookManager.addBook(new Book(3, "Effective Java"));
Log.d(TAG, "now, book list = " + bookManager.getBookList());
} catch (RemoteException e) {
e.printStackTrace();
}
}
public void onServiceDisconnected(ComponentName name) {
}
};
可以看到,客户端在绑定了服务端后,先将服务端的Binder对象转化为自己需要的AIDL接口对象,然后通过该对象进行相关操作。
创建好ServiceConnection后,客户端在onCreate中绑定服务:1
2Intent intent = new Intent(this, BookManagerService.class);
bindService(intent, mServiceConnection, BIND_AUTO_CREATE);
最后运行程序,打印结果如下:
客户端成功地跨进程调用了服务端的方法。
服务端通知客户端有图书更新
新增一个功能:当服务端有新书到来时,就会通知每一个申请了提醒功能的用户。
首先要定义一个接口,当有新书到达时,服务端利用该接口的回调,将新书信息传给客户端。由于AIDL无法使用普通接口,所以定义的接口必须为AIDL接口。
创建一个IOnNewBookArrivedListener.aidl文件:1
2
3
4
5
6
7
8
9// IOnNewBookArrivedListener.aidl
package com.feng.aidltest;
// Declare any non-default types here with import statements
import com.feng.aidltest.Book;
interface IOnNewBookArrivedListener {
void onNewBookArrived(in Book newBook); //当有新书来时,通知注册了提醒的用户
}
点击Build -> Make Project,生成对应的IOnNewBookArrivedListener接口。
另外,还要在IBookManager.aidl里新增两个方法,用于客户端注册和解除监听。代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14// IBookManager.aidl
package com.feng.aidltest;
//导入相关类
import com.feng.aidltest.Book;
import com.feng.aidltest.IOnNewBookArrivedListener;
interface IBookManager {
//...
void registerListener(IOnNewBookArrivedListener listener); //注册提醒
void unregisterListener(IOnNewBookArrivedListener listener); //取消提醒
}
注意:不要漏了import IOnNewBookArrivedListener。之后重新Make Project,生成新的IBookManager。
接下来看服务端和客户端的修改,首先是服务端BookManagerService:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87public class BookManagerService extends Service {
public static final String TAG = "fzh";
//该变量用于标记服务是否已被销毁
private AtomicBoolean mIsServiceDestroyed = new AtomicBoolean(false);
//使用CopyOnWriteArrayList支持并发读写,因为可能又多个客户端同时访问AIDL方法,所以要进行同步
private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArrayList<>();
//存储注册了提醒服务的各客户端的监听
private CopyOnWriteArrayList<IOnNewBookArrivedListener> mListenerList =
new CopyOnWriteArrayList<>();
//服务端创建的Binder继承于IBookManager.Stub,并实现了它内部的AIDL方法
private Binder mBinder = new IBookManager.Stub() {
//...
public void registerListener(IOnNewBookArrivedListener listener) throws RemoteException {
if (!mListenerList.contains(listener)) {
mListenerList.add(listener); //将客户端注册的监听添加到集合中
} else {
Log.d(TAG, "listener already exists");
}
}
public void unregisterListener(IOnNewBookArrivedListener listener) throws RemoteException {
if (mListenerList.contains(listener)) {
mListenerList.remove(listener); //将客户端要解除注册的监听从集合中删除
Log.d(TAG, "unregister success");
} else {
Log.d(TAG, "not found, can not unregister");
}
}
};
public void onCreate() {
//开启服务,每五分钟往服务端添加一本新的书
new Thread(new ServiceWorker()).start();
}
public void onDestroy() {
mIsServiceDestroyed.set(true); //标记服务已被销毁
super.onDestroy();
}
private void newBookArrived(Book book) throws RemoteException {
mBookList.add(book);
Log.d(TAG, "onNewBookArrived, notify " + mListenerList.size() +
" listeners");
//提醒各注册了监听的客户端
for (int i = 0; i < mListenerList.size(); i++) {
mListenerList.get(i).onNewBookArrived(book);
}
}
private class ServiceWorker implements Runnable {
public void run() {
//只有当服务没有被销毁时,才执行下面操作
//每5秒往服务端添加一本新的书,并通知注册了监听的客户
while (!mIsServiceDestroyed.get()) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int bookId = mBookList.size() + 1;
Book newBook = new Book(bookId, "book#" + bookId);
try {
newBookArrived(newBook);
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
}
//...
}
服务端主要做了这两件事:
- 实现了IBookManager的两个新增方法,当有客户注册或解除监听的时候,从监听集合中增加或删除相应的监听。
- 开启一个子线程,在子线程中每5秒往服务端新增一本图书,当通过接口回调提醒各注册了监听的客户端。
接着到客户端,客户端的修改如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68public class BookManagerActivity extends AppCompatActivity {
public static final String TAG = "fzh";
public static final int MESSAGE_NEW_BOOK_ARRIVED = 1;
private IBookManager mBookManager;
"HandlerLeak") (
private Handler mHandler = new Handler() {
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_NEW_BOOK_ARRIVED:
Log.d(TAG, "client know new book arrive : " + msg.obj);
break;
default:
break;
}
}
};
private ServiceConnection mServiceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName name, IBinder service) {
//绑定服务端的服务后,将服务端的Binder对象转换为客户端需要的AIDL接口类型对象
mBookManager = IBookManager.Stub.asInterface(service);
try {
//...
//客户端注册监听
mBookManager.registerListener(mListener);
} catch (RemoteException e) {
e.printStackTrace();
}
}
public void onServiceDisconnected(ComponentName name) {
}
};
private IOnNewBookArrivedListener mListener = new IOnNewBookArrivedListener.Stub() {
public void onNewBookArrived(Book newBook) throws RemoteException {
//因为该方法是在客户端的Binder线程池中回调的,所以利用Handler可以切换回主线程执行
mHandler.obtainMessage(MESSAGE_NEW_BOOK_ARRIVED, newBook).sendToTarget();
}
};
//...
protected void onDestroy() {
if (mBookManager != null && mBookManager.asBinder().isBinderAlive()) {
Log.d(TAG, "client unregister listener:" + mListener);
try {
//客户端解除监听
mBookManager.unregisterListener(mListener);
} catch (RemoteException e) {
e.printStackTrace();
}
}
//...
}
}
客户端做了这几件事:
- 创建IOnNewBookArrivedListener接口实例,重写其回调方法,由于该回调方法是在客户端的Binder线程池中回调的,所以要创建一个Handler,得到图书信息后,切换回主线程进行相关操作。
- 在绑定服务端成功后,客户端要注册监听,传入刚才创建的接口实例,等待服务端的回调。
- 在销毁活动的时候,通知服务端移除自己注册的监听。
最后,运行程序,打印结果如下:
可以看到,客户端成功收到了来自服务端的提醒。
如何解除注册
运行程序后,按back键退出活动后,打印结果如下:
可以发现,解除注册失败了。明明客户端注册和解注册时传的是同一个对象啊,为什么在服务端就找不到这个对象呢?
因为Binder会把客户端传递过来的对象重新转化成一个新的对象,因为跨进程传输的对象都要先序列化,所以服务端得到的对象是反序列化后的新的对象。
那么怎样才能解除注册呢?答案是使用RemoteCallbackList。
RemoteCallbackList的内部有一个Map结构用来保存所有的AIDL回调。其key为IBinder类型,value是Callback类型。虽然多次在跨进程中传输客户端的同一个对象会在服务端生成不同的对象,但这些对象的底层Binder是相同的。利用这点,RemoteCallbackList在解除注册的时候就可以通过同一个Binder对象找到当初注册时的listener,成功解除注册。
另外,RemoteCallbackList还有这两个特点:
- 客户端进程终止后,它能够自动移除客户端所注册的listener。
- 它内部已经实现了线程同步的功能,在使用它的时候无需另外同步。
接下来就在BookManagerService使用RemoteCallbackList,修改如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36// private CopyOnWriteArrayList<IOnNewBookArrivedListener> mListenerList =
// new CopyOnWriteArrayList<>();
private RemoteCallbackList<IOnNewBookArrivedListener> mRemoteCallbackList =
new RemoteCallbackList<>();
//服务端创建的Binder继承于IBookManager.Stub,并实现了它内部的AIDL方法
private Binder mBinder = new IBookManager.Stub() {
//...
@Override
public void registerListener(IOnNewBookArrivedListener listener) throws RemoteException {
mRemoteCallbackList.register(listener);
}
@Override
public void unregisterListener(IOnNewBookArrivedListener listener) throws RemoteException {
mRemoteCallbackList.unregister(listener);
}
};
private void newBookArrived(Book book) throws RemoteException {
mBookList.add(book);
int n = mRemoteCallbackList.beginBroadcast(); //返回注册了的回调数
//提醒各注册了监听的客户端
for (int i = 0; i < n; i++) {
IOnNewBookArrivedListener curr = mRemoteCallbackList.getBroadcastItem(i);
if (curr != null) {
curr.onNewBookArrived(book);
}
}
//记得关闭,beginBroadcast和finishBroadcast必须成对使用
mRemoteCallbackList.finishBroadcast();
}
原来客户端注册的listener是用 CopyOnWriteArrayList存储的,现在改为用RemoteCallbackList存储。要修改的地方有两处:
- 修改IBookManager的registerListener和unregisterListener方法
- 修改newBookArrived方法中通知各注册了的客户端的逻辑
再次运行,然后点击back,打印结果如下:
注意
当客户端调用服务端的方法时,被调用的方法是运行在服务端的Binder线程池中,所以要进行UI操作,需切换回主线程。同时客户端线程会被挂起,所以如果服务端的方法比较耗时,那么就不能在客户端的主线程调用服务端的方法,不然会导致客户端长时间无法响应,甚至ANR。同样,服务端回调客户端方法时也是如此。
权限问题
在默认情况下,任何进程都可以访问我们的远程服务。如果只希望特定的进程访问我们的服务,那么就要加入权限验证,权限验证失败的将无法调用服务端的方法。
在AIDL中进行权限验证,常用的是两种方法:
- 在onBinder中验证,如果验证不通过就返回null,对方就无法得到服务端的Binder实例了。
- 在服务端的onTransact方法中进行验证,如果验证失败就返回false,这样的话服务端就不会执行AIDL方法了。
验证的方式有多种,可以使用permission验证、也可以使用客户端的Uid和Pid来做验证。
使用ContentProvider
ContentProvider是Android提供的专门用于不同应用间进行数据共享的方式,它十分适合进程间通信。ContentProvider的底层实现是Binder。
要定义一个ContentProvider只需继承ContentProvider类并实现其六个抽象方法即可。这六个抽象方法除了onCreate方法运行在主线程,其它五个方法都是由外界回调并运行在Binder线程池中。
定义一个ContentProvider
首先,定义一个BookProvider,它继承于ContentProvider。外界可以通过BookProvider得到图书和用户信息。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43public class BookProvider extends ContentProvider {
public static final String TAG = "fzh";
public boolean onCreate() {
Log.d(TAG, "onCreate, current thread: " + Thread.currentThread().getName());
return false;
}
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
Log.d(TAG, "query, current thread: " + Thread.currentThread().getName());
return null;
}
public String getType(Uri uri) {
Log.d(TAG, "getType");
return null;
}
public Uri insert(Uri uri, ContentValues values) {
Log.d(TAG, "insert");
return null;
}
public int delete(Uri uri, String selection, String[] selectionArgs) {
Log.d(TAG, "delete");
return 0;
}
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
Log.d(TAG, "update");
return 0;
}
}
现在BookProvider提供的还是一个空方法,之后再补充。因为ContentProvider属于四大组件,所以必须给它注册。它的注册信息如下:1
2
3
4
5<provider
android:name=".BookProvider"
android:authorities="com.feng.contentprovidertest.BookProvider"
android:permission="com.feng.PROVIDER"
android:process=":provider"/>
其中,android:authorities是ContentProvider的唯一标识,外界要通过这个标识来访问BookProvider。android:permission是指明了要访问BookProvider所需要的权限。另外,我们让BookProvider运行在其他进程中。
创建数据库来管理用户和图书信息
因为BookProvider是为外界提供用户和图书信息的,所以先创建一个数据库来管理用户和图书信息。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26public class BookOpenHelper extends SQLiteOpenHelper {
public static final String DB_NAME = "book_provider.db";
public static final String BOOK_TABLE_NAME = "book";
public static final String USER_TABLE_NAME = "user";
private static final String CREATE_BOOK_TABLE = "create table " + BOOK_TABLE_NAME +
" (id integer primary key autoincrement, " + "name text)";
private static final String CREATE_USER_TABLE = "create table " + USER_TABLE_NAME +
" (id integer primary key autoincrement, " + "name text)";
public BookOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
super(context, name, factory, version);
}
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_BOOK_TABLE);
db.execSQL(CREATE_USER_TABLE);
}
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
创建了两张表,图书表和用户表。图书表存放图书名,用户表存放用户名。
客户端的实现
知道到外界要访问的表
ContentProvider是通过Uri来辨别外界需要访问的数据,本例中有两个表,为了知道外界要访问哪个表,我们需要为两个表分别定义Uri和对应的UriCode。具体如下:1
2
3
4
5
6
7
8
9
10
11public static final String AUTHORITY = "com.feng.contentprovidertest.BookProvider";
public static final Uri BOOK_CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/book");
public static final Uri USER_CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/user");
public static final int BOOK_URI_CODE = 0;
public static final int USER_URI_CODE = 1;
private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
static {
URI_MATCHER.addURI(AUTHORITY, "book", BOOK_URI_CODE);
URI_MATCHER.addURI(AUTHORITY, "user", USER_URI_CODE);
}
可以看到,在定义了各自的Uri后,利用UriMatcher的addURI方法将Uri和UriCode联系起来。
之后,当得到Uri后,就可以通过对应的UriCode得到要访问的表的名字:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15private String getTableName(Uri uri) {
String tableName = null;
switch (URI_MATCHER.match(uri)) {
case BOOK_URI_CODE:
tableName = BookOpenHelper.BOOK_TABLE_NAME;
break;
case USER_URI_CODE:
tableName = BookOpenHelper.USER_TABLE_NAME;
break;
default:
break;
}
return tableName;
}
一些初始化操作
1 | private Context mContext; |
在onCreate方法中初始化数据库,并返回true
CRUD操作的实现
做完准备工作后,就可以实现各个CRUD方法了。首先是query方法:1
2
3
4
5
6
7
8
9
10
11
12
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
Log.d(TAG, "query, current thread: " + Thread.currentThread().getName());
String table = getTableName(uri);
if (table == null) {
throw new IllegalArgumentException("Unsupported URI: " + uri);
}
return mDatabase.query(table, projection, selection,
selectionArgs, null, null, sortOrder);
}
实现很简单,就是调用数据库的query方法,查询相应表。
接下来是insert、delete和update方法,其实现和query方法大同小异。唯一的区别是这三个方法改变了数据源,所以要通知外界当前ContentProvider发生了改变。具体实现如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public Uri insert(Uri uri, ContentValues values) {
Log.d(TAG, "insert");
String table = getTableName(uri);
if (table == null) {
throw new IllegalArgumentException("Unsupported URI: " + uri);
}
mDatabase.insert(table, null, values);
mContext.getContentResolver().notifyChange(uri, null);
return uri;
}
public int delete(Uri uri, String selection, String[] selectionArgs) {
Log.d(TAG, "delete");
String table = getTableName(uri);
if (table == null) {
throw new IllegalArgumentException("Unsupported URI: " + uri);
}
int count = mDatabase.delete(table, selection, selectionArgs);
if (count > 0) {
mContext.getContentResolver().notifyChange(uri, null);
}
return count;
}
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
Log.d(TAG, "update");
String table = getTableName(uri);
if (table == null) {
throw new IllegalArgumentException("Unsupported URI: " + uri);
}
int count = mDatabase.update(table, values, selection, selectionArgs);
if (count > 0) {
mContext.getContentResolver().notifyChange(uri, null);
}
return count;
}
要注意的是,query、delete、insert和update方法可能存在并发访问,所以内部要做好同步。这里由于只使用了一个SQLiteDatabase对象进行操作,而且SQLiteDatabase内部对数据库的操作已经做了同步处理,所以不用担心同步问题。但如果有多个SQLiteDatabase对象进行操作就无法保证同步了。
外部访问ContentProvider
在MainActivity中访问BookProvider,实现如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49public class MainActivity extends AppCompatActivity {
private static final String TAG = "fzh";
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Uri bookUri = BookProvider.BOOK_CONTENT_URI;
//添加三本图书
ContentValues values = new ContentValues();
values.put("name", "Android 开发艺术探索");
getContentResolver().insert(bookUri, values);
values.clear();
values.put("name", "第一行代码");
getContentResolver().insert(bookUri, values);
values.clear();
values.put("name", "疯狂Java讲义");
getContentResolver().insert(bookUri, values);
values.clear();
//查询图书
Cursor cursor = getContentResolver().query(bookUri, null, null,
null, null,null);
if (cursor.moveToFirst()) {
do {
String bookName = cursor.getString(cursor.getColumnIndex("name"));
Log.d(TAG, "find a book: " + bookName);
} while (cursor.moveToNext());
}
cursor.close();
Uri userUri = BookProvider.USER_CONTENT_URI;
//添加一个用户
ContentValues userValues = new ContentValues();
userValues.put("name", "小明");
getContentResolver().insert(userUri, userValues);
//查询用户
Cursor userCursor = getContentResolver().query(userUri, null, null,
null, null,null);
if (userCursor.moveToFirst()) {
do {
String userName = userCursor.getString(userCursor.getColumnIndex("name"));
Log.d(TAG, "find a user: " + userName);
} while (userCursor.moveToNext());
}
userCursor.close();
}
}
这里主要演示了向两个表插入和查询数据。
运行程序,打印结果如下:
可以看到,借助ContentProvider,成功地进行了跨进程传输数据。
参考
- 《Android 开发艺术探索》