项目需求
友盟上观察到有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来解决这个问题
代码实战
- 调用方:负责启动压缩线程,在收到进度通知后,更新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进程)
这里只是个简单的模型而已,只需理解模型的通讯流程:
- Server端通过Binder驱动在ServiceManager中注册
- Client端通过Binder驱动获取ServiceManager中注册的Server端
- Client端通过Binder驱动和Server端进行通讯
Binder通信原理
理解完模型流程之后,开始理解模型的通讯原理:
以远程方法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文件,系统在编译后,为我们创建了俩类以提供跨进程能力。
我们着重分析下IVideoCompressAIDL
类
继承关系梳理
- 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()
,把封装好的数据,跨进程传输。系统会回调到Stub
的onTransact
,接收跨进程的参数,执行函数。
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了,达到了我们的预期。
值得注意的是,压缩服务有可能启动失败,我们要在服务启动失败后,退化到主进程进行压缩,确保功能稳定。