Android進程間通信(一):AIDL使用詳解

来源:https://www.cnblogs.com/sqchen/archive/2019/04/06/10660939.html
-Advertisement-
Play Games

一、概述 AIDL是Android Interface Definition Language的縮寫,即Android介面定義語言。它是Android的進程間通信比較常用的一種方式。 Android中,每一個進程都有自己的Dalvik VM實例,擁有自己的獨立的記憶體空間,進程與進程之間不共用記憶體,這 ...


一、概述

AIDL是Android Interface Definition Language的縮寫,即Android介面定義語言。它是Android的進程間通信比較常用的一種方式。

Android中,每一個進程都有自己的Dalvik VM實例,擁有自己的獨立的記憶體空間,進程與進程之間不共用記憶體,這就產生了進程間通信的需求。

二、語法

AIDL是Android介面定義語言,是一門語言,所以它擁有自己的語法和特性。

(一)數據類型

AIDL支持的數據類型包括以下幾種:

  1. Java的8種基本數據類型:int,short,long,char,double,byte,float,boolean;
  2. CharSequence類型,如String、SpannableString等;
  3. ArrayList,並且T必須是AIDL所支持的數據類型;
  4. HashMap<K,V>,並且K和V必須是AIDL所支持的數據類型;
  5. 所有Parceable介面的實現類,因為跨進程傳輸對象時,本質上是序列化與反序列化的過程;
  6. AIDL介面,所有的AIDL介面本身也可以作為可支持的數據類型;

有兩個需要註意的地方:

1、在Java中,如果一個對象和引用它的類在同一個package下,是不需要導包的,即不需要import,而在AIDL中,自定義的Parceable對象和AIDL介面定義的對象必須在所引用的AIDL文件中顯式import進來,不管這些對象和所引用它們的AIDL文件是否在同一個包下。

2、如果AIDL文件中使用到自定義的Parceable對象,則必須再創建一個與Parceable對象同名的AIDL文件,聲明該對象為Parceable類型,並且根據上一條語法規定,在AIDL文件中進行顯式import。

(二)文件類型
  1. 所有AIDL文件都是以.aidl作為尾碼的;
  2. 根據用途區分,AIDL文件的有兩種,一種是用於定義介面,另一種是用於聲明parceable對象,以供其他AIDL文件使用;
(三)定向tag

AIDL中,除了基本數據類型,其他類型的方法參數都必須標上數據在跨進程通信中的流向:in、out或inout:

1、in表示輸入型參數:只能由客戶端流向服務端,服務端收到該參數對象的完整數據,但服務端對該對象的後續修改不會影響到客戶端傳入的參數對象;

2、out表示輸出型參數:只能由服務端流向客戶端,服務端收到該參數的空對象,服務端對該對象的後續修改將同步改動到客戶端的相應參數對象;

3、inout表示輸入輸出型參數:可在客戶端與服務端雙向流動,服務端接收到該參數對象的完整數據,且服務端對該對象的後續修改將同步改動到客戶端的相應參數對象;

定向tag需要一定的開銷,根據實際需要去確定選擇什麼tag,不能濫用。

深入理解tag:你真的理解AIDL中的in,out,inout麽?

(四)其他

1、所有AIDL介面都是繼承自IInterface介面的,IInterface介面中只聲明瞭一個asBinder方法:

public interface IInterface
{
    /**
     * Retrieve the Binder object associated with this interface.
     * You must use this instead of a plain cast, so that proxy objects
     * can return the correct result.
     */
    public IBinder asBinder();
}

2、系統會幫我們為所有用於定義介面的AIDL文件生成相應的java代碼,手寫這份java代碼與用AIDL系統生成實際上是一樣的,AIDL可以方便系統為我們生成固定格式的java代碼。

三、基本用法

在AndroidStudio中工程目錄的Android視圖下,右鍵new一個AIDL文件,預設將創建一個與java文件夾同級的aidl文件夾用於存放AIDL文件,且aidl文件夾下的包名與build.gradle中配置的applicationId一致,而applicationId預設值是應用的包名。

AIDL的底層是基於Binder實現的,而Binder機制也是一種請求-響應式的通信模型,請求方一般稱為Client,響應方稱為Server。

Demo介紹:在一個應用內部新起一個進程作為服務端,服務端提供addStudent和getStudentList兩個方法,分別用於客戶端向服務端添加Student數據和獲取Student列表,Student是自定義對象,只有id和name兩個屬性。源碼下載鏈接

(一)服務端

新建AIDL文件,定義一個介面,在這個介面里聲明兩個方法,分別用於添加Student數據和獲取所有Student數據,因為AIDL是介面定義語言,所以不能在AIDL文件里對方法進行實現:

/aidl/com/sqchen/aidltest/IStudentService.aidl

package com.sqchen.aidltest;

//顯式import
import com.sqchen.aidltest.Student;

interface IStudentService {

    List<Student> getStudentList();

    //定向tag
    void addStudent(in Student student);
}

因為IStudentService.aidl介面中使用到的Student是自定義對象,不屬於Java基本數據類型和CharSequence類型,所以按照語法規定,在IStudentService.aidl中需要顯式import,同時我們要讓Student實現Parceable介面,並且新建一個AIDL文件用於聲明Student類是Parceable類型:

/aidl/com/sqchen/aidltest/Student.java

public class Student implements Parcelable {

    private int id;

    private String name;

    public Student(int id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(id);
        dest.writeString(name);
    }

    public void readFromParcel(Parcel parcel) {
        this.id = parcel.readInt();
        this.name = parcel.readString();
    }

    public static Parcelable.Creator<Student> CREATOR = new Parcelable.Creator<Student>() {
        @Override
        public Student createFromParcel(Parcel source) {
            return new Student(source);
        }

        @Override
        public Student[] newArray(int size) {
            return new Student[0];
        }
    };

    private Student(Parcel in) {
        this.id = in.readInt();
        this.name = in.readString();
    }

}

/aidl/com/sqchen/aidltest/Student.aidl

package com.sqchen.aidltest;

parcelable Student;

這裡,我們是在src/main/aidl文件夾下創建Student.java的,實際上這將因為找不到Student.java而報錯,因為在AndroidStudio中使用Gradle構建項目時,預設是在src/main/java文件夾中查找java文件的,如果把Student.java放在src/main/aidl對應包名下,自然就會找不到這個文件了,所以需要修改app的build.gradle文件,在sourceSets下添加對應的源文件路徑,即src/main/aidl

android {
    compileSdkVersion 28
    ...
    sourceSets {
        main {
            java.srcDirs = ["src/main/java", "src/main/aidl"]
        }
    }
}

在將src/main/aidl添加到sourceSets中重新構建項目後,在AndroidStudio的Android視圖下,項目的目錄結構將發生變化,此時會發現aidl文件夾不見了,而在java文件夾下,將出現兩個一樣包名的目錄結構,但這隻是在當前視圖下的一種展示方式,將src/main/aidl下的文件也看作是java文件的存放位置,實際上當切換到Project視圖時,會發現AIDL文件還是存在於aidl文件夾下,與java文件夾同級。

如果Student.java是放在src/main/java對應的包名路徑下,則不需要這個步驟。

接著,創建一個Service用來響應Client端的請求:

/java/com/sqchen/aidltest/StudentService.java

public class StudentService extends Service {

    private static final String TAG = "StudentService";

    private CopyOnWriteArrayList<Student> mStuList;

    private Binder mBinder = new IStudentService.Stub() {

        @Override
        public List<Student> getStudentList() throws RemoteException {
            return mStuList;
        }

        @Override
        public void addStudent(Student student) throws RemoteException {
            mStuList.add(student);
        }
    };

    @Override
    public void onCreate() {
        super.onCreate();
        init();
    }

    private void init() {
        mStuList = new CopyOnWriteArrayList<>();
    }


    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }
}

StudentService中,我們創建了一個Binder對象併在onBind方法中返回它,這個Binder對象繼承自IStudentService.Stub,並實現了內部的AIDL方法。

我們用CopyOnWriteArrayList來存放mStuList對象,是因為AIDL方法是在服務端的Binder線程池中執行的,當有多個客戶端同時連接時,可能存在多個線程同時訪問mStuList對象的情況,而CopyOnWriteArrayList支持併發讀寫,可以保證線程安全。

按照AIDL的語法規定,只支持傳輸ArrayList對象,而CopyOnWriteArrayList不是繼承自ArrayList,為什麼也可以傳輸呢?這是因為AIDL中所支持的是抽象的List,而List只是一個介面,雖然服務端返回的是CopyOnWriteArrayList,但在Binder中,它會按照List的規範去訪問數據並最終形成一個新的ArrayList給客戶端。類似的還有ConcurrentHashMap。

為StudentService服務端另起一個進程,在AndroidManifest.xml配置文件中,聲明android:process=":remote",即可創建一個新的進程實現單應用多進程,從而模擬進程間通信。這個進程的名字就是remote

<service
    android:name="com.sqchen.aidltest.StudentService"
    android:process=":remote"
    android:enabled="true"
    android:exported="true"></service>
(二)客戶端

因為客戶端和服務端是在不同的進程中,所以客戶端要想通過AIDL與遠程服務端通信,那麼必須也要有服務端的這份AIDL代碼。

這裡分為兩種情況:

1、服務端與客戶端是兩個獨立應用

把服務端的aidl文件夾整個複製到客戶端的與java文件夾同級的目錄下,保持客戶端和服務端的aidl文件夾的目錄結構一致。這種情況下需要註意的是,如果前面的Student.java文件是放置src/main/java對應包名路徑下,則在拷貝aidl文件夾到客戶端的同時,也要將對應的Student.java一併拷貝到客戶端相同的包名路徑下。

2、服務端與客戶端是同一應用的不同進程

這種情況下因為客戶端與服務端同屬一個應用,兩個進程都可以使用這份AIDL代碼,則不需要拷貝。

客戶端進程即主進程,在MainActivity.java中綁定遠程StudentService,就可以向服務端進程remote發起請求了:

/java/com/sqchen/aidltest/MainActivity.java

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private static final String TAG = "MainActivity";

    private final static String PKG_NAME = "com.sqchen.aidltest";

    private Button btnBind;
    private Button btnAddData;
    private Button btnGetData;
    private Button btnUnbind;

    private IStudentService mStudentService;

    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mStudentService = IStudentService.Stub.asInterface(service);
            if (mStudentService == null) {
                Log.i(TAG, "mStudentService == null");
                return;
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
        initData();
    }

    private void initView() {
        btnBind = findViewById(R.id.btn_bind);
        btnAddData = findViewById(R.id.btn_add_data);
        btnGetData = findViewById(R.id.btn_get_data);
        btnUnbind = findViewById(R.id.btn_unbind);
        initListener();
    }

    private void initListener() {
        btnBind.setOnClickListener(this);
        btnAddData.setOnClickListener(this);
        btnGetData.setOnClickListener(this);
        btnUnbind.setOnClickListener(this);
    }

    private void initData() {
        mCallback = new ITaskCallback.Stub() {
            @Override
            public void onSuccess(String result) throws RemoteException {
                Log.i(TAG, "result = " + result);
            }

            @Override
            public void onFailed(String errorMsg) throws RemoteException {
                Log.e(TAG, "errorMsg = " + errorMsg);
            }
        };
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_bind:
                bindStudentService();
                break;
            case R.id.btn_add_data:
                addData();
                break;
            case R.id.btn_get_data:
                getData();
                break;
            case R.id.btn_unbind:
                unbindStudentService();
                break;
            default:
                break;
        }
    }

    private void bindStudentService() {
        Intent intent = new Intent(this, StudentService.class);
        intent.setPackage(PKG_NAME);
        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
    }

    private void addData() {
        if (mStudentService == null) {
            Log.i(TAG, "mStudentService = null");
            return;
        }
        try {
            mStudentService.addStudent(new Student(1, "陳賢靖"));
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    private void getData() {
        if (mStudentService == null) {
            Log.i(TAG, "mStudentService = null");
            return;
        }
        try {
            List<Student> studentList = mStudentService.getStudentList();
            Log.i(TAG, "studentList = " + studentList);
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    private void unbindStudentService() {
        unbindService(mConnection);
        mStudentService = null;
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        unbindStudentService();
    }
}

在MainActivity.java中,創建4個按鈕,分別用於綁定服務、添加數據、獲取數據、解綁服務:

1、綁定服務

通過bindService方式啟動StudentService,ServiceConnection是用於監視服務端狀態的一個介面,內部方法都在主線程被調用,所以不能在該介面的方法中進行耗時操作。

/**
 * Called when a connection to the Service has been established, with
 * the {@link android.os.IBinder} of the communication channel to the
 * Service.
 *
 * <p class="note"><b>Note:</b> If the system has started to bind your
 * client app to a service, it's possible that your app will never receive
 * this callback. Your app won't receive a callback if there's an issue with
 * the service, such as the service crashing while being created.
 *
 * @param name The concrete component name of the service that has
 * been connected.
 *
 * @param service The IBinder of the Service's communication channel,
 * which you can now make calls on.
 */
void onServiceConnected(ComponentName name, IBinder service);

onServiceConnected方法是在與Service建立連接時被調用,通過註釋可以發現,如果綁定服務的過程中,Service端如果發生崩潰,該方法將不會被回調。

/**
 * Called when a connection to the Service has been lost.  This typically
 * happens when the process hosting the service has crashed or been killed.
 * This does <em>not</em> remove the ServiceConnection itself -- this
 * binding to the service will remain active, and you will receive a call
 * to {@link #onServiceConnected} when the Service is next running.
 *
 * @param name The concrete component name of the service whose
 * connection has been lost.
 */
void onServiceDisconnected(ComponentName name);

onServiceDisconnected方法是在與Service的連接斷開時被調用,通過註釋可以發現,當Service發生崩潰或者由於某種原因被殺死時,將觸發該回調,但客戶端與Service之間的綁定關係還是存在的,且ServiceConnection對象不會被移除,當Service在下一次被運行起來,那麼還會再次觸發onServiceConnected方法。

通過查看ServiceConnection源碼可以知道,在onServiceConnected方法被觸發之後,就可以對服務端Service進行操作了,但是服務端通過onServiceConnected返回給客戶端的是IBinder對象,我們需要通過mStudentService = IStudentService.Stub.asInterface(service)將IBinder類型的service對象轉化為IStudentService類型對象,然後就可以調用IStudentService的addStudentgetStudentList方法了。

2、添加數據

private void addData() {
    if (mStudentService == null) {
        Log.i(TAG, "mStudentService = null");
        return;
    }
    try {
        mStudentService.addStudent(new Student(1, "陳賢靖"));
    } catch (RemoteException e) {
        e.printStackTrace();
    }
}

先判斷mStudentService對象是否初始化,不為空,則調用addStudent向服務端添加一個Student對象。

3、獲取數據

private void getData() {
    if (mStudentService == null) {
        Log.i(TAG, "mStudentService = null");
        return;
    }
    try {
        List<Student> studentList = mStudentService.getStudentList();
        Log.i(TAG, "studentList = " + studentList);
    } catch (RemoteException e) {
        e.printStackTrace();
    }
}

先判斷mStudentService對象是否初始化,不為空,則調用getStudentList方法獲取服務端的Student列表數據。

4、解綁服務

private void unbindStudentService() {
    unbindService(mConnection);
    mStudentService = null;
}

MainActivity的onDestory中或其他需要的地方調用該方法進行解綁服務。

以上就是AIDL的基本使用,流程可以概括為:

(1)創建服務端的AIDL文件,進行服務端方法的介面定義(IStudentService);

(2)創建服務端的Service,實現AIDL介面定義的方法,並將Binder對象通過onBind方法返回給客戶端;

(3)創建客戶端的AIDL文件,從服務端拷貝即可,但要保持AIDL文件的包名結構在服務端和客戶端是一致的;

(4)客戶端綁定服務端Service,在成功建立與Service的連接之後,拿到服務端返回的Binder對象,並將Binder對象轉為AIDL介面類型的對象(IStudentService);

(5)通過IStudentService類型對象調用Service中的實現方法;

(6)在需要結束與服務端連接的時候,調用unbindService方法進行解綁;

在創建AIDL文件時,如果有報錯,通常說明某個AIDL文件書寫不規範,需要檢查的點有:

1、自定義對象是否實現Parceable介面;

2、引用的AIDL對象是否顯式import;

3、定向tag的使用是否正確;

4、定向tag為inout時,自定義對象是否同時實現writeToParcelreadFromParcel

5、如果有修改過java文件的包名,檢查AIDL文件的包名是否正確(是否與applicationId一致);

當發現問題並修改後,可以嘗試Build->Clean ProjectBuild -> Rebuild以重新刷新或構建項目;

三、回調機制

在基本用法中,只實現了客戶端向服務端發送調用請求的單向通信,但在很多場景下,同時也需要實現服務端主動向客戶端發送數據進行雙向通信,比如在觀察者模式中,當有多個客戶端綁定服務端,如果想要實現在服務端數據變化時主動通知所有與它建立綁定的客戶端時,這個時候就需要用到AIDL的回調機制了。

在服務端aidl文件夾下新建一個AIDL文件,用於定義回調介面,並聲明onSuccess和onFailed方法,這兩個方法是用於業務層的,比如服務端添加數據失敗時調用onFailed,取決於具體場景:

// ITaskCallback.aidl
package com.sqchen.aidltest;

interface ITaskCallback {

    void onSuccess(String result);

    void onFailed(String errorMsg);
}

修改IStudentService.aidl,添加registerunregister方法用於客戶端註冊回調和解除回調:

// IStudentService.aidl
package com.sqchen.aidltest;

import com.sqchen.aidltest.Student;
//註意:aidl介面也要顯式import
import com.sqchen.aidltest.ITaskCallback;

interface IStudentService {

    List<Student> getStudentList();

    void addStudent(inout Student student);

    void register(ITaskCallback callback);

    void unregister(ITaskCallback callback);
}

修改StudentService.java

package com.sqchen.aidltest;

import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.util.Log;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class StudentService extends Service {

    private static final String TAG = "StudentService";

    private CopyOnWriteArrayList<Student> mStuList;

    private static RemoteCallbackList<ITaskCallback> sCallbackList;

    private Binder mBinder = new IStudentService.Stub() {

        @Override
        public void register(ITaskCallback callback) throws RemoteException {
            if (callback == null) {
                Log.i(TAG, "callback == null");
                return;
            }
            sCallbackList.register(callback);
        }

        @Override
        public void unregister(ITaskCallback callback) throws RemoteException {
            if (callback == null) {
                return;
            }
            sCallbackList.unregister(callback);
        }

        @Override
        public List<Student> getStudentList() throws RemoteException {
            return mStuList;
        }

        @Override
        public void addStudent(Student student) throws RemoteException {
            if (mStuList == null) {
                dispatchResult(false, "add student failed, mStuList = null");
            } else {
                mStuList.add(student);
                dispatchResult(true, "add student successfully");
            }
        }
    };

    @Override
    public void onCreate() {
        super.onCreate();
        Log.i(TAG, "onCreate");
        init();
    }

    private void init() {
        mStuList = new CopyOnWriteArrayList<>();
        sCallbackList = new RemoteCallbackList<>();
    }

    /**
     * 分髮結果
     * @param result
     * @param msg
     */
    private void dispatchResult(boolean result, String msg) {
        int length = sCallbackList.beginBroadcast();
        for (int i = 0; i < length; i++) {
            ITaskCallback callback = sCallbackList.getBroadcastItem(i);
            try {
                if (result) {
                    callback.onSuccess(msg);
                } else {
                    callback.onFailed(msg);
                }
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
        sCallbackList.finishBroadcast();
    }

    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }
}

StudentService.java中,Binder對象實現了IStudentService.aidl中新聲明的兩個方法,registerunregister,並創建了一個RemoteCallbackList類型的對象sCallbackList。當接收到客戶端傳來的ITaskCallback對象時,調用將該對象的register或者unregister方法,進行註冊回調和解除回調。

RemoteCallbackList<E extends IInterface> 是系統專門提供的用於跨進程傳遞callback的一種介面,這個介面是泛型,支持管理所有AIDL介面。這裡不能使用普通的List來存放callback,因為在進程間通信時,客戶端的List對象和服務端接收到的List對象不在不同的記憶體空間中。正是因為不是在同一個記憶體空間中,不同進程之間的數據不能進行共用,所以才有進程間通信這個機制。

那麼,為什麼RemoteCallbackList能實現傳輸前後都是相同對象呢?查看RemoteCallbackList源碼可以發現,其內部創建了一個ArrayMap用於保存callback:

ArrayMap<IBinder, Callback> mCallbacks = new ArrayMap<IBinder, Callback>();

這個Map的key是IBinder對象,而value是Callback對象,當客戶端通過register方法註冊回調時,將callback傳遞給服務端,服務端再通過RemoteCallbackList.register方法真正將回調進行保存:

//RemoteCallbackList
public boolean register(E callback, Object cookie) {
    synchronized (mCallbacks) {
        if (mKilled) {
            return false;
        }
        // Flag unusual case that could be caused by a leak. b/36778087
        logExcessiveCallbacks();
        IBinder binder = callback.asBinder();
        try {
            Callback cb = new Callback(callback, cookie);
            binder.linkToDeath(cb, 0);
            mCallbacks.put(binder, cb);
            return true;
        } catch (RemoteException e) {
            return false;
        }
    }
}

將我們關心的部分抽出來:

IBinder binder = callback.asBinder();
Callback cb = new Callback(callback, cookie);
mCallbacks.put(binder, cb);

將客戶端傳遞過來的Callback對象轉為IBinder對象作為key,封裝一個Callback作為value。客戶端傳遞過來的Callback對象雖然在服務端被重新序列化生成一個對象,但它們底層的Binder對象是同一個,所以可以實現Callback的跨進程傳輸。

在服務端註冊客戶端的回調後,服務端就可以通過這個回調主動向客戶端傳遞數據了。比如,在addStudent中,當添加數據成功時,將操作的執行結果或者其他數據分發給所有向該服務端註冊監聽的客戶端:

/**
 * 分髮結果
 * @param result
 * @param msg
 */
private void dispatchResult(boolean result, String msg) {
    int length = sCallbackList.beginBroadcast();
    for (int i = 0; i < length; i++) {
        ITaskCallback callback = sCallbackList.getBroadcastItem(i);
        try {
            if (result) {
                callback.onSuccess(msg);
            } else {
                callback.onFailed(msg);
            }
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }
    //在調用beginBroadcast之後,必須調用該方法
    sCallbackList.finishBroadcast();
}

在客戶端中創建ITaskCallback對象:

//MainActivity.java

ITaskCallback mCallback = new ITaskCallback.Stub() {
    @Override
    public void onSuccess(String result) throws RemoteException {
        Log.i(TAG, "result = " + result);
    }

    @Override
    public void onFailed(String errorMsg) throws RemoteException {
        Log.e(TAG, "errorMsg = " + errorMsg);
    }
};

修改ServiceConnection,在建立連接、調用onServiceConnected方法時,進行Callback的註冊:

private ServiceConnection mConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        mStudentService = IStudentService.Stub.asInterface(service);
        if (mStudentService == null) {
            Log.i(TAG, "mStudentService == null");
            return;
        }
        try {
            if (mCallback != null) {
                Log.i(TAG, "mCallback != null");
                mStudentService.register(mCallback);
            } else {
                Log.i(TAG, "mCallback == null");
            }
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {

    }
};

此時,客戶端與服務端的連接已經建立,且客戶端向服務端註冊了回調,當客戶端向服務端添加數據,服務端執行addStudent方法時,服務端會通過回調將添加數據的執行結果返回給客戶端,從而實現了雙向通信。

四、許可權驗證

預設情況下,如果沒有加入許可權驗證功能,那麼我們的遠程服務是所有進程都可以進行連接的,從系統安全性的角度出發,我們還需要有相應的許可權驗證機制來保證系統的安全,有兩種方式:

1、在建立連接之前

在客戶端通過bindService方法綁定遠程服務時,我們會在服務端的onBind方法中將Binder對象返回給客戶端,那麼我們可以在onBind方法中對來自客戶端的請求進行許可權驗證。

2、在客戶端請求執行服務端的AIDL方法時

實際上,每個AIDL方法都有一個唯一的方法標識code,服務端在Binder.onTransact中根據這個code判斷並確定客戶端想要調用的是哪個AIDL方法,所以,我們可以在Binder.onTransact中進行許可權驗證,攔截非法的客戶端調用。

常用的許可權驗證機制有包名驗證和許可權驗證,即根據客戶端的包名或所聲明的許可權是否符合服務端要求來進行驗證。

修改StudentService.java中的Binder對象:

private Binder mBinder = new IStudentService.Stub() {
    
    ...
    
    @Override
    public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
        //包名驗證
        String[] packages = getPackageManager().getPackagesForUid(getCallingUid());
        String pkgName = null;
        if (packages != null && packages.length > 0) {
            pkgName = packages[0];
        }
        if (TextUtils.isEmpty(pkgName) || !pkgName.startsWith("com.sqchen")) {
            Log.i(TAG, "invalid pkgName : " + pkgName);
            return false;
        }
        return super.onTransact(code, data, reply, flags);
    }
};

這樣,如果客戶端的包名不是以"com.sqchen"開頭的話,則認為是非法請求,在onTranscat中返回false將使得客戶端的請求失敗,從而達到許可權驗證的目的。

五、死亡回調

當客戶端與服務端之間的連接斷開,我們稱之為Binder死亡,此時雖然客戶端和服務端都在運行,但因為連接斷開,客戶端發出的請求是不會得到響應的,所以我們需要知道什麼時候連接斷開,以便進行重新綁定,或者執行其他操作。

前面在看ServiceConnection的源碼時我們發現,當連接斷開時,會調用onServiceDisconnected方法,所以,我們可以在這個方法進行重新綁定服務。

此外,Binder中還有兩個很重要的方法,linkToDeathunlinkToDeath,通過linkToDeath我們可以給Binder設置一個死亡代理IBinder.DeathRecipient,當Binder死亡時,將會調用DeathRecipient的binderDied方法。

修改MainActivity.java,創建一個死亡代理,當客戶端與服務端建立連接時,為Binder設置死亡代理:

private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
    @Override
    public void binderDied() {
        if (mStudentService == null) {
            return;
        }
        //解除死亡代理
        mStudentService.asBinder().unlinkToDeath(mDeathRecipient, 0);
        mStudentService = null;
        //重新綁定服務
        bindStudentService();
        Log.i(TAG, "binderDied, bindService again");
    }
};

private ServiceConnection mConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        mStudentService = IStudentService.Stub.asInterface(service);
        if (mStudentService == null) {
            Log.i(TAG, "mStudentService == null");
            return;
        }
        try {
            //設置死亡代理
            mStudentService.asBinder().linkToDeath(mDeathRecipient, 0);
            if (mCallback != null) {
                Log.i(TAG, "mCallback != null");
                mStudentService.register(mCallback);
            } else {
                Log.i(TAG, "mCallback == null");
            }
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {
        //也可以在這裡重新綁定服務
    }
};

接著,我們模擬服務端意外死亡導致連接斷開的情況,進入adb shell,查找服務端進程remote的pid,並kill掉:

E:\Blog\src\AIDLTest>adb shell
mido:/ # ps
USER           PID  PPID     VSZ    RSS WCHAN            ADDR S NAME
root         26112 28641    8968   1900 sigsuspend 74a7005e08 S sh
root         26116 26112   10540   1992 0          7115eac768 R ps
mido:/ # ps -A | grep com.sqchen
u0_a140      26015   745 5238588  68324 SyS_epoll_wait 7269051c40 S com.sqchen.aidltest
u0_a140      26046   745 5217176  39364 SyS_epoll_wait 7269051c40 S com.sqchen.aidltest:remote
mido:/ # kill 26046

然後,查看日誌:

2019-04-05 21:43:00.530 26015-26015/com.sqchen.aidltest I/MainActivity: mCallback != null
2019-04-05 21:45:21.955 26015-26028/com.sqchen.aidltest I/MainActivity: binderDied, bindService again
2019-04-05 21:45:22.048 26015-26015/com.sqchen.aidltest I/MainActivity: mCallback != null

發現remote被kill之後,確實調用了DeathRecipientbinderDied方法,再次查看remote進程,觀察發現remote進程的pid在被kill掉前後是不一樣的,說明成功地重新綁定服務。

E:\Blog\src\AIDLTest>adb shell
mido:/ # ps -A | grep com.sqchen
u0_a140      26015   745 5239648  68328 SyS_epoll_wait 7269051c40 S com.sqchen.aidltest
u0_a140      26125   745 5217176  39604 SyS_epoll_wait 7269051c40 S com.sqchen.aidltest:remote

binderDiedonServiceDisconnected的區別:

1、binderDied早於onServiceDisconnected被調用(參考:linkToDeath機制瞭解和使用

2、binderDied在客戶端的Binder線程池被調用,不能在這個方法中訪問UI,而onServiceDisconnected在客戶端的UI線程被調用;

六、需要註意的地方

1、客戶端調用遠程服務的方法,被調用的方法運行在服務端的Binder線程池中,同時,客戶端線程會被掛起,進入阻塞狀態,如果被調用的服務端方法比較耗時,那麼我們不能在客戶端的主線程去調用服務端的方法,否則將導致客戶端ANR。

2、查看ServiceConnection源碼時,發現客戶端的onServiceConnnectedonServiceDisconnected方法運行在主線程,即UI線程,所以也不能在這兩個方法中調用服務端的耗時方法。

3、服務端的方法運行在服務端的Binder線程池中,所以在編寫服務端代碼時,不需要新建線程去執行服務端方法。

源碼地址:AIDLTest


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1.下載npm軟體包 點擊鏈接進入下載頁面:npm下載 2.下載完成後將壓縮包放到家目錄下就可以(也可以放到其他地方) 3.解壓 tar -zxvf 壓縮包名稱,解壓後你會得到一個文件夾,進入後是這樣的。 4.然後我們進入scripts這個目錄。 可以看到這裡有一個文件的名稱叫install.sh. ...
  • 起因: 串口IAP升級在正點原子的常式中有講解,正點原子的方法是:在RAM中開闢一個120K的數據空間,用來存放bin文件,bin文件通過串口一次性發送到單片機,然後再實現程式的跳轉。但是這種方法在實際項目中並不實用,因為沒用文件校驗,不能保證bin文件的完整性,如果貿然跳轉,將會是設備陷入到永遠無 ...
  • redis設計關係資料庫 [toc] 前言 最近需要一張用戶信息表,因為數據量並不大,想先放在記憶體中,等需求變更了,再移到磁碟上,或者往mysql塞,那麼問題來了,怎麼用redis的數據類型設計一個關係資料庫呢。 redis只有key value這種存儲結構,如果想利用它做成想其他資料庫一樣具備 等 ...
  • 理解Cursor對象和查詢運算符 cursor對象 cursor對象相當於一個指針,可通過迭代它來訪問MongdoDB資料庫中的一組對象。 在使用 find() 方法查詢時,返回的並非實際文檔,而是一個Cursor對象,也就是一個指向第一個數據之前的指針。 Cursor對象內部存儲了一個指向當前位置 ...
  • 在redis.c的initServerConfig()方法中,通過調用dictCreate方法初始化server端的命令表。這個命令表是一個hashtable,可以通過key找到相關的命令: initServer()函數在初始化服務端的基本配置時,已經提前創建了客戶端的回調函數。具體的調用為:1.創 ...
  • 文章大綱 一、Realm介紹二、Realm實戰三、Realm官方文檔四、項目源碼下載五、參考文章 一、Realm介紹 1. 什麼是Realm Realm 是一個手機資料庫,是用來替代 SQlite 的解決方案,比 SQlite 更輕量級,速度更快,因為它有一套自己的資料庫搜索引擎,並且還具有很多現代 ...
  • 在Android開發中,一般通過網路進行訪問伺服器端的信息(存儲和檢索網路中的數據),如API介面,WebService,網路圖片等。今天主要講解Http訪問的常用方法,僅供學習分享使用。 ...
  • 問題:在A activity中傳遞一個SpannableString到B activity中,並最終傳遞到B activity中的TextView中,但是沒有展示出Span效果。 解決:閱讀TextView.setText()方法 看到會根據BufferType對傳入的text重新賦值,於是回溯找到 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...