AIDL 开发实战踩坑记录

笔者在前段时间接到一位富婆姐姐的客户需求,需要在我司指定 Android 设备(上位机)上通过 USB 和蓝牙方式连接到一台小型打印设备(下位机)实现打印、显示等一系列功能。经过前期调研和可行性分析,以及该项目的特殊功能需求,研发内部讨论采用 C/S 架构来完成这位姐姐的需求。

最终软件设计方案确定为在系统内置一个连接服务,客户端绑定到连接服务后调用服务提供的接口来发送相应的串口或蓝牙指令,服务端接收后将指令处理结果回传给客户端,从而实现上位机和下位机的功能交互。这个方案确定,也使得我有了一次将 AIDL应用于项目实战当中的机会。

时隔半个月,该方案基本落地,空闲之余,将整个开发过程中遇到的几个问题做一次复盘和整理,加深自己对 AIDL 理解的同时也希望对各位读者能所有启发。

关于 AIDL

AIDL,全称Android Interface Definition Language,即Android接口定义语言,是Android提供的用于实现进程间通信的一种机制。通过AIDL,开发者可以在不同的应用程序之间或同一应用的不同进程中定义接口,以进行数据交换和方法调用。本篇内容不对 AIDL 进行详细阐述,若读者对 AIDL 基础内容感兴趣可通过下方引用内容进一步了解和学习。

关于 AIDL 的介绍,可查看开发者官网:AIDL简介

关于 AIDL 的实践内容,可参考该文章: Android:AIDL实战详解

踩坑记录

1. 数组长度不匹配

报错信息:System.err: java.lang.RuntimeException: bad array lengths

客户端

IDataService.aidl

interface IDataService {
    ICmdResp sendData(in byte[] data, inout ICmdResp iCmdResp);
}

ICmdResp.aidl

public class ICmdResp implements Parcelable {
    private static final String TAG = "ICmdResp";
    private byte resultCode;

    private Object data;

    private byte[] orgData = null;

    public ICmdResp() {}

    public ICmdResp(Parcel in) {
        readFromParcel(in);
    }

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

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

    public byte getResultCode() {
        return resultCode;
    }

    public void setResultCode(byte resultCode) {
        this.resultCode = resultCode;
    }
    
    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public byte[] getOrgData() {
        return orgData;
    }

    public void setOrgData(byte[] orgData) {
        this.orgData = orgData;
    }
    
    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeByte(resultCode);
        dest.writeValue(data);
        if (orgData == null) {
            dest.writeInt(0);
        } else {
            dest.writeInt(orgData.length);
            dest.writeByteArray(orgData);
        }
    }

    public void readFromParcel(Parcel in) {
        resultCode = in.readByte();
        data = in.readValue(Object.class.getClassLoader());
        int originLen = in.readInt();
        if (originLen > 0) {
            byte[] origin = new byte[originLen];
            in.readByteArray(origin);
            orgData = origin;
        } else {
            orgData = null;
        }
    }
}

服务端

DataServiceBinder.java

public class DataServiceBinder extends IDataService.Stub {
        @Override
        public ICmdResp sendData(byte[] data, ICmdResp iCmdResp) throws RemoteException {
            if (data == null) {
                LogUtil.d(TAG, "sendData is null");
            } else {
                LogUtil.d(TAG, "sendData: " + StringUtil.hexString(data));
            }
            CmdResp cmdResp = new CmdResp();
            cmdResp.send(500, data, cmdResp);

            iCmdResp.setResultCode(cmdResp.getResultCode());
            iCmdResp.setData(cmdResp.getData());
            iCmdResp.setOrgData(cmdResp.getOrgData());

            return iCmdResp;
        }
}

刚开始排查思路是检查服务端数组反序列化的实现细节,经过多次调整之后验证无效。仔细一想是不是在服务端的问题,检查代码发现服务端回传数据时并未对数组进行判空,与客户端的反序列化实现存在逻辑差异,顺着这个思路修改服务端代码:

public class DataServiceBinder extends IDataService.Stub {
        @Override
        public ICmdResp sendData(byte[] data, ICmdResp iCmdResp) throws RemoteException {
            if (data == null) {
                LogUtil.d(TAG, "sendData is null");
            } else {
                LogUtil.d(TAG, "sendData: " + StringUtil.hexString(data));
            }
            CmdResp cmdResp = new CmdResp();
            cmdResp.send(500, data, cmdResp);

            iCmdResp.setResultCode(cmdResp.getResultCode());
            iCmdResp.setData(cmdResp.getData());
            //添加数组判断空
            if (cmdResp.getOrgData() != null) {
                iCmdResp.setOrgData(cmdResp.getOrgData());
            }

            return iCmdResp;
        }
}

编译验证,一次性通过。从中可以看到,在IPC通信过程中,对象的序列化和反序列必须保持一致性,即对象每个字段的序列化顺序一致。对于 AIDL,序列化操作存在于客户端和服务端,因此需要保证客户端和服务端在序列化对象时的正确实现。

2. AIDL 注册回调到服务端对象为空

客户端

IStateCallback.aidl

interface IStateCallback {

    void onSuccess();

    void onFailed();

    void onTimeout();

    void onDisable();
}

IDataService.aidl

interface IDataService {
    void addStateCallback(in IStateCallback callback);
}

DataServiceManager.java

public class DataServiceManager {
    private static DeviceConnManager instance;
    private IDataService mDataService;

    public static DataServiceManager getInstance() {
        if (null == instance) {
            synchronized (DataServiceManager.class) {
                instance = new DataServiceManager();
            }
        }
        return instance;
    }

    private final ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName componentName, IBinder service) {
            mDataService = IDataService.Stub.asInterface(service);
            Log.i(TAG, "onServiceConnected  :  " + mDataService);
        }

        @Override
        public void onServiceDisconnected(ComponentName componentName) {
            Log.i(TAG, "onServiceDisconnected  : " + mDataService);
            mDataService = null;
        }
    };

    public boolean bindDataService(Context context) {
        Log.i(TAG, "bindDeviceService");
        Intent intent = new Intent();
        intent.setAction(DATE_SERVICE_NAME);
        intent.setClassName(DATE_SERVICE_PACKAGE_NAME, DATE_SERVICE_CLASS_NAME);

        try {
            boolean bindResult = context.bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
            Log.i(TAG, "bindResult = " + bindResult);
            return bindResult;
        } catch (Exception exception) {
            throw exception;
        }
    }

    public void addStateCallback(IStateCallback callback) {
        try {
            if (mDataService != null) {
                mDataService.addStateCallback(callback);
            }
        } catch (RemoteException e) {
            throw new RuntimeException(e);
        }
    }
}

调用:

DataServiceManager dataServiceManager = DataServiceManager.getInstance();
dataServiceManager.bindDataService(this);
dataServiceManager.addStateCallback(new IStateCallback() {
    @Override
    public void onSuccess() throws RemoteException {
        Log.i(TAG, "onSuccess");
    }

    @Override
    public void onFailed() throws RemoteException {
        Log.i(TAG, "onFailed");
    }

    @Override
    public void onTimeout() throws RemoteException {
        Log.i(TAG, "onTimeout");
    }

    @Override
    public void onDisable() throws RemoteException {
        Log.i(TAG, "onDisable");
    }

    @Override
    public IBinder asBinder() {
        return null;
    }
});

服务端

DataServiceBinder.java

public class DataServiceBinder extends IDataService.Stub {
    private ConcurrentHashMap<Integer,IStateCallback> mStateCallbacks;

    @Override
    public void addStateCallback(IStateCallback callback) throws RemoteException {
        if (mStateCallbacks == null) {
            mStateCallbacks = new ConcurrentHashMap<>();
        }
        mStateCallbacks.values().
                removeIf(filter -> filter == null);

        if (callback != null) {
            mStateCallbacks.put(callback.hashCode(), callback);
        }
    }
}

上面代码根据验证结果是:客户端调用服务端接口成功,但是服务端接收到的 IStateCallback 对象实例为 null 。根据经验应该是客户端处理 IStateCallback 没有正确处理,因此我检查了一下addStateCallback 接口传入的对象,发现自己传入的对象是 IStateCallback 实例。

dataServiceManager.addStateCallback(new IStateCallback() {...});

这里我并未确定我的判断,因此利用搜索引擎找了相关问题:解决AIDL客户端向服务端注册回调,服务端收到的回调为空的问题

根据搜索引擎提供参考结果,这里接口实现确实存在问题,查看 AIDL 编译的源码我们可以看到问题答案:

public interface IStateCallback extends android.os.IInterface {
    ...
    public static abstract class Stub extends android.os.Binder implements com.xxxlib.xxx.IStateCallback
    ...
}

IStateCallback 继承 IInterface,本身并不具备跨进程通信能力,真正完成跨进程通信的是其内部的 Stub 子类,Stub 继承 Binder 并重写 asBinder 和 onTransact 方法用于处理客户端的远程调用请求。因此将接口实现修改为:

dataServiceManager.addStateCallback(new IStateCallback.Stub() {...});

于是编译再次验证,发现并依然为空。再次疑惑,于是我再次检查了代码,根据源码实现和搜索引擎提供的结果,处理思路应该是没错的。于是我再次翻了一下搜索引擎的结果,找到一些开发者提到重写 asBinder 问题。于是我找了一些文章:Binder的stub和proxy解析及asBinder,结合 AIDL 编译出的源码,确定原因是自己重写了 asBinder 接口。最终修改如下:

DataServiceManager dataServiceManager = DataServiceManager.getInstance();
dataServiceManager.bindDataService(this);
dataServiceManager.addStateCallback(new IStateCallback.Stub() {
    @Override
    public void onSuccess() throws RemoteException {
        Log.i(TAG, "onSuccess");
    }

    @Override
    public void onFailed() throws RemoteException {
        Log.i(TAG, "onFailed");
    }

    @Override
    public void onTimeout() throws RemoteException {
        Log.i(TAG, "onTimeout");
    }

    @Override
    public void onDisable() throws RemoteException {
        Log.i(TAG, "onDisable");
    }

    @Override
    public IBinder asBinder() {
        return super.asBinder();
    }
});

再次编译验证,完美通过,继续干。

3. 更新接口后客户端未更新出现OOM问题

AndroidRuntime: java.lang.OutOfMemoryError: Failed to allocate a 1700200496 byte allocation with 2288853 free bytes and 509MB until OOM, target footprint 4577709, growth limit 536870912

由于接口设计时是根据功能模块来进行划分,客户端和服务端同时进行实现,前期主要把握整体功能实现,初始化接口设计并未考虑非常全面。因此在后期完善功能时添加了一些必要接口。

客户端

IDataService.aidl

interface IDataService {
    //新增接口
    void addStateCallback(in IStateCallback callback);

    ICmdResp sendData(in byte[] data, inout ICmdResp iCmdResp);
    
    ICmdResp send(in byte[] data);
}

新增接口后,服务端验证成功 IDataService 三个接口,但安装客户端验证是,出现 OOM 问题

java.lang.OutOfMemoryError: Failed to allocate a 1700200496 byte allocation with 2269974 free bytes and 509MB until OOM, target footprint 4539950, growth limit 536870912

顿感莫名其妙,抓 LOG 分析,发现控制台报 sendData 接口的 ICmdResp 在序列化过程中出了问题。我反复确认了 ICmdResp 的序列化和反序列实现,并未看出异常,且 sendData 和 send 接口在此前是验证正常的。正当我百思不得其解时,我突然想起客户端的测试用例是调用的 send 方法,传输的是一张图片数据,但是控制台却报的是 sendData 方法。这时才反应过程,客户端并未更新 aidl 接口。一查看 IDataService.aidl 编译的源码,原来方法的 IDataService.Stub 中的 onTransact 通过方法 ID 来标识里边的方法实现,而 ID 的生成方式为:

int METHOD_ID = android.os.IBinder.FIRST_CALL_TRANSACTION + METHOD_NUM;

在添加方法后,原本 send 方法的 METHOD_ID 后移了一位,导致在客户端未更新接口的情况下调用 send 方法,实际上是调用了服务端的 sendData 方法。而 ICmdResp 是 parcelable 对象,在跨进程通信时反序列化需要大量内存创建数组以保存数据,正是这个原因导致 OOM 问题出现。而解决方案则很简单,更新客户端的 AIDL 接口即可。

总结

在完成这个项目的后,我才对 AIDL 机制有了一个清晰的认识,其本质上是 Android 针对跨进程通信做的一个简易模板,简化了开发者完成跨进程通信代码实现。从上述三个问题我们可以看到,跨进程通信中序列化和反序列化机制中一致性的重要性,简要归纳为以下两个方面:

  1. parcelable 对象的数据格式一致性;
  2. 客户端和服务端的接口一致性。

以上,是笔者对 AIDL 实践过程的复盘和总结,希望对各位读者有所启发。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

知识焦虑症患者

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

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

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

打赏作者

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

抵扣说明:

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

余额充值