Android AIDL實現程序間通訊
今天犯二了, 犯了個超級低階的錯誤, 真的是丟人丟大發了.
剛好順道反思下, 也對工作這幾年做一些簡單的總結. 不知道你們是不是和我一樣也總遇到各種奇葩問題, 明明程式碼是對的可是就是不好使; 我明明聲明瞭許可權了但是就是獲取不到; 這個bug以前沒發生過啊, 我最近程式碼也沒修改沒道理出現問題啊, 肯定是其他地方的bug(尤其是多個人聯合開發某個功能, 需要對接的時候)等等啦, 我以前遇到這個問題, 我的第一反應應該不是程式碼問題啊, 是不是哪裡弄錯了, 我"清楚的"記得程式碼寫的沒錯, 是不是其他地方出問題導致的?. "事出反常必有妖啊", 而且往往證明最後的妖都出在我身上
第一, 先暗地裡提醒自己是不是自己的問題, 不要上來就以為自己的程式碼沒問題, 信誓旦旦找別人對問題, 結果發現最終還是自己的問題, 這臉打的太疼, 我有幾次就是這樣的
第二, 分析下出現問題的可能性, 找出對應的地方的程式碼
第三, 啥也別說, 仔細把出問題的那塊的程式碼仔細看一遍, 我以前在網上看到過一個大神說的讀書方法, 就一個字一個字的看, 我覺得說的對我很合適, 我發現我有段時間很浮躁, 讀書看部落格都是跳著看, 或者一目好幾行, 這樣是很快, 但是容易漏掉內容, 並且往往也領會不到深意, 我自認沒有一目多行的能力, 以後還是老老實實一個字一個字看
第四, 一邊過完, 再一個字一個字過一遍, 我覺得多看一遍不是浪費時間, 在仔細看過後 在去找相關的同事對問題, 這樣能減少被打臉的機率麼不是(/ □ \), 至少也要對自己寫的程式碼負責麼不是
檢查問題一定要仔細, 耐心, 切忌一目多行, 草草了事
好了言歸正傳 Android AIDL使用, 我就是把我在使用AIDL時候遇到的問題和遇到的坑給總結一下, 供以後參考
AIDL(Android Interface Definition Language)即Android 介面定義語言, 主要是用於程序間通訊, 方便程序間資料傳遞, 方法呼叫, 關於概念性的定義這裡就不細講了, 直接開始講解使用方法, 關於注意事項, 我邊講邊提吧
既然是程序間通訊, 肯定要涉及service端和client端, 這裡先說service端怎樣對外提供服務
2.客戶端 客戶端要做的事情就相對簡單些, 首先繫結服務端的Service, 繫結成功後, 將服務端返回的Binder物件轉成AIDL介面所屬的型別, 接著就可以呼叫AIDL中的方法了
Service端
1.在Android Studio裡建立AIDL資料夾
先說明一下service工程的包名: com.wb.aidl_service
工程跳到project模式下, 在工程的main上右鍵->New->Folder->AIDL Folder這樣就建立了一個AIDL的專屬目錄如下圖:
然後建立我們aidl資料夾下面的包: com.aidl.demo, 就是右鍵new->package
2.建立我們需要宣告的aidl檔案及響應的bean類的aidl檔案
這裡我的例子是"Android開發藝術探索"書裡的基礎上改的, 先說下Service裡要用到的幾個類:
Book.java Book物件的bean類, Book.aidl Book物件的aidl檔案, IBookManager.aidl 對外提供方法的aidl檔案, BookManagerService.java 服務端對外提供的service
第一個注意事項在程序間通訊的時候關於引數的傳遞問題: java的基本資料型別都支援, 對於自定義物件型別(例如上面說的Book物件)必須能夠可序列化
眾所周知Android裡實現序列化的方法有兩種implements Parcelable介面或者 implements Serializable介面這兩個我們選擇哪個呢? 當然是選擇Parcelable介面了, 原因如下:
這裡我直接引用別人的總結(李嘉欣-第四維空間 http://blog.csdn.net/ljx19900116/article/details/41699593 點選開啟連結) : Serializable的作用是為了儲存物件的屬性到本地檔案、資料庫、網路流、rmi以方便資料傳輸,當然這種傳輸可以是程式內的也可以是兩個程式間的。而Android的Parcelable的設計初衷是因為Serializable效率過慢,為了在程式內不同元件間以及不同Android程式間(AIDL)高效的傳輸資料而設計,這些資料僅在記憶體中存在,Parcelable是通過IBinder通訊的訊息的載體。
2.1建立實現了Parcelable介面的Book物件
這個建立物件不需要在aidl裡, 就是在工程的src目錄下即可public class Book implements Parcelable {
private String name;
private int price;
public Book() {
}
public Book(int price, String name) {
this.price = price;
this.name = name;
}
protected Book(Parcel in) {
name = in.readString();
price = in.readInt();
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name);
dest.writeInt(price);
}
@Override
public int describeContents() {
return 0;
}
public static final Creator<Book> CREATOR = new Creator<Book>() {
@Override
public Book createFromParcel(Parcel in) {
return new Book(in);
}
@Override
public Book[] newArray(int size) {
return new Book[size];
}
};
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
}
這段程式碼沒什麼可說的, 就是定義兩個屬性, 然後實現set, get方法, 關於需要實現Parcelable的方法系統也會自動生成, 只需要注意一件事情就是包名: com.aidl.demo
2.2建立Book.java對應的Book.aidl檔案
為什麼需要這個檔案呢? 所有實現了Parcelable介面的並且要在AIDL介面中使用到的類都需要新建一個對應類名的.aidl檔案., 檔案內容就寫 parcelable XXX在aidl資料夾下的包下右鍵->new->AIDL->AIDLfile
package com.aidl.demo;
// Declare any non-default types here with import statements
parcelable Book;
程式碼是不是超簡潔, 總共就四行 , 還有兩行是系統註釋, 不過有個要注意的事項: 注意到包名了嗎, 和上面建立的Book.java的包名是一樣的
2.3建立IBookManager.aidl檔案
還是在aidl資料夾下的包下右鍵->new->AIDL->AIDL file// IBookManager.aidl
package com.aidl.demo;
// Declare any non-default types here with import statements
// 一定是全路徑的類引用, 即使實在同一個包下
import com.aidl.demo.Book;
interface IBookManager {
List<Book> getBookList();
void addBook(in Book book);
}
這裡程式碼也不多, 但是有個非常重要的地方需要註釋, 也是困擾我了很久的一個知識點, 聲明瞭一個interface, 裡面兩個方法, 一個是獲取book列表, 一個是新增一本書
這裡注意第二個方法addBook(in Book book), 這裡返回值是void沒啥說的, 注意引數列表裡的in(這個是表明跨程序間資料的流向共有in, out, inout三種), 這裡重點說一下這個, 在對外提供的方法引數有以下幾點要注意: 1.基本型別不需要指明方向例如String, int等
2. in 表現為服務端將會接收到一個那個物件的完整資料,但是客戶端的那個物件不會因為服務端對傳參的修改而發生變動;out 的話表現為服務端將會接收到那個物件的的空物件,但是在服務端對接收到的空物件有任何修改之後客戶端將會同步變動;inout 為服務端將會接收到客戶端傳來物件的完整資訊,並且客戶端將會同步服務端對該物件的任何變動。(這個參考http://blog.csdn.net/luoyanglizi/article/details/51980630點選開啟連結)的, 這裡只看文字描述也許有些抽象, 下文會有例項演示就能比較形象的理解各個tag表示的意思了
3.在使用過程中要根據具體情況來選擇對應的tag, 雖說inout肯定沒錯, 但是不要不管三七二十一直接就用inout, 這個在底層是要花費開銷的, 在使用out和inout的時候還有個坑等著我們, 我在第一次使用的時候, 還是費了點勁的, 下文介紹具體是什麼坑
3.建立伺服器端的Service類
package com.wb.aidl_service.service;
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import com.aidl.demo.Book;
import com.aidl.demo.IBookManager;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Created by wb on 2018/3/19.
*/
public class BookManagerService extends Service {
private static final String TAG = "BookManagerService";
private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArrayList<Book>();
private Binder mBinder = new IBookManager.Stub() {
@Override
public List<Book> getBookList() throws RemoteException {
return mBookList;
}
@Override
public void addBook(Book book) throws RemoteException {
Log.d(TAG, "===receive===" + book.getName() + "===price===" + book.getPrice());
if(book.getName() == null) {
book.setName("為什麼給我傳遞的是空資料");
book.setPrice(100000);
} else {
book.setPrice(200);
}
mBookList.add(book);
}
};
@Override
public void onCreate() {
super.onCreate();
mBookList.add(new Book(100, "Android"));
mBookList.add(new Book(150, "IOS"));
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
}
簡單介紹下: 首先維護了一個book列表, 然後建立了一個Binder物件, 我們可以ctrl+左鍵點選Stub進去看下具體內容, 會跳到一個IBookManager.java的檔案裡, 這個檔案是系統幫我們生成的, 它聲明瞭一個內部類Stub, 這個Stub就是一個Binder類, 當客戶端和服務端都位於同一個程序中的時候, 方法呼叫不會走跨程序的transact過程, 而當二者位於不同程序時, 方法呼叫需要走transact過程, 這個邏輯是由Stub的內部代理類Proxy來完成的. 而這個Stub物件需要實現兩個方法分別是getBookList和addBook, 而這兩個方法就是我們上文宣告的IBookManager.aidl裡宣告的, 具體的程式碼沒什麼難度, 不多做解釋, 至於為什麼判斷book.getName()是否為null, 下文會做說明. onCreate裡就是向list裡新增兩本書做基礎資料, 在onBinder裡會把mBinder物件返回供client端使用, 這樣伺服器端的程式碼就完成了
記得要在Manifest.xml檔案中把service宣告出來, 並且service的exported屬性設定為true
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.wb.aidl_service">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name=".service.BookManagerService"
android:exported="true"/>
</application>
</manifest>
Client端
1.把服務端提供的aidl檔案及目錄原封不動的copy到客戶端, 注意目錄要求一模一樣
我客戶端的包名是: com.wb.aidl_client, Book.java的目錄和兩個aidl檔案的目錄一定要注意要和伺服器端的一樣
2.通過bindservice繫結服務端的service, 呼叫服務端對外提供的方法
繫結服務的程式碼比較簡單, 就是注意服務提供方的包名和service的全路徑名不要寫錯即可:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Intent intent = new Intent();
intent.setComponent(new ComponentName("com.wb.aidl_service",
"com.wb.aidl_service.service.BookManagerService"));
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
}
接下來就是呼叫服務端提供的方法了, 這裡主要是在ServiceConnection物件的onServiceConnected方法裡進行處理了, 先看程式碼:
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
IBookManager bookManager = IBookManager.Stub.asInterface(service);
try {
List<Book> list = bookManager.getBookList();
Log.i(TAG, "===query book list, list type:" + list.getClass().getCanonicalName());
Log.i(TAG, "==query book list:" + list);
// 新增一本圖書
Book book = new Book(110, "Android開發藝術探索");
bookManager.addBook(book);
List<Book> newList = bookManager.getBookList();
Log.i(TAG, "==query book list newList:" + newList);
for (Book temp : newList) {
Log.d(TAG, "==temp : name===" + temp.getName());
Log.d(TAG, "==temp : price===" + temp.getPrice());
}
Log.d(TAG, "===book===name===" + book.getName() + "===price===" + book.getPrice());
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
這裡需要注意的其實就是一行程式碼即
IBookManager bookManager = IBookManager.Stub.asInterface(service);
這裡的asInterface的作用就是將服務端的Binder物件轉換成客戶端所需的AIDL介面型別的物件, 這種轉換過程是區分程序的, 如果客戶端和服務端位於統一程序, 那麼此方法返回的就是服務端的Stub物件本身, 否則返回的是系統封裝後的Stub.proxy物件. 在我們拿到這個物件後, 就可以通過該物件呼叫伺服器端的方法了.
下面是執行效果
Client的log如下:
服務端的log如下:
先看client的log, 主要就看紅色框內的即可, 首先把通過getBookList()方法獲取到的book列表打印出來, 發現多了一本書, 也就是客戶端通過addBook方法新新增的那本只不過在service端把價格改為200了, 再看第二個紅框, 我列印的是客戶端自己new的那個物件可以看到名稱和價格和原來的一模一樣.
有意思的事情要來了, 前面說過在定義aidl介面方法的時候, 方法是有引數的, 而引數是有tag(in, out, inout)的, 重點來了, 前面已經簡單的說過in/out/inout有什麼區別了, 這裡將通過實際操作來進行驗證, 也讓大家有個更清晰的認識
關於in/out/inout的用法及可能遇到的坑
前面我們使用的就是in, 接下來我們換成out, 修改如下幾個地方:
1.service端的程式碼
interface IBookManager {
List<Book> getBookList();
void addBook(out Book book);
}
只是把原來的in改為了out
2.client端的程式碼, 和service端一樣
interface IBookManager {
List<Book> getBookList();
void addBook(out Book book);
}
然後編譯執行
果然還是太年輕, 沒有想象的那麼容易, 這裡遇到了又一個坑 編譯後報瞭如下錯誤:
不過不要著急, 仔細看log發現異常是: readFromParcel(Parcel) 這個方法找不到, 而且是和Book類相關的, 靜下心去Book.java裡看一下, 是不是能發現一個和名字差不多的方法writeToParcel, 再看看writeToParcel是怎麼實現的,
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name);
dest.writeInt(price);
}
再想想, 剛剛我們就是把in改成了out, 就這一點區別, 就報錯了, 肯定和out有關, 那麼in可能就是對應write, 而out根據提示可能就是和read唄, 那我們寫個readFromParcel方法不就行了, 具體寫法就是從parcel裡讀出對應資料
public void readFromParcel(Parcel in){
name = in.readString();
price = in.readInt();
}
然後重新編譯, 哦耶, 編譯通過, 執行看下效果:
client端log:
03-20 20:07:54.680 12520-12520/? I/MainActivity: ===query book list, list type:java.util.ArrayList
03-20 20:07:54.680 12520-12520/? I/MainActivity: ==query book list:[[email protected], [email protected]]
03-20 20:07:54.682 12520-12520/? I/MainActivity: ==query book list newList:[[email protected], [email protected], [email protected]]
03-20 20:07:54.683 12520-12520/? D/MainActivity: ==temp : name===Android
03-20 20:07:54.683 12520-12520/? D/MainActivity: ==temp : price===100
03-20 20:07:54.683 12520-12520/? D/MainActivity: ==temp : name===IOS
03-20 20:07:54.683 12520-12520/? D/MainActivity: ==temp : price===150
03-20 20:07:54.683 12520-12520/? D/MainActivity: ==temp : name===為什麼給我傳遞的是空資料
03-20 20:07:54.683 12520-12520/? D/MainActivity: ==temp : price===100000
03-20 20:07:54.683 12520-12520/? D/MainActivity: ===book===name===為什麼給我傳遞的是空資料===price===100000
Service端log:
03-20 20:07:54.681 12201-12334/? D/BookManagerService: ===receive===null===price===0
看一下log發現, service端收到的book物件居然是一個內容為空的book物件, 也就是雖然有book物件, 但是name是null, 而price是預設值0, 緊接著, service端對新的book進行了賦值
再看client端的日誌, 發現得到的booklist裡的最後一本書不是自己新增的了, 是伺服器端判斷是空物件後修改的了, 更有意思的是客戶端本身new的那個book物件的值, 在伺服器修改後, 客戶端的book物件也被修改了
這個就是out的作用了:out 的話表現為服務端將會接收到那個物件的的空物件,但是在服務端對接收到的空物件有任何修改之後客戶端將會同步變動
interface IBookManager {
List<Book> getBookList();
void addBook(inout Book book);
}
然後編譯執行看log:
client的:
03-20 20:25:44.128 13270-13270/? I/MainActivity: ===query book list, list type:java.util.ArrayList
03-20 20:25:44.128 13270-13270/? I/MainActivity: ==query book list:[[email protected], [email protected]]
03-20 20:25:44.131 13270-13270/? I/MainActivity: ==query book list newList:[[email protected], [email protected], [email protected]]
03-20 20:25:44.131 13270-13270/? D/MainActivity: ==temp : name===Android
03-20 20:25:44.131 13270-13270/? D/MainActivity: ==temp : price===100
03-20 20:25:44.131 13270-13270/? D/MainActivity: ==temp : name===IOS
03-20 20:25:44.132 13270-13270/? D/MainActivity: ==temp : price===150
03-20 20:25:44.132 13270-13270/? D/MainActivity: ==temp : name===Android開發藝術探索
03-20 20:25:44.132 13270-13270/? D/MainActivity: ==temp : price===200
03-20 20:25:44.132 13270-13270/? D/MainActivity: ===book===name===Android開發藝術探索===price===200
service的:
03-20 20:25:44.129 12957-13077/? D/BookManagerService: ===receive===Android開發藝術探索===price===110
發現設定為inout後伺服器能收到book物件, 並且伺服器對book修改後, 客戶端原有的book物件也被修改了
inout 為服務端將會接收到客戶端傳來物件的完整資訊,並且客戶端將會同步服務端對該物件的任何變動。
到此為止我相信大家對in/out/inout能有一個比較清晰的認知了, 也知道什麼時候需要使用in, 什麼時候需要使用out, 和inout了, 接下來我們就預設使用in了, 不再使用out和inout進行測試了.
接下來我們考慮另外一個問題:
假設有一種需求:使用者不想時不時的去查詢圖書列表, 於是他問"能否有新書的時候通知我呢?", 這個就是個典型的觀察者模式, 首先, service要提供一個AIDL的介面, 每個client都需要實現這個介面並向圖書館註冊這個介面, 當然使用者也可以隨時取消註冊, 之所以使用AIDL介面而不是普通介面是因為AIDL中無法使用普通介面
在AIDL中如何使用介面以及介面的註冊和取消註冊
我們先在servic端的程式碼裡建立一個IOnNewBookArrivedListener.aidl檔案, 我們期望的是當有新書到來時, 呼叫IOnNewBookArrivedListener上的onNewArrived方法, 並把新書通過引數傳遞給客戶端, 程式碼如下:
// IOnNewBookArrivedListener.aidl
package com.aidl.demo;
// Declare any non-default types here with import statements
import com.aidl.demo.Book;
interface IOnNewBookArrivedListener {
void onNewBookArrived(in Book book);
}
注意這裡一定要import Book物件
位置如下:
除了新加這個aidl檔案以外還要在 原本的IBookManager.aidl檔案中增加兩個方法用於註冊和取消註冊, 程式碼如下:
// IBookManager.aidl
package com.aidl.demo;
// Declare any non-default types here with import statements
// 一定是全路徑的類引用, 即使實在同一個包下
import com.aidl.demo.Book;
import com.aidl.demo.IOnNewBookArrivedListener;
interface IBookManager {
List<Book> getBookList();
void addBook(in Book book);
void registerListener(IOnNewBookArrivedListener listener);
void unregisterListener(IOnNewBookArrivedListener listener);
}
注意這裡一定要import IOnNewBookArrivedListener.
接著修改服務端Service中的程式碼, 主要是Service中IBookManager.Stub中新添加了兩個方法要進行實現, 同時, 在BookManagerService中還開啟了一個執行緒, 每隔5s向圖書列表中加入一本新書, 並通知使用者, 程式碼如下
在Stub裡新增如下方法, 程式碼比較簡單不需要太多解釋:
@Override
public void registerListener(IOnNewBookArrivedListener listener) throws RemoteException {
if(!mListenerList.contains(listener)) {
mListenerList.add(listener);
} else {
Log.i(TAG, "==already exists.");
}
Log.d(TAG, "==registerListener, size:==" + mListenerList.size());
}
@Override
public void unregisterListener(IOnNewBookArrivedListener listener) throws RemoteException {
if(mListenerList.contains(listener)) {
mListenerList.remove(listener);
Log.i(TAG, "==unregister success.");
} else {
Log.i(TAG, "==not found, can not unregister");
}
Log.d(TAG, "==unregisterListener, current size:==" + mListenerList.size());
}
@Override
public void onCreate() {
super.onCreate();
mBookList.add(new Book(100, "Android"));
mBookList.add(new Book(150, "IOS"));
new Thread(new ServiceWorker()).start();
}
@Override
public void onDestroy() {
mIsServiceDestoryed.set(true);
super.onDestroy();
}
private class ServiceWorker implements Runnable {
@Override
public void run() {
while(!mIsServiceDestoryed.get()) {
try{
Thread.sleep(5000);
} catch (Exception e) {
}
int bookId = mBookList.size() + 1;
Book newBook = new Book(bookId, "new Book" + bookId);
try {
onNewBookArrived(newBook);
} catch (Exception e) {
}
}
}
}
private void onNewBookArrived(Book book) throws RemoteException {
mBookList.add(book);
Log.i(TAG, "==onNewBookArrived, nofity listener:" + mListenerList.size());
for (int i = 0; i < mListenerList.size(); i++) {
IOnNewBookArrivedListener listener = mListenerList.get(i);
Log.i(TAG, "==onNewBookArrived, notify listener:" + listener);
listener.onNewBookArrived(book);
}
}
然後再定義一個work執行緒用於5秒鐘新增一本書到booklist中, 新增完之後, 去呼叫通知客戶端的程式碼onNewBookArrived
方法, 迴圈遍歷ListenerList, 挨個通知客戶端, 伺服器端的程式碼到此就修改完畢.
再來看客戶端的程式碼:
主要是兩方面內容, 首先client要註冊IOnNewBookArrivedListener到service端, 這樣當有新書過來的時候才能通知到client, 同時我們要在Activity退出的時候取消這個註冊, 另一方面, 當有新書過來的時候service會回撥客戶端的IOnNewBookArrivedListener中的onNewBookArrived方法, 但是這個方法是在客戶端的Binder執行緒池中的, 因此為了便於進行UI操作, 我們需要有一個Handler可以將其切換到UI執行緒中
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_NEW_BOOK_ARRIVED:
Log.d(TAG, "===receive new book:" + msg.obj);
break;
default:
super.handleMessage(msg);
}
}
};
private IOnNewBookArrivedListener mOnnewBookArrivedListener = new IOnNewBookArrivedListener.Stub() {
@Override
public void onNewBookArrived(Book newBook) throws RemoteException {
mHandler.obtainMessage(MESSAGE_NEW_BOOK_ARRIVED, newBook).sendToTarget();
}
};
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
bookManager = IBookManager.Stub.asInterface(service);
try {
List<Book> list = bookManager.getBookList();
Log.i(TAG, "===query book list, list type:" + list.getClass().getCanonicalName());
Log.i(TAG, "==query book list:" + list);
// 新增一本圖書
Book book = new Book(110, "Android開發藝術探索");
bookManager.addBook(book);
List<Book> newList = bookManager.getBookList();
Log.i(TAG, "==query book list newList:" + newList);
for (Book temp : newList) {
Log.d(TAG, "==temp : name===" + temp.getName());
Log.d(TAG, "==temp : price===" + temp.getPrice());
}
Log.d(TAG, "===book===name===" + book.getName() + "===price===" + book.getPrice());
// 註冊
bookManager.registerListener(mOnnewBookArrivedListener);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Intent intent = new Intent();
intent.setComponent(new ComponentName("com.wb.aidl_service",
"com.wb.aidl_service.service.BookManagerService"));
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
}
@Override
protected void onDestroy() {
if(bookManager != null && bookManager.asBinder().isBinderAlive()) {
try {
Log.i(TAG, "==unregister listener:==" + mOnnewBookArrivedListener);
bookManager.unregisterListener(mOnnewBookArrivedListener);
} catch (RemoteException e) {
}
}
unbindService(mConnection);
super.onDestroy();
}
主要程式碼如上, 就是建立一個handler, 用於接收資料處理UI, 定義一個IOnNewBookArrivedListener物件, 在onNewBookArrived方法中通過handler傳送訊息到主執行緒進行處理, 然後就是在onServiceConnected的最後一行添加了註冊監聽的程式碼, 在ondestroy會呼叫添加了取消監聽的邏輯
編譯執行log如下:
service端log
03-20 21:10:20.562 15329-15361/? D/BookManagerService: ===receive===Android開發藝術探索===price===110
03-20 21:10:20.565 15329-15361/? D/BookManagerService: ==registerListener, size:==1
03-20 21:10:25.522 15329-15425/? I/BookManagerService: ==onNewBookArrived, nofity listener:1
03-20 21:10:25.522 15329-15425/? I/BookManagerService: ==onNewBookArrived, notify listener:[email protected]
03-20 21:10:30.527 15329-15425/? I/BookManagerService: ==onNewBookArrived, nofity listener:1
03-20 21:10:30.528 15329-15425/? I/BookManagerService: ==onNewBookArrived, notify listener:[email protected]
03-20 21:10:35.533 15329-15425/? I/BookManagerService: ==onNewBookArrived, nofity listener:1
03-20 21:10:35.533 15329-15425/? I/BookManagerService: ==onNewBookArrived, notify listener:[email protected]
03-20 21:10:38.962 15329-15361/? I/BookManagerService: ==not found, can not unregister
03-20 21:10:38.962 15329-15361/? D/BookManagerService: ==unregisterListener, current size:==1
03-20 21:10:40.536 15329-15425/? I/BookManagerService: ==onNewBookArrived, nofity listener:1
03-20 21:10:40.537 15329-15425/? I/BookManagerService: ==onNewBookArrived, notify listener:[email protected]
client端log:
03-20 21:10:20.560 15407-15407/? I/MainActivity: ===query book list, list type:java.util.ArrayList
03-20 21:10:20.561 15407-15407/? I/MainActivity: ==query book list:[[email protected], [email protected]]
03-20 21:10:20.563 15407-15407/? I/MainActivity: ==query book list newList:[[email protected], [email protected], [email protected]]
03-20 21:10:20.563 15407-15407/? D/MainActivity: ==temp : name===Android
03-20 21:10:20.564 15407-15407/? D/MainActivity: ==temp : price===100
03-20 21:10:20.564 15407-15407/? D/MainActivity: ==temp : name===IOS
03-20 21:10:20.564 15407-15407/? D/MainActivity: ==temp : price===150
03-20 21:10:20.564 15407-15407/? D/MainActivity: ==temp : name===Android開發藝術探索
03-20 21:10:20.564 15407-15407/? D/MainActivity: ==temp : price===200
03-20 21:10:20.564 15407-15407/? D/MainActivity: ===book===name===Android開發藝術探索===price===110
03-20 21:10:20.672 1617-1999/? I/KPI-6PA-WMS-6: 6095447 WindowState.performShowLocked() sends empty message FINISHED_STARTING: Token{ae4bc5b ActivityRecord{9607f6a u0 com.wb.aidl_client/.MainActivity t98}}
03-20 21:10:20.676 1617-1857/? I/LaunchCheckinHandler: Displayed com.wb.aidl_client/.MainActivity,cp,ca,420
03-20 21:10:20.676 1617-1857/? I/KPI-6PA-AMS-8: 6095451 windowsDrawn com.wb.aidl_client.MainActivity
03-20 21:10:20.681 1617-1857/? I/ActivityManager: Displayed com.wb.aidl_client/.MainActivity: +416ms
03-20 21:10:25.525 15407-15407/? D/MainActivity: ===receive new book:[email protected]
03-20 21:10:30.531 15407-15407/? D/MainActivity: ===receive new book:[email protected]
03-20 21:10:35.535 15407-15407/? D/MainActivity: ===receive new book:[email protected]
03-20 21:10:38.668 584-584/? D/SFPerfTracer: layers: (5:9) (NavigationBar#0 (0xb1513000): 8:1481) (StatusBar#0 (0xb1845000): 0:2421) (RoundedOverlay#0 (0xb183c000): 0:48) (RoundedOverlay#1 (0xb15fb000): 0:50) (com.android.systemui.ImageWallpaper#0 (0xb19e6000): 0:123)* (AssistPreviewPanel#0 (0xb16d8000): 0:241)* (com.zui.launcher/com.zui.launcher.Launcher#0 (0xb1817000): 0:31)- (Splash Screen com.wb.aidl_client#0 (0xb156d000): 0:31)- (com.wb.aidl_client/com.wb.aidl_client.MainActivity#0 (0xb180f000): 1:11)
03-20 21:10:38.961 15407-15407/? I/MainActivity: ==unregister listener:[email protected]
03-20 21:10:40.540 15407-15407/? D/MainActivity: ===receive new book:[email protected]
看著log發現兩邊執行貌似都很正常, 但是細心的讀者能發現伺服器端有一條log有問題:
03-20 21:10:38.962 15329-15361/? I/BookManagerService: ==not found, can not unregister
也就是當我點選退出客戶端的時候, 客戶端呼叫了unregister但是伺服器端並沒有成功unregister並且, 看客戶端最後一條log, 雖然上一句有unregisterlog, 但是後面還是收到了新的通知, 這裡就遇到又一個坑了
怎麼才能正確的取消註冊監聽
對於上面的問題起始也很好理解, 雖然我們在註冊和解註冊的時候使用的是同一個客戶端物件, 但是通過Binder傳遞到服務端後卻會產生兩個全新的物件, , 別忘了物件是不能擴程序直接傳輸的, 物件的跨程序傳輸是反序列化的過程.
如何解決上述問題呢? 雖然說多次跨程序傳輸客戶端的同一個物件會在服務端生成不同的物件, 但是這些新生成的物件
有一個共同點, 那就是他們底層的Binder物件是同一個, 我們可以利用這個特點來進行解綁
只需要修改service端的unregister方法即可
@Override
public void unregisterListener(IOnNewBookArrivedListener listener) throws RemoteException {
/*if(mListenerList.contains(listener)) {
mListenerList.remove(listener);
Log.i(TAG, "==unregister success.");
} else {
Log.i(TAG, "==not found, can not unregister");
}*/
Log.d(TAG, "==unregisterListener, current size:==" + mListenerList.size());
for (IOnNewBookArrivedListener l : mListenerList) {
if(l.asBinder() == listener.asBinder()) {
mListenerList.remove(l);
Log.i(TAG, "==unregister success.");
}
}
Log.d(TAG, "==unregisterListener, current size:==" + mListenerList.size());
}
再次編譯執行就能發現log正常了
service端log:
03-20 21:32:29.250 16805-16819/? D/BookManagerService: ===receive===Android開發藝術探索===price===110
03-20 21:32:29.254 16805-16819/? D/BookManagerService: ==registerListener, size:==1
03-20 21:32:34.210 16805-16879/? I/BookManagerService: ==onNewBookArrived, nofity listener:1
03-20 21:32:34.211 16805-16879/? I/BookManagerService: ==onNewBookArrived, notify listener:[email protected]
03-20 21:32:39.216 16805-16879/? I/BookManagerService: ==onNewBookArrived, nofity listener:1
03-20 21:32:39.217 16805-16879/? I/BookManagerService: ==onNewBookArrived, notify listener:[email protected]
03-20 21:32:41.485 16805-16819/? D/BookManagerService: ==unregisterListener, current size:==1
03-20 21:32:41.485 16805-16819/? I/BookManagerService: ==unregister success.
03-20 21:32:41.486 16805-16819/? D/BookManagerService: ==unregisterListener, current size:==0
03-20 21:32:44.220 16805-16879/? I/BookManagerService: ==onNewBookArrived, nofity listener:0
client端log:
03-20 21:32:29.249 16862-16862/? I/MainActivity: ===query book list, list type:java.util.ArrayList
03-20 21:32:29.249 16862-16862/? I/MainActivity: ==query book list:[[email protected], [email protected]]
03-20 21:32:29.252 16862-16862/? I/MainActivity: ==query book list newList:[[email protected], [email protected], [email protected]]
03-20 21:32:29.252 16862-16862/? D/MainActivity: ==temp : name===Android
03-20 21:32:29.252 16862-16862/? D/MainActivity: ==temp : price===100
03-20 21:32:29.252 16862-16862/? D/MainActivity: ==temp : name===IOS
03-20 21:32:29.252 16862-16862/? D/MainActivity: ==temp : price===150
03-20 21:32:29.252 16862-16862/? D/MainActivity: ==temp : name===Android開發藝術探索
03-20 21:32:29.252 16862-16862/? D/MainActivity: ==temp : price===200
03-20 21:32:29.252 16862-16862/? D/MainActivity: ===book===name===Android開發藝術探索===price===110
03-20 21:32:29.361 1617-1999/? I/KPI-6PA-WMS-6: 6633934 WindowState.performShowLocked() sends empty message FINISHED_STARTING: Token{2160a98 ActivityRecord{2bb5c7b u0 com.wb.aidl_client/.MainActivity t105}}
03-20 21:32:29.364 1617-1857/? I/LaunchCheckinHandler: Displayed com.wb.aidl_client/.MainActivity,cp,ca,420
03-20 21:32:29.364 1617-1857/? I/KPI-6PA-AMS-8: 6633938 windowsDrawn com.wb.aidl_client.MainActivity
03-20 21:32:29.370 1617-1857/? I/ActivityManager: Displayed com.wb.aidl_client/.MainActivity: +416ms
03-20 21:32:29.381 584-584/? D/SFPerfTracer: layers: (7:13) (NavigationBar#0 (0xb1513000): 0:1699) (StatusBar#0 (0xb1845000): 0:2718) (RoundedOverlay#0 (0xb183c000): 0:54) (RoundedOverlay#1 (0xb15fb000): 0:56) (com.android.systemui.ImageWallpaper#0 (0xb19e6000): 0:134) (AssistPreviewPanel#0 (0xb16d8000): 0:241)* (Sprite#0 (0xb1817000): 0:1)* (com.zui.launcher/com.zui.launcher.Launcher#0 (0xb151d000): 0:33)- (Splash Screen com.wb.aidl_service#0 (0xb181a000): 0:34)- (com.wb.aidl_service/com.wb.aidl_service.MainActivity#0 (0xb152b000): 0:29)- (com.zui.launcher/com.zui.launcher.Launcher#0 (0xb181a000): 0:9) (Splash Screen com.wb.aidl_client#0 (0xb152b000): 5:21) (com.wb.aidl_client/com.wb.aidl_client.MainActivity#0 (0xb180d000): 5:5)*
03-20 21:32:34.214 16862-16862/? D/MainActivity: ===receive new book:[email protected]
03-20 21:32:39.219 16862-16862/? D/MainActivity: ===receive new book:[email protected]
03-20 21:32:41.482 16862-16862/? I/MainActivity: ==unregister listener:[email protected]
03-20 21:32:47.667 584-584/? D/SFPerfTracer: layers: (6:11) (NavigationBar#0 (0xb1513000): 0:1712) (StatusBar#0 (0xb1845000): 0:2723) (RoundedOverlay#0 (0xb183c000): 0:54) (RoundedOverlay#1 (0xb15fb000): 0:56) (com.android.systemui.ImageWallpaper#0 (0xb19e6000): 0:136) (AssistPreviewPanel#0 (0xb16d8000): 0:241)* (Sprite#0 (0xb1817000): 0:1)* (com.zui.launcher/com.zui.launcher.Launcher#0 (0xb181a000): 0:10)- (Splash Screen com.wb.aidl_client#0 (0xb152b000): 0:32)- (com.wb.aidl_client/com.wb.aidl_client.MainActivity#0 (0xb180d000): 0:29)- (com.zui.launcher/com.zui.launcher.Launcher#0 (0xb152b000): 1:50)
伺服器端unregister成功之前是size是1, 之後size是0, 客戶端在解綁後, 也不會收到onNewBookArrived的通知了, 到此為止對AIDL的使用基本功能完成了, 剩下的就是靈活應用了