四大元件之Service_AIDL

嶽小川發表於2019-03-04

四大元件之Service_AIDL

[文章內容來自Developers]

AIDL(Android 介面定義語言)與您可能使用過的其他 IDL 類似。 您可以利用它定義客戶端與服務使用程式間通訊 (IPC) 進行相互通訊時都認可的程式設計介面。 在 Android 上,一個程式通常無法訪問另一個程式的記憶體。 儘管如此,程式需要將其物件分解成作業系統能夠識別的原語,並將物件編組成跨越邊界的物件。 編寫執行這一編組操作的程式碼是一項繁瑣的工作,因此 Android 會使用 AIDL 來處理。

:只有允許不同應用的客戶端用 IPC 方式訪問服務,並且想要在服務中處理多執行緒時,才有必要使用 AIDL。 如果您不需要執行跨越不同應用的併發 IPC,就應該通過實現一個 Binder建立介面;或者,如果您想執行 IPC,但根本不需要處理多執行緒,則使用 Messenger 類來實現介面。無論如何,在實現 AIDL 之前,請您務必理解繫結服務

在您開始設計 AIDL 介面之前,要注意 AIDL 介面的呼叫是直接函式呼叫。 您不應該假設發生呼叫的執行緒。 視呼叫來自本地程式還是遠端程式中的執行緒,實際情況會有所差異。 具體而言:

  • 來自本地程式的呼叫在發起呼叫的同一執行緒內執行。如果該執行緒是您的主 UI 執行緒,則該執行緒繼續在 AIDL 介面中執行。 如果該執行緒是其他執行緒,則其便是在服務中執行您的程式碼的執行緒。 因此,只有在本地執行緒訪問服務時,您才能完全控制哪些執行緒在服務中執行(但如果真是這種情況,您根本不應該使用 AIDL,而是應該通過實現 Binder 類建立介面)。
  • 來自遠端程式的呼叫分派自平臺在您的自有程式內部維護的執行緒池。 您必須為來自未知執行緒的多次併發傳入呼叫做好準備。 換言之,AIDL 介面的實現必須是完全執行緒安全實現。
  • oneway關鍵字用於修改遠端呼叫的行為。使用該關鍵字時,遠端呼叫不會阻塞;它只是傳送事務資料並立即返回。介面的實現最終接收此呼叫時,是以正常遠端呼叫形式將其作為來自 Binder
    執行緒池的常規呼叫進行接收。 如果 oneway
    用於本地呼叫,則不會有任何影響,呼叫仍是同步呼叫。

定義 AIDL 介面


您必須使用 Java 程式語言語法在 .aidl檔案中定義 AIDL 介面,然後將它儲存在託管服務的應用以及任何其他繫結到服務的應用的原始碼(src/目錄)內。
您開發每個包含 .aidl檔案的應用時,Android SDK 工具都會生成一個基於該 .aidl檔案的 IBinder介面,並將其儲存在專案的 gen/目錄中。服務必須視情況實現 IBinder介面。然後客戶端應用便可繫結到該服務,並呼叫 IBinder中的方法來執行 IPC。
如需使用 AIDL 建立繫結服務,請執行以下步驟:

  • 建立 .aidl 檔案
    此檔案定義帶有方法簽名的程式設計介面。
  • 實現介面
    Android SDK 工具基於您的 .aidl檔案,使用 Java 程式語言生成一個介面。此介面具有一個名為 Stub 的內部抽象類,用於擴充套件 Binder類並實現 AIDL 介面中的方法。您必須擴充套件 Stub類並實現方法。
  • 向客戶端公開該介面
    實現 Service並重寫 onBind()以返回 Stub類的實現。

注意:在 AIDL 介面首次釋出後對其進行的任何更改都必須保持向後相容性,以避免中斷其他應用對您的服務的使用。 也就是說,因為必須將您的 .aidl檔案複製到其他應用,才能讓這些應用訪問您的服務的介面,因此您必須保留對原始介面的支援。

1. 建立 .aidl 檔案
AIDL 使用簡單語法,使您能通過可帶引數和返回值的一個或多個方法來宣告介面。 引數和返回值可以是任意型別,甚至可以是其他 AIDL 生成的介面。
您必須使用 Java 程式語言構建 .aidl檔案。每個 .aidl檔案都必須定義單個介面,並且只需包含介面宣告和方法簽名。
預設情況下,AIDL 支援下列資料型別:

  • Java 程式語言中的所有原語型別(如 int、long、char、boolean等等)
  • String
  • CharSequence
  • List
    List中的所有元素都必須是以上列表中支援的資料型別、其他 AIDL 生成的介面或您宣告的可打包型別。 可選擇將 List 用作“通用”類(例如,List)。另一端實際接收的具體類始終是 ArrayList,但生成的方法使用的是 List介面。
  • Map
    Map中的所有元素都必須是以上列表中支援的資料型別、其他 AIDL 生成的介面或您宣告的可打包型別。 不支援通用 Map(如Map形式的 Map)。 另一端實際接收的具體類始終是 HashMap,但生成的方法使用的是 Map 介面。

您必須為以上未列出的每個附加型別加入一個 import語句,即使這些型別是在與您的介面相同的軟體包中定義。
定義服務介面時,請注意:

  • 方法可帶零個或多個引數,返回值或空值。
    所有非原語引數都需要指示資料走向的方向標記。可以是 in、out或 inout(見以下示例)。原語預設為 in,不能是其他方向。

    注意:您應該將方向限定為真正需要的方向,因為編組引數的開銷極大。

  • .aidl檔案中包括的所有程式碼註釋都包含在生成的 IBinder介面中(import 和 package 語句之前的註釋除外)
  • 只支援方法;您不能公開 AIDL 中的靜態欄位。

以下是一個 .aidl
檔案示例:

// IRemoteService.aidlpackage com.example.android;
// Declare any non-default types here with import statements
/**
 Example service interface
 */
interface IRemoteService {
    /**
 Request the process ID of this service, to do evil things with it. 
*/
    int getPid();
    /**
 Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);}複製程式碼

只需將您的 .aidl檔案儲存在專案的 src/目錄內,當您開發應用時,SDK 工具會在專案的 gen/目錄中生成 IBinder介面檔案。生成的檔名與 .aidl檔名一致,只是使用了 .java副檔名(例如,IRemoteService.aidl生成的檔名是 IRemoteService.java)。
如果您使用 Android Studio,增量編譯幾乎會立即生成 Binder 類。 如果您不使用 Android Studio,則 Gradle 工具會在您下一次開發應用時生成 Binder 類 — 您應該在編寫完 .aidl檔案後立即用 gradle assembleDebug(或 gradle assembleRelease)編譯專案,以便您的程式碼能夠連結到生成的類。

2.實現介面
當您開發應用時,Android SDK 工具會生成一個以 .aidl檔案命名的 .java介面檔案。生成的介面包括一個名為 Stub的子類,這個子類是其父介面(例如,YourInterface.Stub)的抽象實現,用於宣告 .aidl檔案中的所有方法。

注:Stub還定義了幾個幫助程式方法,其中最引人關注的是 asInterface(),該方法帶 IBinder(通常便是傳遞給客戶端 onServiceConnected()回撥方法的引數)並返回存根介面例項。 如需瞭解如何進行這種轉換的更多詳細資訊,請參見呼叫 IPC 方法。

如需實現 .aidl生成的介面,請擴充套件生成的 Binder介面(例如,YourInterface.Stub)並實現從 .aidl檔案繼承的方法。
以下是一個使用匿名例項實現名為 IRemoteService的介面(由以上 IRemoteService.aidl示例定義)的示例:

private final IRemoteService.Stub mBinder = new IRemoteService.Stub() {
    public int getPid(){
        return Process.myPid();
    }
    public void basicTypes(int anInt, long aLong, boolean aBoolean,
        float aFloat, double aDouble, String aString) {
        // Does nothing
    }
};複製程式碼

現在,mBinder是 Stub類的一個例項(一個 Binder),用於定義服務的 RPC 介面。 在下一步中,將向客戶端公開該例項,以便客戶端能與服務進行互動。
在實現 AIDL 介面時應注意遵守以下這幾個規則:

  • 由於不能保證在主執行緒上執行傳入呼叫,因此您一開始就需要做好多執行緒處理準備,並將您的服務正確地編譯為執行緒安全服務。
  • 預設情況下,RPC 呼叫是同步呼叫。如果您明知服務完成請求的時間不止幾毫秒,就不應該從 Activity 的主執行緒呼叫服務,因為這樣做可能會使應用掛起(Android 可能會顯示“Application is Not Responding”對話方塊)— 您通常應該從客戶端內的單獨執行緒呼叫服務。
  • 您引發的任何異常都不會回傳給呼叫方。

3.向客戶端公開該介面
您為服務實現該介面後,就需要向客戶端公開該介面,以便客戶端進行繫結。 要為您的服務公開該介面,請擴充套件 Service並實現 onBind(),以返回一個類例項,這個類實現了生成的 Stub(見前文所述)。以下是一個向客戶端公開 IRemoteService
示例介面的服務示例。

public class RemoteService extends Service {
    @Override
    public void onCreate() {
        super.onCreate();
    }
    @Override
    public IBinder onBind(Intent intent) {
        // Return the interface
        return mBinder;
    }
    private final IRemoteService.Stub mBinder = new IRemoteService.Stub() {
        public int getPid(){
            return Process.myPid();
        }
        public void basicTypes(int anInt, long aLong, boolean aBoolean,
            float aFloat, double aDouble, String aString) {
            // Does nothing
        }
    };
}複製程式碼

現在,當客戶端(如 Activity)呼叫 bindService() 以連線此服務時,客戶端的 onServiceConnected()回撥會接收服務的 onBind()方法返回的 mBinder例項。
客戶端還必須具有對 interface 類的訪問許可權,因此如果客戶端和服務在不同的應用內,則客戶端的應用 src/目錄內必須包含 .aidl檔案(它生成 android.os.Binder介面 — 為客戶端提供對 AIDL 方法的訪問許可權)的副本。
當客戶端在 onServiceConnected()回撥中收到 IBinder時,它必須呼叫 YourServiceInterface.Stub.asInterface(service)
以將返回的引數轉換成 YourServiceInterface
型別。例如:

IRemoteService mIRemoteService;private ServiceConnection mConnection = new ServiceConnection() {
    // Called when the connection with the service is established
    public void onServiceConnected(ComponentName className, IBinder service) {
        // Following the example above for an AIDL interface,
        // this gets an instance of the IRemoteInterface, which we can use to call on the service
        mIRemoteService = IRemoteService.Stub.asInterface(service);
    }
    // Called when the connection with the service disconnects unexpectedly
    public void onServiceDisconnected(ComponentName className) {
        Log.e(TAG, "Service has unexpectedly disconnected");
        mIRemoteService = null;
    }
};複製程式碼

如需檢視更多示例程式碼,請參見 ApiDemos 中的 RemoteService.java 類。

通過 IPC 傳遞物件


通過 IPC 介面把某個類從一個程式傳送到另一個程式是可以實現的。 不過,您必須確保該類的程式碼對 IPC 通道的另一端可用,並且該類必須支援Parcelable 介面。支援 Parcelable介面很重要,因為 Android 系統可通過它將物件分解成可編組到各程式的原語。
如需建立支援 Parcelable協議的類,您必須執行以下操作:

  • 讓您的類實現 Parcelable介面。
  • 實現 writeToParcel,它會獲取物件的當前狀態並將其寫入 Parcel。
  • 為您的類新增一個名為 CREATOR的靜態欄位,這個欄位是一個實現 Parcelable.Creator介面的物件。
  • 最後,建立一個宣告可打包類的 .aidl檔案(按照下文 Rect.aidl檔案所示步驟)。如果您使用的是自定義編譯程式,切勿**在您的編譯中新增 .aidl檔案。 此 .aidl檔案與 C 語言中的標頭檔案類似,並未編譯。

AIDL 在它生成的程式碼中使用這些方法和欄位將您的物件編組和取消編組。
例如,以下這個 Rect.aidl檔案可建立一個可打包的 Rect類:

package android.graphics;
// Declare Rect so AIDL can find it and knows that it implements
// the parcelable protocol.
parcelable Rect;複製程式碼

以下示例展示了 Rect類如何實現 Parcelable協議。

import android.os.Parcel;
import android.os.Parcelable;
public final class Rect implements Parcelable {
    public int left;
    public int top;
    public int right;
    public int bottom;
    public static final Parcelable.Creator<Rect> CREATOR = newParcelable.Creator<Rect>() {
        public Rect createFromParcel(Parcel in) {
            return new Rect(in);
        }
        public Rect[] newArray(int size) {
            return new Rect[size];
        }
    };
    public Rect() {
    }
    private Rect(Parcel in) {
        readFromParcel(in);
    }
    public void writeToParcel(Parcel out) {
        out.writeInt(left);
        out.writeInt(top);
        out.writeInt(right);
        out.writeInt(bottom);
    }
    public void readFromParcel(Parcel in) {
        left = in.readInt();
        top = in.readInt();
        right = in.readInt();
        bottom = in.readInt();
    }
}複製程式碼

Rect
類中的編組相當簡單。看一看 Parcel上的其他方法,瞭解您可以向 Parcel 寫入哪些其他型別的值。

警告:別忘記從其他程式接收資料的安全影響。 在本例中,Rect從 Parcel讀取四個數字,但要由您來確保無論呼叫方目的為何這些數字都在相應的可接受值範圍內。

呼叫 IPC 方法


呼叫類必須執行以下步驟,才能呼叫使用 AIDL 定義的遠端介面:

  1. 在專案 src/目錄中加入 .aidl檔案。
  2. 宣告一個 IBinder 介面例項(基於 AIDL 生成)。
  3. 實現 ServiceConnection。
  4. 呼叫 Context.bindService(),以傳入您的 ServiceConnection實現。
  5. 在您的 onServiceConnected()實現中,您將收到一個 IBinder例項(名為 service)。呼叫YourInterfaceName.Stub.asInterface((IBinder)service)
    ,以將返回的引數轉換為 YourInterface 型別。
  6. 呼叫您在介面上定義的方法。您應該始終捕獲 DeadObjectException異常,它們是在連線中斷時引發的;這將是遠端方法引發的唯一異常。
  7. 如需斷開連線,請使用您的介面例項呼叫 Context.unbindService()。

有關呼叫 IPC 服務的幾點說明:

  • 物件是跨程式計數的引用。
  • 您可以將匿名物件作為方法引數傳送。

如需瞭解有關繫結到服務的詳細資訊,請閱讀繫結服務文件。
以下這些示例程式碼摘自 ApiDemos 專案的遠端服務示例程式碼,展示瞭如何呼叫 AIDL 建立的服務。

public static class Binding extends Activity {
    /**
 The primary interface we will be calling on the service.
 */
    IRemoteService mService = null;
    /**
 Another interface we use on the service.
 */
    ISecondary mSecondaryService = null;
    Button mKillButton;
    TextView mCallbackText;
    private boolean mIsBound;
    /**
     * Standard initialization of this activity.  Set up the UI, then wait
     * for the user to poke it before doing anything.
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.remote_service_binding);
        // Watch for button clicks.
        Button button = (Button)findViewById(R.id.bind);
        button.setOnClickListener(mBindListener);
        button = (Button)findViewById(R.id.unbind);
        button.setOnClickListener(mUnbindListener);
        mKillButton = (Button)findViewById(R.id.kill);        mKillButton.setOnClickListener(mKillListener);
        mKillButton.setEnabled(false);
        mCallbackText = (TextView)findViewById(R.id.callback);
        mCallbackText.setText("Not attached.");
    }
    /**
     * Class for interacting with the main interface of the service.
     */
    private ServiceConnection mConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className,
                IBinder service) {
            // This is called when the connection with the service has been
            // established, giving us the service object we can use to
            // interact with the service.  We are communicating with our
            // service through an IDL interface, so get a client-side
            // representation of that from the raw service object.
            mService = IRemoteService.Stub.asInterface(service);
            mKillButton.setEnabled(true);
            mCallbackText.setText("Attached.");
            // We want to monitor the service for as long as we are
            // connected to it.
            try {
                mService.registerCallback(mCallback);
            } catch (RemoteException e) {
                // In this case the service has crashed before we could even
                // do anything with it; we can count on soon being
                // disconnected (and then reconnected if it can be restarted)
                // so there is no need to do anything here.
            }
            // As part of the sample, tell the user what happened.
            Toast.makeText(Binding.this, R.string.remote_service_connected,
                    Toast.LENGTH_SHORT).show();
        }
        public void onServiceDisconnected(ComponentName className) {
            // This is called when the connection with the service has been
            // unexpectedly disconnected -- that is, its process crashed.
            mService = null;
            mKillButton.setEnabled(false);
            mCallbackText.setText("Disconnected.");
            // As part of the sample, tell the user what happened.            Toast.makeText(Binding.this, R.string.remote_service_disconnected,
                    Toast.LENGTH_SHORT).show();
        }
    };
    /**
     * Class for interacting with the secondary interface of the service.
     */
    private ServiceConnection mSecondaryConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className,
                IBinder service) {
            // Connecting to a secondary interface is the same as any
            // other interface.
            mSecondaryService = ISecondary.Stub.asInterface(service);
            mKillButton.setEnabled(true);
        }
        public void onServiceDisconnected(ComponentName className) {
            mSecondaryService = null;
            mKillButton.setEnabled(false);
        }
    };
    private OnClickListener mBindListener = new OnClickListener() {
        public void onClick(View v) {
            // Establish a couple connections with the service, binding
            // by interface names.  This allows other applications to be
            // installed that replace the remote service by implementing
            // the same interface.
            Intent intent = new Intent(Binding.this, RemoteService.class);
            intent.setAction(IRemoteService.class.getName());
            bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
            intent.setAction(ISecondary.class.getName());
            bindService(intent, mSecondaryConnection, Context.BIND_AUTO_CREATE);
            mIsBound = true;
            mCallbackText.setText("Binding.");
        }
    };
    private OnClickListener mUnbindListener = new OnClickListener() {
        public void onClick(View v) {
            if (mIsBound) {
                // If we have received the service, and hence registered with
                // it, then now is the time to unregister.
                if (mService != null) {
                    try {
                        mService.unregisterCallback(mCallback);
                    } catch (RemoteException e) {
                        // There is nothing special we need to do if the service
                        // has crashed.
                    }
                }
                // Detach our existing connection.
                unbindService(mConnection);
                unbindService(mSecondaryConnection);
                mKillButton.setEnabled(false);
                mIsBound = false;
                mCallbackText.setText("Unbinding.");
            }
        }
    };
    private OnClickListener mKillListener = new OnClickListener() {
        public void onClick(View v) {
            // To kill the process hosting our service, we need to know its
            // PID.  Conveniently our service has a call that will return
            // to us that information.
            if (mSecondaryService != null) {
                try {
                    int pid = mSecondaryService.getPid();
                    // Note that, though this API allows us to request to
                    // kill any process based on its PID, the kernel will
                    // still impose standard restrictions on which PIDs you
                    // are actually able to kill.  Typically this means only
                    // the process running your application and any additional
                    // processes created by that app as shown here; packages
                    // sharing a common UID will also be able to kill each
                    // other's processes.
                    Process.killProcess(pid);
                    mCallbackText.setText("Killed service process.");
                } catch (RemoteException ex) {
                    // Recover gracefully from the process hosting the
                    // server dying.
                    // Just for purposes of the sample, put up a notification.
                    Toast.makeText(Binding.this,                            R.string.remote_call_failed,
                            Toast.LENGTH_SHORT).show();
                }
            }
        }
    };
    // ----------------------------------------------------------------------    // Code showing how to deal with callbacks.
    // ----------------------------------------------------------------------
    /**
     * This implementation is used to receive callbacks from the remote
     * service.
     */
    private IRemoteServiceCallback mCallback = new IRemoteServiceCallback.Stub() {
        /**
         * This is called by the remote service regularly to tell us about
         * new values.  Note that IPC calls are dispatched through a thread
         * pool running in each process, so the code executing here will
         * NOT be running in our main thread like most other things -- so,
         * to update the UI, we need to use a Handler to hop over there.
         */
        public void valueChanged(int value) {            mHandler.sendMessage(mHandler.obtainMessage(BUMP_MSG, value, 0));
        }
    };
    private static final int BUMP_MSG = 1;
    private Handler mHandler = new Handler() {
        @Override
 public void handleMessage(Message msg) {
            switch (msg.what) {
                case BUMP_MSG:
                    mCallbackText.setText("Received from service: " + msg.arg1);
                    break;
                default:
                    super.handleMessage(msg);
            }
        }
    };
}複製程式碼

相關文章