android之aidl研究(2)

来源:互联网 发布:淘宝客推广计划 编辑:程序博客网 时间:2024/04/30 00:43

如何使用aidl文件来完成跨进程通信

在进行跨进程通信的时候,在AIDL中定义的方法里包含非默认支持的数据类型与否,我们要进行的操作是不一样的。如果不包含,那么我们只需要编写一个AIDL文件,如果包含,那么我们通常需要写 n+1 个AIDL文件( n 为非默认支持的数据类型的种类数)——显然,包含的情况要复杂一些。所以我接下来将只介绍AIDL文件中包含非默认支持的数据类型的情况,至于另一种简单些的情况相信大家是很容易从中触类旁通的。
  1. 使数据类实现 Parcelable 接口

     由于不同的进程有着不同的内存区域,并且它们只能访问自己的那一块内存区域,所以我们不能像平时那样,传一个句柄过去就完事了——句柄指向的是一个内存区域,现在目标进程根本不能访问源进程的内存,那把它传过去又有什么用呢?所以我们必须将要传输的数据转化为能够在内存之间流通的形式。这个转化的过程就叫做序列化与反序列化。简单来说是这样的:比如现在我们要将一个对象的数据从客户端传到服务端去,我们就可以在客户端对这个对象进行序列化的操作,将其中包含的数据转化为序列化流,然后将这个序列化流传输到服务端的内存中去,再在服务端对这个数据流进行反序列化的操作,从而还原其中包含的数据——通过这种方式,我们就达到了在一个进程中访问另一个进程的数据的目的。而通常,在我们通过AIDL进行跨进程通信的时候,选择的序列化方式是实现 Parcelable 接口。关于实现 Parcelable 接口之后里面具体有那些方法啦,每个方法是干嘛的啦,这些我就不展开来讲了,那並非这篇文章的重点,我下面主要讲一下如何快速的生成一个合格的可序列化的类(以Book.java为例)。
    1. 编译器自动生成

      首先,创建一个类,正常的书写其成员变量,建立getter和setter并添加一个无参构造,然后 implements Parcelable ,接着 as 就会报错,将鼠标移到那里,按下 alt+enter(as默认的自动解决错误的快捷键,如果你们的as有修改过快捷键的话以修改后的为准) 让它自动解决错误,这个时候它会帮你完成一部分的工作:在弹出来的框里选择所有的成员变量,然后确定。你会发现类里多了一些代码,但是现在还是会报错,Book下面仍然有一条小横线,再次将鼠标移到那里,按下 alt+enter 让它自动解决错误:但是请注意,这里有一个坑:默认生成的模板类的对象只支持为 in 的定向 tag 。为什么呢?因为默认生成的类里面只有 writeToParcel() 方法,而如果要支持为 out 或者 inout 的定向 tag 的话,还需要实现 readFromParcel() 方法——而这个方法其实并没有在 Parcelable 接口里面,所以需要我们从头写。```@Overridepublic void writeToParcel(Parcel dest, int flags) {    dest.writeString(name);    dest.writeInt(price);}/** * 参数是一个Parcel,用它来存储与传输数据 * @param dest */public void readFromParcel(Parcel dest) {    //注意,此处的读值顺序应当是和writeToParcel()方法中一致的    name = dest.readString();    price = dest.readInt();}```

      完整的Book类的代码是这样的:

      ```package com.lypeer.ipcclient;import android.os.Parcel;import android.os.Parcelable;/** * Book.java * * Created by lypeer on 2016/7/16. */public class Book implements Parcelable{    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;    }    private String name;    private int price;    public Book(){}    public Book(Parcel in) {        name = in.readString();        price = in.readInt();    }    public static final Creator<Book> CREATOR = new Creator<Book>() {        @Override        public Book createFromParcel(Parcel in) {            return new Book(in);        }        @Override        public Book[] newArray(int size) {            return new Book[size];        }    };    @Override    public int describeContents() {        return 0;    }    @Override    public void writeToParcel(Parcel dest, int flags) {        dest.writeString(name);        dest.writeInt(price);    }    /**     * 参数是一个Parcel,用它来存储与传输数据     * @param dest     */    public void readFromParcel(Parcel dest) {        //注意,此处的读值顺序应当是和writeToParcel()方法中一致的        name = dest.readString();        price = dest.readInt();    }    //方便打印数据    @Override    public String toString() {        return "name : " + name + " , price : " + price;    }}```
    2. 插件生成

  2. 书写AIDL文件

    首先我们需要一个 Book.aidl 文件来将 Book 类引入使得其他的 AIDL 文件其中可以使用 Book 对象。鼠标移到app上面去,点击右键,然后 new->AIDL->AIDL File,按下鼠标左键就会弹出一个框提示生成AIDL文件了。在这里,我们需要两个AIDL文件,我是这样写的:
    // Book.aidl//第一类AIDL文件//这个文件的作用是引入了一个序列化对象 Book 供其他的AIDL文件使用//注意:Book.aidl与Book.java的包名应当是一样的package com.lypeer.ipcclient;//注意parcelable是小写parcelable Book;
    // BookManager.aidl//第二类AIDL文件//作用是定义方法接口package com.lypeer.ipcclient;//导入所需要使用的非默认支持数据类型的包import com.lypeer.ipcclient.Book;interface BookManager {    //所有的返回值前都不需要加任何东西,不管是什么数据类型    List<Book> getBooks();    //传参时除了Java基本类型以及String,CharSequence之外的类型    //都需要在前面加上定向tag,具体加什么量需而定    void addBook(in Book book);}
    大家可能注意到了,在 Book.aidl 文件中,我一直在强调:Book.aidl与Book.java的包名应当是一样的。这似乎理所当然的意味着这两个文件应当是在同一个包里面的——事实上,很多比较老的文章里就是这样说的,他们说最好都在 aidl 包里同一个包下,方便移植——然而在 Android Studio 里并不是这样。如果这样做的话,系统根本就找不到 Book.java 文件,从而在其他的AIDL文件里面使用 Book 对象的时候会报 Symbol not found 的错误。为什么会这样呢?因为 Gradle 。大家都知道,Android Studio 是默认使用 Gradle 来构建 Android 项目的,而 Gradle 在构建项目的时候会通过 sourceSets 来配置不同文件的访问路径,从而加快查找速度——问题就出在这里。Gradle 默认是将 java 代码的访问路径设置在 java 包下的,这样一来,如果 java 文件是放在 aidl 包下的话那么理所当然系统是找不到这个 java 文件的。那应该怎么办呢?又要 java文件和 aidl 文件的包名是一样的,又要能找到这个 java 文件——那么仔细想一下的话,其实解决方法是很显而易见的。首先我们可以把问题转化成:如何在保证两个文件包名一样的情况下,让系统能够找到我们的 java 文件?这样一来思路就很明确了:要么让系统来 aidl 包里面来找 java 文件,要么把 java 文件放到系统能找到的地方去,也即放到 java 包里面去。接下来我详细的讲一下这两种方式具体应该怎么做:  修改 build.gradle 文件:在 android{} 中间加上下面的内容:
    sourceSets {    main {        java.srcDirs = ['src/main/java', 'src/main/aidl']    }}
    把 java 文件放到 java 包下去:把 Book.java 放到 java 包里任意一个包下,保持其包名不变,与 Book.aidl 一致。只要它的包名不变,Book.aidl 就能找到 Book.java ,而只要 Book.java 在 java 包下,那么系统也是能找到它的。但是这样做的话也有一个问题,就是在移植相关 .aidl 文件和 .java 文件的时候没那么方便,不能直接把整个 aidl 文件夹拿过去完事儿了,还要单独将 .java 文件放到 java 文件夹里去。
  3. 移植相关文件

    我们需要保证,在客户端和服务端中都有我们需要用到的 .aidl 文件和其中涉及到的 .java 文件,因此不管在哪一端写的这些东西,写完之后我们都要把这些文件复制到另一端去。
  4. 编写服务端代码

    通过上面几步,我们已经完成了AIDL及其相关文件的全部内容,那么我们究竟应该如何利用这些东西来进行跨进程通信呢?其实,在我们写完AIDL文件并 clean 或者 rebuild 项目之后,编译器会根据AIDL文件为我们生成一个与AIDL文件同名的 .java 文件,这个 .java 文件才是与我们的跨进程通信密切相关的东西。事实上,基本的操作流程就是:在服务端实现AIDL中定义的方法接口的具体逻辑,然后在客户端调用这些方法接口,从而达到跨进程通信的目的。服务端代码:
    /** * 服务端的AIDLService.java * <p/> * Created by lypeer on 2016/7/17. */public class AIDLService extends Service {    public final String TAG = this.getClass().getSimpleName();    //包含Book对象的list    private List<Book> mBooks = new ArrayList<>();    //由AIDL文件生成的BookManager    private final BookManager.Stub mBookManager = new BookManager.Stub() {        @Override        public List<Book> getBooks() throws RemoteException {            synchronized (this) {                Log.e(TAG, "invoking getBooks() method , now the list is : " + mBooks.toString());                if (mBooks != null) {                    return mBooks;                }                return new ArrayList<>();            }        }        @Override        public void addBook(Book book) throws RemoteException {            synchronized (this) {                if (mBooks == null) {                    mBooks = new ArrayList<>();                }                if (book == null) {                    Log.e(TAG, "Book is null in In");                    book = new Book();                }                //尝试修改book的参数,主要是为了观察其到客户端的反馈                book.setPrice(2333);                if (!mBooks.contains(book)) {                    mBooks.add(book);                }                //打印mBooks列表,观察客户端传过来的值                Log.e(TAG, "invoking addBooks() method , now the list is : " + mBooks.toString());            }        }    };    @Override    public void onCreate() {        super.onCreate();        Book book = new Book();        book.setName("Android开发艺术探索");        book.setPrice(28);        mBooks.add(book);       }    @Nullable    @Override    public IBinder onBind(Intent intent) {        Log.e(getClass().getSimpleName(), String.format("on bind,intent = %s", intent.toString()));        return mBookManager;    }}
    接下来在 Manefest 文件里面注册这个我们写好的 Service ,这个不写的话我们前面做的工作都是无用功:
    <service    android:name=".service.AIDLService"    android:exported="true">        <intent-filter>            <action android:name="com.lypeer.aidl"/>            <category android:name="android.intent.category.DEFAULT"/>        </intent-filter></service>
  5. 编写客户端代码

    在客户端我们要完成的工作主要是调用服务端的方法,但是在那之前,我们首先要连接上服务端
    /** * 客户端的AIDLActivity.java * 由于测试机的无用debug信息太多,故log都是用的e * <p/> * Created by lypeer on 2016/7/17. */public class AIDLActivity extends AppCompatActivity {    //由AIDL文件生成的Java类    private BookManager mBookManager = null;    //标志当前与服务端连接状况的布尔值,false为未连接,true为连接中    private boolean mBound = false;    //包含Book对象的list    private List<Book> mBooks;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_aidl);    }    /**     * 按钮的点击事件,点击之后调用服务端的addBookIn方法     *     * @param view     */    public void addBook(View view) {        //如果与服务端的连接处于未连接状态,则尝试连接        if (!mBound) {            attemptToBindService();            Toast.makeText(this, "当前与服务端处于未连接状态,正在尝试重连,请稍后再试", Toast.LENGTH_SHORT).show();            return;        }        if (mBookManager == null) return;        Book book = new Book();        book.setName("APP研发录In");        book.setPrice(30);        try {            mBookManager.addBook(book);            Log.e(getLocalClassName(), book.toString());        } catch (RemoteException e) {            e.printStackTrace();        }    }    /**     * 尝试与服务端建立连接     */    private void attemptToBindService() {        Intent intent = new Intent();        intent.setAction("com.lypeer.aidl");        intent.setPackage("com.lypeer.ipcserver");        bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);    }    @Override    protected void onStart() {        super.onStart();        if (!mBound) {            attemptToBindService();        }    }    @Override    protected void onStop() {        super.onStop();        if (mBound) {            unbindService(mServiceConnection);            mBound = false;        }    }    private ServiceConnection mServiceConnection = new ServiceConnection() {        @Override        public void onServiceConnected(ComponentName name, IBinder service) {            Log.e(getLocalClassName(), "service connected");            mBookManager = BookManager.Stub.asInterface(service);            mBound = true;            if (mBookManager != null) {                try {                    mBooks = mBookManager.getBooks();                    Log.e(getLocalClassName(), mBooks.toString());                } catch (RemoteException e) {                    e.printStackTrace();                }            }        }        @Override        public void onServiceDisconnected(ComponentName name) {            Log.e(getLocalClassName(), "service disconnected");            mBound = false;        }    };}
  6. 开始通信吧!

    将两个app同时运行在同一台手机上,然后调用客户端的 addBook() 方法,我们会看到服务端的 logcat 信息是这样的:
    //服务端的 log 信息,我把无用的信息头去掉了,然后给它编了个号1on bind,intent = Intent { act=com.lypeer.aidl pkg=com.lypeer.ipcserver }2,invoking getBooks() method , now the list is : [name : Android开发艺术探索 , price : 28]3,invoking addBooks() method , now the list is : [name : Android开发艺术探索 , price : 28, name : APP研发录In , price : 2333]
    客户端信息:
    //客户端的 log 信息1,service connected2,[name : Android开发艺术探索 , price : 28]3,name : APP研发录In , price : 30
0 0
原创粉丝点击