Android 多程式通訊

SheHuan發表於2018-03-08

一、Android 中的多程式

1、定義

首先,程式一般指一個執行單元,在移動裝置上就是一個程式或應用,我們在Android中所說的多程式(IPC)一般指一個應用包含多個程式。之所以要使用多程式有兩方面原因:某些模組由於特殊的需求要執行在單獨的程式;增加應用可用的記憶體空間。

2、開啟多程式

Android中開啟多執行緒只有一種方法,就是在AndroidManifest.xml中註冊Service、Activity、Receiver、ContentProvider時指定android:process屬性,例如:

<service
    android:name=".MyService"
    android:process=":remote">
</service>

<activity
    android:name=".MyActivity"
    android:process="com.shh.ipctest.remote2">
</activity>
複製程式碼

我們為MyServiceMyActivity指定的android:process屬性值有所不同,它們的區別如下:

  • :remote:以:開頭是一種簡寫,系統會在當前程式名前附件當前包名,完整的程式名為:com.shh.ipctest:remote,同時以:開頭的程式屬於當前應用的私有程式,其它應用的元件不能和它跑在同一程式。
  • com.shh.ipctest.remote2:這是完整的命名方式,不會附加包名,其它應用如果和該程式的ShareUID簽名相同,則可以和它跑在同一個程式,實現資料共享。
3、多程式引發的問題

開啟多程式雖簡單,但會引發如下問題,必須引起注意。

  • 靜態成員和單例模式失效
  • 執行緒同步機制失效
  • SharedPreferences 可靠性降低
  • Application 被多次建立

對於前兩個問題,可以這麼理解,在Android中,系統會為每個應用或程式分配獨立的虛擬機器,不同的虛擬機器自然佔有不同的記憶體地址空間,所以同一個類的物件會產生不同的副本,導致共享資料失敗,必然也不能實現執行緒的同步。

由於SharedPreferences底層採用讀寫XML的檔案的方式實現,多程式併發的的讀寫很可能導致資料異常。

Application被多次建立和前兩個問題類似,系統在分配多個虛擬機器時相當於把同一個應用重新啟動多次,必然會導致 Application 多次被建立,為了防止在 Application 中出現無用的重複初始化,可使用程式名來做過濾,只讓指定程式的才進行全域性初始:

public class MyApplication extends Application{
    @Override
    public void onCreate() {
        super.onCreate();
        String processName = "com.shh.ipctest";
        if (getPackageName().equals(processName)){
            // do some init
        }
    }
}
複製程式碼
4、Android中的多程式通訊方式

Android中支援的多程式通訊方式主要有以下幾種,它們之間各有優缺點,可根據使用場景選擇選擇:

  • AIDL:功能強大,支援程式間一對多的實時併發通訊,並可實現 RPC (遠端過程呼叫)。
  • Messenger:支援一對多的序列實時通訊, AIDL 的簡化版本。
  • Bundle:四大元件的程式通訊方式,只能傳輸 Bundle 支援的資料型別。
  • ContentProvider:強大的資料來源訪問支援,主要支援 CRUD 操作,一對多的程式間資料共享,例如我們的應用訪問系統的通訊錄資料。
  • BroadcastReceiver:即廣播,但只能單向通訊,接收者只能被動的接收訊息。
  • 檔案共享:在非高併發情況下共享簡單的資料。
  • Socket:通過網路傳輸資料。

這裡我們主要討論四大元件中Service在多程式通訊中的使用,這就涉及到了 AIDLMessenger這兩種多程式通訊方式,接下來重點看這兩種 IPC 方式。

二、AIDL

AIDL 的意思是 Android 介面定義語言,使用AIDL進行程式間通訊需要定義服務端客戶端,其中客戶端和服務端可以在同一應用也可以在不同應用。這裡我們服務端可以看做是圖書館,為客戶端提供近期新書查詢、圖書捐贈、新書通知的服務。

1、服務端實現

先建立一個 AIDL 檔案,宣告服務端要暴露給客戶端的介面,然後建立一個 Service 監聽客戶端的連線請求,並在其中實現 AIDL 檔案中的介面。

注意,為了方便開發,我們一般把 AIDL 相關的檔案放在同一包中,這樣當客戶端是另一個應用時可方便的把整個包複製到客戶端工程中。最終的AIDL檔案包如下:

aidl

首先了解下 AIDL 檔案支援的幾種資料型別:

  • 基本資料型別
  • String、CharSequence
  • ArrayList、HashMap,其內部元素也需要被AIDL支援
  • 實現了 Parcelable 介面的物件
  • AIDL 型別的介面,非普通介面

Book是實現了Parcelable的圖書類,只定義了圖書名name欄位,按照規定如果 AIDL 檔案用到了自定義Parcelable物件,同時需要提供一個Book.aidl檔案:

package com.shh.ipctest;

parcelable Book;
複製程式碼

ILibraryManager.aidl定義了服務端要暴露給客戶端的介面:

package com.shh.ipctest;

import com.shh.ipctest.Book;

interface ILibraryManager{
    // 近期新書查詢
    List<Book> getNewBookList();
    // 圖書捐贈
    void donateBook(in Book book);
}
複製程式碼

注意,儘管ILibraryManager.aidlBook在同一包中,還是需要顯示的匯入Book類。除了基本型別資料外,其它型別的引數需要標註方向,可選的方向標識有:

  • in:輸入型別引數
  • out:輸出型別引數
  • inout:輸入輸出型別引數

接下來就是LibraryManagerService這個服務類了,在編寫服務類前要先編譯專案,這樣在服務類裡使用 AIDL 生成的java類。

public class LibraryManagerService extends Service {

    private static final String TAG = "LibraryManagerService";

    // CopyOnWriteArrayList 支援併發讀寫
    private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArrayList<>();
   
    private Binder mBinder = new ILibraryManager.Stub() {

        @Override
        public List<Book> getNewBookList() throws RemoteException {
            return mBookList;
        }

        @Override
        public void donateBook(Book book) throws RemoteException {
            mBookList.add(book);
        }
    };

    public LibraryManagerService() {
    }

    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        mBookList.add(new Book("book0"));
        mBookList.add(new Book("book1"));
    }
}
複製程式碼

首先通過ILibraryManager.Stub()建立一個mBinder物件,並實現了ILibraryManager.aidl中定義的介面方法,在onBind()方法中返回建立的mBinder,並在服務onCreate()時新增兩本書。 最後在 AndroidManifest.xml 註冊服務:

<service
    android:name=".LibraryManagerService"
    android:process=":aidl_remote">
</service>
複製程式碼

到這裡服務端的基本功能就完成了。

2、客戶端實現

這裡先把客戶端和服務端放在同一個應用,客戶端的實現相對簡單些,首先在專案 appbuild.gradle指定 AIDL 檔案路徑:

android {
    ......

    // 指定 aidl 路徑
    sourceSets {
        main {
            java.srcDirs = ['src/main/java', 'src/main/aidl']
        }
    }
}
複製程式碼

之後就是繫結服務了:

public class AIDLActivity extends AppCompatActivity {
    private static final String TAG = "AIDLActivity";

    private ServiceConnection mServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            ILibraryManager libraryManager = ILibraryManager.Stub.asInterface(service);
            try {
                // 近期新書查詢
                List<Book> books = libraryManager.getNewBookList();
                Log.e(TAG, "books:" + books.toString());
                // 捐贈一本書
                libraryManager.donateBook(new Book("book" + books.size()));
                List<Book> books2 = libraryManager.getNewBookList();
                Log.e(TAG, "books:" + books2.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_aidl);
        bindNewService();
    }

    private void bindNewService() {
        Intent intent = new Intent(AIDLActivity.this, LibraryManagerService.class);
        bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
    }

    @Override
    protected void onDestroy() {
        unbindService(mServiceConnection);
        super.onDestroy();
    }
}
複製程式碼

先實現ServiceConnection介面,在onServiceConnected()方法中將IBinder物件轉換成ILibraryManager物件,通過該物件就能呼叫ILibraryManager.aidl中宣告的方法了。 接下來繫結服務:

Intent intent = new Intent(AIDLActivity.this, LibraryManagerService.class);
bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
複製程式碼

執行專案後觀察log:

log
出現了兩個程式,並在客戶端程式列印了對應的圖書名,到這裡我們一個簡單的 AIDL 多程式通訊就實現了。

如果客戶端和服務端在不同的應用怎麼實現呢?首先將服務端的 AIDL 包複製到客戶端專案的main目錄下,在客戶端專案 appbuild.gradle指定 AIDL 檔案路徑,最後就是繫結服務了,由於客戶端需要隱式繫結服務,所以要先修改服務端LibraryManagerService的註冊方式為:

<service
    android:name=".LibraryManagerService"
    android:process=":aidl_remote">
    <intent-filter>
        <action android:name="android.intent.action.LibraryManagerService" />
    </intent-filter>
</service>
複製程式碼

然後就是在客戶端隱式啟動服務:

Intent intent = new Intent();
intent.setAction("android.intent.action.LibraryManagerService");
intent.setPackage("com.shh.ipctest");
bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
複製程式碼
3、通知

如果要新增一個新書提醒功能,即圖書館每採購一本新書後需要通知訂閱了新書提醒功能的使用者,要怎麼修改服務端和客戶端呢?

首先建立一個服務端通知客戶端的 IOnNewBookArrivedListener.aidl 介面:

package com.shh.ipctest;

import com.shh.ipctest.Book;

interface IOnNewBookArrivedListener {
    void onNewBookArrived(in Book book);
}
複製程式碼

我們約定服務端要先註冊後才能收到通知,同時也可以取消註冊,所以要給之前的ILibraryManager.aidl新增連個方法了:

package com.shh.ipctest;

import com.shh.ipctest.Book;
import com.shh.ipctest.IOnNewBookArrivedListener;

interface ILibraryManager{
    ......
    // 註冊通知
    void register(IOnNewBookArrivedListener listener);
    // 取消註冊
    void unregister(IOnNewBookArrivedListener listener);
}
複製程式碼

接下來就是修改服務端的LibraryManagerService

// 只保留了相關核心程式碼
public class LibraryManagerService extends Service {
    ......
    // 系統提供的專門用於儲存、刪除跨程式 listener 的類
    private RemoteCallbackList<IOnNewBookArrivedListener> mListenerList = new RemoteCallbackList<>();
    // AtomicBoolean 支援併發讀寫
    private AtomicBoolean mIsServiceDestroy = new AtomicBoolean(false);

    private Binder mBinder = new ILibraryManager.Stub() {
        ......
        @Override
        public void register(IOnNewBookArrivedListener listener) throws RemoteException {
            mListenerList.register(listener);
            Log.e(TAG, "register success");
        }

        @Override
        public void unregister(IOnNewBookArrivedListener listener) throws RemoteException {
            mListenerList.unregister(listener);
            Log.e(TAG, "unregister success");
        }
    };

   .......

    @Override
    public void onCreate() {
        super.onCreate();
        ......
        // 在子執行緒中每隔3秒建立一本新書,並通知所有已註冊的客戶端
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 如果服務還沒終止
                while (!mIsServiceDestroy.get()) {
                    try {
                        Thread.sleep(3 * 1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    Book book = new Book("book" + mBookList.size());
                    mBookList.add(book);
                    bookArrivedNotify(book);
                }
            }
        }).start();
    }
    
    private void bookArrivedNotify(Book book) {
        int n = mListenerList.beginBroadcast();
        for (int i = 0; i < n; i++) {
            IOnNewBookArrivedListener listener = mListenerList.getBroadcastItem(i);
            if (listener != null) {
                try {
                    listener.onNewBookArrived(book);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        }
        mListenerList.finishBroadcast();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        mIsServiceDestroy.set(true);
    }
}
複製程式碼

注意這裡用到了RemoteCallbackList類,它是系統提供的專門用於刪除跨程式 listener 的類,用普通的集合難以保證客戶端註冊的 listener 和服務端儲存的 listener 是同一個,會取消註冊失敗。在的register()unregister()中實現了通知介面的繫結和解綁操作。在onCreate()週期性的通知客戶端有新書了。

在客戶端中需要完成通知介面的註冊和取消註冊:

// 只保留了相關核心程式碼
public class AIDLActivity extends AppCompatActivity {
    ......
    private ILibraryManager mLibraryManager;

    private Handler mHandler = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            switch (msg.what) {
                case MESSAGE_NEW_BOOK_ARRIVED:
                    Log.e(TAG, "new book:" + msg.obj);
                    break;
            }
            return true;
        }
    });

    private IOnNewBookArrivedListener listener = new IOnNewBookArrivedListener.Stub() {
        @Override
        public void onNewBookArrived(Book book) throws RemoteException {
            // 由於 onNewBookArrived 方法在子執行緒被呼叫,所以通過Handler切換到UI執行緒,方便UI操作
            mHandler.obtainMessage(MESSAGE_NEW_BOOK_ARRIVED, book).sendToTarget();
        }
    };

    private ServiceConnection mServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            ILibraryManager libraryManager = ILibraryManager.Stub.asInterface(service);
            mLibraryManager = libraryManager;
            try {
                ......
                // 註冊通知
                libraryManager.register(listener);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
        ......
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_aidl);
        bindNewService();
    }

    @Override
    protected void onDestroy() {
        unbindService(mServiceConnection);
        if (mLibraryManager != null && mLibraryManager.asBinder().isBinderAlive()) {
            try {
                // 取消註冊
                mLibraryManager.unregister(listener);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
        super.onDestroy();
    }
}
複製程式碼

首先是建立IOnNewBookArrivedListener介面的物件,為了方便 UI 操作可以在回撥中用Handler將服務端通知的結果切換到 UI 執行緒,並列印新書名,在onServiceConnected()中註冊介面,在onDestroy()中取消註冊。執行專案後看下客戶端、服務端的log:

cilent

sevice

4、許可權校驗

目前我們的服務端任何客戶端都可以連線,專案開發中為了安全起見,我們需要在服務端做許可權校驗,只有客戶端配置了指定的許可權,才能呼叫服務端的方法。

服務端可以使用permission+包名的方式來驗證,首先在服務端的AndroidManifest.xml宣告一個permission

<permission
        android:name="com.shh.ipctest.permission.ACCESS_LIBRARY_SERVICE"
        android:protectionLevel="normal" />
複製程式碼

接下來就是在LibraryManagerService中校驗要連線的客戶端是配置了該permission,以及包名是否符合指定規則,可以做校驗的地方有兩個,第一個是在onBind()中校驗:

@Override
public IBinder onBind(Intent intent) {
    if (!passBindCheck()) {
        Log.e(TAG, "bind denied");
        return null;
    }
    return mBinder;
}

private boolean permissionCheck() {
    // 客戶端是否已申請了指定許可權
    int check = checkCallingOrSelfPermission("com.shh.ipctest.permission.ACCESS_LIBRARY_SERVICE");
    if (check == PackageManager.PERMISSION_DENIED) {
        return false;
    }

    // 檢驗客戶端包名會否以com.shh開頭
    String[] packages = getPackageManager().getPackagesForUid(getCallingUid());
    if (packages != null && packages.length > 0 && !packages[0].startsWith("com.shh")) {
        return false;
    }
    return true;
}
複製程式碼

注意,onBind()是在客戶端連線服務端時呼叫,如果客戶端不能在此處通過校驗則無發連線到服務。如果客戶端和服務端是兩個應用,則無法在onBind()實現校驗的功能!

第二個地方是在ILibraryManager.Stub類的onTransact()方法中,在該方法裡校驗解決了客戶端和服務端是兩個應用時無法在onBind()實現校驗的問題,程式碼如下:

private Binder mBinder = new ILibraryManager.Stub() {
    ......
    @Override
    public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
        if (!permissionCheck()) {
            Log.e(TAG, "bind denied");
            return false;
        }
        return super.onTransact(code, data, reply, flags);
    }
};
複製程式碼

注意,該方法在onBind()之後執行,意味著客戶端和服務端已經連線,當客戶端呼叫服務端的方法時會走onTransact()方法。

最後需要在客戶端的AndroidManifest.xml配置permission許可權:

<uses-permission android:name="com.shh.ipctest.permission.ACCESS_LIBRARY_SERVICE" />
複製程式碼
5、重新連線

服務端程式可能會由於記憶體不足等原因意外終止而導致服務被殺死,所以有必要在這種情況下重新連線到服務。連線的方式有兩種:

第一種相對簡單,是在ServiceConnection介面的onServiceDisconnected()方法中重新連線服務,注意該方法在UI執行緒執行。

第二種是給客戶端得到的Binder物件註冊一個DeathRecipient監聽,首先來建立該監聽:

private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
    @Override
    public void binderDied() {
        if (mLibraryManager != null) {
            mLibraryManager.asBinder().unlinkToDeath(mDeathRecipient, 0);
            mLibraryManager = null;
            // 重新連線服務
            bindNewService();
        }
    }
}
複製程式碼

當服務被殺死時binderDied()方法會被呼叫,接下來就是在服務連線成功後設定死亡監聽:

@Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        ILibraryManager libraryManager = ILibraryManager.Stub.asInterface(service);
        mLibraryManager = libraryManager;
        try {
            // 將mLibraryManager轉成Binder物件然後註冊死亡監聽
            mLibraryManager.asBinder().linkToDeath(mDeathRecipient, 0);
        } catch (RemoteException e) {
            e.printStackTrace();
        }
        ......
    }
複製程式碼

三、Binder

在 AIDL 的例子中,客戶端之所以能和服務端通訊,主要依靠系統提供的Binder類實現,它實現了IBinder介面,我們在前邊的例子已經用到了Binder類,例如在服務端的 Service 中建立了一個Binder物件並在onBind()中返回:

Binder mBinder = new ILibraryManager.Stub(){
......
}
複製程式碼

當在客戶端繫結服務成功後,會得到服務端返回的Binder物件,並將其轉換成可呼叫服務端暴露的方法的ILibraryManager物件:

private ServiceConnection mServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            ILibraryManager libraryManager = ILibraryManager.Stub.asInterface(service);
            .......
        }
        ......
    };
複製程式碼

可以發現不管是客戶端還是服務端都用到了ILibraryManager.java類,回想一下之前我們建立過一個ILibraryManager.aidl檔案,所以ILibraryManager.java應該是ILibraryManager.aidl在編譯後生成的,果然在如下目錄有這個類:

ILibraryManager
ILibraryManager.java的程式碼如下:

public interface ILibraryManager extends android.os.IInterface {
    
    public static abstract class Stub extends android.os.Binder implements com.shh.ipctest.ILibraryManager {
        // Binder 的唯一標識
        private static final java.lang.String DESCRIPTOR = "com.shh.ipctest.ILibraryManager";

        public Stub() {
            this.attachInterface(this, DESCRIPTOR);
        }
        // 將服務端的 Binder 物件轉換成客戶端需要的介面物件
        // 如果客戶端和服務端在同一程式則返回 Stub 物件本身,否則返回Stub.Proxy這個代理物件
        public static com.shh.ipctest.ILibraryManager asInterface(android.os.IBinder obj) {
            if ((obj == null)) {
                return null;
            }
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if (((iin != null) && (iin instanceof com.shh.ipctest.ILibraryManager))) {
                return ((com.shh.ipctest.ILibraryManager) iin);
            }
            return new com.shh.ipctest.ILibraryManager.Stub.Proxy(obj);
        }
        // 返回當前 Binder 物件
        @Override
        public android.os.IBinder asBinder() {
            return this;
        }
        /**
         * 執行在服務端的 Binder 執行緒池,客戶端發起的跨程式方法呼叫最終會交給該方法處理
         * @param code 確定客戶端呼叫的是哪個方法
         * @param data  儲存要呼叫的方法需要的引數
         * @param reply 儲存要呼叫的方法最後的返回值
         * @param flags 
         * @return 返回false呼叫
         * @throws android.os.RemoteException
         */
        @Override
        public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
            switch (code) {
                case INTERFACE_TRANSACTION: {
                    reply.writeString(DESCRIPTOR);
                    return true;
                }
                case TRANSACTION_getNewBookList: {
                    data.enforceInterface(DESCRIPTOR);
                    java.util.List<com.shh.ipctest.Book> _result = this.getNewBookList();
                    // 封裝返回的資料
                    reply.writeNoException();
                    reply.writeTypedList(_result);
                    return true;
                }
                case TRANSACTION_donateBook: {
                    data.enforceInterface(DESCRIPTOR);
                    com.shh.ipctest.Book _arg0;
                    if ((0 != data.readInt())) {
                        // 反序列化出具體的請求引數
                        _arg0 = com.shh.ipctest.Book.CREATOR.createFromParcel(data);
                    } else {
                        _arg0 = null;
                    }
                    this.donateBook(_arg0);
                    reply.writeNoException();
                    return true;
                }
                case TRANSACTION_register: {
                    data.enforceInterface(DESCRIPTOR);
                    com.shh.ipctest.IOnNewBookArrivedListener _arg0;
                    _arg0 = com.shh.ipctest.IOnNewBookArrivedListener.Stub.asInterface(data.readStrongBinder());
                    this.register(_arg0);
                    reply.writeNoException();
                    return true;
                }
                case TRANSACTION_unregister: {
                    data.enforceInterface(DESCRIPTOR);
                    com.shh.ipctest.IOnNewBookArrivedListener _arg0;
                    _arg0 = com.shh.ipctest.IOnNewBookArrivedListener.Stub.asInterface(data.readStrongBinder());
                    this.unregister(_arg0);
                    reply.writeNoException();
                    return true;
                }
            }
            return super.onTransact(code, data, reply, flags);
        }
        // 客戶端跨程式呼叫服務端方法時,會先在客戶端執行該類裡的對應方法,
        // 呼叫mRemote.transact()發起遠端過程呼叫(RPC)請求,同時客戶端對應的執行緒會掛起,
        // 進而執行服務端的onTransact()方法,直到 RPC 請求結束,客戶端執行緒繼續執行,
        // 這可能是一個耗時的過程,所以要避免客戶端出現 ANR 的問題。
        private static class Proxy implements com.shh.ipctest.ILibraryManager {
            private android.os.IBinder mRemote;

            Proxy(android.os.IBinder remote) {
                mRemote = remote;
            }

            @Override
            public android.os.IBinder asBinder() {
                return mRemote;
            }

            public java.lang.String getInterfaceDescriptor() {
                return DESCRIPTOR;
            }

            @Override
            public java.util.List<com.shh.ipctest.Book> getNewBookList() throws android.os.RemoteException {
                // 儲存請求引數的 Parcel 物件
                android.os.Parcel _data = android.os.Parcel.obtain();
                // 儲存返回結果的 Parcel 物件
                android.os.Parcel _reply = android.os.Parcel.obtain();
                java.util.List<com.shh.ipctest.Book> _result;
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    // 發起 RPC 請求
                    mRemote.transact(Stub.TRANSACTION_getNewBookList, _data, _reply, 0);
                    _reply.readException();
                    // 從 RPC 請求的結果中取出最終要返回的資料
                    _result = _reply.createTypedArrayList(com.shh.ipctest.Book.CREATOR);
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }

            @Override
            public void donateBook(com.shh.ipctest.Book book) throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    if ((book != null)) {
                        _data.writeInt(1);
                        // 將引數序列化儲存
                        book.writeToParcel(_data, 0);
                    } else {
                        _data.writeInt(0);
                    }
                    mRemote.transact(Stub.TRANSACTION_donateBook, _data, _reply, 0);
                    _reply.readException();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
            }

            @Override
            public void register(com.shh.ipctest.IOnNewBookArrivedListener listener) throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    _data.writeStrongBinder((((listener != null)) ? (listener.asBinder()) : (null)));
                    mRemote.transact(Stub.TRANSACTION_register, _data, _reply, 0);
                    _reply.readException();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
            }

            @Override
            public void unregister(com.shh.ipctest.IOnNewBookArrivedListener listener) throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    _data.writeStrongBinder((((listener != null)) ? (listener.asBinder()) : (null)));
                    mRemote.transact(Stub.TRANSACTION_unregister, _data, _reply, 0);
                    _reply.readException();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
            }
        }
        // 客戶端可呼叫的方法的對應 code
        static final int TRANSACTION_getNewBookList = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
        static final int TRANSACTION_donateBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
        static final int TRANSACTION_register = (android.os.IBinder.FIRST_CALL_TRANSACTION + 2);
        static final int TRANSACTION_unregister = (android.os.IBinder.FIRST_CALL_TRANSACTION + 3);
    }

    public java.util.List<com.shh.ipctest.Book> getNewBookList() throws android.os.RemoteException;

    public void donateBook(com.shh.ipctest.Book book) throws android.os.RemoteException;

    public void register(com.shh.ipctest.IOnNewBookArrivedListener listener) throws android.os.RemoteException;

    public void unregister(com.shh.ipctest.IOnNewBookArrivedListener listener) throws android.os.RemoteException;
}
複製程式碼

ILibraryManager類繼承android.os.IInterface介面,其實 aidl型別檔案中定義的介面在編譯後都會生成一個繼承IInterface的java類。這個類看起來有點複雜,但結構還是很清晰的,主要有兩部分,首先是繼承了BinderStub類,還有ILibraryManager.aidl中介面宣告的方法。Stub類中關鍵部分都做了註釋,其實它的核心就是將客戶端程式中發起的跨程式方法呼叫轉換到服務端程式去執行,最終得到執行結果!所以直觀的看,在 Service 中Binder是作為跨程式通訊的橋樑存在的,在此基礎上,我們才能更好的理解使用 AIDL 背後的原理!

四、Messenger

Messenger 是一種輕量級的多程式通訊方式,它是在 AIDL 的基礎上封裝而成的,可以看做是 AIDL 的簡化版,支援一對多的序列實時通訊,一次只處理一個請求,不存在併發的問題。和 AIDL 的使用類似,但要簡單的多,同樣需要實現服務端和客戶端。

首先來看服務端,功能就是接收客戶端傳送的訊息,同時回覆一條訊息:

public class MessengerService extends Service {
    private static final String TAG = "MessengerService";
    // 將Messenger和Handler關聯起來
    private Messenger mServiceMessenger = new Messenger(new MessengerHandler());

    public MessengerService() {
    }

    @Override
    public IBinder onBind(Intent intent) {
        return mServiceMessenger.getBinder();
    }

    private static class MessengerHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case MessengerActivity.MESSAGE_FROM_CLIENT:
                    // 列印接收到的客戶端訊息
                    Log.e(TAG, "receive message from client:" + msg.getData().getString("msg"));
                    // 給客戶端回覆一條訊息
                    Messenger clientMessenger = msg.replyTo;
                    Message message = Message.obtain();
                    message.what = MessengerActivity.MESSAGE_FROM_SERVICE;
                    Bundle bundle = new Bundle();
                    bundle.putString("msg", "I am fine,thank you!");
                    message.setData(bundle);
                    try {
                        clientMessenger.send(message);
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                    break;
            }
        }
    }
}
複製程式碼

首先建立一個Handler物件,並在其handleMessage()中進行訊息的接收和回覆,注意回覆訊息是通過客戶端傳遞過來的Messenger物件傳送一個Message物件,有了Handler物件後,需要把它和服務端建立的Messenger關聯起來:

Messenger mServiceMessenger = new Messenger(new MessengerHandler());
複製程式碼

並在onBind()中返回服務端Messenger包含的Binder物件。

下來看客戶端的實現,和服務端連線成功後,給服務端傳送一條訊息,並接收服務端回覆的訊息:

public class MessengerActivity extends AppCompatActivity {
    private static final String TAG = "MessengerActivity";

    public static final int MESSAGE_FROM_CLIENT = 1;
    public static final int MESSAGE_FROM_SERVICE = 2;

    private Messenger mServiceMessenger;

    private Messenger mClientMessenger = new Messenger(new MessengerHandler());

    private ServiceConnection mServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mServiceMessenger = new Messenger(service);
            Message message = Message.obtain();
            message.what = MESSAGE_FROM_CLIENT;
            Bundle bundle = new Bundle();
            bundle.putString("msg", "how are you?");
            message.setData(bundle);
            // 傳遞服務端回覆客戶端時需要使用的Messenger
            message.replyTo = mClientMessenger;
            try {
                mServiceMessenger.send(message);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_messenger);

        Intent intent = new Intent(MessengerActivity.this, MessengerService.class);
        bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
    }

    @Override
    protected void onDestroy() {
        unbindService(mServiceConnection);
        super.onDestroy();
    }

    private static class MessengerHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case MessengerActivity.MESSAGE_FROM_SERVICE:
                    Log.e(TAG, "receive message from service:" + msg.getData().getString("msg"));
                    break;
            }
        }
    }
}
複製程式碼

onServiceConnected()中,將服務端的Binder轉換成服務端的Messenger物件,然後傳送訊息,由於服務端還需要給客服端回覆訊息,所以需要在客戶端建立一個Messenger物件附加在訊息上傳送給服務端使用。

Messenger中也可以進行許可權校驗、服務端終止重新連線的操作,實現了 AIDL 的類似。

最後看下效果:

服務端

客戶端

五、小結

文中內容參考了《Android 開發藝術探索》,希望對你我有所幫助吧! 原始碼地址:github.com/Othershe/IP…

相關文章