Android Studio 实现使用AIDL的IPC通讯

本文通过实战案例,详细讲解了Android AIDL的使用方法。包括AIDL文件的创建与配置、 Parcelable接口的实现、Service端与客户端的交互过程等。

        推荐一篇很全面的关于Android Service的博客 ,博客的后半段详细讲了AIDL通讯。


        程序将实现这样一个简单功能:Service端提供多种饮料信息供选择,Client端先选择种类,再选择数量,随后计算总价。

        新建一个Android项目,我这里取名DrunkService,然后新建一个包aidl。
        在aidl包上右键新建AIDL,如下图(图1):


图1

        名字随意起,但是建议I开头,仅仅是建议......

        接下来会看到AS产生一个和java平级的aidl文件夹,新建的xxx.aidl文件拥有和创建位置一样的包路径(图2)。

图2

        打开IDrunkSelect类如下图(图3):

图3

        类中会有一个默认的类,而这个basecType是我们不需要的,它存在的意思是提醒我们这里可以直接使用的六种基本类型(其实还有CharSequenceListMap、自定义),删掉basicType,写我们自己的方法:

interface IDrunkSelect {

    /**
     * 饮料简单信息列表
     */
    List<String> getDrunkInfoList();

    /**
     * 某个饮料详细信息
     */
    Drunks getDrunk(int id);

    /**
     * 修改库存
     */
    void subtractNum(int id, int subNum);

}

        Drunks是什么鬼?别急,这就说。

        在IDrunkSelect 同包下新建普通类Drunks,继承Parcelable接口,代码如下:

        关于Parcelable可以看这里Android序列化

package com.shareye.drunkservice.aidl;

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

/**
 * Created by ShuaiZhang on 2016/6/24.
 */
public class Drunks implements Parcelable{

    private int id;
    private String name;
    private double price;
    private int num;

    public Drunks(){
        super();
    }
    public Drunks(int id, String name, double price, int num){
        this.id = id;
        this.name = name;
        this.price = price;
        this.num = num;
    }

    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 double getPrice() {
        return price;
    }

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

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
    }

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

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        //这里要保证顺序的正确定
        dest.writeInt(id);
        dest.writeString(name);
        dest.writeDouble(price);
        dest.writeInt(num);
    }

    public static final Parcelable.Creator<Drunks> CREATOR = new Parcelable.Creator<Drunks>() {
        public Drunks createFromParcel(Parcel in) {
            Drunks s = new Drunks();
            s.setId(in.readInt());
            s.setName(in.readString());
            s.setPrice(in.readDouble());
            s.setNum(in.readInt());
            return s;
        }

        public Drunks[] newArray(int size) {
            return new Drunks[size];
        }
    };

}

        这个时候的Drunks还不可以在AIDL中使用,需要在AIDL中声明。

        相同位置新建Drunks.aidl

        代码如下:

// Drunks.aidl
package com.shareye.drunkservice.aidl;

// Declare any non-default types here with import statements
parcelable Drunks;

        仔细看,默认生成的interface Drunks{}被删除掉了。这个很重要。

        好了,然后Build一下。

        依然不能通过编译,IDrunksService依然找不到Drunks,这里需要在IDrunksService中手动引入:

import com.shareye.drunkservice.aidl.Drunks;
        Build吧,这个时候就正常了。

        新建一个DrunkDataUtil类作为数据源。

public class DrunksDataUtil {

    public static Drunks[] drunks= {
            new Drunks(1,"橙汁",3.50,30),new Drunks(2,"橙汁听装",2.00,20),
            new Drunks(3,"雪碧",3.50,20),new Drunks(4,"雪碧听装",2.00,25),
            new Drunks(5,"咖啡",5.00,27),new Drunks(6,"咖啡听装",3.5,15)
    };
}

        新建个类,继承IDrunkSelect.Stub,实现默认方法;这一步也可以省略,在之后的Service中使用匿名内部类也可以。但是匿名内部类有时候不太好控制。代码如下:

public class DrunkSelectImp extends IDrunkSelect.Stub {
    @Override
    public List<String> getDrunkInfoList() throws RemoteException {
        List<String> list = new ArrayList<>();
        for(Drunks drunks : DrunksDataUtil.drunks){
            list.add(drunks.getId()+"."+drunks.getName()+drunks.getPrice());
        }
        return list;
    }

    @Override
    public Drunks getDrunk(int id) throws RemoteException {
        return DrunksDataUtil.drunks[id];
    }

    @Override
    public void subtractNum(int id, int subNum) throws RemoteException {
        DrunksDataUtil.drunks[id].setNum(DrunksDataUtil.drunks[id].getNum()-subNum);
    }
}

        然后是特别简单的Service

public class DrunkService extends Service {

    IBinder iBinder = new DrunkSelectImp();

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

        重要AIDL部分的写完了,接下来就是简单界面代码了。

        Activity_main布局文件:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ListView
        android:id="@+id/lst_drunk"
        android:layout_width="500dp"
        android:layout_height="500dp"/>
</RelativeLayout>

        Item_lst_drunk布局文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:padding="10dp">
        <TextView
            android:id="@+id/txt_id"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
        <TextView
            android:id="@+id/txt_name"
            android:layout_marginLeft="10dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

    </LinearLayout>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <TextView
            android:id="@+id/txt_price"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
        <TextView
            android:id="@+id/txt_num"
            android:layout_marginLeft="40dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

    </LinearLayout>
</LinearLayout>

        MainActivity代码:

 

package com.shareye.drunkservice;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.TextView;

import com.shareye.drunkservice.aidl.Drunks;


public class MainActivity extends AppCompatActivity {

    private DrunkAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        adapter = new DrunkAdapter();
        ListView lstView = (ListView) findViewById(R.id.lst_drunk);
        lstView.setAdapter(adapter);
    }

    @Override
    protected void onResume() {
        super.onResume();
        //重新进入界面刷新数据
        adapter.notifyDataSetChanged();
    }

    private class DrunkAdapter extends BaseAdapter{

        @Override
        public int getCount() {
            return DrunksDataUtil.drunks.length;
        }

        @Override
        public Drunks getItem(int position) {
            return DrunksDataUtil.drunks[position];
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            Drunks drunks = getItem(position);
            //演示,就不写ViewHolder了
            convertView = LayoutInflater.from(MainActivity.this).inflate(R.layout.item_lst_main,null);
            TextView txtId = (TextView) convertView.findViewById(R.id.txt_id);
            txtId.setText(drunks.getId()+".");
            TextView txtName = (TextView) convertView.findViewById(R.id.txt_name);
            txtName.setText(drunks.getName()+"");
            TextView txtPrice = (TextView) convertView.findViewById(R.id.txt_price);
            txtPrice.setText(drunks.getPrice()+"");
            TextView txtNum = (TextView) convertView.findViewById(R.id.txt_num);
            txtNum.setText(drunks.getNum()+"");
            return convertView;
        }
    }


}

 

        有没有发现忘记注册Service了?

        <service android:name=".DrunkService">
            <intent-filter>
                <action android:name="com.shareye.drunkservice.DrunkService" />
            </intent-filter>
        </service>

        接下来是Client端:

        新建项目,我这里取名DrunkClient,这个随便来。

        然后把上个项目的的AIDL移植到当前新建的项目中,方法如下:

        1.打开项目源文件位置project-->app-->src-->main,这里可以看到一个aidl的文件夹;

        2.复制该aidl文件夹;

        3.类似第一步打开新建项目的main文件夹

        4.粘贴,然后AS中就可以看到aidl的包和文件了

        剩下就是通常的Android知识了,代码里做了详细注释,直接看代码就可以了。不过使用流程这里要说一下:进入后点击“开始”按钮,程序从Service端获取到饮料的列表信息,显示在下方的spinner中,点击选择某个饮料,选择的详细信息显示在spinner下面的面板,填写购买数量后点击确认,会看到详细信息中数量有所减少,打开Service端也可看到数量同样减少(这是废话)。如果将两个app都退出再打开,数据重置。

        MainActivity.class代码:

public class MainActivity extends AppCompatActivity {

    //第一次为Spinner填充数据是不触发select事件
    private boolean isFirst = true;
    //连接标识,主要是为了不重复解绑,导致崩溃
    private boolean bBinder;
    //ADIL的接口
    private IDrunkSelect iDrunkSelect;
    //Spinner的数据源,也就是信息列表的数据
    private List<String> selectList;
    //。。。。。。
    Spinner spinner;
    //。。。。。
    private Button btnStart,btnOk;
    //Spinner的自定义Adapter
    MySpinnerAdapter spinnerAdapter;
    //绑定服务时的连接器
    ServiceConnection connection = new ServiceConnection() {
        //成功连接时回调
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            iDrunkSelect = IDrunkSelect.Stub.asInterface(service);

            bBinder = true;//标示已连接
            Toast.makeText(MainActivity.this,bBinder+"",Toast.LENGTH_SHORT).show();
        }

        //连接异常时触发回调
        @Override
        public void onServiceDisconnected(ComponentName name) {
            bBinder =false;//已断开连接
        }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        btnOk = (Button) findViewById(R.id.btn_buy);
        btnOk.setClickable(false);
        btnOk.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                int position = spinner.getSelectedItemPosition();
                EditText editText = (EditText) findViewById(R.id.edt_num);
                //这里应该判断数量小于总量的,懒得写了
                int num = Integer.parseInt(editText.getText().toString());
                try {
                    iDrunkSelect.subtractNum(position,num);//沟通Service端减去被购买数量
                    getDrunkByPosition(position);//获取购买后信息
                    Toast.makeText(MainActivity.this,"Success",Toast.LENGTH_SHORT).show();
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        });
        btnStart = (Button) findViewById(R.id.btn_start);
        btnStart.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                try {
                    selectList = iDrunkSelect.getDrunkInfoList();//获取饮料信息列表
                    spinnerAdapter.setList(selectList);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        });
        //下面几个没啥好说的
        spinner = (Spinner) findViewById(R.id.spn_select);
        selectList = new ArrayList<>();
        spinnerAdapter = new MySpinnerAdapter(selectList,this);
        spinner.setAdapter(spinnerAdapter);
        spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
            @Override
            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
                if (isFirst) {//防护机制
                    isFirst = false;
                    return;
                }
                getDrunkByPosition(position);
            }

            @Override
            public void onNothingSelected(AdapterView<?> parent) {

            }
        });

    }

    /**
     * 通过AIDL获取选中项的完整信息
     * @param position 下标
     */
    private void getDrunkByPosition(int position) {
        try {
            Drunks drunks = iDrunkSelect.getDrunk(position);
            showDrunks(drunks);
            btnOk.setClickable(true);
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    /**
     * 显示某个选中项的全部信息
     * @param drunks
     */
    private void showDrunks(Drunks drunks) {
        TextView txtId = (TextView) findViewById(R.id.txt_id);
        txtId.setText("编号:"+drunks.getId());
        TextView txtName = (TextView) findViewById(R.id.txt_name);
        txtName.setText("品名:"+drunks.getName());
        TextView txtPrice = (TextView) findViewById(R.id.txt_price);
        txtPrice.setText("单价:"+drunks.getPrice()+"");
        TextView txtNum = (TextView) findViewById(R.id.txt_number);
        txtNum.setText("数量:"+drunks.getNum());
    }

    @Override
    protected void onStart() {
        super.onStart();
        //绑定服务,开始AIDL
        Intent intent = new Intent("com.shareye.drunkservice.test.DrunkService");
        bindService(getExplicitIntent(this,intent), connection, BIND_AUTO_CREATE);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (bBinder)//解绑
            unbindService(connection);
        bBinder = false;
    }
    /**
     * 5.0不允许隐式启动,将隐式Intent转成显式。
     */
    public Intent getExplicitIntent(Context context, Intent implicitIntent) {
        // 检索所有能够响应该Intent的服务类
        PackageManager pm = context.getPackageManager();
        List<ResolveInfo> resolveInfo = pm.queryIntentServices(implicitIntent, 0);
        // 如果查询结果不唯一,退出
        if (resolveInfo == null || resolveInfo.size() != 1) {
            return null;
        }
        // 获取组件的信息,然后创建对应的ComponentName对象
        ResolveInfo serviceInfo = resolveInfo.get(0);
        String packageName = serviceInfo.serviceInfo.packageName;
        String className = serviceInfo.serviceInfo.name;
        ComponentName component = new ComponentName(packageName, className);
        // 重用旧Intent对象的信息来新建一个Intent
        Intent explicitIntent = new Intent(implicitIntent);
        // 将前面得到的CompontName加入新intent
        explicitIntent.setComponent(component);
        return explicitIntent;
    }

    /**
     * 自定义Adapter,除了没做holder优化没啥可说的
     */
    private class MySpinnerAdapter extends BaseAdapter{

        private List<String> list;
        private Context context;

        public MySpinnerAdapter(List<String> list, Context context) {
            this.list = list;
            this.context = context;
        }

        @Override
        public int getCount() {
            return list.size();
        }

        @Override
        public String getItem(int position) {
            return list.get(position);
        }

        @Override
        public long getItemId(int position) {
            return position;
        }
        public void setList(List<String> list){
            this.list = list;
            notifyDataSetChanged();
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            convertView = LayoutInflater.from(context).inflate(R.layout.item_spn_drunk,null);
            TextView name = (TextView) convertView.findViewById(R.id.txt_describe);
            name.setText(getItem(position));
            return convertView;
        }
    }
}

完整Demo下载地址


        上面的整个来说,其实是一个固定流程,把握套路后就简单了很多,根据个人理解和认识,画了下面一张图,希望有助于各位理解(图4)。

图4


 

        这里基于源码说名几个问题:

        1.为什么DrunkSelectImp继承IDrunkSelect.Stub而不是继承IDrunkSelect

        这里先看一个Stub的声明截图(图5):

图5

        可以看到Stub是继承了IDrunkSelect的,也是DrunkSelectImp间接继承了IDrunkSelect,但是它这里又继承了BInder,这为什么呢?

打个比喻,两家分公司寄东西,自己不方便送就得找快递公司吧,这里的Binder就是类似“快递公司”的作用。先打上BInder的标记运输,再转回公司的模板使用。

        2.按照1的说法,我们定义两个内容相同名字相同的接口不就可以使用了?我使用的到底是谁?

        如果两边不统一,会报异常,具体原因我没找到,下面是查找原因时的可能地方,供参考一下。

        Stub的第一行代码是一个finalString常量,该常量根据包名+类名来自动命名(图6)。

图6

        这是一个身份标识,它的作用就是检查是否互相匹配。而且可以看到我的截图多结了一个构造器,构造器内的方法直接是调用了一个父类的方法。这里接下来会用到。

        接下来看一下(图7)我们如何从“快递”手里变回“分公司”自己用的

图7

        这段代码分析一下,我们最需要关心的是iin的那一个判断。点进去看一下iin的获取过程,也就是BInder类中的queryLocalInterface(String str)方法(图8):

图8

        mDescriptor又是谁?哪里来的?Stub的构造器所调用函数(Binder的)对比一下,就是它(图9)!

图9

        跨进程传过来的IBunder对象携带的信息,在和当下完整路径做匹配会返回一个null;只有本进程的调用才会在这里拿到inn非空对象。

        由于这里iin为空,我们实际使用的是一个代理Proxy类。



        完事!吐槽一下,这特么大概是我到目前为止写的篇幅最长的一篇文档了,真累!








评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

公贵买其鹿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值