AIDL踩坑实战

AIDL理论

AIDL(Android 接口定义语言)可以利用它定义客户端与服务使用进程间通信 (IPC) 进行相互通信时都认可的编程接口。 在 Android 上,一个进程通常无法访问另一个进程的内存。 尽管如此,进程需要将其对象分解成操作系统能够识别的原语,并将对象编组成跨越边界的对象。 编写执行这一编组操作的代码是一项繁琐的工作,因此 Android 会使用 AIDL 来处理。

如需使用 AIDL 创建绑定服务,请执行以下步骤:

  1. 创建 .aidl 文件
    此文件定义带有方法签名的编程接口。
    2.实现接口
    Android SDK 工具基于您的 .aidl 文件,使用 Java 编程语言生成一个接口。此接口具有一个名为 Stub 的内部抽象类,用于扩展 Binder 类并实现 AIDL 接口中的方法。您必须扩展 Stub 类并实现方法。
    3.向客户端公开该接口
    实现 Service 并重写 onBind() 以返回 Stub 类的实现。

创建 .aidl 文件

AIDL 使用简单语法,使您能通过可带参数和返回值的一个或多个方法来声明接口。 参数和返回值可以是任意类型,甚至可以是其他 AIDL 生成的接口。
必须使用 Java 编程语言构建 .aidl 文件。每个 .aidl 文件都必须定义单个接口,并且只需包含接口声明和方法签名。
默认情况下,AIDL 支持下列数据类型:

  • Java 编程语言中的所有原语类型(如 int、long、char、boolean 等等)
  • String
  • CharSequence
  • List
    List 中的所有元素都必须是以上列表中支持的数据类型、其他 AIDL 生成的接口或您声明的可打包类型。 可选择将 List 用作“通用”类(例如,List<String>)。另一端实际接收的具体类始终是 ArrayList,但生成的方法使用的是 List 接口。
  • Map
    Map 中的所有元素都必须是以上列表中支持的数据类型、其他 AIDL 生成的接口或您声明的可打包类型。 不支持通用 Map(如 Map<String,Integer> 形式的 Map)。 另一端实际接收的具体类始终是 HashMap,但生成的方法使用的是 Map 接口。

必须为以上未列出的每个附加类型加入一个 import 语句,即使这些类型是在与您的接口相同的软件包中定义。

定义服务接口时,需要注意:

  • 方法可带零个或多个参数,返回值或空值。
  • 所有非原语参数都需要指示数据走向的方向标记。可以是 in、out 或 inout。原语默认为 in,不能是其他方向。in 表示由客户端设置,修饰输入参数,非基本类型的输入参数必须使用in修饰。out 表示由服务器端设置,修饰输出参数,非基本类型的输出参数必须使用out修饰。inout 表示既是输入参数,也是输出参数。应该将方向限定为真正需要的方向,因为编组参数的开销极大。
  • .aidl 文件中包括的所有代码注释都包含在生成的 IBinder 接口中(import 和 package 语句之前的注释除外)
  • 只支持方法;不能公开 AIDL 中的静态字段。
// IRemoteService.aidl
package com.example.android;

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

/** Example service interface */
interface IRemoteService {
    /** Request the process ID of this service, to do evil things with it. */
    int getPid();

    /** Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);
}

实现接口

android sdk会生成一个以.aidl文件命名的.java接口文件,生成的接口包括一个Stub子类,用于声明.aidl中的所有方法。
以下是一个使用匿名实例实现名为 IRemoteService 的接口(由以上 IRemoteService.aidl 示例定义)的示例:

private final IRemoteService.Stub mBinder = new IRemoteService.Stub() {
    public int getPid(){
        return Process.myPid();
    }
    public void basicTypes(int anInt, long aLong, boolean aBoolean,
        float aFloat, double aDouble, String aString) {
        // Does nothing
    }
};

现在,mBinder 是 Stub 类的一个实例(一个 Binder),用于定义服务的 RPC 接口。 在下一步中,将向客户端公开该实例,以便客户端能与服务进行交互。

在实现 AIDL 接口时应注意遵守以下这几个规则:

  • 由于不能保证在主线程上执行传入调用,因此您一开始就需要做好多线程处理准备,并将您的服务正确地编译为线程安全服务。
  • 默认情况下,RPC 调用是同步调用。如果您明知服务完成请求的时间不止几毫秒,就不应该从 Activity 的主线程调用服务,因为这样做可能会使应用挂起(Android 可能会显示“Application is Not Responding”对话框)— 您通常应该从客户端内的单独线程调用服务。
  • 您引发的任何异常都不会回传给调用方。

向客户端公开该接口

您为服务实现该接口后,就需要向客户端公开该接口,以便客户端进行绑定。 要为您的服务公开该接口,请扩展 Service 并实现 onBind(),以返回一个类实例,这个类实现了生成的 Stub(见前文所述)。以下是一个向客户端公开 IRemoteService 示例接口的服务示例。

public class RemoteService extends Service {
    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public IBinder onBind(Intent intent) {
        // Return the interface
        return mBinder;
    }

    private final IRemoteService.Stub mBinder = new IRemoteService.Stub() {
        public int getPid(){
            return Process.myPid();
        }
        public void basicTypes(int anInt, long aLong, boolean aBoolean,
            float aFloat, double aDouble, String aString) {
            // Does nothing
        }
    };
}

现在,当客户端(如 Activity)调用 bindService() 以连接此服务时,客户端的 onServiceConnected() 回调会接收服务的 onBind() 方法返回的 mBinder 实例。

客户端还必须具有对 interface 类的访问权限,因此如果客户端和服务在不同的应用内,则客户端的应用 src/ 目录内必须包含 .aidl 文件(它生成 android.os.Binder 接口 -- 为客户端提供对 AIDL 方法的访问权限)的副本。

当客户端在 onServiceConnected() 回调中收到 IBinder 时,它必须调用 YourServiceInterface.Stub.asInterface(service) 以将返回的参数转换成 YourServiceInterface 类型。例如:

IRemoteService mIRemoteService;
private ServiceConnection mConnection = new ServiceConnection() {
    // Called when the connection with the service is established
    public void onServiceConnected(ComponentName className, IBinder service) {
        // Following the example above for an AIDL interface,
        // this gets an instance of the IRemoteInterface, which we can use to call on the service
        mIRemoteService = IRemoteService.Stub.asInterface(service);
    }

    // Called when the connection with the service disconnects unexpectedly
    public void onServiceDisconnected(ComponentName className) {
        Log.e(TAG, "Service has unexpectedly disconnected");
        mIRemoteService = null;
    }
};

以上内容摘自https://developer.android.com/guide/components/aidl#CreateAidl

动手实战

说实话,这些理论我是没有看明白,目前我只希望知道要如何使用,就算完成任务了(最好还是要理解了)。

服务端

首先编写服务端代码,新建一个项目,我的叫做AIDLServer。创建过程如下:

创建项目.png

选择使用的sdk版本.png
选择创建模版.png
主Activity名称.png

全部完成后系统就会自动创建一个工程,此时已经埋下了第一个坑。创建完成后的项目如图所示。


项目完成的目录结构.png

接下来就来创建aidl文件,创建一个名为student的aidl文件(当年学数据库的时候,就是拿各种学生表,课程表来举例,找回上学的感觉),创建过程如下:

1.两个手指点击触控板,弹出并作如下选择


创建AIDL文件

2.输入AIDL文件名称,这里输入Student


输入AIDL文件名

3.系统自动创建AIDL文件
系统自动创建的AIDL文件

4.创建Student类,用来存储姓名和年龄,使用插件(android parcelable code generator ,可以在studio中直接下载,下载完成后在mac中的使用方法是按com+N,选择Parcelable即可)自动实现Parcelable接口,手动添加如下代码

    /**
     * <p>从parcel中读取,顺序与write一致</p>
     * 如果要支持为 out 或者 inout 的定向 tag 的话,需要实现 readFromParcel() 方法
     * @param dest
     */
    public void readFromParcel(Parcel dest) {
        name = dest.readString();
        age = dest.readInt();
    }

此时,Student.java类完成。完整代码如下:

package com.tom.aidlserver;

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

/**
 * <p>Title: Student</p>
 * <p>Description: </p>
 *
 * @author tom
 * @date 2018/11/20 16:31
 **/
public class Student implements Parcelable {
    private String name;
    private int age;

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

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

    /**
     * <p>从parcel中读取,从parcel中读取,顺序与write一致</p>
     * 如果要支持为 out 或者 inout 的定向 tag 的话,需要实现 readFromParcel() 方法
     * @param dest
     */
    public void readFromParcel(Parcel dest) {
        name = dest.readString();
        age = dest.readInt();
    }

    public Student() {
    }

    protected Student(Parcel in) {
        this.name = in.readString();
        this.age = in.readInt();
    }

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

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

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}


5.回到Student.aidl,作出如下修改

//引入一个序列化对象Student供其他AIDL文件使用
//Student.aidl 与Student的包名应该要相同
package com.tom.aidlserver;
//parcelable 要小写
parcelable Student;

6.按照相同的套路,创建StudentManager.aidl文件,并修改如下:

// StudentManager.aidl
package com.tom.aidlserver;

// Declare any non-default types here with import statements
import com.tom.aidlserver.Student;

interface StudentManager {

    //获取学生总数
    List<Student> getStudents();
    //添加学生
    void addStudent(in Student student);

}

7.创建服务端代码AIDLService,代码如下:

package com.tom.aidlserver;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.Log;

import java.util.ArrayList;
import java.util.List;

public class AIDLService extends Service {

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

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

    //由AIDL文件生成的StudentManager.Stub
    private final StudentManager.Stub mStudentManager = new StudentManager.Stub() {
        @Override
        public List<Student> getStudents() throws RemoteException {
            synchronized (this) {
                if (mStudentList != null) {
                    return mStudentList;
                }
            }
            return new ArrayList<>();
        }

        @Override
        public void addStudent(Student student) throws RemoteException {
            synchronized (this) {
                if (mStudentList == null) {
                    mStudentList = new ArrayList<>();
                }
                if (student == null) {
                    Log.d(TAG, "student is null");
                    student = new Student();
                }

                //如果名字为Tom,修改年龄为18
                if (TextUtils.equals(student.getName().toString().trim(), "Tom")) {
                    student.setAge(18);
                }

                //假设学生不能重名
                if (!mStudentList.contains(student)) {
                    mStudentList.add(student);
                }

                Log.d(TAG, "Student: " + student.toString());

            }

        }
    };

    @Override
    public void onCreate() {
        super.onCreate();
        Student student = new Student();
        student.setName("小明");
        student.setAge(10);
        mStudentList.add(student);
    }

    @Override
    public IBinder onBind(Intent intent) {
        Log.e(TAG, String.format("on bind,intent = %s", intent.toString()));
        return mStudentManager;
    }
}

代码部分就写完了,还需要在AndroidManifest.xml文件中添加如下内容才算完成。

        <service
            android:name=".AIDLService"
            android:enabled="true"
            android:exported="true">
            <intent-filter >
                <action android:name="com.tom.aidlserver.ACTION_AIDL_SERVICE"/>
                <category android:name="android.intent.category.DEFAULT"/>
            </intent-filter>
        </service>

这样,这个service就可以供其他应用使用了。给其他应用使用,我选择的是提供jar 包给第三方,这里涉及到把服务端代码打包成jar包的情况,此时,就是解决第一个坑的时候了。
1.打开build.gradle(Module:app),第一行修改为:

apply plugin: 'com.android.library'

2.把applicationId这一行注释掉。此时的文件如图所示

apply plugin: 'com.android.library'

android {
    compileSdkVersion 28
    defaultConfig {
//        applicationId "com.tom.aidlserver"
        minSdkVersion 26
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }




}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

3.编译aar包给第三方应用使用。
点击右边的Gradle,按图所示,双击,在outputs下的aar包,就是我们需要的aar包。

生成aar包

生成的aar包

如果生成过程有错误,可以尝试把整个build文件夹删掉,再来执行。
至此,整个服务端的代码都ok了,把aar包复制出来,然后把第1,2步的修改改回去,安装到机器上,有一点需要注意的是,执行配置哪里选择app,如图:
修改执行配置为app

客户端

客户端就比较简单了,直接创建就可以了,我的项目名字叫做AIDLClient
客户端输出日志:

12-31 19:43:53.259 7236-7236/com.tom.aidlclient E/MainActivity: Student{name='Tom', age=32}

服务端输出日志:

12-31 19:43:53.258 6215-6233/com.tom.aidlserver D/AIDLService: Student: Student{name='Tom', age=18}

从日志可以看到,已经将年龄修改为18了。
客户端的代码比较简单,新建一个libs包,把aar包扔进去,在gradle文件添加一行,我将导出的aar改名了,这里的名字和libs里面的对应就好了。

implementation(name: 'aidlserver', ext: 'aar')

给出MainActivity的完整代码

package com.tom.aidlclient;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Toast;

import com.tom.aidlserver.Student;
import com.tom.aidlserver.StudentManager;

import java.util.List;

public class MainActivity extends AppCompatActivity {

    //由aidl生成的java类
    private StudentManager mStudentManager = null;

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

    //包含Student对象的list
    private List<Student> mStudentList;

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


    public void addStudent(View view) {
        if (!mBound) {
            attemptToBindService();
            Toast.makeText(this, "当前与服务端处于未连接状态,正在尝试重连,请稍后再试", Toast.LENGTH_SHORT).show();
            return;
        }
        if (mStudentManager == null) return;

        Student student = new Student();
        student.setName("Tom");
        student.setAge(32);
        try {
            mStudentManager.addStudent(student);
            Log.e(getLocalClassName(), student.toString());
        } catch (RemoteException e) {
            e.printStackTrace();
        }

    }



    @Override
    protected void onStart() {
        super.onStart();
        if (!mBound) {
            attemptToBindService();
        }
    }

    @Override
    protected void onStop() {
        super.onStop();
        if (mBound) {
            unbindService(mServiceConnection);
            mBound = false;
        }
    }

    private void attemptToBindService() {
        Intent intent = new Intent();
        intent.setAction("com.tom.aidlserver.ACTION_AIDL_SERVICE");
        intent.setPackage("com.tom.aidlserver");
        bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);

    }


    private ServiceConnection mServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mStudentManager = StudentManager.Stub.asInterface(service);
            mBound = true;

            if (mStudentManager != null) {
                try {
                    mStudentList = mStudentManager.getStudents();
                    Log.d(getLocalClassName(),"获取到学生数据: " + mStudentList.toString());
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
                mBound = false;
        }
    };
}

布局文件activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="添加学生"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:onClick="addStudent"/>

</android.support.constraint.ConstraintLayout>

整体就是这样了,自己动手搞一波,会用,理解的话我再慢慢来学习把。

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

推荐阅读更多精彩内容