Binder總結篇2-Binder使用

黎偉傑發表於2018-08-18

Binder總結篇2-Binder使用

在上一篇文章Binder總結篇1-Binder原理中,我們大概理解了Binder的執行原理,那麼我們在什麼樣子的應用場景下會使用到Binder呢?

就我個人而言,是在IM系統開發當中使用到多程式開發,也就需要Binder來進行通訊了,本文是編寫實際的例子的,涉及到的點有:

  1. AIDL

包括支援的資料型別,定義以及使用等

  1. Service

包括一些啟動,資料獲取以及資料回傳等。

本文的demo地址是:AndroidBinderSample

AIDL

關於AIDL的詳細描述,可以看官網Android 介面定義語言 (AIDL)

他是Android的介面定義語言,用來具體實現Binder通訊過程的資料傳遞,格式跟java的介面程式碼的編寫差不多。 他是使用.aidl檔案結尾,存放在man資料夾下的aidl資料夾下,當然你可以通過在gradle中配置aidl.srcDirs來指定。

AIDL支援的資料型別

  1. Java的基本型別,也包括String型別和CharSequence型別
  2. List 和Map,其中List和Map中的元素必須是AIDL支援的資料型別,而且在Server端必須使用ArrayList或者是HashMap來接收。
  3. 其他的AIDL生成的介面
  4. 實現了Parcelable介面的實體,可以看詳細介紹Android中Parcelable的原理和使用方法

AIDL檔案,總得來說,AIDL分為兩類檔案,一種是介面型別,就是需要被呼叫被實現的。一種是宣告Parcelable資料,作用就是把對應的Java實現了Parcelable介面的類對映到AIDL中,然後被AIDL的介面檔案引用,需要注意的是這個AIDL檔案的包名需要與Java實現Parcelable檔案對應的包名一致。 例如: 我們宣告瞭一個JavaDomain,在包app.androidbinder.domain下,大致如下

package app.androidbinder.domain;
import android.os.Parcel;
import android.os.Parcelable;
/**
 * 作者:黎偉傑 on 2018/8/13.
 * 郵箱:liweijie@qq.com
 * description:
 * update by:
 * update day:
 *
 * @author liweijie
 */
public class UserInfo implements Parcelable {
      //省略程式碼
    public UserInfo() {
    }
    protected UserInfo(Parcel in) {
          //省略程式碼
    }
    public static final Creator<UserInfo> CREATOR = new Creator<UserInfo>() {
        @Override
        public UserInfo createFromParcel(Parcel in) {
            return new UserInfo(in);
        }
        @Override
        public UserInfo[] newArray(int size) {
            return new UserInfo[size];
        }
    };
    @Override
    public int describeContents() {
        return 0;
    }
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        //省略程式碼
    }
    public void readFromParcel(Parcel in) {
        //省略程式碼
    }
}
複製程式碼

然後在AIDL檔案下也需要建立對應的包名,然後編寫UserInfo.aidl,如下:

package app.androidbinder.domain;
/**
 * 作者:黎偉傑 on 2018/8/14.
 * 郵箱:liweijie@qq.com
 * description:
 * update by:
 * update day:
 *
 * @author liweijie
 */
parcelable UserInfo;
複製程式碼

這樣子,在別的AIDL中就可以引用這個UserInfo了。 定義介面型別的AIDL如下:

// UserService.aidl
package app.androidbinder;
import java.util.List;
import app.androidbinder.domain.UserInfo;
// Declare any non-default types here with import statements
interface UserService {
    String getUserName(int userId);
    void saveUser(in UserInfo param);
    UserInfo getUserInfo(int userId);
    List<UserInfo> queryUser();
    //for in out inout
    UserInfo handleIn(in UserInfo info);
    UserInfo handleOu(out UserInfo info);
    UserInfo handleInOut(inout UserInfo info);
}
複製程式碼

這就是兩種AIDL檔案型別以及對應的大致編寫。

in、out、inout

在官方文件中支出,所有的非原語引數需要指示資料的方向標記,可以是in、out、inout。預設的原語是in,不能是其他流向。 這裡指定的非原語是指:除了Java的基本型別外的其他引數,也就是物件。我們在AIDL使用的時候需要知道這個引數的流向。 那什麼是資料的方向標記呢? 首先,資料的方向標記是針對客戶端中的那個傳入的方法引數而言。資料流向的識別符號不能使用在返回引數上,只能使用在方法引數上面。

  1. in:他表示的是這個引數只能從客戶端流向服務端,比如客戶端傳遞了一個User物件給服務端,服務端會收到一個完整的User物件,然後假如在服務端對這個物件進行操作,那麼這個改變是不會反映到客戶端的,這個流向也就是隻能從客戶端到服務端。
  2. out:他表示,當客戶端傳遞引數到服務端的時候,服務端將會收到一個空的物件,假如服務端對該物件進行操作,將會反映到客戶端。比如,客戶端傳遞一個User物件到服務端,服務端接收到的是一個空的User物件(不是null,只是有點像new一個User物件)。當服務端對這個User物件進行改變的時候,他的值變化將會反映到客戶端。
  3. inout,它具有這二者的功能,也就是客戶端傳遞物件到服務端,可以接收到完整的物件,同時服務端改變物件,也會反映到客戶端。 總結來說,in類似於傳值,out類似於傳引用,只是out的引用值到了服務端為空,inout則具有二者的功能,預設的是in。

Service

多程式之間的通訊離不開的是Service,Android四大元件之一,這裡不過多的贅述,只是需要知道我們在多程式通訊當中,服務端最少提供一個Service來,在onBind()方法中返回實現AIDL介面的IBinder物件。然後客戶端通過bindService()來連線,通過在ServiceConnectiononServiceConnected方法,通過呼叫AIDL 生成的檔案中的StubasInterface()來在客戶端獲取服務例項,繼而呼叫服務端的方法。需要注意,在跨app的程式呼叫中,對外暴露的Service需要在清單檔案中把android:exported設定為true。一般而言,我們還會配置一些fillter來進行過濾。 關於如何使用Service以及他的一些生命週期,一些方法區別(比如startService和bindService)請自己另外查閱文件,這裡就不描述了。以下是本文Demo中的例項,使用的還是上面的AIDL,使用之前確保成功編譯出對應的aidl生成檔案:

服務端的Service

package app.androidbinder2.services;
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.annotation.Nullable;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
import app.androidbinder.UserService;
import app.androidbinder.domain.UserInfo;
/**
 * 作者:黎偉傑 on 2018/8/12.
 * 郵箱:liweijie@qq.com
 * description:
 * update by:
 * update day:
 *
 * @author liweijie
 */
public class App2Service extends Service {
    /**
     * 模擬一些測試資料
     */
    private List<UserInfo> data = new ArrayList<>();
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return super.onStartCommand(intent, flags, startId);
    }
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        data.clear();
        data.add(new UserInfo(1, "A", 20));
        data.add(new UserInfo(2, "B", 30));
        data.add(new UserInfo(3, "C", 40));
        data.add(new UserInfo(4, "D", 50));
        return userService;
    }
    private Binder userService = new UserService.Stub() {
        @Override
        public String getUserName(int userId) throws RemoteException {
            for (UserInfo item : data) {
                if (item.getUserId() == userId) {
                    return item.getUserName();
                }
            }
            return null;
        }
        @Override
        public void saveUser(UserInfo param) throws RemoteException {
            data.add(param);
        }
        @Override
        public UserInfo getUserInfo(int userId) throws RemoteException {
            for (UserInfo item : data) {
                if (item.getUserId() == userId) {
                    return item;
                }
            }
            return null;
        }
        @Override
        public List<UserInfo> queryUser() throws RemoteException {
            return data;
        }
        @Override
        public UserInfo handleIn(UserInfo info) throws RemoteException {
            info.setUserName("嘿嘿嘿");
            return info;
        }
        @Override
        public UserInfo handleOu(UserInfo info) throws RemoteException {
            if (info == null) {
                Log.e("App2Service", "UserInfi server is null");
                info = new UserInfo();
            }
            info.setUserName("嘻嘻嘻");
            return info;
        }
        @Override
        public UserInfo handleInOut(UserInfo info) throws RemoteException {
            info.setUserName("哈哈哈");
            return info;
        }
    };
}
複製程式碼

清單檔案的配置是:

     <service
            android:name=".services.App2Service"
            android:exported="true">
            <intent-filter>
                <action android:name="app.androidbinder2.user_service.action"/>
            </intent-filter>
        </service>
複製程式碼

客戶端

主要的程式碼如下:

//...省略程式碼
    private UserService userServerService;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        //...省略程式碼
    }
    ServiceConnection userConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            showToast("連線成功");
            userServerService = UserService.Stub.asInterface(service);
        }
        @Override
        public void onServiceDisconnected(ComponentName name) {
            showToast("與伺服器斷開連線");
        }
    };
    public void startServer(View view) {
        Intent startServer = new Intent();
        startServer.setAction("app.androidbinder2.user_service.action");
        startServer.setComponent(new ComponentName("app.androidbinder2", "app.androidbinder2.services.App2Service"));
        bindService(startServer, userConnection, Service.BIND_AUTO_CREATE);
    }
    public void getUserName(View view) {
        try {
        //實際呼叫
            String name = userServerService.getUserName(1);
            showToast(name);
        } catch (Exception e) {
            e.printStackTrace();
            showToast("出現錯誤");
        }
    }
//...省略程式碼
複製程式碼

AIDL的實際使用

我們就以上面定義的AIDL檔案作為我們的例子。我們需要實現的一個需求是兩個App之間通過AIDL實現資料互通,至於一個App之間多程式也是類似,後面再做例子。 其實上面已經是把例子的程式碼羅列的差不多了。這裡列一下跨APP程式呼叫demo一般的步驟:

  1. 假如有需要自定義的資料需要通過實現Parcelable傳遞,那麼先編寫這種java檔案
  2. 在客戶單編寫一份引入上面編寫的Parcelable實體的AIDL檔案,再編寫一份AIDL介面(一般而言會把所有跨程式的介面編寫在一起)
  3. 通過編譯,當沒有問題之後,需要把上面的需要跨程式的Parcelable的Java檔案以及AIDL檔案複製到Server端,包名需要一致,然後編譯。
  4. 在Server端,新建繼承Service的服務實現,然後在onBinder()中,返回實現我們編譯生成的AIDL介面檔案的Stub子類。
  5. 在Client端,通過ServiceConnection獲取到Server端的Binder例項,進行呼叫。這裡主要用到的是Stub的靜態方法asInterface()。客戶端就可以通過這個例項進行跨程式呼叫了。

同一個APP中跨程式通訊

我們其實在實際中,使用最多的還是這一種情況,目前我所接觸到的就是自己設計的IM系統中使用到。 他其實跟跨APP沒有什麼區別,同一個APP中跨程式,他只是需要一份AIDL檔案即可。因為系統分配給一個程式的記憶體是有限的,而且預設的主程式處理的事務較多,在保活方面,在資料接收處理方面,使用多程式是有優勢的。 Android的APP啟用多程式方式是在清單檔案中新增android:process,設定該元件所處的程式名稱既可。 比如

android:process=":local"//他設定所處的程式名稱是包名+local,他表示的是該元件處於自己的私有程式,其他程式的元件不可以跑在同一個程式中。
android:process="com.app.sample.local"//這裡的name就是程式名稱。這種則是宣告他是全域性程式,其他應用的元件可以通過ShareUID來達到跟他位於同一個程式執行。
複製程式碼

以上就是AIDL的基本使用,當然我們在實際使用中,會更加的複雜,比如我們會在IM程式中開啟多個執行緒來處理訊息,接受訊息,輪訓訊息等,而且需要通過回撥的方式主動通知主程式,一般需要IM端一個Service,主程式端一個Service等,這些我們後面在做講述。

本文如有錯誤或侵權還請指出或聯絡刪除,謝謝。

參考文章

Android 介面定義語言 (AIDL)

詳細介紹Android中Parcelable的原理和使用方法