《Android开发艺术探索》读书笔记

Activity 的生命周期和启动模式

Tips

  • 新Activity是透明主题时,旧Activity不会走onStop;
  • Activity切换时,旧Activity的onPause会先执行,然后才会启动新的Activity;
  • Activity在异常情况下被回收时,onSaveInstanceState方法会被回调,回调时机是在onStop之前,当Activity被重新创建的时 候,onRestoreInstanceState方法会被回调,时序在onStart之后;
  • 标识Activity任务栈名称的属性:TaskAffinity,默认为应用包名。

Activity的LaunchMode

  1. standard 系统默认。每次启动会重新创建新的实例,谁启动了这个Activity,这个Activity就在谁的栈里。
  2. singleTop 栈顶复用模式。该Activity的onNewIntent方法会被回调,onCreate和onStart并不会被调用。
  3. singleTask 栈内复用模式。只要该Activity在一个栈中存在,都不会重新创建,onNewIntent会被回调。如果不存在,系统会先寻找是否存在需要的栈,如果不存在该栈,就创建一个任务栈,然后把这个Activity放进去;如果存在,就会创建到已经存在的这个栈中。
  4. singleInstance。具有此种模式的Activity只能单独存在于一个任务栈。

IntentFilter匹配规则。

  1. action匹配规则:要求intent中的action 存在 且 必须和过滤规则中的其中一个相同 区分大小写;
  2. category匹配规则:系统会默认加上一个android.intent.category.DEAFAULT,所以intent中可以不存在category,但如果存在就必须匹配其中一个;
  3. data匹配规则:data由两部分组成,mimeType和URI,要求和action相似。如果没有指定URI,URI但默认值为content和file(schema)

IPC 机制

使用android:process会带来的问题

  1. 静态成员和单例模式完全失效;
  2. SharedPreferences可靠性下降;
  3. Application会多次创建;

Binder的工作机制

Binder的工作机制.png

Android中的IPC方式

  1. Bundle

  2. 文件共享(不建议使用系统的SharedPreferences)

  3. Messenger(轻量级IPC,底层依然是AIDL)工作原理(Page70 & 代码)

  4. AIDL
    4.1. AIDL支持的数据类型:基本数据类型;String和CharSequence;List只支持ArrayList,里面每个元素都必须被AIDL支持;Map只支持HashMap,里面每个元素都必须被AIDL支持(包括key和value);Parcelable;AIDL接口本身;
    4.2. 服务端可以使用CopyOnWriteArrayList和ConcurrentHashMap来进行自动线程同步,客户端拿到的依然是ArrayList和HashMap;
    4.3. 服务端和客户端之间做监听器,服务端需要使用RemoteCallbackList,否则客户端的监听器无法收到通知(因为服务端实质还是一份新的序列化后的监听器实例,并不是客户端那份);
    dd. 客户端调用远程服务方法时,因为远程方法运行在服务端的binder线程池中,同时客户端线程会被挂起,所以如果该方法过于耗时,而客户端又是UI线程,会导致ANR,所以当确认该远程方法是耗时操作时,应避免客户端在UI线程中调用该方法。同理,当服务器调用客户端的listener方法时,该方法也运行在客户端的binder线程池中,所以如果该方法也是耗时操作,请确认运行在服务端的非UI线程中。另外,因为客户端的回调listener运行在binder线程池中,所以更新UI需要用到handler。
    4.4. 客户端通过IBinder.DeathRecipient来监听Binder死亡,也可以在onServiceDisconnected中监听并重连服务端。区别在于前者是在binder线程池中,访问UI需要用Handler,后者则是UI线程。
    4.5. 可通过自定义权限在onBind或者onTransact中进行权限验证。

  5. ContentProvider

  6. Socket 一般用于网络通信,AIDL用这种方式会过于繁琐,不建议。

  7. Binder连接池,通过BinderPool的方式将Binder的控制与Service本身解耦,同时只需要维护一份Service即可。这里用到了CountDownLatch,大概解释下用意:线程在await后等待,直到CountDownLatch的计数为0,BinderPool里使用它的目的是为了保证Activity获取BinderPool的时候Service已确定bind完成~

View 的事件体系

View的定义

可以把View理解成组合模式里的叶子结点和有枝节点的关系,本质都是Composite,而这里本质ViewGroup和View都是View,组合模式最大的好处就是遍历的时候不用关注是怎样的结点,因为抽象都是一样的。

View的位置参数

  1. View的宽高和坐标关系:width = right - left,height = top - bottom。
  2. View在平移过程中,top和left表示的是原始左上角的位置信息,其值不会改变,发生改变的是x、y、translationX、translationY这四个参数,x是View左上角的坐标,translation是view移动后相对于父容器(这里其实就是刚才说的左上角)的偏移量,所以有x = left + translationX。y的原理相同。

MotionEvent典型事件:ACTION_DOWN, ACTION_MOVE, ACTION_UP。

TouchSlop:系统所能识别的被认为是滑动的最小距离,我们可以用这个常量来判断用户的滑动是否达到阈值,提升用户体验。获取方法:ViewConfiguration.get(getContext()).getScaledTouchSlop()。

VelocityTracker加速度追踪:使用很简单,看书就好。经过测试一般建议类似ViewPager这样的控件,将时间间隔设置为1000(也就是1秒)时,加速度阈值设为1000-2000左右体验较好,各位可自行测试。

View的滑动

  1. ScrollBy和ScrollTo,简单归纳下: ScrollBy的0点在一般情况下均为可见的top那条线,有一种特殊情况就是(书中未提及)当某个ViewGroup在内部的layout的时候设置margin为负值的View时,0点会在可见top上方的高度(或宽度)为margin的地方,这个鬼东西实在是太绕口,具体的参考blog,参考headerView的设置。竖向滑动时,上滑ScrollY不断增加(所以应该传正值),下滑时ScrollY不断减少(所以应该传负值);同理,横向滑动时,左滑ScrollX不断增加,右滑不断减少。 ScrollTo可以理解为把View滑动到ScrollX或ScrollY为指定值的位置
  2. 动画:注意View动画的View移动只是位置移动,其本身还是在原来位置,会导致一些bug。
  3. 通过LayoutParams

Scoller

  1. TouchEvent的ACTION_UP事件中,用户滑动速度很快,但是滑动距离又不足以“翻页”的时候,通过scroller来帮助用户scrollBy掉滑动一页还需要的dx或dy。
  2. 当用户滑动到最上端或最下端时,我们仍然允许用户继续滑动,但是一旦松手,就把页面弹回到最上端和最下端的位置,用IOS的用户都知道IOS几乎所有页面都有这个弹性效果,用户体验非常好,其实我们用scroller也能轻松实现。
  3. 第三种场景和第一种类似,大家依然可以参考上面发的那篇仿网易的blog,现在不是翻页,当用户滑动的加速度很大的时候,我们认为用户需要滑动的距离肯定是不只他手从放下到松开的那段距离的,所以这种情况下我们需要通过scroller帮助用户去多滑一段,这个距离具体设置一般需要交互给出,设置的是滑加速度的十分之一,感觉还是太少,各位可以自行设置。

View事件的分发机制

  1. 三大方法关系的伪代码

    public boolean dispatchTouchEvent(MotionEvent ev) { 
        boolean consume = false;
        if(onInterceptTouchEvent(ev)) { 
            consume = onTouchEvent(ev);
        } else { 
            consume = child.dispatchTouchEvent(ev); 
        }
        return consume; 
    } 
    

关系很清楚了,如果当前View拦截事件,就交给自己的onTouchEvent去处理,否则就丢给子View继续走相同的流程。

  1. onTouchListener优先级高于onTouchEvent。
  2. 事件传递顺序:Activity -> Window -> View,如果View都不处理,最终将由Activity的onTouchEvent处理。
  3. 一些结论:拦截的一定是事件序列;不消耗ACTION_DOWN,则事件序列都会由其父元素处理;只消耗ACTION_DOWN事件,该事件会消失,消失的事件最终会交给Activity来处理;requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,除了ACTION_DOWN;

事件分发源码解析

  1. Window的实现类为PhoneWindow。

  2. 获取Activity的contentView的方法
    ((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0);

  3. View的滑动冲突处理,普通需求基本直接复用代码就能搞定。但是如果想要深刻理解,只有自己多写多测多读代码,才能很好的掌握,对于自定义View来说,掌握这个专题将对你的功力有大幅的提升。

View 的工作原理

ViewRoot和DecorView

  1. ViewRoot对应ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程均通过ViewRoot来完成。
  2. ActivityThread中,Activity创建完成后,会将DecorView添加到Window中,同时创建ViewRootImpl对象,并建立两者的关联。
  3. View的绘制流程从ViewRoot的performTraversals方法开始,经过measure、layout和draw三大流程。

MeasureSpec

  1. 复习下位运算…… a) <<左移,>>右移,所以源码中EXACTLY = 1 << MODE_SHIFT就相当于010000……..0000(30个0),其他可类推; b) &运算 0011 & 1100 = 0000,按位与;|或运算 0011 | 1100 = 1111,按位或; c)~取反运算,~0000 = 1111,所以(size & ~MODE_MASK) | (mode & MODE_MASK) 就好理解了;
  2. 三类specMode:UNSPECIFIED,基本无视;EXACTLY,精确大小或match_parent;AT_MOST,warp_content;
  3. 子View和父容器的MeasureSpec关系归纳:
    a. 子View为精确宽高,无论父容器的MeasureSpec,子View的MeasureSpec都为精确值且遵循LayoutParams中的值。
    b. 子View为match_parent时,如果父容器是精确模式,则子View也为精确模式且为父容器的剩余空间大小;如果父容器是wrap_content,则子View也是wrap_content且不会超过父容器的剩余空间。
    c. 子View为wrap_content时,无论父View是精确还是wrap_content,子View的模式总是wrap_content,且不会超过父容器的剩余空间。

View的工作流程:

onMeasure, onLayout, onDraw

  1. getSuggestedMinimumWidth的逻辑:View如果没有背景,那么返回android:minWidth这个属性指定的值,这个值可以为0;如果设置了背景,则返回背景的最小宽度和minWidth中的较大值。
  2. 一个比较好的习惯是在onLayout方法中去获取View的测量宽高或最终宽高。
  3. 如何在Activity初始化时获取View的宽高:
    a. Activity或者View的onWindowFocusChanged方法(注意该方法会在Activity Pause和resume时被多次调用)。
    b. view.post(new Runnable( {@Overiddepublic void run(){})});在run方法中获取。
    c. ViewTreeObserver中的onGlobalLayoutListener中。
    d. view.measure手动获取: match_parent:无法测量; 精确值:int wMeasureSpec = MeasureSpec.makeMeasureSpec(exactlyValue, MeasureSpec.EXACTLY); wrap_content:int wMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST); PS一下,还不懂(1<<30) - 1的,再多一句嘴,其实就是1111….11111(30个)
  4. layout中可能会使view的大小大于测量的大小
  5. draw的过程:绘制背景(background.draw(canvas)),绘制自己(onDraw()),绘制chidren(dispatchDraw),绘制装饰(onDrawScrollBars)
  6. setWillNotDraw方法用于在一个View不需要绘制时的优化(设置为true时)。

自定义View:

  1. 直接继承View或ViewGroup的需要自己处理wrap_content。
  2. View要在onDraw方法中要处理padding,而ViewGroup要在onMeasure和onLayout中处理padding和margin。
  3. View中的post方法可以取代handler。
  4. 在View的onDetachedFromWindow中停止动画,线程或回收其他资源。
  5. 滑动冲突处理。

理解RemoteViews

Notification

Notification的自定义View只能使用setTextViewText, setImageViewResource, setOnClickPendingIntent等固定方法来设置View,不能像操作普通View的方式来操作。

AppWidgetProvider

  1. 本质是一个广播,配置步骤:定义界面xml,定义配置信息xml,定义实现类(继承AppWidgetProvider),AndroidManifest中声明。
  2. 重要回调:onEnable,第一次被添加时调用,只有一次;onUpdate,添加或更新时回调;onDelete,每次删除时回调;onDisable,最后一次删除时回调;onReceive,接收广播的action。

PendingIntent

  1. 典型的使用场景就是和RemoteViews的点击事件配合使用;
  2. 支持三种待定Intent:Activity,Service和Broadcast
  3. PendingIntent相同的定义:内部的Intent和requestCode都相同。Intent相同的定义:两个Intent的componentName和intent-filter相同(不包括extras)
  4. flag定义:FLAG_NO_CREATE,基本不使用;FLAG_ONE_SHOT,以第一个为准,后续的会全部和第一条保持一致,任意一条被触发,其他的都cancel;FLAG_CANCEL_CURRENT,前面的相同的PendingIntent都会被cancel,只有最新的可用;FLAG_UDPATE_CURRENT,前面的PendingIntent都会被更新(它们Intent中的extras都会被更新)

RemoteViews的内部机制

  1. 支持的View类型:Layout:FrameLayout, LinearLayout, RelativeLayout, GridLayout; View: AnalogClock, Button, Chronometer, ImageButton, ImageView, ProgressBar, TextView, ViewFlipper, ListView, GridView, StackView, AdapterViewFlipper, ViewStub。
  2. 简单归纳,客户端的remoteViews通过action对象,由binder机制来更新服务端的remoteViews,所以RemoteViews本身也实现了Parcelable接口(参考图Page233)
  3. RemoteViews中真正操作View的方法apply和reapply,前者会加载布局并更新界面,后者则只更新界面。

跨进程的RemoteViews传递

模拟通知栏的实现。注意不同应用间RemoteViews的id不同,需要约定名称然后重新通过Resource.getIdentifier来获取。

Android 的 Drawable

Android 动画深入分析

理解 Window 和 WindowManager

一些基础知识:

  1. Window的实现类是PhoneWindow。
  2. Window的具体实现位于WindowManagerService中,WindowManager和WindowManagerService的交互是一个IPC过程。
  3. Window实际是View的直接管理者。

常用的WindowManager.LayoutParams的Flag和Type

  1. FLAG: FLAG_NOT_FOCUSABLE,当前Window不获取焦点,也不接收各种输入事件,会同时启用FLAG_NOT_TOUCH_MODAL,事件会传递给下层具有焦点的Window。 FLAG_NOT_TOUCH_MODAL,当前Window区域外的单击事件传递给底层,区域内的单击事件自己处理,一般都需要开启。 FLAG_SHOW_WHEN_LOCKED,可以让Window显示在锁屏界面上。
  2. Type: 应用Window,一般对应一个Activity。层级范围1~99。 子Window,不能单独存在,需要特定的父Window,比如一般的Dialog。层级范围1000~1999。 系统Window,需要权限声明,比如Toast。层级范围2000~2999。一般可以选用WindowManager.LayoutParams.TYPE_SYSTEM_ERROR,同时声明权限。
  3. WindowManager提供的功能:addView,updateViewLayout,removeView

Window的内部机制

Window并不实际存在,以View的形式存在。每个Window对应着一个View和ViewRootImpl,Window和View通过ViewRootImpl建立联系。所以在实际使用中其实我们并不能访问到真正的Window,而只能通过WindowManager。

  1. 几个重要的window类的关系
    这里写图片描述
  2. Window的添加过程
    a. WindowManagerGlobal中的addView:
    b. 检查参数是否合法;
    c. 如果子Window还需要调节布局参数;
    d. 创建ViewRootImpl并将View添加到列表中;
    e. 通过ViewRootImpl的setView来更新界面并完成Window的添加过程:
    requestLayout中的scheduleTraversals是View绘制的入口,最终通过WindowSession来完成Window的添加过程,注意其实这里是个IPC过程,最终会通过WindowManagerService的addWindow方法来实现Window的添加。

  3. Window的删除过程
    a. WinodwManagerGlobal中的removeView;
    b. findViewLocked来查找待删除待View的索引,再调用removeViewLocked来做进一步删除;
    c. removeViewLocked通过ViewRootImpl的die方法来完成删除操作,包括同步和异步两种方式,同步方式可能会导致意外的错误,不推荐,一般使用异步的方式,其实就是通过handler发送了一个删除请求,将View添加到mDyingViews中;
    d. die方法本质调用了doDie方法,真正删除View的逻辑在该方法的dispatchDetachedFromWindow方法中,主要做了四件事:垃圾回收,通过Session的remove方法删除Window,调用View的dispatchDetachedFromWindow方法同时会回调View的onDetachedFromWindow以及onDetachedFromWindowInternal,调用WindowManagerGlobal的doRemoveView刷新数据。

  4. Window的更新过程
    a. WindowManagerGlobal的updateViewLayout;
    b. 更新View的LayoutParams;
    c. 更新ViewImple的LayoutParams,实现对View的重新测量,布局,重绘;
    d. 通过WindowSession更新Window的视图,WindowManagerService.relayoutWindow()。

Window的创建过程

  1. Activity
    a. Activity的attach方法中,系统会创建Activity所属的Window并为其设置回调;
    b. Window对象的创建通过PolicyManager的makeNewWindow方法;
    c. Window的具体实现是PhoneWindow类;
    d. Window创建好之后,通过PhoneWindow的setContentView将Activity与Window进行关联,这个方法大致步骤:
    d.1. 如果没有DecorView就创建,id是android.R.id.content;
    d.2. 将Activity设置的ContentView设置到DecorView的mContentParent中;
    d.3. 回调Activity的onContentChanged方法通知Activity视图已经发生改变;
    d.4. Activity onResume的时候会调用Activity的makeVisible方法真正完成DecorView的添加和显示。
  2. Dialog
    a. 通过PolicyManager的makeNewWindow方法创建Window;
    b. 初始化DecorView,和Activity类似;
    c. Dialog的show方法中,通过WindowManager将DecorView添加到Window中;
    d. Dialog关闭时,会通过WindowManager来移除DecorView,方法为removeViewImmediate(mDecor);
    e. 想要创建一个使用application context的Dialog可按照本章2-2的方法设置,dialog.getWindow.setType(WindowManager.LayoutParams.TYPE_SYSTEM_ERROR),记得在manifest中设置权限。
  3. Toast
    a. Toast内部有两类IPC:Toast访问NotificationManagerService;NotificationManagerService(下文简称NMS)访问Toast的TN接口;
    b. Toast属于系统Window,内部视图mNextView一种为系统默认样式,另一种通过setView方法来指定一个自定义View。
    c. TN是一个Binder类,NMS处理Toast的显示隐藏请求时会跨进程回调TN中的方法,所以TN运行在Binder线程池中,所以需要handler切换到当前发送Toast请求的线程中,也就是说没有Looper的线程是无法弹出Toast的。
    d. Toast的show方法调用了NMS的enqueueToast方法,该方法先将Toast请求封装成ToastRecord并丢入mToastQueue队列中(非系统应用最多塞50个)。
    e. NMS通过showNextToastLocked方法来显示当前View,Toast显示由ToastRecord的callback方法中的show方法完成,callback其实就是TN对象的远程Binder,所以最终调用的是TN中的方法,并运行在发起Toast请求应用的Binder线程池中。
    f. 显示以后,NMS通过scheduleTimeoutLocked方法发送延时消息,延时后NMS通过cancelToastLocked方法来隐藏Toast并从队列中移除,隐藏依然通过ToastRecord的callback中的hide方法实现。
    g. callback回调TN的show和hide方法后,会通过handler发送两个Runnable,里面的handleShow和handleHide方法是真正完成显示和隐藏Toast的地方。handleShow方法中将Toast的视图添加到Window中,handleHide方法将Toast视图从Window中移除。

四大组件的工作过程

四大组件概述:

  1. Activity的主要作用是展示一个界面并和用户交互,它扮演的是一种前台界面的角色。
  2. Service是一种计算型组件,用于在后台执行一系列计算任务,但因为其本身还是运行在主线程中的,因此耗时的后台计算仍然需要在单独的线程中去完成。
  3. BroadcastReceiver是一种消息型组件,用于在不同的组件乃至不同的应用之间传递消息。广播注册有两种方式,动态注册通过Context.registerReceiver()来实现,必须要应用启动才能注册;静态注册则在AndroidManifest文件中进行,应用安装时会被系统解析,不需要启动应用就可接收广播。
  4. ContentProvider是一种共享型组件,用于向其他组件乃至其他应用共享数据。
startActivity.png
startService.png
bindService.png

Android 的消息机制

三大件

Hanlder,MessageQueue,Looper。

  1. MessageQueue内部的数据结构并非队列,而是单链表,它只是用来存储数据。
  2. Looper是真正的数据处理者,线程默认没有Looper,使用Handler必须为线程创建Looper,UI线程也就是ActivityThread创建时会初始化Looper,所以主线程中默认可以直接使用Handler。
    UI线程检查当前线程的操作在ViewRootImpl的checkThread方法中,我们常见的不能在子线程中访问view的异常就是在这里抛出的。
    不允许子线程访问主线程的原因是UI控件不是线程安全的,而加锁又会导致UI的操作过于复杂。
  3. Handler的工作过程,图(page374),单概括一下就是:假设创建Handler的线程是A,耗时操作的线程是B,B线程中拿到handler实例发送消息或者post一个Runnable,实际是调用了MessageQueue的enqueueMessage方法,进入了消息队列,线程A中的Looper发现了这个消息,就会处理这个消息,也就是消息中的Runnable或者handler的handleMessage方法会被调用,这样handler中的业务逻辑就被切换到线程A中去了。
  4. ThreadLocal看上去只new了一份,但在每个不同的线程中却可以拥有不同数据副本的神奇类。其本质是ThreadLocal中的Values类维护了一个Object[],而每个Thread类中有一个ThreadLocal.Values成员,当调用ThreadLocal的set方法时,其实是根据一定规则把这个线程中对应的ThreadLocal值塞进了Values的Object[]数组中的某个index里。这个index总是为ThreadLocal的reference字段所标识的对象的下一个位置。

三大件原理

  1. MessageQueue的工作原理:主要方法为enqueueMessage和next。
    a. enqueueMessag主要就是一个单链表的插入操作,
    b. next方法是一个无限循环,如果消息队列中没有消息,next方法就阻塞,有新消息到来时,next方法会返回这条消息并将其从单链表中删除。
    2。 Looper的工作原理:
    a. prepare方法,为当前没有Looper的线程创建Looper。
    b. prepareMainLooper和getMainLooper方法用于创建和获取ActivityThread的Looper。
    c. quit和quitSafely方法,前者立即退出,后者只是设定一个标记,当消息队列中的所有消息处理完毕后会才安全退出。子线程中创建的Looper建议不需要的时候都要手动终止。 d. loop方法,死循环,阻塞获取msg并丢给msg.target.dispatchMessage方法去处理,这里的target就是handler。
  2. Handler的工作原理:
    a. 无论sendMessage还是post最终都是调用的sendMessageAtTime方法。
    b. 发送消息其实就是把一条消息通过MessageQueue的enqueueMessage方法加入消息队列,Looper收到消息就会调用handler的dispatchMessage方法。它的处理过程参考书page388的流程图,一看就懂~
    c. 这里我补充一个东西,当我们直接Handler h = new Handler()时,本质调用的是Handler(Callback callback, Boolean async)构造方法,这个方法里会调用Looper.myLooper()方法,这个方法其实就是返回的ThreadLocal里保存的当前线程的Looper,这也就解释了为什么我们在主线程中这样new没有问题,子线程中如果不先Looper.prepare会抛出异常的原因,前面多次说了,因为ActivityThread会在初始化的时候创建自己的Looper。
    主线程的消息循环:


    这里写图片描述

Android 的线程和线程池

Bitmap 的加载和 Cache

综合技术

JNI 和 NDK 编程

Android 性能优化

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

推荐阅读更多精彩内容