Android IPC機制

1. IPC基礎概念——多進程

IPC是Inter Process Communication的縮寫,意為進程間通信或者跨進程通信,是指兩個進程之間進行數據交換。

1.1 Android中的多進程

在了解Android多進程之前,得先了解什么是進程?

按照操作系統的描述,線程是CPU調度的最小單元,而進程一般指一個執行單元,在移動設備上指一個程序或應用;一個進程可以包含多個線程。

1.2 Android開啟多進程模式

在Android中要開啟多進程模式,只要給四大組件指定 android:process 屬性。
是不是很簡單的就開啟了多進程,其實遠沒有那么簡單,這只是挖坑的開始,先看一下開啟多進程的實例操作:

        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        // 注意進程以:開頭
        <activity android:name=".SecondActivity" 
            android:process=":remote"/>

        <activity android:name=".ThirdActivity"
            android:process="com.czj.ipcdemo.remote2"/>

在終端輸入adb shell ps | com.czj.ipcdemo查看一個包名中當前應用所存在的進程信息。
會發現一共存在3個進程。
此處劃重點:

  • 以:開頭的屬于當前應用私有進程,其它應用的組件不可以和它跑在同一個進程;
  • 進程名不以:開頭的,屬于全局進程,其它應用通過shareUID方式可以和它跑在同一個進程中。
App進程.png

1.3 多進程造成的問題

  • 靜態成員和單例模式失效
  • 線程同步機制失效
  • SharedPreferences的可靠性降低
  • Application會創建多次

Android虛擬機分配規則
Android會為每一個進程都分配一個獨立的虛擬機,不同的虛擬機在內存分配上有不同的地址看空間,這就導致在不同的虛擬機(進程)中訪問同一個對象會產生多份副本。所以,在多進程環境中,想要依靠內存來共享數據,是不會達到你預期的結果的。

現在來分析一下上述列出的問題:
第1個問題和第2個問題都是因為不同進程的虛擬機是獨立的,不同虛擬機內存地址當然就是不一樣的,既然不是同一塊內存地址,鎖對象或者鎖類也達不到線程同步。
第3個問題,SharedPreferences不支持兩個進程同時做寫操作。
第4個問題,一個組件跑在新的進程中,系統要為其分配獨立的虛擬機,也就相當于重新啟動一個應用。

在進行跨進程通信之前,還得了解Android的序列化(Serializable 和 Parcelable)以及Binder的概念。

2. IPC基礎概念——序列化

關于Android序列化,附個傳送門:Android序列化基礎知識

3. IPC基礎概念——Binder

從各個角度看Binder:
IPC:Binder是Android中的跨進程通信方式。
Android Framework:BInder是ServiceManger連接各種Manager和響應MangerService的橋梁。
Android應用層:Binder是客戶端和服務端進行通信的媒介。

Binder主要用在Service中,包括AIDL和Messenger(底層是AIDL),普通的Service中的Binder不涉及進程間通信。后面主要對AIDL的使用,進行分析,一個AIDL過程,即是一個RPC的過程。
后面第四部分會對AIDL的使用進行實例分析。

4. Android實現IPC的幾種方式

  • Bundle
  • 文件共享
  • Messenger
  • AIDL
  • ContentProvider
  • Socket

4.1 Bundle

Android中的Activity、Service、Receiver都支持使用Intent來傳遞Bundle數據,Bundle實現了Parcelable接口,所以能夠使用Bundle來實現數據的進程間傳輸。
需要注意的一點是,傳輸的對象必須能夠被序列化(基本類型、Parcelable、Serializable)

4.2 文件共享

兩個進程對同一個文件進行讀和寫,來實現進程間的通信。實現無非就是文件IO操作,實例此處略過。
但是,這里說的文件共享不包括SharedPreferences。
雖然SharedPreferences是XML文件,但是系統會對它的讀寫有緩存策略,就是說再內存中會有一份SharedPreferences緩存,但是多進程是分配在獨立的虛擬機上的,所以SharedPreferences數據在多進程是不可靠的。

4.3 Messenger

Messenger是一種輕量級的IPC方案,它的底層實現是AIDL。

服務端進程:

public class MessengerService extends Service {

    private static final String TAG = "MessengerService";

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

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

    @Override
    public IBinder onBind(Intent intent) {
        return mMessenger.getBinder();
    }

    private static class MessengerHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case Constants.MSG_CLIENT:
                    // 收到客戶端消息
                    Log.e(TAG, "收到客戶端進程消息:" + msg.getData().getString("msg"));
                    // 回復客戶端消息
                    Messenger client = msg.replyTo;
                    Message replyMessage = Message.obtain(null, Constants.MSG_SERVER);
                    Bundle bundle = new Bundle();
                    bundle.putString("reply", "來自Service的回復");
                    replyMessage.setData(bundle);
                    try {
                        client.send(replyMessage);
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                    break;
                default:
                    super.handleMessage(msg);
            }
        }
    }
}

創建一個Service來處理客戶端的連接請求,同時創建一個Handler內部類來處理消息,并用來實例化Messenger對象。最后在onBind返回Messenger對象的Binder。日志打印如下:

10-24 14:08:47.542 31636-31636/com.czj.ipcdemo:remote3 E/MessengerService: 收到客戶端進程消息:client msg

客戶端進程:

public class MessengerActivity extends AppCompatActivity {

    private static final String TAG = "MessengerActivity";

    private Messenger mService;

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

    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mService = new Messenger(service);
            Message msg = Message.obtain(null, Constants.MSG_CLIENT);
            Bundle bundle = new Bundle();
            bundle.putString("msg", "client msg");
            msg.setData(bundle);
            // 將客戶端的 Messenger 傳遞給服務端
            msg.replyTo = mMessenger;
            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() {
        unbindService(mConnection);
        super.onDestroy();
    }

    private static class MessengerHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case Constants.MSG_SERVER:
                    Log.e(TAG, "收到服務端回信:" + msg.getData().get("reply"));
                    break;
                default:
                    super.handleMessage(msg);
            }
        }
    }
}

客戶端進程,首先要綁定服務端的Service,然后用服務端給的IBinder去創建Messenger來與服務端進行通信。
如果需要服務端能夠回復客戶端,則客戶端要定義個Messenger,然后賦值給Message.replyTo傳遞給服務端。
日志打印如下:

10-24 14:08:47.548 31568-31568/com.czj.ipcdemo E/MessengerActivity: 收到服務端回信:來自Service的回復

使用Messenger進行進程間的通信,使用方法簡單,但它也有些局限性。

  1. Messenger是以串行的方式處理客戶端消息,服務端只能一個個處理,不適合有大量并發請求的情況
  2. Messenger的主要作用是傳遞消息,無法跨進程調用服務端的方法(也是RPC過程)

4.4 AIDL

AIDL(Android Interface Definition Language,Android接口定義語言):如果在一個進程中要調用另一個進程中對象的方法,可使用AIDL生成可序列化的參數,AIDL會生成一個服務端對象的代理類,通過它客戶端實現間接調用服務端對象的方法。

4.4.1 AIDL文件定義

1. AIDL接口創建
AIDL接口就是暴露給客戶端調用的方法,此AIDL文件編譯后在build目錄下會生成對應的Java代碼,所以從本質上來說AIDL是系統提供了一套可快速實現Binder的工具。示例代碼如下:

package com.czj.ipcdemo;
// 這里要注意,即使在同一個包下,也要把引用的類import進來
import com.czj.ipcdemo.Book;

interface IBookManager {

    List<Book> getBookList();

    void addBook(in Book book);

}

AIDL文件中的數據類型要求:

  • 基本數據類型:byte,int,long,float,double,boolean,char
  • String類型
  • CharSequence類型
  • ArrayList、HashMap且里面的每個元素都能被AIDL支持
  • 實現Parcelable接口的對象
  • 所有AIDL接口本身

需要注意的是,AIDL中除了基本類型外,其它類型的參數必須標明傳遞方向:

  • in:輸入型參數
    表示數據只能由客戶端流向服務端。
    服務端將會接收到這個對象的完整數據,但在服務端修改它不會對客戶端輸入的對象產生影響。
  • out: 輸出型參數
    表示數據只能由服務端流向客戶端。
    服務端將會接收到這個對象的的空對象,但在服務端對接收到的空對象有任何修改之后客戶端將會同步變動。
  • inout:輸入輸出型參數
    表示數據可在服務端與客戶端之間雙向流通。
    服務端將會接收到客戶端傳來對象的完整信息,且客戶端將會同步服務端對該對象的任何變動。

2. 自定義Parcelable類對應AIDL文件
如果AIDL文件中用到了自定義的Parcelable對象,則必須新建一個和它同名的AIDL文件:

package com.czj.ipcdemo;

// Declare any non-default types here with import statements

parcelable Book;

3. 服務端Service實現

public class BookManagerService extends Service {

    private static final String TAG = "BMS";

    // CopyOnWriteArrayList支持并發,當多個客戶端同時訪問時
    // 前面提到AIDL只支持ArrayList
    // AIDL中使用的是List接口去訪問,此處服務端返回CopyOnWriteArrayList實例,但是Binder會轉成ArrayList給客戶端,所以通過AIDL接口去獲取的時候已經是ArrayList
    private List<Book> mBookList = new CopyOnWriteArrayList<>();

    private Binder mBinder = new IBookManager.Stub() {
        @Override
        public List<Book> 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("忒修斯之船"));
    }

    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }
}

4. 客戶端實現

public class BookManagerActivity extends AppCompatActivity {

    private final static String TAG = "BookManagerActivity";

    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            // 將服務端Binder轉成AIDL接口類型
            IBookManager bookManager = IBookManager.Stub.asInterface(service);
            try {
                // 獲取服務端數據
                List<Book> list = bookManager.getBookList();
                Log.e(TAG, "list type : " + list.getClass().getCanonicalName());
                Log.e(TAG, "book list : " + list);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            Log.e(TAG, "onServiceDisconnected");
        }
    };

    @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() {
        unbindService(mConnection);
        super.onDestroy();
    }
}

輸出(注意此處的List輸出類型):
10-24 09:51:26.122 13500-13500/com.czj.ipcdemo E/BookManagerActivity: list type : java.util.ArrayList
10-24 09:51:26.123 13500-13500/com.czj.ipcdemo E/BookManagerActivity: book list : [[bookName:忒修斯之船]]

AIDL變量和方法解釋(存在于AIDL文件編譯轉換成的Java類中)

  • DESCRIPTOR
    Binder的唯一標識,用當前Binder的類名表示,如:com.czj.ipcdemo.IBookManager
  • asInterface(android.os.IBinder obj)
    客戶端調用,將服務端的返回的Binder對象,轉換成客戶端所需要的AIDL接口類型對象。如果客戶端和服務端同屬一個進程,此方法返回的是服務端對象本身,否則返回系統封裝后的Stub.proxy對象。
  • asBinder()
    返回當前Binder對象
  • onTransact()
    運行服務端的Binder線程池中,當客戶端發起跨進程請求時,遠程請求會通過系統底層封裝后交由此方法來處理。
  • transact()
    運行在客戶端,當客戶端發起遠程請求的同時將當前線程掛起。之后調用服務端的onTransact()直到遠程請求返回,當前線程才繼續執行。

Binder死亡的處理

如果服務端進程意外終止,則客戶端到服務端的Binder連接也就終止了,這會導致客戶端遠程調用失敗。

面對這種情況,有兩種解決方案:

  • 給Binder設置死亡監聽:DeathRecipient
  • 在onServiceDisconnected中重新綁定遠程服務

下面給出為Binder設置死亡監聽的示例:

private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
        @Override
        public void binderDied() {
                if (bookManager == null) return;
                bookManager.asBinder().unlinkToDeath(mDeathRecipient, 0);
                bookManager = null;
                // TODO 此處重新綁定遠程Service
        }
 }

然后在客戶端綁定Service的回調中,設置Binder的死亡代理:

    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            IBookManager bookManager = IBookManager.Stub.asInterface(service);
            // 設置死亡代理            
            service.linkToDeath(mDeathRecipient, 0);
            ......
        }
        ......
    };

Binder的跨進程傳輸

不知道有沒有發現一個問題,就是服務端和客戶端是運行在兩個進程上的,那么服務端的Binder是如何傳遞給客戶端的ServiceConnection中,顯然不是通過內存直接傳遞Binder對象。
Binder的傳遞:將Binder打包進Parcel來傳輸,發現如果是同進程的,則收到的是原始對象,而不是對象的拷貝。如果是跨進程的,則收到的BinderProxy。

Service注冊到Service Manager時,將Binder實體跨進程傳輸給ServiceManager,由于是跨進程的,所以必須先經過Binder驅動,驅動會保存這個Binder實體,并生成一個Binder引用返給ServiceManager。Client端如果向Service Manager請求該Service,ServiceManager會將該Service的Binder引用返回,經過Binder驅動時,如果Client和Service在同一個進程,那么Binder驅動會直接返回該Service的Binder實體,如果不在同一個進程,則Binder驅動會返回Binder的引用。

推薦參考 關于Binder的跨進程傳輸

4.5 ContentProvider

ContentProvider用于在多個應用程序之間共享數據,所以適合多進程通信的場景,它的底層實現也是Binder。

  • 除了onCreate是在UI線程,query、update、insert、delete都運行在Binder線程
  • SQLiteDatabase內部有同步處理,但是多個SQLiteDatabase的情況,要做好同步處理

4.6 Socket

Socket:套接字,能夠建立網絡長連接。不僅支持進程間通信,還能進行跨設備通信。

在Service進程中創建一個Socket服務,客戶端去連接該服務,也能夠實現進程間通信。直接看實例:

  • 首先需要聲明網絡訪問權限
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
  • 服務端代碼
public class TCPServerService extends Service {

    private final static String TAG = "TCPServerService";

    private boolean mIsDestory = false;

    private String[] mReturnMsgAry = { "你好傻貓威~", "買了否冷?", "鬼刀一開,看不見,走位走位", "學貓叫" };

    @Override
    public void onCreate() {
        Log.e(TAG, "開啟服務");
        new Thread(new TcpServer()).start();
        super.onCreate();
    }

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

    @Override
    public void onDestroy() {
        mIsDestory = true;
        super.onDestroy();
    }

    private class TcpServer implements Runnable {

        @Override
        public void run() {
            ServerSocket serverSocket = null;
            try {
                serverSocket = new ServerSocket(9527);
                Log.e(TAG, "服務已開啟,正在監聽客戶端消息");
            } catch (IOException e) {
                Log.e(TAG, "run socket server failed, port:8688");
                e.printStackTrace();
                return;
            }
            while (!mIsDestory) {
                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 (!mIsDestory) {
            String str = in.readLine();
            if(str == null)
                break;//客戶端斷開連接
            int i = new Random().nextInt(mReturnMsgAry.length);
            String msg = mReturnMsgAry[i];
            out.println(msg);
        }
        in.close();
        out.close();
        client.close();
    }

}
  • 客戶端代碼:
public class TCPClientActivity extends AppCompatActivity implements View.OnClickListener{

    private final static int MSG_RECEIVE_MSG = 1;

    private final static int MSG_SOCKET_CONNECTED = 2;

    private Button mSendBtn;

    private TextView mMsgTextView;

    private EditText mMsgEditText;

    private PrintWriter mPrintWriter;

    private Socket mClient;

    private Handler mHander = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_RECEIVE_MSG:
                    mMsgTextView.setText(mMsgTextView.getText() + (String)msg.obj);
                    break;
                case MSG_SOCKET_CONNECTED:
                    mSendBtn.setEnabled(true);
                    break;
                default:
                    break;
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_tcpclient);
        mMsgEditText = findViewById(R.id.et_msg);
        mMsgTextView = findViewById(R.id.tv_content);
        mSendBtn = findViewById(R.id.btn_send);
        mSendBtn.setOnClickListener(this);
        // 啟動服務進程
        Intent intent = new Intent(this, TCPServerService.class);
        startService(intent);
        new Thread() {
            @Override
            public void run() {
                connectTCPServer();
            }
        }.start();

    }

    @Override
    protected void onDestroy() {
        if(mClient != null) {
            try {
                mClient.shutdownInput();
                mClient.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        super.onDestroy();
    }

    private void connectTCPServer() {
        Socket socket = null;
        while (socket == null) {
            try {
                socket = new Socket("localhost", 9527);
                mClient = socket;
                mPrintWriter = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
                // 發送連接成功的消息
                mHander.sendEmptyMessage(MSG_SOCKET_CONNECTED);
            } catch (IOException e) {
                SystemClock.sleep(1000);
                System.out.println("連接失敗,重連中...");
            }
        }

        try {
            BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            while (!TCPClientActivity.this.isFinishing()) {
                String msg = br.readLine();
                System.out.println("receive: " + msg);
                if (msg != null) {
                    String time = formateTime(System.currentTimeMillis());
                    // 追加消息
                    final String showMsg = "server(" + time + "):" + msg + "\n";
                    mHander.obtainMessage(MSG_RECEIVE_MSG, showMsg).sendToTarget();
                }
            }
            System.out.println("斷開連接...");
            mPrintWriter.close();
            br.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private String formateTime(long time) {
        return new SimpleDateFormat("HH:mm:ss").format(new Date(time));
    }


    @Override
    public void onClick(View v) {
        if(v.getId() == mSendBtn.getId()) {
            final String msg = mMsgEditText.getText().toString();
            if(!TextUtils.isEmpty(msg) && mPrintWriter != null) {
                new Thread() {
                    @Override
                    public void run() {
                        mPrintWriter.println(msg);
                    }
                }.start();
                mMsgEditText.setText("");
                String time = formateTime(System.currentTimeMillis());
                String showMsg = "client(" + time + "):" + msg + "\n";
                mMsgTextView.setText(mMsgTextView.getText() + showMsg);
            }
        }
    }
}
  • 程序截圖


    Screenshot_2018-10-26-13-39-35-286_IPCDemo.png

IPC方式的優缺點和適用場景

名稱 優點 缺點 適用場景
Bundle 簡單易用 只能傳輸Bundle支持的數據 四大組件之間的進程通信
文件共享 簡單易用 不適合高并發場景,并且無法做到進程間的即時通信 無并發訪問,交換簡單的數據,且實時性不高的場景
AIDL 功能強大,支持一對多并發通信,支持實時通信 使用較復雜,需要處理好線程同步 一對多通信且有RPC需求
Messenger 支持一對多串行通信,實時通訊 不能很好處理高并發情形,不支持RPC,數據通過Message進行傳輸,因此只能傳輸Bundle數據 低并發的一對多即時通信,無RPC需求,或者無須返回結果的RPC需求
ContentProvider 在數據源訪問方面功能強大,支持一對多并發共享數據,可通過Call方法擴展其他操作 可以理解為受約束的AIDL,主要提供數據源的CRUD操作 一對多的進程間的數據共享
Socket 功能強大,可以通過網絡傳輸數據,支持一對多并發實時通信 實現細節繁瑣,不支持直接的RPC 網絡數據交換
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。