《Android 開發藝術探索》筆記2--IPC機制

IPC機制.png

Android IPC簡介

IPC是Inter-Process Communication縮寫,含義為進程間通信. 按照操作系統中的描述,線程是cpu調度的最小單元,而進程一般指一個執行單元. 進程中可以有一個或者多個線程.

不同的操作系統有著不同的IPC機制:

  • Windows: 通過剪切板, 管道, 信號量來進行進程間通信
  • Linux: 通過命名管道, 共享內存, 信號量等來進行進行進程間通信
  • android: 雖然基于Linux內核,但是使用了獨有的Binder機制, 也可以Socket進行通信

使用場景: 可能有些模塊因為特殊原因需要運行在單獨的進程中; 或者為了加大一個應用可使用的內存; 又或者我們需要去另外一個進程去獲取數據,必然需要跨進程.

Android中的多進程的模式

開啟多進程模式

如果你想在一個應用中使用多個進程,通過清單文件給四大組件添加android:process屬性,就可以很方便的開啟多進程.

還有一種非常規的創建方式,通過JNI在native層去fork一個新的進程.這種只做了解.

image

例如這樣,當我們依次打開MainActivity, SecondActivity, ThirdActivity.此時應該打開了三個進程.

我們來檢測一下, 你可以直接使用DDMS來查看進程,這里使用命令行來測試

$ adb shell ps | grep com.szysky 你可以直接使用adb shell ps這會把系統所有進程展示出來, 你可以加上過濾信息| grep xxx xxx替換你需要過濾出來信息即可

image

你可能已經發現在創建新進程的時候使用兩種不同的方式

  • 當以:開頭的進程,屬于當前應用的私有進程,其他應用的組件不可以和它跑在同一個進程
  • 當不以:開頭,那么進程屬于全局進程,其他應用通過ShareUID方法可以和它跑在同一個進程

Android系統會為每一個應用分配唯一的UID. 相同UID的應用才能共享數據. 但是兩個應用通過ShareUID跑在同一個進程是有要求的. 除了具有相同的ShareUID并且還要簽名相同才可以. 這時如果不在同一進程他們之間可以共享data目錄,組件信息等. 如果還在同一進程, 那么他們還能共享內存數據.

進程模式的運行機制

開啟多進程簡單,但是如果不能處理好其中的特性,那么受傷的總會是你.

先說第一個比較嚴重的問題. 靜態變量不在共享. 還是直接三個類的例子,如果在mainActivity中對靜態變量進行修改, 在SecondActivity取出這個靜態發現是main沒修改之前的. 這說明兩個進程間即使是靜態屬性也是無法共享.

其實這是因為這兩個類運行在兩個進程間,而每個單獨的進程又會分配一個獨立的虛擬機, 所以每個虛擬機在內存分配上有不同的地址空間.對于不同虛擬機訪問同一個對象就會產生多份副本. 副本之間互相獨立不干擾彼此.

一般情況下多進程可能面臨的問題:

  1. 靜態成員和單例模式完全失效
  2. 線程同步機制完全失效
  3. SharedPreferences的可靠性下降
  4. Application會多次創建

2中因為不是一塊內存,所以不管是鎖對象還是鎖全局都無法保證線程同步,因為不是同一個對象. 3中因為Sp不支持兩個進程同時讀寫,因為底層是通過讀寫XML文件實現的,并發可能會觸發異常. 4中運行在多個進程中,那么就會創建多個虛擬機,每個虛擬機都有一個對應Application并需要啟動加載這個文件.

一個應用的多進程:它就相當于兩個不同的應用采用了ShareUID的模式. 每個進程都會擁有獨立的虛擬機, Application以及內存空間

IPC基礎概念

關于IPC主要包含三方面的內容: Serializable接口, Parcelable接口, 以及Binder

Serializable接口

Serializable是Java提供的一個序列化接口,這個一個空接口. 如果我們想使用只需要實現Serializable接口,并聲明一個long類型的常量serialVersionUID(不聲明也是可以,但是在反序列化會出現錯誤).

javabean 的實現

public class Student implements Serializable{

    public static final long serialVersionUID = 123456789L;

    //.....省略創建屬性,打印等操作

}

序列化的代碼如下圖,并附上結果.


image.png

好了說一下serialVersionUID這個屬性. 即使我們不聲明系統會根據當前類結構(成員變量等)生成一個hash為serialVersionUID, 雖然這樣也可以但是如果在你把一個對象序列化的到磁盤的一個文件的時候. 對這個對象增加了一個成員變量,那么在反序列的時候就會報錯. 因為當你反序列化的時候對象如果沒有serialVersionUID還會重新計算.這時反序列化的hash和序列化的hash就不一致了.

關于根據當前類結構計算hash值,有兩點需要注意:

  • 靜態成員變量屬于類不屬于對象,所以不參與序列化的過程
  • 其次用transient關鍵字標記的成員變量不參與序列化的過程.

系統默認的序列化過程是可以改變的,通過實現writeObjectreadObject可以重寫默認的序列化和反序列化過程. 這里就不詳細說明

Parcelable接口

系統已經為我們提供了很多實現了Parcelable接口的類,他們都可以直接序列化. 例如intent, Bundle, Bitmap, 同時List和Map也可以序列化.前提是他們里面的每個元素都可以序列化.

實現Parcelable接口主要復寫四個, 我們可以直接定義好javabean直接讓AS幫我們實現.

  • writeToParcel() 主要完成序列化功能
  • CREATOR 主要完成反序列化
  • 接收參數parcel的構造函數 用于從序列化后的對象中創建原始對象
  • describeContents() 幾乎所有情況下都返回0,只有當前對象中存在文件描述符時返回1

關于Parcelable和Serializable的取舍

  • Serializable: 適合序列化到設備或者序列化后通過網絡傳輸.
  • Parcelable: 主要用在內存序列化上. 不需要大量的I/O操作,所以在內存中使用高效.

了解Binder[]

image.png
  • 代碼層面: Binder是Android中的一個類,它實現了IBinder接口
  • IPC角度: Binder是Android中的一種跨進程通信方式.
  • 物理設備角度: Binder也可以認為是一種虛擬的物理設備,設備驅動是/dev/binder
  • Framework角度: Binder是ServiceManager連接各種Manager和相應的ManagerService的橋梁.
  • Android應用角度:Binder是客戶端和服務端進行通信的媒介.

日常開發中,Binder主要用在Service包括AIDLMessenger. 而普通的Service中的Binder不涉及進程間的通信,無法觸及Binder的核心. 而Messenger底層其實就是AIDL.所以我們利用AIDL來分析Binder的工作機制

創建AIDL實例

新建三個文件 Book.java Book.aidlIBookManager.aidl

首先創建一個Book類并實現Parcelable接口,然后在這個類所在的包上右鍵,如圖所示

image

如果名字不能為Book,可以先隨便寫一個,創建之后修改. 然后按圖修改,

image

文件聲明完,我們只需要重新Make一下工程就可以.Build --> Rebuild Project或者Make Project

image.png

Make之后會在app -> build -> generated -> source -> aidl -> debug -> … 出現系統自動生成好的java類. 我們需要對其進行分析

看下圖了解一個大體結構

image

上圖圈出了兩個部分,部分二應該很清楚就是我們定義在aidl中的兩個抽象方法. 而部分一在圖上的內部類Stub寫了說明. 我們自定義的兩個抽象方法,在內部類中用了兩個整形int值來標識兩個抽象方法,用在transact()中可以識別客戶端請求哪個方法.

這個繼承了IInterface的接口的核心實現:就是內部類Stub和Stub的內部代理Proxy

先看一下內部類的結構圖:

image

說明直接放圖,寫在代碼上,在git上的根目錄的aidl的java類說明文件夾也有添加了注釋的類.

image

有兩點需要注意:

  1. 客戶端發起遠程請求時,當前線程會被掛起直到服務器進程返回數據,所以注意線程是否在意耗時
  2. 由于服務端Binder方法運行在Binder線程池中,所以不管Binder方法是否耗時都應該采用同步方式,因為已經在一個線程中了

我們也可以手動實現Binder類,這里不再細說,在git倉庫的項目中有一個manual包里面是關于手動實現Binder的代碼.

其實 不管是手動實現Binder也好,或者AIDL文件實現Binder也好. 其實兩者的工作原理都是一樣的, AIDL文件的存在意義是系統為我們提供了一種快速實現Binder的工具,僅此而已.

Binder生命狀態的監聽

由于Binder是運行在服務端,如果服務端進程異常終止,那么我們到服務端的Binder連接也就斷裂(Binder死亡).就會導致調用失敗,所里系統提供了死亡代理的方法 就是當Binder死亡時,我們就會收到通知,這個時候就可以重新發起連接請求而恢復連接

首先創建監聽的DeathRecipient對象

IBinder.DeathRecipient mDeat = new IBinder.DeathRecipient() {

            // 當Binder死亡的時候,系統會回調binderDied()方法

            @Override

            public void binderDied() {

                if (mBookManager == null)

                    return ;

                //清除掉已經無用的Binder連接

                mBookManager.asBinder().unlinkToDeath(mDeat,0);

                mBookManager == null;

                //TODO  進行重新綁定遠程服務

            }

        };

當客戶端綁定遠程服務成功的時候,給binder設置死亡代理

binder.linkToDeath(mDeat,0);

linkToDeath的第二個參數是個標志位,直接設0即可. 另外也可以通過Binder的方法isBindAlive也可以判斷Binder是否死亡.

Android的幾種跨進程的方式

使用Bundle

由于Bundle實現了Parcelable接口,所以在四大組件中的三大組件(Activity, Service, Receiver)都支持在Intent中傳遞Bundle.

所以如果在一個進程中啟動了另一個進程的三大組件,就可以在Bundle中附加我們需要的信息通過Intent發送出去. 當然傳遞的類型必須是能夠被序列化的, 例如基本數據類型,實現了Parcelable和Serializable接口的對象和一些Android支持的特殊對象.

使用文件共享

文件共享適合在對數據同步要求不高的進程之間進行通信,并且要妥善的處理并發讀寫的問題.

兩個進程通過讀/寫同一個文件來交換數據. 例如進程A把數據寫入文件中,而進程B從文件中讀取出來數據.

Android是基于Linux系統, 所以對于并發讀寫文件可以沒有限制的執行. 這里不像Windows系統,對于一個文件如果加了排斥鎖將會導致其他線程無法對其進行訪問.

關于這部分的練習, 在之前練習Binder的時候已經練習過了. 代碼在項目中的MainActivity中

雖然序列化反序列達到的效果是可以恢復對象里面的屬性值,但是反序列每回都是一個新的對象.

SharePreferencess是Android提供的一個輕量級方案,通過鍵值對存儲數據,底層采用XML文件來進行存儲. 存儲路徑/data/data/package name/shared_prefs目錄下. 也屬于文件的一種,但是由于系統對SP的讀寫存在一定的緩存策略,內存中會有一份緩存,所以多進程下,系統對它的讀寫也就變得不可靠.

使用Messenger

Messenger(信使). 不同的進程中可以傳遞Message對象, 在Message中放入我們需要傳遞的數據,就可實現進程間傳遞. Messenger是一種輕量級的IPC方案,它的底層實現AIDL. 看一些構造函數

public Messenger(Handler target) {

        mTarget = target.getIMessenger();

    }

public Messenger(IBinder target) {

   mTarget = IMessenger.Stub.asInterface(target);

}

無論是IMessenger還是Stub.asInterface. 可以明顯看出AIDL的痕跡.

因為Messenger對AIDL進行了封裝,使得在使用時更加簡單,并且它的處理方式是一次處理一個請求,因此服務器端不用考慮線程同步因為服務端不存在并發執行的情形.

具體實現Messenger

1.服務端

創建一個Service作為服務端來處理客戶端的請求, 同時創建一個Handle并通過它來創建一個Messenger對象,然后在Service的onBind()方法中返回這個Messenger對象底層的Binder.
最后這個組件在清單文件中聲明加上android:process="com.szysky.test"屬性.已達到模擬多進程的場景

public class MessengerService extends Service {

    /**

     * 編寫一個類繼承Handler,并對客戶端發來的消息進行處理操作進行添加

     */

    private static  class MessengerHandler extends Handler{

        private static final String TAG = "MessengerHandler";

        @Override

        public void handleMessage(Message msg) {

            switch (msg.what){

                //客戶端發來的信息標識

                case MessengerActivity.FROM_CLIENT:

                    Log.d(TAG, "handleMessage: receive msg form clinet-->" +msg.getData().getString("msg"));

                    //對客戶端進行reply回答

                    // 1\. 通過接收到的到客戶端的Message對象獲取到Messenger信使

                    Messenger client = msg.replyTo;

                    // 2\. 創建一個信息Message對象,并把一些數據加入到這個對象中

                    Message replyMessage = Message.obtain(null, MessengerActivity.FROM_SERVICE);

                    Bundle bundle = new Bundle();

                    bundle.putString("reply", "我是服務端發送的消息,我已經接收到你的消息了,你應該在你的客戶端可以看到");

                    replyMessage.setData(bundle);

                    // 3\. 通過信使Messenger發送封裝好的Message信息

                    try {

                        client.send(replyMessage);

                    } catch (RemoteException e) {

                        e.printStackTrace();

                    }

                    break;

                default:

                    super.handleMessage(msg);

            }

        }

    }

    /**

     * 創建一個Messenger信使

     */

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

    @Nullable

    @Override

    public IBinder onBind(Intent intent) {

        return mMessenger.getBinder();

    }

}

別忘了清單文件

<service android:name=".message.MessengerService"

            android:process="com.szysky.test"/>

2.客戶端

直接用一個activity作為客戶端, 首先綁定之前創建的服務端的Service, 綁定成功時通過ServiceConnection對象接收到服務端返回的IBinder,用IBinder對象創建一個Messenger. 通過這個Messenger就可以往服務端發送消息. 如果我們需要服務端也能夠回應客戶端. 那么就要在客戶端同之前服務端一樣通過Handle創建一個Messenger對象, 并把這個Messenger在通過連接成功返回的IBinder創建的Message對象通過replyTo參數傳遞給服務器. 這個服務器就可以通過replyTo參數來回應客戶端.


/**

* 聲明一個本進程的信使 用來監聽并處理服務端傳入的消息

*/

private Messenger mGetReplyMessenger =  new Messenger(new Handler(){

   @Override

   public void handleMessage(Message msg) {

       switch (msg.what){

           case FROM_SERVICE:

               Log.d(TAG, "handleMessage: 這里是客戶端:::"+msg.getData().getString("reply"));

               break;

           default:

               super.handleMessage(msg);

       }

   }

});

/**

* 創建一個服務監聽連接對象 并在成功的時候給服務器發送一條消息

*/

private ServiceConnection mConnection = new ServiceConnection() {

   //綁定成功回調

   @Override

   public void onServiceConnected(ComponentName name, IBinder service) {

       // 利用服務端返回的binder對象創建Messenger并使用此對象想服務端發送消息

       Messenger mService = new Messenger(service);

       Message obtain = Message.obtain(null, FROM_CLIENT);

       Bundle bundle = new Bundle();

       bundle.putString("msg", "你好啊,  我是從客戶端來");

       obtain.setData(bundle);

       // 需要把接收服務端回復的Messenger通過Message的replyTo傳遞給服務端

       obtain.replyTo = mGetReplyMessenger;

       try {

           mService.send(obtain);

       } catch (RemoteException e) {

           e.printStackTrace();

       }

   }

   @Override

   public void onServiceDisconnected(ComponentName name) {

   }

};

//進行遠端服務的連接

 Intent intent = new Intent(MessengerActivity.this, MessengerService.class);

 bindService(intent, mConnection, Context.BIND_AUTO_CREATE);

本例測試代碼在倉庫對應項目的message包中

在使用Messenger進行數據傳遞必須將數據放入到Message中. 而Messenger和Message都實現了序列化接口. 所以可以在進程間通信.

Message的能使用的載體只有what, arg1, arg2, Bundle 以及replyTo. 這里有一個載體需要注意object,它在同一個進程很實用,但是在版本2.2之前是不支持跨進程的,雖然進行了改進之后,但是也只是支持系統提供實現的某些對象才可以. 所以使用的時候需要注意.

順便繪制了一個Messenger通信的流程圖, 可以對代碼的調用順序理解的更清楚.

image

使用AIDL

雖然Messenger使用方便, 但是要清楚它是以串行的方式處理客戶端發來的消息,如果有大量并發的請求. 或者需求是跨進程調用服務端的方法時. 就無法使用Messenger. 這個時候就該AIDL

對于使用AIDL的流程簡單梳理一遍

服務端

服務端創建一個Service用來監聽客戶端的連接請求, 然后創建一個AIDL文件,將暴露給客戶端的接口在這個AIDL文件中聲明,最后在Service中實現這個AIDL接口并在onBind()返回即可.

客戶端

綁定服務端的Service,綁定成功后,將服務端返回來的Binder對象轉成AIDL接口所屬的類型,接著就可以直接調用AIDL中的方法了.

AIDL中所支持的類型

  • 基本數據類型
  • String 和 CharSequence
  • List: 只支持ArrayList, 里面每個元素都必須能被AIDL支持
  • Map: 只支持HashMap, 里面的每個元素都必須被AIDL支持
  • Parcelable: 所有實現了Parcelable接口的對象
  • AIDL: 所有的AIDL接口本身也可以在AIDL文件中使用

這里請注意,上面支持類型中Parcelable和AIDL比較特殊,自定義的Parcelable對象和AIDL對象必須要顯示的import引入, 這是AIDL的規范需要遵循, 如下Book類


import com.szysky.note.androiddevseek_02.aidl.Book;//必須Book的全限定名

interface IBookManager {

   List<Book> getBookList();

   void addBook(in Book book);  //這里標明輸入型

}

如果用到了自定義對象實現了Parcelable那么就需要創建一個同名的aidl文件

package com.szysky.note.androiddevseek_02.aidl;

parcelable Book;

AIDL中除了基本數據類型外,其他類型的參數必須標上方向out , in, inout.分別表示輸入,輸出,輸入輸出型. 按需而定可以節省不必要的操作在底層實現的開銷. 最后一點AIDL接口中只支持方法,不支持聲明靜態常量.

在服務端用了CopyOnWriteArrayList數組來保存所有書籍. 這個集合的特性是支持并發讀寫. 在說Binder的時候提到過, AIDL方法是在服務端Binder線程池中執行的, 所以當多個客戶端同時連接,會存在多線程并發的問題. 所以使用CopyOnWriteArrayList集合可以進行自動的線程同步.與之相似的還有ConcurrentHashMap這個在LRU機制中使用到過

這里有知識點. 之前說過AIDL中能過使用的只有ArrayList. 而CopyOnWriteArrayList也并不是ArrayList的子類. 其實AIDL所支持的是抽象的List, 而List只是一個接口, 雖然服務端返回的是CopyOnWriteArrayList,但是在Binder中會按照List的規范去訪問數據并最終形成一個新的ArrayList傳遞給客戶端.

看下面的log圖在客戶端接收返回的CopyOnWriteArrayList實際上是ArrayList類型

image

git倉庫的代碼的aidl包中 最后保存的是實現了客戶端和服務端的觀察者模式(可以通過git版本切換之前代碼), 通過客戶端注冊監聽接口,
在服務端每當有新書來的時候,通知已經注冊了的客戶端.

需要注意的幾點

線程問題

當有新書的時候,服務端回調的是客戶端實現的接口里面的方法. 這個方法實際是在客戶端的線程池中執行的. 所以要處理處理UI的問題, 解決方案可以創建一個Handler,將其切換到客戶端的主線程中

private INewBookArrivedListener mNewBookListener = new INewBookArrivedListener.Stub() {

        @Override

        public void onNewBookArrived(Book book) throws RemoteException {

            // 如果有新書 那么此方法會被回調,  并且由于調用處服務端的Binder線程池, 所以給主線程的Handler發送消息,以切換線程

            mhandler.obtainMessage(NEW_BOOK_ARRIVED, book).sendToTarget();

        }

    };

對象不一致,導致接觸綁定失敗

服務端不能再用CopyOnWriteArrayList來記錄綁定過的客戶端. 因為這里一定要清楚對象是不能跨進程的當我們客戶端注冊監聽傳入一個監聽對象到服務端, 在解綁的時候再次傳入一個進行判斷與注冊時相同的對象時刪除達到解除綁定效果時是無效的. 因為服務端在注冊和解綁的時候是兩個反序列化的對象完全不一致.

RemoteCallbackList是系統專門提供的用于刪除跨進程listener的接口. 接收的是一個泛型,支持管理任意的AIDL接口,從聲明就可以看出, 因為AIDL接口都繼承IInterface

內部實現是一個Map結構 key是IBinder類型, value是Callback類型.


ArrayMap<IBinder, Callback> mCallbacks= new ArrayMap<IBinder, Callback>();

IBinder key = listener.asBinder();

Callback value = new Callback(listener, cookie);    //這里的Callback封裝了真正的監聽對象

不管是注冊還是解注冊,多進程到服務端都會生成不同的對象. 但是這些不同的對象有一個共同點, 底層的Binder對象是同一個, 利用這個特性可解決上面的問題.

RemoteCallbackList 當客戶端進程終止后, 它能夠自動移出客戶端所注冊的listener. 并且內部實現了線程同步的功能, 所以在注冊和解注冊的時候不需要做額外的線程工作.

在使用的使用,雖然名字有List但是他并不是一個List我們要遍歷的通知監聽者的時候,要使用bigenBroadcastfinishBroadcase成對出現.

//遍歷集合  去調用客戶端方法

int N = mListeners.beginBroadcast();

for (int i = 0; i<N; i++){

  INewBookArrivedListener listener = mListeners.getBroadcastItem(i);

  if (listener != null){

      listener.onNewBookArrived(newBook);

  }

}

mListeners.finishBroadcast();

當客戶端調用遠程服務的方法,被調用的方法運行在服務端的Binder線程池中,同時客戶端會被掛起, 所以你如果在主線程(客戶端的onServiceConnectedonServiceDisconnected就是UI線程)調用服務端的耗時方法, 你多點幾次就很容易出現ANR. 比方說在服務端的getBookList()睡上十秒,可以復現ANR.

監聽死亡狀態: 在整理Binder的時候有說了一種DeathRecipient的方式,下兩種都可以

  • onServiceDisconnected() UI線程被回調
  • binderDied() 在客戶端的Binder線程池中被回調

還記得在綁定的時候bindService(intent,mConnection, Context.BIND_AUTO_CREATE);其中參數3如果設置這個模式, 當服務或線程死亡,還會重新啟動的.

權限驗證

  1. 在服務端的onBinder()回調中判斷權限.
  2. 在服務端實現的AIDL接口中的onTransact()進行包名判斷或者權限

第一種:

先清單文件中注冊一個自定義的權限

<permission

        android:name="com.szysky.permission.ACCESS_BOOK_SERVICE"

        android:protectionLevel="normal"/>

在清單文件中添加這個權限的使用資格

<uses-permission android:name="com.szysky.permission.ACCESS_BOOK_SERVICE"/>

然后在onBinder()進行判斷,如果沒有那么就返回null, 這樣客戶端是無法綁定服務的

public IBinder onBind(Intent intent) {

    //做一下權限的驗證  在清單文件中聲明了一個,  并添加了使用權限

    int check = checkCallingOrSelfPermission("com.szysky.permission.ACCESS_BOOK_SERVICE");

    if (check == PackageManager.PERMISSION_DENIED){

      return  null;

    }

    return mBinder;

}

第二種

可以判斷客戶端的包名是否滿足我們的需求,這里用com.szysky開頭為例. 如果不符合方法返回false.那么調用服務的方法也會失效


@Override

public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {

  String packageName = null;

  String[] packagesForUid = getPackageManager().getPackagesForUid(getCallingUid());

//下面是獲得客戶端的包名

  if (packagesForUid != null && packagesForUid.length >0){

      packageName = packagesForUid[0];

  }

  Log.e(TAG, "onTransact: -----------------------------" + packageName);

  if (!packageName.startsWith("com.szysky")){

      return false;

  }

  return super.onTransact(code, data, reply, flags);

}

使用ContentProvider"

相關代碼在倉庫項目的java路徑下的provider包中

ContentProvider是Android提供專門用于不用應用進行數據共享的方式. 它的底層同樣也是Binder. 因為系統封裝, 所以它的使用比起AIDL要簡單很多.

要實現一個內容提供者, 只需要寫一個類繼承ContentProvider,并復寫六個抽象方法. 其中有四個是CURD操作方法. 一個onCreate()用來做初始化. 一個getType()用來返回一個Uri請求所對應的MIME類型,比如圖片還是視頻等. 如果我們不關心那么可是直接返回NULL或者*/*.

這六個方法根據Binder工作原理,都是運行在ContentProvider的進程中. 除了onCreate()是被系統回調運行在主線程, 其余的都在Binder的線程池中.

主要存儲方式是表格的形式, 也可以支持文件格式,例如圖片視頻, 可以返回這類文件的句柄給外界來訪問ContentProvider中的文件信息.

<provider

           android:authorities="com.szysky.note.androiddevseek_02.provider"

           android:name=".provider.BookProvider"

           android:permission="com.szysky.PROVIDER"/>

  • authorities: 后面的值是指定這個ContentProvider的唯一標識.
  • permission: 添加一個權限認證, 對于訪問者必須添加了這個使用權限的聲明.

查詢的時候通過Uriauthorities聲明值得解析就可以找到對應的ContentProvider

Uri uri = Uri.parse("content://com.szysky.note.androiddevseek_02.provider");

getContentResolver().query(uri, null, null, null, null);

為了后續操作, 這里利用SQLiteOpenHelper來管理數據庫,并創建兩個表userbook,代碼在倉庫有,這里不寫實現過程.

由于有兩個表支持被訪問, 所以應該為每一個不同的表設定單獨的Uri和Uri_Code 并將其關聯. 這樣外界訪問的時候可以根據Uri得到Uri_Code. 也就在ContentProvider知道要處理的具體事件.

在新建的ContentProvider類中進行關聯, 如下


private static final String AUTHORITY = "com.szysky.note.androiddevseek_02.provider";

    /**

     * 指定兩個操作的Uri

     */

    private static final Uri BOOK_CONTENT_URI = Uri.parse("content://" +AUTHORITY + "/book");

    private static final Uri USER_CONTENT_URI = Uri.parse("content://" +AUTHORITY + "/user");

    /**

     * 創建Uri對應的Uri_Code

     */

    private static final int BOOK_URI_CODE = 1;

    private static final int USER_URI_CODE = 2;

    /**

     * 創建一個管理Uri和Uri_Code的對象

     */

    private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

    static {

        //進行關聯

        sUriMatcher.addURI(AUTHORITY, "book",BOOK_URI_CODE);

        sUriMatcher.addURI(AUTHORITY, "user",USER_URI_CODE);

    }

針對query方法進行演示,其他三個類似,代碼有全部實現的例子, 在自定義Provider文件中.

/**

* 通過自動以的Uri來判斷對應的數據庫表名

*/

private String getTableName(Uri uri){

   String tableName = null;

   switch (sUriMatcher.match(uri)){

       case BOOK_URI_CODE:

           tableName = DbHelper.BOOK_TABLE_NAME;

           break;

       case USER_URI_CODE:

           tableName = DbHelper.USER_TABLE_NAME;

           break;

       default:break;

   }

   return tableName;

}

/**

 *   在query中, 獲取到Uri傳入要查詢的具體表名, 使用SQLiteOpenHelper來進行query的查詢,并把結果返回

 */

public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {

        //獲取表名

        String tableName = getTableName(uri);

        if (tableName == null)

            throw new IllegalArgumentException("不被支持的Uri參數-->"+uri );

        return mDb.query(tableName, projection, selection, selectionArgs, null, null, sortOrder,null);

    }

如果需要監聽Provider內容的變化, 那么可以在Provider中update, delete, insert. 中操作完數據庫之后. 使用getContentResolver().notifyChange(uri,null);來通知外界當前ContentProvider中數據已經發生改變. 而外部要想觀察其變化. 使用ContentResolverrigisterContentObserver方法來注冊觀察者.

線程安全問題

如果只有一個SQLiteDataBase對象被使用, 那么增刪改查不會出現線程安全問題, 因為其內部對數據庫的操作是有同步處理. 但是如果多個SQLiteDataBase對象來操作數據庫就無法保證其線程安全. 這個時候就要注意了.

使用Socket

Socket也稱為套接字. 是網絡通信中的概念, 它分為流式套接字和用戶數據包套接字兩種. 分別對應于網絡的傳輸控制層中TCP和UDP協議.

使用Socket通信, 需要在清單文件添加權限的申請

<!--Socket通信額外需要的線程-->

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

<uses-permission android:name="android.permission.INTERNET"/>

服務端 需要啟動服務, 并且在線程中建立TCP服務, 然后監聽一個端口. 當客戶端建立連接的時候就會生成一個Socket流. 可以保持后續的持續通信. 每一個客戶端都對應一個Socket. 如果客戶端斷開連接. 服務端需要做好相應的Socket流關閉并結束通話線程. 可以通過在客戶端斷開的時候服務端的接收字節流會是null來判斷連接是否還存活.

首先需要開啟一個新線程, Runnable接口這樣實現, 以下是偽代碼, 沒有捕捉異常

// 監聽 3333 端口

 ServerSocket serverSocket = new ServerSocket(3333);

// 判斷服務是否斷開 沒有斷開就繼續監聽端口 

  while (!mIsServiceDestoryed) {

        //這個是阻塞方法, 當有新的客戶端連接,才會返回Socket值

        final Socket accept = serverSocket.accept();

        // 有了新的客戶端 那就需要創建一個新的線程去維護

        new Thread() {

            public void run() {

                //  這里做對一個Socket的具體操作

                responseClient(accept);

            }}.start();

  }

private void responseClient(Socket client) throws IOException {

        //接收客戶端消息

        BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));

        //發送到客戶端 , 設置true參數就不需要手動的刷新輸出流

        PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(client.getOutputStream())),true);

        out.println("歡迎來到直播間");

        //判斷服務標志是否銷毀, 沒有銷毀那么就一直監聽此鏈接的Socket流

        while (!mIsServiceDestoryed) {

            String str = in.readLine(); //這是一個阻塞方法

            //判斷如果取出來的是null,那么就說明連接已經斷開

            if (str == null)

                break;

            // 對客戶端進行回復

            out.println("我是回復消息");

        }

        //準備關閉一系列的流

        ......

    }

最后就是要在onDestroy()中把循環中判斷服務存活的標識置為false, 讓開啟的線程都能自動走完關閉.

客戶端

onCreate()先startService開啟TCP的服務, 然后開啟一個線程準備連接Socket. 可以加上失敗重連的的機制. 只要獲取到了Socket, 就和之前服務端一樣獲取輸入輸出流進行對應的操作.

貼出客戶端的核心代碼, Runnable接口實現的 ,同樣是偽代碼

Socket socket = null;

// 試圖連接服務器, 如果失敗休眠一秒重試

while(socket == null){

  try {

      // 如果可以連接 3333 端口成功那么socket就不為null, 此循環也就結束

      socket = new Socket("localhost", 3333);

      mClientSocket = socket;

      // 獲得輸出流, 并設置true,自動刷新輸出流里面的內容

      mPrintWrite = new PrintWriter(new BufferedWriter(new OutputStreamWriter(mClientSocket.getOutputStream())),true );

  } catch (IOException e) {

      SystemClock.sleep(1000);

      e.printStackTrace();

  }

}

//準備接收服務器的消息.

 BufferedReader in = new BufferedReader(new InputStreamReader(mClientSocket.getInputStream()));

  //獲得了socket流的讀入段  只要activity不關閉一直循環讀

  while(!SocketActivity.this.isFinishing()){

      // readLine()同樣也是阻塞方法

      String strLine = in.readLine();

      if (strLine != null){

          //TODO 獲取到了服務端發來的數據, 做一些事情

          //....

      }

  }

//走到這里 說明界面已經不存在, 進行掃尾動作

in.close();

mPrintWrite.close();

socket.close();

demo圖例Socket相關代碼存在倉庫的socket包中

image

各種IPC的差異以及選擇

名稱 優點 缺點 使用場景
Bundle 簡單易用 只能傳輸Bundle支持的數據類型 四大組件間的進程間通信
文件共享 簡單易用 不適合高并發場景,并且無法做到進程間的即時通 無法并發訪問情形, 交換簡單的數據實時性不高的場景
AIDL 功能強大 使用稍復雜,需要處理好線程同步 一對多通信且有RPC需求
ContentProvider 在數據源訪問方面功能強大,支持一對多并發數據共享 可以理解為受約束的AIDL,主要提供數據源的CRUD操作 一對多的進程間的數據共享
Messenger 功能一般, 支持一對多串行通信,支持實時通信 不能很好處理高并發,不支持RPC,數據通過Message進行傳輸, 因此只能傳輸Bundle支持的數據類型 低并發的一對多即時通信,無RPC需求,或者無需要返回結果的RPC需求
Socket 功能強大,可以通過網絡傳輸字節流,支持一對多并發實時通信 實現細節稍微有點繁瑣,不支持直接的 網絡數據交換

參看文章

《Android 開發藝術探索》書集
《Android 開發藝術探索》 02-IPC機制
https://github.com/feiwodev/AndroidDevelopmentArt

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,412評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,514評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,373評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,975評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,743評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,199評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,262評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,414評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,951評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,780評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,527評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,218評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,649評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,889評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,673評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,967評論 2 374