Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android開發 二:IPC機制(中)

Android開發 二:IPC機制(中)

編輯:關於Android編程

好的,我們繼續來了解IPC機制,在上篇我們可能就是把理論的知識寫完了,然後現在基本上是可以實戰了。

一.Android中的IPC方式

本節我們開始詳細的分析各中跨進程的方式,具體方式有很多,比如可以通過在Intent中附加extras來傳遞消息,或者通過共享文件的方式來共享數據,還可以采用Binder方式來跨進程通信,另外,ContentProvider天生就是支持擴進程訪問的,所以通過Socket也可以實現IPC,上述的各種方法都能實現IPC,他們的使用方式和側重點上有很大的區別,我們在這裡都會一一說的;

1.使用Bundler

Bundler,用Intent傳值的引用對象,我們都知道A,S,B三大組件都是可以通過intent的Bundler傳值的,那是因為Bundler實現了Parcelable接口,所以他可以在不同的進程間傳輸,基於這一點,當我們在一個進程中使用另外一個進程的A,S,B,我們就可以在Bunlder中附加我們需要傳輸給遠程進程的信息,然後用intent發送過去,當然,我們傳輸的數據必須能夠序列化,比如基本數據類型,實現了Parcelable接口的對象,實現了Serializable接口的對象以及一些Android支持的特殊對象,具體內容可以看下Bundler這個類,就可以看出他所支持的類型了,Bundler不支持的類型我們無法通過他在進程間傳遞數據,這個很簡單,就不再詳細介紹了,這是一種很簡單的進程間通信方式

除了直接傳遞數據這種典型的使用場景,他還有一種特殊的使用場景,比如A進程正在進行一個計算,計算完成之後他要啟動B進程一個組件並把計算結果傳遞給B進程,可是遺憾的是這個數據不支持放在Bundler裡,因此無法使用intent來傳遞,這個時候如果我們用其他IPC,那就稍微有些麻煩了,可以考慮如下形式:通過intent啟動進程B的一個Service組件,讓Service去完成計算,這樣就不再需要什麼跨進程了,這也只是一種小的技巧

2.使用文件共享

文件共享是一種不錯的進程間通訊的方式,兩個進程通過讀/寫同一個文件來交換數據,比如A進程把數據寫入文件,B再去讀取,我們知道,在Winddows上,一個文件如果被加了排斥鎖將會導致其他線程無法對其訪問,甚至兩個線程同時進度寫的操作也是不允許的,盡管這可能出問題,通過文件交換數據很好使用,除了可以交換一些文本信息外,我們還可以序列化UI和對象到文件裡,引用的時候再恢復,下面就來展示下這個功能

還是本章的例子,在onResume序列化一個對象到sd卡,第二個Activity去恢復,,關鍵代碼如下:

MainActivity

private void persistToFile() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                User user = new User("1", "jek", false);
                File dir = new File(PATH);
                if (!dir.exists()) {
                    dir.mkdir();
                }
                File cachedFile = new File(PATH);
                ObjectOutputStream objectOutputStream = null;
                try {
                    objectOutputStream = new ObjectOutputStream(new FileOutputStream(cachedFile));
                    objectOutputStream.writeObject(user);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

SecondActivity

 private void recoverFromFile() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                User user = null;
                File cachedFile = new File(MainActivity.PATH);
                if(cachedFile.exists()){
                    ObjectInputStream objectInputStream = null;
                    try {
                        objectInputStream = new ObjectInputStream(new FileInputStream(cachedFile));
                        user = (User) objectInputStream.readObject();
                    } catch (IOException e) {
                        e.printStackTrace();
                    } catch (ClassNotFoundException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }

如果我們查看log的話會發現,,是成功的恢復了User對象的數據的

通過文件共享這種方式來共享數據對文件格式是沒有具體要求的,比如可以是文本文件,也可以是XML文件,只要讀/寫雙方約定數據格式即可。通過文件共享的方式也是有局限性的,比如並發讀/寫的問題,像上面的那個例子,如果並發讀/寫,那麼我們讀出的內容就有可能不是最新的,如果是並發寫的話那就更嚴重了。因此我們要盡量避免並發寫這種情況的發生或者考慮使用線程同步來限制多個線程的寫操作。通過上面的分析,我們可以知道,文件共享方式適合在對數據同步要求不高的進程之間進行通信,並且要妥善處理並發讀/寫的問題。

當然,SharedPreferences是個特例,眾所周知,SharedPreferences是Android中提供的輕量級存儲方案,它通過鍵值對的方式來存儲數據,在底層實現上它采用XML文件來存儲鍵值對,每個應用的SharedPreferences文件都可以在當前包所在的data目錄下查看到,一般來說,它的目錄位於/data/data/package name/shared_prefs目錄下,其中package name表示的是當前應用的包名。從本質上來說,SharedPreferences也屬於文件的一種,但是由於系統對它的讀/寫有一定的緩存策略,即在內存中會有一份SharedPreferences文件的緩存,因此在多進程模式下,系統對它的讀/寫就變得不可靠,當面對高並發的讀/寫訪問Sharedpreferences有很大幾率會丟失數據,因此,不建議在進程間通信中使SharedPreferences。

3.使用Messenger

Messenger可以翻譯為信使,顧名思義,通過它可以在不同進程中傳遞Message對象,在Message中放入我們需要傳遞的數據,就可以輕松地實現數據的進程間傳遞了。Messenger是一種輕量級的IPC方案,它的底層實現是AIDL,為什麼這麼說呢,我們大致看一下Messenger這個類的構造方法就明白了。下面是Messenger的兩個構造方法,從構造方法的實現上我們可以明顯看出AIDL的痕跡,不管是IMessenger還是Stub.asInterface,這種使用方法都表明它的底層是AIDL。

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

    public Messenger(IBinder target){
        mTarget = IMessenger.Stud.asInterface(target);
    }

Messenger的使用方法很簡單,它對AIDL做了封裝,使得我們可以更簡便地進行進程通信。同時,由於它一次處理一個請求,因此在服務端我們不用考慮線程同步的問題,這是因為服務端中不存在並發執行的情形。實現一個Messenger有如下幾個步驟,分為服
務端和客戶端。

1.服務端進程

首先,我們需要在服務端創建一個Service來處理客戶端的連接請求,同時創建一個Handler並通過它來創建一個Messenger對象,然後在Service的onBind中返回這個Messenger對象底層的Binder即可。

2.客戶端進程

客戶端進程中,首先要綁定服務端的Service,綁定成功後用服務端返回的IBinder對象創建一個Messenger,通過這個Messenger就可以向服務端發送消息了,發消息類型為
Message對象。如果需要服務端能夠回應客戶端,就和服務端一樣,我們還需要創建一個Handdler並創建一個新的Messenger,並把這個Messenger對象通過Message的replyTo參數傳遞給服務端,服務端通過這個replyTo參數就可以回應客戶端。這聽起來可能還是有點抽
象,不過看了下面的兩個例子,讀者肯定就都明白了。首先,我們來看一個簡單點的例子,
這個例子中服務端無法回應客戶端。。

首先看服務端的代碼,這是服務端的典型代碼,可以看到MessengerHandler用來處理客戶端發送的消息,並從消息中取出客戶端發來的文本信息,而mMessenger是一個Mwsswnger對象,他和MessengerHandler相關聯,並在onBind方法中返回他裡面的IBind對象

public class MessengerService extends Service {

    public static final String TAG = "MessengerService";

    private static class MessengerHandler extends Handler{
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case 100:
                    Log.i(TAG,"數據:" + msg.getData());
                    break;
                default:

                super.handleMessage(msg);
            }
        }
    }

    private final Messenger mMessenger = new Messenger(new MessengerHandler());

    @Override
    public IBinder onBind(Intent intent) {

        return mMessenger.getBinder();
    }
}

然後注冊下這個Service,讓其在獨立進程中運行

 

接下來我們看看服務端,服務端比較簡單

public class MessengerActivity extends AppCompatActivity {

    public static final String TAG = "MessengerActivity";

    private Messenger mService;

    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mService =  new Messenger(service);
            Message msg = Message.obtain(null,10);
            Bundle data = new Bundle();
            data.putString("msg","hell this is client");
            msg.setData(data);
            try {
                mService.send(msg);
            } 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(this,MessengerService.class);
        bindService(intent,mConnection, Context.BIND_AUTO_CREATE);
    }

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

這樣運行之後就能收到發送的消息了

通過上面的例子可以看出,在Messenger中進行數據傳遞必須將數據放入Messsage中,而Messenger和Message都實現了Parcelable接口,因此可以跨進程傳輸,簡單來說,Messebger所支持的數據類型就是Message所支持的傳輸類型。實際上,通過 Messenger來傳遞Message,Message中能使用的載體就只有what、arg1、arg2、Bundle以及replyTo。Message的另一個字段object在同一個進程中是很實用的,但是在進程間通信的時候,在Android2.2以前object字段不支持跨進程傳輸,即便是2.2以後,也僅僅是系統提供的實現了Parcelable接口的對象才能通過它來傳輸。這就意味著我們自定義的Parcelable對象是無法通過object字段來傳輸的,讀者可以試一下。非系統的Parcelable對象的確無法通過object字段來傳輸,這也導致了object字段的實用性大大降低,所幸我們還有Bundle,Bundle中
可以支持大量的數據類型。

上面的例子演示了如何在服務端接收客戶端中發送的消息,但是有時候我們還需要能回應客戶端,下面就介紹如何實現這種效果。還是采用上面的例子,但是稍微做一下修改,每當客戶端發來一條消息,服務端就會自動回復一條“嗯,你的消息我已經收到,稍後
回復你。”,這很類似郵箱的自動回復功能。

首先看服務端的修改,服務端只需要修改MessengerHandler,當收到消息後,會立即回復一條消息給客戶端

private static class MessengerHandler extends Handler{
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case 100:
                    Log.i(TAG,"數據:" + msg.getData());
                    Messenger messenger = msg.replyTo;
                    Message reply = Message.obtain(null,200);
                    Bundle bundle = new Bundle();
                    bundle.putString("reply","收到你的消息,等下回復");
                    try {
                        messenger.send(reply);
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                    break;
                default:
                    super.handleMessage(msg);
            }
        }
    }

接著我們來看看客戶端的修改,為了接受服務端的恢復,客戶端也需要准備一個接收消息的Messenger和handler,如下:

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

    private static class MessengerHandler extends Handler{
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what){
                case 200:
                    Log.i(TAG,"Service:" + msg.getData().getString("reply"));
                    break;
                default:
                    super.handleMessage(msg);
            }
        }
    }

除了上述的修改,還有關鍵的一點,當客戶端發送消息的時候,需要把接收服務端回復的Messenger通過Message的replyTo參數傳遞給服務端,如下:

private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mService =  new android.os.Messenger(service);
            Message msg = Message.obtain(null,100);
            Bundle data = new Bundle();
            data.putString("msg","hell this is client");
            //注意這句話
            msg.replyTo  = mGetReplyMessenger;
            msg.setData(data);
            try {
                mService.send(msg);
            } catch (RemoteException e) {
                e.printStackTrace();
            }

        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };

通過上述的修改,我們再次運行,就達到了自動回復的效果了;

到這裡,我們采用Messenger進程通訊的例子就說完了,我們畫一張工作原理圖,這樣更加便於理解:

這裡寫圖片描述

關於進程間通信,這個只能針對於同一個應用的通訊,那有沒有針對不同應用的?是這樣的,之所以選擇在同一個應用內進行進程間通信,是因為操作起來比較方便,但是效果和在兩個應用間進行進程間通信是一樣的。在本章剛開始就說過,同一個應用的不同組件,如果它們運行在不同進程中,那麼和它們分別屬於兩個應用沒有本質區別,關於這點需要深刻理解,因為這是理解進程間通信的基礎。

四.使用AIDL

上一節我們介紹了使用Messenger來進行進程間通信的方法,可以發現,Messenger是以串行的方式處理客戶端發來的消息,如果大量的消息同時發送到服務端,服務端仍然只能一個個處理,如果有大量的並發請求,那麼用Messenger就不太合適了。同時,Messenger的作用主要是為了傳遞消息,很多時候我們可能需要跨進程調用服務端的方法,這種情形用Messenger就無法做到了,但是我們可以使用AIDL來實現跨進程的方法調用。AIDL是Messenger的底層實現,因此Messenger本質上也是AIDL,只不過系統為我們做了封裝,從而方便上層的調用而已。在上一節中,我們介紹了Binder的概念,大家對Binder也有了一定的了解,在Binder的基礎上我們可以更加容易地理解AIDL。這裡先介紹使用AIDL
來進行進程間通信的流程,分為服務端和客戶端兩個方面。

1.服務端

服務端首先要創建一個Service用來監聽客戶端的連接請求,然後創建一個AIDL文件,將暴露給客戶端的接口在這個AIDL文件中聲明,最後在Service中實現這個AIDL接口即可

2.客戶端

客戶端所要做事情就稍微簡單一些,首先需要綁定服務端的Service,綁定成功後,將服務端返回的Binder對象轉成AIDL接口所屬的類型,接著就可以調用AIDL中的方法了

上面描述的只是一個感性的過程,AIDL的實現過程遠不止這麼簡單,接下來會對其中的細節和難點進行詳細介紹,並完善我們在Binder那一節所提供的的實例。

3.AIDL接口的創建

首先看AIDL接口的創建,如下所示,我們創建了一個後綴為AIDL.的文件,在裡面明了一個接口和兩個接口方法。

// IBookManager.aidl
package com.liuguilin.ipcsample;

import com.liuguilin.ipcsample.Book;

interface IBookManager {
    /**
     * Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
    ListgetBookList();
    void addBook(in Book book);
}

在AlDL文件中,並不是所有的數據類型都是可以使用的,那麼到底AIDL文件哪些數據類型呢?如下所示

基本數據類型(int、long、char、boolean、double等); Sting和CharSequence: List:只支持ArrayList,裡面每個元素都必須能夠被AIDL支持; Map:只支持HashMap,裡面的每個元素都必須被AIDL支持,包括key和value: Parcelable:所有實現了Parcelable接口的對象; AIDL.所有的AIDL接口本身也可以在AIDL.文件中使用

以上6種數據類型就是AIDL所支持的所有類型,其中自定義的Parcelable對象和AIDL對家比須要顯式import進來,不管它們是否和當前的AIDL文件位於同一個包內。比如IBookManager.AIDL這個文件,裡面用到了Book這個類,這個類實現了Parcelable接口並且和IBookManager.AIDL位於同一個包中,但是遵守AIDL的規范,我們仍然需要顯式的

import com.liuguilin.ipcsample.Book;

AIDL中會大量使用到Parcelable,至於如何使用,在本章的前面已經介紹過,這裡就不再整述。

另外一個需要注意的地方就是,如果AIDL文件中用到了自定義的Parcelable對象,那麼必須新增一個和他同名的AIDL文件,並在其中聲明它為Parcelable 類型。在上面的IBookManager.AIDL這個類中我們用到Book,所以,我們必須要創建Book.aidl,然後
在裡面添加如下內容:

// Book.aidl.aidl
package com.liuguilin.ipcsample;

parcelable Book;

我們需要注意,AIDL中每個實現了Parcelable接口的類都需要按照上面那種方式去創建相應的AIDL文件並聲明那個類為parcelable。除此之外,AIDL中除了基本數據類型,其他類型的參數必須標上方向:in、out或者inout,in表示輸入型參數,out表示輸出型參數,inout表示輸入輸出型參數,至於它們具體的區別,這個就不說了。我們要根據實際需要去指定參數類型,不能一概使用out或者inout,因為這在底層實現是有開銷的。最後,,AIDL接口中只支持方法,不支持聲明靜態常量,這一點區別於傳統的接口。

為了方便AIDL的開發,建議把所有和AIDL相關的類和文件全部放入同一個包中,這樣做的好處是,當客戶端是另外一個應用時,我們可以直接把整個包復制到客戶端工程,對於本例來說,就是要把aidl這個包和包中的文件原封不動地復制到客戶端中。如果AIDL相關的文件位於不同的包中時,那麼就需要把這些包一一復制到服務端和客戶端要保持一致,否則運行會出錯,這是因為客戶端需要反序列化服務端中和AIDL接口相關的所有類,如果類的完整路徑不一樣的話,就無法成功反序列化,程序也就無法正常運行。為了方便演示,本章的所有示例都是在同一個工程中進行的,但是讀者要理解,一個工程和兩個工程的多進程本質是一樣的,兩個工程的情況,除了需要復制AIDL接口所相關的包到客戶端,其他完全一樣,讀者可以自行試驗。

4.遠程服務端Service的實現

上述我們講了如何定義一個AIDL的接口,接下來我們實現以下這個接口:

public class BookManagerService extends Service {

    private static final String TAG = "BookManagerService";

    private CopyOnWriteArrayList mBookList = new CopyOnWriteArrayList<>();

    private Binder mBinder = new IBookManager.Stub() {

        @Override
        public List getBookList() throws RemoteException {
            return mBookList;
        }

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

    @Override
    public void onCreate() {
        super.onCreate();

        mBookList.add(new Book(1,"Android"));
        mBookList.add(new Book(1,"IOS"));
    }

    @Override
    public IBinder onBind(Intent intent) {

        return mBinder;

    }
}

上面是一個服務端Service的典型實現,首先在onCreate中初始化添加了兩本圖書的信息,然後創建了一個Binder對象並在onBind中返回它,這個對象繼承自IBookManager.Stub並且實現了它內部的AIDL方法,這個過程在Binder那一節已經介紹過了,這裡就不多說了。這裡主要看getBookList和addBook這兩個AIDL方法的實現,實現過程也比較簡單,注意這裡采用了CopyOnWriteArrayList,這個CopyOnWriteArrayList支持並發讀/寫。在前面我們提到,AIDL方法是在服務端的Binder線程池中執行的,因此當多個客戶端同時連接的
時候,會存在多個線程同時訪問的情形,所以我們要在AIDL方法中處理線程同步,而我們直接使用CopyonWriteArayList來進行自動的線程同步。

前面我們提到,AIDL中能夠使用的List只有ArrayList,但是我們這裡卻使用了CopyOnWriteArrayList,注意它不是繼承ArrayList,為什麼能夠正常工作呢?這是因為AIDL中所支持的是抽象的List,而List只是一個接口,因此雖然服務端返回的是CopyOnWriteArrayList,但是在Binder中會按照List的規范去訪問數據並最終形成一個新的ArrayList傳遞給客戶端。所以,我們在服務端采用CopyOnWriteArrayList是完全可以的。和此類似的還有ConcurrentHashMap,你可以體會一下這種轉換情形,然後我們需要在清單文件中注冊一下

五.客戶端的實現

客戶端的實現就比較簡單了,我們綁定遠程服務,綁定成功之後返回的Binder對象轉換成AIDL接口,讓後可以通過這個接口去調用服務端的遠程方法,代碼如下:

public class BookManagerActivity extends AppCompatActivity {

    private static final String TAG = "BookManagerActivity";

    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            IBookManager bookManager = IBookManager.Stub.asInterface(service);
            try {
                List list = bookManager.getBookList();
                Log.i(TAG, "list Type :" + list.getClass());
                getCanonicalName();
                Log.i(TAG, "list string : " + list.toString());
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };

    private void getCanonicalName() {
    }

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

        Intent intent = new Intent(this, BookManagerService.class);
        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);

    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        unbindService(mConnection);
    }
}

綁定成功之後,會通過bookManager去掉用getBookList方法,然後打印出所獲取的圖書信息,需要注意的是,服務端的方法可能需要很久才會執行完畢,這個時候下面的代碼就會導致ANR,這一點需要注意,後面我們再解釋這種情況,之所以要這樣學是為了讓你更加熟悉AIDL的步驟

我們執行一下,log也就打印了,可以發現,雖然我們服務端返回的CopyOnWriteArrayList類型,但是客戶端收到的仍然是ArrayList,這也證實了我們前面所做的分析,第二個log得到信息;

這就是一個完整的AIDL的運行過程了,但是這些都只是皮毛而已,真正的AIDL應用匯復雜許多,我們再來聊一下AIDL的難點:

我們將诶下來再啟動另一個接口addBook,我們在客戶端給服務端添加一本書在獲取一次看看現在是什麼樣的;

private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            IBookManager bookManager = IBookManager.Stub.asInterface(service);
            try {
                List list = bookManager.getBookList();
                Log.i(TAG, "list Type :" + list.getClass());
                Book newBook = new Book(3, "降龍十八掌");
                bookManager.addBook(newBook);
                Log.i(TAG, "add Book: " + newBook);
                List newList = bookManager.getBookList();
                Log.i(TAG, "query list : " + newList.toString());
                getCanonicalName();
                Log.i(TAG, "list string : " + list.toString());
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

運行的結果想必大家都知道了,我們成功的添加了一本書;

現在我們考慮一種情況,假設有一種需求:用戶不想時不時地去查詢圖書列表了,太累了,於是,他去問圖書館,“當有新書時能不能把書的信息告訴我呢?”。大家應該明白了,這就是一種典型的觀察者模式,每個感興趣的用戶都觀察新書,當新書到的時候,圖書館就通知每一個對這本書感興趣的用戶,這種模式在實際開發中用得很多,下面我們就來模擬這種情形。首先,我們需要提供一個AIDL接口,每個用戶都需要實現這個接口並且向圖書館申請新書的提醒功能,當然用戶也可以隨時取消這種提醒。之所以選擇AIDL接口而不是普通接口,是因為AIDL中無法使用普通接口。這裡我們創建一個IOnNewBookArrivedListener.aidl文件,我們所期望的情況是:當服務端有新書到來時,就會通知每一個己經申請提醒功能的用戶。從程序上來說就是調用所有IOnNewBookArivedListener對象中的onNewBookArived方法,並把新書的對象通過參數傳遞給客戶端,內容如下所示。

// IOnNewBookArrivedListener.aidl
package com.liuguilin.ipcsample;

import com.liuguilin.ipcsample.Book;

interface IOnNewBookArrivedListener {

   void onNewBookArrived(in Book newBook);
}

除了要新增加一個AIDL外,還需要在原有的接口中添加兩個新的方法

// IBookManager.aidl
package com.liuguilin.ipcsample;

import com.liuguilin.ipcsample.Book;
import com.liuguilin.ipcsample.IOnNewBookArrivedListener.aidl;

interface IBookManager {

    ListgetBookList();
    void addBook(in Book book);
    void registerListener(IOnNewBookArrivedListener listener);
    void unregisterListener(IOnNewBookArrivedListener listener);
}

接下來,我們的服務端也是要稍微的修改一下,每隔5s向感興趣的用戶提醒

public class BookManagerService extends Service {

    private static final String TAG = "BookManagerService";

    private CopyOnWriteArrayList mBookList = new CopyOnWriteArrayList<>();

    private AtomicBoolean mIsServiceDestoryed = new AtomicBoolean(false);

    private CopyOnWriteArrayListmListenerList = new CopyOnWriteArrayList<>();

    private Binder mBinder = new IBookManager.Stub() {

        @Override
        public List getBookList() throws RemoteException {
            return mBookList;
        }

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

        @Override
        public void registerListener(IOnNewBookArrivedListener listener) throws RemoteException {
            if(!mListenerList.contains(listener)){
                mListenerList.add(listener);
            }else {
                Log.i(TAG,"already exists");
            }
            Log.i(TAG,"registerListener size:" + mListenerList.size());
        }

        @Override
        public void unregisterListener(IOnNewBookArrivedListener listener) throws RemoteException {
                if(mListenerList.contains(listener)){
                    mListenerList.remove(listener);
                    Log.i(TAG,"remove listener");
                }else {
                    Log.i(TAG,"can not remove listener");
                }
            Log.i(TAG,"unregisterListener size:" + mListenerList.size());
        }
    };

    @Override
    public void onCreate() {
        super.onCreate();

        mBookList.add(new Book(1,"Android"));
        mBookList.add(new Book(1,"IOS"));
        new Thread(new ServiceWorker()).start();
    }

    @Override
    public IBinder onBind(Intent intent) {

        return mBinder;

    }

    @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 (InterruptedException e) {
                    e.printStackTrace();
                }
                int bookId = mBookList.size()+1;
                Book newBook = new Book(bookId,"new book#" + bookId);
                try {
                    onNewBookArrived(newBook);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private void onNewBookArrived(Book book) throws RemoteException{
        mBookList.add(book);
        Log.i(TAG,"onNewBookArrived size:" + mListenerList.size());
        for (int i = 0; i < mListenerList.size(); i++) {
            IOnNewBookArrivedListener listener = mListenerList.get(i);
            Log.i(TAG,"listener: "+ listener);
            listener.onNewBookArrived(book);
        }
    }

}

最後,我們還需要修改一下客戶端的代碼,主要是兩方面,首先是客戶端要注冊IOnNewBookArrivedListener到遠程的服務器,這樣當有新書時服務端才能通知客戶端,同時在我們的Activity的onDestory方法裡面去取消綁定,,另一方面,當有新書的時候,服務端會會帶哦客戶端的Binder線程池中執行,因此,為了便於進行UI操作,我們需要有一個Hnadler可以將其切換到客戶端的主線程去執行,這個原理在Binder中已經做了分析了,這裡不多說,把代碼貼上:

package com.liuguilin.ipcsample;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteException;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;

import java.util.List;

/**
 * Created by lgl on 16/10/5.
 */

public class BookManagerActivity extends AppCompatActivity {

    private static final String TAG = "BookManagerActivity";

    public static final int MESSAGE_NEW_BOOK_ARRIVED = 1;

    private IBookManager mRemoteBookManager;

    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MESSAGE_NEW_BOOK_ARRIVED:
                    Log.i(TAG, "Handler");
                    break;
                default:
                    super.handleMessage(msg);
                    break;
            }
        }
    };

    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            IBookManager bookManager = IBookManager.Stub.asInterface(service);
            try {
                List list = bookManager.getBookList();
                Log.i(TAG, "list Type :" + list.getClass());
                Book newBook = new Book(3, "降龍十八掌");
                bookManager.addBook(newBook);
                Log.i(TAG, "add Book: " + newBook);
                List newList = bookManager.getBookList();
                Log.i(TAG, "query list : " + newList.toString());
                getCanonicalName();
                Log.i(TAG, "list string : " + list.toString());
                //注冊
                bookManager.registerListener(mOnNew);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            mRemoteBookManager = null;
        }
    };

    private static void getCanonicalName() {
    }

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

        Intent intent = new Intent(this, BookManagerService.class);
        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);

    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        unbindService(mConnection);
    }

    private void onNewBookArrived
    throws RemoteException

    {
        mBookList.add(book);
        for (int i = 0; i < mListenerList.size(); i++) {
            IOnNewBookArrivedListener listener = mListenerList.get(i);
            listener.onNewBookArrived(book);
        }
    }

    private class ServiceWorker implements Runnable {

        @Override
        public void run

        {
            while (!mIsServiceDestoryed.get()) {
                try {
                    Thread.sleep(5000);
                } catch (InteruptedException e) {
                    e.toString();
                }

                int bookId = mBookList.size() + 1;
                Book newBook = new Book(bookId, "new book#" + bookId):
                try {
                    onNewBookArrived(newBook);
                } catch () {

                }
            }
        }
    }

}

最後我們如果運行的話,不難發現,我們的每隔5s新書推送是成功的

如果你以為AIDL就這樣結束了,那你就錯了,之前就說過,AIDL遠不止這麼簡單,目前我們還有一些難點還沒有涉及

從上面的代碼我們可以看出,當BookManagerActivity關閉時,我們會在onDestory中去接觸已經注冊的服務端的listener,這就相當於我們不想再接收圖書館的新書提醒,所以我們可以隨時退出這個activity

從上log可以看出,程序沒有像我們所預期的那樣執行。在解注冊的過程中,服務端竟然無法找到我們之前注冊的那個listener,在客戶端我們注冊和解注冊時明明傳的是同一個listener啊!最終,服務端由於無法找到要解除的listener而宣告解注冊失敗!這當然不是我們想要的結果,但是仔細想想,好像這種方式的確無法完成解注冊。其實,這是必然的,這種解注冊的處理方式在日常開發過程中時常使用到,但是放到多進程中卻無法奏效,因為Binder會把客戶端傳遞過來的對象重新轉化並生成一個新的對象。雖然在注冊和解注冊過程中使用的是同一個客戶端對象,但是通過Binder傳遞到服務端後會產生兩個全新的對象。別忘了對象是不能跨進程直接傳輸的,對象的跨進程傳輸本質上都是反序列化的過程,這就是為什麼AIDL中的自定義對象都必須要實現Parcelable接口的原因。那麼我們要怎麼做才能實現解注冊的功能尼?答案是用RemoteCallbackList,這看起來很抽象,不過沒關系,請看接下來的分析;

RemoteCallbackList是系統專門用來刪除listener的接口,RemoteCallbackList是一個泛型,支持管理任意的AIDL接口,這點從他的聲明就可以看出,,因為它的工作原理很簡單,在它的內部有一個Map結構專門用來保存所有的AIDL回調,這個Map的key是IBinder類型,value是Callback類型,如下所示。

ArrayMap mCallbacks = new ArrayMap();

其中Callback中封裝了真正的遠程listener。當客戶端注冊listener的時候,它會把這個listener的信息存入mCallbacks中,其中key和value分別通過下面的方式獲得:

IBinder key = listener.asBinder();
Callback value = new Callback(listener,cookie);

到這裡,我相信讀者應該都明白了,雖然說多次跨進程傳輸客戶端的同一個對象會在服務端生成不同的對象,但是這些新生成的對象都有一個共同點,那就是他們底層的Binder對象是同一個,利用這些特性,就可以實現我們無法實現的功能了,當客戶端解注冊的時候,我們只要遍歷服務端所有的listener,找到那個和解注冊listener具有相同Binder對象的服務端listener並把它刪掉,這就是RemoteCallbackList為我們做的事情,同時RemoteCallbackList還有一個很有用的功能,就是當客戶端終止後,它能夠自動移除客戶端的listener,另外,RemoteCallbackList內部自動實現了線程同步的功能,所以我們使用他來注冊和解注冊,不需要做額外的線程工作,由此可見,RemoteCallbackList是一個很有價值的類,下面我們來演示一下他是如何解注冊的

RemoteCallbackList使用起來很很簡單,我們要BookManagerService做一些修改,首先我們創建一個RemoteCallbackList對象來替代之前的CopyonWriteArrayList

 private RemoteCallbackListmListenerList = new RemoteCallbackList();

然後修改registerListener 和unregisterListener這兩個接口的實現

  @Override
        public void registerListener(IOnNewBookArrivedListener listener) throws RemoteException {
            mListenerList.registener(listener);
        }

        @Override
        public void unregisterListener(IOnNewBookArrivedListener listener) throws RemoteException {
            mListenerList.unregistener(listener);
        }

怎麼樣,使用起來是不是很簡單,接下來我們修改onNewBookArrived方法,當有新書的時候,我們就要通知所有已注冊的listener

private void onNewBookArrived(Book book) throws RemoteException{
        mBookList.add(book);
        final int N = mListenerList.beginBroadcast();
        for (int i = 0; i < N; i++) {
            IOnNewBookArrivedListener l = mListenerList.getBroadcast(i);
            if(i != null){
                l.onNewBookArrived(book);
            }
        }
        mListenerList.finishBroadcast();
    }

BookManagerService的修改已經完畢,為了方便我們驗證程序的功能,我們還需要添加一些log,這裡我們就不演示了

到這裡,AIDL的基本使用方法已經介紹完了,但是有幾點還需要再次說明一下。我們知道,客戶端調用遠程服務的方法,被調用的方法運行在服務端的Binder線程池中,同時客戶端線程會被掛起,這個時候如果服務端方法執行比較耗時,就會導致客戶端線程長時間地阻塞在這裡,而如果這個客戶端線程是UI線程的話,就會導致客戶端ANR,這當然不是我們想要看到的。因此,如果我們明確知道某個遠程方法是耗時的,那麼就要避免在客戶端的UI線程中去訪問遠程方法。由於客戶端的onServiceConnected和 onServiceDisconnected方法都運行在UI線程中,所以也不可以在它們裡面直接調用服務端的耗時方法,這點要尤其注意。另外,由於服務端的方法本身就運行在服務端的Binder線程池中,所以服務端方法本身就可以執行大量耗時操作,這個時候切記不要在服務端方法中開線程執行異步任務,除非你明確知道自己在干什麼,否則不建議這麼做。下面我們稍微改造一下服務端getBookList,我們讓他耗時

    @Override
        public List getBookList() throws RemoteException {
            SystemClock.sleep(5000);
            return mBookList;
        }

這樣你多來幾下就ANR了

我們本章節先講到這裡,實在是太長了,我們還有下半部分繼續講

Makedown:http://pan.baidu.com/s/1o7Z4Djs 密碼:xdgt

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved