likes
comments
collection
share

Android原生开发-Binder

作者站长头像
站长
· 阅读数 3

背景

不要通过背诵的方式掌握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<Book> 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<Book> getBooks() throws RemoteException {
        Parcel data = Parcel.obtain();
        Parcel replay = Parcel.obtain();
        List<Book> 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<Book> CREATOR = new Creator<Book>() {
        @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<Book> 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<Book> getBookList();
}

2. 自动实现Parcelable接口

Android原生开发-Binder

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. 编译,自动生成代码

Android原生开发-Binder

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<Book>());
                    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<Book> books = (List<Book>) 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. 想得少,知其然与所以然
转载自:https://juejin.cn/post/6961409017545490462
评论
请登录