急速开发系列——RxJava实战技巧大全

这几天一直看《激荡三十年》,感觉非常不错。这篇文章本身并不想写,因为总觉得没什么含量。后面写着写着,发现其中的有些点还是非常有意思的,因此这篇文章的重点在rxjava使用场景说明及rxjava内存优化上。rxjava的使用场景更多的取决于我们所面临的业务以及当前rxjava的一些操作符是否提供了对业务的支持,而对于内存优化上,则是要求我们尽可能快的解除订阅关系。

另外本文对RxBinding,RxLifecycle的介绍并未做过多的深入,原因在于这两个库无论是从实现还是从使用角度来说都是比较简单的,我们完全可以自行书写类似的库。

话不多说,步入正题。推荐关注[江湖人称小白哥][2]


引入RxBinding

在引入RxBinding之前首先要弄明吧RxBinding是什么?。

所谓的RxBinding是用来为界面元素绑定事件的,比如为Buttong设置点击事件等。浏览其源码,不难发现其实现原理也是通过包装原有事件实现的。

为什么要引入RxBinding?使用RxJava一定要引入RxBinding么?

首先很确定的说使用RxJava不要求你一定要使用RxBinding,大部分情况下没必要用。这里之所以要谈RxBinding一方面是完善我们的知识体系,看看响应式编程的思想是如何应用在android界面元素上,另一方面是看看RxBinding能够有效的解决什么问题?

RxBinding提供了和RxJava一致的api体验,更重要的是它更好的符合RxJava做法:通过将事件转化为Observable对象,最终可以利用RxJava一系列操作符对其处理,最典型的使用场景是界面防抖动,这点我们在rxjava真实应用场景中做详细的介绍。

关于如何使用RxBinding,直接参见RxBinding项目说明即可:https://github.com/JakeWharton/RxBinding


rxjava真实应用场景

在上一篇文章中为解决异常问题,我们引入了rxjava的支持。接下来我们来看看rxjava在实际工程中的显著应用。

场景零:线程切换

rxjava引入让使得线程切换更加的容易,几行代码就可以搞定。RxAnroid的引入更是让我们非常容易的能够切换到UI线程。可以说,引入RxJava,就放弃古老而沉重的AsyncTask吧(初学者还是要学AsyncTask的)。最典型的就是从网络中获取数据,然后在更新界面,很显然获取数据操作需要发生在子线程,更新UI操作发声明在主线程。这里我们以模拟从数据库中获取联系人操作为例:

private void getConcactFromDB() {
        Observable.create(new Observable.OnSubscribe<List<String>>() {
            @Override
            public void call(Subscriber<? super List<String>> subscriber) {
                //模拟从数据库中获取数据
                ArrayList<String> list = new ArrayList<>();
                for (int i = 0; i < 100; i++) {
                    list.add("user name:" + i);
                }
                //模拟耗时操作
                SystemClock.sleep(5000);

                subscriber.onNext(list);
                subscriber.onCompleted();
            }
        }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Action1<List<String>>() {
                    @Override
                    public void call(List<String> list) {
                        Log.d("MainActivity", "更新界面:" + list.size());
                    }
                });
    }

场景一:接口依赖(flatmap)

目前大部分服务端接口设计都是通过用户名和密码登录获取access token,后面其他api的请求都是借助该token。对于需要注册功能的产品来说,我们经常面对这样的问题:使用用户名和密码登录成功后,保存服务器返回的access token,再调用服务端接口获取用户的详情信息。不难发现,这里获取用户详情的请求依赖登录请求.我们先来看传统方法是如何解决这问题:

private void handleLogin2(LoginPost post) {
        ApiFactory.getBaseApi().login(post).subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new BaseSubscriber<Result<Token>>(this){
                    @Override
                    public void onNext(Result<Token> tokenResult) {
                        if (tokenResult.isOk()) {
                            Token data = tokenResult.getData();
                            String token = data.getToken();
                           //保存token操作 ApiFactory.getUserApi().getUserProfile(token).subscribeOn(Schedulers.io())
                                    .observeOn(AndroidSchedulers.mainThread())
                                    .subscribe(new BaseSubscriber<Result<User>>(LoginActivity.this){
                                        @Override
                                        public void onNext(Result<User> userResult) {
                                            //处理用户信息
                                        }

                                        @Override
                                        public void onError(Throwable e) {
                                            //处理错误
                                        }
                                    });
                        }
                    }

                    @Override
                    public void onError(Throwable e) {
                        //处理错误
                    }
                });
    }

开始时,我们大部分人会写出类似以上的代码。当然,这实现了我们想要的逻辑,但当你仔细思考的时候,会发现几个问题:

  1. 回调嵌套看起来令人疑惑。由于我们在大多数情况下是线性思维,那么此时当你看到onNext(Result<Token> tokenResult)中又去嵌套处理获取用户信息的接口你的思维不得不跳跃一下。
  2. 登录功能的异常处理点被分隔了,使我们不得不写出冗余的代码。
  3. 多次线程开销好像可以被进一步优化。

实际上这三个问题的根本原因在于我们在实现登录功能的时候是以方法作为最小单位,而不是以登录逻辑为最小单位,因此看起不是那么的连贯。现在来看看我们应该怎么样让上面的代码具有连贯性:

private void handleLogin(LoginPost post) {
        ApiFactory.getBaseApi().login(post).flatMap(new Func1<Result<Token>, Observable<Result<User>>>() {

            @Override
            public Observable<Result<User>> call(Result<Token> tokenResult) {
                if (tokenResult.isOk()) {//获取token成功
                    Token data = tokenResult.getData();
                    String token = data.getToken();
                    //保存token操作
                    return ApiFactory.getUserApi().getUserProfile(token);//获取用户信息
                } else {//获取token,直接触发onError()方法
                    return Observable.error(new ApiException(tokenResult.getCode(), tokenResult.getMsg()));
                }
            }
        }).subscribeOn(Schedulers.io())
                .doOnSubscribe(new Action0() {
                    @Override
                    public void call() {
                        showWaitDialog();
                    }
                }).subscribeOn(AndroidSchedulers.mainThread())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new BaseSubscriber<Result<User>>(this) {
                    @Override
                    public void onCompleted() {
                    }

                    @Override
                    public void onNext(Result<User> userResult) {
                        //处理用户信息

                    }

                    @Override
                    public void onError(Throwable e) {
                       、//处理错误
                    }
                });


    }

通过flatmap操作符,不但解决了接口依赖问题,而且使得代码逻辑相比之前更具有连贯性。另外,这里引入的BaseSubscriber在我们[Retrofit响应数据及异常处理策略][1]做过说明了,不明白的同学可以自行查阅。

场景二:接口合并(merge)

很多情况下,一个界面中需要的数据来自多个数据源(请求),而只有当所有的请求的响应数据都拿到之后才能渲染界面。

接口结果同类型

当前数据源来自多个渠道,拿到的结果属于同一类型的,比如有些数据需要从本地数据读取,而另一些数据则从网络中获取,但无论哪个数据源今最后返回的数据类型是一样的,比如:

 private Observable<ArrayList<String>> getDataFromNet() {
        ArrayList<String> list = new ArrayList<>();
        for(int i=0;i<10;i++) {
            list.add("data from net:" + i);
        }

        return Observable.just(list);
    }

    private Observable<ArrayList<String>> getDataFromDisk() {
        ArrayList<String> list = new ArrayList<>();
        for(int i=0;i<10;i++) {
            list.add("data from disk:" + i);
        }

        return Observable.just(list);
    }

上面的两个方法分别从磁盘和网络中获取数据,且最后的数据类型都是ArrayList<String>,现在我们来合并这两个接口:

   private void getData() {
        Observable.merge(getDataFromDisk(), getDataFromNet()).subscribe(new Subscriber<ArrayList<String>>() {
            @Override
            public void onCompleted() {
                //更新界面
            }

            @Override
            public void onError(Throwable e) {

            }

            @Override
            public void onNext(ArrayList<String> list) {
                for (String s : list) {
                    Log.d("MainActivity", s);
                }
            }
        });

接口结果不同类型

有些情况下,不同数据源返回的结果类型不一致,那该如何解决呢?比如当前存在两个接口:

@GET("dict/locations")
Observable<Result<ArrayList<String>>> getLocationList();

@GET("user")
Observable<Result<User>> getUserInfo(@Query("id") String id);

只有当这两个请求都完成后才能更新UI,那我们该怎么做呢?同样还是使用merge操作符,关键在于如何区分响应:

    private void getData(String uid) {
        Observable<Result<ArrayList<String>>> locationOb = ApiFactory.getUserApi().getLocationList();
        Observable<Result<User>> userOb = ApiFactory.getUserApi().getUserInfo(uid);

        Observable.merge(locationOb,userOb).subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Subscriber<Result<? extends Object>>() {
                    @Override
                    public void onCompleted() {
                        //更新UI
                    }

                    @Override
                    public void onError(Throwable e) {

                    }

                    @Override
                    public void onNext(Result<? extends Object> result) {
                        Object data = result.getData();
                        if(data instanceof User ){
                            //处理用户数据
                        } else if (data instanceof ArrayList) {
                            //处理位置列表
                        }
                    }
                });

    }

场景三:构建多级缓存(concat)

缓存机制想必是众所周知。这里我们就以常见的三级缓存机制为例:首先从内存中获取数据,如果内存中不存在,则从硬盘中获取数据,如果硬盘中不存在数据,则从网络中获取数据。现在看看RxJava是如何帮我们解决这个问题:

 //获取数据
 private void getData(String url) {
        
        Observable.concat(getDataInMemory(url),getDataInDisk(url),getDataInNet(url)).takeFirst(new Func1<Bitmap, Boolean>() {
            @Override
            public Boolean call(Bitmap bitmap) {
                return bitmap!=null;
            }
        }).observeOn(AndroidSchedulers.mainThread()).subscribe(new Action1<Bitmap>() {
            @Override
            public void call(Bitmap bitmap) {
                //处理图片
            }
        });
    }

    //从内存中获取
    private Observable<Bitmap> getDataInMemory(final String url) {
        final Map<String, Bitmap> memoryCache = new HashMap<>();
        //模拟内存中的数据
        //...

        return Observable.create(new Observable.OnSubscribe<Bitmap>() {

            @Override
            public void call(Subscriber<? super Bitmap> subscriber) {
                if (memoryCache.containsKey(url)) {
                    subscriber.onNext(memoryCache.get(url));
                }
                subscriber.onCompleted();
            }
        });
    }

    //从硬盘中获取
    private Observable<Bitmap> getDataInDisk(final String url) {
        final Map<String, Bitmap> diskCache = new HashMap<>();
        //模拟内存中的数据
        //...

        return Observable.create(new Observable.OnSubscribe<Bitmap>() {

            @Override
            public void call(Subscriber<? super Bitmap> subscriber) {
                if (diskCache.containsKey(url)) {
                    subscriber.onNext(diskCache.get(url));
                }
                subscriber.onCompleted();
            }
        });

    }

    //从网络中获取
    private Observable<Bitmap> getDataInNet(final String url) {
        return Observable.create(new Observable.OnSubscribe<Bitmap>(){

            @Override
            public void call(Subscriber<? super Bitmap> subscriber) {
                Bitmap bitmap=null;
                //从网络获取图片bitmap

                subscriber.onNext(bitmap);
                subscriber.onCompleted();
            }
        }).subscribeOn(Schedulers.io());
    }

rxjava为我们提供的concat操作符可以很容的的实现多级缓存机制。这里需要记住在getData()方法中不要忘记使用takeFirst()。concat操作符接受多个Observable,并按其顺序串联,
在订阅的时候会返回所有Observable的数据(按顺序依次返回)。换言之,如果在getData()中不实用takeFirst(),将会并行的从内存,硬盘及网络中检索数据,这显然不是我们想要的。takeFirst操作符可以从返回的数据中取出第一个,并中断数据检索的过程。我们知道,检索速度:内存>硬盘>网络,这就意味着当我们从内存中获取到数据的时候就不会再从硬盘中获取数据,反之,则从硬盘中获取数据;当我们从硬盘中获取到数据的时候就不会再从网络中获取到数据了,反之,则从网络中获取。

这样就实现了我们的最终目标。

场景四:定时任务(timer)

在一些情况下我们需要执行定时任务,传统的做法上有两种方式可选择:Timer和SchelchExector。但是在引入rxjava之后,我们有了第三种选择:

    private void startTimerTask() {
        Observable.timer(2, TimeUnit.SECONDS).subscribe(new Action1<Long>() {
            @Override
            public void call(Long aLong) {
                Log.d("MainActivity", "start execute task:" + Thread.currentThread().getName());
            }
        });
    }

场景五:周期任务(interval)

当然rxjava通过interval提供了周期任务的支持:

 private void startIntervalTask() {
        Observable.interval(5, TimeUnit.SECONDS).subscribe(new Action1<Long>() {
            @Override
            public void call(Long aLong) {
                Log.d("MainActivity", "start task:" + Thread.currentThread().getName());
            }
        });
    }

场景六:数据过滤(filter)

在处理集合时,我们经常需要过滤操作,这时候使用filte操作符就非常有用,用个简单示例:

   private void dataFilter() {
        final HashSet<String> hashSet = new HashSet<>();
        hashSet.add("1");
        hashSet.add("2");

        ArrayList<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("");

        Observable.from(list).filter(new Func1<String, Boolean>() {
            @Override
            public Boolean call(String s) {
                return !TextUtils.isEmpty("") && !hashSet.contains(s);
            }
        }).subscribe(new Action1<String>() {
            @Override
            public void call(String s) {
                Log.d("MainActivity", "result: " + s);
            }
        });

    }

场景七:界面防抖动(throttleFirst)

所谓的界面防抖动就是用于处理快速点击某控件导致重复打开界面的操作,比如点击某个button可以打开一个Activity,正常情况下,我们一旦点击了该Button便会等待该Activity。在应用响应比较慢,用户以为无响应而多次点击Button或者恶意快速点击的情况下,会重复打开同一个Activity,当用户想要退出该Activity的时候体验会非常差。

通过rxjava提供的throttleFirst操作符我们能够很容易防止按钮在单位时间内被重复点击的问题:

RxView.clicks(mBtnTest2).throttleFirst(1L, TimeUnit.SECONDS).subscribe(new Action1<Void>() {
        @Override
        public void call(Void aVoid) {
            Toast.makeText(MainActivity.this, "button2 clicked", Toast.LENGTH_SHORT).show();
        }
    });

场景八:老接口适配(just)

当你在为老项目添加rxjava支持的时候,难免需要将一些方法返回类型转为Observable.通过just操作符不需要对原方法进行任何修改便可实现:

private int oldMethod(int x, int y) {
        return x+y;
    }

private void addTest() {
    Observable.just(oldMethod(4, 9)).subscribe(new Action1<Integer>() {
        @Override
        public void call(Integer result) {
            Log.d("MainActivity", "result:" + result);
        }
    });
    }

    

场景十:响应式界面

界面元素更新

在信息填充界面时,我们经常会遇到只有填写完必要的信息之后,提交按钮才能被点击的情况。比如在登录界面时,只有我们填写完用户名和密码之后,登录按钮才能被点击。通过借助rxjava提供的combineLatest操作符我们可以容易的实现这种响应式界面

  EditText mEditUsername = (EditText) findViewById(R.id.editText3);
        EditText mEditPwd = (EditText) findViewById(R.id.editText4);
        final Button mBtnLogin = (Button) findViewById(R.id.button2);
        mBtnLogin.setEnabled(false);

        Observable<CharSequence> usernameOb = RxTextView.textChanges(mEditUsername);
        Observable<CharSequence> pwdOb = RxTextView.textChanges(mEditPwd);

        Observable.combineLatest(usernameOb, pwdOb, new Func2<CharSequence, CharSequence, Boolean>() {
            @Override
            public Boolean call(CharSequence username, CharSequence pwd) {

                return !TextUtils.isEmpty(username) && !TextUtils.isEmpty(pwd);
            }
        }).subscribe(new Action1<Boolean>() {
            @Override
            public void call(Boolean isLogin) {
                mBtnLogin.setEnabled(isLogin);
            }
        });

RxJava内存优化

内存优化

借助rxjava提供的线程调度器Scheduler我们可以很容的实现线程切换,目前Scheduler提供了一下几种调度策略:

  • Schedulers.immediate():默认的调度策略,不指定线程,也就是运行在当前线程
  • Schedulers。newThread():运行在一个新创建的线程当中,相当于new Thread()操作。
  • Schedulers.io():采用了线程池机制,内部维护了一个不限制线程数量的线程池,用于IO密集型操作。
  • Schedulers.computation():同样采用了线程池机制,只不过线程池中线程的数量取决与CPU的核数,以便实现最大性能。通常用于CPU密集型操作,比如图形处理。

通过上面的介绍,我们基本能做出以下使用规则:对于网络请求及读写大量本地数据等操作,既可以采用Schedulers.newThread()也可以采用Schedulers.io(),但是优先采用Schedulers.io(),对于计算量比较大的,当然是采用Schedulers.computation()。
这样,我们既能达到较好的性能,又尽可能的减少内存占用。

内存泄漏

尽管rxjava非常简单易用,但是随着订阅的增多内存开销也会随之增大,尤其是在配合使用网络请求的时候,稍不注意就容易造成内存泄漏。早期我也犯过多次这种错误。

当我们不需要的时候,主动取消订阅。比如在下面的代码中,我们开启一个周期任务用来不断的输出信息,那么我们需要在该Activity被销毁的时候调用mSubscription.unsubscribe()来主动的解除订阅关系防止内存泄漏。

public class MainActivity extends AppCompatActivity {

    private Subscription mSubscription;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button mBtnTest1 = (Button) findViewById(R.id.btn_test1);

        mBtnTest1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mSubscription = startIntervalTask();
            }
        });
    }

    private Subscription startIntervalTask() {
        return Observable.interval(5, TimeUnit.SECONDS).subscribe(new Action1<Long>() {
            @Override
   
            public void call(Long aLong) {
                Log.d("MainActivity", "start task:" + Thread.currentThread().getName());
            }
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //主动解除订阅关系
        if (mSubscription != null && !mSubscription.isUnsubscribed()) {
            mSubscription.unsubscribe();
        }
    }
}

看完上面简单的示例,想必你也明白rxjava所造成的内存泄漏往往和组件的生命周期相关。也就是我们要重点关注那些在在组件销毁之后,订阅关系却仍然存在的情况。大部分情况下,当我们的视图销毁之后,订阅关系就没有必要存在了,所以需要我们主动取消订阅即可。

存在一种特殊情况:当我们进入某个界面后,往往会发出网络请求,在返回数据后首先需要缓存数据,然后在更新界面视图。这种情况下当然不能在视图销毁后立刻解除订阅关系。那么这里需要注意的是更新UI之前需要自行判断当前视图是否存在,存在则更新,不存在就没有必要更新了。

在我们的工程中,往往存在很多个视图(Activity,Fragment等),如果在每个视图当中都要手动的解除订阅关系是件很繁琐的事情。这里有两种方式:一是在基类当中,比如BaseActivity,BaseFragment中统一取消订阅,另外一种方式就是使用RxLifeCycle这个库。

使用RxLifecycle

RxLifeCycle可以帮助我们在组件生命周期的某个阶段或者指定事件发生时自动取消订阅,其项目地址在:https://github.com/trello/RxLifecycle,如果你不介意引入新的库,建议使用。

compile 'com.trello:rxlifecycle:0.3.1'
compile 'com.trello:rxlifecycle-components:0.3.1'

RxLifeCycle主要提供了两个方法bindToLifecycle()和bindUntilEvent(),分别用来绑定生命周期和事件。

绑定生命周期(bindToLifecycle())

绑定生命周期的做法本质上是通过监听组件(Activity,Fragment)生命周期的变化来自动解除订阅关系。用法如下:

//1. 需要继承对应的RxAppCompatActivity
public class MainActivity extends RxAppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @Override
    protected void onResume() {
        super.onResume();
        //2. 在onResume中绑定,将在对应的onPause()方法中解除订阅关系
        //在onStart()中绑定,将在对应的onStop()中解除订阅关系
        //在onCreate()中绑定,将在对应的onDestory()中解除订阅关系
        startIntervalTask();
    }

    private Subscription startIntervalTask() {
        return Observable.interval(5, TimeUnit.SECONDS).compose(this.<Long>bindToLifecycle()).doOnUnsubscribe(new Action0() {
            @Override
            public void call() {
                Log.d("MainActivity", "解除订阅");
            }
        }).subscribe(new Action1<Long>() {
            @Override
            public void call(Long aLong) {
                Log.d("MainActivity", "start task:" + Thread.currentThread().getName());
            }
        });
    }

}

绑定生命周期的方式适应于在一开始就能够确定开始点,即能明确的知道订阅关系发生在哪个阶段(onCreate()?onResume(),onStart()),这样才能在恰当的方式解除订阅关系。比如在onResume()中联网获取用户数据,那么需要在onPause()中解除。

绑定事件(bindUntilEvent())

RxLifecycle中将组件生命周期的各个阶段转化为相对应的事件,因此绑定事件的方式和绑定生命周期的方式并无太大区别。和使用bindToLifecycle()不一样的是,绑定事件的方式只关心何时解除订阅关系。因为在很多情况下,我们所做的操作并不一定是在onResume()开始,在onPause()结束,此时显然不能用绑定生命周期的方法。来看看绑定事件如何使用:

//1. 需要继承对应的RxAppCompatActivity
public class MainActivity extends RxAppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        Button mBtnTest1 = (Button) findViewById(R.id.btn_test1);
        
              mBtnTest1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //
                startIntervalTask();
    }


    //ActivityEvent提供了相关的事件
    //当pause发生时,自动解除订阅关系
    private Subscription startIntervalTask() {
        return Observable.interval(5, TimeUnit.SECONDS).compose(this.bindUntilEvent(ActivityEvent.PAUSE)).doOnUnsubscribe(new Action0() {
            @Override
            public void call() {
                Log.d("MainActivity", "解除订阅");
            }
        }).subscribe(new Action1<Object>() {
            @Override
            public void call(Object o) {
                Log.d("MainActivity", "start task");
            }
        });
    }

}

-----------------

总结
==============
这里介绍了有关rxjava一些实际应用场景。尽管rxjava看起来非常容易 使用,但其内存使用问题需要我们重点关注。


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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,431评论 25 707
  • 我从去年开始使用 RxJava ,到现在一年多了。今年加入了 Flipboard 后,看到 Flipboard 的...
    Jason_andy阅读 5,451评论 7 62
  • Github:https://github.com/ReactiveX/RxJavahttps://github....
    才兄说阅读 1,625评论 2 10
  • 今天是2017年10月13日,天气有些凉。 <一> 女儿推我买菜的车去上学。她说,车在前面走,车走过的路我走,我走...
    季遇阅读 172评论 0 0
  • 曾梦想仗剑走天涯 看一看世界的繁华 年少的心总有些轻狂 如今你四海为家 ——许巍《曾经的你》 前晚,和洪哥一起去花...
    晓彬Jerome阅读 567评论 0 1