PendingIntent的内部机制

转自https://my.oschina.net/youranhongcha/blog/196933

1 概述
在Android中,我们常常使用PendingIntent来表达一种“留待日后处理”的意思。从这个角度来说,PendingIntent可以被理解为一种特殊的异步处理机制。不过,单就命名而言,PendingIntent其实具有一定误导性,因为它既不继承于Intent,也不包含Intent,它的核心可以粗略地汇总成四个字——“异步激发”。
很明显,这种异步激发常常是要跨进程执行的。比如说A进程作为发起端,它可以从系统“获取”一个PendingIntent,然后A进程可以将PendingIntent对象通过binder机制“传递”给B进程,再由B进程在未来某个合适时机,“回调”PendingIntent对象的send()动作,完成激发。
在Android系统中,最适合做集中性管理的组件就是AMS(Activity Manager Service)啦,所以它义不容辞地承担起管理所有PendingIntent的职责。这样我们就可以画出如下示意图:


注意其中的第4步“递送相应的intent”。这一步递送的intent是从何而来的呢?简单地说,当发起端获取PendingIntent时,其实是需要同时提供若干intent的。这些intent和PendingIntent只是配套的关系,而不是聚合的关系,它们会被缓存在AMS中。日后,一旦处理端将PendingIntent的“激发”语义传递到AMS,AMS就会尝试找到与这个PendingIntent对应的若干intent,并递送出去。
当然,以上说的只是大概情况,实际的技术细节会更复杂一点儿。下面我们就来谈谈细节。

2 PendingIntent的技术细节
2.1 发起端获取PendingIntent
我们先要理解,所谓的“发起端获取PendingIntent”到底指的是什么。难道只是简单new一个PendingIntent对象吗?当然不是。此处的“获取”动作其实还含有向AMS“注册”intent的语义。
在PendingIntent.java文件中,我们可以看到有如下几个比较常见的静态函数:

public static PendingIntent getActivity(Context context, int requestCode, Intent intent, int flags)
public static PendingIntent getBroadcast(Context context, int requestCode, Intent intent, int flags)
public static PendingIntent getService(Context context, int requestCode, Intent intent, int flags)
public static PendingIntent getActivities(Context context, int requestCode, Intent[] intents, int flags)
public static PendingIntent getActivities(Context context, int requestCode, Intent[] intents, int flags, Bundle options)

它们就是我们常用的获取PendingIntent的动作了。
坦白说,这几个函数的命名可真不怎么样,所以我们简单解释一下。上面的getActivity()的意思其实是,获取一个PendingIntent对象,而且该对象日后激发时所做的事情是启动一个新activity。也就是说,当它异步激发时,会执行类似Context.startActivity()那样的动作。相应地,getBroadcast()和getService()所获取的PendingIntent对象在激发时,会分别执行类似Context.sendBroadcast()和Context.startService()这样的动作。至于最后两个getActivities(),用得比较少,激发时可以启动几个activity。
我们以getActivity()的代码来说明问题:

public static PendingIntent getActivity(Context context, int requestCode, Intent intent, int flags, Bundle options) {
    String packageName = context.getPackageName(); 
    String resolvedType = intent != null ? intent.resolveTypeIfNeeded(context.getContentResolver()) : null; 
    try { 
        intent.setAllowFds(false); 
        IntentSender target = ActivityManagerNative.getDefault().getIntentSender( ActivityManager.INTENT_SENDER_ACTIVITY, packageName, null, null, requestCode, new Intent[] { intent }, resolvedType != null ? new String[] { resolvedType } : null, flags, options); 
        return target != null ? new PendingIntent(target) : null; 
    } catch (RemoteException e) {
        e.printStackTrace();
    } 
    return null;
}

其中那句new PendingIntent(target)创建了PendingIntent对象,其重要性自不待言。然而,这个对象的内部核心其实是由上面那个getIntentSender()函数得来的。而这个IntentSender核心才是我们真正需要关心的东西。
说穿了,此处的IntentSender对象是个binder代理,它对应的binder实体是AMS中的PendingIntentRecord对象。PendingIntent对象构造之时,IntentSender代理作为参数传进来,并记录在PendingIntent的mTarget域。日后,当PendingIntent执行异步激发时,其内部就是靠这个mTarget域向AMS传递语义的。
我们前文说过,PendingIntent常常会经由binder机制,传递到另一个进程去。而binder机制可以保证,目标进程得到的PendingIntent的mTarget域也是合法的IntentSender代理,而且和发起端的IntentSender代理对应着同一个PendingIntentRecord实体。示意图如下:

2.2 AMS里的PendingIntentRecord
那么PendingIntentRecord里又有什么信息呢?它的定义截选如下:

class PendingIntentRecord extends IIntentSender.Stub {
    final ActivityManagerService owner; 
    final Key key; // 最关键的key域 
    final int uid; 
    final WeakReference<PendingIntentRecord> ref; 
    boolean sent = false; 
    boolean canceled = false; 
    String stringName; 
}

请注意其中那个key域。这里的Key是个PendingIntentRecord的内嵌类,其定义截选如下:

final static class Key { 
    final int type; 
    final String packageName; 
    final ActivityRecord activity; 
    final String who; 
    final int requestCode; 
    final Intent requestIntent; // 注意! 
    final String requestResolvedType; 
    final Bundle options; 
    Intent[] allIntents; // 注意!记录了当初获取PendingIntent时,用户所指定的所有
    intent String[] allResolvedTypes; 
    final int flags; 
    final int hashCode; 
}

请注意其中的allIntents[]数组域以及requestIntent域。前者记录了当初获取PendingIntent时,用户所指定的所有intent(虽然一般情况下只会指定一个intent,但类似getActivities()这样的函数还是可以指定多个intent的),而后者可以粗浅地理解为用户所指定的那个intent数组中的最后一个intent。现在大家应该清楚异步激发时用到的intent都存在哪里了吧。
Key的构造函数截选如下:

Key(int _t, String _p, ActivityRecord _a, String _w, int _r, Intent[] _i, String[] _it, int _f, Bundle _o) { 
    type = _t; 
    packageName = _p; 
    activity = _a; 
    who = _w; 
    requestCode = _r; 
    requestIntent = _i != null ? _i[_i.length-1] : null; 
    // intent数组中的最后一个 
    requestResolvedType = _it != null ? _it[_it.length-1] : null; 
    allIntents = _i; 
    // 所有intent allResolvedTypes = _it; 
    flags = _f; 
    options = _o; 
}

Key不光承担着记录信息的作用,它还承担“键值”的作用。

2.3 AMS中的PendingIntentRecord总表
在AMS中,管理着系统中所有的PendingIntentRecord节点,所以需要把这些节点组织成一张表:

final HashMap<PendingIntentRecord.Key, WeakReference<PendingIntentRecord>> mIntentSenderRecords

这张哈希映射表的键值类型就是刚才所说的PendingIntentRecord.Key。
以后每当我们要获取PendingIntent对象时,PendingIntent里的mTarget是这样得到的:AMS会先查mIntentSenderRecords表,如果能找到符合的PendingIntentRecord节点,则返回之。如果找不到,就创建一个新的PendingIntentRecord节点。因为PendingIntentRecord是个binder实体,所以经过binder机制传递后,客户进程拿到的就是个合法的binder代理。如此一来,前文的示意图可以进一步修改成下图:


2.4 AMS里的getIntentSender()函数
现在,我们回过头继续说前文的getActivity(),以及其调用的getIntentSender()。我们先列一遍getActivity()的原型:

public static PendingIntent getActivity(Context context, int requestCode, Intent intent, int flags, Bundle options)

context参数是调用方的上下文。
requestCode是个简单的整数,起区分作用。
intent是异步激发时将发出的intent。
flags可以包含一些既有的标识,比如FLAG_ONE_SHOT、FLAG_NO_CREATE、FLAG_CANCEL_CURRENT、FLAG_UPDATE_CURRENT等等。不少同学对这个域不是很清楚,我们后文会细说。
options可以携带一些额外的数据。
getActivity()的代码很简单,其参数基本上都传给了getIntentSender()。

IIntentSender target = ActivityManagerNative.getDefault().getIntentSender(. . . . . .)

getIntentSender()的原型大体是这样的:

public IIntentSender getIntentSender(int type, String packageName, IBinder token, String resultWho, int requestCode, Intent[] intents, String[] resolvedTypes, int flags, Bundle options) throws RemoteException;

其参数比getActivity()要多一些,我们逐个说明。
type参数表明PendingIntent的类型。getActivity()和getActivities()动作里指定的类型值是INTENT_SENDER_ACTIVITY,getBroadcast()和getService()和动作里指定的类型值分别是INTENT_SENDER_BROADCAST和INTENT_SENDER_SERVICE。另外,在Activity.java文件中,我们还看到一个createPendingResult()函数,这个函数表达了发起方的activity日后希望得到result回馈的意思,所以其内部调用getIntentSender()时指定的类型值为INTENT_SENDER_ACTIVITY_RESULT。
packageName参数表示发起端所属的包名。
token参数是个指代回馈目标方的代理。这是什么意思呢?我们常用的getActivity()、getBroadcast()和getService()中,只是把这个参数简单地指定为null,表示这个PendingIntent激发时,是不需要发回什么回馈的。不过当我们希望获取类型为INTENT_SENDER_ACTIVITY_RESULT的PendingIntent时,就需要指定token参数了。具体可参考createPendingResult()的代码:

public PendingIntent createPendingResult(int requestCode, Intent data, int flags) { 
    String packageName = getPackageName(); 
    try { 
        data.setAllowFds(false); 
        IntentSender target = ActivityManagerNative.getDefault().getIntentSender( ActivityManager.INTENT_SENDER_ACTIVITY_RESULT, packageName, mParent == null ? mToken : mParent.mToken, mEmbeddedID, requestCode, new Intent[] { data }, null, flags, null); return target != null ? new PendingIntent(target) : null; 
    } catch (RemoteException e) { 
        // Empty 
    } 
    return null;
}

看到了吗?传入的token为Activity的mToken或者其mParent.mToken。说得简单点儿,AMS内部可以根据这个token找到其对应的ActivityRecord,日后当PendingIntent激发时,AMS可以根据这个ActivityRecord确定出该向哪个目标进程的哪个Activity发出result语义。
resultWho参数和token参数息息相关,一般也是null啦。在createPendingResult()中,其值为Activity的mEmbeddedID字符串。
requestCode参数是个简单的整数,可以在获取PendingIntent时由用户指定,它可以起区分的作用。
intents数组参数是异步激发时希望发出的intent。对于getActivity()、getBroadcast()和getService()来说,都只会指定一个intent而已。只有getActivities()会尝试一次传入若干intent。
resolvedTypes参数基本上和intent是相关的。一般是这样得到的:

String resolvedType = intent != null ? intent.resolveTypeIfNeeded( context.getContentResolver()) : null;

这个值常常和intent内部的mData URI有关系,比如最终的值可能是URI对应的MIME类型。
flags参数可以指定PendingIntent的一些行为特点。它的取值是一些既有的比特标识的组合。目前可用的标识有:FLAG_ONE_SHOT、FLAG_NO_CREATE、FLAG_CANCEL_CURRENT、FLAG_UPDATE_CURRENT等等。有时候,flags中还可以附带若干FILL_IN_XXX标识。我们把常见的标识定义列举如下:
【PendingIntent中】

public static final int FLAG_ONE_SHOT = 1<<30;
public static final int FLAG_NO_CREATE = 1<<29;
public static final int FLAG_CANCEL_CURRENT = 1<<28;
public static final int FLAG_UPDATE_CURRENT = 1<<27;

【Intent中】

public static final int FILL_IN_ACTION = 1<<0;
public static final int FILL_IN_DATA = 1<<1;
public static final int FILL_IN_CATEGORIES = 1<<2;
public static final int FILL_IN_COMPONENT = 1<<3;
public static final int FILL_IN_PACKAGE = 1<<4;
public static final int FILL_IN_SOURCE_BOUNDS = 1<<5;
public static final int FILL_IN_SELECTOR = 1<<6;
public static final int FILL_IN_CLIP_DATA = 1<<7;

这些以FILL_IN_打头的标志位,主要是在intent对象的fillIn()函数里起作用:
public int fillIn(Intent other, int flags)

我们以FILL_IN_ACTION为例来说明,当我们执行类似srcIntent.fillIn(otherIntent, ...)的句子时,如果otherIntent的mAction域不是null值,那么fillIn()会在以下两种情况下,用otherIntent的mAction域值为srcIntent的mAction域赋值:
1) 当srcIntent的mAction域值为null时;
2) 如果fillIn的flags参数里携带了FILL_IN_ACTION标志位,那么即便srcIntent的mAction已经有值了,此时也会用otherIntent的mAction域值强行替换掉srcIntent的mAction域值。
其他FILL_IN_标志位和FILL_IN_ACTION的处理方式类似,我们不再赘述。
options参数可以携带一些额外数据。

2.4.1 getIntentSender()函数
getIntentSender()函数摘录如下:

public IntentSender getIntentSender(int type, String packageName, IBinder token, String resultWho, int requestCode, Intent[] intents, String[] resolvedTypes, int flags, Bundle options) { 
    // 先判断intents数组,可以用伪代码checkIntents(intents)来表示         
    checkIntents(intents); 
    int callingUid = Binder.getCallingUid();
    if (callingUid != 0 && callingUid != Process.SYSTEM_UID) { 
        int uid = AppGlobals.getPackageManager().getPackageUid(packageName, UserId.getUserId(callingUid)); 
        if (!UserId.isSameApp(callingUid, uid)) {
            throw new SecurityException(msg); 
        }
     } 
     return getIntentSenderLocked(type, packageName, Binder.getOrigCallingUid(), token, resultWho, requestCode, intents, resolvedTypes, flags, options); 
}

getIntentSender()函数中有一段逐条判断intents[]的代码,我用伪代码checkIntents(intents)来表示,这部分对应的实际代码如下:

for (int i=0; i<intents.length; i++) { 
    Intent intent = intents[i]; 
    if (intent != null) { 
        if (intent.hasFileDescriptors()) { 
            throw new IllegalArgumentException("File descriptors passed in Intent"); 
        } 
        if (type == ActivityManager.INTENT_SENDER_BROADCAST && (intent.getFlags()&Intent.FLAG_RECEIVER_BOOT_UPGRADE) != 0) { 
            throw new IllegalArgumentException("Can't use FLAG_RECEIVER_BOOT_UPGRADE here"); 
        } 
        intents[i] = new Intent(intent); 
    }
}

这段代码说明在获取PendingIntent对象时,intent中是不能携带文件描述符的。而且如果这个PendingIntent是那种要发出广播的PendingIntent,那么intent中也不能携带FLAG_RECEIVER_BOOT_UPGRADE标识符。“BOOT_UPGRADE”应该是“启动并升级”的意思,它不能使用PendingIntent。
getIntentSender()中最核心的一句应该是调用getIntentSenderLocked()的那句。

2.4.2 getIntentSenderLocked()函数
getIntentSenderLocked()的代码截选如下:
【frameworks/base/services/java/com/android/server/am/ActivityManagerService.java】

IIntentSender getIntentSenderLocked(int type, String packageName, int callingUid, IBinder token, String resultWho, int requestCode, Intent[] intents, String[] resolvedTypes, int flags, Bundle options) { 
    // 如果是INTENT_SENDER_ACTIVITY_RESULT类型,那么要判断token所代表的activity是否还在activity栈中 
    // 整理flags中的信息
    PendingIntentRecord.Key key = new PendingIntentRecord.Key(type, packageName, activity, resultWho, requestCode, intents, resolvedTypes, flags, options); 
    // 尽力从哈希映射表中查找key对应的PendingIntentRecord,如果找不到就创建一个新的节点。 
    WeakReference<PendingIntentRecord> ref; 
    ref = mIntentSenderRecords.get(key); 
    PendingIntentRecord rec = ref != null ? ref.get() : null; 
    if (rec != null) { 
        // 找到了匹配的PendingIntent,现在考虑要不要更新它,或者取消它。 
        if (!cancelCurrent) {
            if (updateCurrent) { 
                // 如果明确指定了FLAG_UPDATE_CURRENT,那么更新找到的节点 
                if (rec.key.requestIntent != null) { 
                    rec.key.requestIntent.replaceExtras(intents != null ? intents[intents.length - 1] : null); 
                } 
                if (intents != null) { 
                    intents[intents.length-1] = rec.key.requestIntent; 
                    rec.key.allIntents = intents; 
                    rec.key.allResolvedTypes = resolvedTypes; 
                } else { 
                    rec.key.allIntents = null; 
                    rec.key.allResolvedTypes = null; 
                } 
            } 
            // 凡是能找到对应的节点,而且又不取消该节点的,那么就return这个节点 
            return rec; 
        } 
        // 如果PendingIntent的标志中带有FLAG_CANCEL_CURRENT,则从哈希映射表中删除之 
        rec.canceled = true; 
        mIntentSenderRecords.remove(key); 
    } 
    if (noCreate) { 
        // 如果明确表示了不创建新节点,也就是说标志中带有FLAG_NO_CREATE,那么不管是不是Cancel了PendingIntent,此时一概直接返回。 
        return rec; 
    } 
    // 从哈希映射表中找不到,而且又没有写明FLAG_NO_CREATE,此时创建一个新节点 
    rec = new PendingIntentRecord(this, key, callingUid);     
    mIntentSenderRecords.put(key, rec.ref); 
    if (type == ActivityManager.INTENT_SENDER_ACTIVITY_RESULT) { 
    // 如果intent需要返回结果,那么修改token对应的ActivityRecord的pendingResults域。 
        if (activity.pendingResults == null) { 
            activity.pendingResults = new HashSet<WeakReference<PendingIntentRecord>>(); 
        } 
        activity.pendingResults.add(rec.ref); 
    } 
    return rec;
}

上面这段代码主要做的事情有:
1)将传进来的多个参数信息整理成一个PendingIntentRecord.Key对象(key);
2)尝试从mIntentSenderRecords总表中查找和key相符的PendingIntentRecord节点;
3)根据flags参数所含有的意义,对得到的PendingIntentRecord进行加工。有时候修改之,有时候删除之。
4)如果在总表中没有找到对应的PendingIntentRecord节点,或者根据flags的语义删除了刚找到的节点,那么此时的默认行为是创建一个新的PendingIntentRecord节点,并插入总表。除非flags中明确指定了FLAG_NO_CREATE,此时不会创建新节点。

2.4.3 说说flags
从getIntentSenderLocked()的代码中,我们终于搞明白了flags中那些特定比特值的意义了。我们现在总结一下。
应该说这些flags比特值基本上都是在围绕着mIntentSenderRecords总表说事的。其中,FLAG_CANCEL_CURRENT的意思是,当我们获取PendingIntent时,如果可以从总表中查到一个相符的已存在的PendingIntentRecord节点的话,那么需要把这个节点从总表中清理出去。而在没有指定FLAG_CANCEL_CURRENT的大前提下,如果用户指定了FLAG_UPDATE_CURRENT标识,那么会用新的intents参数替掉刚查到的PendingIntentRecord中的旧intents。
而不管是刚清理了已存在的PendingIntentRecord,还是压根儿就没有找到符合的PendingIntentRecord,只要用户没有明确指定FLAG_NO_CREATE标识,系统就会尽力创建一个新的PendingIntentRecord节点,并插入总表。
至于FLAG_ONE_SHOT标识嘛,它并没有在getIntentSenderLocked()中露脸儿。它的名字是“FLAG_ONE_SHOT”,也就是“只打一枪”的意思,那么很明显,这个标识起作用的地方应该是在“激发”函数里。在最终的激发函数(sendInner())里,我们可以看到下面的代码:
【frameworks/base/services/java/com/android/server/am/PendingIntentRecord.java】

int sendInner(int code, Intent intent, String resolvedType, IntentReceiver finishedReceiver, String requiredPermission, IBinder resultTo, String resultWho, int requestCode, int flagsMask, int flagsValues, Bundle options) { 
    synchronized(owner) { 
        if (!canceled) { 
            sent = true; 
            if ((key.flags & PendingIntent.FLAG_ONE_SHOT) != 0) { 
                owner.cancelIntentSenderLocked(this, true); 
                canceled = true;
            }
        }
    } 
    return ActivityManager.START_CANCELED;
}

意思很简单,一进行激发就把相应的PendingIntentRecord节点从总表中清理出去,而且把PendingIntentRecord的canceled域设为true。这样,以后即便外界再调用send()动作都没用了,因为再也无法进入if (!canceled)判断了。

2.4.4 将PendingIntentRecord节点插入总表
接下来getIntentSenderLocked()函数new了一个PendingIntentRecord节点,并将之插入mIntentSenderRecords总表中。

2.5 PendingIntent的激发动作
下面我们来看PendingIntent的激发动作。在前文我们已经说过,当需要激发PendingIntent之时,主要是通过调用PendingIntent的send()函数来完成激发动作的。PendingIntent提供了多个形式的send()函数,然而这些函数的内部其实调用的是同一个send(),其函数原型如下:

public void send(Context context, int code, Intent intent, OnFinished onFinished, Handler handler, String requiredPermission) throws CanceledException

该函数内部最关键的一句是:

int res = mTarget.send(code, intent, resolvedType, onFinished != null ? new FinishedDispatcher(this, onFinished, handler) : null, requiredPermission);

我们前文已经介绍过这个mTarget域了,它对应着AMS中的某个PendingIntentRecord。
所以我们要看一下PendingIntentRecord一侧的send()函数,其代码如下:

public int send(int code, Intent intent, String resolvedType, IIntentReceiver finishedReceiver, String requiredPermission) { 
    return sendInner(code, intent, resolvedType, finishedReceiver, requiredPermission, null, null, 0, 0, 0, null);
}

其中sendInner()才是真正做激发动作的函数。
sendInner()完成的主要逻辑动作有:
1) 如果当前PendingIntentRecord节点已经处于canceled域为true的状态,那么说明这个节点已经被取消掉了,此时sendInner()不会做任何实质上的激发动作,只是简单地return ActivityManager.START_CANCELED而已。 2) 如果当初在创建这个节点时,使用者已经指定了FLAG_ONE_SHOT标志位的话,那么此时sendInner()会把这个PendingIntentRecord节点从AMS中的总表中摘除,并且把canceled域设为true。而后的操作和普通激发时的动作是一致的,也就是说也会走下面的第3)步。 3) 关于普通激发时应执行的逻辑动作是,根据当初创建PendingIntentRecord节点时,用户指定的type类型,进行不同的处理。这个type其实就是我们前文所说的INTENT_SENDER_ACTIVITY、INTENT_SENDER_BROADCAST、INTENT_SENDER_SERVICE等类型啦,大家如有兴趣,可自己参考本文一开始所说的getActivity()、getBroadcast()、getService()等函数的实现代码。
现在还有一个问题是,既然我们在当初获取PendingIntent时,已经指定了日后激发时需要递送的intent(或intent数组),那么为什么send()动作里还有一个intent参数呢?它们的关系又是什么呢?我猜想,PendingIntent机制的设计者是希望给激发端一个修改“待激发的intent”的机会。比如当初我们获取PendingIntent对象时,如果在flags里设置了FILL_IN_ACTION标志位,那么就说明我们允许日后在某个激发点,用新的intent的mAction域值,替换掉我们最初给的intent的mAction域值。如果一开始没有设置FILL_IN_ACTION标志位,而且在最初的intent里已经有了非空的mAction域值的话,那么即使在激发端又传入了新intent,它也不可能修改用新intent的mAction域值替换旧intent的mAction域值。
细心的读者一定记得,当初获取PendingIntent对象时,我们可是向AMS端传递了一个intent数组噢,虽然一般情况下这个数组里只有一个intent元素,但有时候我们也是有可能一次性传递多个intent的。比如getActivities()函数就可以一次传递多个intent。可是现在激发动作send()却只能传递一个intent参数,这该如何处理呢?答案很简单,所传入的intent只能影响已有的intent数组的最后一个intent元素。大家可以看看sendInner里allIntents[allIntents.length-1] = finalIntent;一句。
Ok,intent说完了,下面就该做具体的激发了。我们以简单的INTENT_SENDER_BROADCAST型PendingIntentRecord来说明,此时的激发动作就是发送一个广播:

owner.broadcastIntentInPackage(key.packageName, uid, finalIntent, resolvedType, finishedReceiver, code, null, null, requiredPermission, (finishedReceiver != null), false, UserId.getUserId(uid));

至于其他类型的PendingIntentRecord的激发动作,大家可以自行查阅代码,它们的基本代码格局都是差不多的。

3 小结
本文是基于我早先的一点儿笔记整理而成的。当时为了搞清楚PendingIntent的机理,也查阅了一些网上的相关文章,只是都不大满足我的要求,后来只好自己看代码,终于得了些自己的浅见。现在把我过去的一点儿认识整理出来,希望能对学习PendingIntent的同学有点儿帮助。

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

推荐阅读更多精彩内容