Android程式間的通訊

AND_YOU_with_ME發表於2017-04-17

1. 多程式使用場景

1) 應用某些模組因為特殊需求需要執行在單獨程式中。如訊息推送,使訊息推送程式與應用程式能單獨存活,訊息推送程式不會因為應用程式程式crash而受影響。
2) 為加大一個應用可使用的記憶體,需要多程式來獲取多份記憶體空間。

2. 如何開啟多程式

給四大元件(Activity、Service、Receiver、ContentProvider)在AndroidMainfest中指定Android:process屬性指定。

  • 如果程式以”:”開頭的程式,代表應用的私有程式,其他應用的元件不可以和它跑在同一個程式中;
  • 程式不以”:”開頭的程式屬於全域性程式,其他應用可通過shareUID可以和它跑在同一個程式中。
  • 若兩個不同應用設定了相同的shareUID,它們之間想共享資料,還需要有相同的簽名才可以。

3. 多程式會造成的問題

(1) 靜態成員和單例失效,資料同步失敗;
Android會為每一個應用/每一個程式分配一個獨立的虛擬機器,不同虛擬機器在記憶體分配上有不同的地址空間,這就導致不同虛擬機器中訪問同一個類物件會產生多個副本。
(2) 執行緒同步機制失效;
因為不同程式不是同一塊記憶體,不同程式鎖的不是同一物件。
(3) SharedPreferences可靠性下降;
SharedPreferences底層是通過讀/寫XML檔案實現的,併發寫可能會出問題,所以它不支援多個程式同時去執行寫操作,否則會導致一定機率的資料丟失。
(4) Application會建立多次;
當一個元件跑在一個新程式中,系統會給它重新分配獨立虛擬機器,這其實就是啟動一個應用的過程,故執行在不同程式中的元件屬於不同的虛擬機器和不同的Application。

4. 資料序列化

Intent和Binder傳輸資料時,或是物件持久化轉存/通過網路傳輸給其他客戶端時,需要使用Parcelable或Serializable將物件轉換成可以傳輸的形式。
(1) Serializable介面
Serializable是Java提供的一個序列化介面,它是一個空介面,想要某個類實現序列化,只需要相應類實現Serializable介面即可。Serializable是藉助ObjectOutputStream和ObjectInputStream實現物件的序列化和反序列化
一般在相應類實現Serializable類中還會定義一個final long型的serialVersionUID,不用它也能實現物件的序列化,它是用來輔助反序列化的。序列化時會把當前類的serialVersionUID寫入序列化檔案中,當反序列化系統會檢測檔案中的serialVersionUID,看它是否和當前類的serialVersionUID一致,如果一致說明序列化的類的版本和當前類的版本相同,這時可反序化成功;否則說明當前類和序列化的類相比發生了變換,如增加或減少某個成員變數,這時無法正常反序列化。
(2) Parcelable介面
在序列化過程中需要實現的功能有:
1) 序列化:由writeToParcel方法完成,最終通過Parcel中的一系列的write方法完成;
2) 反序列化:由CREATOR完成,內部標識瞭如何建立序列化物件和陣列,最終通過Parcel的一系列read方法來完成反序列化;
3) 內容描述符:由describeContents方法來完成,幾乎所有情況下該方法都返回0,僅噹噹前物件中存在檔案描述符時返回1。
(3)兩者區別
Serializable是Java中序列化介面,使用簡單但開銷大,序列和反序列化需要大量I/O操作;
Parcelable是Android中特有的序列化介面,使用複雜但開銷小,效率高,是Android推薦的序列化方法;
Parcelable主要用在記憶體序列化上,如果將物件序列化到儲存裝置或將物件序列化後通過網路傳輸,過程會比較複雜,建議使用Serializable。

5. Binder

Binder是什麼?

  • Binder是Android中的一個類,它繼承了IBinder介面;
  • 從IPC角度來說,Binder是Android中一種跨程式通訊的方式;
  • Binder還可以理解為一種虛擬的物理裝置,它的裝置驅動是/dev/binder;
  • 從AndroidFramework角度來說,Binder是ServiceManager連線各種Manager(ActivityManager, WindowManager)和相應ManagerService的橋樑;
  • 從Android應用層來說,Binder是客戶端和服務端進行通訊的媒介,當bindService時,服務端會返回一個包含了服務端業務的Binder物件,通過這個Binder物件,客戶端就可以獲取服務端提供的服務或資料,這裡的服務包括普通服務和基於AIDL的服務。

6.android中跨程式通訊方式

6.1 結合Bundle使用Intent

在一個程式中啟動另一個程式的Activity, Service, Receiver元件時,可以使用Bundle附加上需要傳遞的訊息給遠端程式,並通過Intent傳送出去。

6.2 使用檔案共享

兩個程式通過讀/寫同一個檔案來交換資料,還可以序列化一個物件到檔案系統中,從另一個程式中恢復這個物件。它的實現原理是通過ObjectOutputStream將檔案寫入檔案中,再通過ObjectInputSteam將檔案恢復。通過檔案共享的方式有一定侷限性,如併發讀/寫,讀出的內容可能不是最新的,併發寫就可能導致資料混亂。因此儘量避免併發寫這種操作,或考慮用執行緒同步來限制多個執行緒的寫操作。檔案共享適合在對資料要求不高的程式之間通訊。
SharedPreferences是Android提供的一種輕量級儲存方案,它通過鍵值對方式儲存資料,底層它是採用XML來儲存鍵值對。由於系統對SharedPreferences的讀寫有一定的快取策略,即在記憶體中會有一份SharedPreferences檔案的快取,在多程式模式下,系統對它的讀/寫就變得不可靠,當面對高併發讀/寫訪問時,SharedPreferences會有很大機率會丟失資料。因此不建議在程式間通訊中使用SharedPreferences。

6.3 使用Messenger

Messenager可以在不同程式中傳遞Message物件,在Message中放入我們要傳遞的資料。它的底層實現是AIDL,下面是Messenger的兩個構造方法:


public Messenger(Handler target) {
        mTarget = target.getIMessenger();
    }
public Messenger(IBinder target) {
        mTarget = IMessenger.Stub.asInterface(target);
    }

不管是IMessenger還是Stub.asInterface,這種使用方法都表明它的底層是AIDL,它一次只處理一個請求,不存線上程同步問題。
實現步驟:
(1) 服務端程式
1) 定義一個Service用於客戶端的繫結,建立一個Handler,在handleMessage裡處理客戶端傳送過來的訊息。

//用ServiceHandler接收並處理來自於客戶端的訊息
    private class ServiceHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            if(msg.what == RECEIVE_MESSAGE_CODE){
                Bundle data = msg.getData();
                if(data != null){
                    String str = data.getString("msg");
                }
                //通過Message的replyTo獲取到客戶端自身的Messenger,
                //Service可以通過它向客戶端傳送訊息
                clientMessenger = msg.replyTo;
                if(clientMessenger != null){
                    Message msgToClient = Message.obtain();
                    msgToClient.what = SEND_MESSAGE_CODE;
                    //可以通過Bundle傳送跨程式的資訊
                    Bundle bundle = new Bundle();
                    bundle.putString("msg", "你好,客戶端,我是MyService");
                    msgToClient.setData(bundle);
                    try{
                        clientMessenger.send(msgToClient);
                    }catch (RemoteException e){
                        e.printStackTrace();
                        Log.e("DemoLog", "向客戶端傳送資訊失敗: " + e.getMessage());
                    }
                }
            }
        }
    }

2) 通過Handler建立一個Messenger物件

//serviceMessenger是Service自身的Messenger,其內部指向了ServiceHandler的例項
    //客戶端可以通過IBinder構建Service端的Messenger,從而向Service傳送訊息,
    //並由ServiceHandler接收並處理來自於客戶端的訊息
    private Messenger serviceMessenger = new Messenger(new ServiceHandler());
3) 在Service的onBind中通過Messenger.getBinder()返回底層的Binder物件。

@Override
    public IBinder onBind(Intent intent) {
        Log.i("DemoLog", "MyServivce -> onBind");
        //獲取Service自身Messenger所對應的IBinder,並將其傳送共享給所有客戶端
        return serviceMessenger.getBinder();
    }
4) 註冊服務,讓其執行在單獨程式中

<service
    android:name=".MyService"
    android:enabled="true"
    android:process=":remote" >
</service>
(2) 客戶端程式
1) 繫結服務端的Service

private ServiceConnection conn = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder binder) {
            //客戶端與Service建立連線
            Log.i("DemoLog", "客戶端 onServiceConnected");

            //我們可以通過從Service的onBind方法中返回的IBinder初始化一個指向Service端的Messenger
            serviceMessenger = new Messenger(binder);
            isBound = true;

            Message msg = Message.obtain();
            msg.what = SEND_MESSAGE_CODE;

            //此處跨程式Message通訊不能將msg.obj設定為non-Parcelable的物件,應該使用Bundle
            //msg.obj = "你好,MyService,我是客戶端";
            Bundle data = new Bundle();
            data.putString("msg", "你好,MyService,我是客戶端");
            msg.setData(data);

            //需要將Message的replyTo設定為客戶端的clientMessenger,
            //以便Service可以通過它向客戶端傳送訊息
            msg.replyTo = clientMessenger;
            try {
                Log.i("DemoLog", "客戶端向service傳送資訊");
                serviceMessenger.send(msg);
            } catch (RemoteException e) {
                e.printStackTrace();
                Log.i("DemoLog", "客戶端向service傳送訊息失敗: " + e.getMessage());
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            //客戶端與Service失去連線
            serviceMessenger = null;
            isBound = false;
            Log.i("DemoLog", "客戶端 onServiceDisconnected");
        }
    };

    Intent intent = new Intent();            
    ComponentName componentName = new ComponentName(packageName, serviceNmae);
    intent.setComponent(componentName);
    try{
        Log.i("DemoLog", "客戶端呼叫bindService方法");
        bindService(intent, conn, BIND_AUTO_CREATE);
    }catch(Exception e){
        e.printStackTrace();
        Log.e("DemoLog", e.getMessage());
    }
}

繫結成功後用服務端返回的IBinder物件建立一個Messenger,通過它向服務端傳送Message訊息。
如果需要服務端給客戶端傳送訊息,需要在Handler的handleMessage方法裡,根據客戶端傳送過來的Message.replyTo獲取到客戶端的Messenger物件,就可以向客戶端傳送訊息了。同時客戶端需要在服務連線的onServiceConnected方法中,將客戶端的Messenger物件通過Message.replyTo給收到服務端傳送過來的Message物件,這樣就實現雙方通訊。

6.4 AIDL

6.4.1 在服務端新建需要的AIDL類

(1)先新建一個Book.java實現Parcelable介面,表示一個圖書的資訊類;

import android.os.Parcel;
import android.os.Parcelable;

/**
 * Book.java
 */
public class Book implements Parcelable{
    private String name;
    private int price;
    public Book(){}
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    public Book(Parcel in) {
        name = in.readString();
        price = in.readInt();
    }

    public static final Creator<Book> CREATOR = new Creator<Book>() {
        @Override
        public Book createFromParcel(Parcel in) {
            return new Book(in);
        }

        @Override
        public Book[] newArray(int size) {
            return new Book[size];
        }
    };

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(name);
        dest.writeInt(price);
    }

    /**
     * 引數是一個Parcel,用它來儲存與傳輸資料
     * @param dest
     */
    public void readFromParcel(Parcel dest) {
        //注意,此處的讀值順序應當是和writeToParcel()方法中一致的
        name = dest.readString();
        price = dest.readInt();
    }

    //方便列印資料
    @Override
    public String toString() {
        return "name : " + name + " , price : " + price;
    }
}

(2) 新建一個Book.aidl,表示Book類在AIDL中的宣告;

// Book.aidl
//這個檔案的作用是引入了一個序列化物件 Book 供其他的AIDL檔案使用
//注意:Book.aidl與Book.java的包名應當是一樣的
//注意parcelable是小寫
parcelable Book;
(3) 新建一個IBookManger.aidl介面,裡面包含相應的方法;

// IBookManger.aidl
//匯入所需要使用的非預設支援資料型別的包
import com.lypeer.ipcclient.Book;

interface IBookManger {

    //所有的返回值前都不需要加任何東西,不管是什麼資料型別
    List<Book> getBooks();
    Book getBook();
}
該方法儲存後,會在gen目錄下生成一個IBookManger.java類,它是系統為BookManger.aidl生成的Binder類,繼承android.os.IInterface,它自己也還是個介面。
它包括以下內容:
1) 定義了一個String型的DESCRIPTOR,Binder的唯一標識;
2) asInterface(android.os.IBinder obj)方法,將服務端的Binder物件轉換成客戶端所需的AIDL介面型別物件,當客戶端和服務端處於同一程式,直接返回服務端的Stub物件本身,否則返回系統封裝後的Stub.proxy物件。
3) asBinder方法:返回當前Binder物件。
4) onTransact方法:執行在服務端的Binder執行緒池中,當客戶端發起遠端請求,遠端請求會通過系統封裝後交由此方法處理,該方法原型為public Boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags),服務端通過code可以確定客戶端請求的是哪一個方法,從data中取出目標方法所需的引數,執行相應方法後,將返回值寫入reply中,如果此方法返回false,客戶端會請求失敗。
5) IBookManger.aidl介面中宣告的方法的代理實現,此方法執行在客戶端,當客戶端呼叫此方法時,首先需要建立此方法需要的輸入型別Parcel物件_data, 輸出型別Parcel物件_reply和返回值物件,接著將引數資訊寫入_data中(若有引數的話),再呼叫transact方法發起RPC(遠端過程呼叫),當前執行緒掛起,服務端的onTransact方法會被呼叫,直到RPC過程返回後,當前執行緒繼續執行,並從_reply中取出RPC過程的返回結果;最後返回_reply中的資料。
6) 宣告瞭IBookManger.aidl中宣告的方法;
7) 宣告瞭相應的整型id分別用於標識宣告的方法,該id用於標識在transact過程中判斷客戶端請求的到底是哪個方法;
8) 宣告瞭一個類部類Stub,繼承android.os.Binder實現IBookManager.aidl中類介面,當客戶端和服務端處於同一程式,方法呼叫不會走跨程式的transact過程,若位於不同程式,則由Stub的內部代理Proxy,呼叫跨程式transact過程。
需要注意的是:客戶端發起請求時,當前執行緒會掛起直至服務端返回資料,若方法是一個耗時操作,不能在UI執行緒中發起此遠端請求。服務端的Binder方法執行在Binder執行緒池中,所以不管操作是否耗時,都應採用同步的方法去實現。
(4)linkToDeath unlinkToDeath
由於Binder執行在服務端程式中,如果服務端程式由於某種原因終止,這時客戶端服務端的Binder連線斷裂,會導致遠端呼叫失敗。但客戶端很可能不知道,解決此問題的辦法,利用Binder提供的兩個配對方法linkToDeath和unlinkToDeath,通過linkToDeath可以給Binder設定一個死亡代理,當Binder被終止時,我們會接收到通知,這時可通過重新發起請求恢復連線。
實現方法:
1) 先宣告一個DeathRecipient物件,它是一個介面,內部只有一個方法binderDied,我們需要實現該方法,當Binder死亡時,系統回撥binderDied方法,我們可以移出之前binder代理並重新建立遠端服務。
private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
        @Override
        public void binderDied() {
            if (mBookManager == null) {
                return;
            }
            mBookManger.asBinder().unlinkToDeath(mDeathRecipient, 0);
            mBookManger = null;
            //重新繫結遠端服務
        }
    };
2) 在客戶端繫結遠端服務成功後,給binder設定死亡代理;

mService = IBookManager.Stub.asInterface(binder);
binder.linkToDeath(mDeathRecipient, 0);

6.4.2 遠端服務端Serivce的實現

(1)Serivce建立
我們需要新建一個Service,稱為AIDLService,程式碼如下:

/**
 1. 服務端的AIDLService.java
 */
public class AIDLService extends Service {

    public final String TAG = this.getClass().getSimpleName();

    //包含Book物件的list
    private List<Book> mBooks = new ArrayList<>();

    //由AIDL檔案生成的IBookManager
    private final IBookManager.Stub mBookManager = new IBookManager.Stub() {
        @Override
        public List<Book> getBooks() throws RemoteException {
            synchronized (this) {
                Log.e(TAG, "invoking getBooks() method , now the list is : " + mBooks.toString());
                if (mBooks != null) {
                    return mBooks;
                }
                return new ArrayList<>();
            }
        }


        @Override
        public void addBook(Book book) throws RemoteException {
            synchronized (this) {
                if (mBooks == null) {
                    mBooks = new ArrayList<>();
                }
                if (book == null) {
                    Log.e(TAG, "Book is null in In");
                    book = new Book();
                }
                //嘗試修改book的引數,主要是為了觀察其到客戶端的反饋
                book.setPrice(2333);
                if (!mBooks.contains(book)) {
                    mBooks.add(book);
                }
                //列印mBooks列表,觀察客戶端傳過來的值
                Log.e(TAG, "invoking addBooks() method , now the list is : " + mBooks.toString());
            }
        }
    };

    @Override
    public void onCreate() {
        super.onCreate();
        Book book = new Book();
        book.setName("Android開發藝術探索");
        book.setPrice(28);
        mBooks.add(book);   
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        Log.e(getClass().getSimpleName(), String.format("on bind,intent = %s", intent.toString()));
        return mBookManager;//在onBinder中返回服務端的Binder物件
    }
}

(2)註冊服務

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

6.4.3 客戶端的實現

/**
 2. 客戶端的AIDLActivity.java
 */
public class AIDLActivity extends AppCompatActivity {

    //由AIDL檔案生成的Java類
    private IBookManager mBookManager = null;

    //標誌當前與服務端連線狀況的布林值,false為未連線,true為連線中
    private boolean mBound = false;

    //包含Book物件的list
    private List<Book> mBooks;

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

    /**
     * 按鈕的點選事件,點選之後呼叫服務端的addBookIn方法
     *
     * @param view
     */
    public void addBook(View view) {
        //如果與服務端的連線處於未連線狀態,則嘗試連線
        if (!mBound) {
            attemptToBindService();
            Toast.makeText(this, "當前與服務端處於未連線狀態,正在嘗試重連,請稍後再試", Toast.LENGTH_SHORT).show();
            return;
        }
        if (mBookManager == null) return;

        Book book = new Book();
        book.setName("APP研發錄In");
        book.setPrice(30);
        try {
            mBookManager.addBook(book);//通過服務端的Binder物件調服務端方法
            Log.e(getLocalClassName(), book.toString());
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    /**
     * 嘗試與服務端建立連線
     */
    private void attemptToBindService() {
        Intent intent = new Intent();
        intent.setAction("aidl.AIDLService");
        intent.setPackage("com.xx.packagename");
        bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);//繫結服務
    }

    @Override
    protected void onStart() {
        super.onStart();
        if (!mBound) {
            attemptToBindService();
        }
    }

    @Override
    protected void onStop() {
        super.onStop();
        if (mBound) {
            unbindService(mServiceConnection);
            mBound = false;
        }
    }

    private ServiceConnection mServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            Log.e(getLocalClassName(), "service connected");
            mBookManager = IBookManager.Stub.asInterface(service);
            mBound = true;

            if (mBookManager != null) {
                try {
                    mBooks = mBookManager.getBooks();//呼叫服務端的getBooks方法
                    Log.e(getLocalClassName(), mBooks.toString());
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            Log.e(getLocalClassName(), "service disconnected");
            mBound = false;
        }
    };
}

通過獲取服務端的Binder物件,就可以和服務端進行通訊了。

6.4.4 其他知識點

(1)跨程式listener介面
如果在服務端定宣告瞭介面,客戶端進行註冊和反註冊,會丟擲異常,無法反註冊,因為在多程式中,Binder會把客戶端傳過來的物件重新轉化成一個新物件,這樣雖然客戶端註冊和反註冊使用的是同一個物件,但是通過Binder傳遞到服務端卻變成兩個物件,物件的跨程式傳輸本質都是反序列化過程。
可以使用RemoteCallbackList,它是系統專門用來刪除跨程式listener介面。它實現原理利用底層Binder物件是同一個,只要遍歷服務端所有listener,找出和解註冊listener具有相同Binder物件的服務端listener,並把它刪除掉。同時RemoteCallbackList還有一個作用,當客戶端程式終止,它會移除客戶端所註冊的所有listener.
(2)許可權驗證
在註冊服務時新增permission許可權驗證,或是在onTransact方法中進行許可權驗證;
(3) AIDL訪問流程總結:
先建立一個Service和一個AIDL介面,再建立一個類繼承自AIDL介面中的Stub類並實現Stub中的抽象方法;在Service的onBind方法中返回這個物件,再在客戶端就可以繫結服務端service,建立連線後就可以訪問遠端服務端的方法了。

6.5 ContentProvider

和Messenger一樣,ContentProvider底層同樣是Binder.系統預置了很多ContentProvider,如通訊錄、日程表等,只需通過ContentResolver的query/update/insert/delete就可以跨程式訪問這些資訊。具體實現步驟如下:
1) 新建一個繼承自系統ContentProvider的Provider,並重寫onCreate, query, getType, insert, delete, update六個方法。
這六個程式執行在ContentProvider的程式中,除了onCreate由系統回撥執行在主執行緒中,其他五個方法執行在Binder執行緒池中。
2) 註冊Provider

<provider android:name=".provider.XXProvider"
        android:authorities="com.xx.xx.provider"
        android:permission="com.xx.PROVIDER"
        android:process=":provider">
</provider>
3) 在另一個程式中訪問我們定義的ContentProvider

Uri uri = Uri.parse("content://com.xx.xx.provider");
getContentResolver().query(uri, null, null, null, null);
getContentResolver().query(uri,)

4) ContentProvider資料來源發生變化時,可通過ContentResolver的notifyChange方法來通知外界資料發生改變,外界可通過ContentResolver的registerContentObserver方法來註冊觀察者,通過unregisterContentObserver方法來解除觀察者。

6.6 Socket

Socket分為流式套接字和使用者資料包套接字,分別是對應於網路的傳輸控制層中的TCP/UDP
TCP:面向連線的協議,穩定的雙向通訊功能,連線需要“三次握手”,提供了超時重傳機制,具有很高的穩定性;
UDP:面向無連線協議,不穩定的單向通訊功能,也可提供雙向通訊功能。效率高,不能保證資料一定能正確傳輸。
實現步驟:
1)服務端在新建一個Service,並建立TCP服務

@Override
    public void onCreate() {
        new Thread(new TcpServer()).start();//阻塞監聽客戶端連線
        super.onCreate();
    }

    @Override
    public IBinder onBind(Intent intent) {
        throw null;
    }

    private class TcpServer implements Runnable {
        @Override
        public void run() {
            ServerSocket serverSocket;
            try {
                //監聽8688埠
                serverSocket = new ServerSocket(8688);
            } catch (IOException e) {
                return;
            }
            while (!isServiceDestroyed) {
                try {
                    // 接受客戶端請求,並且阻塞直到接收到訊息
                    final Socket client = serverSocket.accept();
                    new Thread() {
                        @Override
                        public void run() {
                            try {
                                //有訊息接收到,新開一個執行緒進行處理訊息
                                responseClient(client);
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }.start();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private void responseClient(Socket client) throws IOException {
        // 用於接收客戶端訊息,將客戶端的二進位制資料流格式轉成文字格式
        BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
        // 用於向客戶端傳送訊息
        PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(client.getOutputStream())), true);
        out.println("您好,我是服務端");
        while (!isServiceDestroyed) {
            String str = in.readLine();//通過資料流的readLine,可直接變成文字格式
            Log.i("moon", "收到客戶端發來的資訊" + str);
            if (TextUtils.isEmpty(str)) {
                //客戶端斷開了連線
                Log.i("moon", "客戶端斷開連線");
                break;
            }
            String message = "收到了客戶端的資訊為:" + str;
            // 從客戶端收到的訊息加工再傳送給客戶端
            out.println(message);
        }
        out.close();//關閉流
        in.close();
        client.close();
    }

2)註冊服務
<service
    android:name=".SocketServerService"
    android:process=":remote" />
3) 客戶端先建立連線服務端socket
Socket socket = null;
while (socket == null) {
    try {
        //選擇和伺服器相同的埠8688
        socket = new Socket("localhost", 8688);
        mClientSocket = socket;
        mPrintWriter = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
    } catch (IOException e) {
        SystemClock.sleep(1000);
    }
}

4) 客戶端和服務端通訊,通過while迴圈不斷去讀取伺服器傳送過來的訊息

// 接收伺服器端的訊息
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
while (!isFinishing()) {
    final String msg = br.readLine();
    if (msg != null) {
        runOnUiThread(new Runnable() {
                          @Override
                          public void run() {
                              tv_message.setText(tv_message.getText() + "\n" + "服務端:" + msg);
                          }
                      }
        );
    }
}

5) 客戶端在onCreate中單開執行緒啟動連線服務
Intent service = new Intent(this, SocketServerService.class);
        startService(service);
        new Thread() {
            @Override
            public void run() {
                connectSocketServer();
            }
        }.start();

6.7 程式間通訊方式比較


相關文章