Android進程間通信(四):進程間通信的方式之AIDL

轉載請以鏈接形式標明出處:
本文出自:103style的博客

《Android開發藝術探索》 學習記錄

base on AndroidStudio 3.5.1


目錄

  • 前言
  • AIDL接口創建
  • AIDL支持的數據格式
  • 服務端實現
    • 創建 BookManagerService.java
    • 處理併發情況
  • 客戶端實現
    • 創建 BookManagerActivity.java
    • 運行程序查看日誌
  • AIDL添加和解除回調
    • 添加服務端新增數據的回調
    • 解除回調失敗?RemoteCallbackList ?
  • AIDL添加權限驗證
  • 小結

前言

前面我們介紹了 進程間通信基礎介紹通過AIDL介紹Binder的工作機制 ,以及 通過 Bundle、文件共享、Messenger實現進程間通信 , 不瞭解的可以先看下。

通過之前對 Messenger 的介紹,我們知道 Messenger 是以串行的方式處理消息的,所以當有 大量消息併發請求 時,Messenger 可能就不太合適了。
同時 Messenger 主要是用來傳遞消息,很多時候我們可能需要 跨進程調用其他進程的方法 ,這個是 Messenger 做不到的。

這時候就輪到 AIDL 展示自己的實力了。
Messenger 也是基於 AIDL 的,是系統對 AIDL 的封裝,方便上層調用。

我們在 通過AIDL介紹Binder的工作機制 中介紹了 Binder 的概念,大家對 Binder 應該有了一定的瞭解。

這裏我們先介紹下AIDL 來進行進程間通信的流程,包括 AIDL接口創建服務端客戶端


AIDL接口創建

tips: 爲了方便開發,建議把 AIDL 相關的類和文件放到統一的目錄,這樣當客戶端和服務端是不同應用時,可以把整個包複製過去。
注意: 客戶端和服務端的 AIDL 包結構必須保持一致,否則會運行報錯。

創建 IBookManager.aidl

//IBookManager.aidl:
package aidl;
import aidl.Book;
interface IBookManager {
    List<Book> getBookList();
    void addBook(in Book book);
}

然後我們先來介紹下AIDL支持的數據格式。


AIDL支持的數據格式

AIDL 支持的大部分數據格式,不過也不是所有的數據類型都能使用的,可以用如下類型:

  • 基本數據類型(int、long、char、boolean、double 等)
  • StringCharSequence
  • List : 只能是 ArrayList,而且其中的元素的格式都要能被 AIDL 支持。
  • Map : 只能是 HashMap,而且其中的元素的格式都要能被 AIDL 支持。
  • AIDL:所有 AIDL 接口也可以在 AIDL 中使用。需要import導入
  • Parcelable:所有實現該接口的對象。需要import導入,該對象還需創建 類名.aidl 文件,然後添加如下內容,以上述示例中的 Book 爲例:
    //Book.aidl
    package aidl;
    parcelable Book;
    

除了基本類型之外,其他的類型在作爲參數的時候必須標上方向:inoutinout

in:表示輸入型參數
out:表示輸出型參數
inout:表示輸入輸出型參數

而且不能一概使用 inout,因爲底層性能是有開銷的,所以要按需使用。
例如上述示例中的 void addBook(in Book book);


服務端實現

首先我們在服務端創建一個 Service 來處理客戶端的連接請求,然後在 Service 中實現在 AIDL 中的聲明暴露給客戶端的接口。

創建 BookManagerService.java

//BookManagerService.java
public class BookManagerService extends Service {
    private static final String TAG = "BookManagerService";
    private CopyOnWriteArrayList<Book> bookList = new CopyOnWriteArrayList<>();
    private Binder mBinder = new IBookManager.Stub() {
        @Override
        public List<Book> getBookList() throws RemoteException {
            return bookList;
        }
        @Override
        public void addBook(Book book) throws RemoteException {
            bookList.add(book);
        }
    };
    @Override
    public void onCreate() {
        super.onCreate();
        bookList.add(new Book(1, "Android藝術開發探索"));
        bookList.add(new Book(2, "Java併發編程指南"));
    }
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }
}

上述示例中主要是創建 實現 AIDL 中聲明的方法的 BInder 類,並在 Service 的 onBind 中返回。

然後前面提到是在服務端的 Binder 線程池中執行的,所以會存在多個線程同時訪問的情況。所以我們要在 AIDL 方法中處理線程同步,因爲 CopyOnWriteArrayList 是支持併發讀寫的,這裏我們直接用 CopyOnWriteArrayList 來進行線程自動同步。

但是在上面介紹 AIDL支持的數據格式 時,我們知道 List 只支持 ArrayList,而 CopyOnWriteArrayList 也不是 ArrayList 的子類,那爲什麼能供支持工作呢?
這是因爲 AIDL 中所支持的是抽象的 List,而 List 是一個接口,因此雖然服務端返回的是 CopyOnWriteArrayList,但是在 Binder 中會按照 List 的規範去訪問數據並最終形成一個新的 ArrayList 傳給客戶端。

然後在 AndroidManifest.xml 中聲明所在的進程 :remote

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.ipc">
    <application>
        ...
        <service
            android:name="test.BookManagerService"
            android:process=":remote" />
    </application>
</manifest>

客戶端實現

客戶端首先要綁定服務端的 Service, 綁定成功後用服務端返回的 Binder 對象轉成 AIDL 接口所屬的類型,然後就可以調用 AIDL 的方法了。

創建 BookManagerActivity.java

//BookManagerActivity.java
public class BookManagerActivity extends AppCompatActivity {
    private static final String TAG = "BookManagerActivity";
    private ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            IBookManager iBookManager = IBookManager.Stub.asInterface(service);
            try {
                List<Book> list = iBookManager.getBookList();
                Log.e(TAG, "query book list, type = " + list.getClass().getCanonicalName());
                Log.e(TAG, list.toString());
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
        @Override
        public void onServiceDisconnected(ComponentName name) {
        }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_book_manager);
        Intent intent = new Intent(this, BookManagerService.class);
        bindService(intent, connection, Context.BIND_AUTO_CREATE);
    }
    @Override
    protected void onDestroy() {
        unbindService(connection);
        super.onDestroy();
    }
}

運行程序,看到日誌信息如下:

BookManagerActivity: query book list, type = java.util.ArrayList
BookManagerActivity: [Book{bookId=1, bookName='Android藝術開發探索'}, Book{bookId=2, bookName='Java併發編程指南'}]

AIDL添加和解除回調

我們在上面的代碼中實現以下功能,當服務端有新的書添加時,通知客戶端。

來,直接開擼。
因爲 AIDL 中無法使用普通接口,所以我們得創建一個 AIDL接口 IBookAddListener.aidl

//IBookAddListener.aidl
package aidl;
import aidl.Book;
interface IBookAddListener{
    void onBookArrived(in Book newBook);
}

然後在之前的 IBookManager.aidl 中添加接口的添加和刪除方法。

//IBookManager.aidl
package aidl;
import aidl.Book;
import aidl.IBookAddListener;
interface IBookManager {
    List<Book> getBookList();
    void addBook(in Book book);
    void registerListener(IBookAddListener listener);
    void unregisterListener(IBookAddListener listener);
}

然後在修改上面的服務端代碼 BookManagerService 中的 mBinder 實現 新增的兩個方法,並且創建一個 Worker 定時往服務端的 bookList 中添加數據。

//BookManagerService.java
public class BookManagerService extends Service {
    private static final String TAG = "BookManagerService";
    //服務是否已經銷燬
    private AtomicBoolean destroyed = new AtomicBoolean(false);
    private CopyOnWriteArrayList<Book> bookList = new CopyOnWriteArrayList<>();
    private CopyOnWriteArrayList<IBookAddListener> listeners = new CopyOnWriteArrayList<>();
    private Binder mBinder = new IBookManager.Stub() {
        ...
        @Override
        public void registerListener(IBookAddListener listener) throws RemoteException {
            if (!listeners.contains(listener)) {
                listeners.add(listener);
            } else {
                Log.e(TAG, "lister is already exist");
            }
            Log.e(TAG, "registerListener:  listeners.size = "  + listeners.size());
        }
        @Override
        public void unregisterListener(IBookAddListener listener) throws RemoteException {
            if (listeners.contains(listener)) {
                listeners.remove(listener);
            } else {
                Log.e(TAG, "lister not found, can not unregister");
            }
            Log.e(TAG, "unregisterListener:  listeners.size = "  + listeners.size());
        }
    };
    @Override
    public void onCreate() {
        super.onCreate();
        bookList.add(new Book(1, "Android藝術開發探索"));
        bookList.add(new Book(2, "Java併發編程指南"));
        new Thread(new Worker()).start();
    }
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }
    @Override
    public void onDestroy() {
        destroyed.set(true);
        super.onDestroy();
    }
    private void onBookAdd(Book book) throws RemoteException {
        bookList.add(book);
        Log.e(TAG, "onBookAdd: notify listeners.size = " + listeners.size());
        for (IBookAddListener listener : listeners) {
            listener.onBookArrived(book);
        }
    }
    private class Worker implements Runnable {
        @Override
        public void run() {
            while (!destroyed.get()) {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                int bookId = bookList.size() + 1;
                Book book = new Book(bookId, "new book#" + bookId);
                try {
                    onBookAdd(book);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

然後修改客戶端的 BookManagerActivity 添加服務端的監聽。

//BookManagerActivity.java
public class BookManagerActivity extends AppCompatActivity {
    private static final String TAG = "BookManagerActivity";
    private static final int BOOK_ADD_MSG = 0x001;
    private IBookManager remoteBookManager;
    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case BOOK_ADD_MSG:
                    Log.e(TAG, "a new book add :" + msg.obj);
                    break;
                default:
                    super.handleMessage(msg);
            }
        }
    };
    //監聽服務端的回調
    private IBookAddListener bookAddListener = new IBookAddListener.Stub() {
        @Override
        public void onBookArrived(Book newBook) throws RemoteException {
            //運行在客戶端的Binder線程池,不能執行訪問UI
            handler.obtainMessage(BOOK_ADD_MSG, newBook).sendToTarget();
        }
    };
    private ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            IBookManager iBookManager = IBookManager.Stub.asInterface(service);
            try {
                remoteBookManager = iBookManager;
                List<Book> list = iBookManager.getBookList();
                Log.e(TAG, "query book list, type = " + list.getClass().getCanonicalName());
                Log.e(TAG, list.toString());
                Book book = new Book(3, "Android軟件安全指南");
                remoteBookManager.addBook(book);
                remoteBookManager.registerListener(bookAddListener);//添加回調監聽
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
        @Override
        public void onServiceDisconnected(ComponentName name) {
            remoteBookManager = null;
            Log.e(TAG, "binder died ");
        }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_book_manager);
        Intent intent = new Intent(this, BookManagerService.class);
        bindService(intent, connection, Context.BIND_AUTO_CREATE);//綁定服務
    }
    @Override
    protected void onDestroy() {
        unregisterListener();//解除註冊
        unbindService(connection);//解綁服務
        super.onDestroy();
    }
    private void unregisterListener() {
        if (remoteBookManager != null && remoteBookManager.asBinder().isBinderAlive()) {
            try {
                remoteBookManager.unregisterListener(bookAddListener);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
    }
}

然後運行程序,打印如下日誌。

//客戶端進程
BookManagerActivity: query book list, type = java.util.ArrayList
BookManagerActivity: [Book{bookId=1, bookName='Android藝術開發探索'}, Book{bookId=2, bookName='Java併發編程指南'}]
BookManagerActivity: a new book add :Book{bookId=4, bookName='new book#4'}
BookManagerActivity: a new book add :Book{bookId=5, bookName='new book#5'}
BookManagerActivity: a new book add :Book{bookId=6, bookName='new book#6'}
//服務端 :remote進程
BookManagerService: registerListener:  listeners.size = 1
BookManagerService: onBookAdd: notify listeners.size = 1
BookManagerService: onBookAdd: notify listeners.size = 1
BookManagerService: onBookAdd: notify listeners.size = 1
→ BookManagerService: lister not found, can not unregister
BookManagerService: unregisterListener:  listeners.size = 1
BookManagerService: onBookAdd: notify listeners.size = 1

我們從日誌中可以看到,確實有監聽到每隔5s就新增一條數據。
但是我們發現一個問題: 解除註冊的時候提示 lister not found, can not unregister。說明解除註冊失敗了,這是爲什麼呢?

這是因爲 Binder 的機制的問題,Binder會把客戶端傳遞過來的對象重新轉化並生成一個新的對象
因爲對象是不能直接跨進程傳輸的,對象傳輸的本質都是反序列化的過程,這就是爲什麼 AIDL 中的對象都得實現 Parcelabe 接口的原因。

那我們怎麼才能解註冊呢? 就得使用系統提供的 RemoteCallbackList,專門提供用於刪除跨進程的 回調接口,從它的泛型我們可以看到,它是支持管理任意的 AIDL 接口。
public class RemoteCallbackList<E extends IInterface> {}

接下來我們來修改我們之前的 BookManagerService:

//BookManagerService.java  只貼了要修改的地方
public class BookManagerService extends Service {
    ...
    private RemoteCallbackList<IBookAddListener> listeners = new RemoteCallbackList<>();
    private Binder mBinder = new IBookManager.Stub() {
        ...
        @Override
        public void registerListener(IBookAddListener listener) throws RemoteException {
            listeners.register(listener);
            final int N = listeners.beginBroadcast();
            Log.e(TAG, "registerListener: size = " + N);
            listeners.finishBroadcast();
        }
        @Override
        public void unregisterListener(IBookAddListener listener) throws RemoteException {
            listeners.unregister(listener);
            final int N = listeners.beginBroadcast();
            Log.e(TAG, "unregisterListener: size = " + N);
            listeners.finishBroadcast();
        }
    };
    private void onBookAdd(Book book) throws RemoteException {
        bookList.add(book);
        final int N = listeners.beginBroadcast();
        for (int i = 0; i < N; i++) {
            IBookAddListener listener = listeners.getBroadcastItem(i);
            if (listener != null) {
                listener.onBookArrived(book);
            }
        }
        listeners.finishBroadcast();
    }
}

運行程序,從日誌我們可以看到解註冊成功了。

//客戶端進程
BookManagerActivity: query book list, type = java.util.ArrayList
BookManagerActivity: [Book{bookId=1, bookName='Android藝術開發探索'}, Book{bookId=2, bookName='Java併發編程指南'}]
BookManagerActivity: a new book add :Book{bookId=4, bookName='new book#4'}
//服務端:remote進程
E/BookManagerService: registerListener: size = 1
E/BookManagerService: unregisterListener: size = 0

使用 RemoteCallbackList,有一點需要注意,雖然名字中有 List,但是我們不能像 List 一樣去操作它。
遍歷其數據 或者 獲取其大小,我們必須配對使用 beginBroadcastfinishBroadcast,參考上面代碼中回調的註冊和解註冊的方法。

至此,AIDL 的基本使用方法已經介紹完了,但是還有幾點需要再強調以下:

  • 客戶端調用遠程服務的方法是運行在服務端的 Binder 線程池中的,客戶端會被掛起直到方法執行完成,如果方法比較耗時的話,客戶端如果在 UI線程 中直接調用則會出現 ANR。所以在知道方法耗時時,我們不能直接在UI線程中調用,需要通過子線程去處理,如示例中客戶端 BookManagerActivity 中的 ServiceConnection 的兩個方法 onServiceConnectedonServiceDisconnected 都是運行在UI線程的。
  • 另外就是客服端中的回調,即示例 BookManagerActivity 中的 bookAddListener,是運行在客戶端的 Binder 線程池的,所以不能直接訪問UI內容的,如需訪問UI,則需要通過 Handler 等切換線程。

另外,爲了程序的健壯性,我們還的防止 Binder 意外死亡,這往往是由於服務端進程意外停止了,這是我們需要重連服務。有兩種方法:

  • 給Binder設置 DeathRecipient 監聽,當 Binder死亡時,我們會收到 binderDied 回調,這個我們已經在 Binder的工作機制 這裏介紹過了 。
  • 在 onServiceDisconnected 中去重連遠程服務。

AIDL添加權限驗證

默認情況下,我們的遠程服務任何人都可以連接,但這是我們不想要的,所以我們要在AIDL中添加權限驗證。這裏介紹兩種方法:
1.在 obBinder 中驗證
驗證不通過時直接返回 null,這樣驗證失敗的客戶端直接無法綁定服務。至於驗證方式有多種,比如 permission驗證,使用這種驗證,我們需要在 AndroidManifest.xml 中聲明所需要的權限,示例如下:

// AndroidManifest.xml 
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.okhttptesst">
    //聲明權限
    <permission android:name="com.aidl.test.permission.ACCESS_BOOK_SERVICE"
        android:protectionLevel="normal"/>
    ...
</manifest>

//BookManagerService.java
public class BookManagerService extends Service {
    ...
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        int check = checkCallingOrSelfPermission("com.aidl.test.permission.ACCESS_BOOK_SERVICE");
        if (check == PackageManager.PERMISSION_DENIED) {
            return null;
        }
        return mBinder;
    }
    ...
}

然後再我們要綁定服務的應用內聲明權限即可。

// AndroidManifest.xml 
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.okhttptesst">
    //註冊權限
    <uses-permission android:name="com.aidl.test.permission.ACCESS_BOOK_SERVICE"/>
    ...
</manifest>

2.在服務端的 onTransact 中驗證
在 onTransact 中驗證失敗即返回 false,這樣服務端就終止執行AIDL中的方法從而達到保護服務端的效果。具體驗證方法也有很多。
可以採用第一種驗證中的 permission 驗證,具體實現也一樣。
也可以 Uid 和 Pid 來做驗證,通過 getCallingUidgetCallingPid 可以獲取客戶端所屬應用的 Uid 和 Pid,通過這兩個參數我們做 包名驗證 等。
示例如下,我們重寫 BookManagerService 中 mBinder 的 onTransact 方法,添加權限和包名驗證:

//BookManagerService.java
public class BookManagerService extends Service {
    ...
    private Binder mBinder = new IBookManager.Stub() {
        @Override
        public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
            int check = checkCallingOrSelfPermission("com.aidl.test.permission.ACCESS_BOOK_SERVICE");
            if (check == PackageManager.PERMISSION_DENIED) {
                Log.e(TAG, "PERMISSION_DENIED");
                return false;
            }
            String packageName = null;
            String[] packages = getPackageManager().getPackagesForUid(getCallingUid());
            if (packages != null && packages.length > 0) {
                packageName = packages[0];
            }
            if (!packageName.startsWith("com.aidl")) {
                Log.e(TAG, "packageName is illeagl = " + packageName);
                return false;
            }
            return super.onTransact(code, data, reply, flags);
        }
        ...
    };
    ...
}

啓動程序,查看日誌:

BookManagerService: PERMISSION_DENIED

申明權限之後,再運行:

BookManagerService: packageName is illeagl = com.example.aidltest
BookManagerService: packageName is illeagl = com.example.aidltest
BookManagerService: packageName is illeagl = com.example.aidltest

除了上面兩個驗證方法之外,我們還可以通過 給 Service 指定 android:permission 屬性等。


小結

我們再來回顧下本文的內容:

  • 介紹了 AIDL 的基本使用方法,以及AIDL支持的數據格式。
  • 通過 RemoteCallbackList 給 AIDL 添加和刪除回調,遍歷數據或者獲取大小 必須配對使用 beginBroadcastfinishBroadcast
  • 以及介紹了 通過 permission驗證包名驗證 給AIDL做權限驗證。

下一節我們介紹通過 ContentProvider 來進行IPC.

如果覺得本文不錯的話,請幫忙點個讚唄。

以上


掃描下面的二維碼,關注我的公衆號 Android1024, 點關注,不迷路。
Android1024

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章