---
title: 'Android IPC 進程 - 通訊方法'
disqus: kyleAlien
---
Android IPC 進程 - 通訊方法
===
## OverView of Content
想了解 IPC 的基本觀念,可以先參考這篇 [**IPC 通訊機制**](https://hackmd.io/jh6jLfzWQZarUjsQOEQZpw?view),這裡就不說明 IPC
我們這裡探討 IPC 通訊在 Android 中的實做方式
[TOC]
## Android IPC 方案
1. **序列化 Serializable/Parcelable 接口**
* Java Serializable:是一個標示接口,代表了該類是可以被序列化、反序列化
* Android 則有令一個特色 Parcelable:它的實現較為麻煩,並且有順序限制,不過這並不影響他的優越之處
* **Bundle** 就實現了 Parcelable 接口,可用於不同進程傳輸
> Activity、Service、Receiver 都是在 Intent 中通過 Bundle 來進行數據傳遞
1. 它可以用來進程間傳遞(同進程也可以)
2. 節省空間在底層是不斷複用的對象
2. **AIDL 方案**
* Messager 工具:它的底層也是 AIDL,不過它經過包裝後更方便使用,**但僅限輕量級 IPC 方案,因為它 ++無法傳輸對象 Object++**
> 可以用來簡單通訊
* ContentProvider:它是四大組件之一,底層也是 AIDL,不過它經過包裝後更方便使用
* AIDL 手動實做:可以用來進行較大量的進程間通訊(實做較為麻煩一些)
3. **文件共享**:將數據除存到外部裝置,給多個進程訪問
* 該方案是超級輕量級 IPC 通訊,不適用於常訪問 IPC 通訊 (速度慢),而且需要配合資源鎖
### Android 開啟多進程
* 四大組件都可以設定運行在不同進程,而設定方式都是透過在 **AndroidManifest 中,Application 的 subtag,中設定 `process` 欄位**
> 這邊我們以 Service 為例,並設定兩個 Service
```xml=
<!-- AndroidMenifest.xml 檔案 -->
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AIDL_Project"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- 設定 process 欄位 -->
<service android:name=".PrivateServer"
android:process=":private.remote"/>
<!-- 設定 process 欄位 -->
<service android:name=".PublicService"
android:process="public.remote"/>
</application>
```
* **process 欄位** 的取名可以隨意,但其中 **有一個特別規則**
1. 以 `:` 開頭取名:**該進程是單獨運行在一個進程,不與其他進程共享資源**
2. 以小寫英文開頭:**該進程運行的進程是可以共享的,可以共享資源**
* 以下來簡單複習一下,啟動 Service、建立 Service 的方式,不會都寫出,在啟動該 Activity 後我們在 Logcat 中觀察 Service 進程建立的情況
```java=
// Service 實作
public class PublicService extends Service {
private final PublicBookLibrary publicBookLibrary = new PublicBookLibrary();
@Nullable
@Override
public IBinder onBind(Intent intent) {
return publicBookLibrary;
}
private static final class PublicBookLibrary extends Binder {
private final Map<Integer, String> map = new HashMap<>();
public String getBook(int id) {
return map.get(id);
}
public void addBook(int id, String name) {
if(map.containsKey(id)) {
return;
}
map.put(id, name);
}
public int librarySize() {
return map.size();
}
}
}
// ---------------------------------------------------------------
// 在 Activity 內啟動 Service
private void bindPublicService() {
bindService(new Intent(this, PublicService.class),
new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.i(TAG, "Success public service: " + name);
}
@Override
public void onServiceDisconnected(ComponentName name) {
Log.i(TAG, "Success public service: " + name);
}
}, BIND_AUTO_CREATE);
}
```
* 以下為 Logcat 中看到所有進程 (有 3 個)
> **Private 進程前會串上該 Project 的名稱,而 Public 進程則不會**
> 
### 多進程 - 問題
* 由於有設定不同進程 (AnroidManifest 組件設定 process),AMS 就會通知 Zygote 進程產生一個新的 Process 進程,並啟動一個新的 Application
> 以下情況,在 MyApplication 設定 Log 並顯示 pid,並啟動另一個進程的 Service
```java=
public class MyApplication extends Application {
private static final String TAG = "TEST123";
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "MyApplication --- onCreate --- Process pid: " + Process.myPid());
}
}
```
* 從結果會看到 **啟動 2 個 Application、1 個 Service**,一個是由 Service 啟動、另一個是由 Activity 啟動 (原本的主應用)
> 
* 解決方案:我們可以透過 **AMS** 查看當前裝置所有運行中的進程,**比對 PID 後執行相對應的行為 (不同進程執行不同行為)**
```java=
// MyApplication 應用
public class MyApplication extends Application {
private static final String TAG = "TEST123";
@Override
public void onCreate() {
super.onCreate();
checkProcess();
}
private void checkProcess() {
String appName = "";
ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningAppProcessInfo> runningAppProcesses = activityManager.getRunningAppProcesses();
for(ActivityManager.RunningAppProcessInfo info : runningAppProcesses) {
if(info.pid == Process.myPid()) {
appName = info.processName;
break;
}
}
Log.d(TAG, "MyApplication --- onCreate --- " +
"\n Process pid: " + Process.myPid() +
"\n name: " + appName);
}
}
```
## Messenger
Messenger 是一個輕量級 IPC 通訊,可以用來傳遞基本的 8 大數據,**但 ++無法傳遞對象++**
簡單來看可以把 Messenger 當成跨進程的 Handler,用來 trigger 進程間的行為 **但 ++無法直接呼叫遠端方法++**
### Messenger - Client 傳送資料
* Messenger 是用在不同的進程間傳送 Message 對象,Messenger 實作順序如下
1. 設定 AndroidManifest 設定 Service 到另外一個進程
```xml=
<service android:name=".MyMessengerService"
android:process=":messenger"/>
```
2. 在 onBinder 方法創建 Messenger 對象 (使用 DCL 單例機制,防止多進程創建多個對象)
```java=
public class MyMessengerService extends Service {
public static final String TAG = "TEST_Messenger";
public static final int MSG_START = 0x0f;
public static final String KEY = "MSG";
private volatile Messenger messenger;
private final Handler handler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(@NonNull Message msg) {
if(msg.what == MSG_START) {
Log.d(TAG, "MSG_START! \n" +
"data = " + msg.getData().getString(KEY));
}
}
};
@Nullable
@Override
public IBinder onBind(Intent intent) {
// DCL
if(messenger == null) {
synchronized (MyMessengerService.class) {
if(messenger == null) {
// 創建單例
messenger = new Messenger(handler);
}
}
}
return messenger.getBinder();
}
}
```
3. Client 端的 Activity 啟動遠端 Service 對象
```java=
// Client 端的 Activity
private void bindMessengerService() {
bindService(new Intent(this, MyMessengerService.class),
new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Messenger messenger = new Messenger(service);
Message msg = Message.obtain(null, MyMessengerService.MSG_START);
Bundle bundle = new Bundle();
bundle.putString(MyMessengerService.KEY, "Hello ~ I'm client.");
msg.setData(bundle);
try {
messenger.send(msg);
Log.d(MyMessengerService.TAG, "Client sending msg.");
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
}, BIND_AUTO_CREATE);
}
```
運行結果如下圖,遠端 Service 有收到 Client 端的 Message
> 
:::danger
* Messager 無法傳遞對象 (就算該對象有實作 Parcelable 接口)
> 在 Service 端接收到 Message 後,Parcelable 會拋出 ClassNotFoundException
```java=
// Client 端
private void bindMessengerService() {
bindService(new Intent(this, MyMessengerService.class),
serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Messenger messenger = new Messenger(service);
Message msg = Message.obtain(null, MyMessengerService.MSG_START);
Bundle bundle = new Bundle();
// bundle.putString(MyMessengerService.KEY, "Hello ~ I'm client.");
bundle.putParcelable(MyMessengerService.KEY_OBJ, new BookObj("Android", 5566));
msg.setData(bundle);
try {
messenger.send(msg);
Log.d(MyMessengerService.TAG, "Client sending msg.");
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
}, BIND_AUTO_CREATE);
}
// -----------------------------------------------------------------
// Service 端
public class BookObj implements Parcelable {
private final String name;
private final int page;
public BookObj(String name, int page) {
this.name = name;
this.page = page;
}
protected BookObj(Parcel in) {
name = in.readString();
page = in.readInt();
}
@NonNull
@Override
public String toString() {
return "Book name: " + name + ", page: " + page;
}
public static final Creator<BookObj> CREATOR = new Creator<BookObj>() {
@Override
public BookObj createFromParcel(Parcel in) {
return new BookObj(in);
}
@Override
public BookObj[] newArray(int size) {
return new BookObj[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name);
dest.writeInt(page);
}
}
```
> 
:::
### Messenger - Service 回覆訊息
* 上面我們只做了單向的 Client 端傳遞給 Service 端訊息,我們接著修改程式讓 Service 端可以回覆訊息給 Client 端
1. Client 端:**需要帶上 Messenger 對象**,該對象是給 Service 回覆訊息的 Messenger
```java=
// Client 端設置
private final Handler handler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(@NonNull Message msg) {
if (msg.what == MyMessengerService.SERVICE_REPLAY) {
Log.d(TAG, "SERVICE_REPLAY! \n" +
"data = " + msg.getData().getString(MyMessengerService.KEY));
}
}
};
private final Messenger clientMessenger = new Messenger(handler);
private void bindMessengerService() {
bindService(new Intent(this, MyMessengerService.class),
serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Messenger messenger = new Messenger(service);
Message msg = Message.obtain(null, MyMessengerService.MSG_START);
Bundle bundle = new Bundle();
bundle.putString(MyMessengerService.KEY, "Hello ~ I'm client.");
msg.setData(bundle);
// ! 帶入 Client 端的 Messenger
msg.replyTo = clientMessenger;
try {
messenger.send(msg);
Log.d(MyMessengerService.TAG, "Client sending msg.");
} catch (RemoteException e) {
e.printStackTrace();
}
}
...
}, BIND_AUTO_CREATE);
}
```
2. Service 端:取出 Client 端傳遞過來的 Messenger 對象,並用該對象傳遞資料回去 (修改在 Handler)
```java=
// Service 端
private final Handler handler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(@NonNull Message msg) {
if(msg.what == MSG_START) {
Log.d(TAG, "MSG_START! \n" +
"data = " + msg.getData().getString(KEY));
// 取出 Client 端的 Messenger
Messenger replyTo = msg.replyTo;
Message message = Message.obtain(null, SERVICE_REPLAY);
Bundle bundle = new Bundle();
bundle.putString(MyMessengerService.KEY, "I'm Service, I got your request.");
try {
// 使用 Client 端提供的 Messenger 回覆訊息
replyTo.send(message);
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
};
```
> 
## ContentProvider
ContentProvider 是四大組件之一,它的功能大多是來管理數據,並且 **它的底層同樣是使用 Binder 機制**
這裡是簡單實現 ContentProvider 機制,並不注重細節
### ContentProvider Service 端
* 創建 ContentProvider 服務來管理 DB 數據
1. GameDbHelper 類繼承 `SQLiteOpenHelper` 建立 DB 資料庫,以下是 TABLE 表內容
| TABLE LABLE | SQL 格式 |
| - | - |
| \_id | int |
| name | TEXT |
| describe | TEXT |
```java=
/**
* 數據庫
*/
public class GameDbHelper extends SQLiteOpenHelper {
public static final String TAG = "content_provider";
public static final String TABLE_NAME = "game";
public static final int VERSION = 1;
public GameDbHelper(@Nullable Context context) {
/*
* Context context
* String name
* SQLiteDatabase.CursorFactory factory
* int version
*/
super(context, TABLE_NAME, null, VERSION);
}
private static final String CREATE_GAME_TABLE_CMD = "create table if not exists " +
TABLE_NAME +
"(_id integer primary key, " + "name TEXT, " + "describe TEXT)";
@Override
public void onCreate(SQLiteDatabase db) {
// 建立 DB TABLE 格式
db.execSQL(CREATE_GAME_TABLE_CMD);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
```
2. GameProvider 類繼承 `ContentProvider` 類,用來管理 DB 資料,並在初始化時創建一個資料
```java=
public class GameProvider extends ContentProvider {
// 這個 AUTH 驗證,用在 URI
// 要與 ++AndroidManifest#authorities 相同++
public static final String AUTH = "AUTH_9527";
// 在該 URI 下創建一個 game 資料夾
public static final Uri GAME_CONTENT_URI = Uri.parse("content://" + AUTH + "/game");
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
// URI 添加 AUTH 驗證、PATH
uriMatcher.addURI(AUTH, "game", 0);
}
private SQLiteDatabase database;
private Context context;
// 表名稱
private final String table = GameDbHelper.TABLE_NAME;
@Override
public boolean onCreate() {
context = getContext();
// 創建 DB && 寫入
database = new GameDbHelper(context).getWritableDatabase(); // 取得 Database 對象
// 初始化完畢,插入一行資料 (insert)
new Thread(() -> {
database.execSQL("delete from " + GameDbHelper.TABLE_NAME);
// SQL 資料結構
database.execSQL("insert into game values(1, 'MapleStory ', 'Pan PC online game');");
}).start();
return false;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
return database.query(table, projection, selection, selectionArgs, null, sortOrder, null);
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
return null;
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
database.insert(table, null, values);
context.getContentResolver().notifyChange(uri, null); // 通知 content provider 刷新
return null;
}
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0;
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0;
}
}
```
3. 當然也請不要忘記在 AndroidManifest.xml 中聲明 `provider`,並且設定到別的 process
```xml=
<!-- AndroidManifest.xml -->
<!-- authorities 必須對應 ! -->
<provider
android:name=".content_provider.GameProvider"
android:authorities="AUTH_9527"
android:exported="false"
android:process=":provider" />
```
### ContentProvider Client 端
* 以下使用一個 Activity 作為客戶端
1. 創建 GameBean,用來管理 DB 數據
```java=
// 不一定要實現 Parcelable 接口
public class GameBean implements Parcelable {
private final String name;
private final String describe;
public GameBean(String name, String describe) {
this.name = name;
this.describe = describe;
}
protected GameBean(Parcel in) {
name = in.readString();
describe = in.readString();
}
@NonNull
@Override
public String toString() {
return "Game name: " + name + ", describe: " + describe;
}
public static final Creator<GameBean> CREATOR = new Creator<GameBean>() {
@Override
public GameBean createFromParcel(Parcel in) {
return new GameBean(in);
}
@Override
public GameBean[] newArray(int size) {
return new GameBean[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name);
dest.writeString(describe);
}
}
```
2. 透過 Context#getContentResolver 取得遠端 ContentProvider Server,並對其操作
```java=
// Client 端
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_content_provider);
Uri uri = GameProvider.GAME_CONTENT_URI;
ContentValues contentValues = new ContentValues();
contentValues.put("_id", 2);
contentValues.put("name", "百變恰吉");
contentValues.put("describe", "Alien PC online game");
// 插入數據
getContentResolver().insert(uri, contentValues);
Cursor cursor = getContentResolver().query(uri, new String[]{"name", "describe"}, null, null, null);
while(cursor.moveToNext()) {
// 透過 Cursor 取得數據
GameBean gameBean = new GameBean(cursor.getString(0), cursor.getString(1));
Log.d(GameDbHelper.TAG, gameBean.toString());
}
}
```
* 最終實現結果,證明使用 ContextProvider 可以簡單的訪問遠端進程
> 
## AIDL
Messenger 的缺點可以透過自己實做 AIDL 來達成,對象傳遞、方法調用
這裡簡單得來實踐 AIDL,在更進接的請參考 [**AIDL & Binder 分析**](https://hackmd.io/yNGrVdN-RtelUqYQgn9avg#AIDL-%E7%A8%8B%E5%BC%8F%E5%88%86%E6%9E%90)
### AIDL 資料夾、文件
* 如果們不改變 AIDL 文件目錄的話(如果要修改可以透過 gradle,但不建議這樣做),我們緊需要 Android java 包同層創建一個 aidl 資料夾
> 
* aidl 文件創建方式
1. 在 aidl 目錄右鍵 `New -> AIDL -> AIDL File`
2. 手動自己創建一個 aidl 檔案
* aidl 支持數據
| 數據類型 | 說明 |
| -------- | -------- |
| 基礎 8 大數據 | int, long, boolean, float, dooble, String |
| CharSequence | |
| List | **只支援 ArrayList、而其內容也要支持 AIDL 才可傳遞** |
| Map | **只支援 HashMap、而其內容也要支持 AIDL 才可傳遞** |
| 實現 Parcelable 對象 | 使用 import 關鍵字 |
| 所有 AIDL 接口 | 使用 import 關鍵字 |
1. 撰寫 AIDL 功能檔案:以下範例包括基礎數據、List、Parcelable 對象 (手動 import AIDL 對象路徑)
> 
```java=
// IMyBookLibrary.aidl
import com.example.aidl_project.BookBean; // 手動 import aidl
interface IMyBookLibrary {
// 有一個 BookBean 對象,該對象我們要實現 Parcelable 接口
List<BookBean> getLibrary();
// 基礎數據
void addBook(String name);
// 基礎數據
void removeBook(String name);
}
```
2. 由於上面的 BookBean 是一個空檔案,我們需要撰寫該檔案,並 import 真正實現 Parcelable 接口的類
```java=
// package 指定目標 java 檔案的路徑 !
package com.example.aidl_project.AIDL;
// 真正 實現 Parcelable 接口的類
parcelable BookBean;
```
3. 實現 BookBean 並實現 Parcelable 接口
```java=
// BookBean.java
public class BookBean implements Parcelable {
public final String name;
public final int page;
public BookBean(String name, int page) {
this.name = name;
this.page = page;
}
protected BookBean(Parcel in) {
name = in.readString();
page = in.readInt();
}
@NonNull
@Override
public String toString() {
return "Book name: " + name + ", page: " + page;
}
public static final Creator<BookBean> CREATOR = new Creator<BookBean>() {
@Override
public BookBean createFromParcel(Parcel in) {
return new BookBean(in);
}
@Override
public BookBean[] newArray(int size) {
return new BookBean[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name);
dest.writeInt(page);
}
}
```
最後按下 Build 按鈕,IDE 就會透過 AIDL 工具自動幫你生成對應的 Java 檔案,該檔案位置在 `app/build/generated/aidl_source_output_dir` 資料夾下
> 
### AIDL Service 端
* 在 Android IDE 的幫助下,會依照你設定的 aidl 生成對應的 Java 檔案,而 Service 端需要實現的是 <aidl_file_name>**==.Stub 類== (關鍵字)**
1. 繼承 or 匿名創建 IMyBookLibrary.Stub 類
```java=
// Service 端
// 以下簡單實現~ 不糾結細節
public class BookService_AIDL extends Service {
private final List<BookBean> bookBeans = new ArrayList<>();
// Service 端需要實現 Stub 類
private final IMyBookLibrary.Stub bookLibrary = new IMyBookLibrary.Stub() {
@Override
public List<BookBean> getLibrary() throws RemoteException {
Log.i("IMPL_AIDL", "Service: getLibrary");
return bookBeans;
}
@Override
public void addBook(String name) throws RemoteException
Log.i("IMPL_AIDL", "Service: addBook - " + name);
if(name == null) {
return;
}
bookBeans.add(new BookBean(name, -1));
}
@Override
public void removeBook(String name) throws RemoteException {
Log.i("IMPL_AIDL", "Service: removeBook - " + name);
for(BookBean bookBean : bookBeans) {
if(bookBean.name.equals(name)) {
bookBeans.remove(bookBean);
break;
}
}
}
};
@Nullable
@Override
public IBinder onBind(Intent intent) {
return bookLibrary;
}
}
```
2. 當然也請不要忘記在 AndroidManifest.xml 中聲明 `service`,並且設定到別的 process
```xml=
<!-- AndroidManifest.xml -->
<service android:name=".AIDL.BookService_AIDL"
android:process=":BookService_AIDL"/>
```
### AIDL Client 端
* 在 Android IDE 的幫助下,會依照你設定的 aidl 生成對應的 Java 檔案,而 Client 端使用的是 <aidl_file_name>**==.Proxy 類== (關鍵字)**
1. 接收到 onServiceConnected 回應後,將 IBinder 對象轉為 IMyBookLibrary 接口 (事實上是取得代理類)
> 建議透過 stub#asInterface 方法方法取得 Poxy
```java=
// Client 端 Activity
private void bindAIDL() {
bindService(new Intent(this, BookService_AIDL.class),
serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
// 可以強制轉型 IBinder,但有更好的方法 (如下)
// 透過 stub 類提供的方法取得 Poxy
IMyBookLibrary iMyBookLibrary = IMyBookLibrary.Stub.asInterface(service);
try {
String Book = "Android_IPC";
iMyBookLibrary.addBook(Book);
List<BookBean> library = iMyBookLibrary.getLibrary();
for(BookBean bean : library) {
Log.i("IMPL_AIDL", "Client: book name - " + bean.name);
}
iMyBookLibrary.removeBook(Book);
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
}, BIND_AUTO_CREATE);
}
```
* 最終實現結果,證明 AIDL 可以直接調用遠端 Service 的方法 (注意:數據是保留在 Service 端,我們是操控遠端存取數據)
> 
## Socket
### Socket 服務端
* Socket 服務端使用 **ServerSocket 對象(並指定 Port)**,並透過 accept 方法接收客端 socket 的連接請求(這個行為是堵塞,所以這裡開啟另一個 Thread 處理)
```java=
public class SocketService extends Service {
public static final String TAG = "SocketService";
public static final int PORT = 8765;
private ServerSocket serverSocket;
private boolean isServiceDestroy;
@Override
public void onCreate() {
super.onCreate();
new Thread(() -> {
try {
serverSocket = new ServerSocket(PORT);
listenerClient(serverSocket);
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
private void listenerClient(ServerSocket serverSocket) throws IOException {
Socket accept = serverSocket.accept();
createClientThread(accept);
}
private void createClientThread(Socket accept) {
if(accept == null) return;
new Thread(() -> {
try(BufferedReader br = new BufferedReader(new InputStreamReader(accept.getInputStream()));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(accept.getOutputStream()));
PrintWriter pw = new PrintWriter(bw, true)
) {
pw.write("I'm Service, ready accept msg.");
while (!isServiceDestroy) {
String msg;
if((msg = br.readLine()) != null) {
Log.i(TAG, "Client Msg: " + msg);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
accept.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
@Override
public void onDestroy() {
super.onDestroy();
isServiceDestroy = true;
try {
if(serverSocket == null) {
return;
}
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
throw new UnsupportedOperationException();
}
}
```
### Socket 客戶端
* 由於 Service 沒有實做 onBind 方法,所以客端在需要使用 startService 方法啟動 Service
```java=
public class SocketActivity extends AppCompatActivity {
public static void startActivity(Context context) {
Intent intent = new Intent(context, SocketActivity.class);
context.startActivity(intent);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_socket);
startService(new Intent(this, SocketService.class));
new Thread(() -> {
try {
Thread.sleep(2500);
startClientSocket();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
public static final String[] MSG = new String[] {
"Hello World 1",
"Hello World 2",
"Hello World 3",
"Hello World 4",
"Hello World 5",
};
private void startClientSocket() throws IOException {
Socket socket = null;
while (socket == null) {
try {
socket = new Socket("localhost", SocketService.PORT);
} catch (IOException e) {
SystemClock.sleep(1000);
}
}
try(BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
PrintWriter pw = new PrintWriter(bw, true)
) {
pw.write(MSG[new Random().nextInt(5)]);
while (!isFinishing()) {
String serviceMsg;
if((serviceMsg = br.readLine()) != null) {
Log.i(SocketService.TAG, "Service msg: " + serviceMsg);
}
}
} finally {
socket.close();
}
}
}
```
* 當然也請不要忘記在 AndroidManifest.xml 中聲明 `service`,並且設定到別的 process
```xml=
<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<service android:name=".socket.SocketService"
android:process=":socket.service" />
```
## Appendix & FAQ
:::info
* Socket I/O 怪怪的 !?
:::
###### tags: `Android 進階`