Framework和Binder的內容挺深的,本文還是站在應用層開發者的角度來建立基本認知,能在遇到問題的時候有思路和方向即可。(本文將帶着關鍵問題和核心流程展開,不會面面俱到)
大綱:
- 背景
- 為什么要多進程
- 為什么要Binder
- Binder簡單架構
- 簡單示例
- 源碼分析
- 客戶端與驅動交互
- 服務端與驅動交互
- 總結
- 細節補充
- Binder為什么高效
- Binder為什么不用shm
- 提問
- 參考資料
本文約4.0k字,閱讀大約17分鍾。
Android源碼基於8.0。
背景
為什么要多進程
Binder是Android系統的一種跨進程通信(IPC)機制。
在Android系統中,單個進程被分配了有限的內存,多進程可以使用更多內存、隔離崩潰風險等。
多進程在Android中常見的使用場景有獨立進程的WebView、推送、保活、系統服務等,既然是多進程場景,那么就需要跨進程通信了。
為什么要Binder
Linux自帶了一些跨進程通信方式:
-
管道(pipe):管道描述符是半雙工,單向的,數據只能往一個方向流,想要讀寫需要兩個管道描述符。Linux提供了pipe(fds)來獲取一對描述符,一個讀一個寫。匿名管道只能用在具有親緣關系的父子進程間的通信,有名管道無此限制。
-
Socket:全雙工,可讀可寫。如Zygote進程等待AMS系統服務發起socket請求來創建應用進程。
-
共享內存(shm,Shared Memory):會映射一段能被多個進程訪問的內存,是最高效的IPC方式,他通常需要結合其他跨進程方式如信號量來同步信息。Android基於shm改進得到匿名共享內存Ashmem(Anonymous Shared Memory),因高效而適合處理較大的數據,如應用進程通過共享內存來讀取SurfaceFlinger進程合成的視圖數據,進行展示。
-
內存映射(mmap):Linux通過將一個虛擬內存區域與一個磁盤上的文件關聯起來,以初始化這個虛擬內存區域的內容。通過指針的方式讀寫內存,系統會同步進對應的磁盤文件。Binder用到了mmap。
-
信號(signal):單向的,發個信號就完事,無返回結果。只能發信號,帶不了參數。如子進程被殺掉后系統會發出SIGCHLD信號,父進程會清理子進程在進程表的描述信息防止
僵屍進程
的發生。
另外還有文件共享、消息隊列(Message)等跨進程通信方式…
這些跨進程通信方式都各有優劣,Android最終選擇了自建一套兼顧好用、高效、安全的Binder。
- 好用:易用的C/S架構(借助AIDL后只需編寫業務邏輯)
- 高效:用mmap進行內存映射,只需一次拷貝
- 安全:內核態管理身份標記,每個App有UID來校驗權限,同時支持實名(系統服務)和匿名(自己創建的服務)
Binder簡單架構
Linux內存被分為用戶空間和內核空間,用戶空間需要經過系統調用才能訪問到內核空間。
(圖片來源:「寫給Android應用工程師的Binder原理剖析」)
Binder整體基於C/S架構。運行在內核空間的Binder驅動程序,會為用戶空間暴露出一個設備文件/dev/binder
,進程間通過該文件來建立通信通道。
Binder的啟動過程:
- 打開binder驅動(open)
- 將驅動文件的描述符(mDriverFD)進行內存映射(mmap),分配緩沖區
- 服務端運行binder線程,把線程注冊到binder驅動,進入循環等待客戶端的指令(兩端通過ioctl與驅動交互)
簡單示例
AIDL(Android接口定義語言)可以輔助生成Binder的Java類,減少重復工作,使用姿勢網上有很多,這里就直接手寫吧,方便理解。
示例調用流程如下:
代碼不多,大部分是log,重點看注釋就行。
客戶端Activity:
//NoAidlActivity.java
protected void onCreate(Bundle savedInstanceState) {
Intent intent = new Intent(this, MyService.class);
bindService(intent, new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
//1\. 從對象池拿到可復用的對象(享元模式)
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
Log.e("哈利迪", "--- 我是客戶端 NoAidlActivity , pid = "
+ Process.myPid() + ", thread = "
+ Thread.currentThread().getName());
String str = "666";
Log.e("哈利迪", "客戶端向服務端發送:" + str);
//2\. 往data寫數據,作為請求參數
data.writeString(str);
//3\. 拿到服務端的IBinder句柄,調用transact
//約定行為碼是1;需要服務端的返回值,所以flags傳0表示同步調用
service.transact(1, data, reply, 0);
Log.e("哈利迪", "--- 我是客戶端 NoAidlActivity , pid = "
+ Process.myPid() + ", thread = "
+ Thread.currentThread().getName());
//4\. 從reply讀取服務端的返回值
Log.e("哈利迪", "客戶端接收服務端返回:" + reply.readString());
}
}, Context.BIND_AUTO_CREATE);
}
service.transact傳入了flags為0
,表示同步調用,會阻塞等待服務端的返回值。如果服務端進行了耗時操作,此時用戶操作UI則會引起ANR。
flags的另一個值是1
,表示異步調用的one way,不需要等待服務端的返回結果,先忽略。
來看服務端運行的Service,
class MyService extends Service {
@Override
public IBinder onBind(Intent intent) {
//返回服務端的IBinder句柄
return new MyBinder();
}
}
注冊服務,讓服務端Service運行在:remote
進程,來實現跨進程,
<service
android:name=".binder.no_aidl.MyService"
android:process=":remote" />
運行在服務端的Binder對象,
class MyBinder extends Binder {
@Override
protected boolean onTransact(int code, Parcel data, Parcel reply, int flags){
if (code == 1) {//如果是約定好的行為碼1
Log.e("哈利迪", "--- 我是服務端 MyBinder , pid = "
+ Process.myPid() + ", thread = "
+ Thread.currentThread().getName());
//1\. 從data讀取客戶端參數
Log.e("哈利迪", "服務端收到:" + data.readString());
String str = "777";
Log.e("哈利迪", "服務端返回:" + str);
//2\. 從reply向客戶端寫返回值
reply.writeString(str);
//3\. 處理完成
return true;
}
return super.onTransact(code, data, reply, flags);
}
}
運行如下,7行日志:
由於我們的flags傳入的是0
同步調用,可以試着在服務端onTransact里sleep幾秒,會發現客戶端需要幾秒后才能打印出返回值。所以如果服務端需要進行耗時操作,客戶端則需要在子線程里進行binder調用。
延伸:從
IT互聯網大叔
的「android獲取進程名函數,如何優化到極致」一文可見,在使用系統API時,如果有更好的方案,還是建議將跨進程方案getSystemService放到最后作為兜底,因為他需要的binder調用本身有開銷,而且作為應用層開發者也很少會去關注遠方進程的內部實現,萬一對方有潛在的耗時操作呢?
通過這個例子,我們可以看出,Binder機制使用了Parcel來序列化數據,客戶端在主線程調用了transact來請求(Parcel data傳參),服務端在Binder線程調用onTransact來響應(Parcel reply回傳結果)。
源碼分析
Binder的調用流程大致如下,native層BpBinder的Bp
指的是Binder proxy
,
可見,需要經過如下調用才能完成一次通信:
- 請求:客戶端Java層->客戶端native層->Binder驅動層->服務端native層->服務端Java層
- 響應:服務端Java層->服務端native層->Binder驅動層->客戶端native層->客戶端Java層
即Binder驅動層充當着一個中轉站的作用,有點像網絡分層模型。
客戶端與驅動交互
先來看客戶端與驅動的交互。因為是跨進程調用(指定了:remote
),示例里onServiceConnected回調回來的service對象是個BinderProxy代理實例(不跨進程的話會發生遠程轉本地
,后面講),我們以service.transact(1, data, reply, 0)這行調用作為入口跟進。
BinderProxy類寫在Binder類文件里面:
//BinderProxy.java
public boolean transact(int code, Parcel data, Parcel reply, int flags){
//調用了native方法
return transactNative(code, data, reply, flags);
}
這個native方法在android_util_Binder.cpp里注冊,
//android_util_Binder.cpp
//JNI注冊
static const JNINativeMethod gBinderProxyMethods[] = {
{ "transactNative",
"(ILandroid/os/Parcel;Landroid/os/Parcel;I)Z",
(void*)android_os_BinderProxy_transact},
};
//native方法具體實現
static jboolean android_os_BinderProxy_transact(JNIEnv* env, jobject obj,
jint code, jobject dataObj, jobject replyObj, jint flags){
//轉成native層的Parcel
Parcel* data = parcelForJavaObject(env, dataObj);
Parcel* reply = parcelForJavaObject(env, replyObj);
//拿到native層的句柄BpBinder
IBinder* target = (IBinder*)
env->GetLongField(obj, gBinderProxyOffsets.mObject);
//調用BpBinder的transact
status_t err = target->transact(code, *data, reply, flags);
}
繼續跟BpBinder.cpp,
//BpBinder.cpp
status_t BpBinder::transact(...){
//交給線程單例處理,驅動會根據mHandle值來找到對應的binder句柄
status_t status = IPCThreadState::self()->transact(
mHandle, code, data, reply, flags);
}
IPCThreadState是一個線程單例,負責與binder驅動進行具體的指令通信,跟進IPCThreadState.cpp,
//IPCThreadState.cpp
status_t IPCThreadState::transact(...){
//將數據寫入mOut,見1.1
err = writeTransactionData(BC_TRANSACTION, flags, handle, code, data, NULL);
//...先忽略one way異步調用的代碼,只看有返回值的同步調用
//跟binder驅動交互,傳入reply接收返回數據,見1.2
err = waitForResponse(reply);
}
//1.1 將數據寫入mOut
status_t IPCThreadState::writeTransactionData(...)
{
binder_transaction_data tr;
//...打包各種數據(data size、buffer、offsets)
tr.sender_euid = 0;
//將BC_TRANSACTION指令寫入mOut
mOut.writeInt32(cmd);
//將打包好的binder_transaction_data寫入mOut
mOut.write(&tr, sizeof(tr));
}
//1.2 跟binder驅動交互,傳入reply接收返回數據
status_t IPCThreadState::waitForResponse(...){
//這個循環很重要,客戶端就是在這里休眠等待服務端返回結果的
while (1) {
//跟驅動進行數據交互,往驅動寫mOut,從驅動讀mIn,見1.3
talkWithDriver();
//讀取驅動回復的指令
cmd = (uint32_t)mIn.readInt32();
switch (cmd) {
case BR_TRANSACTION_COMPLETE:
//表示驅動已經收到客戶端的transact請求
//如果是one way異步調用,到這就可以結束了
if (!reply && !acquireResult) goto finish;
break;
case BR_REPLY:
//表示客戶端收到服務端的返回結果
binder_transaction_data tr;
//把服務端的數據讀出來,打包進tr
err = mIn.read(&tr, sizeof(tr));
//再把tr的數據透傳進reply
reply->ipcSetDataReference(...);
//結束
goto finish;
}
}
}
//1.3 跟驅動進行數據交互,往驅動寫mOut,從驅動讀mIn
status_t IPCThreadState::talkWithDriver(bool doReceive){
binder_write_read bwr;
//指定寫數據大小和寫緩沖區
bwr.write_size = outAvail;
bwr.write_buffer = (uintptr_t)mOut.data();
//指定讀數據大小和讀緩沖區
if (doReceive && needRead) {
bwr.read_size = mIn.dataCapacity();
bwr.read_buffer = (uintptr_t)mIn.data();
} else {
bwr.read_size = 0;
bwr.read_buffer = 0;
}
//ioctl的調用進入了binder驅動層的binder_ioctl
ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr);
if (bwr.write_consumed > 0) {
//數據已經寫入驅動,從mOut移除
if (bwr.write_consumed < mOut.dataSize())
mOut.remove(0, bwr.write_consumed);
else
mOut.setDataSize(0);
}
if (bwr.read_consumed > 0) {
//從驅動讀出數據存入mIn
mIn.setDataSize(bwr.read_consumed);
mIn.setDataPosition(0);
}
}
ioctl的調用進入了binder驅動層的binder_ioctl,驅動層的代碼先不跟。
服務端與驅動交互
從「一圖摸清Android應用進程的啟動」一文可知,服務端創建了一個線程注冊進binder驅動,即binder線程,在ProcessState.cpp,
//ProcessState.cpp
virtual bool threadLoop()
{ //把binder線程注冊進binder驅動程序的線程池中
IPCThreadState::self()->joinThreadPool(mIsMain);
return false;
}
//IPCThreadState.cpp
void IPCThreadState::joinThreadPool(bool isMain){
//向binder驅動寫數據,表示當前線程需要注冊進binder驅動
mOut.writeInt32(isMain ? BC_ENTER_LOOPER : BC_REGISTER_LOOPER);
status_t result;
do {
//進入死循環,等待指令的到來,見1.1
result = getAndExecuteCommand();
} while (result != -ECONNREFUSED && result != -EBADF);
//向binder驅動寫數據(退出循環,線程結束)
mOut.writeInt32(BC_EXIT_LOOPER);
}
//1.1 等待指令的到來
status_t IPCThreadState::getAndExecuteCommand(){
//跟驅動進行數據交互,驅動會把指令寫進mIn
talkWithDriver();
//從mIn讀出指令
cmd = mIn.readInt32();
//執行指令,見1.2
result = executeCommand(cmd);
return result;
}
//1.2 執行指令
status_t IPCThreadState::executeCommand(int32_t cmd){
//客戶端發請求到驅動,驅動轉發到服務端
switch ((uint32_t)cmd) {
case BR_TRANSACTION:{
//服務端收到BR_TRANSACTION指令
binder_transaction_data tr;
//讀出客戶端請求的參數
result = mIn.read(&tr, sizeof(tr));
//准備數據,向上傳給Java層
Parcel buffer; Parcel reply;
buffer.ipcSetDataReference(...);
//cookie保存的是binder實體,對應服務端的native層對象就是BBinder
reinterpret_cast<BBinder*>(tr.cookie)->transact(tr.code, buffer,
&reply, tr.flags);
//服務端向驅動寫返回值,讓驅動轉發給客戶端
sendReply(reply, 0);
}
}
}
//1.3 服務端向驅動寫返回值,讓驅動轉發給客戶端
status_t IPCThreadState::sendReply(const Parcel& reply, uint32_t flags){
err = writeTransactionData(BC_REPLY, flags, -1, 0, reply, &statusBuffer);
//服務端返回結果給客戶端就行,不用等待客戶端,所以傳NULL
return waitForResponse(NULL, NULL);
}
然后看下BBinder的transact是怎么向上傳遞到Java層的,在Binder.cpp中,
//Binder.cpp
status_t BBinder::transact(uint32_t code, const Parcel& data,
Parcel* reply, uint32_t flags){
switch (code) {
//ping指令用來判斷連通性,即binder句柄是否還活着
case PING_TRANSACTION:
reply->writeInt32(pingBinder());
break;
default:
//看這,通過JNI調用到Java層的execTransact,見1.1
err = onTransact(code, data, reply, flags);
break;
}
return err;
}
//android_util_Binder.cpp
//1.1 通過JNI調用到Java層的execTransact
virtual status_t onTransact(...){
JNIEnv* env = javavm_to_jnienv(mVM);
jboolean res = env->CallBooleanMethod(mObject, gBinderOffsets.mExecTransact, ...);
}
回到Java層,execTransact如下:
//android.os.Binder.java
private boolean execTransact(...) {
res = onTransact(code, data, reply, flags);
}
至此就回調到了示例代碼中服務端MyBinder的onTransact了,我們在示例中處理請求參數data和返回值reply,最后由native層的sendReply(reply, 0)真正向驅動寫返回值,讓驅動轉發給客戶端。
將調用代碼和流程圖結合起來:
然后是指令交互圖(非one way模式):
binder同步調用等到服務端的BR_REPLY
指令后就真正結束,服務端則繼續循環,等待下一次請求。
總結
本文主要介紹了Binder的背景和調用流程,將留下3個疑問繼續探討。
- binder句柄是怎么傳輸和管理的(binder驅動和ServiceManager進程)
- binder句柄的
遠程轉本地
- one way異步模式和他的串行調用(async_todo)、同步模式的並行調用
系列文章:
細節補充
Binder為什么高效
Linux用戶空間是無法直接讀寫磁盤的,系統所有的資源管理(讀寫磁盤文件、分配回收內存、從網絡接口讀寫數據)都是在內核空間完成的,用戶空間需要通過系統調用讓內核空間完成這些功能。
傳統IPC傳輸數據:發送進程需要copy_from_user
從用戶到內核,接收進程再copy_to_uer
從內核到用戶,兩次拷貝。
而Binder傳輸數據:用mmap將binder內核空間的虛擬內存和用戶空間的虛擬內存映射到同一塊物理內存。copy_from_user
將數據從發送進程的用戶空間拷貝到接收進程的內核空間(一次拷貝),接收進程通過映射關系能直接在用戶空間讀取內核空間的數據。
(圖片來源:「寫給Android應用工程師的Binder原理剖析」)
Binder為什么不用shm
shm通常需要結合其他跨進程方式如信號量來同步信息,使用沒有mmap方便。
提問
- 上期提問: SurfaceFlinger進程為什么不是通過Zygote進程的fork創建,而是由init進程創建?
參考資料
- 書籍 - Android系統源代碼情景分析
- 博客 - 王小二的Android站
- 博客 - 寫給Android應用工程師的Binder原理剖析
- 博客 - Binder傳輸機制篇_中
- 博客 - 共享內存和文件內存映射的區別
本文在開源項目:https://github.com/Android-Alvin/Android-LearningNotes 中已收錄,里面包含不同方向的自學編程路線、面試題集合/面經、及系列技術文章等,資源持續更新中…
作者:哈利迪
鏈接:https://juejin.im/post/6890088205916307469