热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Android原生开发Binder

Android原生开发-Binder-背景不要通过背诵的方式掌握Binder,找一个Binder的应用场景,然后去实践解决下这个场景的问题,在解决完问题后去思考下为什么要这样设计

背景

不要通过背诵的方式掌握Binder,找一个Binder的应用场景,然后去实践解决下这个场景的问题,在解决完问题后去思考下为什么要这样设计,为什么要使用Binder.

也就是通过实践中去掌握

  1. Binder是什么?
  2. 怎么用?
  3. 为什么是Binder机制而不是内存共享或者通道机制?

Binder相关背景知识

Binder是用来解决进程通信的.首先想到的问题是为什么要进程通信,这个很简单,因为使用了多进程. 那么为什么要使用多进程呢?

为什么要使用多进程

以一个成熟的输入法应用的生命周期举例.

1.0 App=输入进程

输入进程是输入法的核心进程,用来接管系统的InputMethodService,简单的业务表象就是用户打字,我负责出词上屏.

2.0 App=输入进程+容器进程

随着业务的发展,输入法延伸出一些相关功能,比如自定义输入皮肤,斗图,小游戏,emoji,文字识别,语音识别等等.

从技术上看这些业务还经历了WebView->ReactNative->Flutter的演进

这些重量级的业务和组件放在输入法进程里显然不合理,这样做会造成输入的时候可能莫名卡顿,输入法服务冷启动的时候明显变慢.所以需要把这些功能拆分到一个容器进程里面,交给桌面应用接管.

3.0 App=输入进程+容器进程+通信进程

随着业务的发展,需要进一步优化输入体验,除了常规的技术优化. 站在业务的角度上看,就是一些不必要的后台通信需要拆分出来.

简单来说就是:

  1. 日志上报,客户端会有大量的加密,脱敏后的数据需要上报到后台服务器.
  2. 业务弹窗,通知的下发.
  3. 业务策略下发,比如广告策略,功能策略等等.

这些一般运行在子线程里面,但是又无法保证他们不会去跟输入法进程的主线程抢夺CPU时间片,所以直接独立到后台通信进程.

这样做还有一个好处,比如容器进程或者后台进程代码发送崩溃不会影响到输入进程. 尤其是用户在其他App使用输入法的时候,不会因为架构不合理造成不好的体验.

4.0 常规应用为什么要使用多进程

可能对大多数应用而言,用不到上诉的东西,因为输入法App太小众了.但是还是有一些场景可以斟酌下,挖掘下,比如:

微信移动开发团队在 《Android内存优化杂谈》 一文中就说到:“对于webview,图库等,由于存在内存系统泄露或者占用内存过多的问题,我们可以采用单独的进程。微信当前也会把它们放在单独的tools进程中”。

我想大多数团队都会用到webview,图库和图片吧,如果使用量比较大就可以考虑拆分进程了.

另外现在ReactNative/Flutter跨端技术比较热门,如果团队对这个技术栈把控不深,又期望可以逐步探索掌握.是不是可以考虑使用单独进程+混合栈的方案来逐步落地.

为什么跨进程通讯要使用Binder

那么下一个问题是为什么要使用Binder来通信.

假若回到十几,二十年前, 现在你是Android进程通信机制的设计者,摆在你面前的跨进程通信方案有三大类

  1. 内存共享
  2. Binder
  3. 通道/Socket...等

考虑性能和安全,你如何选择?

为什么不用内存共享

内存共享即直接把应用A的内存拿给应用B,这样就实现了内存零拷贝的通信,性能应该是最好的方案了.但是这里有个严重的安全问题.

比如应用A,B分别在不同进程,有块内存区域存储的是应用A的账号信息,假如这块内存被共享给了应用B,账号就泄露了.所以需要鉴权,确定B是否有权限访问应用A.

道理我都懂,可是怎么编码呢?

把内存的物理地址全部编码记录下下来,然后再做映射和签定么?

这样做代码实现的难度呢?这个方案复杂度会不会造成内存零拷贝带来的那点优势被牺牲掉呢?

所以不用内存共享,是无法保证安全性.

为什么不是通道

方式: 应用A把内存拷贝到系统,然后系统再把内存拷贝到应用B. 两次拷贝

因为是一个明晰的C/S架构,有明确的通信代码,所以直接在通信的时候鉴权,安全比较好保证.

不用他的原因是因为性能问题.

2次拷贝太多了,A的数据发给B,直接把A的数据拷贝过去不就行了么?

是的,可以,这就是Binder.

Binder机制和通道类似. 区别在于应用A并没有真正的把数据拷贝到系统内存,然后系统再把内存拷贝到应用B. Binder机制做了一个巧妙的转换,A拷贝到系统的时候没有拷贝,而是等到B需要A的数据的时候,Binder直接把A的数据拷贝给B. 这套方案就叫做内存映射方案.

怎么使用Binder

因为输入法项目还涉及到一些公司机密,我就不使用输入法演示了,直接使用一个常见的Demo例子.

远程服务

目的: 让Activity进程可以调用Service进程的getBooks()方法和addBook()方法. 按照代码写的顺序逐步实现:

1.创建IBinder接口类->申明跨进程传输的能力

因为接口除了本身的能力,还需要具备Binder传输的能力,所以让接口继承IInterface

public interface BookManager extends IInterface {

    List getBooks() throws RemoteException;

    void addBook(Book book) throws RemoteException;
}

2.继承Binder类->实现跨进程传输的能力

为了去实现IInterface接口的asBinder方法,必须有一个Binder对象,所以最简单的方式就是直接让Stub继承Binder类

public abstract class Stub extends Binder implements BookManager {

    public Stub() {
        this.attachInterface(this, DESCRIPTOR);
    }

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

}

3.实现静态方法asInterface->链接调用进程

服务端已经实现了跨进程的能力,怎么把这个能力给到调用端呢?

先看看调能拿到的东西. 通过服务Service绑定能拿到ServiceConnection,其中的onServiceConnected提供一个IBinder对象.


    private void attemptToBindService() {
        Intent intent = new Intent(this, RemoteService.class);
        intent.setAction("com.baronzhang.ipc.server");
        bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
    }
    
    private ServiceConnection serviceCOnnection= new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            // 这里的IBinder service就是远端传递过来的.
        }
    }

那么现在的问题就变成了把IBinder转化成BookManager. 所以有了以下转化代码:

    public static BookManager asInterface(IBinder binder) {
        if (binder == null)
            return null;
        IInterface iin = binder.queryLocalInterface(DESCRIPTOR);
        // 如果是同一个进程直接强制转化
        if (iin != null && iin instanceof BookManager)
            return (BookManager) iin;
        // 如果是两个进程使用Proxy
        return new Proxy(binder);
    }

4.实现Proxy->跨进程传输的具体实现

接着上面的问题,上面的asInterface方法是运行在调用进程的,因为在不同进程没有办法把需要的BookManager实例对象(即Stub)直接拿到,所以需要有一个代理对象(Proxy)帮助我们实现好像拿到了BookManager实例对象的感觉.

所以Proxy需要实现BookManager

public class Proxy implements BookManager {

实现后,BookManager还有3个方法需要实现怎么办? 既然是代理对象肯定不会去实现了,真正的实现应该是在服务进程. Proxy需要把消息发送给服务进程


    private IBinder remote;

    public Proxy(IBinder remote) {

        this.remote = remote;
    }
    
    @Override
    public List getBooks() throws RemoteException {
        Parcel data = Parcel.obtain();
        Parcel replay = Parcel.obtain();
        List result;

        try {
            data.writeInterfaceToken(DESCRIPTOR);
            remote.transact(Stub.TRANSAVTION_getBooks, data, replay, 0);
            replay.readException();
            result = replay.createTypedArrayList(Book.CREATOR);
        } finally {
            replay.recycle();
            data.recycle();
        }
        return result;
    }

例如getBooks()方法,Proxy的作用就是利用Parcel封装对象,利用IBinder(服务连接的时候传递归来的参数)发送.

5.实现Parcelable->支持跨进程传输

JavaBean对象Book会被跨进程传输,那么就需要支持Parcelable接口

IDE自动生成Parcelable代码

public class Book implements Parcelable {
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(this.price);
        dest.writeString(this.name);
    }

    protected Book(Parcel in) {
        this.price = in.readInt();
        this.name = in.readString();
    }

    public static final Creator CREATOR = new Creator() {
        @Override
        public Book createFromParcel(Parcel source) {
            return new Book(source);
        }

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

6.补全onTranstale方法->实现接口

调用进程把消息和参数发送给服务进程,服务进程怎么生产出结果后回复给调用进程呢?

利用onTransact()方法

    @Override
    protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
        switch (code) {
            case TRANSAVTION_getBooks:
                data.enforceInterface(DESCRIPTOR);
                List result = this.getBooks();
                reply.writeNoException();
                reply.writeTypedList(result);
                return true;
        }
    }

到此一个完整的手写Binder服务通信示例代码就实现了.

这个例子我直接借用了Github上的一个示例工程.参考Demo.

AIDL

上面的例子直接使用Binder来完成了Demo.因为Binder通信的代码大多长得差不多. IDE干脆给了个模板,帮助开发者自动生成Binder代码.

AIDL就是这个开发模板

1. 定义AIDL接口

interface AIDLBookManager {
    // 自动生成
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);
    // 补充接口
    void addBook(in Book book);

    List getBookList();
}

2. 自动实现Parcelable接口

3. 依赖Parcelable+自动生成代码

  1. 在AIDL文件的同级目录新建一个文件Book.aidl,填写
// IBookManager.aidl
package com.baronzhang.ipc;
parcelable Book;
  1. 然后在AIDLBookManager.aidl引入依赖
package com.baronzhang.ipc;
// 依赖
import com.baronzhang.ipc.Book;
  1. 编译,自动生成代码

Stub Proxy 类

asInterface() onTransact() 方法 都自动生成.

为什么代码都在一个文件里面

  1. 代码是自动生成去解决一类问题的,没必要考虑可读性.
  2. 代码生成的方式应该和APT类似,直接一个文件生成写起来简单点.

Messenger

  1. 定义服务进程
    public IBinder onBind(Intent intent) {
        return messenger.getBinder();
    }

    Messenger messenger = new Messenger(new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case 1:
                    Message replyMsg = Message.obtain(null, 1, books != null ? books : new ArrayList());
                    try {
                        msg.replyTo.send(replyMsg);
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                    break;
                case 2:
                    Book book = (Book) msg.obj;
                    book.setPrice(book.getPrice() * 2);
                    books.add(book);
                    break;
                default:
                    throw new IllegalArgumentException("未实现");
            }
            super.handleMessage(msg);
        }
    });
  1. 定义调用进程
    // 定义
    Messenger messenger;
    Messenger handleMessengr = new Messenger(new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case 1:
                    List books = (List) msg.obj;
                default:
                    break;
            }
            super.handleMessage(msg);
        }
    });
    
    // 初始化
    Messenger messenger = new Messenger(service);
    
    // 执行
    Message message = Message.obtain();
    message.what = 1;
    message.replyTo = handleMessengr;
    messenger.send(message);

可以看到Messenger使用起来比较容易,但是为什么我们往往使用了AIDL而不是Messenger?

  1. AIDL直接定义了方法通信,而Messager需要把方法转换成消息,消息再转化为方法,这样两边代码的可读性就大大降低了.
  2. Messenger基于Handler来实现消息处理,Handler是线程通信模型,运行在单一线程,这样Messenger也就成了单一线程,顺序执行的结构了.

总结

掌握不住一个经典知识点,大概有几个原因

  1. 用得少,用多了就熟悉,最好是实战用
  2. 想得少,知其然与所以然

推荐阅读
  • CentOS 7部署KVM虚拟化环境之一架构介绍
    本文介绍了CentOS 7部署KVM虚拟化环境的架构,详细解释了虚拟化技术的概念和原理,包括全虚拟化和半虚拟化。同时介绍了虚拟机的概念和虚拟化软件的作用。 ... [详细]
  • 本文讨论了在VMWARE5.1的虚拟服务器Windows Server 2008R2上安装oracle 10g客户端时出现的问题,并提供了解决方法。错误日志显示了异常访问违例,通过分析日志中的问题帧,找到了解决问题的线索。文章详细介绍了解决方法,帮助读者顺利安装oracle 10g客户端。 ... [详细]
  • 本文介绍了一个React Native新手在尝试将数据发布到服务器时遇到的问题,以及他的React Native代码和服务器端代码。他使用fetch方法将数据发送到服务器,但无法在服务器端读取/获取发布的数据。 ... [详细]
  • 这个问题困扰了我两天,卸载Dr.COM客户端(我们学校上网要装这个客户端登陆服务器,以后只能在网页里输入用户名和密码了),问题解决了。问题的现象:在实验室机台式机上安装openfire和sp ... [详细]
  • 本文详细介绍了SQL日志收缩的方法,包括截断日志和删除不需要的旧日志记录。通过备份日志和使用DBCC SHRINKFILE命令可以实现日志的收缩。同时,还介绍了截断日志的原理和注意事项,包括不能截断事务日志的活动部分和MinLSN的确定方法。通过本文的方法,可以有效减小逻辑日志的大小,提高数据库的性能。 ... [详细]
  • Android中高级面试必知必会,积累总结
    本文介绍了Android中高级面试的必知必会内容,并总结了相关经验。文章指出,如今的Android市场对开发人员的要求更高,需要更专业的人才。同时,文章还给出了针对Android岗位的职责和要求,并提供了简历突出的建议。 ... [详细]
  • 图解redis的持久化存储机制RDB和AOF的原理和优缺点
    本文通过图解的方式介绍了redis的持久化存储机制RDB和AOF的原理和优缺点。RDB是将redis内存中的数据保存为快照文件,恢复速度较快但不支持拉链式快照。AOF是将操作日志保存到磁盘,实时存储数据但恢复速度较慢。文章详细分析了两种机制的优缺点,帮助读者更好地理解redis的持久化存储策略。 ... [详细]
  • Java在运行已编译完成的类时,是通过java虚拟机来装载和执行的,java虚拟机通过操作系统命令JAVA_HOMEbinjava–option来启 ... [详细]
  • 服务器上的操作系统有哪些,如何选择适合的操作系统?
    本文介绍了服务器上常见的操作系统,包括系统盘镜像、数据盘镜像和整机镜像的数量。同时,还介绍了共享镜像的限制和使用方法。此外,还提供了关于华为云服务的帮助中心,其中包括产品简介、价格说明、购买指南、用户指南、API参考、最佳实践、常见问题和视频帮助等技术文档。对于裸金属服务器的远程登录,本文介绍了使用密钥对登录的方法,并提供了部分操作系统配置示例。最后,还提到了SUSE云耀云服务器的特点和快速搭建方法。 ... [详细]
  • Oracle优化新常态的五大禁止及其性能隐患
    本文介绍了Oracle优化新常态中的五大禁止措施,包括禁止外键、禁止视图、禁止触发器、禁止存储过程和禁止JOB,并分析了这些禁止措施可能带来的性能隐患。文章还讨论了这些禁止措施在C/S架构和B/S架构中的不同应用情况,并提出了解决方案。 ... [详细]
  • 本文概述了JNI的原理以及常用方法。JNI提供了一种Java字节码调用C/C++的解决方案,但引用类型不能直接在Native层使用,需要进行类型转化。多维数组(包括二维数组)都是引用类型,需要使用jobjectArray类型来存取其值。此外,由于Java支持函数重载,根据函数名无法找到对应的JNI函数,因此介绍了JNI函数签名信息的解决方案。 ... [详细]
  • Mono为何能跨平台
    概念JIT编译(JITcompilation),运行时需要代码时,将Microsoft中间语言(MSIL)转换为机器码的编译。CLR(CommonLa ... [详细]
  • Question该提问来源于开源项目:react-native-device-info/react-native-device-info ... [详细]
  • 微信商户扫码支付 java开发 [从零开发]
    这个教程可以用作了解扫码支付的整体运行过程,已经实现了前端扫码,记录订单,回调等一套完整的微信扫码支付。相关链接:微信支 ... [详细]
  • 腾讯T3大牛亲自教你!2021大厂Android面试经验,经典好文
    本篇将由环境搭建、实现原理、编程开发、插件开发、编译运行、性能稳定、发展未来等七个方面,对当前的ReactNative和Flutter进行全面的分析对比, ... [详细]
author-avatar
呵呵
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有