Binder : AIDL

Binder AIDL的使用

參考Demo:https://github.com/gqq519/BinderAIDL

  • Binder是Android的一個類,實現了IBinder接口
  • IPC角度來說,Binder是Android的一種跨進程通信方式,可以理解爲一種虛擬的物理設備,設備驅動是/dev/binder。
  • 從Framework角度說,Binder是ServiceManager連接各種Manager(ActivityManager、WindowManager等)和相應的ManagerService的橋樑。
  • 從應用層來說,Binder是客戶端和服務端進行通信的媒介,當bindService的時候,服務端會返回服務端業務調用的Binder對象,客戶端由此獲取服務端提供的服務或數據。服務包括普通服務和基於AIDL的服務。

Android中,Binder主要用於Service中,包括AIDL和Messenger,普通Service不涉及進程間通信,不涉及Binder的核心,而Messenger底層其實也是AIDL實現的,所以拿AIDL來了解Binder的工作機制。

AIDL簡述

AIDL:Android Interface Definition Language,通過編寫aidl文件,系統會編譯生成Binder接口,用於進程間通信。

AIDL支持的數據格式:

  • Java的基本數據類型
  • String和CharSequence
  • List和Map:
    1. 元素必須是 AIDL 支持的數據類型
    2. 具體的類裏則必須是 ArrayList 或者 HashMap
  • 其他AIDL生成的接口
  • 實現Parcelable的類

創建AIDL示例

1. 創建工程

2. 創建要操作的實體類,需要實現Parcelable接口,跨進程使用

public class User implements Parcelable {

    private int id;
    private String name;

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

    // 反序列化方法
    protected User(Parcel in) {
        id = in.readInt();
        name = in.readString();
    }

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

    // 內容描述
    @Override
    public int describeContents() {
        // 一般返回0,另一個特殊返回CONTENTS_FILE_DESCRIPTOR,爲有FileDescriptor,放入Parcelable需指定。。
        // 然而。。好像並沒有什麼用,所以返回0就好了
        return 0;
    }

    // 反序列化
    public static final Creator<User> CREATOR = new Creator<User>() {
        @Override
        public User createFromParcel(Parcel in) {
            // 反序列化對象
            return new User(in);
        }

        @Override
        public User[] newArray(int size) {
            // 反序列化數組
            return new User[size];
        }
    };
}

3. 創建實體類的映射aidl文件

右鍵新建AIDL文件User.aidl(名稱與實體類保持一致),會在main下生成aidl目錄,包名與java包名一致。

User.aidl文件爲實體類的映射文件,需要聲明映射的實體類和類型:

// User.aidl
// 包名與實體類包名一致
package com.gqq.binderaidl;

parcelable User;

4. 創建操作接口aidl文件

在aidl目錄的包名下創建AIDL文件IUserManager.aidl,內部是一個接口,主動實現了void basicTypes()方法,在接口中定義需要跨進程操作的接口:

// IUserManager.aidl
package com.gqq.binderaidl;

// Declare any non-default types here with import statements
import com.gqq.binderaidl.User;

interface IUserManager {
    /**
     * 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);

    void addUser(in User user);
    List<User> getUserList();
}

比如我們定義了兩個方法用於操作:

  • addUser()添加User
  • getUserList()獲取用戶列表

注意:

  • 定義的Parcelable實體類型,需要導入它的全路徑,比如User,需要導入import com.gqq.binderaidl.User;
  • 方法參數中,除了基本數據類型外都需要標上類型:in(輸入), out(輸出), inout(輸入輸出)

5. Make Project,生成Binder的java文件

上述操作完成後,點擊Build -> Make Project,完成後可以在build/generated/source/aidl/packageName/下找到生成的java文件。

IUserManager的大致預覽:

生成的代碼主要給客戶端使用,後續再介紹裏面的內容吧~

至此,通信的媒介我們已經完成了。

注意:Make Project 出現錯誤可能的原因

  1. 映射的aidl:User.aidl和實體類User,名稱要保持一致,包名要保持一致。
  2. 定義的AIDL接口文件的方法參數需要標上類型。
  3. AIDL接口文件中導入實體類的包名。

編寫服務端代碼

1. 創建Service

在項目中創建Service,需要實現onBind()方法,返回值爲IBinder,根據上述生成的IUserManager.java,內部Stub繼承自Binder,所以,onBind() 的返回值設置爲AIDL的接口的實例。

public class UserService extends Service {

    private List<User> users;

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        users = new ArrayList<>();
        // 返回AIDL生成的Binder實例
        return new UserServiceImpl();
    }

    // 創建AIDL生成的Binder實例
    public class UserServiceImpl extends IUserManager.Stub {

        // 實現的AIDL接口的方法
        @Override
        public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {

        }

        @Override
        public void addUser(User user) throws RemoteException {
            user.name += "-Service";
            users.add(user);
        }

        @Override
        public List<User> getUserList() throws RemoteException {
            return users;
        }
    }
}

2. 清單註冊Service

Service創建好之後在清單文件註冊:

        <service android:name=".UserService"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="com.gqq.binderaidl.IUserManager" />
            </intent-filter>
        </service>

至此,服務端工作已完成。

編寫客戶端代碼

1. 新建Client工程

客戶端可以做爲一個單獨的App,或者跟服務端在不同的進程都可。

2. 拷生成文件到Client工程

在客戶端工程下,將服務端生成的AIDL的java文件以及實體類User.java 一起拷過來,更改client的目錄與Server保持一致(拷過來會報錯)。

3. 綁定服務

public class MainActivity extends AppCompatActivity {

    private IUserManager userManager;

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

        // 通過Server對外的接口操作數據並顯示等
        final TextView textView = findViewById(R.id.tv_show);
        findViewById(R.id.btn_client).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (userManager == null) return;
                try {
                    userManager.addUser(new User(1, "gqq"));
                    List<User> userList = userManager.getUserList();
                    textView.setText(userList.toString());
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        });

        Intent intent = new Intent();
        intent.setAction("com.gqq.binderaidl.IUserManager");
        // Android 5.0 以後必須顯式啓動,參考:https://blog.csdn.net/vrix/article/details/45289207
        intent.setPackage("com.gqq.binderaidl");
        bindService(intent, connection, Context.BIND_AUTO_CREATE);
    }

    private ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            // 綁定成功之後獲取服務對象
            userManager = IUserManager.Stub.asInterface(service);
            Toast.makeText(MainActivity.this, "onServiceConnected", Toast.LENGTH_SHORT).show();
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            userManager = null;
        }
    };

運行

首先運行服務端,再運行客戶端,就可以通過客戶端操作了。

升級:設置監聽

假如現在用戶希望服務端當有新用戶時實時的告訴我,這個是一個典型的觀察者模式,在實際中也用到很多。

1. 提供AIDL接口,作爲監聽

提供一個AIDL接口,客戶端需要實現這個接口並註冊提醒的功能,也可以隨時取消這個提醒。使用AIDL接口是因爲AIDL中無法使用普通接口。

服務端創建一個aidl文件:

    package com.gqq.binderaidl;

    import com.gqq.binderaidl.User;

    // Declare any non-default types here with import statements

    interface IOnNewUserArrivedListener {
        /**
         * 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);
        void onNewUserArrived(in User user);
    }

2. 在原用的IUserManager.aidl接口中添加註冊和取消註冊的方法

添加註冊和反註冊的方法,以便於客戶端可以監聽。

    package com.gqq.binderaidl;

    // Declare any non-default types here with import statements
    import com.gqq.binderaidl.User;
    import com.gqq.binderaidl.IOnNewUserArrivedListener;

    interface IUserManager {
        /**
         * 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);

        void addUser(in User user);
        List<User> getUserList();
        void registerListener(IOnNewUserArrivedListener listener);
        void unregisterListener(IOnNewUserArrivedListener listener);
    }

寫完make project。

3. 完善服務端的Service

主要是實現Service中的IUserManager.Stub的實現,因爲新增了兩個方法。另外模擬場景:開啓線程,每隔5s新增一個用戶並通知客戶端。

    // ------------定義的變量---------------    
        private List<IOnNewUserArrivedListener> listeners = new ArrayList<>();
        private boolean isServiceDestoryed = false;

    // ------------重寫方法,開啓工作線程---------
        @Override
        public void onCreate() {
            super.onCreate();
            new Thread(new ServiceWorker()).start();

        }

        @Override
        public void onDestroy() {
            super.onDestroy();
            isServiceDestoryed = true;
        }

    // -------------工作的線程-----------------
        public class ServiceWorker implements Runnable {
            @Override
            public void run() {
                while (!isServiceDestoryed) {
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    int userId = users.size() + 1;
                    User user = new User(userId, "new User:"+userId);
                    try {
                        onNewUserArrived(user);
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                }
            }
        }

    private void onNewUserArrived(User user) throws RemoteException {
            users.add(user);
            for (int i = 0; i < listeners.size(); i++) {
                IOnNewUserArrivedListener onNewUserArrivedListener = listeners.get(i);
                onNewUserArrivedListener.onNewUserArrived(user);
            }
        }

    // --------------重寫的方法實現------------------
        @Override
        public void registerListener(IOnNewUserArrivedListener listener) throws RemoteException {

            if (!listeners.contains(listener)) {
                listeners.add(listener);
            } else {
                Log.i("TAG", "listener already exists");
            }
        }

        @Override
        public void unregisterListener(IOnNewUserArrivedListener listener) throws RemoteException {
            if (listeners.contains(listener)) {
                listeners.remove(listener);
            } else {
                Log.i("TAG", "not found");
            }
        }

服務端的修改已經完成。

4. 客戶端註冊監聽並處理接收

把服務端新加的aidl文件生成的java文件複製到客戶端項目中,客戶端註冊監聽,並在頁面退出時解除註冊。同時在onUserArrived方法中接收到數據後要回到主線程顯示等,所以藉助Handler實現。

        private ServiceConnection connection = new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                Toast.makeText(MainActivity.this, "onServiceConnected", Toast.LENGTH_SHORT).show();
                userManager = IUserManager.Stub.asInterface(service);
                try {

                    // 註冊監聽
                    userManager.registerListener(listener);

                    service.linkToDeath(deathRecipient, 0);
                } catch (RemoteException e) {
                    Log.i("TAG", "RemoteException");
                    e.printStackTrace();
                }
            }

    // 註冊的監聽
            private IOnNewUserArrivedListener listener = new IOnNewUserArrivedListener.Stub() {
                @Override
                public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {
                }

                @Override
                public void onNewUserArrived(User user) throws RemoteException {
                    handler.obtainMessage(MESSAGE_WHAT_ARRIVED, user).sendToTarget();
                }
            };

    // Handler的處理
            private Handler handler = new Handler() {
                @Override
                public void handleMessage(Message msg) {
                    switch (msg.what) {
                        case MESSAGE_WHAT_ARRIVED:
                            Log.i("TAG", "received new user" + msg.obj);
                            List<User> userList = null;
                            try {
                                userList = userManager.getUserList();
                                textView.setText(userList.toString());
                            } catch (RemoteException e) {
                                e.printStackTrace();
                            }
                            break;
                        default:
                            super.handleMessage(msg);
                    }
                }
            };

    // 取消監聽
            @Override
            protected void onDestroy() {
                if (userManager != null && userManager.asBinder().isBinderAlive()) {
                    try {
                        userManager.unregisterListener(listener);
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                }
                unbindService(connection);
                super.onDestroy();
            }

升級:取消監聽

在上述的場景設置中,當退出客戶端頁面,取消註冊,通過日誌可以查看到,當取消註冊的時候會發現unregisterListener中remove的時候發生了異常,因爲在多線程中,Binder會把客戶端傳遞過來的對象重新轉化並生成一個新的對象。對象是不能跨進程傳遞的,我們跨進程傳遞的時候都是把對象進行序列化和反序列化。那如何實現取消註冊呢?需要藉助RemoteCallbackList

RemoteCallbackList是系統專門提供的用於刪除跨進程listener的接口,可以從源碼中看出:

    public class RemoteCallbackList<E extends IInterface> 

    // Callback中封裝了真正的遠程listener
    ArrayMap<IBinder, Callback> mCallbacks = new ArrayMap<IBinder, Callback>();

跨進程的對象雖然不一樣,但Binder是同一個,利用Binder來實現,客戶端解除註冊的時候遍歷所有的服務端的listener,將具有相同Binder 的listener刪除即可,這個是RemoteCallbackList所做的事情。同時,還有一個功能,當客戶端進行終止後,它會自動解除客戶端所註冊的listener。

代碼改善

利用RemoteCallbackList實現解除註冊:用RemoteCallbackList代替List<>

    private RemoteCallbackList<IOnNewUserArrivedListener> listeners = new RemoteCallbackList<>();

修改註冊和解註冊的方法:

        @Override
        public void registerListener(IOnNewUserArrivedListener listener) throws RemoteException {
            listeners.register(listener);

            // 在此處打印下注冊的監聽的數量
            int i = listeners.beginBroadcast();
            listeners.finishBroadcast();
            Log.i("TAG", "registerListener listener size:"+ i);
        }

        @Override
        public void unregisterListener(IOnNewUserArrivedListener listener) throws RemoteException {
            listeners.unregister(listener);

            // 注意:在此處打印下注冊的監聽的數量
            int i = listeners.beginBroadcast();
            listeners.finishBroadcast();
            Log.i("TAG", "unregisterListener listener size:"+ i);
        }

修改onNewUserArrived方法:

        private void onNewUserArrived(User user) throws RemoteException {
            users.add(user);
            int size = listeners.beginBroadcast();
            for (int i = 0; i < size; i++) {
                IOnNewUserArrivedListener onNewUserArrivedListener = listeners.getBroadcastItem(i);
                if (onNewUserArrivedListener != null) {
                    onNewUserArrivedListener.onNewUserArrived(user);
                }
            }
            listeners.finishBroadcast();
        }

注意:RemoteCallbackList並不是一個List,無法像操作List一樣操作它,要像上述的方式一樣去遍歷它,其中beginBroadcast()finishBroadcast()必須配對使用,哪怕是想要獲取RemoteCallbackList的元素個數。

升級:耗時處理

客戶端調用服務端的方法,被調用的方法運行在服務端的Binder線程池中,同時客戶端掛起。如果服務端的方法是耗時的操作,客戶端在UI線程的話,就會導致ANR。客戶端的onServiceConnected和onServiceDisconnected運行在UI線程中,不能直接在裏面調用服務端的耗時方法。服務端的方法本身運行在Binder線程池中,可以做大量的耗時操作,不用再開啓線程去進行異步操作。模擬一下耗時的操作:

    // 服務端模擬耗時操作的方法
        @Override
        public List<User> getUserList() throws RemoteException {
            SystemClock.sleep(5 * 1000);
            return users;
        }

客戶端點擊按鈕去直接調用服務端的方法獲取list數據,多次點擊就會出現ANR,那麼就需要把調用放到非UI線程,比如:

        findViewById(R.id.btn_client).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (userManager == null) return;
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            List<User> userList = userManager.getUserList();
                        } catch (RemoteException e) {
                            e.printStackTrace();
                        }
                    }
                }).start();
            }
        }

同樣,我們不可以在服務端UI線程中調用客戶端耗時的操作,另外AIDL接口方法都運行在Binder線程池中,訪問UI需要切換線程。例如客戶端的onNewUserArrived方法。

升級:服務重連

Binder在通信中可能意外死亡,往往由於服務端進程意外停止,需要重新連接服務。

1. Binder設置DeathRecipient監聽

設置DeathRecipient監聽,當Binder死亡時,會收到binderDied方法的回調,可以在binderDied方法中重連服務。

2. 在OnServiceDisconnected中重連遠程服務

區別:onServiceDisconnected在客戶端UI線程被回調,binderDied在客戶端的binder線程池中被回調,即binderDied中不能訪問UI。

升級:權限驗證

定義權限非本節重點:定義權限參考

首先在服務端的AndroidMenifest中聲明所需權限

    <permission   
    android:name="com.gqq.binderaidl.permission.ACCESS_USER_SERVICE"
    android:protectionLevel="normal" />
    <uses-permission android:name="com.gqq.binderaidl.permission.ACCESS_USER_SERVICE" />

第一種方法:在onBind中進行驗證(permission驗證)

      @Override
        public IBinder onBind(Intent intent) {
            int check = checkCallingOrSelfPermission("com.gqq.binderaidl.permission.ACCESS_USER_SERVICE");
            if (check == PackageManager.PERMISSION_DENIED) {
                Log.i(TAG, "onbind check=" + check);
                return null;
            }
            return mBinder;
        }

第二種方法:在onTransact中進行驗證(包名驗證)

    public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws RemoteException {
                // 權限驗證
                int check = checkCallingOrSelfPermission("com.ryg.chapter_2.permission.ACCESS_BOOK_SERVICE");
                if(check == PackageManager.PERMISSION_DENIED){
                    L.d("Binder 權限驗證失敗");
                    return false;
                }
                // 包名驗證
                String packageName=null;
                String[] packages = getPackageManager().getPackagesForUid(getCallingUid());
                if(packages!=null && packages.length>0){
                    packageName = packages[0];
                }
                if(!packageName.startsWith("com.ryg")){
                    L.d("包名驗證失敗");
                    return false;

                }
                return super.onTransact(code, data, reply, flags);
            };

客戶端AndroidMenifest中聲明:

<uses-permission android:name="com.gqq.binderaidl.permission.ACCESS_USER_SERVICE" />

參考Demo

總結

主要是AIDL的整體使用流程。

再貼一遍代碼地址,歡迎指正!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章