Android Binder实战

项目需求

友盟上观察到有App有内存溢出导致的崩溃,经排查发现视频压缩时,内存暴涨100MB,容易造成OOM。

改造思路

项目中视频压缩使用了MediaCodec在子线程中,逐帧硬解,此过程中内存占用上升100mb

视频压缩内存监控

因此将视频压缩剥离到独立进程,通过AIDL和主进程进行双向通信,削平主进程100mb的内存波峰。
视频压缩参考https://github.com/fishwjy/VideoCompressor

背景知识

android进程

进程创建

我们知道,安卓系统里的所有APP都是被系统所托管的,也就是说,安卓系统负责APP进程的创建和回收。
进程创建很简单,就是在APP启动的时候,由Zygote进程fork出一个新的进程出来(注:系统服务system server也是由Zygote所创建)。一个应用默认只有一个进程,这个进程(主进程)的名称就是应用的包名。进程的特点:

  • 进程是系统资源和分配的基本单位,而我们常见的线程是调度的基本单位。
  • 每个进程都有自己独立的资源和内存空间
  • 其它进程不能任意访问当前进程的内存和资源
  • 系统给每个进程分配的内存会有限制
如何创建独立进程

Android多进程创建很简单,只需要在AndroidManifest.xml的声明四大组件的标签中增加"android:process"属性即可。
process分私有进程和全局进程:

  • 私有进程
    android:process=":remote",以冒号开头,冒号后面的字符串原则上是可以随意指定的。如果我们的包名为“com.kimmy.test”,则实际的进程名 为“com.kimmy.test:remote”。这种设置形式表示该进程为当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程中。
  • 全局进程
    进程名称不以冒号开头的进程都可以叫全局进程,如android:process="com.secondProcess",其他应用通过设置相同的ShareUID可以和它跑在同一个进程。
独立进程的限制
  • 静态成员和单例模式失效
  • 线程同步机制完全失效
  • SharePreferences的可靠性下降
  • Application会多次创建

android跨进程解决方案Binder

Binder机制具有两层含义:

  • 是一种跨进程通信的方式(IPC)
  • 是一种远程过程调用方式(PRC)

而从实现的角度来说,Binder核心被实现成一个Linux驱动程序,并运行于内核态。这样它才能具有强大的跨进程访问能力。

什么是Binder
  • 从来类的角度来说,Binder就是Android的一个类,它继承了IBinder接口
  • 从IPC的角度来说,Binder是Android中的一种跨进程通信方式,Binder还可以理解为一种虚拟的物理设备,它的设备驱动是/dev/binder,该通信方式在Linux中没有(由于耦合性太强,Linux没有接纳)
  • 从Android Framework角度来说,Binder是ServiceManager连接各种Manager(ActivityManager、WindowManager等)和相应的ManagerService的桥梁
  • 从Android应用层的角度来说,Binder是客户端和服务端进行通信的媒介,当你bindService的时候,服务端会返回一个包含了服务端业务调用的Binder对象,通过这个Binder对象,客户端就可以获取服务端提供的服务或者数据,这里的服务包括普通服务和基于AIDL的服务。
AIDL

AIDL即Android Interface Definition Language(安卓接口定义语言)。准确的来说,它是用于定义客户端与服务器相互通信接口的一种描述语言。它是一种IDL语言,可以拿来生成用于IPC的代码。从某种意义上说它其实是一个模板
当我们创建了这个接口后,系统会自动生成其对应的Binder类,它继承了IInterface, 内部有一个静态抽象类Stub和Stub内部的Proxy类。其中Stub继承了Binder类,所以AIDL中的Stub即为一个Binder对象。 在服务端实现该接口后,支持在客户端远程调用(RPC)。
综上AIDL定义的接口,它除了是一个接口以外,它还是一个Binder对象,支持在接口和Binder之间相互转换(asBinder(), asInterface())。

使用AIDL好处
  • C/S 代码复杂度
    设计这门语言的目的是为了实现进程间通信,尤其是在涉及多进程并发情况的下的进程间通信IPC。
    通过AIDL,可以让本地调用远程服务器的接口就像调用本地接口那么简单,让用户无需关注内部细节,只需要实现自己的业务逻辑接口,内部复杂的参数序列化发送、接收、客户端调用服务端的逻辑,你都不需要去关心了。

  • 数据传输复杂度
    在Android process 之间不能用通常的方式去访问彼此的内存数据。他们把需要传递的数据解析成基础对象,使得系统能够识别并处理这些对象。因为这个处理过程很难写,所以Android使用AIDL来解决这个问题

代码实战

交互UML图
  • 调用方:负责启动压缩线程,在收到进度通知后,更新UI
  • 压缩线程:负责压缩视频,并持有调用方以通知进度

交互接口定义

在决定使用AIDL进行跨进程通信后,我们的接口定义如下:

  • 主进程访问压缩(启动、删除任务)
//视频压缩AIDL类
interface IVideoCompressAIDL {
//添加、启动
void addCompressTask(String videoPath);
//移除任务
void removeCompressTask(String videoPath);

//注册回调
void register(IVideoCompressCallbackAIDL callBack);
//移除回调
void unregister(IVideoCompressCallbackAIDL callBack);
}
  • 压缩回调主进程(告知压缩进度)
//视频压缩回调类
interface IVideoCompressCallbackAIDL {
     void onStart(String videoPath);

     /**
      * @param percent 0~100
      */
     void onProgress(String videoPath,int percent);


     void onSuccess(String videoPath);

     void onFail(String videoPath);

     void onCanceled(String videoPath);
}

写完重新编译后,系统自动为我们生成了对应的Binder类,自动帮我们拓展了跨进程访问能力。

压缩service

public class SingleVideoCompressService extends Service {
    private RemoteCallbackList mCallbackList = new RemoteCallbackList<>();
    IVideoCompressAIDL.Stub mVideoCompressImpl= new IVideoCompressAIDL.Stub() {
        @Override
        public void addCompressTask(final String videoPath) {
//添加压缩任务
        }

        @Override
        public void removeCompressTask(String videoPath) {
      //删除压缩任务
        }

        @Override
        public void register(IVideoCompressCallbackAIDL callBack) {
            mCallbackList.register(callBack);
        }

        @Override
        public void unregister(IVideoCompressCallbackAIDL callBack) {
            mCallbackList.unregister(callBack);
        }
    };

    private synchronized void notifyCallback(String path, int actionCode, int arg) {
        try {
            int callbackLength = mCallbackList.beginBroadcast();

            for (int i = 0; i < callbackLength; i++) {
                IVideoCompressCallbackAIDL callbackAIDL = mCallbackList.getBroadcastItem(i);
//通知回调
            }
            mCallbackList.finishBroadcast();
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }
    @Override
    public void onDestroy() {
        for (CompressProcess process :
                mCompressTaskList) {
            process.cancel();
        }
        mCallbackList.kill();
        super.onDestroy();
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return mVideoCompressImpl;
    }
}

RemoteCallbackList是安卓提供的一个类,强化对跨进程的接口的管理
我们可以看到mVideoCompressImpl实现了压缩任务的添加删除、注册注销,并通过onBind暴露给调用方
notifyCallback让压缩服务能够跨进程通知调用方压缩进度

在manifest里用独立进程的方式声明此服务

 

调用方


    private ServiceConnection mCompressConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mService = IVideoCompressAIDL.Stub.asInterface(service);
            try {
                mService.register(mCallbackAIDL);
                compressInRemote();
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            try {
                mService.unregister(mCallbackAIDL);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
    };


bindService(mServiceIntent, mCompressConnection, BIND_AUTO_CREATE);

调用方在服务连接成功后的onServiceConnected回调里,通过系统自动生成的代码Stub.asInterface()拿到了Binder对象,从而能够进行双向通信。

总结

以上就是双向通信的全部代码,AIDL为我们自动实现了跨进程交互的细节, 使得我们能专注于业务代码,而不用去关心跨进程数据传输、对象访问的问题。
下面我们来分析下AIDL是如何做到的。

Binder介绍

Binder通信模型

通信模型

首先在理解模型之前先熟悉这几个概念:

  • Client进程:跨进程通讯的客户端(运行在某个进程)
  • Server进程:跨进程通讯的服务端(运行在某个进程)
  • Binder驱动:跨进程通讯的介质
  • ServiceManager:跨进程通讯中提供服务的注册和查询(运行在System进程)

这里只是个简单的模型而已,只需理解模型的通讯流程:

  1. Server端通过Binder驱动在ServiceManager中注册
  2. Client端通过Binder驱动获取ServiceManager中注册的Server端
  3. Client端通过Binder驱动和Server端进行通讯

Binder通信原理

image

理解完模型流程之后,开始理解模型的通讯原理:
以远程方法add为例

  • Service端通过Binder驱动在ServiceManager的查找表中注册Object对象的add方法
  • Client端通过Binder驱动在ServiceManager的查找表中找到Object对象的add方法,并返回proxy对象的add方法,add方法是个空实现,proxy对象也不是真正的Object对象,是通过Binder驱动封装好的代理类的add方法
  • 当Client端调用add方法时,Client端会调用proxy对象的add方法,通过Binder驱动去请求ServiceManager来找到Service端真正对象,然后调用Service端的add方法

结合本项目来说:

调用方要实现IPC,必须获得远程服务Binder对象在binder驱动层对应的mRemote引用,如何获得呢?首先绑定远程服务,绑定成功后的ServiceConnection中的IBinder service其实就是mRemote引用,但是因为是使用AIDL方式,所以需要在客户端中调用IVideoCompressAIDL.Stub.asInterface(android.os.IBinder obj)方法将服务器返回的Binder对象转换成AIDL接口,然后就可以通过这个接口去调用服务器的远程方法了。
这个方法逻辑很清晰,如果在同一个线程,则返回Binder对象;跨线程则返回Binder对象的代理,自动帮我们托管了跨线程的函数调用、数据流传输。

        public static IVideoCompressAIDL asInterface(android.os.IBinder obj) {
            if ((obj == null)) {
                return null;
            }
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if (((iin != null) && (iin instanceof IVideoCompressAIDL))) {
                return ((VideoCompressAIDL) iin);
            }
            return new IVideoCompressAIDL.Stub.Proxy(obj);
        }

我们得出AIDL的使用流程,其实很简单,大致就是在服务端创建一个Service,然后创建一个Binder对象,最后在客户端得到这个Binder对象。
AIDL使用流程:
先建立AIDL,如果在你建立的AIDL接口中,有自定义的类,那么也需要建立这个类的AIDL,并且名字要完全相同。同时在使用的时候,一定要显示的导入这个类。接下来的流程就是跟Binder的一样了。
服务器端:创建Binder对象,并且实现接口中的方法。
客户端:绑定service,得到Binder对象在驱动层对应的mRemote引用。

代码分析

我们定义好的AIDL文件,系统在编译后,为我们创建了俩类以提供跨进程能力。


自动生成的aidl文件

我们着重分析下IVideoCompressAIDL

AIDL类图

继承关系梳理

  • IVideoCompressAIDL:继承 android.os.IInterface
  • -Stub:继承android.os.Binder、实现IVideoCompressAIDL
  • --Proxy:实现IVideoCompressAIDL

业务代码中,我们和AIDL生成的额外代码,打交道的地方有俩处
1、 SingleVideoCompressService里,通过new IVideoCompressAIDL.Stub() {...}创建IVideoCompressAIDL的IPC本地实现。
2、 调用方,通过IVideoCompressAIDL.Stub.asInterface(IBinder)将IBinder对象转化成IVideoCompressAIDL接口

asInterface()
是实现跨进程的关键函数,用于将服务器的Binder对象转换成客户端所需的AIDL接口类型的对象,这种转换过程是区分进程的,如果客户端和服务端位于统一进程,那么返回服务器的Stub对象本身,否则返回的是系统封装后的Stub.proxy对象。

  public static net.kimmy.android.common.compress.serv.IVideoCompressAIDL asInterface(android.os.IBinder obj) {
            if ((obj == null)) {
                return null;
            }
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if (((iin != null) && (iin instanceof net.kimmy.android.common.compress.serv.IVideoCompressAIDL))) {
                return ((net.kimmy.android.common.compress.serv.IVideoCompressAIDL) iin);
            }
            return new net.kimmy.android.common.compress.serv.IVideoCompressAIDL.Stub.Proxy(obj);
        }

onTransact(int code,android.os.Parcel data,android.os.Parcel reply,int flags)
这个方法运行在服务端的Binder线程池中,当客户端发起跨进程请求时,远程请求会通过系统底层封装后交由服务端 的onTransact方法来处理。这个方法有四个参数,分别是code ,data,reply,flags.code是确定客户端请求的方法是哪个,data是目标方法所需的参数,reply是服务器端执行完后的返回值。如果这个方法返回false,那么客户端的请求会失败。

Proxy用跨进程调的方式,处理了数据传输和函数执行:调用了mRemote.transact(),把封装好的数据,跨进程传输。系统会回调到StubonTransact,接收跨进程的参数,执行函数。


package net.kimmy.android.common.compress.serv;
//视频压缩AIDL类
public interface IVideoCompressAIDL extends android.os.IInterface {
    /**
     * Local-side IPC implementation stub class.
     */
    public static abstract class Stub extends android.os.Binder implements net.kimmy.android.common.compress.serv.IVideoCompressAIDL {
        private static final java.lang.String DESCRIPTOR = "net.kimmy.android.common.compress.serv.IVideoCompressAIDL";

        /**
         * Construct the stub at attach it to the interface.
         */
        public Stub() {
            this.attachInterface(this, DESCRIPTOR);
        }

        /**
         * Cast an IBinder object into an net.kimmy.android.common.compress.serv.IVideoCompressAIDL interface,
         * generating a proxy if needed.
         */
        public static net.kimmy.android.common.compress.serv.IVideoCompressAIDL asInterface(android.os.IBinder obj) {
            if ((obj == null)) {
                return null;
            }
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if (((iin != null) && (iin instanceof net.kimmy.android.common.compress.serv.IVideoCompressAIDL))) {
                return ((net.kimmy.android.common.compress.serv.IVideoCompressAIDL) iin);
            }
            return new net.kimmy.android.common.compress.serv.IVideoCompressAIDL.Stub.Proxy(obj);
        }

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

        @Override
        public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
            java.lang.String descriptor = DESCRIPTOR;
            switch (code) {
                case INTERFACE_TRANSACTION: {
                    reply.writeString(descriptor);
                    return true;
                }
                case TRANSACTION_addCompressTask: {
                    data.enforceInterface(descriptor);
                    java.lang.String _arg0;
                    _arg0 = data.readString();
                    this.addCompressTask(_arg0);
                    reply.writeNoException();
                    return true;
                }
                case TRANSACTION_removeCompressTask: {
                    data.enforceInterface(descriptor);
                    java.lang.String _arg0;
                    _arg0 = data.readString();
                    this.removeCompressTask(_arg0);
                    reply.writeNoException();
                    return true;
                }
                case TRANSACTION_register: {
                    data.enforceInterface(descriptor);
                    net.kimmy.android.common.compress.serv.IVideoCompressCallbackAIDL _arg0;
                    _arg0 = net.kimmy.android.common.compress.serv.IVideoCompressCallbackAIDL.Stub.asInterface(data.readStrongBinder());
                    this.register(_arg0);
                    reply.writeNoException();
                    return true;
                }
                case TRANSACTION_unregister: {
                    data.enforceInterface(descriptor);
                    net.kimmy.android.common.compress.serv.IVideoCompressCallbackAIDL _arg0;
                    _arg0 = net.kimmy.android.common.compress.serv.IVideoCompressCallbackAIDL.Stub.asInterface(data.readStrongBinder());
                    this.unregister(_arg0);
                    reply.writeNoException();
                    return true;
                }
                default: {
                    return super.onTransact(code, data, reply, flags);
                }
            }
        }

        private static class Proxy implements net.kimmy.android.common.compress.serv.IVideoCompressAIDL {
            private android.os.IBinder mRemote;

            Proxy(android.os.IBinder remote) {
                mRemote = remote;
            }

            @Override
            public android.os.IBinder asBinder() {
                return mRemote;
            }

            public java.lang.String getInterfaceDescriptor() {
                return DESCRIPTOR;
            }

            @Override
            public void addCompressTask(java.lang.String videoPath) throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    _data.writeString(videoPath);
                    mRemote.transact(Stub.TRANSACTION_addCompressTask, _data, _reply, 0);
                    _reply.readException();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
            }

            @Override
            public void removeCompressTask(java.lang.String videoPath) throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    _data.writeString(videoPath);
                    mRemote.transact(Stub.TRANSACTION_removeCompressTask, _data, _reply, 0);
                    _reply.readException();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
            }

            @Override
            public void register(net.kimmy.android.common.compress.serv.IVideoCompressCallbackAIDL callBack) throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    _data.writeStrongBinder((((callBack != null)) ? (callBack.asBinder()) : (null)));
                    mRemote.transact(Stub.TRANSACTION_register, _data, _reply, 0);
                    _reply.readException();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
            }

            @Override
            public void unregister(net.kimmy.android.common.compress.serv.IVideoCompressCallbackAIDL callBack) throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    _data.writeStrongBinder((((callBack != null)) ? (callBack.asBinder()) : (null)));
                    mRemote.transact(Stub.TRANSACTION_unregister, _data, _reply, 0);
                    _reply.readException();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
            }
        }

        static final int TRANSACTION_addCompressTask = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
        static final int TRANSACTION_removeCompressTask = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
        static final int TRANSACTION_register = (android.os.IBinder.FIRST_CALL_TRANSACTION + 2);
        static final int TRANSACTION_unregister = (android.os.IBinder.FIRST_CALL_TRANSACTION + 3);
    }

    public void addCompressTask(java.lang.String videoPath) throws android.os.RemoteException;

    public void removeCompressTask(java.lang.String videoPath) throws android.os.RemoteException;

    public void register(net.kimmy.android.common.compress.serv.IVideoCompressCallbackAIDL callBack) throws android.os.RemoteException;

    public void unregister(net.kimmy.android.common.compress.serv.IVideoCompressCallbackAIDL callBack) throws android.os.RemoteException;
}

总结

压缩进程剥离后,app在压缩上传时,主进程占用内存没有再上涨100mb了,达到了我们的预期。
值得注意的是,压缩服务有可能启动失败,我们要在服务启动失败后,退化到主进程进行压缩,确保功能稳定。

你可能感兴趣的:(Android Binder实战)