IPC机制之AIDL的学习笔记(一)

前言

ipc机制简介

IPC机制是什么?最简单的名词解释就叫跨进程通信,意思是两个不同的进程之间可以相互协作完成一些事情。比如a进程调用b进程的一些方法来达到更新列表的目的。在Android系统中一个应用默认只有一个进程,每个进程都有自己独立的资源和内存空间,其它进程不能任意访问当前进程的内存和资源,系统给每个进程分配的内存会有限制。如果一个进程占用内存超过了这个内存限制,就会报OOM的问题,很多涉及到大图片的频繁操作或者需要读取一大段数据在内存中使用时,很容易报OOM的问题,为了彻底地解决应用内存的问题,Android引入了多进程的概念,它允许在同一个应用内,为了分担主进程的压力,将占用内存的某些页面单独开一个进程,比如Flash、视频播放页面,频繁绘制的页面等。

Andriod多进程使用方法

  • 通过 JNI 在 native 层 fork 一个新的进程。(比较少用)
  • 在AndroidManifest.xml的声明四大组件的标签中增加”android:process”属性即可
    1.以:为前缀:在当前进程前面加上包名。属于当前App的私有进程,其他App的组件不可以和她跑在同一个进程中。
    2.以.为前缀:不会附加包名。全局进程,其他App通过 ShareUID 的方式可以和她跑在一个进程中。

效果图展示

gif

正文

AIDL简介

描述

  • AIDL(Android接口描述语言)是一个IDL语言,它可以生成一段代码,可以是一个在Android设备上运行的两个进程使用内部通信进程进行交互。在Android上,一个进程通常无法访问另一个进程的内存。AIDL只有在你允许来自不同应用的客户端跨进程通信访问你的Service,并且想要在你的Service种处理多线程的时候才是必要的。 简单地来说,就是多个客户端,多个线程并发的情况下要使用 AIDL 。

语法

  • 通常引引用方式传递的其他AIDL生成的接口,必须要import 语句声明。
    Java编程语言的主要类型 (int, boolean等) —不需要 import 语句。
    在AIDL文件中,并不是所有的数据类型都是可以使用的,那么到底AIDL文件中支持哪些数据类型呢?
    如下所示:
    1、基本数据类型(int,long,char,boolean,float,double,byte,short八种基本类型);
    2、String和CharSequence;
    3、List:只支持ArrayList,里面每个元素都必须能够被AIDL支持;
    4、Map:只支持HashMap,里面的每个元素都必须被AIDL支持,包括key和value;
    5、Parcelable:所有实现了Parcelable接口的对象;
    6、AIDL:所有的AIDL接口本身也可以在AIDL文件中使用;
    以上6中数据类型就是AIDL所支持的所有类型,其中自定义的Parcelable对象和AIDL对象必须要显式import进来,不管它们是否和当前的AIDL文件位于同一个包内。

  • 关于Aidl中in out inout修饰参数的理解【摘自你真的理解AIDL中的in,out,inout么?
    写得很详细,摘录记录一下笔记
    AIDL中的定向 tag 表示了在跨进程通信中数据的流向,其中 in 表示数据只能由客户端流向服务端, out 表示数据只能由服务端流向客户端,而 inout 则表示数据可在服务端与客户端之间双向流通。其中,数据流向是针对在客户端中的那个传入方法的对象而言的。in 为定向 tag 的话表现为服务端将会接收到一个那个对象的完整数据,但是客户端的那个对象不会因为服务端对传参的修改而发生变动;out 的话表现为服务端将会接收到那个对象的的空对象,但是在服务端对接收到的空对象有任何修改之后客户端将会同步变动;inout 为定向 tag 的情况下,服务端将会接收到客户端传来对象的完整信息,并且客户端将会同步服务端对该对象的任何变动。

AIDL的使用步骤

  • 在AS中我们可以在同一个project中实现多进程的测试,只要在服务端的service在manifest文件中指定android:process=":remote"属性,它就是运行在另外一个进程的服务了。当然,这种做法是比较简单的。另外一种方法是将服务端和客户端分别运行不同的项目中。
    今天项目是要建立两个module,一个服务端module,一个客户端module。虽然上面两种方法的机制是一样的。但是运行在两个module的两个细节还是挺多的。今天主要写一下AIDL的使用方法和其中的细节采坑。

目录两个module的目录接口

客户端

服务端

服务端

1.创建Person.java【关于Parcelable的创建不在这里介绍,aidl中客户端和服务端的数据必须是可以序列化的】

package com.example.newserver;

import android.os.Parcel;
import android.os.Parcelable;

/**
 * Created by Administrator on 2017/8/21.
 */

public class Person implements Parcelable {
    private int id;
    private String name;
    private String age;

    public Person() {
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public String getAge() {
        return age;
    }

    public void setAge(String age) {
        this.age = age;
    }

    protected Person(Parcel in) {
        id = in.readInt();
        name = in.readString();
        age = in.readString();
    }

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

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

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

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

    public void readFromParcel(Parcel dest){
        id = dest.readInt();
        name = dest.readString();
        age = dest.readString();
    }
}

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

// Person.aidl
package com.example.newserver;

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

parcelable Person;

然后创建IMyAidlInterface.aidl

// IMyAidlInterface.aidl
package com.example.newserver;
import com.example.newserver.Person;

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

interface IMyAidlInterface {
    /**
     * Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
    void addPerson(in Person person);
     List<Person> getPersonList();
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);
}

注意点:

  • Person.aidl与Person.java的包名应当是一样的。
    关于这点有的人说需要把这两个文件同时放在aidl文件夹下就可以了,但是有的人说这样没用,系统会找不到java文件,Gradle 在构建项目的时候会通过 sourceSets 来配置不同文件的访问路径,从而加快查找速度。Gradle 默认是将 java 代码的访问路径设置在 java 包下的。如果不配置,它就不会在aidl文件夹下找到java文件。关于这点的解决方法是在gradle文件下配置一下soureSets
sourceSets {
        main {
            java.srcDirs = ['src/main/java', 'src/main/aidl']
        }
    }

这样他就会在aidl文件夹找java文件了,其实发现后来运行的时候不配置也没有报错。不知道什么原因,如果你的工程包类找不到。你就尝试加这个东西吧。
当然还有另外一种解决方法。就是保证aidl【com.example.server】和java【com.example.server】的文件夹名字是相同的。然后把person.java文件放在java文件夹下面就不会有什么问题啦,不过我一般都会采用第二种方法,放在java文件下面。关于两种方法你都可以试一下。感觉第一种比较简单实用。

3.创建service【MyService.java】


public class MyService extends Service {

   ArrayList<Person> mPersonArrayList = new ArrayList<>();

   IMyAidlInterface.Stub mBinder = new IMyAidlInterface.Stub() {
       @Override
       public void addPerson(Person person) throws RemoteException {
           if (null == mPersonArrayList) {
               mPersonArrayList = new ArrayList<>();
           }
           mPersonArrayList.add(person);
       }

       @Override
       public List<Person> getPersonList() throws RemoteException {
           if (null != mPersonArrayList) {
             return mPersonArrayList;
           }
           return null;
       }

       @Override
       public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {

       }
   };

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

基本上就是实现接口的方法,这里会创建一个Binder返回给客户端,这个Binder是已经实现了定义的接口方法的,所以客户端会根据这个binder调用相关的接口方法。这样就实现了客户端调用了服务端定义的接口方法了,关于binder的原理可以看看这一篇的,写的比较详细Binder的原理讲解,我们可以在这里了解一下binder的原理。通过上述步骤实际上就实现了客户端与服务端的通信了,关于客户端怎么拿到binder,具体又怎么用binder的,我们等会看看客户端的代码就知道了。

客户端实现

1.将aidl整个文件夹复制到客户端module下面去
而且在aidl中涉及的java文件也要拷贝过去。例如本例中的person文件。由于我在服务端module中的person.java文件放在了java文件目录下面,所以我拷贝的时候也将它放在了客户端module对应的java文件夹下面。这个时候就有个问题要注意的,因为这里真的是完全拷贝。
我们看看客户端person.java

person

这里package的目录页必须是server目录的person.java文件的目录,也就是

package com.example.newserver;

而不是

package com.example.client;

否则变异会报错。上面标红只是警告,编译不会有问题的。
还有客户端这里的aidl文件夹下面的aidl文件,涉及到person导包的,都是要导入server下面的person的,即

image.png

这里复制过来的文件路径都不能因为迁移到客户端改动。

2.我们来看看 MainActivity中怎么获取和使用Binder吧

public class MainActivity extends AppCompatActivity {
  
    IMyAidlInterface binder;

    ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            binder = IMyAidlInterface.Stub.asInterface(service);
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };

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

    private void initView() {
        etId = (EditText) findViewById(R.id.et_id);
        etName = (EditText) findViewById(R.id.et_name);
        etAge = (EditText) findViewById(R.id.et_age);
        btnAdd = (Button) findViewById(R.id.btn_add);
        btnGet = (Button) findViewById(R.id.btn_get);
        mRecyclerView = (RecyclerView) findViewById(R.id.recyclerview);
        mRecyclerView.setLayoutManager(new LinearLayoutManager(mContext));
        final MyAdapter myAdapter = new MyAdapter(mContext);
        mRecyclerView.setAdapter(myAdapter);
        btnAdd.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                name = etName.getText().toString();
                id = etId.getText().toString();
                age = etAge.getText().toString();
                if (checkData()) {
                    try {
                        Person person = new Person();
                        person.setName(name);
                        person.setId(Integer.valueOf(id));
                        person.setAge(age);
                        binder.addPerson(person);
                        clearPerson();
                        Toast.makeText(mContext,"添加英雄成功,点击获取任务可查看英雄列表",Toast.LENGTH_LONG).show();
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        btnGet.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                try {
                    if (null != binder) {
                        mList = (ArrayList<Person>) binder.getPersonList();
                    }
                    if (null != mList && mList.size() > 0) {
                        myAdapter.addList(mList);
                    } else {
                        Toast.makeText(mContext,"还没有添加英雄,赶紧添加一个吧~",Toast.LENGTH_SHORT).show();
                    }
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        });
        Intent intent = new Intent();
        intent.setPackage("com.example.newserver");
        intent.setAction("com.example.newserver.aidl.IMyAidlInterface");
        bindService(intent,mConnection,mContext.BIND_AUTO_CREATE);
    }
}

上面我只看核心的实现。定义一个刚才我们在服务端定义的接口变量。然后在ServiceConnection中的onServiceContected方法中将IBinder接口转换一下这样我们就可以使用IMyAidlInterface 这个接口来调用服务端定义的方法了。

运行代码

  • 首先我们先跑我们的服务端的代码,然后点击主界面让他运行在后台中。
  • 然后跑一下客户端代码。这样就可以看到效果了。

结语

我们看到关于实现是非常简单的,今天先写一下实现过程,下一章在写它到底是怎么实现,
然后我们可以脱离aidl代码来实现这个功能。

代码传送门
传送门~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容