Android - Retorfit 2 使用详解

本文中使用 Retrofit 2.3.0 版本
侧重于如何使用,至于原理暂不考虑。

面临秋招的准大四狗,看了看各大公司的面经,什么 Retrofit、RxJava,还有一众图片加载库 比如 Glide、Picasso 等等都快成了标配了,动辄分析其源码。原来一直以自己入门尚短为由拖沓没去了解这些开源库(实际上可以算作是18年年初才开始接触),现在不得不学习这戏开源库了。

于是乎,以这篇介绍 Retrofit 的使用开始慢慢苦修之路吧。


1. 简介

1.1 Retrofit 是啥?

Retrofit 是大名鼎鼎的 Square 公司开源的一个 网络加载框架,它的底层封装了 OkHttp 3,准确的说,网络请求的工作本质是 OkHttp 完成的,而 Retrofit 仅仅负责网络请求接口的封装。

Retrofit 的一大特点就是大量的注解使用,不过这也是我暂时比较疑惑的一点?为什么使用注解

至于 Retrofit 相较于 OkHttp 3 的优势,我现在了解的并不很深,仅仅知道是由于 Retrofit + RxJava 的便利使得众多开发者选择使用 Retrofit。

上述可能有误,但暂时理解就是这样的。挖下的坑自然等到理解加深后再来填了。

1.2 Retrofit 的好处?

初步理解的优点:

  • 支持 同步&异步
  • 提供对 RxJava 的支持
  • 使用起来比较简单,通过注解即可配置网络请求参数

网传的一些比较迷惑的优点:

  • 性能好、处理快(需要分析源码)
  • 解耦彻底(据网上的文章分析,Retrofit 为了实现高度解耦,采用了大量的设计模式,同样需要分析源码)

2. 使用介绍

2.1 注解

在介绍如何使用之前,先来学习一下前置技能:Retrofit 中的注解。
Retrofit 中的注解可以分为三个类型:网络请求方法、标志以及网络请求参数。

2.1.1 网络请求方法

图示如下:

序号 注解代码 说明
1 @GET GET 请求
2 @POST POST 请求
3 @PUT PUT 请求
4 @DELETE DELETE 请求
5 @PATCH PATCH 请求
6 @HEAD HEAD 请求
7 @OPTIONS OPTIONS 请求
8 @HTTP 供自定义扩展

说明:

  • 对于序号 1 ~ 7 :

    • 需要接收一个字符串表示 path,与后面的 baseUrl 组成完整的 url
    • 接收的 path 字符串可以使用 “变量”,如 {id},结合 @Path("id") 注解为 {id}提供值
      public interface Api{
          //注意此处 Call 的泛型是 ResponseBody,表示将直接返回服务器响应的 ResponseBody,即未作任何处理
          @GET("info/{id}")
          Call<ResponseBody> getInfo<@Path("id") int id>
      }
      
    • 当然也可以不指定 path,此时通过 @Url 注解指定 url
      public interface Api{
          @GET
          Call<ResponseBody> getInfo<@Url String url>
      }
      
  • 对于序号 8:

    • 可用于替换 以上 7 个,以及其扩展;
    • 有 3 个属性 method、path、hasBody
       interface Api {
           /**
           * method 请求方法,不区分大小写
           * path 路径,
           * hasBody 是否有请求体
           */ 
          @HTTP(method = "DELETE", path = "remove/", hasBody = true)
          Call<ResponseBody> deleteObject(@Body RequestBody object);
       }
      

2.1.2 标志

图示图下:

注解代码 说明
@FormUrlEncoded 表示请求主体将使用表单
@Multipart 表示请求体是多部分的
@Streaming 在返回响应的方法中处理 Response(没有该注释,则会将body()转换为 byte[],并全部载入到内存,之后从内存中读取数据)

说明:

  • FormUrlEncoded
    • 需要添加:Content-Type:application/x-www-form-urlencoded
  • Multipart
    • 需要添加:Content-Type: multipart/form-data
  • Streaming
    • 数据量大时,需要使用该注解,以免将数据全部载入内存造成 OOM

2.1.3 网络请求参数

图示如下:

注解代码 说明
@Body 直接指定 PUT/POST 的请求体,但并非作为请求参数或者表单的请求体
@Field 表单字段
@FieldMap 以键值对方式设置表单字段
@Header 添加请求头(不固定的请求头)
@HeaderMap 以 Map 形式添加请求头
@Headers 添加包含值的请求头(固定的请求头)
@Part 表示一个多部分请求的单个部分(多用于文件上传)
@PartMap 表示一个多部分请求的 name 和 value 字段(多用于文件上传)
@Path 替换 URL 中被 {} 包裹起来的字段
@Query 向 url 追加参数
@QueryMap 向 url 追加键值对参数
@QueryName 为没有 value 的 name 字段传值
@Url 使用全路径复写 baseUrl,用于非统一 baseUrl 的场景

上面这些注解,其实我也了解的不是很多,示例代码可以打开对应链接,查看官方示例。

说明:

  • Body

    //默认情况下
    @PUT/@POST("user/info")
    Call<ResponseBody> createUser(@Body User user);
    
    //为 Retorfit 添加转换器,此时可以将 ResponseBody 转换为指定类
    @PUT/@POST("user/info")
    Call<User> createUser(@Body User user);
    
  • Field & FieldMap

    二者体现在请求体(表单),即适用于POST方式(注意和 Query 等的区别)

    • Field

      @FormUrlEncoded
      @POST("login")
      Call<ResponseBody> userLogin(@Field("name") String name,@Field("password") String password);
      
      //当如下调用时
      xxx.userLogin("whdalive","nice666");
      url-> name=whdalive&password=nice666
      
    • FieldMap

      @FormUrlEncoded
      @POST("login")
      Call<ResponseBody> userLogin(@Fieldmap Map<String,String> fields);
      
      //当如下调用时
      xxx.userLogin(ImmutableMap.of("name","whdalive","password","nice666"));
      url-> name=whdalive&password=nice666
      
  • Header & HeaderMap & Headers

    • Header (作用于方法的参数)
      使用 @Header 注解动态更新 header,即不固定的 header

      @GET("xxx")
      Call<ResponseBody> getXXX(@Header(Accept-Language) String lang);
      
    • HeaderMap

      @GET("xxx")
      Call<ResponseBody> getXXX(@HeaderMap Map<String, String> headers);
      
      //通过以下方式设置:
       emm.getXXX(ImmutableMap.of("Accept", "text/plain", "Accept-Charset", "utf-8"));
      
      //headers 会设置成如下样式:
      //Accept: text/plain and Accept-Charset: utf-8
      
    • Headers (作用于方法)

       @Headers("Cache-Control: max-age=640000")
       @GET("/")
       ...
      
       @Headers({"X-Foo: Bar","X-Ping: Pong"})
       @GET("/")
       ...
           
       //@Headers 设置的所有请求头不会相互覆盖,即便名字相同
      
  • Part & PartMap

    @Part MultipartBody.part 代表文件,@Part("key") RequestBody 代表参数

    关于这个的具体使用场景,官方文档中没有提及。

    • Part:一般用来上传单个文件
       @Multipart
       @POST("/upload")
       Call<ResponseBody> upload(
           @Part("description") String description,
           @Part(value = "image", encoding = "8-bit") RequestBody image);
      
  • PartMap:一般用来批量上传

    @Multipart
    @POST("/upload")
    Call<ResponseBody> upload(
         @Part("file") RequestBody file,
         @PartMap Map<String, RequestBody> params);
    
  • Path

    动态替换 {} 包含的字符串。

    @GET("/image/{id}")
    Call<ResponseBody> example(@Path("id") int id);
    //调用 foo.example(1) ,则定位到 /image/1.
    
    //其中 还有一个属性 encoded 表示 url 是否解码,默认为 false
     @GET("/user/{name}")
     Call<ResponseBody> encoded(@Path("name") String name);
    
     @GET("/user/{name}")
     Call<ResponseBody> notEncoded(@Path(value="name", encoded=true) String name);
    
    //分别进行调用,以及其结果:
     foo.encoded("John+Doe"); -> /user/John%2BDoe
      foo.notEncoded("John+Doe"); -> /user/John+Doe.
    
  • Query & QueryMap & QueryName

    三者体现在 url 上,即适用于 GET 方式,也可以用作 POST 方式(注意与 Field 等的区别)

    • Query

      //传单一值
      @GET("/friends")
      Call<ResponseBody> friends(@Query("page") int page);
      foo.friends(1); -> /friends/page=1
      //传空值时,无效化,不会添加字段
      
      //传数组
      @GET("/friends")
      Call<ResponseBody> friends(@Query("page") String... pages);
      foo.friends("me","you"); -> /friends/page=me&page=you
      
      //同样适用 encoded 属性,不再举例
      
  • QueryMap

    @GET("/friends")
    Call<ResponseBody> friends(@QueryMap Map<String, String> filters);
    
    foo.friends(ImmutableMap.of("group", "coworker", "age", "42")) -> /friends?group=coworker&age=42.
        
    //同样适用 encoded 属性
    
  • QueryName

    //单一值
    @GET("/friends")
    Call<ResponseBody> friends(@QueryName String filter);
    
    foo.friends("contains(Bob)"); -> /friends?contains(Bob).
    
    //数组
    @GET("/friends")
    Call<ResponseBody> friends(@QueryName String... filters);
    
    oo.friends("contains(Bob)", "age(42)"); -> /friends?contains(Bob)&age(42).
    
  • Url

    用来指定最终 url 中的 path 部分,一般在 @GET 等注解后不传入字符串时使用

    @GET
    Call<ResponseBody> getInfo<@Url String url>
    

2.2 步骤

使用 Retrofit 的步骤如下:

  1. 添加依赖与网络权限
  2. 创建接收服务器返回数据的类
  3. 创建描述网络请求的接口
  4. 创建 Retrofit 实例
  5. 创建 网络请求接口实例 并 配置网络请求参数
  6. 发送网络请求,处理返回数据

2.2.1 添加依赖与网络权限

添加依赖:

//build.gradle
dependencies {
    // Retrofit库
    implementation 'com.squareup.retrofit2:retrofit:2.3.0'

    // 使用 Retrofit 2 以前的版本时需要添加 OkHttp 依赖
    // 而在 Retrofit 2 以后不再需要添加 OkHttp 的依赖

}

添加网络权限:

//Manifest.xml
<uses-permission android:name="android.permission.INTERNET"/>

2.2.2 创建接收服务器返回数据的类


Hotkey.java

public class Hotkey{
    // ...
    // 具体定义视返回数据的格式和解析方式而定(xml/json)
}

2.2.3 创建描述网络请求的接口

Api.java

//以 wanAndroid 的 API 为例
//http://www.wanandroid.com/hotkey/json
public interface Api {
    @GET("hotkey/json")
    Call<HotKey> getHotkey();
}

说明:

  • Retrofit 将 Http请求抽象成 Java 接口,并在接口里面采用注解来配置网络请求参数。用动态代理将该接口的注解“翻译”成一个 Http 请求,最后再执行 Http请求 注意: 接口中的每个方法的参数都需要使用注解标注,否则会报错

  • Call 的泛型可以有多种形式

    • Call< ResponseBody >:Responsebody 是 Retrofit 网络请求回来的原始数据类,如果不需要转换则使用此种形式,比如看看 json 看看 xml。
    • Call< Type >:Type 指代我们需要转换的类,比如将原始 json 数据通过 Gson 转换为 Type 类。

2.2.4 创建 Retrofit 实例

示例代码:

Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("http://www.wanandroid.com/") // 设置网络请求的Url地址
                .addConverterFactory(GsonConverterFactory.create()) // 设置数据解析器
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) // 支持RxJava平台
                .build();

需要对上述代码做一些补充。

2.2.4.1 数据解析器 -- Converter

添加方式如前面代码中的 addConverterFactory(GsonConverterFactory.create()),作用就是通过 Gson 将服务器返回的 json 数据解析成目标类。

Retrofit 为我们提供了诸多解析方式,通常能满足我们的需求,也就是说我们不通常需要自定义解析器了。 然而在 Retrofit 2.0 之后,我们需要手动添加解析器的依赖。

数据解析器 Gradle依赖
Gson com.squareup.retrofit2:converter-gson:2.3.0
Jackson com.squareup.retrofit2:converter-jackson:2.3.0
Simple XML com.squareup.retrofit2:converter-simplexml:2.3.0
Protobuf com.squareup.retrofit2:converter-protobuf:2.3.0
Moshi com.squareup.retrofit2:converter-moshi:2.3.0
Wire com.squareup.retrofit2:converter-wire:2.3.0
Scalars com.squareup.retrofit2:converter-scalars:2.3.0

2.2.4.2 网络请求适配器 -- CallAdapter

Retrofit 支持多种网络请求适配器方式:guava、Java8 和 RxJava

默认为 DefaultCallAdapterFacroty

通过 RxJava 和 默认适配器对比理解

  • 网络请求接口中,默认适配器返回的接口类型为 Call<T>
  • RxJava 返回的接口类型为 Observable<T>

也就是说,CallAdapter 实际上作用的地方是 Call / Observable,如果我们不需要结合 RxJava 使用,而且默认的适配器就足够的话,那么我们不用考虑 CallAdapter 的问题。相反如果我们需要结合 RxJava 使用,我们需要像上面代码中一样添加 RxJava 的适配器。

和 数据解析器 一样,网络请求适配器使用前需要添加依赖,如下所示:

网络请求适配器 Gradle依赖
Guava com.squareup.retrofit2:adapter-guava:2.3.0
Java8 com.squareup.retrofit2:adapter-java8:2.3.0
RXJava com.squareup.retrofit2:adapter-rxjava:2.3.0

2.2.4.3 URL 的组成

在上面那段代码时,还有一个点要说,那就是 baseUrl() 方法。

我们在前面介绍注解的时候,有什么 @GET("url"),或者@Url("url"),然后又提过好多次 path,那么这二者到底是什么关系呢?

结论:

  • 网站请求的完整 Url = .baseUrl() 部分 + 网络请求接口注解部分(path 部分)

具体使用:

类型 具体使用
推荐,符合日常使用习惯 path = 相对路径<br />baseUrl = 目录形式 Url = “http://host:port/a/b/path”,其中:<br />path = “path”<br />baseUrl = “http://host:port/a/b/
不推荐 path = 绝对路径 Url = “http://host:port/a/b/path”,其中:<br />path = “/path”<br />baseUrl = “http://host:port
不推荐 path = 相对路径<br />baseUrl = 文件形式 Url = “http://host:port/a/path”,其中:<br />path = “path”<br />baseUrl = “http://host:port/a/b

2.2.5 创建 网络请求接口实例 并 配置网络请求参数

//创建 网络请求接口 的实例
Api request = retrofit.create(Api.class);

//对发送请求进行封装
Call<HotKey> call = request.getHotKey();

2.2.6 发送网络请求,处理返回数据

//发送网络请求(异步)
call.enqueue(new Callback<HotKey>() {
    //请求成功时回调
    @Override
    public void onResponse(Call<HotKey> call, Response<HotKey> response) {
       //通过 Response 类的 body() 对返回的数据进行处理 
        response.body().xxx();
    }

    @Override
    public void onFailure(Call<HotKey> call, Throwable t) {
       //请求失败时候的回调
    }
});


//发送网络请求(同步),与处理返回数据
Response<HotKey> response = call.execute();
reponse.body().show()

以上就是最普通的通过 Retrofit 发送网络请求并处理返回数据的使用流程了。


3. 实例介绍

下面,将用两个实例分别介绍具体如何用 GET 方式 和 POST 方式进行网络请求。

Demo 地址:GitHub - DemoRetrofit

3.1 实例一 (GET)

实现功能: wanAndroid 网站的热词查看

官方API:

具体步骤:

  1. 添加依赖与网络权限
  2. 创建接收服务器返回数据的类
  3. 创建描述网络请求的接口
  4. 创建 Retrofit 实例
  5. 创建 网络请求接口实例 并 配置网络请求参数
  6. 发送网络请求,处理返回数据

3.1.1 添加依赖与网络权限

build.gradle

implementation 'com.squareup.retrofit2:retrofit:2.3.0'
    
//使用 Gson 数据解析器解析返回的 Json 数据 
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'

AndroidManifests.xml

<uses-permission android:name="android.permission.INTERNET"/>

3.1.2 创建接收服务器返回数据的类

HotKey.java ( 通过 GsonFormat 生成即可)

public class HotKey {

    /**
     * data : [{"id":6,"link":"","name":"面试","order":1,"visible":1},{"id":9,"link":"","name":"Studio3","order":1,"visible":1},{"id":5,"link":"","name":"动画","order":2,"visible":1},{"id":1,"link":"","name":"自定义View","order":3,"visible":1},{"id":2,"link":"","name":"性能优化 速度","order":4,"visible":1},{"id":3,"link":"","name":"gradle","order":5,"visible":1},{"id":4,"link":"","name":"Camera 相机","order":6,"visible":1},{"id":7,"link":"","name":"代码混淆 安全","order":7,"visible":1},{"id":8,"link":"","name":"逆向 加固","order":8,"visible":1}]
     * errorCode : 0
     * errorMsg :
     */

    private int errorCode;
    private String errorMsg;
    private List<DataBean> data;

    public int getErrorCode() {
        return errorCode;
    }

    public void setErrorCode(int errorCode) {
        this.errorCode = errorCode;
    }

    public String getErrorMsg() {
        return errorMsg;
    }

    public void setErrorMsg(String errorMsg) {
        this.errorMsg = errorMsg;
    }

    public List<DataBean> getData() {
        return data;
    }

    public void setData(List<DataBean> data) {
        this.data = data;
    }

    public static class DataBean {
        /**
         * id : 6
         * link :
         * name : 面试
         * order : 1
         * visible : 1
         */

        private int id;
        private String link;
        private String name;
        private int order;
        private int visible;

        public int getId() {
            return id;
        }

        public void setId(int id) {
            this.id = id;
        }

        public String getLink() {
            return link;
        }

        public void setLink(String link) {
            this.link = link;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getOrder() {
            return order;
        }

        public void setOrder(int order) {
            this.order = order;
        }

        public int getVisible() {
            return visible;
        }

        public void setVisible(int visible) {
            this.visible = visible;
        }
    }
}

3.1.3 创建描述网络请求的接口

Api.java

public interface Api {

    @GET("hotkey/json")
    Call<HotKey> getHotkey();
}

3.1.4 创建 Retrofit 实例

MainActivity.java

Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("http://www.wanandroid.com/")
                .addConverterFactory(GsonConverterFactory.create())
                .build();

3.1.5 创建 网络请求接口实例 并 配置网络请求参数

MainActivity.java

Api request = retrofit.create(Api.class);

Call<HotKey> call = request.getHotkey();

3.1.6 发送网络请求,处理返回数据

MainActivity.java

call.enqueue(new Callback<HotKey>() {
            @Override
            public void onResponse(Call<HotKey> call, Response<HotKey> response) {
                for(HotKey.DataBean dataBean : response.body().getData()){
                    Log.d(TAG, "id: "+ dataBean.getId());
                    Log.d(TAG, "link: "+ dataBean.getLink());
                    Log.d(TAG, "name: "+ dataBean.getName());
                    Log.d(TAG, "order: "+ dataBean.getOrder());
                    Log.d(TAG, "visible: "+ dataBean.getVisible());
                }
            }

            @Override
            public void onFailure(Call<HotKey> call, Throwable t) {
                Log.d(TAG, "onFailure: 连接失败");
            }
        });

结果

3.2 实例二 (POST)

实现功能: wanAndroid 网站的搜索功能

官方API:

具体步骤:

  1. 添加依赖与网络权限
  2. 创建接收服务器返回数据的类
  3. 创建描述网络请求的接口
  4. 创建 Retrofit 实例
  5. 创建 网络请求接口实例 并 配置网络请求参数
  6. 发送网络请求,处理返回数据

3.2.1 添加依赖与网络权限

build.gradle

implementation 'com.squareup.retrofit2:retrofit:2.3.0'
    
//使用 Gson 数据解析器解析返回的 Json 数据 
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'

AndroidManifests.xml

<uses-permission android:name="android.permission.INTERNET"/>

3.2.2 创建接收服务器返回数据的类

SearchResult.java

完整返回的的 json 在下方有所展示,此处只针对性的选择了几个属性而已,毕竟只是用于展示

public class SearchResult {
    
    private DataBean data;

    public DataBean getData() {
        return data;
    }

    public void setData(DataBean data) {
        this.data = data;
    }

    public static class DataBean {

        private List<DatasBean> datas;
        
        public List<DatasBean> getDatas() {
            return datas;
        }

        public void setDatas(List<DatasBean> datas) {
            this.datas = datas;
        }

        public static class DatasBean {
            
            private String author;
            private String chapterName;
            private String title;
            
            public String getAuthor() {
                return author;
            }

            public void setAuthor(String author) {
                this.author = author;
            }


            public String getChapterName() {
                return chapterName;
            }

            public void setChapterName(String chapterName) {
                this.chapterName = chapterName;
            }


            public String getTitle() {
                return title;
            }

            public void setTitle(String title) {
                this.title = title;
            }
        }
    }
}

返回的 json 格式如下:

{
    "data": {
        "curPage": 1,
        "datas": [
            {
                "apkLink": "",
                "author": "鸿洋公众号",
                "chapterId": 73,
                "chapterName": "面试相关",
                "collect": false,
                "courseId": 13,
                "desc": "",
                "envelopePic": "",
                "fresh": false,
                "id": 3177,
                "link": "https://mp.weixin.qq.com/s/haZRurfMHQzzr-ffxAh20w",
                "niceDate": "2018-07-24",
                "origin": "",
                "projectLink": "",
                "publishTime": 1532442910000,
                "superChapterId": 186,
                "superChapterName": "热门专题",
                "tags": [],
                "title": "我的杭州<em class='highlight'>面试<\/em>之旅",
                "type": 0,
                "userId": -1,
                "visible": 1,
                "zan": 0
            }
        ],
        "offset": 0,
        "over": false,
        "pageCount": 3,
        "size": 20,
        "total": 52
    },
    "errorCode": 0,
    "errorMsg": ""
}

3.2.3 创建描述网络请求的接口

Api.java

public interface Api {

    //...
    
    @FormUrlEncoded
    @POST("article/query/{page}/json")
    Call<SearchResult> search(@Path("page") int page , @Field("k") String key);
}

3.2.4 创建 Retrofit 实例

MainActivity.java

Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("http://www.wanandroid.com/")
                .addConverterFactory(GsonConverterFactory.create())
                .build();

3.2.5 创建 网络请求接口实例 并 配置网络请求参数

MainActivity.java

 Api request = retrofit.create(Api.class);
Call<SearchResult> call2 = request.search(0,"面试");

3.2.6 发送网络请求,处理返回数据

MainActivty.java

call2.enqueue(new Callback<SearchResult>() {
            @Override
            public void onResponse(Call<SearchResult> call, Response<SearchResult> response) {
                for (SearchResult.DataBean.DatasBean dataBean:response.body().getData().getDatas()){
                    Log.d(TAG, "author: "+ dataBean.getAuthor()
                            +"; chapterName: "+ dataBean.getChapterName()
                            +"; title: "+ dataBean.getTitle());
                }
            }

            @Override
            public void onFailure(Call<SearchResult> call, Throwable t) {
                Log.d(TAG, "onFailure: 连接失败");
            }
        });

结果


4. 扩展

说起扩展使用,当然第一反应就是 Retrofit + RxJava 了。
令 Retrofit 支持 RxJava 也很简单,只需要在构建 Retrofit 的实例时使用如下代码即可

Retrofit retrofit = new Retrofit.Builder()
    ...
    .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) // 支持RxJava
    .build()

至于实际如何怎么用,请期待日后的 RxJava 的文章。(其实是我现在没有开始学 RxJava


总结

在本文开篇我就说过,此文仅仅是介绍如何使用 Retrofit,受众只是那些想要快速入门的新手玩家。
同时也是因为我现在还没有深入源码学习它是如何设计的,因此理解不免有所偏误。
如有错误,还请大佬们指出,不胜感激。

最后,慢慢长路,共勉。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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