进程间通信:bindService的替换方案

本文介绍了一种利用ContentProvider的call方法实现跨进程同步调用的方法,避免了使用Service时bindService的异步问题,提供了简洁高效的进程间通信解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

随着项目的业务和复杂度的增大,对内存的压力越来越明显,有时不得不使用多进程的方案,将一些功能放到另一个进程中去完成。其实很多时候,简单的业务也需要开一个单独的进程,如音乐播放器。

我们就以音乐播放器为例,播放音乐的实现功能部份我们往往放在Service中去做,并把这个Service运行在一个单独的进程中(通过设置android:process属性)。这样做的好处是,音乐播放器的UI部份在一个进程,可以退出释放内存。而Service部份所在的进程没有UI资源,占用的内存也较少。

麻烦一点的就是,每次UI进程打开Activity界面时需要使用bindService来绑定Service,才能和它进行通讯,如读取现在的播放进度,控制暂停快进等功能。

其实在音乐播放器这个场景中,Service这个方案是很合理的。但有些时候,我们需要调用一个远程进程中的功能,但是想在一个同步方法中调用,而且不能排除用户是在主线程调用。这种情况如果用Service的方案来实现,那么,我们只能通过bindService去绑定Service获取它的服务接口(Binder实例),但问题出在这个bindService是异步方法,调用后要在它的回调方法ServiceConnection中才能获取到它的服务接口。

如果我们想同步调用一个远程进程中提供的方法,应该怎么办呢?

我有想过像系统提供的服务一样,如ActivityManagerService,在系统服务注册,然后提供给Client端同步调用。

后来一想,这种方案大复杂了。很多时候我们单独进程中跑的功能并不复杂,有点杀鸡用牛刀的感觉。然后就是查找一些资料,没查到什么好的方案,我就去看一下ContentProvider的源码,想看看它是怎么实现,其实他和我的需求很像,可以进程间访问,每个方法都是同步的,需要时才启动,就是方法要封装成insert/delete/query这样的。后来一看,ContentProvider有一个call方法,完全可以实现我的需求。

    @Override
    public Bundle call(String method, String arg, Bundle extras) {
        if (extras == null) {
            return null;
        }
        extras.setClassLoader(GoeasywayContentProvider.class.getClassLoader());
        CallArgs callBundleArgs = extras.getParcelable(METHOD_CALL_ARGS);
        ......

注意代码中的extras.setClassLoader,如果我们需要用extras传递自定义的Pacelable对象,那么这里要指定一下它的ClassLoader,不然这里会无法解封CallArgs(自定义的Pacelable对象)。

可以用method区分每个方法,参数可以放到Bundle中。返回值也是一个Bundle对象,可以在返回值也传递一个自己定义的Pacelable对象。我把参数和返回值放在一个Pacelable中:

public class CallArgs implements Parcelable {

    public String method;
    public Object[] methodArgs;
    public Object result;

    public CallArgs() {

    }

    protected CallArgs(Parcel in) {
        readFromParcel(in);
    }

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

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

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

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(method);
        dest.writeArray(methodArgs);
        dest.writeValue(result);
    }

    public void readFromParcel(Parcel in) {
        method = in.readString();
        methodArgs = in.readArray(CallArgs.class.getClassLoader());
        result = in.readValue(CallArgs.class.getClassLoader());
    }
}

访问就是通过getContentResolver().call这个方法,我们简单的封装了一下:

    private CallArgs callRemoteMethod(String method, Object... args) {
        CallArgs CallArgs = new CallArgs();
        CallArgs.method = method;
        CallArgs.methodArgs =  args;
        android.os.Bundle bundleArgs = new android.os.Bundle();
        bundleArgs.putParcelable(METHOD_CALL_ARGS, CallArgs);
        android.os.Bundle bundleResult = hostContext.getContentResolver().call(
            CALL_URI, // 一个Uri,和使用ContentProvider的query方法一样的
            method, null, bundleArgs);

        if (bundleResult == null) {
            return null;
        }
        bundleResult.setClassLoader(CallArgs.class.getClassLoader());
        CallArgs result = bundleResult.getParcelable(METHOD_CALL_RESULT);
        return result;
    }

这个使用起来就像一个简单的API调用,我们用很少的代码就实现了一个进程间的同步调用,不用但心远程进程已经被系统KILL掉,Android系统会帮我们重新创一个进程并继续之前的调用。

这里要注意:

  1. 在进程间传递的对象都是现实Parcelable接口,即CallArgs方法中的参数(methodArgs)和返回值(result)都应该实现Parcelable接口。
  2. 进程间通信是通过Binder机制进行的,每个调用线程都会被block,不管它是主线程还是子线程,直到Binder的服务端(本例中是ContentProvider)的binder线程执行完成返回给调用端时,线程才会继续运行。所以如果ContentProvider中的操作较耗时,最好是用子线程去调用。
  3. 每个Binder线程能传输的数据是有大小受Binder缓冲区的大小限制的,一般一个线程最大能占用128KB。超过这个限制会报TransactionTooLargeException异常。


文/goeasyway(简书作者)
原文链接:http://www.jianshu.com/p/d30e333d6e2e
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。
Diboot是一个面向开发人员的低代码开发平台,将重复性的工作自动化,提高质量、效率、可维护性。 diboot主要有diboot-core和diboot-devtools组成,diboot-core又为springboot专门封装了diboot-core-spring-boot-starter组件,diboot-devtools也以starter提供。 diboot特点: 1、专门为springboot开发,引入依赖,配置好相对路径即可使用; 2、并且支持较为常用的五种数据库(MySQL,ORACLE,SQLServer,PostgreSQL,MariaDB); 3、代码自动生成到项目相应的目录下; 4、数据库表结构更改后,可以同步更改对应文件的代码,而不影响到你自己写的代码; 5、基础框架中依靠注解实现的数据关联(一对一、一对多、多对多、数据字典关联等),将关联问题简单化了; 6、对数据关联设置简单,代码生成与更新也可以自动支持一对一、一对多、多对多、数据字典关联; 7、支持开发以及维护过程中对数据库相关表的数据结构更改进行记录,并写入到对应的数据库更改日志文件中; 8、支持启用lombok的代码形式; 9、支持启用swagger的项目; 10、依赖少、简便轻量、灵活性高。   Diboot轻代码开发平台 更新日志: v2.2.1 新增: @BindQuery注解新增strategy参数,支持空值处理策略(默认忽略空字符串) 基于Spring的内存缓存实现(BaseCacheManager),并优化绑定缓存实现 SqlFileInitializer新增executeMultipleUpdateSqlsWithTransaction,支持事务的多SQL更新 BaseService新增IService的getMap(queryWrapper)等接口 @CollectThisApi注解,自动提取注解对应的rest接口 工具类 S.splitToList,D.formatDurationLabel等 优化: 关联绑定的实现中字段名列名的转换由规则转换改为精确转换 支持BindField&BindDict组合使用 BeanUtils.convertValueToFieldType支持LocalDateTime转换 修复: 修复@BindQuery查询不支持自定义逻辑删除字段的问题 升级依赖jar至最新(spring boot 2.4.5, mybatis-plus 3.4.2等)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值