Android 中的 IPC方式

1.Android中的多进程模式:

在android中最有特色的进程通讯方式就是Binder了,通过Binder可以轻松的实现进程间通讯,除了Binder,还有Socket,通过Socket也可以实现任意两个进程之间的通讯,当然同一个设备的两个进程通过Socket也是可以通讯的。通过四大组件的addroid:process属性,就可以开启多线程模式,但是却会有很多其他问题,下面会说到。

1.1开启多线程模式

在Android 中使用多进程只有一个方法,那就是在AndroidMenifest中指定android:process属性,除此之外没有任何其他方法,也就是说我们无法给一个线程或一个实体类指定其运行时的进程。其实还有一个非常规的方法,那就是通过JNI在native层去fork一个新的进程。但是不用作考虑这种特殊情况。

下面演示如何使用:


    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER"

                    />
            </intent-filter>
        </activity>
        <activity android:name=".SecondActivity" android:process=":processB"/>
        <activity android:name=".ThirdActivity" android:process="jian.com.ipctest.remote"/>
    </application>

上面的SecondActivity和ThirdActivity指定了process属性。并且他们属性不同,这意味着当前应用又增加了两个新进程。
我们尝试从MainActivity调到SecondActivity中,然后到ThirdActivity。
在命令行中输入:shell ps |grep 你的包名可得:


2017-11-06 21-29-01屏幕截图.png

可以看到我们成功的启用了多个进程。
在SecondActivity和ThirdActivity中的android:process属性一个有带包名一个没带。":"的含义是在当前的进程名称加上当前的包名,这个简单的写法。它是属于当前进程的私有进程。其他应用不可以和他跑在同一个进程中,而进程名不“:”开头的进程属于全局进程,其他应用通过ShareUID方式可以和他跑在同一个进程中。

1.2多进程模式运行机制

1.2.1多进程中的问题
新建一个UserManager。

public class UserManager {
    public static int mUserId = 1;
}

然后在上面的MainActivity中将其赋值为2,并打印,然后跳到SecondActivity中再次打印,正常情况下两个都应该是2才对。我们看结果:


2017-11-06 21-57-02屏幕截图.png

并没有像我们在以前在用一个进程中使用的那样。所以多进程并不是在android:process中设置属性那么简单。
那么为什么会出现这种情况呢?原因是SecondActivity运行在了一个独立的进程中。android为每一个应用分配了一个独立的虚拟机,或者说为每个进程都分配一个独立的虚拟机。不同的虚拟机在内存中有不同的地址空间,这样的话,不同的虚拟机访问同一个类的对象的时候就会产生多份副本。也就是说。这两个进程都存在一个UserManger类,并且互补干扰,在同一个进程中修改mUserId只会影响当前进程,对其他进程没有任何的影响的。这就是为什么SecondActivity中的mUserId依然还是1的原因。

使用多进程的问题:

  1. 静态成员和单例模式完全失效。
  2. 线程同步机制完全失效
  3. SharePreference的可靠性降低
  4. Application会多次创建

2Andorid中IPC方式

2.1Bundle

Activity ,Service,Receiver 都是支持Intent 中传递Bundle数据的,由于Bundle实现了Parcelable接口,所以它可以方便地在不o同广东进程间传输。基于这一点,当我们在一个进程中启动了另一个进程的Activity,Service或Receiver,我们就可以在Bundle中附加我们需要传输给远程进程的信息并通过Intent发送出去。
能被传数据类型必须要是基本类型或实现了Parcelable接口的对象,实现了Serializable接口的对象。

下面作为测试的例子。因为方便直接在一个model中使用。设置不同的进程可以在AndroidManifest.xml中使用andorid :process=""标签
如:

    <activity android:name=".MainActivity" android:process=":processA">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER"

                    />
            </intent-filter>
        </activity>
        <activity android:name=".ActivityB" android:process=":processB"/>

MainActivity在进程A中,ActivityB在进程B中

MainActivity.java


public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this,ActivityB.class);
                Bundle  bundle = new Bundle();
                bundle.putInt("id",10);
                intent.putExtra(ActivityB.BUNDLE_TAG,bundle);
                startActivity(intent);
            }
        });

    }
}

ActivityB.java

public class ActivityB extends AppCompatActivity {
        public static final String BUNDLE_TAG = "BUNDLE";
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activityb);
            Intent intent =getIntent();
            Bundle bundle = intent.getBundleExtra(BUNDLE_TAG);
            int id = bundle.getInt("id");
            Toast.makeText(ActivityB.this,"id="+id,Toast.LENGTH_LONG).show();
        }


}

运行结果


x.gif

可以看到是可以成功的传输过去,和在同一进程没啥分别。

2.2使用文件共享

在Andorid中,使用并发读/写文件可以没有限制的进行,甚至两个线程同时对同一个文件进行写操作都是可以的,尽管这可能出现问题。通过文件交换数据很好用。除了可以交换一些文本信息。还可以序列化一个对象文件系统中的同事从另一个进程中恢复这个对象,下面就展示这个使用方法。

我们新建一个User类:

public class User implements Serializable
{
    private int id;
    private String info;
    private boolean isLogin;

    public User(int id, String info, boolean isLogin) {
        this.id = id;
        this.info = info;
        this.isLogin = isLogin;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", info='" + info + '\'' +
                ", isLogin=" + isLogin +
                '}';
    }
}

在Activity中执行以下方法

private void persistToFile(){
        PATH =getFilesDir().toString();
        new Thread(new Runnable() {
            @Override
            public void run() {
                User user = new User(1,"hello",false);
                File dir = new File(PATH,"User");
                if(!dir.exists()){
                    dir.mkdirs();
                }
                File cacheFile = new File(PATH,CACHPATH);
                ObjectOutputStream objectOutputStream  = null;
                try {
                    objectOutputStream = new ObjectOutputStream(new FileOutputStream(cacheFile));
                    objectOutputStream.writeObject(user);
                    Log.d("MainActivity---User","persist user:"+user);
                } catch (IOException e) {
                    e.printStackTrace();
                }finally {
                    try {
                        objectOutputStream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }


            }
        }).start();
    }

在SecondActivity中执行以下方法:

   private void recoverFromFile(){
        new Thread(new Runnable() {
            @Override
            public void run() {
                User user = null;

                File cacheFile = new File(getFilesDir().toString(),CACHPATH);
                if(cacheFile.exists()) {

                    ObjectInputStream objectInputStream = null;
                    try {
                        objectInputStream = new ObjectInputStream(new FileInputStream(cacheFile));
                        user = (User) objectInputStream.readObject();
                        Log.d("SecondActivity---User", "persist user:" + user);
                    } catch (IOException e) {
                        e.printStackTrace();
                    } catch (ClassNotFoundException e) {
                        e.printStackTrace();
                    } finally {
                        try {
                            objectInputStream.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }

                }
            }
        }).start();
    }

,然后查看运行的结果:


2017-11-06 23-50-26屏幕截图.png

我们可以看到上面成功的恢复了之前保存的User对象中的内容,说是内容是因为得到的对象只是在内容上和序列化前的是一样的,但本质上是两个不同的对象。
tip:文件共享的方式适合用于数据同步要求不高的进程之间进行通讯。并且要妥善处理并发读/写的问题。

关于SharedPreferences是个特例,众所周知,SharedPreferences是Android中提供的轻量级的存储方案,底层用XML文件来存储键值对。本质来说sharePreferences也属于文件的一种,但是由于系统对他的读/写有一定的缓存策略,即使在内存中有一份SharedPreferences文件的缓存,因此在多进程模式下,系统对它的读/写就变得不可靠了。当面对文件并发的读/写会有很大的可能丢失数据,因此不建议在多进程通讯中使用SharePreferences。

2.3使用Messenger

Messenger是一个轻量级的IPC方案,它的底层实现是AIDL我们来看一下Messenger的构造方法就知道了.明显的可以看到有AIDL的痕迹在这里。

public Messenger(Handler target){
    mTarget =target.getIMessenger();
}

public Messenger(IBinder target){
    mTarget = IMessenger.Stub.asInterface(target);
} 

由于它每一次处理一个请求,因此在服务端不需要考虑多线程的同步的问题。因为服务端不存在并发执行的情形。实现一个Messenger的步骤分为服务端和客服端。

  1. 服务端进程
    首先,我们需要在服务器端创建一个Service来处理客户端的连接请求,同时创建一个Handler并通过它来创建一个Messenger对象,然后在Service的onBind中班会这个Messenger对象即可。

  2. 客户端进程
    客户端进程首先要先绑定服务端的Service,绑定成功后用服务端返回的IBind对象创建一个Messenger,通过这个Messenger就可以向服务器发送消息了,发送消息类型为Messenger对象。如果需要服务端能回应客户端,就和服务端一样,我们还需要创建一个Handler并创建一个新的Messenger,并把这个messenger对象通过replyTo参数传递给服务端,服务端通过这个参数就可以回应客服端。下面的例子说明了这个。
    创建MessengerService.java
    用MessengerHandler来处理客户端发送的消息,并返回一个消息给客户端。Messenger的作用是将客户端发送的消息传递给MessengerHandler处理

public class MessengerService extends Service {
    private static final String TAG = "MessengerService";


    private static class MessengerHandler extends Handler{
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what){
                case 1:
                    Log.i(TAG,"接收到消息从客户端:"+msg.getData().getString("msg"));
                    //回复消息给客户端
                    Messenger client = msg.replyTo;

                    Message replyMessage = Message.obtain(null,2);

                    Bundle bundle = new Bundle();
                    bundle.putString("reply","我收到了你的消息了,稍后给你信息");
                    replyMessage.setData(bundle);

                    try {
                        client.send(replyMessage);
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                    break;
                default:
                    super.handleMessage(msg);

            }
        }
    }
    private final Messenger mMessenger = new Messenger(new MessengerHandler());


    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        return mMessenger.getBinder();
    }


}

然后注册service,让它独立运行在一个进程中。


        <service
            android:name=".service.MessengerService"
            android:process=":remote" />

客户端的实现

public class MessengerActivity extends AppCompatActivity {
    private static final String TAG  = "MessengerActivity";
    private Messenger mService;
    private ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
            mService =new Messenger(iBinder);
            Message msg = Message.obtain(null,1);
            Bundle data = new Bundle();
            data.putString("msg","hello jian,I am client");
            msg.setData(data);
            //这个将需要接收服务端回复的Messenger通过Message的replyTo参数传递给服务端
            msg.replyTo = mGetReplyMessager;
            try{
                //通过Messenger发送消息
                mService.send(msg);
            }catch (RemoteException e){
                e.printStackTrace();
            }

        }

        @Override
        public void onServiceDisconnected(ComponentName componentName) {

        }
    };
    private Messenger mGetReplyMessager = new Messenger(new MessengerHandler());
    //  处理服务端返回的消息
    private static class MessengerHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what){
                case 2:
                    Log.i(TAG,"接收服务端的消息:"+msg.getData().getString("reply"));
                    break;
                default:
                    super.handleMessage(msg);

            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_messenger);
        findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                //绑定Service
                Intent intent = new Intent(MessengerActivity.this, MessengerService.class);
                bindService(intent,connection, Context.BIND_AUTO_CREATE);
            }
        });


    }

    @Override
    protected void onDestroy() {
        unbindService(connection);
        super.onDestroy();

    }
}

预期的结果是点击按钮,服务端接收到消息,然后客户端接收到服务端返回的消息

mess.gif

上面可以看出,在Messenger中进行数据传递必须将数据放入Message中,而Message都实现了Parcelable接口,因此可以跨进程传输。这里用到Bundle,bundle可以支持大量的数据类型。有一个很关键的是,如果需要接收服务端返回的参数,那么就需要设置客户端Message的replyTo参数,也就是一个Handler。(可以理解错传递了一个对象国服务端,然后服务端调用这个对象里面的方法)

Messenger工作原理

2017-11-07 09-25-07屏幕截图.png

未完待续
纪录于andorid开发艺术探索。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,099评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,828评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,540评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,848评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,971评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,132评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,193评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,934评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,376评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,687评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,846评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,537评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,175评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,887评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,134评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,674评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,741评论 2 351

推荐阅读更多精彩内容