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

very_on發表於2018-07-08

前言


為使應用程式之間能夠彼此通訊,Android提供了IPC (Inter Process Communication,程式間通訊)的一種獨特實現: AIDL (Android Interface Definition Language, Android介面定義語言)。

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

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

原始碼在我的Github上,文末有放出。

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


  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和簽名

因此,在Android中,當一個應用被執行時,有一些操作是被限制的,比如訪問記憶體,訪問感測器,等等。這樣做可以最大化地保護系統,免得應用程式“為所欲為”。

那我們有時需要在應用間互動,怎麼辦呢?於是,Android需要實現IPC協議。

關於IPC協議,可以參看下面摘自維基百科的內容:

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

使用IPC 的理由:

  • 資訊共享:Web伺服器,通過網頁瀏覽器使用程式間通訊來共享web檔案(網頁等)和多媒體。
  • 加速:維基百科使用通過程式間通訊進行交流的多伺服器來滿足使用者的請求。
  • 模組化。
  • 私有權分離。

與直接共享記憶體地址空間的多執行緒程式設計相比,IPC的缺點:

  • 採用了某種形式的核心開銷,降低了效能;
  • 幾乎大部分IPC都不是程式設計的自然擴充套件,往往會大大地增加程式的複雜度。

對於程式和執行緒的聯絡和區別,可以參看阮一峰老師的這篇圖文:程式與執行緒的一個簡單解釋,非常形象生動。

關於Android中的程式和執行緒,可以參看官方開發文件:
https://developer.android.com/guide/components/processes-and-threads.html
(國內的朋友也可以去這裡:https://developer.android.google.cn/guide/components/processes-and-threads.html


我們知道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、下載。https://github.com/frogoscar/HelloSumAIDL

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



作者:程式設計師聯盟
連結:https://www.jianshu.com/p/ef86f682a8f9
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。

相關文章