Android 进程间交互 (IPC) 开发实践

让app多进程运行

应用为了获取更多的内存空间,可以采用多进程的方式.
在AndroidManifest.xml中, 为四大组件指定"android:process"属性实现多进程, 只有这一个方法, 也就是说我们无法给一个实例类或是一个线程指定其运行所在的进程.

<activity
        android:name="com.qihoo360.accounts.core.a.FirstActivity"
        android:configChanges="orientation|keyboardHidden|screenSize|fontScale"
        android:exported="false"
        android:process="com.qihoo360.accounts"
        >
</activity>

<activity
        android:name="com.qihoo360.accounts.SecondActivity"
        android:configChanges="orientation|keyboardHidden|screenSize|fontScale"
        android:exported="false"
        android:process=":remote"
        >
</activity>

以:开始的进程名是当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程中.
以全称设置的进程名是全局进程, 其他应用通过shareUID的方式可以和它跑在同一个进程中,前提是两个应用的签名也必须相同.
系统为每个应用分配唯一的UID,两个应用的组件跑在同一个进程除了要求UID相同,签名也必须相同, 这样两个应用就能共享data目录,共享内存数据等.

使用多进程带来的问题.
  1. 静态成员和单例模式失效, 每个进程都有自己独立的静态成员和单例.
  2. 线程同步机制失效
  3. SharedPreference可靠性下降, 因为两个进程同时对同一份SharedPreference的底层xml文件执行写操作时, 可能会造成数据丢失.
  4. Application多次创建,每个进程有自己独立的虚拟机,有自己独立的Application对象.
实现对象序列化的接口
Serializable接口

java提供的序列化接口,它是一个空接口,使用时CityItem implements Serializable, 并提供一个private static final long serialVersionUID = 1L;值即可, 系统就会自动帮我们对这个类的对象实现序列化和反序列化了.

public interface Serializable {
}

静态成员变量属于类不属于对象, 因此不参与序列化过程, 另外, 用transient标记的变量不参与序列化.
通过ObjectOutputStreamd的writeObject()就可以把一个对象写入一个文件.

LoggingInfo logInfo = new LoggingInfo("MIKE", "MECHANICS");  
System.out.println(logInfo.toString());  
FileOutputStream fos = new FileOutputStream("c:\\logging.obj");  
ObjectOutputStream oos = new ObjectOutputStream(fos);  
oos.writeObject(logInfo);  
oos.close();  

通过ObjectInputStream的readObject()就可以从一个文件中读出一个对象.

FileInputStream fis = new FileInputStream("c:\\logging.obj");  
ObjectInputStream ois = new ObjectInputStream(fis);  
LoggingInfo info = (LoggingInfo) ois.readObject();  
ois.close();  
System.out.println(info); 
Parcelable接口

android提供的序列化方案, 一个类只有实现了Parcelable接口,它的对象才可以通过intent和Binder传递.
需要自己实现3个方法,writeToParcel(Parcel dest, int flags), describeContents(), 内部接口Creator里的createFromParcel(Parcel source). 要序列化的数据就保存在参数Parcel对象中。

 public class MyParcelable implements Parcelable {
     private int mData;

     public int describeContents() {
         return 0;
     }

     public void writeToParcel(Parcel out, int flags) {
         out.writeInt(mData); //通过Parcel的一系列write方法实现序列化
     }

     public static final Parcelable.Creator<MyParcelable> CREATOR
             = new Parcelable.Creator<MyParcelable>() {
         public MyParcelable createFromParcel(Parcel in) {
             return new MyParcelable(in);//内部实现上, 通过Parcel的一系列write方法实现反序列化
         }

         public MyParcelable[] newArray(int size) {
             return new MyParcelable[size];
         }
     };
     
     private MyParcelable(Parcel in) {
         mData = in.readInt();
     }
 }

注意这句话:

Classes implementing the Parcelable interface must also have a non-null static field called "CREATOR" of a type that implements the Parcelable.Creator interface.

难点在于理解CREATOR这个成员变量, 为什么要提供一个名字必须是"CREATOR"的成员变量呢? 改为其他的名字成不成呢?
答案是不行, 因为在反序列化时, Parcel类要获取Parcelable接口实现类中名称为"CREATOR"的这个成员变量,也就是说这个名字是被写死的。

//Parcel.java file
try {
    Class c = loader == null ?
        Class.forName(name) : Class.forName(name, true, loader);
    Field f = c.getField("CREATOR");
    creator = (Parcelable.Creator)f.get(null);
}
Serializable接口和Parcelable接口都能实现序列化,选择哪个?

Parcelable比Serializable效率高, 一般情况下首先Parcelable。
Parcelable主要用在把对象序列化到内存上,虽然通过Parcelable也可以把对象写入存储设备或是把对象序列化后通过网络传输, 但稍显复杂,所以在这两种情况下, 建议通过Serializable实现对象的序列化.

实现跨进程通信的方式
1. 通过Intent使用Bundle对象携带数据

Intent持有一个成员变量, private Bundle mExtras; 而Intent实现了Parcelable接口, 因此Intent的对象可以被跨进程传输.

public class Intent implements Parcelable, Cloneable {
    ...
    private String mAction;
    private Uri mData;
    private ArraySet<String> mCategories;
    private Bundle mExtras;
    ...
}

需要通过intent传递数据时,调用Intent的一系列putExtra(String name, T value)方法,把各种类型的数据存入Bundle对象中.
以T为String为例.

public Intent putExtra(String name, String value) {
if (mExtras == null) {
    mExtras = new Bundle();
}
mExtras.putString(name, value);
return this;
}

而Bundle的内部维护了一个ArrayMap mMap, 以键值对的方式存储着所有的数据.
Bundle.java的定义如下:

/**
 * A mapping from String values to various Parcelable types.
 *
 */
public final class Bundle extends BaseBundle implements Cloneable, Parcelable {
    public static final Bundle EMPTY;
    static final Parcel EMPTY_PARCEL;

    static {
        EMPTY = new Bundle();
        EMPTY.mMap = ArrayMap.EMPTY;
        EMPTY_PARCEL = BaseBundle.EMPTY_PARCEL;
    }
public class BaseBundle {
    ...
    ArrayMap<String, Object> mMap = null;
    ...
    public void putString(String key, String value) {
        unparcel();
        mMap.put(key, value);
    }
    ...
}

被启动的组件接收到intent后, 通过intent把数据取出来, 有两种方式.
一是通过intent.getExtras()把整个Bundle对象取出来, 然后再通过Bundle的一系列getXXX(String key)方法从map中把数据取出来.

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    bundle = getIntent().getExtras();
    weiboType = bundle.getInt(ShareBlogActivity.EXTRA_WEIBO_TYPE);
    weiboPicURI = bundle.getString(ShareBlogActivity.EXTRA_PIC_URI);
    weiboContent = bundle.getString(ShareBlogActivity.EXTRA_WEIBO_CONTENT);
}


二是直接通过一系列intent.getXXXExtras(String key)方法, 直接从Bundle的map中取特定类型的数据.

final String title = intent.getStringExtra("title");
final String filename = intent.getStringExtra("filename");
final long id = intent.getLongExtra("id");


//Intent.java file
    public String getStringExtra(String name) {
        return mExtras == null ? null : mExtras.getString(name);
    }

//BaseBundle.java file
public String getString(String key) {
    unparcel();
    final Object o = mMap.get(key);
    try {
        return (String) o;
    } catch (ClassCastException e) {
        typeWarning(key, o, "String", e);
        return null;
    }
}


总结: Intent有一个成员变量Bundle mExtras存储自己的数据, 而Bundle实际上就是一个ArrayMap, 以键值对的方式保存着各种类型的数据.
why ArrayMap? not use HashMap, 看到ArrayMap的注释有这么一段.
/**

  • ArrayMap is a generic key->value mapping data structure that is
  • designed to be more memory efficient than a traditional java.util.HashMap.
    */
2. 使用文件共享

多个进程对同一份文件进行读写, 典型例子是SharedPreferences, 它通过键值对的方式在xml文件中存储数据, 文件位于/data/data/package_name/shared_prefs目录下. 如果应用是多进程模式的, 不同进程就会读写同一份xml文件,也就实现了跨进程通信. 但由于SharedPreferences有缓存策略, 即在内存中有一份SharedPreferences。因此多进程下容易发生数据丢失, 因此不建议采用SharedPreferences实现跨进程通信.

3. Messager

两个进程间通过互相发送message通信,服务端 mMessenger = new Messenger(mHandler), 客户端使用bindlerService请求连接远程服务端, 服务端返回Messager的binder对象, 客户端onServiceConnected()中用接收到的IBinder对象创建一个客户端的Messager对象, 然后通过它发送消息给服务端即可实现跨进程通信.
public void onServiceConnected(ComponentName name, IBinder service) {
rMessenger = new Messenger(service);     
......
}
http://blog.csdn.net/cs_lht/article/details/6698468
实际项目中, 还没有使用过这种方式, 初步了解一下即可,它只能传递消息, 无法完成跨进程的方法调用, 它的底层也是通过binder实现的。

4. 使用AIDL实现服务端和客户端方法的双向调用

*.aidl接口中只支持方法, 不支持声明静态常量, 这一点区别于传统接口.
*.aidl接口中声明的方法在服务端的binder线程池中执行. 当多个客户端同时连接一个服务端时, 不同客户端调用的aidl方法就在不同的线程中执行, 这些aidl方法中访问的数据就要考虑线程同步的问题, 比如当aidl方法需要访问ArrayList时, 最好是采用CopyOnWriteArrayList,因为它对内部元素的访问实现了线程同步, 这样我们的aidl方法就不用考虑线程同步的问题了, 类似的情形还有ConcurrentHashMap. 另外, 这些因为aidl方法本身就是运行在binder线程池, 所以也没有必要开启新的线程去执行耗时操作.
方法中的参数要标上方向, in, out 或是inout.
服务端和客户端拥有相同的aidl文件, 并且aidl文件所在的包目录也必须一致, 因为客户端需要反序列化服务端中和aidl接口相关的所有的类. 一般的做法是服务端代码中编写所有的aidl文件,并把这些文件放在同一个目录, 然后原封不动的拷贝一份到客户端的代码中.
这一节中有一些需要注意的点, 在下面的代码注释中进行总结.
//code: https://github.com/singwhatiwanna/android-art-res/tree/master/Chapter_2/src/com/ryg/chapter_2/aidl

服务端代码:
第一步, 编写aidl文件.
//Book.aidl file
package com.ryg.chapter_2.aidl;

parcelable Book;

//IBookManager.aidl file, 编译后在gen目录下生成IBookManager.java
package com.ryg.chapter_2.aidl;

import com.ryg.chapter_2.aidl.Book;
import com.ryg.chapter_2.aidl.IOnNewBookArrivedListener;

interface IBookManager {
     List<Book> getBookList();
     void addBook(in Book book);
     void registerListener(IOnNewBookArrivedListener listener);
     void unregisterListener(IOnNewBookArrivedListener listener);
}
//生成的IBookManager.java
public interface IBookManager extends android.os.IInterface
{
/** Local-side IPC implementation stub class. */
public static abstract class Stub extends android.os.Binder implements IBookManager
{
    ...
    ...
}
//最重要的是生成了这个IBookManager.Stub这个内部类, 它继承Binder. 这个Stub就是一个Binder.

生成类的结构
public interface IBookManager extends android.os.IInterface
{
    /** Local-side IPC implementation stub class. */
    public static abstract class Stub extends android.os.Binder implements IBookManager
    {
        ...
        public static IBookManager asInterface(android.os.IBinder obj)
        {
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            return ((IBookManager)iin);
        }
        ...
    }
    public java.util.List<com.qihoo360.mobilesafe.service.Book> getBookList() throws android.os.RemoteException;
    public void addBook(com.qihoo360.mobilesafe.service.Book book) throws android.os.RemoteException;
    public void registerListener(com.qihoo360.mobilesafe.service.IOnNewBookArrivedListener listener) throws android.os.RemoteException;
    public void unregisterListener(com.qihoo360.mobilesafe.service.IOnNewBookArrivedListener listener) throws android.os.RemoteException;
}
// IOnNewBookArrivedListener.aidl file
package com.ryg.chapter_2.aidl;

import com.ryg.chapter_2.aidl.Book;

interface IOnNewBookArrivedListener {
    void onNewBookArrived(in Book newBook);
}
// Book.java
package com.ryg.chapter_2.aidl;

import android.os.Parcel;
import android.os.Parcelable;

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);
    }

}
第二步, 实现服务端,继承Service
package com.ryg.chapter_2.aidl;

//服务端继承Service
public class BookManagerService extends Service {

    private static final String TAG = "BMS";

    private AtomicBoolean mIsServiceDestoryed = new AtomicBoolean(false);

    //不用普通ArrayList的原因是,服务端可能被多个客户端同时绑定, aild方法就被多个binder线程同时执行, 因此要保证线程同步,而CopyOnWriteArrayList已经为我们实现了操作list时的线程同步, 这样调用aidl方法时就不要考虑线程同步的问题了.
    private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArrayList<Book>();
    // private CopyOnWriteArrayList<IOnNewBookArrivedListener> mListenerList =
    // new CopyOnWriteArrayList<IOnNewBookArrivedListener>();

    //保存所有客户端的对象,当图书列表发生变化时,可以遍历这个list,调用客户端的方法.
    //RemoteCallbackList是系统提供的专门用于删除跨进程listener的接口.
    //用RemoteCallbackList,而不用ArrayList的原因是, 客户端的对象注册进来后, 服务端会通过它反序列化出一个新的对象保存一起,所以说已经不是同一个对象了. 在客户端调用解除注册方法时, 在list中根本就找不到它的对象, 也就无法从list中删除客户端的对象. 而RemoteCallbackList的内部保存的是客户端对象底层的binder对象, 这个binder对象在客户端对象和反序列化的新对象中是同一个对象,  RemoteCallbackList的实现原理就是利用的这个特性.
    private RemoteCallbackList<IOnNewBookArrivedListener> mListenerList = new RemoteCallbackList<IOnNewBookArrivedListener>();

    private Binder mBinder = new IBookManager.Stub() {//创建一个生成类IBookManager.Stub内部类的对象, 并在其中对aidl中的方法进行代码实现.

        @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 boolean onTransact(int code, Parcel data, Parcel reply, int flags)
                throws RemoteException {
        //为保护Service不被任意的其他应用绑定, 可以检查客户端是否具有特定的permission.
            //对客户端的permission进行验证, 验证不通过就返回null.
        //需要在服务端的AndroidManifest.xml中, 声明<permission android:name="com.ryg.chapter_2.permission.ACCESS_BOOK_SERVICE"/>
        //检查在客户端的AndroidManifest.xml中,是否使用了<uses-permission android:name="com.ryg.chapter_2.permission.ACCESS_BOOK_SERVICE">标签
            int check = checkCallingOrSelfPermission("com.ryg.chapter_2.permission.ACCESS_BOOK_SERVICE");
            Log.d(TAG, "check=" + check);
            if (check == PackageManager.PERMISSION_DENIED) {
                return false;
            }

        //通过getCallingUid()得到客户端的uid, 再通过PackageManager根据uid查到package name进行检查.
            String packageName = null;
            String[] packages = getPackageManager().getPackagesForUid(
                    getCallingUid());
            if (packages != null && packages.length > 0) {
                packageName = packages[0];
            }
            Log.d(TAG, "onTransact: " + packageName);
            if (!packageName.startsWith("com.ryg")) {
                return false;
            }

            return super.onTransact(code, data, reply, flags);
        }

        @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);
        };

    };

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

    @Override
    public IBinder onBind(Intent intent) {
    //为保护Service不被任意的其他应用绑定, 可以检查客户端是否具有特定的permission.
        //对客户端的permission进行验证, 验证不通过就返回null.
    //需要在服务端的AndroidManifest.xml中, 声明<permission android:name="com.ryg.chapter_2.permission.ACCESS_BOOK_SERVICE"/>
    //检查在客户端的AndroidManifest.xml中,是否使用了<uses-permission android:name="com.ryg.chapter_2.permission.ACCESS_BOOK_SERVICE">标签
    //也可在onTransact()中检查
        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;//在onBind()方法内把生成类IBookManager.Stub内部类的对象返回给客户端,ServiceConnection类对象的onServiceConnected(ComponentName                //className, IBinder service), 把mBinder赋值给IBinder service参数.
    }

    @Override
    public void onDestroy() {
        mIsServiceDestoryed.set(true);
        super.onDestroy();
    }

    private void onNewBookArrived(Book book) throws RemoteException {
        mBookList.add(book);
    //RemoteCallbackList类的beginBroadcast()和finishBroadcast()的配对使用, 完成对List的遍历. 用法比较特殊, 不同于对普通List的遍历.
        final int N = mListenerList.beginBroadcast();
        for (int i = 0; i < N; i++) {
            IOnNewBookArrivedListener l = mListenerList.getBroadcastItem(i);
            if (l != null) {
                try {
                    l.onNewBookArrived(book);//onNewBookArrived()方法在客户端的binder线程池中执行, 当前线程被挂起, 因此如果是耗时操作的话, 注意不要在UI线程执行客户端的aidl方法().
                } 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();
                }
            }
        }
    }

}
客户端代码
//BookManagerActivity.java
package com.ryg.chapter_2.aidl;

public class BookManagerActivity extends Activity {

    private static final String TAG = "BookManagerActivity";
    private static final int MESSAGE_NEW_BOOK_ARRIVED = 1;

    private IBookManager mRemoteBookManager;

    @SuppressLint("HandlerLeak")
    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 IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
        @Override
        public void binderDied() { //服务端进程停止时被binder线程池回调.
            Log.d(TAG, "binder died. tname:" + Thread.currentThread().getName());
            if (mRemoteBookManager == null)
                return;
            mRemoteBookManager.asBinder().unlinkToDeath(mDeathRecipient, 0);
            mRemoteBookManager = null;
            // TODO:这里重新绑定远程Service
        }
    };

    private ServiceConnection mConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className, IBinder service) {
        //绑定成功后, 把参数IBinder service对象转化为生成类IBookManager.java的对象供客户端在后面使用
            IBookManager bookManager = IBookManager.Stub.asInterface(service);
            mRemoteBookManager = bookManager;
            try {
        //为保证程序的健壮性, 使用Binder类的linkToDeath()方法对服务端进程意外停止时进行处理.
        //服务端进程停止时, IBinder.DeathRecipient mDeathRecipient对象的binderDied()方法会被回调.
        //也可选择在onServiceDisconnected()时处理服务端进程意外停止的情况, 区别是binderDied()方法在线程池中执行,而onServiceDisconnected()在UI线程执行, 选择一种处理方式即可.
                mRemoteBookManager.asBinder().linkToDeath(mDeathRecipient, 0);

        //调用服务端的方法, 但要注意onServiceConnected()和onServiceDisconnected()都是在UI线程执行, 当调用远程服务端方法时, 服务端的方法在binder线程池中执行, 客户端会被挂起,因此这里要确保远程服务的方法不是耗时方法, 否则客户端会发生ANR. 不确定是否耗时的情况下, 要开新线程执行,不要直接在onServiceConnected()中调用远程服务端的方法.
                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 {
        //这个方法在客户端的binder线程池中执行, 因此如果方法内要操作UI的话,就要通过Handler对象发送一个消息, 把执行逻辑切换到UI线程.
            mHandler.obtainMessage(MESSAGE_NEW_BOOK_ARRIVED, newBook)
                    .sendToTarget();
        }
    };

    @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);//绑定Service, 成功后ServiceConnection mConnection的onServiceConnected()方法被回调.
    }

    public void onButton1Click(View view) {
        Toast.makeText(this, "click button1", Toast.LENGTH_SHORT).show();
    //开启一个新线程执行远程服务端的方法是良好的编程方法, 因为调用方法返回前当前线程会被挂起,如果在UI线程中执行就可能会发生ANR.
        new Thread(new Runnable() {

            @Override
            public void run() {
                if (mRemoteBookManager != null) {
                    try {
                        List<Book> newList = mRemoteBookManager.getBookList();
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }

    @Override
    protected void onDestroy() {
        if (mRemoteBookManager != null
                && mRemoteBookManager.asBinder().isBinderAlive()) {
            try {
                Log.i(TAG, "unregister listener:" + mOnNewBookArrivedListener);
                mRemoteBookManager
                        .unregisterListener(mOnNewBookArrivedListener);//把自己的对象从服务端的通知list中移除.
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
        unbindService(mConnection);//在Activity销毁时, 应该解除对service的绑定.
        super.onDestroy();
    }

}


5. 通过ContentProvider跨进程交换数据

ContentProvider的底层实现也是Binder. 系统本身就内置了一些ContentProvider, 比如通信录, 日历表.
ContentProvider: 数据提供者
ContentResolver: 数据使用者
创建一个自定义的ContentProvider很简单, 继承自ContentProvider, 并实现6个抽象方法.
onCreate(), query(), update(), insert(),delete()以及getType().
除了onCreate()运行在主线程,其他5个均运行在binder线程池.
ContentProvider以表格的形式来存储数据,但对底层的数据存储方式没有要求,可以是数据库, 也可以是普通的文件,甚至可以是用内存中的一个对象来存储数据.
下面通过一个完整的例子,并在代码中插入注释的方式方便我们的理解.

几个知识点:
  1. UriMatcher的使用,将uri和int整形进行关联,对int值 switch-case获知客户要访问的是数据库中的哪个表.
  2. 数据源变化时, 如何通知客户.mContext.getContentResolver().notifyChange(uri, null)
    客户用mContext.getContentResolver().registerContentObserver("content://com.ryg.chapter_2.book.provider/book", false, mBookObserver);注册一个ContentObserver对象.
  3. 客户调用query(uri)后得到的cursor对象对结果集进行遍历的操作.
//BookProvider.java
package com.ryg.chapter_2.provider;

public class BookProvider extends ContentProvider {

    private static final String TAG = "BookProvider";

    public static final String AUTHORITY = "com.ryg.chapter_2.book.provider"; //对应在AndroidManifest.xml
        //        <provider
        //                android:name="com.ryg.chapter_2.provider.BookProvider"
        //                android:authorities="com.ryg.chapter_2.book.provider"
        //                >
        //    </provider>
    public static final Uri BOOK_CONTENT_URI = Uri.parse("content://"
            + AUTHORITY + "/book");   //"book"表对应的完整uri
    public static final Uri USER_CONTENT_URI = Uri.parse("content://"
            + AUTHORITY + "/user");   //"user"表对应的完整uri

    public static final int BOOK_URI_CODE = 0;
    public static final int USER_URI_CODE = 1;

    //使用UriMatcher的目的是把一个uri和一个int数对应起来, 这样再调用sUriMatcher.match(uri)就返回一个int数,再根据这个int数switch-case出客户要访问的是数据库中的哪个表了.
    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();//创建一个名为"book_provider.db"的数据库
        mDb.execSQL("delete from " + DbOpenHelper.BOOK_TABLE_NAME);//删除"book"表中的数据
        mDb.execSQL("delete from " + DbOpenHelper.USER_TALBE_NAME);//删除"user"表中的数据
    //为两个表插入数据
        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());//执行在binder线程池
        String table = getTableName(uri); //首先根据uri判断出客户到底要访问的是数据库中的哪个table
        if (table == null) {
            throw new IllegalArgumentException("Unsupported URI: " + uri);
        }
    //返回一个cursor对象,也就是一个迭代器,客户使用这个迭代器对查询到的结果集进行遍历. (参看设计模式与源码书中对迭代器的讲解.)
        return mDb.query(table, projection, selection, selectionArgs, null, null, sortOrder, null); //到实际的"book_provider.db"数据库中进行查询
    }

    @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);//首先根据uri判断出客户到底要访问的是数据库中的哪个table
        if (table == null) {
            throw new IllegalArgumentException("Unsupported URI: " + uri);
        }
        mDb.insert(table, null, values);
        mContext.getContentResolver().notifyChange(uri, null);// 通知系统, 这个uri对应的数据源发生了变化, 系统会根据这个uri查看所有调用了        registerContentObserver(Uri uri, boolean notifyForDescendents, ContentObserver observer)的ContentResolver, 回调observer对象的onChange()方法.
        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); //通知系统uri对应的数据源发生了变化
        }
        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);//通知系统uri对应的数据源发生了变化
        }
        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;
    }
}
用SQLite建立一个数据库
//DbOpenHelper.java
package com.ryg.chapter_2.provider;

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

//继承自SQLiteOpenHelper,目的是定制数据库的名字, 以及这里在创建对象时,默认建立了两个表"book"和"user"
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;//升级时, 要记得更新DB_VERSION的值

    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
    }

}
客户端
//ProviderActivity.java
package com.ryg.chapter_2.provider;


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


    @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"); //这个uri值对应AndroidManifest.xml的<ContentProvider>中android:authorities="com.ryg.chapter_2.book.provider" 指定的值, "book"是表的名字,一个ContentProvider可以分多个table存不同的数据.

    //使用ContentValues的对象,插入数据
        ContentValues values = new ContentValues();
        values.put("_id", 6);
        values.put("name", "程序设计的艺术");
        getContentResolver().insert(bookUri, values);

    //查询到的是一个结果集,用返回的cursor对象,也就是一个迭代器,对结果集进行遍历.
        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();

    //自己扩展, 书中忘写了.
    //向系统注册某个uri对应的observer, observer和某个特定的uri对应起来
    //当数据源发生变化并调用了notifyChange(uri, null)时,系统回调uri对应的所有的observer的onChange()方法.
    mBookObserver = new ContentObserver {
        @Override
        public void onChange(boolean selfChange) {
        Log.d(TAG, "book table 中数据源发生了变化");
        }
    }
    this.getContentResolver().registerContentObserver("content://com.ryg.chapter_2.book.provider/book", false, mBookObserver);

    mUserObserver = new ContentObserver {
        @Override
        public void onChange(boolean selfChange) {
        Log.d(TAG, "user table 中的数据源发生了变化");
        }
    }
    this.getContentResolver().registerContentObserver("content://com.ryg.chapter_2.book.provider/user", false, mUserObserver);

    }
}

6. 使用socket

分为TCP和UDP
TCP连接的建立需要经过"三次握手"才能完成, 为了提供稳定的数据传输功能, 实现了超时重传机制,因此具有很高的稳定性.
UDP具有更好的效率,但不保证数据一定能够正确传输,尤其是在网络阻塞的情况下.
不能在UI线程访问网络,否则在android 4.0以上的手机会抛出android.os.NetworkOnMainThreadException异常.
socket使用上的知识点就不总结了。

Binder连接池

注意这里说的不是Binder线程池.
考虑一种情形,有10个业务模块都需要和服务端进行通信,每个业务模块都拥有一个自己的aidl文件和服务端共享. 如果在服务端为每个aidl都创建一个Service,创建一个Binder对象,去实现aidl文件生成类中的Stub内部类中的方法(真正去实现那些aidl方法), 那么这个服务端就有10个service在同时运行,这个应用就会很重, 是用户无法接受的, 那么如何去解决这个问题呢?
这个时候就需要引入binder连接池的概念.
原理就是在服务端, 为每个业务模块建立一个类,继承自aidl文件对应java类中的Stub内部类,在这个新建类中实现Stub类中所描述的aidl方法. 这样有几个业务模块就新建几个这样的类。
以例子中有两个业务模块为例, 在服务端创建3个Binder对象, 分别实现3个aidl文件对应的Stub内部类中的aidl方法. 2个业务模块的binder对象, 和一个IBinderPool对应的Binder对象. 服务端绑定成功后, 就把这个IBinderPool的对象返还给客户端。客户端用这个binder对象,再用IBinderPool.aidl中声明的queryBinder(int binderCode)获取到实际要使用的binder对象,就可以用这个业务模块的binder对象进行实际的业务方法的调用了.

通过完整的代码来说明如何实现一个binder连接池.
服务端代码

两个业务模块, 分别对应各自的aidl文件.

// ISecurityCenter.aidl file, 生成ISecurityCenter.java, 最重要的是内部类ISecurityCenter.Stub.
package com.ryg.chapter_2.binderpool;

interface ISecurityCenter {
    String encrypt(String content);
    String decrypt(String password);
}
//ICompute.aidl file, 生成ICompute.java, 最重要的是内部类ICompute.Stub.
package com.ryg.chapter_2.binderpool;

interface ICompute {
    int add(int a, int b);
}

对ISecurityCenter.aidl中的aidl方法进行实现, 内部类 Stub extends android.os.Binder,因此SecurityCenterImpl的对象就是一个Binder对象.

// SecurityCenterImpl.java file
package com.ryg.chapter_2.binderpool;

import android.os.RemoteException;

public class SecurityCenterImpl extends ISecurityCenter.Stub {

    private static final char SECRET_CODE = '^';

    @Override
    public String encrypt(String content) throws RemoteException {
        char[] chars = content.toCharArray();
        for (int i = 0; i < chars.length; i++) {
            chars[i] ^= SECRET_CODE;
        }
        return new String(chars);
    }

    @Override
    public String decrypt(String password) throws RemoteException {
        return encrypt(password);
    }

}

对ICompute.aidl中的aidl方法进行实现,ComputeImpl的对象就是一个Binder对象.

ComputeImpl.java file
package com.ryg.chapter_2.binderpool;

import android.os.RemoteException;

public class ComputeImpl extends ICompute.Stub {

    @Override
    public int add(int a, int b) throws RemoteException {
        return a + b;
    }

}

引入一个IBinderPool.aidl,给客户提供一个queryBinder(int code)方法, 客户端调用这个方法, 可以得到实现了某个具体业务aidl方法的Binder对象.

//IBinderPool.aidl
package com.ryg.chapter_2.binderpool;

interface IBinderPool {

    /**
     * @param binderCode, the unique token of specific Binder<br/>
     * @return specific Binder who's token is binderCode.
     */
    IBinder queryBinder(int binderCode);
}

新建一个BinderPoolServer,它的内部类BinderPoolImpl继承IBinderPool.Stub,并实现queryBinder(int binderCode)方法.

package com.ryg.chapter_2.binderpool;

import java.util.concurrent.CountDownLatch;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;

public class BinderPoolServer {
    private static final String TAG = "BinderPoolServer";
    public static final int BINDER_NONE = -1;
    public static final int BINDER_COMPUTE = 0;
    public static final int BINDER_SECURITY_CENTER = 1;


    private BinderPoolServer(Context context) {
    }

    public static class BinderPoolImpl extends IBinderPool.Stub {

        public BinderPoolImpl() {
            super();
        }

        @Override
        public IBinder queryBinder(int binderCode) throws RemoteException {
            IBinder binder = null;
            switch (binderCode) {
            case BINDER_SECURITY_CENTER: {
                binder = new SecurityCenterImpl();
                break;
            }
            case BINDER_COMPUTE: {
                binder = new ComputeImpl();
                break;
            }
            default:
                break;
            }

            return binder;
        }
    }

}


提供一个BinderPoolService, 继承自Service, 绑定成功后给客户端返回Binder mBinderPool = new BinderPoolServer.BinderPoolImpl(); 这个Binder对象.

package com.ryg.chapter_2.binderpool;

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

public class BinderPoolService extends Service {

    private static final String TAG = "BinderPoolService";

    private Binder mBinderPool = new BinderPoolServer.BinderPoolImpl();

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

    @Override
    public IBinder onBind(Intent intent) {
        Log.d(TAG, "onBind");
        return mBinderPool;
    }

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

}


看看客户端怎么用

新建一个BinderPoolClient, 用于绑定远程服务,在onServiceConnected()时, 得到IBinderPool的实际Binder对象.

package com.ryg.chapter_2.binderpool;

import java.util.concurrent.CountDownLatch;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;

public class BinderPoolClient {
    private static final String TAG = "BinderPoolClient";
    public static final int BINDER_NONE = -1;
    public static final int BINDER_COMPUTE = 0;
    public static final int BINDER_SECURITY_CENTER = 1;

    private Context mContext;
    private IBinderPool mBinderPoolClient;
    private static volatile BinderPool sInstance;
    private CountDownLatch mConnectBinderPoolCountDownLatch;

    private BinderPoolClient(Context context) {
        mContext = context.getApplicationContext();
        connectBinderPoolService();//在BinderPoolClient对象构造时, 与远程服务端BinderPoolService.class进行绑定.
    }

    //单例提供一个BinderPoolClient的对象, 使用double check null的方式
    public static BinderPoolClient getInsance(Context context) {
        if (sInstance == null) {
            synchronized (BinderPoolClient.class) {
                if (sInstance == null) {
                    sInstance = new BinderPoolClient(context);
                }
            }
        }
        return sInstance;
    }

//用CountDownLatch的目的是把bindService()的操作由异步操作转换为同步操作.
    private synchronized void connectBinderPoolService() {
        mConnectBinderPoolCountDownLatch = new CountDownLatch(1);//初始化计数值为1.
        Intent service = new Intent(mContext, BinderPoolService.class);
        mContext.bindService(service, mBinderPoolConnection,
                Context.BIND_AUTO_CREATE);
        try {
        //让当前线程进入阻塞状态, 直到mConnectBinderPoolCountDownLatch的值为0时, 当前线程才能往下执行.
            //这就保证了在执行完onServiceConnected()后,才能继续往下执行, 当前方法才会退出.
        //这样就把一个异步操作转换成了同步操作,
            mConnectBinderPoolCountDownLatch.await(); 
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * query binder by binderCode from binder pool
     * 
     * @param binderCode
     *            the unique token of binder
     * @return binder who's token is binderCode<br>
     *         return null when not found or BinderPoolService died.
     */
    public IBinder queryBinder(int binderCode) {
        IBinder binder = null;
        try {
            if (mBinderPoolClient != null) {
                binder = mBinderPoolClient.queryBinder(binderCode);
            }
        } catch (RemoteException e) {
            e.printStackTrace();
        }
        return binder;
    }

    private ServiceConnection mBinderPoolConnection = new ServiceConnection() {

        @Override
        public void onServiceDisconnected(ComponentName name) {
            // ignored.
        }

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mBinderPoolClient = IBinderPool.Stub.asInterface(service); //给mBinderPoolClient赋值
            try {
                mBinderPoolClient.asBinder().linkToDeath(mBinderPoolDeathRecipient, 0);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
            mConnectBinderPoolCountDownLatch.countDown();//把mConnectBinderPoolCountDownLatch的值减1.
        }
    };

    private IBinder.DeathRecipient mBinderPoolDeathRecipient = new IBinder.DeathRecipient() {
        @Override
        public void binderDied() {
            Log.w(TAG, "binder died.");
            mBinderPoolClient.asBinder().unlinkToDeath(mBinderPoolDeathRecipient, 0);
            mBinderPoolClient = null;
            connectBinderPoolService();
        }
    };
}


客户端真正的使用方法
package com.ryg.chapter_2.binderpool;

import com.ryg.chapter_2.R;
import android.app.Activity;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;

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

    private ISecurityCenter mSecurityCenter;
    private ICompute mCompute;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_binder_pool);
        new Thread(new Runnable() {

            @Override
            public void run() {
                doWork();
            }
        }).start();
    }

    private void doWork() {
        BinderPoolCient binderPoolClient = BinderPool.getInsance(BinderPoolActivity.this);//创建BinderPool对象, 创建时与BinderPoolService.class进行了绑定.
        IBinder securityBinder = binderPoolClient
                .queryBinder(BinderPool.BINDER_SECURITY_CENTER); //得到的是实现了ISecurityCenter.Stub的binder对象

        mSecurityCenter = (ISecurityCenter) SecurityCenterImpl
                .asInterface(securityBinder);
        Log.d(TAG, "visit ISecurityCenter");
        String msg = "helloworld-安卓";
        System.out.println("content:" + msg);
        try {
            String password = mSecurityCenter.encrypt(msg);//调用服务端的aidl方法
            System.out.println("encrypt:" + password);
            System.out.println("decrypt:" + mSecurityCenter.decrypt(password));
        } catch (RemoteException e) {
            e.printStackTrace();
        }

        Log.d(TAG, "visit ICompute");
        IBinder computeBinder = binderPoolClient
                .queryBinder(BinderPool.BINDER_COMPUTE);//得到的是实现了ICompute.Stub的binder对象
        ;
        mCompute = ComputeImpl.asInterface(computeBinder);
        try {
            System.out.println("3+5=" + mCompute.add(3, 5));//调用服务端的aidl方法
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

}


总结: 书中的BinderPool代码需要在客户端和服务器端都拥有一份相同的代码, 不是很好理解, 我这里对BindPool进行的裁剪, 客户端的叫BinderPoolClient,只提供绑定服务的功能. 服务器端的叫BinderPoolServer, 只提供创建IBinderPool.Stub的子类的对象,并在onBind()时把这个对象返回给客户端使用.
还有一个知识点是, 用CountDownLatch对象, 把一个异步操作转换为一个同步操作.

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

推荐阅读更多精彩内容