Rxjava2 + Retrofit +DBflow + 自定義緩存框架搭建

最喜歡干的事霉颠,莫過于拿著工資搭框架了。
其實這個框架已經(jīng)出來很久了荆虱,并不是什么新鮮玩意兒了蒿偎,只不過我一直沒有嘗試著去寫一篇內(nèi)容比較大的文章來分享,這次就賣弄一下怀读,希望各種大神輕噴诉位,有什么問題也希望各位大神不吝賜教。

Retrofit的接入

ApiService

首先Retrofit的框架架構(gòu)搭建其實比較簡單菜枷,因為Retrofit本身已經(jīng)極致簡單了苍糠。

/**
 * Author       : yizhihao (Merlin)
 * Create time  : 2017-08-23 15:48
 * contact      :
 * 562536056@qq.com || yizhihao.hut@gmail.com
 */
public interface ApiService {

    @GET("{url}")
    Observable<ResponseBody> executeGet(
            @Path("url") String url,
            @QueryMap Map<String, String> maps);


    @POST("{url}")
    Observable<ResponseBody> executePost(
            @Path("url") String url,
            @QueryMap Map<String, String> maps);

    @POST("{url}")
    Observable<ResponseBody> executeCachePost(
            @Path("url") String url,
            @QueryMap Map<String, String> maps);

    @POST("{url}")
    Observable<ResponseBody> uploadFiles(
            @Path("url") String url,
            @Path("headers") Map<String, String> headers,
            @Part("filename") String description,
            @PartMap()  Map<String, RequestBody> maps);

    @Streaming
    @GET
    Observable<ResponseBody> downloadFile(@Url String fileUrl);
}

上面的代碼通過將接口返回類型通用化返回結(jié)合rxjava的Observable這樣我們就可以愉快的用rxjava來處理線程切換了。

Retrofit接口對象
public static Retrofit retrofit() {
        return retrofit(sBaseUrl);
    }

    public static Retrofit retrofit(String baseUrl) {
        return new Retrofit.Builder()
                .baseUrl(baseUrl)
                .client(getInstance().getHttpClient())//添加自定義OkHttpClient
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create(GsonUtils.getInstance().getGson()))
                .build();
    }

    public OkHttpClient getHttpClient() {
        if (client == null) {
            client = new OkHttpClient.Builder()
                 //.addNetworkInterceptor(newCacheNetworkInterceptor())
                    //日志啤誊,可以配置 level 為 BASIC / HEADERS / BODY
                    .addInterceptor(new LoggingInterceptor()) 
                    .connectTimeout(DEFAULT_TIME_OUT, TimeUnit.SECONDS)
                    //.cache(provideCache())
                    .retryOnConnectionFailure(true)
                    .build();
        }
        return client;
    }

Retrofit管理類主要是整合okhttp進行必要的配置

緩存攔截器

細心的讀者可能發(fā)現(xiàn)了CacheNetworkInterceptor這個注釋的攔截器岳瞭,它的職責(zé)本來要添加的NetworkInterceptor是為了做緩存Hook的。
但是查閱了一些資料蚊锹,還有okhttp源碼瞳筏,其實okhttp本身是自帶緩存邏輯的,這套邏輯完全遵守RFC協(xié)議進行緩存控制的枫耳。很多人都去hook掉了這步乏矾。其實查閱源碼可以看到


image.png

從源碼中不難看出,我們在自定義cache的時候,okhttp會把自己的internalCache給廢棄掉钻心,而我們在okhttp的內(nèi)部攔截器中也會看到CacheInterceptor凄硼,這個類其實就是實現(xiàn)了okhttp的Cache-control。所以我并沒有選擇去攔截Response手動添加Cache-control進行緩存處理捷沸。當然大家要用我也攔不住摊沉,畢竟也挺方便的。

日志攔截器

LoggingInterceptor攔截器主要是為了打印請求發(fā)送和收到請求的Log.

public class LoggingInterceptor implements Interceptor {
    private boolean debugMode = DebugConstant.isDebug;

    @Override
    public Response intercept(Chain chain) throws IOException {
        if(!debugMode){
            return chain.proceed(chain.request());
        }
        //這個chain里面包含了request和response痒给,所以你要什么都可以從這里拿
        Request request = chain.request();

        long t1 = System.nanoTime();//請求發(fā)起的時間
        LogUtils.e(String.format("發(fā)送請求 %s on %s%n%s", request.url(), chain.connection(), request.headers()));

        Response response = chain.proceed(request);

        long t2 = System.nanoTime();//收到響應(yīng)的時間

        //這里不能直接使用response.body().string()的方式輸出日志
        //因為response.body().string()之后说墨,response中的流會被關(guān)閉,程序會報錯苍柏,我們需要創(chuàng)建出一
        //個新的response給應(yīng)用層處理
        ResponseBody responseBody = response.peekBody(1024 * 1024);

        LogUtils.d(String.format("接收響應(yīng): [%s]" +
                        "\n %n返回json:【%100s】 " +
                        "\n請求執(zhí)行時間%.1fms" +
                        "\n%n%s",
                response.request().url(),
                responseBody.string(),
                (t2 - t1) / 1e6d,
                response.headers()));
        return response;
    }
}

加上日志攔截器之后log如下圖


image.png

看到打印出來的詳細的log有木有感覺很酸爽尼斧。

Rxjava的封裝

綁定Activity生命周期

對rxjava中的subcriber的封裝,這里主要是將activity的生命周期和subcriber綁定聯(lián)系起來试吁,當activity被finish的時候我們的subcriber也應(yīng)該dispose取消掉棺棵。

   private CompositeDisposable disposables2Stop;// 管理Stop取消訂閱者者
    private CompositeDisposable disposables2Destroy;// 管理Destroy取消訂閱者者

在baseaActivity中通過CompositeDisposable組合管理添加進來的Disposable。然后在ondestroy中進行統(tǒng)一取消熄捍,防止內(nèi)存泄漏烛恤。

@Override
    protected void onDestroy() {
        super.onDestroy();
        if (disposables2Destroy == null) {
            throw new IllegalStateException(
                    "onDestroy called multiple times or onCreate not called");
        }
        disposables2Destroy.dispose();
        disposables2Destroy = null;
        if (mDelegate != null) {
            mDelegate.ondestroy();
            mDelegate = null;
        }
    }
基類訂閱者BaseObserver

通用BaseObserver是繼承于rxjava的Observer,在錯誤回調(diào)中的代碼余耽,前半部分是獲取錯誤的堆棧進行打印的邏輯缚柏,后面是對各類錯誤的通用處理。

public void onError(Throwable e) {
        if (BuildConfig.DEBUG) {
            StringBuilder sb = new StringBuilder();
            StackTraceElement[] stacks = e.getStackTrace();
            sb.append(e.getMessage());
            sb.append("\n");
            for (StackTraceElement stack : stacks) {
                sb.append(stack.getMethodName());
                sb.append("(");
                sb.append(stack.getClassName());
                sb.append(".java:");
                sb.append(stack.getLineNumber());
                sb.append(")");
                sb.append("\n");
            }
            LogUtils.e("Retrofit", sb.toString());
        }
        mBaseImpl.dismissProgress();
        if (e instanceof HttpException) {                 //   HTTP錯誤
            onException(ExceptionReason.BAD_NETWORK);
        } else if (e instanceof ConnectException
                || e instanceof UnknownHostException) {   //   連接錯誤
            onException(ExceptionReason.CONNECT_ERROR);
        } else if (e instanceof InterruptedIOException) { //  連接超時
            onException(ExceptionReason.CONNECT_TIMEOUT);
        } else if (e instanceof JsonParseException
                || e instanceof JSONException
                || e instanceof ParseException) {         //  解析錯誤
            onException(ExceptionReason.PARSE_ERROR);
        } else {
            onException(ExceptionReason.UNKNOWN_ERROR);
        }
    }

Observer中另一個最重要的結(jié)果回調(diào)onNext中對errcode進行過濾碟贾,因為我自己封裝的model層返回的BaseResponce是沒有errorCode的币喧,這個model后面會講到,當然我也可以自己給通用BaseResponse加上200的code但是總感覺這兩個邏輯還是不要耦合的好缕陕,萬一code變了我model也要改,所以我在我在BaseResponce中設(shè)置了一個變量fromCache用于標記返回結(jié)果為緩存粱锐。代碼如下:

@Override
    public void onNext(@NonNull T tBaseResponce) {
        LogUtils.d(tBaseResponce.errCode + " || from cache : " + tBaseResponce.fromCache);
        if (tBaseResponce.errCode == 200 || tBaseResponce.fromCache) {
            onSuccess(tBaseResponce);
        } else {
            onFail(tBaseResponce);
        }
    }
public class BaseResponse<T>{

    @SerializedName("code")
    public int errCode;

    @SerializedName("msg")
    public String errMsg;

    @SerializedName("data")
    public T realData;

    /**
     * 請求結(jié)果是否來自緩存
     */
    public boolean fromCache = false;

    public BaseResponse<T> setData(T data){
        realData = data;
        return this;
    }

    @Override
    public String toString() {
        return "BaseResponse{" +
                "errCode='" + errCode + '\'' +
                ", errMsg='" + errMsg + '\'' +
                ", data=" + realData +
                '}';
    }
}

另外BaseObserver引用的BaseImpl是activity的抽象接口,托管了進度條和綁定了activity的生命周期的邏輯扛邑。

public abstract class BaseObserver<T extends BaseResponse> implements Observer<T> {

    private BaseImpl mBaseImpl;
    //  Activity 是否在執(zhí)行onStop()時取消訂閱
    private boolean isAddInStop = false;
    private boolean needProgress = false;

    public BaseObserver(BaseImpl mBaseImpl,boolean needProgress) {
        this.needProgress = needProgress;
        this.mBaseImpl = mBaseImpl;
    }

    @Override
    public void onSubscribe(@NonNull Disposable d) {
        if(needProgress) mBaseImpl.showProgress("加載中");
        if (isAddInStop) {    //  在onStop中取消訂閱
            mBaseImpl.addRxStop(d);
        } else { //  在onDestroy中取消訂閱
            mBaseImpl.addRxDestroy(d);
        }
    }

    @Override
    public void onNext(@NonNull T tBaseResponce) {
        LogUtils.d(tBaseResponce.errCode + " || from cache : " + tBaseResponce.fromCache);
        if (tBaseResponce.errCode == 200 || tBaseResponce.fromCache) {
            onSuccess(tBaseResponce);
        } else {
            onFail(tBaseResponce);
        }
    }

    @Override
    public void onError(Throwable e) {
        if (BuildConfig.DEBUG) {
            StringBuilder sb = new StringBuilder();
            StackTraceElement[] stacks = e.getStackTrace();
            sb.append(e.getMessage());
            sb.append("\n");
            for (StackTraceElement stack : stacks) {
                sb.append(stack.getMethodName());
                sb.append("(");
                sb.append(stack.getClassName());
                sb.append(".java:");
                sb.append(stack.getLineNumber());
                sb.append(")");
                sb.append("\n");
            }
            LogUtils.e("Retrofit", sb.toString());
        }
        mBaseImpl.dismissProgress();
        if (e instanceof HttpException) {                 //   HTTP錯誤
            onException(ExceptionReason.BAD_NETWORK);
        } else if (e instanceof ConnectException
                || e instanceof UnknownHostException) {   //   連接錯誤
            onException(ExceptionReason.CONNECT_ERROR);
        } else if (e instanceof InterruptedIOException) { //  連接超時
            onException(ExceptionReason.CONNECT_TIMEOUT);
        } else if (e instanceof JsonParseException
                || e instanceof JSONException
                || e instanceof ParseException) {         //  解析錯誤
            onException(ExceptionReason.PARSE_ERROR);
        } else {
            onException(ExceptionReason.UNKNOWN_ERROR);
        }
    }

    @Override
    public void onComplete() {
        if(needProgress) mBaseImpl.dismissProgress();
    }

    /**
     * 請求成功
     *
     * @param response 服務(wù)器返回的數(shù)據(jù)
     */
    abstract public void onSuccess(T response);

    /**
     * 服務(wù)器返回數(shù)據(jù)怜浅,但響應(yīng)碼不為200
     *
     * @param response 服務(wù)器返回的數(shù)據(jù)
     */
    public void onFail(T response) {
        String message = response.errMsg;
        if (TextUtils.isEmpty(message)) {
            ToastUtils.showShort(R.string.response_return_error);
        } else {
            ToastUtils.showShort(message);
        }
    }

    /**
     * 請求異常
     *
     * @param reason
     */
    public void onException(ExceptionReason reason) {
        switch (reason) {
            case CONNECT_ERROR:
                ToastUtils.showShort(R.string.connect_error, Toast.LENGTH_SHORT);
                break;

            case CONNECT_TIMEOUT:
                ToastUtils.showShort(R.string.connect_timeout, Toast.LENGTH_SHORT);
                break;

            case BAD_NETWORK:
                ToastUtils.showShort(R.string.bad_network, Toast.LENGTH_SHORT);
                break;

            case PARSE_ERROR:
                ToastUtils.showShort(R.string.parse_error, Toast.LENGTH_SHORT);
                break;

            case UNKNOWN_ERROR:
            default:
                ToastUtils.showShort(R.string.unknown_error, Toast.LENGTH_SHORT);
                break;
        }
    }

    /**
     * 請求網(wǎng)絡(luò)失敗原因
     */
    public enum ExceptionReason {
        /**
         * 解析數(shù)據(jù)失敗
         */
        PARSE_ERROR,
        /**
         * 網(wǎng)絡(luò)問題
         */
        BAD_NETWORK,
        /**
         * 連接錯誤
         */
        CONNECT_ERROR,
        /**
         * 連接超時
         */
        CONNECT_TIMEOUT,
        /**
         * 未知錯誤
         */
        UNKNOWN_ERROR,
    }
}

Model層的封裝

邏輯流程圖

然后說下上面提到的model層,我定義了接口IRepository蔬崩。
這個model的主要邏輯是 :


image.png

首先判斷是否需要強制刷新恶座,如果不需要強制刷新則去數(shù)據(jù)庫緩存中查看是否含有對象的緩存,如果是網(wǎng)絡(luò)獲取判斷是否需要緩存沥阳。這里的邏輯主要由客戶端控制跨琳。

public interface IRepository<T> {

    /**
     * 用于gson解析,以及一些Logname的打印桐罕。
     * @return
     */
    Class getTClass();

    Observable<T> getEntry(final String url, Map<String, String> queryMap, final boolean needCache, boolean forceRefresh);

    Observable<T> getEntry(final String url, Map<String, String> queryMap);

    T getCache(String url) throws Exception;

    Observable<T> getEntryFromNet(String url, Map<String, String> queryMap, boolean needCache);

    void saveCache(String url, T baseBeanList);

    String getCacheKey(String url, Map<String, String> queryMap);

    void clearCache();

}
model的實現(xiàn)

拿目前公司的restful接口數(shù)據(jù)格式類型舉例:

{
    "code":200,
    "msg":"請求成功",
    "data":{
        "count":10,
        "game_list":[
            {
                "gameid":362938
            }
        ]
    }
}

可以看出BaseResponce返回的泛型T對應(yīng)的data數(shù)據(jù)還需要繼續(xù)解析脉让。所以以目前的IRepository<T>

public abstract class IDBFlowRespository<BeanContainer,DBBean> implements IRepository<BaseResponse<BeanContainer>>{

是代碼是不能很好的封裝滿足需求的桂敛,所以我定義了抽象類繼承IRepository。
定義了2個泛型BeanContainer和DBBean溅潜,數(shù)據(jù)庫的相關(guān)操作基本由DBBean泛型實例完成术唬,網(wǎng)絡(luò)層的解析由BeanContainer完成。
各司其職滚澜。GameContainer對應(yīng)的是上圖json的data粗仓,gameList對應(yīng)的是上圖json的game_list。當然如果有其他類型的restful結(jié)構(gòu)设捐,我只需要在定義對應(yīng)類型的repository抽象類就好了借浊,畢竟現(xiàn)在返回的restful接口的json格式非常局限滿世界也就那么幾種,所以不用擔心repository的擴展類太多的問題萝招。

而真正的Repository實例代碼非常少蚂斤,只需要繼承4個接口就能滿足上述定義的model接口的功能,如下:

public class GameBeanRespository extends DBListRepository<GameContainerBean,GameContainerBean.GameListBean> {
    //用于Gson對泛型的解析
    @Override
    public Class getTClass() {
        return GameContainerBean.class;
    }

    //用于DB抽象類獲取對數(shù)據(jù)庫的引用
    @Override
    public Class getTableClass() {
        return GameContainerBean.GameListBean.class;
    }

    @Override
    public List<GameContainerBean.GameListBean> mapContainer(GameContainerBean beanContainer) {
        return beanContainer.gameList;
    }

    @Override
    public GameContainerBean mapTableBean(List<GameContainerBean.GameListBean> gameListBeen) {
        return new GameContainerBean(gameListBeen);
    }
}

BaseModel是我對實體的抽象繼承的是DBflow的BaseModel可以進行數(shù)據(jù)庫的增刪改即寒,很方便橡淆。

public abstract class BaseModel extends com.raizlabs.android.dbflow.structure.BaseModel{
    public static final String KEY = "keyUrl";
    @Column(name = KEY)
    public String keyUrl;
}

其中key是對每個bean對應(yīng)的數(shù)據(jù)庫增加的字段主要是用來根據(jù)url進行緩存查詢的。
其中key是由Url拼接上queryMap的參數(shù)組成母赵,邏輯如下:

public String getCacheKey(String url, Map<String, String> queryMap) {
        StringBuilder sb = new StringBuilder();
        sb.append(url);
        if (queryMap != null && !queryMap.isEmpty()) {
            Set<String> keys = queryMap.keySet();
            sb.append("?");
            for (String key : keys) {
                sb.append(key).append("=").append(queryMap.get(key));
            }
        }
        return sb.toString();
    }

有個小問題,因為網(wǎng)絡(luò)數(shù)據(jù)獲取是從我們定義的retrofit通用接口中返回具滴,返回的對象是Obserable<ResponseBody>而我們的model接受的參數(shù)是Observable<BaseResponce<T>>凹嘲,等于是承包了GsonConvertFactory的工作,我們把返回的Observer通過rxjava的map轉(zhuǎn)成我們的Model對應(yīng)的的Observer類型就行了构韵。

@Override
    public Observable<BaseResponse<Container>> getEntryFromNet(String url, Map<String, String> queryMap, boolean needCache) {
        return HttpRequestFactory.retrofit().create(ApiService.class)
                .executeGet(url,queryMap).map(new Function<ResponseBody, BaseResponse<Container>>() {
                    @Override
                    public BaseResponse<Container> apply(@NonNull ResponseBody responseBody) throws Exception {
                        return GsonUtils.getInstance().fromJson(responseBody.string(), GsonUtils.type(BaseResponse.class,getTClass()));
                    }
                });
    }

獲取model集合的的主要邏輯代碼塊如下:

@Override
    public Observable<BaseResponse<Container>> getEntry(final String url, Map<String, String> queryMap, final boolean needCache, boolean forceRefresh) {

        final String key = getCacheKey(url, queryMap);

        //get cache
        Observable<BaseResponse<Container>> fromCache = Observable.create(new ObservableOnSubscribe<BaseResponse<Container>>() {
            @Override
            public void subscribe(@NonNull ObservableEmitter<BaseResponse<Container>> e) throws Exception {
                final BaseResponse<Container> cacheResponce = getCache(key);
                if (cacheResponce != null) {
                    LogUtils.e("Cache hint  | key = " + key);
                    cacheResponce.fromCache = true;
                    e.onNext(cacheResponce);
                } else {
                    e.onComplete();
                }
            }
        });

        //save cache
        Observable<BaseResponse<Container>> fromNet = getEntryFromNet(url, queryMap ,needCache).map(new Function<BaseResponse<Container>, BaseResponse<Container>>() {
            @Override
            public BaseResponse<Container> apply(@NonNull BaseResponse<Container> tBaseResponse) throws Exception {
                if (needCache) saveCache(key, tBaseResponse);
                return tBaseResponse;
            }
        });

        if (forceRefresh) {
            return fromNet;
        }
        return Observable.concat(fromCache, fromNet)
                .subscribeOn(Schedulers.io())
                .unsubscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread());
    }

可以看到周蹭,Cache命中的時候會將里面BaseResponce的fromCache標記為true。這樣就能和上面的提到的

BaseObserver對應(yīng)的onNext邏輯相吻合了疲恢。

測試實例

new GameBeanRespository()
                .getEntry(new UrlConstant.Builder(false).shuffix().game().list().build()//參數(shù)url
                        ,new RxMap()
                                .put("page","2")
                                .put("offset","10")
                                .build())//參數(shù)query maps
                .subscribe(new BaseObserver<BaseResponse<GameContainerBean>>(this,false) {
                    @Override
                    public void onSuccess(BaseResponse<GameContainerBean> response) {
                        LogUtils.d(response.realData);
                    }
                });

GameRepository繼承于DBlistRepository需要做的事情很少

public class GameBeanRespository extends DBListRepository<GameContainerBean,GameContainerBean.GameListBean> {

    @Override
    public Class getTClass() {
        return GameContainerBean.class;
    }

    @Override
    public Class getTableClass() {
        return GameContainerBean.GameListBean.class;
    }

    @Override
    public List<GameContainerBean.GameListBean> mapContainer(GameContainerBean beanContainer) {
        return beanContainer.gameList;
    }

    @Override
    public GameContainerBean mapTableBean(List<GameContainerBean.GameListBean> gameListBeen) {
        return new GameContainerBean(gameListBeen);
    }

}

上面看到的rxMap只是我寫的一個鏈式調(diào)用的Map包裝類凶朗,鏈式調(diào)用編寫的效率和心情大家應(yīng)該都理解 -3-

有興趣的可以拿去用,也就是個小玩意兒显拳。

public class RxMap<T,R>{

    Map<T,R> map;

    public static <T,R> RxMap<T,R> newInstance(){
        return new RxMap<>();
    }

    public RxMap() {
        this.map = new HashMap<>();
    }

    public RxMap(Map<T,R> map) {
        this.map = map;
    }

    public RxMap<T,R> put(T t, R r){
        map.put(t,r);
        return this;
    }

    public Map<T,R> build(){
        return map;
    }
}
DBflow

簡單的說下DBflow,可能你直接看到了bean的實例進行了數(shù)據(jù)庫的save操作棚愤,覺得很酸爽,確實很酸爽杂数,而且DBflow繼承了GreenDao和OrmLite各自的優(yōu)點宛畦,簡單易用上無可挑剔,自動生成數(shù)據(jù)Dao類揍移,只需要類似于OrmLite利用注解聲明各個bean之間的關(guān)系次和,另外繼承BaseModel就讓bean自己具備了增刪改的能力了。

關(guān)于DBflow這個數(shù)據(jù)庫的使用我就不多說了那伐,因為太簡單踏施,學(xué)習(xí)成本低石蔗,推薦大家去用,用了感覺不爽來打我 - -3┬巍养距!,當然我不會告訴你我在哪里上班的束亏。

后續(xù)我會抽出一個框架的demo的github地址補充在文章下面铃在。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市碍遍,隨后出現(xiàn)的幾起案子定铜,更是在濱河造成了極大的恐慌,老刑警劉巖怕敬,帶你破解...
    沈念sama閱讀 212,884評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件揣炕,死亡現(xiàn)場離奇詭異,居然都是意外死亡东跪,警方通過查閱死者的電腦和手機畸陡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,755評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來虽填,“玉大人丁恭,你說我怎么就攤上這事≌眨” “怎么了牲览?”我有些...
    開封第一講書人閱讀 158,369評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長恶守。 經(jīng)常有香客問我第献,道長,這世上最難降的妖魔是什么兔港? 我笑而不...
    開封第一講書人閱讀 56,799評論 1 285
  • 正文 為了忘掉前任庸毫,我火速辦了婚禮,結(jié)果婚禮上衫樊,老公的妹妹穿的比我還像新娘飒赃。我一直安慰自己,他們只是感情好橡伞,可當我...
    茶點故事閱讀 65,910評論 6 386
  • 文/花漫 我一把揭開白布盒揉。 她就那樣靜靜地躺著,像睡著了一般兑徘。 火紅的嫁衣襯著肌膚如雪刚盈。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,096評論 1 291
  • 那天挂脑,我揣著相機與錄音藕漱,去河邊找鬼欲侮。 笑死,一個胖子當著我的面吹牛肋联,可吹牛的內(nèi)容都是我干的威蕉。 我是一名探鬼主播,決...
    沈念sama閱讀 39,159評論 3 411
  • 文/蒼蘭香墨 我猛地睜開眼橄仍,長吁一口氣:“原來是場噩夢啊……” “哼韧涨!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起侮繁,我...
    開封第一講書人閱讀 37,917評論 0 268
  • 序言:老撾萬榮一對情侶失蹤虑粥,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后宪哩,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體娩贷,經(jīng)...
    沈念sama閱讀 44,360評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,673評論 2 327
  • 正文 我和宋清朗相戀三年锁孟,在試婚紗的時候發(fā)現(xiàn)自己被綠了彬祖。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,814評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡品抽,死狀恐怖储笑,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情圆恤,我是刑警寧澤南蓬,帶...
    沈念sama閱讀 34,509評論 4 334
  • 正文 年R本政府宣布,位于F島的核電站哑了,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏烧颖。R本人自食惡果不足惜弱左,卻給世界環(huán)境...
    茶點故事閱讀 40,156評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望炕淮。 院中可真熱鬧拆火,春花似錦、人聲如沸涂圆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽润歉。三九已至模狭,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間踩衩,已是汗流浹背嚼鹉。 一陣腳步聲響...
    開封第一講書人閱讀 32,123評論 1 267
  • 我被黑心中介騙來泰國打工贩汉, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人锚赤。 一個月前我還...
    沈念sama閱讀 46,641評論 2 362
  • 正文 我出身青樓匹舞,卻偏偏與公主長得像,于是被迫代替她去往敵國和親线脚。 傳聞我的和親對象是個殘疾皇子赐稽,可洞房花燭夜當晚...
    茶點故事閱讀 43,728評論 2 351

推薦閱讀更多精彩內(nèi)容