IPC机制 -- IPC方式 -- AIDL(1)

一、AIDL基础

文件类型:

后缀是 .aidl,而不是 .java。

数据类型:

1.默认支持的数据类型
在使用这些数据类型的时候是不需要导包的。
(1)Java中的八种基本数据类型,包括 byte,short,int,long,float,double,boolean,char。
(2)String 类型。
(3)CharSequence类型。
(4)List类型:只支持ArrayList,里面的所有元素都必须能够被AIDL支持。
注意:CopyOnWriteArrayList,支持并发读/写,可在服务端中使用,在Binder中会按照List的规范去访问数据并最终形成一个新的ArrayList传递给客户端。
(5)Map类型:只支持HashMap,里面的所有元素都必须能够被AIDL支持,包括key和value。
注意:ConcurrentHashMap,支持并发读/写,可在服务端中使用,在Binder中会按照Map的规范去访问数据并最终形成一个新的HashMap传递给客户端。

2.非默认支持的数据类型
在使用这些数据类型的时候必须导包,就算目标文件与当前正在编写的 .aidl 文件在同一个包下也一样要导包。
(1)Parcelable:所有实现了Parcelable接口的对象。
(2)AIDL:所有的AIDL接口本身也可以在AIDL文件中使用。

定向tag:

AIDL中的定向 tag 表示了在跨进程通信中数据的流向,其中 in 表示数据只能由客户端流向服务端, out 表示数据只能由服务端流向客户端,而 inout 则表示数据可在服务端与客户端之间双向流通。其中,数据流向是针对客户端中传入方法的对象参数而言的。
in:表现为服务端将会接收到一个对象参数的完整数据,但是客户端的那个对象参数不会因为服务端对接收到的对象参数的修改而发生变动;
out:表现为服务端将会接收到一个对象参数的空对象,但是在服务端对接收到的空对象有任何修改之后客户端将会同步变动;
inout:表现为服务端将会接收到一个对象参数的完整数据,并且在服务端对接收到的对象参数有任何修改之后客户端将会同步变动。
另外,Java 中的基本类型和 String ,CharSequence 的定向 tag 默认且只能是 in。

AIDL文件分类:

所有的AIDL文件大致可以分为两类:
第一类:用来定义parcelable对象,以供其他AIDL文件使用AIDL中非默认支持的数据类型的;
第二类:用来定义方法接口,以供系统使用来完成跨进程通信的。
可以看到,两类文件都是在"定义"些什么,而不涉及具体的实现,这就是为什么它叫做"Android接口定义语言"。
注意:所有的非默认支持的数据类型必须通过第一类AIDL文件定义才能被使用。

其它:

1.AIDL接口中只支持方法,不支持声明静态常量。

二、AIDL使用

1.使数据类实现Parcelable接口

由于不同的进程有着不同的内存区域,并且它们只能访问自己的那一块内存区域,因此我们无法从源进程向目标进程传递一个句柄(句柄指向的是一个内存区域),而必须将要传输的数据转化为能够在内存之间流通的形式。这个转化的过程就叫做序列化与反序列化。
序列化与反序列化的大概流程为:我们要将一个对象的数据从客户端传到服务端,我们就可以在客户端对这个对象进行序列化的操作,将其中包含的数据转化为序列化流,然后将这个序列化流传输到服务端的内存中,再在服务端对这个数据流进行反序列化的操作,从而还原其中包含的数据。通过这种方式,我们就达到了在一个进程中访问另一个进程的数据的目的。

//数据类实现Parcelable接口
public class Book implements Parcelable{
    private String name;
    private int price;

    public Book(){}

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }

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

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

    public static final Creator CREATOR = new Creator() {
        @Override
        public Book createFromParcel(Parcel in) {
            return new Book(in);
        }

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

    private Book(Parcel in) {
        name = in.readString();
        price = in.readInt();
    }

    //AIDL文件中,方法参数的定向tag为 out 跟 inout 时,需要用到此方法
    //作用:服务端修改客户端传递的对象参数,传回到客户端,客户端通过此方法读取传回的数据
    public void readFromParcel(Parcel dest) {
        //注意,此处的读值顺序应当是和writeToParcel()方法中一致的
        name = dest.readString();
        price = dest.readInt();
    }

    @Override
    public String toString() {
        return "name : " + name + " , price : " + price;
    }
}

2.创建AIDL文件

鼠标移到app上面去,点击右键,然后 new==>AIDL==>AIDL File,按下鼠标左键就会弹出一个框提示生成AIDL文件了。项目的目录比起以前多了一个叫做 aidl 的包,而且它的层级是和 java 包相同的,并且 aidl 包里默认有着和 java 包里默认的包结构。

//Book.aidl
//第一类AIDL文件
//作用是引入了一个序列化对象 Book 供其他的AIDL文件使用
//注意:Book.aidl与Book.java的包名应当是一样的
package com.tomorrow.androidtest7.aidl;

//注意parcelable是小写
parcelable Book;


//BookManager.aidl
//第二类AIDL文件
//作用是定义方法接口
package com.tomorrow.androidtest7.aidl;
//导入所需要使用的非默认支持数据类型的包
import com.tomorrow.androidtest7.aidl.Book;

interface BookManager {

    //所有的返回值前都不需要加任何东西,不管是什么数据类型
    List getBooks();

    //传参时除了Java基本类型以及String,CharSequence之外的类型
    //都需要在前面加上定向tag,具体加什么量需而定
    void addBookWithTagIn(in Book book);
    void addBookWithTagOut(out Book book);
    void addBookWithTagInOut(inout Book book);
}

3.移植相关文件

我们需要保证,在客户端和服务端中都有我们需要用到的 .aidl 文件(如Book.aidl、BookManager.aidl)和其中涉及到的 .java 文件(如Book.java),因此不管在哪一端写这些文件,写完之后我们都要把这些文件复制到另一端去,并且保证在客户端跟服务端有相同的目录。

4.编写服务端代码

在我们写完AIDL文件并 clean 或者 rebuild 项目之后,编译器会根据AIDL文件为我们生成一个与AIDL文件同名的 .java 文件,这个 .java 文件与跨进程通信密切相关。
基本的操作流程就是:在服务端实现AIDL中定义的方法接口的具体逻辑,然后在客户端调用这些方法接口,从而达到跨进程通信的目的。

//服务端代码
public class AIDLService extends Service {

    public final String TAG = this.getClass().getSimpleName();

    //包含Book对象的list
    private List mBooks = new ArrayList<>();

    //由AIDL文件生成的BookManager
    private final BookManager.Stub mBookManager = new BookManager.Stub() {
        @Override
        public List getBooks() throws RemoteException {
            synchronized (this) {
                Log.e(TAG, "zwm, invoking getBooks() method , now the list is : " + mBooks.toString());
                if (mBooks != null) {
                    return mBooks;
                }
                return new ArrayList<>();
            }
        }

        @Override
        public void addBookWithTagIn(Book book) throws RemoteException {
            synchronized (this) {
                if (mBooks == null) {
                    mBooks = new ArrayList<>();
                }
                if (book == null) {
                    Log.e(TAG, "zwm, Book is null in In");
                    book = new Book();
                }
                //尝试修改book的参数,主要是为了观察其到客户端的反馈
                book.setPrice(5968);
                if (!mBooks.contains(book)) {
                    mBooks.add(book);
                }
                //打印mBooks列表,观察客户端传过来的值
                Log.e(TAG, "zwm, invoking addBooks() method , now the list is : " + mBooks.toString());
            }
        }

        @Override
        public void addBookWithTagOut(Book book) throws RemoteException {
            Log.e(TAG, "zwm, addBookWithTagOut");
            addBookWithTagIn(book);
        }

        @Override
        public void addBookWithTagInOut(Book book) throws RemoteException {
            Log.e(TAG, "zwm, addBookWithTagInOut");
            addBookWithTagIn(book);
        }
    };

    @Override
    public void onCreate() {
        super.onCreate();
        Log.e(TAG, "zwm, onCreate");
        Book book = new Book();
        book.setName("Android开发艺术探索");
        book.setPrice(88);
        mBooks.add(book);
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        Log.e(getClass().getSimpleName(), String.format("zwm, on bind,intent = %s", intent.toString()));
        return mBookManager;
    }
}

//在AndroidManifest.xml注册Service

    
        
        
    

5.编写客户端代码

客户端的工作主要是绑定服务端,并调用服务端的方法。

//客户端代码
public class AIDLActivity extends AppCompatActivity {
    //由AIDL文件生成的Java类
    private BookManager mBookManager = null;

    //标志当前与服务端连接状况的布尔值,false为未连接,true为连接中
    private boolean mBound = false;

    //包含Book对象的list
    private List mBooks;

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

    /**
     * 按钮的点击事件,点击之后调用服务端的addBook方法
     */
    public void addBook() {
        Log.e(getLocalClassName(), "zwm, addBook");
        //如果与服务端的连接处于未连接状态,则尝试连接
        if (!mBound) {
            attemptToBindService();
            Toast.makeText(this, "当前与服务端处于未连接状态,正在尝试重连,请稍后再试", Toast.LENGTH_SHORT).show();
            return;
        }
        if (mBookManager == null) return;

        Book book = new Book();
        book.setName("APP研发录");
        book.setPrice(66);
        try {
            mBooks = mBookManager.getBooks();
            Log.e(getLocalClassName(), "zwm, before add book, 参数:" + book.toString());
            Log.e(getLocalClassName(), "zwm, before add book, 返回值:" + mBooks.toString());
            mBookManager.addBookWithTagInOut(book);
            mBooks = mBookManager.getBooks();
            Log.e(getLocalClassName(), "zwm, after add book, 参数:" + book.toString());
            Log.e(getLocalClassName(), "zwm, after add book, 返回值:" + mBooks.toString());
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

        /**
         * 尝试与服务端建立连接
         */
    private void attemptToBindService() {
        Log.e(getLocalClassName(), "zwm, attemptToBindService");
        Intent intent = new Intent();
        intent.setClassName("com.tomorrow.androidtest7", "com.tomorrow.androidtest7.service.AIDLService");
        bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
    }

    @Override
    protected void onStart() {
        super.onStart();
        Log.e(getLocalClassName(), "zwm, onStart");
        if (!mBound) {
            attemptToBindService();
        }
    }

    @Override
    protected void onStop() {
        super.onStop();
        Log.e(getLocalClassName(), "zwm, onStop");
        if (mBound) {
            unbindService(mServiceConnection);
            mBound = false;
        }
    }

    private ServiceConnection mServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            Log.e(getLocalClassName(), "zwm, service connected");
            mBookManager = BookManager.Stub.asInterface(service);
            mBound = true;

            if (mBookManager != null) {
                try {
                    mBooks = mBookManager.getBooks();
                    Log.e(getLocalClassName(), "zwm, " + mBooks.toString());
                    addBook();
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            Log.e(getLocalClassName(), "zwm, service disconnected");
            mBound = false;
        }
    };
}

//在AndroidManifest.xml注册Activity

6.测试

将两个app同时运行在同一台手机上,就可以开始进程间通信了。

三、高级用法

1.给Binder设置死亡代理

Binder运行在服务端进程,如果服务端进程由于某种原因异常终止,这个时候我们到服务端的Binder连接断裂(称为Binder死亡),会导致我们的远程调用失败。更关键的是,如果我们不知道Binder连接已经断裂,那么客户端的功能就会受到影响。这时我们可以给Binder设置一个死亡代理,当Binder死亡时,我们就会收到通知,然后可以重新发起连接请求从而恢复连接。Binder提供两个配对方法linkToDeathunlinkToDeath,使用代码如下:

//AIDLActivity
private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {

    @Override
    public void binderDied() {
        Log.e(getLocalClassName(), "zwm, binderDied");
        if(mBookManager == null)
            return;
        mBookManager.asBinder().unlinkToDeath(mDeathRecipient, 0);
        mBookManager = null;
        //重新绑定远程service
        attemptToBindService();
    }
};

private ServiceConnection mServiceConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        mBookManager = BookManager.Stub.asInterface(service);
        try {
            service.linkToDeath(mDeathRecipient, 0); //第二个参数直接设为0即可
        } catch (RemoteException e) {
            e.printStackTrace();
        }
        //省略
    }
    //省略
}

注意:binderDied方法运行在Binder线程池。
另外,通过Binder的方法isBinderAlive也可以判断Binder是否死亡。

2.观察者模式

定义AIDL监听接口,实现客户端对服务端的数据进行监听,当服务端的数据改变时会通知客户端。

//AIDL监听接口
package com.tomorrow.androidtest7.aidl;
import com.tomorrow.androidtest7.aidl.Book;
interface OnNewBookArrivedListener {
    void onNewBookArrived(in Book newBook);
}

//在原有的接口中添加两个新方法
package com.tomorrow.androidtest7.aidl;
import com.tomorrow.androidtest7.aidl.Book;
import com.tomorrow.androidtest7.aidl.OnNewBookArrivedListener;
interface BookManager {
    List getBooks();
    void addBookWithTagIn(in Book book);
    void addBookWithTagOut(out Book book);
    void addBookWithTagInOut(inout Book book);

    void registerListener(OnNewBookArrivedListener  listener); //注册监听
    void unregisterListener(OnNewBookArrivedListener  listener); //解注册监听
}

问题:
客户端创建OnNewBookArrivedListener.Stub Binder对象,调用服务端BookManager.Stub.Proxy#registerListener(listener)方法进行注册监听,注册监听之后客户端能成功接收到服务端的通知。
客户端再调用服务端BookManager.Stub.Proxy#unregisterListener(listener)方法进行解注册监听,解注册监听之后客户端仍然能成功接收到服务端的通知。
原因:
对客户端来说,注册监听跟解注册监听的listener对象是同一个。但是对服务端来说,服务端接收到的注册监听跟解注册监听的listener对象却是不同的,因为对象不能跨进程直接传输,对象的跨进程传输本质上都是反序列化的过程,因此在服务端重新生成了两个对象,导致解注册失败。
方案:
在服务端使用系统专门提供的用于删除跨进程listener的接口RemoteCallbackList。
RemoteCallbackList是一个泛型,支持管理任意的AIDL接口。虽然多次跨进程传输客户端的同一个对象会在服务端生成不同的对象,但是这些新生成的对象有一个共同点,那就是它们底层的Binder对象是同一个,RemoteCallbackList就是利用这个特性来完成注册监听跟解注册监听功能。

//服务端BookManager$Stub的实现代码
private RemoteCallbackList mListenerList = new RemoteCallbackList();

@Override
public void registerListener(OnNewBookArrivedListener listener) throws RemoteException {
    mListenerList.register(listener);
}

@Override
public void unregisterListener(OnNewBookArrivedListener listener) throws RemoteException {
    mListenerList.unregister(listener);
}

private void onNewBookArrived(Book book) throws RemoteException {
    mBookList.add(book);
    final int N = mListenerList.beginBroadcast(); //要跟finishBroadcast匹配
    for(int i = 0; i < N; i++) {
        OnNewBookArrivedListener  listener = mListenerList.getBroadcastItem(i);
        if(listener != null) {
            try {
                listener.onNewBookArrived(book);
            } catch(RemoteException e) {
                e.printStackTrace();
            }
        }
    }
    mListenerList.finishBroadcast(); //要跟beginBroadcast匹配
}

3.在AIDL中进行权限验证

方法一:在服务端的AIDL方法中进行权限验证

//在服务端的AndroidManifest.xml中声明所需的权限


//服务端的AIDL方法
int check = checkCallingOrSelfPermission("com.tomorrow.androidtest7.permission.ACCESS_BOOK_SERVICE");
if(check == PackageManager.PERMISSION_DENIED) {
    return;
}
...

//如果客户端想绑定服务,需要在AndroidManifest.xml中使用所需的权限

方法二:在服务端Stub的onTransact方法中进行权限验证

@Override  
public boolean onTransact(int code, Parcel data, Parcel reply, int flags)  
        throws RemoteException {  
    // TODO Auto-generated method stub  
    int checkPermission = checkCallingOrSelfPermission("com.tomorrow.androidtest7.permission.ACCESS_BOOK_SERVICE");  
    if (checkPermission == PackageManager.PERMISSION_DENIED) {  
        return false;  
    }  
    String packageName = "";  
    //通过getCallingUid()方法得到客户端的uid,  
    //接着通过PackageManager的getPackagesForUid(int uid)方法得到客户端Package相关信息  
    String[] packages = getPackageManager().getPackagesForUid(getCallingUid());  
    if (packages != null && packages.length > 0) {  
        //字符串数组packages中第一个字符串是包名信息  
        packageName = packages[0];  
    }  
    //如果包名不是以"com.tomorrow"开头,直接返回false,权限验证失败  
    if (!packageName.startsWith("com.tomorrow")) {  
        return false;  
    }  
    return super.onTransact(code, data, reply, flags);  
}

方法三:为服务端Service指定android:permission属性
方法四:其它方法

你可能感兴趣的:(IPC机制 -- IPC方式 -- AIDL(1))