86

【漫画技术】Android跨进程通信

 6 years ago
source link: https://juejin.im/post/59f32fd5f265da43346f2a99
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

【漫画技术】Android跨进程通信

2017年10月27日 13:09 ·  阅读 4789


类型 描述 用时
选题 silencezwm 0.5小时
写作时间 2017年10月25日 5.5小时
审稿 silencezwm、Mya婷婷 2小时
校对上线 silencezwm 1小时

Tips:4个环节,共计约9小时的精心打磨完成上线,同时也非常感谢参与审稿的同学。




你好,欢迎来到【漫画技术】栏目



让你看的开心,学的舒心

看漫画,涨薪资(¥) >>>【小猪的传奇一生】:

小猪的传奇一生

该漫画描述了小猪仔出生后,就生活在猪圈中,快乐的成长着。有一天,小猪长大了,屠宰场的老板就会通过船将肥猪运输过来进行屠宰,然后将猪肉销往世界各地。

看完该漫画后,是不是觉得小猪仔的一生有点小悲凉,要怪就怪可恶的人类,无肉不欢,哈哈。

精彩的漫画背后,总隐藏着一丝丝技术的小知识。本文将为你介绍“Android跨进程通信”的相关知识,通过本文的学习,你可以了解到:

一、单进程通信与多进程通信之间的区别

二、跨进程通信常见的五种实现方式

三、跨进程通信的注意事项

一、单进程通信与多进程通信之间的区别

概念普及:IPC(Inter-Process Communication)机制,即进程间通信或者跨进程通信机制,是指两个进程之间进行数据交换的过程。

1.1、单进程通信

在很久以前,小猪仔从生到死都是在猪圈中生活的,没有外来者的入侵。同样的,在Android开发中,默认情况下,程序运行后,都只是运行在一个以项目包名创建的主进程中(就好比猪圈),例如

项目包名为:com.silencezwm.ipcdemo
默认进程名:com.silencezwm.ipcdemo
复制代码

单进程通信就如:不同品类的猪仔(Android中不同的组件)在相同的猪圈(在同一进程中)中生活(运行)。

1.2、多进程通信

突然有一天,河流的右边来了一个商人,他发现河对岸有不少肥猪在游荡。于是,他发现了一个发家致富的机会,他在这里建了一个屠宰场。然后经常通过船将对岸的肥猪运送过来,进行屠宰,赚的盆满钵满。

这个就类似Android程序,本来只有一个进程在运行,但是因为产品提了个奇葩的需求,使得我们程序猿们不得不多开一个进程来实现该需求。

宝宝不开心

程序猿们一顿牢骚后,最后还是动手干活了。

他们在AndroidManifest.xml文件中相应的组件添加了android:process属性,并指定进程名。然后打开了两个Activity,但是BActivity被指定运行在新的进程,当程序跑起来后,此时可以看到有两个进程正在运行,如图:

两个进程正在运行

我们知道,在单进程中通信,组件间是可以随意进行通信,因为它们都处于同一个内存空间。那多进程之间是怎样通信的呢?

漫画中,屠宰场的老板通过船将河对岸的肥猪运送过来,就因为船的存在,该老板就可以跨越河流到达对岸。那么,Android跨进程通信中,我们也需要拥有同样功能的船,它就是Binder,通过Binder的中转,进程之间就能顺利的进行数据交换了。

跨进程通信

二、跨进程通信常见的五种实现方式

五种常见的实现方式可分为两大类:四大组件的跨进程通信和AIDL。

2.1、四大组件

Activity、Service、BroadcastReceiver、Content Provider四大组件只需要在AndroidManifest.xml文件相应的组件中添加android:process属性,并指定进程名。程序运行起来后,它们就会运行在不同的进程中,它们之间的通信,官方已经给我们做了非常好的封装,所以使用起来也非常方便,这里就不多做解释了。

2.2、AIDL

概念普及:AIDL(Android interface definition Language),即Android接口定义语言。

要想应用AIDL技术,就至少需要有两个进程存在,A进程通过定义的AIDL接口文件与B进程进行通信,具体的实现步骤如下:

1、准备两个进程:新建一个项目 IPCDemo,然后新建一个Module IPCClient,这样我们就准备好了两个进程,完成后的项目结构如图:

两个进程项目结构

2、创建AIDL文件:在服务端IPCDemo中新建一个AIDL接口文件:IMyAidlInterface.aidl,其中会默认实现basicTypes方法,然后我们再定义一个login方法,IMyAidlInterface.aidl完整代码如下;

package com.silencezwm.ipcdemo;

/**
 *  定义的AIDL接口文件
 */
interface IMyAidlInterface {
    /**
     *  AIDL默认实现的方法
     */
    void basicTypes(int anInt, long aLong, boolean aBoolean, float                     aFloat,double aDouble, String aString);

    /**
     *  定义了一个登录方法,包含用户名、密码两个参数
     */
    void login(String username, String password);

}
复制代码

3、build项目:此时,Android Studio会自动为你生成一个继承自IInterface的java文件,IMyAidlInterface.java完整代码如下;

package com.silencezwm.ipcdemo;

/**
 * 定义的AIDL接口文件
 */
public interface IMyAidlInterface extends android.os.IInterface {
    /**
     * Local-side IPC implementation stub class.
     */
    public static abstract class Stub extends android.os.Binder implements com.silencezwm.ipcdemo.IMyAidlInterface {
        private static final java.lang.String DESCRIPTOR = "com.silencezwm.ipcdemo.IMyAidlInterface";

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

    /**
     * Cast an IBinder object into an   com.silencezwm.ipcdemo.IMyAidlInterface interface,
     * generating a proxy if needed.
     */
    public static com.silencezwm.ipcdemo.IMyAidlInterface asInterface(android.os.IBinder obj) {
        if ((obj == null)) {
            return null;
        }
        android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
        if (((iin != null) && (iin instanceof com.silencezwm.ipcdemo.IMyAidlInterface))) {
            return ((com.silencezwm.ipcdemo.IMyAidlInterface) iin);
        }
        return new com.silencezwm.ipcdemo.IMyAidlInterface.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 {
        switch (code) {
            case INTERFACE_TRANSACTION: {
                reply.writeString(DESCRIPTOR);
                return true;
            }
            case TRANSACTION_basicTypes: {
                data.enforceInterface(DESCRIPTOR);
                int _arg0;
                _arg0 = data.readInt();
                long _arg1;
                _arg1 = data.readLong();
                boolean _arg2;
                _arg2 = (0 != data.readInt());
                float _arg3;
                _arg3 = data.readFloat();
                double _arg4;
                _arg4 = data.readDouble();
                java.lang.String _arg5;
                _arg5 = data.readString();
                this.basicTypes(_arg0, _arg1, _arg2, _arg3, _arg4, _arg5);
                reply.writeNoException();
                return true;
            }
            case TRANSACTION_login: {
                data.enforceInterface(DESCRIPTOR);
                java.lang.String _arg0;
                _arg0 = data.readString();
                java.lang.String _arg1;
                _arg1 = data.readString();
                this.login(_arg0, _arg1);
                reply.writeNoException();
                return true;
            }
        }
        return super.onTransact(code, data, reply, flags);
    }

    private static class Proxy implements com.silencezwm.ipcdemo.IMyAidlInterface {
        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;
        }

        /**
         * AIDL默认实现的方法
         */
        @Override
        public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, java.lang.String aString) 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.writeInt(anInt);
                _data.writeLong(aLong);
                _data.writeInt(((aBoolean) ? (1) : (0)));
                _data.writeFloat(aFloat);
                _data.writeDouble(aDouble);
                _data.writeString(aString);
                mRemote.transact(Stub.TRANSACTION_basicTypes, _data, _reply, 0);
                _reply.readException();
            } finally {
                _reply.recycle();
                _data.recycle();
            }
        }

        /**
         * 定义了一个登录方法,包含用户名、密码两个参数
         */
        @Override
        public void login(java.lang.String username, java.lang.String password) 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(username);
                _data.writeString(password);
                mRemote.transact(Stub.TRANSACTION_login, _data, _reply, 0);
                _reply.readException();
            } finally {
                _reply.recycle();
                _data.recycle();
            }
        }
    }

    static final int TRANSACTION_basicTypes = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
    static final int TRANSACTION_login = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
}

    /**
    * AIDL默认实现的方法
    */
    public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, java.lang.String aString) throws android.os.RemoteException;

    /**
    * 定义了一个登录方法,包含用户名、密码两个参数
    */
    public void login(java.lang.String username, java.lang.String password) throws android.os.RemoteException;
}
复制代码

4、新建Service:继续在服务器端src/main/java/包名目录下新建一个Service,并在配置文件中注册,然后设置其action值为com.silencezwm.ipcdemo,以供客户端进行调用,并在Service内部创建一个内部类继承静态抽象类IMyAidlInterface.Stub,AidlService.java完整代码如下:

package com.silencezwm.ipcdemo;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;

public class AidlService extends Service {

    private static final String TAG = AidlService.class.getName();

    public AidlService() {

    }

    @Override
    public IBinder onBind(Intent intent) {
        return new MyBinder();
    }

    class MyBinder extends IMyAidlInterface.Stub {

        @Override
        public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {
            Log.d(TAG, "=====:basicTypes");
        }

        @Override
        public void login(String username, String password) throws RemoteException {
            Log.d(TAG, "=====:login" + username + "==" + password);
        }
    }
}
复制代码

5、拷贝aidl目录文件:至此,服务端的代码编写完成,接下来我们只需要完成客户端的调用代码即可。相当简单,首先把服务端aidl整个文件夹拷贝到客户端src/main目录下(至于拷贝的原因,稍后会进行阐述),然后build项目。此时,客户端、服务端就同时拥有AIDL相同的代码;

6、绑定服务端Service:在客户端你想要的地方通过服务端Service所在地的包名以及action来进行绑定,然后将Service连接成功后返回的IBinder对象,通过IMyAidlInterface.Stub.asInterface方法转换为我们定义的aidl对象,然后根据该对象调用我们所定义的方法即可完成整个通信过程,客户端MainActivity.java调用代码如下:

package com.silencezwm.ipcclient;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;

import com.silencezwm.ipcdemo.IMyAidlInterface;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private Button mBtnLogin;

    private IMyAidlInterface mIMyAidlInterface;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mBtnLogin = (Button) findViewById(R.id.btn_login);
        mBtnLogin.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.btn_login:
                Intent intent = new Intent();
                // 服务端AndroidManifest.xml文件该Service所配置的action
                intent.setAction("com.silencezwm.ipcdemo");
                // Service所在的包名
                intent.setPackage("com.silencezwm.ipcdemo");
                bindService(intent, new ConnectCallBack(), Context.BIND_AUTO_CREATE);
                break;
        }
    }

    class ConnectCallBack implements ServiceConnection{

        // 服务连接成功回调
        @Override
        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
            mIMyAidlInterface = IMyAidlInterface.Stub.asInterface(iBinder);
            login();
        }

        // 服务断开连接回调
        @Override
        public void onServiceDisconnected(ComponentName componentName) {
            mIMyAidlInterface = null;
        }
    }

    private void login() {
        try {
            mIMyAidlInterface.login("silencezwm", "123456");
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

}
复制代码

7、结果验证:收获的季节来了,先将服务端程序跑起来,之后将客户端程序跑起来,点击登录按钮,不出意外的话,我们已经绑定了服务端的Service,并调用了login方法,将数据传递到服务端了,来看看Log的打印信息:

回顾AIDL整个实现过程,其实并不复杂。此时,聪明的人往往都会有一个疑问:

客户端的数据到底是如何传递到服务端的呢?
复制代码

接下来,我们就来一探究竟,以下的代码主要涉及到自动生成的IMyAidlInterface.java文件。

在客户端调用代码中,我们知道,一旦绑定Service成功后,会返回一个IBinder对象,调用IMyAidlInterface.Stub.asInterface(iBinder)方法将该对象转换为了我们所定义的AIDL接口对象,该方法具体做了什么呢?来看看:

private static final java.lang.String DESCRIPTOR = "com.silencezwm.ipcdemo.IMyAidlInterface";
    
public Stub() {
    this.attachInterface(this, DESCRIPTOR);
}

public static com.silencezwm.ipcdemo.IMyAidlInterface asInterface(android.os.IBinder obj) {
    if ((obj == null)) {
        return null;
    }
    android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
    if (((iin != null) && (iin instanceof com.silencezwm.ipcdemo.IMyAidlInterface))) {
        return ((com.silencezwm.ipcdemo.IMyAidlInterface) iin);
    }
    return new com.silencezwm.ipcdemo.IMyAidlInterface.Stub.Proxy(obj);
}
复制代码

上面这段代码中,首先Stub构造方法被调用,跟着attachInterface方法被调用:

private IInterface mOwner;
private String mDescriptor;
...

public void attachInterface(IInterface owner, String descriptor) {
    mOwner = owner;
    mDescriptor = descriptor;
}
复制代码

相当好理解,就是进行接口对象和字符串标识符的赋值。接下来在asInterface方法中,会根据标识符去IBinder的本地去查找是否有该对象,也就是调用obj.queryLocalInterface(DESCRIPTOR)方法,继续源码中Binder.java

public IInterface queryLocalInterface(String descriptor) {
    if (mDescriptor.equals(descriptor)) {
        return mOwner;
    }
    return null;
}
复制代码

意思就是如果本地存在这个标识符的IInterface对象,那就直接返回之前构造方法中初始化的mOwner对象,否则返回null,因为我们这里涉及到了跨进程通信,所以这里会直接返回null。代码继续往下走,很显然,以下这段代码会调用:

return new com.silencezwm.ipcdemo.IMyAidlInterface.Stub.Proxy(obj);
复制代码

代码字面意思就是返回IBinder的代码对象,如下:

private android.os.IBinder mRemote;

Proxy(android.os.IBinder remote) {
    mRemote = remote;
}
复制代码

到这里,我们就拿到了一个IBinder的代理对象,通过代理对象,我们就可以调用之前所定义的login方法啦,代码:

@Override
public void login(java.lang.String username, java.lang.String password) 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(username);
        _data.writeString(password);
        mRemote.transact(Stub.TRANSACTION_login, _data, _reply, 0);
        _reply.readException();
    } finally {
        _reply.recycle();
        _data.recycle();
    }
}
复制代码

这里就涉及到了一个重要的类Parcel,Parcel天生具备跨进程传输数据能力。在文章开头的漫画中,可不是直接把猪仔赶上船就行的,万一猪仔乱跑掉河里去了怎么办,所以屠宰场老板就准备了些猪笼。首先将猪仔赶进猪笼中,待船靠岸后,打开猪笼,将猪仔放出来即可。我们这里的Parcel就好比猪笼,我们把需要传递的数据写入Parcel中,然后到达目标进程后,将Parcel中的数据读出即可,所以可以将Parcel称为数据传输载体。Parcel支持的数据类型非常之多,足以满足我们日常开发所需。

现在你知道客户端的数据是如何传递到服务端了吗?

三、跨进程通信的注意事项

3.1、客户端与服务端aidl文件以及包名必须一致,否则无法正常通信。

3.2、在绑定服务端Service的时候,intent最好设置目标Service所在的包名,如intent.setPackage("com.silencezwm.ipcdemo"),当SDK版本大于14的时候,你会碰到这个错误 java.lang.IllegalArgumentException: Service Intent must be explicit:。

3.3、跨进程传递实体类必须进行序列化,不信你试试看。

3.4、Parcel所占用的内存,会随着你传递的数据量大小而相应变化。

好啦,本篇“Android跨进程通信”的相关介绍就到这里了,感谢你的到来!




About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK