Android探索之旅 | AIDL原理和實例講解

-- 做者 謝恩銘 轉載請註明出處javascript

前言


爲使應用程序之間可以彼此通訊,Android提供了IPC (Inter Process Communication,進程間通訊)的一種獨特實現: AIDL (Android Interface Definition Language, Android接口定義語言)。html

網上有很多關於AIDL的文章,寫得都很不錯。不過例子構造大多略微複雜: 創建兩個Android項目,一個是client(客戶端),一個是server(服務端,提供service(服務))。java

這篇文章將首先介紹AIDL的原理,再經過一個Android項目來介紹AIDL用法。服務端和客戶端包含在這同一個項目中,原理和分別在兩個項目中是同樣的,不太輕省許多。android

源碼在個人Github上,文末有放出。git

這篇博文包含如下四個部分:


  1. AIDL介紹
  2. 實現步驟
  3. 實例: HelloSumAIDL
    3.1 建立工程
    3.2 定義AIDL文件
    3.3 實現遠程服務(Service)
    3.4 「暴露」服務
    3.5 相關代碼
  4. 後記和源碼

1. AIDL介紹


在Android中,默認每一個應用(application)執行在它本身的進程中,沒法直接調用到其餘應用的資源,這也符合「沙箱」(SandBox)的理念。所謂沙箱原理,通常來講用在移動電話業務中,簡單地說旨在部分地或所有地隔離應用程序。程序員

Android沙箱技術:
Android「沙箱」的本質是爲了實現不一樣應用程序和進程之間的互相隔離,即在默認狀況 下,應用程序沒有權限訪問系統資源或其它應用程序的資源。
每一個APP和系統進程都被分配惟一而且固定的User Id(用戶身份標識),這個uid與內核層進程的uid對應。
每一個APP在各自獨立的Dalvik虛擬機中運行,擁有獨立的地址空間和資源。
運行於Dalvik虛擬機中的進程必須依託內核層Linux進程而存在,所以Android使用Dalvik虛擬機和Linux的文件訪問控制來實現沙箱機制,任何應用程序若是想要訪問系統資源或者其它應用程序的資源必須在本身的manifest文件中進行聲明權限或者共享uid。
本段關於沙箱的解釋轉載自:Android的權限機制之—— 「沙箱」機制sharedUserId和簽名github

所以,在Android中,當一個應用被執行時,有一些操做是被限制的,好比訪問內存,訪問傳感器,等等。這樣作能夠最大化地保護系統,省得應用程序「隨心所欲」。web

那咱們有時須要在應用間交互,怎麼辦呢?因而,Android須要實現IPC協議。編程

關於IPC協議,能夠參看下面摘自維基百科的內容:瀏覽器

進程間通訊(IPC,Inter-Process Communication),指至少兩個進程或線程間傳送數據或信號的一些技術或方法。
進程是計算機系統分配資源的最小單位(嚴格說來是線程)。每一個進程都有本身的一部分獨立的系統資源,彼此是隔離的。
爲了能使不一樣的進程互相訪問資源並進行協調工做,纔有了進程間通訊。舉一個典型的例子,使用進程間通訊的兩個應用能夠被分類爲客戶端和服務器(主從式架構),客戶端進程請求數據,服務端回覆客戶端的數據請求。有一些應用自己既是服務器又是客戶端,這在分佈式計算中,時常能夠見到。這些進程能夠運行在同一計算機上或網絡鏈接的不一樣計算機上。
進程間通訊技術包括消息傳遞、同步、共享內存和遠程過程調用(Remote Procedure Call,縮寫是RPC)。IPC是一種標準的Unix通訊機制。

使用IPC 的理由:

  • 信息共享:Web服務器,經過網頁瀏覽器使用進程間通訊來共享web文件(網頁等)和多媒體。
  • 加速:維基百科使用經過進程間通訊進行交流的多服務器來知足用戶的請求。
  • 模塊化。
  • 私有權分離。

與直接共享內存地址空間的多線程編程相比,IPC的缺點:

  • 採用了某種形式的內核開銷,下降了性能;
  • 幾乎大部分IPC都不是程序設計的天然擴展,每每會大大地增長程序的複雜度。

對於進程和線程的聯繫和區別,能夠參看阮一峯老師的這篇圖文:進程與線程的一個簡單解釋,很是形象生動。

關於Android中的進程和線程,能夠參看官方開發文檔:
developer.android.com/guide/compo…
(國內的朋友也能夠去這裏:developer.android.google.cn/guide/compo…


咱們知道Android中要實現IPC,有好多種方式:

  1. 在Intent中附加extras來傳遞信息。
  2. 共享文件。
  3. SharedPreferences(不建議在進程間通訊中使用,由於在多進程模式下,系統對SharedPreferences的讀/寫會變得不可靠,面對高併發的讀/寫訪問,有很大概率會丟失數據)。
  4. 基於Binder的AIDL。
  5. 基於Binder的Messenger(翻譯爲「信使」,其實Messenger本質上也是AIDL,只不過系統作了封裝以方便上層調用)。
  6. Socket。
  7. 天生支持跨進程訪問的ContentProvider。

然而,若是咱們要在Android中本身來實現IPC這個協議,仍是有點複雜的,主要由於須要實現數據管理系統(在進程或線程間傳遞數據)。爲了暫時減緩這個「會呼吸的痛」,Android爲咱們實現了一種定製的IPC,也就是梁靜茹,oh,sorry,是AIDL。

不要把AIDL和JNI及NDK混淆起來,這幾個的功用是這樣的:

  • AIDL:是Android中IPC(進程間通訊)的一種方式, 由於Android中不一樣應用通常是位於不一樣進程中的,而即便同一個應用中的組件(component。參看Android四大組件:Activity,Service,ContentProvider,BroadcastReceiver)也能夠位於不一樣進程(經過在AndroidManifest.xml中爲組件設置android:process屬性來實現)。例如,同一個應用中,若是Activity和Service二者處於不一樣進程,但Activity 須要給Service傳遞一些信息,就能夠用到AIDL這種機制。

  • JNI:Java Native Interface的縮寫,表示「Java原生接口」。爲了方便Java調用Native(原生)代碼(好比C和C++,等等)所封裝的一層接口。JNI是Java語言的東西,並不專屬於Android。

  • NDK:Native Development Kit的縮寫,表示「原生開發工具集」。NDK是Google爲Android開發的工具集,專屬於Android。利用NDK,咱們能夠在Android中更加方便地經過JNI來調用原生代碼(好比C和C++,等等)。NDK還提供了交叉編譯器,咱們只須要簡單修改.mk文件就能夠生成指定CPU平臺的動態庫。NDK還有其餘一些優點。

2. 實現步驟


在Android官方開發文檔中有這麼一段話,是關於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利用遠程過程調用(Remote Procedure Call,簡稱RPC)提供了一種進程間通訊(Inter-Process Communication,簡稱IPC)機制,經過這種機制,被Activity或其餘應用程序組件調用的方法將(在其餘進程中)被遠程執行,而全部的結果將被返回給調用者。這就要求把方法調用及其數據分解到操做系統能夠理解的程度,並將其從本地的進程和地址空間傳輸至遠程的進程和地址空間,而後在遠程進程中從新組裝並執行這個調用。執行後的返回值將被反向傳輸回來。Android提供了執行IPC事務所需的所有代碼,所以只要把注意力放在定義和實現RPC編程接口上便可。

要執行IPC,應用程序必須用bindService()綁定到服務上。詳情請參閱服務Services開發指南

AIDL是IPC的一個輕量級實現,用到了Java開發者很熟悉的語法。Android也提供了一個工具,能夠自動建立Stub。

問:"Stub又是什麼呢?"
答:"Stub在英語中是「樹樁」的意思,這個stub的概念並非Android專有的,其餘編程開發中也會用到,根據維基百科的解釋:

Stub(樁)指用來替換一部分功能的程序段。樁程序能夠用來模擬已有程序的行爲(好比一個遠端機器的過程)或是對將要開發的代碼的一種臨時替代。所以,打樁技術在程序移植、分佈式計算、通用軟件開發和測試中用處很大。

所以,簡單的說,Android中的Stub是一個類,實現了遠程服務的接口,以便你能使用它,就好像此服務是在本地同樣。比如在本地打了一個遠程服務的「樁」,你就能夠用來造房子什麼的。"

當咱們要在應用間用AIDL來通訊時,咱們須要按如下幾步走:

  1. 定義一個AIDL接口。
  2. 爲遠程服務(Service)實現對應Stub。
  3. 將服務「暴露」給客戶程序使用。

3. 實例: HelloSumAIDL


AIDL的語法很相似Java的接口(Interface),只須要定義方法的簽名。

AIDL支持的數據類型與Java接口支持的數據類型有些不一樣:

  1. 全部基礎類型(int, char, 等)
  2. String,List,Map,CharSequence等類
  3. 其餘AIDL接口類型
  4. 全部Parcelable的類

爲了更好地展現AIDL的用法,咱們來看一個很簡單的例子: 兩數相加。

3.1 建立工程


事不宜遲,咱們就用Android Studio建立一個Android項目。

如下是項目的基本信息(不必定要同樣):

  • 項目(project)名稱: HelloSumAIDL
  • 包(package)名: com.android.hellosumaidl
  • Activity名稱: HelloSumAidlActivity

新建項目

點擊Next(下一步):

選擇平臺:手機和平板,及最小SDK

默認配置便可,點擊Next(下一步):

選擇空白Activity

點擊Next(下一步):

填寫Activity信息

點擊Finish(完成),Android Studio就會開始幫你建立新項目。稍等片刻,便可看到以下圖所示的項目:

3.2 建立AIDL文件


此時的項目視圖是默認的Android。

鼠標左鍵選中 HelloSumAIDL/app/src/main/java這個路徑,以下圖所示:

點擊鼠標右鍵,新建一個AIDL文件(依次選擇New->AIDL->AIDL File),取名爲 IAdditionService。

填入AIDL文件名 IAdditionService

點擊Finish。

Android Studio就會爲你新建一個IAdditionService.aidl文件,位於新建的路徑 HelloSumAIDL/app/src/main/aidl 中,它的包名也是 com.android.hellosumaidl,由於包名在咱們建立項目時已經定了,能夠在AndroidManifest.xml文件中能夠看到

項目的包名是 com.android.hellosumaidl

在新建的這個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也有一些格式規範,主要是in和out關鍵字,in表明傳入的參數,out表明輸出的參數,inout表明傳入和輸出的參數。Java語言內置的類型(好比int,boolean,等等)只能經過in來傳入。

IAdditionService.aidl文件

一旦文件被保存,Android Studio會自動在 HelloSumAIDL/app/build/generated/source/aidl/debug/com/android/hellosumaidl 這個路徑(若是你的Favorites是release,那麼debug會是release)裏自動生成對應的IAdditionService.java這個文件。

爲了能看到app/build/generated/中的文件,須要把項目視圖從默認的Android改選爲Project Files。

從默認的Android改選爲Project Files

而後,你就能找到IAdditionService.java這個文件了,以下圖所示:

IAdditionService.java文件

在這個文件裏,咱們能夠看到add方法也被自動添加了:

由於IAdditionService.java這個文件是自動生成的,因此無需改動。這個文件裏就包含了Stub,能夠看到就是

public static abstract class Stub extends android.os.Binder implements com.android.hellosumaidl.IAdditionService複製代碼

那一行。

咱們接下來要爲咱們的遠程服務實現這個Stub。

3.3 實現遠程服務


首先咱們來理清一下思路,如今咱們的項目有兩個主要的文件:

  • 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實現與服務端的通訊。

注意:使用AIDL進行客戶端和服務端的通訊有一個條件須要知足,那就是服務器端的各個AIDL文件(由於aidl目錄下也許不止一個文件,咱們項目中只建立了一個而已)需要被拷貝到客戶端的相同包名下,否則會不成功。例如:

  • HelloSumAIDLServer/app/src/main/aidl/com/android/hellosumaidl/IAdditionService.aidl :假如HelloSumAIDLServer是一個表示AIDL服務端的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同樣。

咱們這篇文章中,由於客戶端和服務端是在同一項目中,所以存在一份AIDL文件就夠了,就是 HelloSumAIDL/app/src/main/aidl/com/android/hellosumaidl/IAdditionService.aidl。

咱們尚未寫遠程服務端的代碼,所以咱們來實現之:

在HelloSumAIDL/app/src/main/java/com/android/hellosumaidl 這個路徑中新建一個Service,取名叫AdditionService.java。這個就是咱們的服務端了。

建立AdditionService.java

爲了實現咱們的服務,咱們須要讓這個類中的onBind方法返回一個IBinder類的對象。這個IBinder類的對象就表明了遠程服務的實現。

咱們要用到自動生成的子類IAdditionService.Stub。在其中,咱們也必須實現咱們以前在AIDL文件中定義的add()函數。下面是咱們遠程服務的代碼:

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 * 服務端,將服務(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) * 實現了add方法 */
      @Override
      public int add(int value1, int value2) throws RemoteException {
        return value1 + value2;
      }
    };
  }
}複製代碼

AdditionService.java(服務端)和HelloSumAidlActivity.java(客戶端)被放在同一個路徑下:

3.4 「暴露」服務


一旦實現了服務中的onBind方法,咱們就能夠把客戶端程序(在咱們的項目裏是HelloSumAidlActivity.java)與服務鏈接起來了。

爲了創建這樣的一個連接,咱們須要實現ServiceConnection類。

咱們在HelloSumAidlActivity.java中建立一個內部類 AdditionServiceConnection,這個類繼承ServiceConnection類,而且重寫了它的兩個方法:onServiceConnected和onServiceDisconnected。

下面給出內部類的代碼:

/* * This inner class is used to connect to the service * 這個內部類用於鏈接到服務(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 相關代碼


爲了完成咱們的測試項目,咱們須要首先改寫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 * 這個內部類用於鏈接到服務(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(客戶端)鏈接到服務(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(客戶端)從服務(service)斷開 */
  private void releaseService() {
    unbindService(connection);
    connection = null;
  }
}複製代碼

將此項目運行起來,獲得的兩個截圖以下:

Fig 1 : 填寫數字前

Fig 2 : 按下計算按鈕(等號)後

4. 後記和源碼


  1. 光是一個AIDL,就涉及到不少Android知識點。因此說:Android是一個「龐然大物」,要學習的東西不少。「少年,路漫漫其修遠兮」,要成爲Android大牛必須付出努力!

  2. 能夠看到AIDL的原理仍是著名的客戶端和服務端原理。其底層實現用到了Android的Binder。關於Binder的實現原理,能夠去看《Android開發藝術探索》一書。

  3. 網上通常的AIDL實例是將服務端(Server)和客戶端(Client)分開放到兩個Android項目中,咱們的這個項目,將服務端和客戶端放在同一個項目中,原理是相似的。

  4. 以上項目的源碼我放到本身的Github上了,歡迎查看、fork、下載。github.com/frogoscar/H…

  5. 歡迎留言補充、指正,謝謝。


人世間,
萬千情感皆有溫度,
千萬代碼似有性格。
這裏有原創教程,IT叢林......
和你一塊兒探索程序人生。
微信公衆號「程序員聯盟」ProgrammerLeague
我是謝恩銘,在巴黎奮鬥的嵌入式軟件工程師。
我的簡介熱愛生活,喜歡游泳,略懂烹飪。人生格言:「向着標杆直跑」

相關文章
相關標籤/搜索