--- 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 進程則不會** > ![](https://i.imgur.com/WvK7gxw.png) ### 多進程 - 問題 * 由於有設定不同進程 (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 啟動 (原本的主應用) > ![](https://i.imgur.com/k7QmlzZ.png) * 解決方案:我們可以透過 **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 > ![](https://i.imgur.com/zKweRcL.png) :::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); } } ``` > ![](https://i.imgur.com/NTw8sl9.png) ::: ### 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(); } } } }; ``` > ![](https://i.imgur.com/2UodvH1.png) ## 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 可以簡單的訪問遠端進程 > ![](https://i.imgur.com/2T5qxIF.png) ## 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 資料夾 > ![](https://i.imgur.com/3vgmiOM.png) * 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 對象路徑) > ![](https://i.imgur.com/ZJpKXKo.png) ```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` 資料夾下 > ![](https://i.imgur.com/kNQfKjT.png) ### 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 端,我們是操控遠端存取數據) > ![](https://i.imgur.com/5nrjSWy.png) ## 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 進階`