-- 作者 謝恩銘 轉(zhuǎn)載請注明出處
前言
為使應(yīng)用程序之間能夠彼此通信,Android提供了IPC (Inter Process Communication,進程間通信)的一種獨特實現(xiàn): AIDL (Android Interface Definition Language, Android接口定義語言)。
網(wǎng)上有不少關(guān)于AIDL的文章,寫得都很不錯。不過例子構(gòu)造大多略微復雜: 建立兩個Android項目,一個是client(客戶端),一個是server(服務(wù)端,提供service(服務(wù)))。
這篇文章將首先介紹AIDL的原理,再通過一個Android項目來介紹AIDL用法。服務(wù)端和客戶端包含在這同一個項目中,原理和分別在兩個項目中是一樣的,不過輕省許多。
源碼在我的Github上,文末有放出。
這篇博文包含以下四個部分:
- AIDL介紹
- 實現(xiàn)步驟
- 實例: HelloSumAIDL
3.1 創(chuàng)建工程
3.2 定義AIDL文件
3.3 實現(xiàn)遠程服務(wù)(Service)
3.4 “暴露”服務(wù)
3.5 相關(guān)代碼 - 后記和源碼
1. AIDL介紹
在Android中,默認每個應(yīng)用(application)執(zhí)行在它自己的進程中,無法直接調(diào)用到其他應(yīng)用的資源,這也符合“沙箱”(SandBox)的理念。所謂沙箱原理,一般來說用在移動電話業(yè)務(wù)中,簡單地說旨在部分地或全部地隔離應(yīng)用程序。
Android沙箱技術(shù):
Android“沙箱”的本質(zhì)是為了實現(xiàn)不同應(yīng)用程序和進程之間的互相隔離,即在默認情況 下,應(yīng)用程序沒有權(quán)限訪問系統(tǒng)資源或其它應(yīng)用程序的資源。
每個APP和系統(tǒng)進程都被分配唯一并且固定的User Id(用戶身份標識),這個uid與內(nèi)核層進程的uid對應(yīng)。
每個APP在各自獨立的Dalvik虛擬機中運行,擁有獨立的地址空間和資源。
運行于Dalvik虛擬機中的進程必須依托內(nèi)核層Linux進程而存在,因此Android使用Dalvik虛擬機和Linux的文件訪問控制來實現(xiàn)沙箱機制,任何應(yīng)用程序如果想要訪問系統(tǒng)資源或者其它應(yīng)用程序的資源必須在自己的manifest文件中進行聲明權(quán)限或者共享uid。
本段關(guān)于沙箱的解釋轉(zhuǎn)載自:Android的權(quán)限機制之—— “沙箱”機制sharedUserId和簽名
因此,在Android中,當一個應(yīng)用被執(zhí)行時,有一些操作是被限制的,比如訪問內(nèi)存,訪問傳感器,等等。這樣做可以最大化地保護系統(tǒng),免得應(yīng)用程序“為所欲為”。
那我們有時需要在應(yīng)用間交互,怎么辦呢?于是,Android需要實現(xiàn)IPC協(xié)議。
關(guān)于IPC協(xié)議,可以參看下面摘自維基百科的內(nèi)容:
進程間通信(IPC,Inter-Process Communication),指至少兩個進程或線程間傳送數(shù)據(jù)或信號的一些技術(shù)或方法。
進程是計算機系統(tǒng)分配資源的最小單位(嚴格說來是線程)。每個進程都有自己的一部分獨立的系統(tǒng)資源,彼此是隔離的。
為了能使不同的進程互相訪問資源并進行協(xié)調(diào)工作,才有了進程間通信。舉一個典型的例子,使用進程間通信的兩個應(yīng)用可以被分類為客戶端和服務(wù)器(主從式架構(gòu)),客戶端進程請求數(shù)據(jù),服務(wù)端回復客戶端的數(shù)據(jù)請求。有一些應(yīng)用本身既是服務(wù)器又是客戶端,這在分布式計算中,時常可以見到。這些進程可以運行在同一計算機上或網(wǎng)絡(luò)連接的不同計算機上。
進程間通信技術(shù)包括消息傳遞、同步、共享內(nèi)存和遠程過程調(diào)用(Remote Procedure Call,縮寫是RPC)。IPC是一種標準的Unix通信機制。
使用IPC 的理由:
- 信息共享:Web服務(wù)器,通過網(wǎng)頁瀏覽器使用進程間通信來共享web文件(網(wǎng)頁等)和多媒體。
- 加速:維基百科使用通過進程間通信進行交流的多服務(wù)器來滿足用戶的請求。
- 模塊化。
- 私有權(quán)分離。
與直接共享內(nèi)存地址空間的多線程編程相比,IPC的缺點:
- 采用了某種形式的內(nèi)核開銷,降低了性能;
- 幾乎大部分IPC都不是程序設(shè)計的自然擴展,往往會大大地增加程序的復雜度。
對于進程和線程的聯(lián)系和區(qū)別,可以參看阮一峰老師的這篇圖文:進程與線程的一個簡單解釋,非常形象生動。
關(guān)于Android中的進程和線程,可以參看官方開發(fā)文檔:
https://developer.android.com/guide/components/processes-and-threads.html
(國內(nèi)的朋友也可以去這里:https://developer.android.google.cn/guide/components/processes-and-threads.html )
我們知道Android中要實現(xiàn)IPC,有好多種方式:
- 在Intent中附加extras來傳遞信息。
- 共享文件。
- SharedPreferences(不建議在進程間通信中使用,因為在多進程模式下,系統(tǒng)對SharedPreferences的讀/寫會變得不可靠,面對高并發(fā)的讀/寫訪問,有很大幾率會丟失數(shù)據(jù))。
- 基于Binder的AIDL。
- 基于Binder的Messenger(翻譯為“信使”,其實Messenger本質(zhì)上也是AIDL,只不過系統(tǒng)做了封裝以方便上層調(diào)用)。
- Socket。
- 天生支持跨進程訪問的ContentProvider。
然而,如果我們要在Android中自己來實現(xiàn)IPC這個協(xié)議,還是有點復雜的,主要因為需要實現(xiàn)數(shù)據(jù)管理系統(tǒng)(在進程或線程間傳遞數(shù)據(jù))。為了暫時減緩這個“會呼吸的痛”,Android為我們實現(xiàn)了一種定制的IPC,也就是梁靜茹,oh,sorry,是AIDL。
不要把AIDL和JNI及NDK混淆起來,這幾個的功用是這樣的:
- AIDL:是Android中IPC(進程間通信)的一種方式, 因為Android中不同應(yīng)用一般是位于不同進程中的,而即使同一個應(yīng)用中的組件(component。參看Android四大組件:Activity,Service,ContentProvider,BroadcastReceiver)也可以位于不同進程(通過在AndroidManifest.xml中為組件設(shè)置android:process屬性來實現(xiàn))。例如,同一個應(yīng)用中,如果Activity和Service兩者處于不同進程,但Activity 需要給Service傳遞一些信息,就可以用到AIDL這種機制。
- JNI:Java Native Interface的縮寫,表示“Java原生接口”。為了方便Java調(diào)用Native(原生)代碼(比如C和C++,等等)所封裝的一層接口。JNI是Java語言的東西,并不專屬于Android。
- NDK:Native Development Kit的縮寫,表示“原生開發(fā)工具集”。NDK是Google為Android開發(fā)的工具集,專屬于Android。利用NDK,我們可以在Android中更加方便地通過JNI來調(diào)用原生代碼(比如C和C++,等等)。NDK還提供了交叉編譯器,我們只需要簡單修改.mk文件就可以生成指定CPU平臺的動態(tài)庫。NDK還有其他一些優(yōu)勢。
2. 實現(xiàn)步驟
在Android官方開發(fā)文檔中有這么一段話,是關(guān)于IPC的:
Android offers a mechanism for interprocess communication (IPC) using remote procedure calls (RPCs), in which a method is called by an activity or other application component, but executed remotely (in another process), with any result returned back to the caller. This entails decomposing a method call and its data to a level the operating system can understand, transmitting it from the local process and address space to the remote process and address space, then reassembling and reenacting the call there. Return values are then transmitted in the opposite direction. Android provides all the code to perform these IPC transactions, so you can focus on defining and implementing the RPC programming interface.
To perform IPC, your application must bind to a service, using bindService(). For more information, see the Services developer guide.
翻譯如下:
Android利用遠程過程調(diào)用(Remote Procedure Call,簡稱RPC)提供了一種進程間通信(Inter-Process Communication,簡稱IPC)機制,通過這種機制,被Activity或其他應(yīng)用程序組件調(diào)用的方法將(在其他進程中)被遠程執(zhí)行,而所有的結(jié)果將被返回給調(diào)用者。這就要求把方法調(diào)用及其數(shù)據(jù)分解到操作系統(tǒng)可以理解的程度,并將其從本地的進程和地址空間傳輸至遠程的進程和地址空間,然后在遠程進程中重新組裝并執(zhí)行這個調(diào)用。執(zhí)行后的返回值將被反向傳輸回來。Android提供了執(zhí)行IPC事務(wù)所需的全部代碼,因此只要把注意力放在定義和實現(xiàn)RPC編程接口上即可。
要執(zhí)行IPC,應(yīng)用程序必須用bindService()綁定到服務(wù)上。詳情請參閱服務(wù)Services開發(fā)指南。
AIDL是IPC的一個輕量級實現(xiàn),用到了Java開發(fā)者很熟悉的語法。Android也提供了一個工具,可以自動創(chuàng)建Stub。
問:"Stub又是什么呢?"
答:"Stub在英語中是“樹樁”的意思,這個stub的概念并不是Android專有的,其他編程開發(fā)中也會用到,根據(jù)維基百科的解釋:
Stub(樁)指用來替換一部分功能的程序段。樁程序可以用來模擬已有程序的行為(比如一個遠端機器的過程)或是對將要開發(fā)的代碼的一種臨時替代。因此,打樁技術(shù)在程序移植、分布式計算、通用軟件開發(fā)和測試中用處很大。
因此,簡單的說,Android中的Stub是一個類,實現(xiàn)了遠程服務(wù)的接口,以便你能使用它,就好像此服務(wù)是在本地一樣。好比在本地打了一個遠程服務(wù)的“樁”,你就可以用來造房子什么的。"
當我們要在應(yīng)用間用AIDL來通信時,我們需要按以下幾步走:
- 定義一個AIDL接口。
- 為遠程服務(wù)(Service)實現(xiàn)對應(yīng)Stub。
- 將服務(wù)“暴露”給客戶程序使用。
3. 實例: HelloSumAIDL
AIDL的語法很類似Java的接口(Interface),只需要定義方法的簽名。
AIDL支持的數(shù)據(jù)類型與Java接口支持的數(shù)據(jù)類型有些不同:
- 所有基礎(chǔ)類型(int, char, 等)
- String,List,Map,CharSequence等類
- 其他AIDL接口類型
- 所有Parcelable的類
為了更好地展示AIDL的用法,我們來看一個很簡單的例子: 兩數(shù)相加。
3.1 創(chuàng)建工程
事不宜遲,我們就用Android Studio創(chuàng)建一個Android項目。
以下是項目的基本信息(不一定要一樣):
- 項目(project)名稱: HelloSumAIDL
- 包(package)名: com.android.hellosumaidl
- Activity名稱: HelloSumAidlActivity
點擊Next(下一步):
默認配置即可,點擊Next(下一步):
點擊Next(下一步):
點擊Finish(完成),Android Studio就會開始幫你創(chuàng)建新項目。稍等片刻,即可看到如下圖所示的項目:
3.2 創(chuàng)建AIDL文件
此時的項目視圖是默認的Android。
鼠標左鍵選中 HelloSumAIDL/app/src/main/java這個路徑,如下圖所示:
點擊鼠標右鍵,新建一個AIDL文件(依次選擇New->AIDL->AIDL File),取名為 IAdditionService。
點擊Finish。
Android Studio就會為你新建一個IAdditionService.aidl文件,位于新建的路徑 HelloSumAIDL/app/src/main/aidl 中,它的包名也是 com.android.hellosumaidl,因為包名在我們創(chuàng)建項目時已經(jīng)定了,可以在AndroidManifest.xml文件中可以看到
在新建的這個IAdditionService.aidl文件中將已有代碼替換為如下代碼:
package com.android.hellosumaidl;
// Interface declaration (接口聲明)
interface IAdditionService {
// You can pass the value of in, out or inout
// The primitive types (int, boolean, etc) are only passed by in
int add(in int value1, in int value2);
}
add是英語“加”的動詞。addition是“加”的名詞。
AIDL也有一些格式規(guī)范,主要是in和out關(guān)鍵字,in代表傳入的參數(shù),out代表輸出的參數(shù),inout代表傳入和輸出的參數(shù)。Java語言內(nèi)置的類型(比如int,boolean,等等)只能通過in來傳入。
一旦文件被保存,Android Studio會自動在 HelloSumAIDL/app/build/generated/source/aidl/debug/com/android/hellosumaidl 這個路徑(如果你的Favorites是release,那么debug會是release)里自動生成對應(yīng)的IAdditionService.java這個文件。
為了能看到app/build/generated/中的文件,需要把項目視圖從默認的Android改選為Project Files。
然后,你就能找到IAdditionService.java這個文件了,如下圖所示:
在這個文件里,我們可以看到add方法也被自動添加了:
因為IAdditionService.java這個文件是自動生成的,所以無需改動。這個文件里就包含了Stub,可以看到就是
public static abstract class Stub extends android.os.Binder implements com.android.hellosumaidl.IAdditionService
那一行。
我們接下來要為我們的遠程服務(wù)實現(xiàn)這個Stub。
3.3 實現(xiàn)遠程服務(wù)
首先我們來理清一下思路,現(xiàn)在我們的項目有兩個主要的文件:
HelloSumAIDL/app/src/main/java/com/android/hellosumaidl/HelloSumAidlActivity.java :這個HelloSumAidlActivity.java是我們的客戶端(client)。
HelloSumAIDL/app/src/main/aidl/com/android/hellosumaidl/IAdditionService.aidl :這個是AIDL。客戶端通過AIDL實現(xiàn)與服務(wù)端的通信。
注意:使用AIDL進行客戶端和服務(wù)端的通信有一個條件需要滿足,那就是服務(wù)器端的各個AIDL文件(因為aidl目錄下也許不止一個文件,我們項目中只創(chuàng)建了一個而已)須要被拷貝到客戶端的相同包名下,不然會不成功。例如:
- HelloSumAIDLServer/app/src/main/aidl/com/android/hellosumaidl/IAdditionService.aidl :假如HelloSumAIDLServer是一個表示AIDL服務(wù)端的Android項目。
- HelloSumAIDLClient/app/src/main/aidl/com/android/hellosumaidl/IAdditionService.aidl :假如HelloSumAIDLClient是一個表示AIDL客戶端的Android項目。
那么,HelloSumAIDLClient/app/src/main/aidl/com/android/hellosumaidl/IAdditionService.aidl 就要和 HelloSumAIDLServer/app/src/main/aidl/com/android/hellosumaidl/IAdditionService.aidl一樣。
我們這篇文章中,因為客戶端和服務(wù)端是在同一項目中,因此存在一份AIDL文件就夠了,就是 HelloSumAIDL/app/src/main/aidl/com/android/hellosumaidl/IAdditionService.aidl。
我們還沒有寫遠程服務(wù)端的代碼,因此我們來實現(xiàn)之:
在HelloSumAIDL/app/src/main/java/com/android/hellosumaidl 這個路徑中新建一個Service,取名叫AdditionService.java。這個就是我們的服務(wù)端了。
為了實現(xiàn)我們的服務(wù),我們需要讓這個類中的onBind方法返回一個IBinder類的對象。這個IBinder類的對象就代表了遠程服務(wù)的實現(xiàn)。
我們要用到自動生成的子類IAdditionService.Stub。在其中,我們也必須實現(xiàn)我們之前在AIDL文件中定義的add()函數(shù)。下面是我們遠程服務(wù)的代碼:
package com.android.hellosumaidl;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
/*
* This class exposes the service to client
* 服務(wù)端,將服務(wù)(service)"暴露"給客戶端(client)
*/
public class AdditionService extends Service {
public AdditionService() {
}
@Override
public IBinder onBind(Intent intent) {
return new IAdditionService.Stub() {
/*
* Implement com.android.hellosumaidl.IAdditionService.add(int, int)
* 實現(xiàn)了add方法
*/
@Override
public int add(int value1, int value2) throws RemoteException {
return value1 + value2;
}
};
}
}
AdditionService.java(服務(wù)端)和HelloSumAidlActivity.java(客戶端)被放在同一個路徑下:
3.4 “暴露”服務(wù)
一旦實現(xiàn)了服務(wù)中的onBind方法,我們就可以把客戶端程序(在我們的項目里是HelloSumAidlActivity.java)與服務(wù)連接起來了。
為了建立這樣的一個鏈接,我們需要實現(xiàn)ServiceConnection類。
我們在HelloSumAidlActivity.java中創(chuàng)建一個內(nèi)部類 AdditionServiceConnection,這個類繼承ServiceConnection類,并且重寫了它的兩個方法:onServiceConnected和onServiceDisconnected。
下面給出內(nèi)部類的代碼:
/*
* This inner class is used to connect to the service
* 這個內(nèi)部類用于連接到服務(wù)(service)
*/
class AdditionServiceConnection implements ServiceConnection {
@Override
public void onServiceConnected(ComponentName name, IBinder boundService) {
service = IAdditionService.Stub.asInterface(boundService);
Toast.makeText(HelloSumAidlActivity.this, "Service connected", Toast.LENGTH_LONG).show();
}
@Override
public void onServiceDisconnected(ComponentName name) {
service = null;
Toast.makeText(HelloSumAidlActivity.this, "Service disconnected", Toast.LENGTH_LONG).show();
}
}
3.5 相關(guān)代碼
為了完成我們的測試項目,我們需要首先改寫activity_hello_sum_aidl.xml(主界面的布局文件)和string.xml (字符串定義文件):
布局文件 activity_hello_sum_aidl.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/hello"
android:textSize="22sp" />
<EditText
android:id="@+id/value1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:hint="@string/hint1" >
</EditText>
<TextView
android:id="@+id/TextView01"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/plus"
android:textSize="36sp" />
<EditText
android:id="@+id/value2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:hint="@string/hint2" >
</EditText>
<Button
android:id="@+id/buttonCalc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:hint="@string/equal" >
</Button>
<TextView
android:id="@+id/result"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/result"
android:textSize="36sp" />
</LinearLayout>
string.xml
<resources>
<string name="app_name">HelloSumAIDL</string>
<string name="hello">Hello Sum AIDL</string>
<string name="result">Result</string>
<string name="plus">+</string>
<string name="equal">=</string>
<string name="hint1">Value 1</string>
<string name="hint2">Value 2</string>
</resources>
最后,我們的HelloSumAidlActivity.java如下:
package com.android.hellosumaidl;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
public class HelloSumAidlActivity extends AppCompatActivity {
IAdditionService service;
AdditionServiceConnection connection;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_hello_sum_aidl);
initService();
Button buttonCalc = (Button)findViewById(R.id.buttonCalc);
buttonCalc.setOnClickListener(new View.OnClickListener() {
EditText value1 = (EditText)findViewById(R.id.value1);
EditText value2= (EditText)findViewById(R.id.value2);
TextView result = (TextView)findViewById(R.id.result);
@Override
public void onClick(View v) {
int v1, v2, res = -1;
v1 = Integer.parseInt(value1.getText().toString());
v2 = Integer.parseInt(value2.getText().toString());
try {
res = service.add(v1, v2);
} catch (RemoteException e) {
e.printStackTrace();
}
result.setText(Integer.valueOf(res).toString());
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
releaseService();
}
/*
* This inner class is used to connect to the service
* 這個內(nèi)部類用于連接到服務(wù)(service)
*/
class AdditionServiceConnection implements ServiceConnection {
@Override
public void onServiceConnected(ComponentName name, IBinder boundService) {
service = IAdditionService.Stub.asInterface(boundService);
Toast.makeText(HelloSumAidlActivity.this, "Service connected", Toast.LENGTH_LONG).show();
}
@Override
public void onServiceDisconnected(ComponentName name) {
service = null;
Toast.makeText(HelloSumAidlActivity.this, "Service disconnected", Toast.LENGTH_LONG).show();
}
}
/*
* This method connects the Activity to the service
* 這個方法使Activity(客戶端)連接到服務(wù)(service)
*/
private void initService() {
connection = new AdditionServiceConnection();
Intent i = new Intent();
i.setClassName("com.android.hellosumaidl", com.android.hellosumaidl.AdditionService.class.getName());
bindService(i, connection, Context.BIND_AUTO_CREATE);
}
/*
* This method disconnects the Activity from the service
* 這個方法使Activity(客戶端)從服務(wù)(service)斷開
*/
private void releaseService() {
unbindService(connection);
connection = null;
}
}
將此項目運行起來,得到的兩個截圖如下:
4. 后記和源碼
光是一個AIDL,就涉及到很多Android知識點。所以說:Android是一個“龐然大物”,要學習的東西很多。“少年,路漫漫其修遠兮”,要成為Android大牛必須付出努力!
可以看到AIDL的原理還是著名的客戶端和服務(wù)端原理。其底層實現(xiàn)用到了Android的Binder。關(guān)于Binder的實現(xiàn)原理,可以去看《Android開發(fā)藝術(shù)探索》一書。
網(wǎng)上一般的AIDL實例是將服務(wù)端(Server)和客戶端(Client)分開放到兩個Android項目中,我們的這個項目,將服務(wù)端和客戶端放在同一個項目中,原理是類似的。
以上項目的源碼我放到自己的Github上了,歡迎查看、fork、下載。https://github.com/frogoscar/HelloSumAIDL
歡迎留言補充、指正,謝謝。
我是謝恩銘,在巴黎奮斗的軟件工程師。
熱愛生活,喜歡游泳,略懂烹飪。
人生格言:「向著標桿直跑」