Android IPC入门

一、Android IPC简介

IPC是Inter-Process Communication的缩写,含义就是进程间通信或者跨进程通信,是指两个进程之间进行数据交换的过程。那么什么是进程,什么是线程,进程和线程是两个截然不同的概念。在操作系统中,线程是CPU调度的最小单元,同时线程是一种有限的系统资源。而进程指的一个执行单元,在PC和移动设备上指的是一个程序或者一个应用。一个进程可以包含多个线程,因此进程和线程是包含被包含的关系,最简单情况下,一个进程可以只有一个线程,即主线程,在Android里面也叫UI线程,在UI线程里才能操作界面元素。

那么在Android中,有特色的进程间通信方式就是Binder了,通过Binder可以轻松实现进程间通信。除了Binder,Android还支持Socket,通过Socket也可以实现任意两个终端之间的通信,当然一个设备上的两个进程之间通过Socket通信自然也是可以的。

说到IPC的使用场景就必须提到多进程,只有面对多进程这种场景下,才需要考虑进程间通信。所有运行在不同进程中的四大组件,只要它们之间需要通过内存来共享数据,都会共享失败,这也是多进程所带来的主要影响。正常情况下,四大组件中间不可能不通过一些中间层来共享数据,那么通过简单地指定进程名来开启多进程都会无法正确运行。一般来说,使用多进程会造成如下几方面的问题:

  • 静态成员和单例模式完全失效
  • 线程同步机制完全失效
  • SharedPreferences的可靠性下降
  • Application会多次创建

二、Android中的多进程模式

1.开启多进程模式

Android中一个应用可以存在多个进程,在Android中组件使用多进程的方式只有一种方法,设置AndroidMenifiest里的android:process属性。

还有一种非常规方法,通过JNI在native层去fork一个新的进程。

<activity
            android:name=".SecondActivity"
            android:configChanges="screenLayout"
            android:label="@string/app_name"
            android:process=":remote" />
<activity
            android:name=".ThirdActivity"
            android:configChanges="screenLayout"
            android:label="@string/app_name"
            android:process="com.ryg.chapter_2.remote" />

进程分全局进程和私有进程:

  • 全局进程:默认包名,无 “:”分割,可以通过ShareUID跑在一起,具有相同的UID可以共享数据
  • 私有进程:有“:”分割,其他应用组件不能跑在同一进程里。

Android会为每一个应用分配唯一的UID,相同的UID才能共享数据,两个应用通过ShareUID跑在同一个进程是有要求的,需要ShareUID并且签名相同才可以,并且可以访问对方私有数据,比如data目录、组件信息、甚至是内存数据等。

2.多进程模式的运行机制

Android从细处说可以是为每个进程分配了一个虚拟机,每个虚拟机中都保留这一份副本。

一般来说,使用多进程会有以下几个问题

  • 静态成员和单例模式完全失效(处于不同的内存块(进程),拥有各自的副本)
  • 线程同步机制完全失效(同一差不多)
  • SharedPreferences的可靠性降低:因为SharedPreferences不支持两个进程同时去读写xml文件
  • Application会多次创建:开启一个进程其实就等同于开多一个Application

总结:同一应用不同组件运行在不同的进程里,会拥有独立的虚拟机、Application以及内存空间,虽然是同一个应用,但可以看成两个不同应用采用了SharedUID的模式进行数据的共享。


三、IPC基础概念介绍

主要包含三个方面的内容:Serializable接口、Parcelable接口以及Binder。

Serializable接口、Parcelable接口可以完成对象序列化过程,我们使用Intent和Binder传输数据的时候就要使用序列化数据,同时需要将对象持久化存储到存储设备上或者通过网络传输给客户端,也需要序列化。

1. Serializable接口

Serializable是一个序列化接口,为对象提供标准的序列化和反序列化操作。使用只要在类声明中指定一个标志就可实现默认化的序列化程序

public class User implements Parcelable, Serializable {
    
     private static final long serialVersionUID = 519067123721295773L;
}
//序列化的过程

User user = new User(0,"jake",true);
ObjectOutputStream out = new ObjectOutputStream(
    new FileOutputStream("cache.txt");
out.writeObject(user);
out.close();


//反序列化过程

ObjectInputStream in = new ObjectInputStream(
    new FileInputStream(cache.txt));
User newUser = (User) in.readObject();
in.close();


serialVersionUID的详细工作机制:
serialVersionUID是一串数字,可有可无,它相当于身份标识的作用。序列化的时候系统会把当前的类的serialVersionUID写入到序列化文件中,当反序列化的时候会去检查文件中的serialVersionUID,看它是否和当前类的一致,如果一致就证明反序列化中的版本同当前类的版本是相同的,可以进行反序列化,如果不相同,则证明当前类发生了某些变化,比如成员数量类型等变化,这个时候就反序列化不成功,就会报错。

一般来说,我们应该指定serialVersionUID的值,也可以通过当前类的结构去自动生成它的hash值。指定serialVersionUID的值,这样两者在序列化和反序列化的serialVersionUID是相同的。如果不指定,当序列化类的某些变量改变后,系统会生成新的hash值给serialVersionUID,从而导致与反序列化中的不一致,导致反序列化不成功。一般程序要做到的是尽可能的恢复数据。

使用Serializable接口的方法:

  • bean类继承该接口
  • 然后使用ObjectInputStream/OutputStream就正常的读写文件,系统自动实现序列化

serialVersionUID:是序列化的标志,相同可以发生反序列化,不同则不能发生反序列化

两点注意:

  • 静态成员属于类不属于对象,无法序列化
  • transient关键字的变量不参与序列化

2.Parcelable接口

 public int userId;
    public String userName;
    public boolean isMale;

    public Book book;

    public User() {
    }

    public User(int userId, String userName, boolean isMale) {
        this.userId = userId;
        this.userName = userName;
        this.isMale = isMale;
    }

    public int describeContents() {
        return 0;
    }

    public void writeToParcel(Parcel out, int flags) {
        out.writeInt(userId);
        out.writeString(userName);
        out.writeInt(isMale ? 1 : 0);
        out.writeParcelable(book, 0);
    }

    public static final Parcelable.Creator<User> CREATOR = new Parcelable.Creator<User>() {
        public User createFromParcel(Parcel in) {
            return new User(in);
        }

        public User[] newArray(int size) {
            return new User[size];
        }
    };

    private User(Parcel in) {
        userId = in.readInt();
        userName = in.readString();
        isMale = in.readInt() == 1;
        book = in
                .readParcelable(Thread.currentThread().getContextClassLoader());
    }

    @Override
    public String toString() {
        return String.format(
                "User:{userId:%s, userName:%s, isMale:%s}, with child:{%s}",
                userId, userName, isMale, book);
    }

我们来描述一下这个类,Parcel内部包装了可序列化的数据,在序列化的过程中需要实现的功能有序列化、反序列化和内容描述。序列化功能由writeToParcel来实现;反序列化由CREATOR来完成,其内部会返回一个序列化对象和数组,并通过Parcel的一些列read方法来完成反序列化的过程;内容描述符由describeContents利来实现,几乎都是返回0,除非存在文件描述符,则返回1;另外一点,book是一个可序列化对象,它的反序列化需要传递当前线程的上下文加载器。

方法 功能 标记位
createFromParcel(Parcel in) 从序列化后的对象中创建原始对象
newArray(int size) 创建指定长度的原始对象数组
User(Parcel in) 从序列化后的对象中创建原始对象
writeToParcel(Parcel out,int flags) 当前对象写入序列化结构中,一般情况下flag为0 PARCELABLE_WRITE_RETURN_VALUE
describeContents 有文件描述符的时候需要返回1,没有的时候返回0 CONTENTS_FILE_DESCRIPTOR

系统已经为我们提供了许多实现了Parcelable的类了,都可以直接序列化,比如Intent、Bundle、Bitamp、Map、List等,前提是他们内部元素也是可以序列化才行。

Serializable是Java中的序列化接口,其使用起来简单但是开销很大,在序列化和反序列化过程中需要大量的I/O操作。而Parcelable是Android中的序列化方式,因此更适合用在Android平台上,它的缺点就是使用起来稍微麻烦点,但是它的效率很高。

3.Binder

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

Binder是一个跨进程通信的方式,可以用于Android的通信中,主要用于Service,包括AIDL和Messenger,其中普通的Service中的Binder不涉及进程间的通信,不触及Binder的核心;而Messenger的底层是AIDL,而AIDL是通过Binder来实现,我们通过AIDL来分析Binder。

我们借助Android开发艺术中的demo来理解Binder:

首先我们新建Book.java、Book.aidl、IBookManager.aidl三个文件

public class Book implements Parcelable {

    public int bookId;
    public String bookName;

    public Book() {

    }

    public Book(int bookId, String bookName) {
        this.bookId = bookId;
        this.bookName = bookName;
    }

    public int describeContents() {
        return 0;
    }

    public void writeToParcel(Parcel out, int flags) {
        out.writeInt(bookId);
        out.writeString(bookName);
    }

    public static final Parcelable.Creator<Book> CREATOR = new Parcelable.Creator<Book>() {
        public Book createFromParcel(Parcel in) {
            return new Book(in);
        }

        public Book[] newArray(int size) {
            return new Book[size];
        }
    };

    private Book(Parcel in) {
        bookId = in.readInt();
        bookName = in.readString();
    }

    @Override
    public String toString() {
        return String.format("[bookId:%s, bookName:%s]", bookId, bookName);
    }

}
package com.ryg.chapter_2.aidl;

parcelable Book;

import com.ryg.chapter_2.aidl.Book;


interface IBookManager {
     List<Book> getBookList();
     void addBook(in Book book);
}

上面这三个文件,Book.java代表图书信息的类,实现了Parcelable接口,Book.aidl是Book类在AIDL中的声明。IBookManager.aidl是我们定义的一个接口,理由有两个方法。我们可以看到,虽然Book和IBookManager位于同一个包内,但是用到的时候还需要直接导入。现在我们来看看IBookManager.aidl产生的Binder类,我们找到他的.java文件,如下:

/*
 * This file is auto-generated.  DO NOT MODIFY.
 * Original file: D:\\LDProject\\Chapter_2\\app\\src\\main\\aidl\\com\\ryg\\chapter_2\\aidl\\IBookManager.aidl
 */
package com.ryg.chapter_2.aidl;
public interface IBookManager extends android.os.IInterface
{
/** Local-side IPC implementation stub class. */
public static abstract class Stub extends android.os.Binder implements com.ryg.chapter_2.aidl.IBookManager
{
private static final java.lang.String DESCRIPTOR = "com.ryg.chapter_2.aidl.IBookManager";
/** Construct the stub at attach it to the interface. */
public Stub()
{
this.attachInterface(this, DESCRIPTOR);
}
/**
 * Cast an IBinder object into an com.ryg.chapter_2.aidl.IBookManager interface,
 * generating a proxy if needed.
 */
public static com.ryg.chapter_2.aidl.IBookManager asInterface(android.os.IBinder obj)
{
if ((obj==null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin!=null)&&(iin instanceof com.ryg.chapter_2.aidl.IBookManager))) {
return ((com.ryg.chapter_2.aidl.IBookManager)iin);
}
return new com.ryg.chapter_2.aidl.IBookManager.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_getBookList:
{
data.enforceInterface(DESCRIPTOR);
java.util.List<com.ryg.chapter_2.aidl.Book> _result = this.getBookList();
reply.writeNoException();
reply.writeTypedList(_result);
return true;
}
case TRANSACTION_addBook:
{
data.enforceInterface(DESCRIPTOR);
com.ryg.chapter_2.aidl.Book _arg0;
if ((0!=data.readInt())) {
_arg0 = com.ryg.chapter_2.aidl.Book.CREATOR.createFromParcel(data);
}
else {
_arg0 = null;
}
this.addBook(_arg0);
reply.writeNoException();
return true;
}
}
return super.onTransact(code, data, reply, flags);
}
private static class Proxy implements com.ryg.chapter_2.aidl.IBookManager
{
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 java.util.List<com.ryg.chapter_2.aidl.Book> getBookList() throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
java.util.List<com.ryg.chapter_2.aidl.Book> _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
mRemote.transact(Stub.TRANSACTION_getBookList, _data, _reply, 0);
_reply.readException();
_result = _reply.createTypedArrayList(com.ryg.chapter_2.aidl.Book.CREATOR);
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
@Override public void addBook(com.ryg.chapter_2.aidl.Book book) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
if ((book!=null)) {
_data.writeInt(1);
book.writeToParcel(_data, 0);
}
else {
_data.writeInt(0);
}
mRemote.transact(Stub.TRANSACTION_addBook, _data, _reply, 0);
_reply.readException();
}
finally {
_reply.recycle();
_data.recycle();
}
}

finally {
_reply.recycle();
_data.recycle();
}
}
}
static final int TRANSACTION_getBookList = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
static final int TRANSACTION_addBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);

}
public java.util.List<com.ryg.chapter_2.aidl.Book> getBookList() throws android.os.RemoteException;
public void addBook(com.ryg.chapter_2.aidl.Book book) throws android.os.RemoteException;

}

这个类看起来很混乱,但其实逻辑很清晰,首先先声明了getBookList和addBook,这是我们在IBookManager.aidl中声明的方法。同时还声明了两个整型的id标志两个方法用来区分到底是请求了谁(服务器或客户端),接着声明了一个内部类Stub,相当于Binder类,当两者位于同一个进程,方法调用不会使用transact过程,当两者处于不同的进程,方法调用transact过程,这个逻辑由Stub的内部代理Proxy完成。

从这个类我们可以看出,这个接口的核心是它的内部类Stub和Stub的内部代理类,下面我们介绍下每个方法的含义:

  • DESCRIPTOR:Binder的唯一标志,一般是用当前Binder的类名表示,比如本例中的“com.ryg.chapter_2.aidl.IBookManager”
  • asInterface(android.os.IBinder obj):用于将服务端的Binder对象转换为客户端所需的AIDL接口类型的对象,这种转换区分进程,若客户端和服务器同一进程,返回的就是服务端的Stub对象本身,否则是系统封装好的Stub.proxy对象
  • asBinder:返回当前Binder对象
  • onTransact:这个方法运行在服务端的Binder线程池中,当客户端发起跨进程通信时会交由该方法,该方法的原型为public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags)。服务端通过code可以确定客户端请求的目标方法是什么,接着从data中取出目标方法所需的参数,然后执行目标方法。当目标方法执行完毕,就向reply中写入返回值。如果此方法返回false,则表示请求失败。
  • Proxy#getBookList:这个方法运行在客户端,当客户端远程调用此方法时:首先创建该方法需要的输入型Parcel对象_data、输出型Parcel对象_reply和返回值对象List,然后把该方法的参数信息写入到_data;接着调用transact方法来发远程过程调用请求,同时当前线程挂起;然后服务端的onTransact方法会被调用直到道RPC过程返回,当前线程继续执行,并从_reply中取出返回的结果,返回_reply中的数据。
  • Proxy#addBook:这个方法执行过程和getBookList方法调用过程类似,只是没有返回值。

总结一下:首先,当客户端发起远程请求的时候,由于当前线程会被挂起直至服务端进程返回数据,如果远程方法是比较耗时的,那么不能在UI线程中发起远程请求;其次,由于服务端的Binder方法运行在Binder线程池中,所以Binder方法不管是否耗时都应该采用同步的方式去实现。下面是Binder的工作机制图:

image

四、Android中的IPC方式

1.使用Bundle

我们知道,四大组件中三大组件(activity、service、receiver)都是支持在Intent中传递Bundle数据的,由于Bundle实现了Parcelable接口,所以它可以方便地在不同的进程间传输。

Bundle实现的是Parcelable接口,可以在不同进程间传输数据,把传输的数据放到Bundle,再使用Intent去启动目标组件,从而可以实现跨进程通信。

2.使用文件共享

共享文件也是一种不错的进程间通信方式,两个进程间通过读/写同一个文件来交换数据,比如A进程把数据写入文件,B进程通过读取这个文件来获取数据。Android是基于Linux系统的,所以并发读/写任务没有太多限制,甚至可以在两个线程中对同一个文件进行读写操作。

这次我们在MainActivity的onResume中去序列化一个User到sd卡中,然后再SecondActivity中的onResume中去反序列化,恢复User的对象的值,两个Activity是在不同的进程中的。

代码示例:

//在MainActivity中的修改
 @Override
    protected void onResume() {
        Log.d(TAG, "UserManage.sUserId=" + UserManager.sUserId);
        persistToFile();

        super.onStart();
    }

    private void persistToFile() {
        new Thread(new Runnable() {

            @Override
            public void run() {
                User user = new User(1, "hello world", false);
                File dir = new File(MyConstants.CHAPTER_2_PATH);
                if (!dir.exists()) {
                    dir.mkdirs();
                }
                File cachedFile = new File(MyConstants.CACHE_FILE_PATH);
                ObjectOutputStream objectOutputStream = null;
                try {
                    objectOutputStream = new ObjectOutputStream(
                            new FileOutputStream(cachedFile));
                    objectOutputStream.writeObject(user);
                    Log.d(TAG, "persist user:" + user);
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    MyUtils.close(objectOutputStream);
                }
            }
        }).start();
    }
    
    //SecondActivity中的修改
    
     @Override
    protected void onResume() {
        super.onResume();
        User user = (User) getIntent().getSerializableExtra("extra_user");
        Log.d(TAG, "user:" + user.toString());
        // Log.d(TAG, "UserManage.sUserId=" + UserManager.sUserId);
        recoverFromFile();
    }

    private void recoverFromFile() {
        new Thread(new Runnable() {

            @Override
            public void run() {
                User user = null;
                File cachedFile = new File(MyConstants.CACHE_FILE_PATH);
                if (cachedFile.exists()) {
                    ObjectInputStream objectInputStream = null;
                    try {
                        objectInputStream = new ObjectInputStream(
                                new FileInputStream(cachedFile));
                        user = (User) objectInputStream.readObject();
                        Log.d(TAG, "recover user:" + user);
                    } catch (IOException e) {
                        e.printStackTrace();
                    } catch (ClassNotFoundException e) {
                        e.printStackTrace();
                    } finally {
                        MyUtils.close(objectInputStream);
                    }
                }
            }
        }).start();
    }
    
07-12 02:02:34.769 6940-6979/com.ryg.chapter_2 D/MainActivity: persist user:User:{userId:1, userName:hello world, isMale:false}, with child:{null}
07-12 02:03:35.413 6962-6962/com.ryg.chapter_2:remote D/SecondActivity: user:User:{userId:0, userName:jake, isMale:true}, with child:{[bookId:0, bookName:null]}

通过文件共享数据对文件格式是没有具体的要求,可以是文本或者xml文件,只要双方约定好数据格式就可以。但文件共享方式是由局限性的,可能存在读写不同步,所以适用在读写数据同步要求不高的前提下。

一般不建议是用SharePreferences读写,因为系统对它的读写有一定的缓存策略,即在内存中会有一份SharedPreferences文件的缓存,在多进程模式下,系统对它的读写就变得不可靠。

3.使用Messenger

Messenger可以翻译为信使,可以在不同进程中传递Message对象,在Message中放入我们需要传递的数据,就可以实现数据的进程间的传递了,底层是通过AIDL实现的。

我们看一下Messenger这个类的构造方法,不管是IMessenger还是Stub.asInterface,这种使用方法都表明底层使用了AIDL

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

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

Messenger对AIDL做了封装,使得我们可以更简便地进行进程间的通信。同时由于一次处理一个请求,因此服务端我们不用考虑线程同步的问题。实现一个Messenger有多个步骤,分为服务端和客户端:

  • 服务端:创建一个Service来处理客户端的连接请求,同时创建一个Handle并通过它来创建一个Messenger对象,然后再Service的onBind中返回这个Messenger对象的底层Binder即可。
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 MyConstants.MSG_FROM_CLIENT:
                Log.i(TAG, "receive msg from Client:" + msg.getData().getString("msg"));
                Messenger client = msg.replyTo;
                Message relpyMessage = Message.obtain(null, MyConstants.MSG_FROM_SERVICE);
                Bundle bundle = new Bundle();
                bundle.putString("reply", "嗯,你的消息我已经收到,稍后会回复你。");
                relpyMessage.setData(bundle);
                try {
                    client.send(relpyMessage);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
                break;
            default:
                super.handleMessage(msg);
            }
        }
    }

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

    @Override
    public IBinder onBind(Intent intent) {
        return mMessenger.getBinder();
    }

    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return super.onStartCommand(intent, flags, startId);
    }

}

从服务端的代码中可以看到,MessageHandler用来处理客户端发过来的消息,并从客户端中取出文本信息,而mMessenger是和客户端关联在一起的,在onBind方法中返回它里面的Binder对象,这里的Messenger的作用是将客户端发送的消息转交给MessengerHandler处理。

  • 客户端:首先绑定服务端的Service,用返回的IBinder创建一个Messenger,通过这个Messenger像服务器发送Message类型的数据,如果需要服务端能够回应客户端,同时还需要创建一个Handle并创建一个新的Messenger,把这个Messenger对象通过Message的replyTo传递给服务端。
public class MessengerActivity extends Activity {

    private static final String TAG = "MessengerActivity";

    private Messenger mService;
    private Messenger mGetReplyMessenger = new Messenger(new MessengerHandler());
    
    private static class MessengerHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case MyConstants.MSG_FROM_SERVICE:
                Log.i(TAG, "receive msg from Service:" + msg.getData().getString("reply"));
                break;
            default:
                super.handleMessage(msg);
            }
        }
    }

    private ServiceConnection mConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className, IBinder service) {
            mService = new Messenger(service);
            Log.d(TAG, "bind service");
            Message msg = Message.obtain(null, MyConstants.MSG_FROM_CLIENT);
            Bundle data = new Bundle();
            data.putString("msg", "hello, this is client.");
            msg.setData(data);
            msg.replyTo = mGetReplyMessenger;
            try {
                mService.send(msg);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        public void onServiceDisconnected(ComponentName className) {
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_messenger);
        Intent intent = new Intent("com.ryg.MessengerService.launch");
        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
    }
    
    @Override
    protected void onDestroy() {
        unbindService(mConnection);
        super.onDestroy();
    }
}

从例子看来,在Messenger中进行数据传递必须是将数据放到Message中,实际上,Messenger来传输Message,Message中能使用的载体有what,arg1,arg2,Bundle以及replyTo。Message的object在同一个进程中的使用是很实用的。

Messenger跨进程通信原理图:

image

4.使用AIDL

通过上一节,我们可以看到Messenger是串行方式来处理客户端发来的消息,如果有大量消息同时发送到客户端,服务器只能一个一个处理,这就有点耗时了。所以我们要使用AIDL来实现跨进程通信的方法调用。

(1)服务端

服务端首先要创建一个Service用来监听客户端的请求连接,然后创建一个AIDL文件,将暴露给客户端的接口在这个AIDL文件中声明,在Service中实现这个接口

(2)客户端

首先绑定好服务端的Service,将服务端返回的Binder对象转成AIDL接口所属的类型,接着调用AIDL中的方法

(3)AIDL接口的创建

我们要先创建一个后缀为AIDL的文件,在里面声明一个接口和两个接口方法

package com.ryg.chapter_2.aidl;

import com.ryg.chapter_2.aidl.Book;

interface IBookManager {
     List<Book> getBookList();
     void addBook(in Book book);
    
}
  • AIDL并不支持所有数据,可以使用的数据类型如下:
    • 基本数据类型(int,long,char,boolean,double等)
    • String和CharSequence
    • List:只支持ArrayList
    • Map:只支持HashMap
    • Parcelable
    • AIDL

AIDL支持如上所有数据类型,但是对于自定义Parcelable和AIDL对象必须显示import进去,同时对于自定义的Parcelable对象,需要同等的为它创建一个.aidl

public class Book implements Parcelable {

    public int bookId;
    public String bookName;

    public Book() {

    }

    public Book(int bookId, String bookName) {
        this.bookId = bookId;
        this.bookName = bookName;
    }

    public int describeContents() {
        return 0;
    }

    public void writeToParcel(Parcel out, int flags) {
        out.writeInt(bookId);
        out.writeString(bookName);
    }

    public static final Parcelable.Creator<Book> CREATOR = new Parcelable.Creator<Book>() {
        public Book createFromParcel(Parcel in) {
            return new Book(in);
        }

        public Book[] newArray(int size) {
            return new Book[size];
        }
    };

    private Book(Parcel in) {
        bookId = in.readInt();
        bookName = in.readString();
    }

    @Override
    public String toString() {
        return String.format("[bookId:%s, bookName:%s]", bookId, bookName);
    }

}
package com.ryg.chapter_2.aidl;

parcelable Book;

AIDL中除了基本数据类型外,其他类型参数必须标明方向:in表示出入参数、out表示出书参数、inout表示输入输出型参数,并且不支持声明静态常量。

(4)远程服务端Service的实现

创建完AIDL接口后,我们就要实现这个接口,先创建一个Service,称为BookManagerService

public class BookManagerService extends Service {

    private static final String TAG = "BMS";

    private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArrayList<Book>();

    private Binder mBinder = new IBookManager.Stub() {

        @Override
        public List<Book> getBookList() throws RemoteException {
            SystemClock.sleep(5000);
            return mBookList;
        }

        @Override
        public void addBook(Book book) throws RemoteException {
            mBookList.add(book);
        }
    };

    @Override
    public void onCreate() {
        super.onCreate();
        mBookList.add(new Book(1, "Android"));
        mBookList.add(new Book(2, "Ios"));
    }

    @Override
    public IBinder onBind(Intent intent) {
        int check = checkCallingOrSelfPermission("com.ryg.chapter_2.permission.ACCESS_BOOK_SERVICE");
        Log.d(TAG, "onbind check=" + check);
        if (check == PackageManager.PERMISSION_DENIED) {
            return null;
        }
        return mBinder;
    }

}

上面是一个服务端Service的典型实现,首先在onCreate方法中初始化信息,然后创建一个Binder对象并返回它,这个对象继承自IBookManager.Stub ,并实现它内部的AIDL方法,我们这里采用了CopyOnWriteArrayList,CopyOnWriteArrayList支持并发读写的ArrayList,AIDL方法是在服务端的Binder线程池中执行的因此多个客户端同时连接的时候存在多个线程访问的情形,所以我们要在AIDL中处理线程同步。。

前面我们提到AIDL中能够使用的List只有ArrayList,其实是支持抽象的List,而List是一个接口,虽然服务端返回的是CopyOnWriteArrayList,但在Binder会按照List的规范去访问数据并形成一个新的ArrayList传递给客户端。

(5)客户端的实现:

客户端首先要绑定远程服务,绑定成功后返回Binder,将Binder对象转为AIDL接口,然后通过这个接口去调用远程服务的方法:

public class BookManagerActivity extends Activity {

    private static final String TAG = "BookManagerActivity";

    private ServiceConnection mConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className, IBinder service) {
            IBookManager bookManager = IBookManager.Stub.asInterface(service);
            try {
                List<Book> list = bookManager.getBookList();
                Log.i(TAG, "query book list, list type:"
                        + list.getClass().getCanonicalName());
                Log.i(TAG, "query book list:" + list.toString());
                bookManager.addBook(newBook);
                Log.i(TAG, "add book:" + newBook);
                List<Book> newList = bookManager.getBookList();
                Log.i(TAG, "query book list:" + newList.toString());
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

    };


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_book_manager);
        Intent intent = new Intent(this, BookManagerService.class);
        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
    }

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

}

绑定成功后,就会通过bookManager去调用远程AIDL中的方法。

现在我们来考虑一种情况,当有新书到,可不可以自动通知用户,这是一种典型的观察者模式。首先我们要定义一个AIDL接口,每个用户都需要实现这个接口并且向图书馆申请新书的提醒功能,当然也可以随时取消这种功能。

package com.ryg.chapter_2.aidl;

import com.ryg.chapter_2.aidl.Book;

interface IOnNewBookArrivedListener {
    void onNewBookArrived(in Book newBook);
}

interface IBookManager {
     List<Book> getBookList();
     void addBook(in Book book);
     void registerListener(IOnNewBookArrivedListener listener);
     void unregisterListener(IOnNewBookArrivedListener listener);
}

接着要修改下Service的实现,主要是Service中IBookManage.Stub的实现:

   private RemoteCallbackList<IOnNewBookArrivedListener> mListenerList = new RemoteCallbackList<IOnNewBookArrivedListener>();
        @Override
        public void registerListener(IOnNewBookArrivedListener listener)
                throws RemoteException {
            mListenerList.register(listener);

            final int N = mListenerList.beginBroadcast();
            mListenerList.finishBroadcast();
            Log.d(TAG, "registerListener, current size:" + N);
        }

        @Override
        public void unregisterListener(IOnNewBookArrivedListener listener)
                throws RemoteException {
            boolean success = mListenerList.unregister(listener);

            if (success) {
                Log.d(TAG, "unregister success.");
            } else {
                Log.d(TAG, "not found, can not unregister.");
            }
            final int N = mListenerList.beginBroadcast();
            mListenerList.finishBroadcast();
            Log.d(TAG, "unregisterListener, current size:" + N);
        };
        
        
    private void onNewBookArrived(Book book) throws RemoteException {
        mBookList.add(book);
        final int N = mListenerList.beginBroadcast();
        for (int i = 0; i < N; i++) {
            IOnNewBookArrivedListener l = mListenerList.getBroadcastItem(i);
            if (l != null) {
                try {
                    l.onNewBookArrived(book);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        }
        mListenerList.finishBroadcast();
    }
    
    private class ServiceWorker implements Runnable {
        @Override
        public void run() {
            // do background processing here.....
            while (!mIsServiceDestoryed.get()) {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                int bookId = mBookList.size() + 1;
                Book newBook = new Book(bookId, "new book#" + bookId);
                try {
                    onNewBookArrived(newBook);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        }
    } 
    
    @Override
    public void onCreate() {
        super.onCreate();
        mBookList.add(new Book(1, "Android"));
        mBookList.add(new Book(2, "Ios"));
        new Thread(new ServiceWorker()).start();
    }    

最后我们修改下客户端的代码,主要有两个方面:首先客户端要注册IOnNewBookArrivedListener到远程服务端,同时我们在Activity退出的时候要解除这个注册。

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case MESSAGE_NEW_BOOK_ARRIVED:
                Log.d(TAG, "receive new book :" + msg.obj);
                break;
            default:
                super.handleMessage(msg);
            }
        }
    };
    private ServiceConnection mConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className, IBinder service) {
            IBookManager bookManager = IBookManager.Stub.asInterface(service);
            mRemoteBookManager = bookManager;
            try {
                mRemoteBookManager.asBinder().linkToDeath(mDeathRecipient, 0);
                List<Book> list = bookManager.getBookList();
                Log.i(TAG, "query book list, list type:"
                        + list.getClass().getCanonicalName());
                Log.i(TAG, "query book list:" + list.toString());
                Book newBook = new Book(3, "Android进阶");
                bookManager.addBook(newBook);
                Log.i(TAG, "add book:" + newBook);
                List<Book> newList = bookManager.getBookList();
                Log.i(TAG, "query book list:" + newList.toString());
                bookManager.registerListener(mOnNewBookArrivedListener);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        public void onServiceDisconnected(ComponentName className) {
            mRemoteBookManager = null;
            Log.d(TAG, "onServiceDisconnected. tname:" + Thread.currentThread().getName());
        }
    };
    private IOnNewBookArrivedListener mOnNewBookArrivedListener = new IOnNewBookArrivedListener.Stub() {

        @Override
        public void onNewBookArrived(Book newBook) throws RemoteException {
            mHandler.obtainMessage(MESSAGE_NEW_BOOK_ARRIVED, newBook)
                    .sendToTarget();
        }
    };
    @Override
    protected void onDestroy() {
        if (mRemoteBookManager != null
                && mRemoteBookManager.asBinder().isBinderAlive()) {
            try {
                Log.i(TAG, "unregister listener:" + mOnNewBookArrivedListener);
                mRemoteBookManager
                        .unregisterListener(mOnNewBookArrivedListener);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
        unbindService(mConnection);
        super.onDestroy();
    }

RemoteCallbackList是系统专门提供的用于删除跨进程listener的接口。它是一个泛型类,支持AIDL接口

public class RemoteCallbackList<E extends IInterface>

它的工作原理很简单,在它的内部有一个Map结构专门来保存所有AIDL回调,这么Map的key是IBinder类型,value是Callback类型,如下所示:

ArrayMap<IBinder,Callback> mCallbacks = new ArrayMap<IBinder,callback>()

其中Callback封装了真正的远程listener,当客户端注册listener的时候,将listener信息存入到mCallbacks,其中key和value通过以下方式获取:

IBinder key = listener.asBinder();
Callback value = new Callback(listener,cookie);

虽然说跨进程通信传输客户端的对象会在服务端中生成不同的对象,但是这些新生成的对象有一个共同点,就是底层的Binder是同一个,当客户端解注的时候只要遍历服务器端的所有listener,找出和解注listener具有相同的Binder并把它删除。

5.使用ContentProvider

ContentProvider是Android中提供的专门用于不同应用间进行数据共享的方式,它的底层实现同样也是Binder。

系统预制了许多ContentProvider,比如通讯记录、日程表信息,要实现跨进程通信,只要通过ContentResolve的query、update、insert、delete方法即可。

我们可以定义一个继承于ContentProvide的子类,实现里面的6个方法:

  • onCrete:在主线程中调用,主要做一些初始化工作
  • getType:返回一个Uri请求对应的MIME类型,如果我们的应用不关注这个选项可以直接返回null
  • 剩下的四个方法对应CRUD操作,主要由外界调用并应用在Binder线程池中。

ContentProvider主要以表格的形式来组织数据,并可以返回多个表,对于每个表格来说具有行和列的层次。除此之外还支持文件数据、比如图片视频等等。

下面是ContentProvider的一个demo:


public class BookProvider extends ContentProvider {

    private static final String TAG = "BookProvider";

    public static final String AUTHORITY = "com.ryg.chapter_2.book.provider";

    public static final Uri BOOK_CONTENT_URI = Uri.parse("content://"
            + AUTHORITY + "/book");
    public static final Uri USER_CONTENT_URI = Uri.parse("content://"
            + AUTHORITY + "/user");

    public static final int BOOK_URI_CODE = 0;
    public static final int USER_URI_CODE = 1;
    private static final UriMatcher sUriMatcher = new UriMatcher(
            UriMatcher.NO_MATCH);

    static {
        sUriMatcher.addURI(AUTHORITY, "book", BOOK_URI_CODE);
        sUriMatcher.addURI(AUTHORITY, "user", USER_URI_CODE);
    }

    private Context mContext;
    private SQLiteDatabase mDb;

    @Override
    public boolean onCreate() {
        Log.d(TAG, "onCreate, current thread:"
                + Thread.currentThread().getName());
        mContext = getContext();
        initProviderData();
        return true;
    }

    private void initProviderData() {
        mDb = new DbOpenHelper(mContext).getWritableDatabase();
        mDb.execSQL("delete from " + DbOpenHelper.BOOK_TABLE_NAME);
        mDb.execSQL("delete from " + DbOpenHelper.USER_TALBE_NAME);
        mDb.execSQL("insert into book values(3,'Android');");
        mDb.execSQL("insert into book values(4,'Ios');");
        mDb.execSQL("insert into book values(5,'Html5');");
        mDb.execSQL("insert into user values(1,'jake',1);");
        mDb.execSQL("insert into user values(2,'jasmine',0);");
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
            String[] selectionArgs, String sortOrder) {
        Log.d(TAG, "query, current thread:" + Thread.currentThread().getName());
        String table = getTableName(uri);
        if (table == null) {
            throw new IllegalArgumentException("Unsupported URI: " + uri);
        }
        return mDb.query(table, projection, selection, selectionArgs, null, null, sortOrder, null);
    }

    @Override
    public String getType(Uri uri) {
        Log.d(TAG, "getType");
        return null;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        Log.d(TAG, "insert");
        String table = getTableName(uri);
        if (table == null) {
            throw new IllegalArgumentException("Unsupported URI: " + uri);
        }
        mDb.insert(table, null, values);
        mContext.getContentResolver().notifyChange(uri, null);
        return uri;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        Log.d(TAG, "delete");
        String table = getTableName(uri);
        if (table == null) {
            throw new IllegalArgumentException("Unsupported URI: " + uri);
        }
        int count = mDb.delete(table, selection, selectionArgs);
        if (count > 0) {
            getContext().getContentResolver().notifyChange(uri, null);
        }
        return count;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection,
            String[] selectionArgs) {
        Log.d(TAG, "update");
        String table = getTableName(uri);
        if (table == null) {
            throw new IllegalArgumentException("Unsupported URI: " + uri);
        }
        int row = mDb.update(table, values, selection, selectionArgs);
        if (row > 0) {
            getContext().getContentResolver().notifyChange(uri, null);
        }
        return row;
    }

    private String getTableName(Uri uri) {
        String tableName = null;
        switch (sUriMatcher.match(uri)) {
        case BOOK_URI_CODE:
            tableName = DbOpenHelper.BOOK_TABLE_NAME;
            break;
        case USER_URI_CODE:
            tableName = DbOpenHelper.USER_TALBE_NAME;
            break;
            default:break;
        }

        return tableName;
    }
}

接着我们需要注册这个BookProvider,其中android:authorities是ContentProvider的唯一标识,通过这个标志可以访问到我们的provider,并且可以给provider加上一个访问权限permission,如果分别声明了读写权限,外界也必须一次声明相应的权限才可以进行读写操作。

        <provider
            android:name=".provider.BookProvider"
            android:authorities="com.ryg.chapter_2.book.provider"
            android:permission="com.ryg.PROVIDER"
            android:process=":provider" >
        </provider>

现在我们给我们的ContentProvider提供一个数据库。我们借助SQLiteOpenHelper来管理数据库的创建、升级和降级。通过ContentProvider的Uri来区分外界要访问哪种数据。当要观察一个ContentProvider中的数据是否改变了,我们可以使用ContentResolve的registerContentObserver方法来注册观察者,通过unregisterContentObserver来解除观察者。

package com.ryg.chapter_2.provider;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

public class DbOpenHelper extends SQLiteOpenHelper {

    private static final String DB_NAME = "book_provider.db";
    public static final String BOOK_TABLE_NAME = "book";
    public static final String USER_TALBE_NAME = "user";

    private static final int DB_VERSION = 3;

    private String CREATE_BOOK_TABLE = "CREATE TABLE IF NOT EXISTS "
            + BOOK_TABLE_NAME + "(_id INTEGER PRIMARY KEY," + "name TEXT)";

    private String CREATE_USER_TABLE = "CREATE TABLE IF NOT EXISTS "
            + USER_TALBE_NAME + "(_id INTEGER PRIMARY KEY," + "name TEXT,"
            + "sex INT)";

    public DbOpenHelper(Context context) {
        super(context, DB_NAME, null, DB_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_BOOK_TABLE);
        db.execSQL(CREATE_USER_TABLE);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // TODO ignored
    }

}


public class ProviderActivity extends Activity {
    private static final String TAG = "ProviderActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_provider);
        // Uri uri = Uri.parse("content://com.ryg.chapter_2.book.provider");
        // getContentResolver().query(uri, null, null, null, null);
        // getContentResolver().query(uri, null, null, null, null);
        // getContentResolver().query(uri, null, null, null, null);

        Uri bookUri = Uri.parse("content://com.ryg.chapter_2.book.provider/book");
        ContentValues values = new ContentValues();
        values.put("_id", 6);
        values.put("name", "程序设计的艺术");
        getContentResolver().insert(bookUri, values);
        Cursor bookCursor = getContentResolver().query(bookUri, new String[]{"_id", "name"}, null, null, null);
        while (bookCursor.moveToNext()) {
            Book book = new Book();
            book.bookId = bookCursor.getInt(0);
            book.bookName = bookCursor.getString(1);
            Log.d(TAG, "query book:" + book.toString());
        }
        bookCursor.close();

        Uri userUri = Uri.parse("content://com.ryg.chapter_2.book.provider/user");
        Cursor userCursor = getContentResolver().query(userUri, new String[]{"_id", "name", "sex"}, null, null, null);
        while (userCursor.moveToNext()) {
            User user = new User();
            user.userId = userCursor.getInt(0);
            user.userName = userCursor.getString(1);
            user.isMale = userCursor.getInt(2) == 1;
            Log.d(TAG, "query user:" + user.toString());
        }
        userCursor.close();
    }
}

6.使用scoket

Socket来实现进程间通信,Socket也成为套接字,是网络通信中的概念,它分为流式套接字和用户数据报套接字两种,分别对应TCP和UDP
Java提供了良好的接口进行通信

使用Socket前要获取权限:

    <uses-permission android:name="com.ryg.PROVIDER" />
    <uses-permission android:name="com.ryg.chapter_2.permission.ACCESS_BOOK_SERVICE" />

其次是不能在主线程中访问网络,不然会产生NetworkOnMainThreadException

在Java中能充当服务器接受请求的类是ServerSocket,它来监听scoket连接。一直处于在线状态。ServerSocket包含一个监听来自客户端连接请求的方法:

  • Socket accept():该方法返回与客户端对应的scoket,否则线程会被阻塞

ServerScoket类有几个构造器:

  • ServerScoket(int port)
  • ServerScoket(int port,int backlog):增加一个用来改变连接队列长度的参数backlog
  • ServerScoket(int port,int backlog,InetAddress localAddr):localAddr用来绑定指定的ip地址

ServerScoket应不断的调用accpet()来响应客户端的所有请求

2.使用Socket进行通信

Scoket提供两个构造器

  • Scoket(InetAddress/String remoteAddress,int port)
  • Scoket(InetAddress/String remoteAddress,int port,InetAddress localAddress,int localPort)

当客户端和服务端都生成了自己的socket之后,且服务端accept()后,两个socket就可以互相通信,Scoket提供两个方法来获取输入流和输出流

  • InputStream getInputStream():从该Socket对象中取出数据
  • OutputStream getOutputStream():返回该Socket的输出流,向该Socket输出数据

对于设定客户端Scoket的连接时长,由于不存在包含此参数的构造器,所以需要通过Scoket的connect()方法设置


五、选择合适的IPC方式

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

推荐阅读更多精彩内容