Retrofit使用详解(一)

本文翻译自https://futurestud.io/tutorials/retrofit-getting-started-and-android-client
OKHttp(一)之Calls

OKHttp(二)之Connections

OkHttp(三)之使用方法

OkHttp(四)之拦截器

OkHttp(五)之HTTPS

Retrofit使用详解(一)

Retrofit使用详解(二)

Retrofit使用详解(三)

Retrofit使用详解(四)

Retrofit使用详解(五)

Retrofit使用详解(六)

一、创建安卓客户端

什么是Retrofit

官方是这样的描述的

A type-safe REST client for Android and Java.

使用注解来描述HTTP请求,默认会集成URL参数替换。还提供了自定义头信息,多请求体,文件上传下载,模拟响应等功能。

准备你的安卓工程

可以通过Android Studio的Gradle来添加依赖。

定义依赖:Gradle

此处只介绍retrofit2的用法

Retrofit默认使用OkHttp作为网络层,并在其之上建立。

如果你在retrofit2中想要将JSON映射到GSON上,添加以下依赖:

dependencies {  
    // Retrofit & OkHttp
    compile 'com.squareup.retrofit2:retrofit:2.1.0'
    compile 'com.squareup.retrofit2:converter-gson:2.1.0'
}

Android 网络权限

Retrofit针对Internet上某个服务器上运行的API执行HTTP请求。 从Android应用程序执行这些请求需要Internet权限才能打开网络套接字。 您需要在AndroidManifest.xml文件中定义权限。 如果您尚未设置Internet权限,请在您的AndroidManifest.xml定义中添加以下行:

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

怎样描述API终端

在你发起第一个请求之前,你需要描述你需要与之交互的API终端。首先你需要创建一个接口并且定义一个方法。

GitHubClient

下面的代码定义了一个接口GitHubClient和一个方法reposForUser来请求给定用户的仓库列表。
@GET注解声明此请求使用HTTP GET方法。在定义的方法中,当调用reposForUser方法时,{user}路径将替换为给定的变量值。

public interface GitHubClient {  
    @GET("/users/{user}/repos")
    Call<List<GitHubRepo>> reposForUser(
        @Path("user") String user
    );
}

定义了一个类,GitHubRepo,这个类包括返回数据的所有属性。

public class GitHubRepo {  
    private int id;
    private String name;

    public GitHubRepo() {
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }
}

关于前面提到的JSON映射:GitHubClient接口定义了一个名为reposForUser的方法,返回类型为List <GitHubRepo>。 Retrofit确保服务器响应得到正确映射。

Retrofit REST Client

在描述完APi接口和对象模型后,下面准备创建请求。Retrofit的所有请求的基础是Retrofit(2.0+)这个类。你可以使用构造器为所有请求设置一些常规选项,包括BaseURl和converter。

创建adapter后,可以创建一个客户端来执行请求。

String API_BASE_URL = "https://api.github.com/";

OkHttpClient.Builder httpClient = new OkHttpClient.Builder();

Retrofit.Builder builder =  
    new Retrofit.Builder()
            .baseUrl(API_BASE_URL)
            .addConverterFactory(
                GsonConverterFactory.create()
            );

Retrofit retrofit =  
    builder
        .client(
            httpClient.build()
        )
        .build();

GitHubClient client =  retrofit.create(GitHubClient.class);  

在上边的代码中我们定义了BaseURl是"https://api.github.com/",并且使用了最少的配置。Retrofit可以有更多的配置,但是在本例中不使用。

JSON Mapping

在大多数情况下,对服务器的请求和来自服务器的响应不是Java对象。 它们映射到一些其他格式中,如JSON。 GitHub的API使用JSON,在Retrofit2中,你需要显式的将一个转换器(convert)田家达Retrofit对象。

Retrofit in Use

在Retrofit 2中,您使用客户端获取call对象。一旦你对创建的call对象调用了.enqueue(异步请求),请求将由Retrofit进行。

// Create a very simple REST adapter which points the GitHub API endpoint.
GitHubClient client =  retrofit.create(GitHubClient.class);

// Fetch a list of the Github repositories.
Call<List<GitHubRepo>> call =  
    client.reposForUser("fs-opensource");

// Execute the call asynchronously. Get a positive or negative callback.
call.enqueue(new Callback<List<GitHubRepo>>() {  
    @Override
    public void onResponse(Call<List<GitHubRepo>> call, Response<List<GitHubRepo>> response) {
        // The network call was a success and we got a response
        // TODO: use the repository list and display it
    }

    @Override
    public void onFailure(Call<List<GitHubRepo>> call, Throwable t) {
        // the network call was a failure
        // TODO: handle error
    }
});

Retrofit将返回一个方便的列表<GitHubRepo>,你可以进一步使用它来显示您的应用程序中的数据。

二、API终端

怎样描述一个API终端

我们在一个接口文件中描述我们的API终端。

public interface GitHubClient {  
    @GET("/users/{user}/repos")
    Call<List<GitHubRepo>> reposForUser(
        @Path("user") String user
    );
}

现在让我们来看看这些选项中的细节。

HTTP Method

在Java接口中使用注解来描述每一个API终端,最终处理请求。第一件事就是定义HTTP请求方法,如GET,POST,PUT,DELETE等。Retrofit为每个请求方法都提供了注解,你只需要为每个HTTP方法添加下面的注解即可:
@GET,@PSOT,@PUT,@DELETE,@PATCH,@HEAD。

下面是几个简单的例子:

public interface FutureStudioClient {  
    @GET("/user/info")
    Call<UserInfo> getUserInfo();

    @PUT("/user/info")
    Call<UserInfo> updateUserInfo(
        @Body UserInfo userInfo
    );

    @DELETE("/user")
    Call<Void> deleteUser();
}

HTTP Resource Location

此外,你需要为你的注解添加相对与BaseURL的String参数来完成路径,例如 @GET(“/ user / info”)。 在大多数情况下,您只会传递相对网址,而不传递完整网址(例如http://futurestud.io/api/user/info)。 这具有的优点是,Retrofit只需要一次请求基本URL(http://futurestud.io)。 如果你要更改API基本网址,则只需在一个位置更改它。 此外,它使一些更高端的事情,如动态基本URL,更容易。 不过,你可以指定完整的URL。

public interface FutureStudioClient {  
    @GET("/user/info")
    Call<UserInfo> getUserInfo();

    @PUT("/user/info")
    Call<UserInfo> updateUserInfo(
        @Body UserInfo userInfo
    );

    @DELETE("/user")
    Call<Void> deleteUser();

    // example for passing a full URL
    @GET("https://futurestud.io/tutorials/rss/")
    Call<FutureStudioRssFeed> getRssFeed();
}

Function Name & Return Type

Java方法声明:Call<UserInfo> getUserInfo(); 这包含三个部分:

  • 方法名---getUserInfo

你可以自由定义方法名称。 Retrofit不在乎,它不会对功能产生任何影响。不过,你应该选择一个名称,这将有助于你和其他开发人员了解什么是API请求。

  • 方法返回的类型---UserInfo

你必须定义你期望从服务器的什么样的数据。例如,当您请求某些用户信息时,您可以将其指定为Call <UserInfo>。 UserInfo类包含将保存用户数据的属性。 Retrofit会自动映射它,您不必进行任何手动解析。如果你想要原始响应,你可以使用ResponseBody而不是像UserInfo这样的特定类。如果你根本不在乎服务器响应什么,你可以使用Void。在所有这些情况下,你必须将它包装到一个类型的Retrofit Call <>类中。

  • 方法传递的参数---此处为空

您可以将参数传递给方法。有各种各样的可能选项,此处列举一些:

@Body: 发送Java对象作为请求体
@Url: 使用动态地址
@Field: 以表单形式发送数据
public interface FutureStudioClient {  
    @GET("/user/info")
    Call<UserInfo> getUserInfo();

    @PUT("/user/info")
    Call<Void> updateUserInfo(
        @Body UserInfo userInfo
    );

    @GET
    Call<ResponseBody> getUserProfilePhoto(
        @Url String profilePhotoUrl
    );
}

Path Parameters

REST API是基于动态URL构建的。 您可以通过替换部分URL来访问资源,例如获取我们网页上的第三个教程可能是http://futurestud.io/api/tutorials/3。 最后的3指定您要访问的页面。 Retrofit提供了一种简单的方法来替换路径参数。 例如:

public interface GitHubClient {  
    @GET("/users/{user}/repos")
    Call<List<GitHubRepo>> reposForUser(
        @Path("user") String user
    );
}

这里,{user}值是动态的,并且将在请求发生时设置。 如果在URL中包含路径参数,则需要添加@Path()函数参数,其中@Path值与URL中的占位符匹配(在本例中为@Path(“user”))。 如有必要,您可以使用多个占位符。 只需确保您始终具有匹配参数的确切数量。 您甚至可以使用可选的路径参数。

Query Parameters

动态URL的另一个大部分是查询参数 与路径参数不同,您不需要将它们添加到注释URL。 你可以简单地添加一个方法参数@Query()和查询参数名称,描述类型,你很好去。 Retrofit会自动将其附加到请求。 如果传递一个空值作为查询参数,Retrofit将忽略它。 您还可以添加多个查询参数。

public interface FutureStudioClient {  
    @GET("/tutorials")
    Call<List<Tutorial>> getTutorials(
        @Query("page") Integer page
    );

    @GET("/tutorials")
    Call<List<Tutorial>> getTutorials(
            @Query("page") Integer page,
            @Query("order") String order,
            @Query("author") String author,
            @Query("published_at") Date date
    );
}    

在上面的例子中,你可以使用第二个方法来替换第一个方法,只需要把其他的值设置为null即可。

创建一个可复用的客户端

The ServiceGenerator

Retrofit对象及其构建器是所有请求的核心。 在这里,你可以配置和准备请求,响应,认证,日志记录和错误处理。
让我们从简单的代码开始。 在其当前状态下,它仅定义一种方法为给定类/接口创建基本REST客户端,该接口从接口返回服务类。

public class ServiceGenerator {

    private static final String BASE_URL = "https://api.github.com/";

    private static Retrofit retrofit;

    private static Retrofit.Builder builder =
            new Retrofit.Builder()
                    .baseUrl(BASE_URL)
                    .addConverterFactory(GsonConverterFactory.create());

    private static OkHttpClient.Builder httpClient =
            new OkHttpClient.Builder();

    public static <S> S createService(Class<S> serviceClass) {
        return retrofit.create(serviceClass);
    }
}

ServiceGenerator类使用Retrofit的Retrofit构造器创建具有BaseURL(BASE_URL)的新REST客户端。 例如,GitHub的API的BaseURL位于https://api.github.com/

createService方法将serviceClass(它是API请求的注释接口)作为参数,并从中创建一个可用的客户端。 在生成的客户端上,您将能够执行网络请求。

Why Is Everything Declared Static Within the ServiceGenerator?

我们想在整个应用程序中使用相同的对象(OkHttpClient,Retrofit,...),只打开一个套接字连接,处理所有的请求和响应,包括缓存和更多。 通常的做法是只使用一个OkHttpClient实例来重用开放套接字连接。 这意味着,我们需要通过依赖注入或使用静态字段将OkHttpClient注入到此类中。 正如你所看到的,我们选择使用静态字段。 并且因为我们在这个类中使用OkHttpClient,我们需要使所有字段和方法静态。

除了加快速度,我们可以在移动设备上节省一些有价值的内存,当我们不必一遍又一遍地重新创建相同的对象。

Using the ServiceGenerator

GitHubClient client = ServiceGenerator.createService(GitHubClient.class);  

Preparing Logging

使用Retrofit 2进行日志记录是由称为HttpLoggingInterceptor的拦截器完成的。 您需要向OkHttpClient添加此拦截器的实例。 例如,您可以通过以下方式解决它:

public class ServiceGenerator {

    private static final String BASE_URL = "https://api.github.com/";

    private static Retrofit retrofit;

    private static Retrofit.Builder builder =
            new Retrofit.Builder()
                    .baseUrl(BASE_URL)
                    .addConverterFactory(GsonConverterFactory.create());

    private static HttpLoggingInterceptor logging =
            new HttpLoggingInterceptor()
                    .setLevel(HttpLoggingInterceptor.Level.BODY);

    private static OkHttpClient.Builder httpClient =
            new OkHttpClient.Builder();

    public static <S> S createService(
        Class<S> serviceClass) {
        if (!httpClient.interceptors().contains(logging)) {
            httpClient.addInterceptor(logging);
            builder.client(httpClient.build());
            retrofit = builder.build();
        }

        return retrofit.create(serviceClass);
    }
}

有一些事情你必须知道。 首先,确保你没有不小心多次添加拦截器!通过httpClient.interceptors().contains(logging)来检查日志拦截器已经存在。 其次,确保不在每次createService调用上创建新的对象。 否则,将会使ServiceGenerator失去意义。

Prepare Authentication

需要在创建客户端时传递附加参数到createService。

让我们看一个Hawk身份验证的示例:

public class ServiceGenerator {

    private static final String BASE_URL = "https://api.github.com/";

    private static Retrofit retrofit;

    private static Retrofit.Builder builder =
            new Retrofit.Builder()
                    .baseUrl(BASE_URL)
                    .addConverterFactory(GsonConverterFactory.create());

    private static HttpLoggingInterceptor logging =
            new HttpLoggingInterceptor()
                    .setLevel(HttpLoggingInterceptor.Level.BODY);

    private static OkHttpClient.Builder httpClient =
            new OkHttpClient.Builder();

    public static <S> S createService(
            Class<S> serviceClass, final HawkCredentials credentials) {
        if (credentials != null) {
            HawkAuthenticationInterceptor interceptor =
                    new HawkAuthenticationInterceptor(credentials);

            if (!httpClient.interceptors().contains(interceptor)) {
                httpClient.addInterceptor(interceptor);

                builder.client(httpClient.build());
                retrofit = builder.build();
            }
        }

        return retrofit.create(serviceClass);
    }
}

createService现在具有HawkCredentials的第二个参数。 如果传递非空值,它将创建必要的Hawk身份验证拦截器并将其添加到Retrofit客户端。 我们还需要重建Retrofit以将更改应用到下一个请求。

URL的处理与解析

Url Handling Introduction

在Retrofit 2中,所有的URL都是使用由HttpUrl类来解决的,它是由OkHttp3提供给你的。 尽管如此,它引入了您需要处理您的应用程序中的所有网址的方式的更改:BaseURL和endpointURl以及为特定请求定义的任何动态网址。

请记住:Retrofit 2中的网址会像网页上的链接一样处理:<a href="..."> ... </a>。

baseUrl Resolution

使用Retrofit,您需要一个总是具有相同BaseURL的特定API。这个BaseURl共享相同的方案和主机,您可以在一个地方(使用Retrofit.Builder())定义它,并在必要时更改它,而不必触及应用程序中的每个终端。

Retrofit.Builder builder = new Retrofit.Builder()  
            .baseUrl("https://your.base.url/api/");

BaseURL用于每个请求,任何终端(如@GET等)都将针对此地址解析。BaseURL必修以斜杠结尾:/。具有相对路径地址的每个终端定义将正确解析,因为它将自身附加到已经定义或包括路径参数的BaseURL。

让我们来看一个例子:

# Good Practice
base url: https://futurestud.io/api/  
endpoint: my/endpoint  
Result:   https://futurestud.io/api/my/endpoint

# Bad Practice
base url: https://futurestud.io/api  
endpoint: /my/endpoint  
Result:   https://futurestud.io/my/endpoint  

上面的示例说明了如果你不以斜杠结尾你的BaseURl的api路径参数将被忽略,并从生成的请求网址中删除。

实际上,Retrofit帮助你,如果你传递一个基本url没有尾部斜线。 它会抛出异常,告诉你,你的基本url需要以斜杠结尾。

Absolute Urls

你可以将绝对url传递给你的端点url。 尽管如此,这种技术可能需要在您的应用程序中调用适当的端点。 随着时间的推移,您的后端将发布一个新的API版本。 根据版本控制的类型,让我们假设您的后端开发人员选择在网址中的API版本。 您需要将基本网址从v2压缩到v3。 此时,您必须处理API v3引入的所有突变。 要依赖于所选的v2端点,可以使用绝对URL来直接指定API版本。

# Example 1
base url: https://futurestud.io/api/v3/  
endpoint: my/endpoint  
Result:   https://futurestud.io/api/v3/my/endpoint

# Example 2
base url: https://futurestud.io/api/v3/  
endpoint: /api/v2/another/endpoint  
Result:   https://futurestud.io/api/v2/another/endpoint  

在更改BaseURL的情况下,你将自动更需吧所有端点以使用新的URL和请求。 可以看到,示例1的工作原理与预期一样,只是将端点url附加到针对v3的API调用的基本URL。

示例2说明了将基本URL升级到v3并仍然使用所选端点的API v2的情况。 这可能是由于您的客户端的巨大升级造成的,并且您仍然希望从其他端点的API v3的所有其他好处中获益。

Dynamic Urls or Passing a Full Url

使用Retrofit 2,您可以将给定的URL传递到端点,然后用于请求。也就是说,如果您已使用https定义了BaseURL,并且想要确保应用程序中的所有其他请求也都使用https,那么你只需使用双斜线//开始请求网址即可。 这是Web中的常见做法,以避免浏览器在同一页面上使用安全和不安全的资源时发出警告。

# Example 3 — completely different url
base url: http://futurestud.io/api/  
endpoint: https://api.futurestud.io/  
Result:   https://api.futurestud.io/

# Example 4 — Keep the base url’s scheme
base url: https://futurestud.io/api/  
endpoint: //api.futurestud.io/  
Result:   https://api.futurestud.io/

# Example 5 — Keep the base url’s scheme
base url: http://futurestud.io/api/  
endpoint: //api.github.com  
Result:   http://api.github.com  

示例3显示了在使用完全不同的URL时替换BaseURL。 此示例在请求具有不同位置的文件或图像时很有用,例如某些文件在您自己的服务器上,而其他文件或图像存储在Amazon的S3上。 您只将该位置作为网址接收并使用Retrofit,您可以传递请求端点的完整网址。

如前所述,您可以保留BaseURL的方案。 在示例4中,我们不使用API的路径段,而是使用子域。 我们仍然想保留以前定义的方案,因此只传递带有前导//的完整网址。

示例5使用与定义的BaseURL相同的方案,但用给定的端点url替换主机和路径段。

在运行时更换BaseURL

The Core: ServiceGenerator

ServiceGenerator使用多个静态字段和一个String常量API_BASE_URL,它保存API基址url:

public class ServiceGenerator {  
    public static final String API_BASE_URL = "http://futurestud.io/api";
    private static Retrofit retrofit;

    private static Retrofit.Builder builder =
            new Retrofit.Builder()
                    .addConverterFactory(GsonConverterFactory.create())
                    .baseUrl(API_BASE_URL);

    private static OkHttpClient.Builder httpClient =
            new OkHttpClient.Builder();

    // No need to instantiate this class.
    private ServiceGenerator() {

    }

    public static <S> S createService(Class<S> serviceClass, AccessToken token) {
        String authToken = token.getTokenType().concat(token.getAccessToken());
        return createService(serviceClass, authToken);
    }

    // more methods
    // ...
}

Adjusting the ServiceGenerator

通过此设置,您无需在运行时更改API_BASE_URL常量。假如你在源代码中改变它,编译一个新的.apk并再次测试它,这是非常不方便,如果你正在使用多个API部署,我们将对ServiceGenerator类进行小的更改:

public class ServiceGenerator {  
    public static String apiBaseUrl = "http://futurestud.io/api";
    private static Retrofit retrofit;

    private static Retrofit.Builder builder =
            new Retrofit.Builder()
                    .addConverterFactory(GsonConverterFactory.create())
                    .baseUrl(apiBaseUrl);

    private static OkHttpClient.Builder httpClient =
            new OkHttpClient.Builder();

    // No need to instantiate this class.
    private ServiceGenerator() {
    }

    public static void changeApiBaseUrl(String newApiBaseUrl) {
        apiBaseUrl = newApiBaseUrl;

        builder = new Retrofit.Builder()
                        .addConverterFactory(GsonConverterFactory.create())
                        .baseUrl(apiBaseUrl);
    }

    public static <S> S createService(Class<S> serviceClass, AccessToken token) {
        String authToken = token.getTokenType().concat(token.getAccessToken());
        return createService(serviceClass, authToken);
    }

    // more methods
    // ...
}

将常量API_BASE_URL重命名为非最终字段apiBaseUrl。添加新的静态方法changeApiBaseUrl(String newApiBaseUrl),它将更改apiBaseUrl变量。 它还会创建一个新版本的Retrofit.Builder实例构建器。 因为我们正在为请求重新使用构建器,如果我们不创建一个新的实例,所有的请求仍然会违反原来的apiBaseUrl值。

Example Usage

public class DynamicBaseUrlActivity extends AppCompatActivity {

    public static final String TAG = "CallInstances";
    private Callback<ResponseBody> downloadCallback;

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

        downloadCallback = new Callback<ResponseBody>() {
            @Override
            public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
                Log.d(TAG, "server contacted at: " + call.request().url());
            }

            @Override
            public void onFailure(Call<ResponseBody> call, Throwable t) {
                Log.d(TAG, "call failed against the url: " + call.request().url());
            }
        };

        // first request
        FileDownloadService downloadService = ServiceGenerator.create(FileDownloadService.class);
        Call<ResponseBody> originalCall = downloadService.downloadFileWithFixedUrl();
        originalCall.enqueue(downloadCallback);

        // change base url
        ServiceGenerator.changeApiBaseUrl("http://development.futurestud.io/api");

        // new request against new base url
        FileDownloadService newDownloadService = ServiceGenerator.create(FileDownloadService.class);
        Call<ResponseBody> newCall = newDownloadService.downloadFileWithFixedUrl();
        newCall.enqueue(downloadCallback);
    }
}

在第一个请求执行后,我们使用新的ServiceGenerator.changeApiBaseUrl()方法将基本url更改为我们的开发环境。 最后,我们将再次提出相同的下载请求。 当我们启动应用程序时,我们会收到以下日志:

D/CallInstances: server contacted at: http://futurestud.io/resource/example.zip  
D/CallInstances: server contacted at: http://development.futurestud.io/resource/example.zip 

When To Change the Base Url at Runtime

上面的演示代码简化了一些东西。我们通常在应用程序的调试版本中实现一个按钮,测试人员可以在其中选择所需的服务器环境。因此,根据您的情况,您可能需要编写更多的代码来决定何时以及到哪个基本URL Retrofit应该更改。

此外,我们只建议这样做的调试目的。我们不认为这是一个让您的应用程序与各种服务器同时工作的好方法。如果您的应用程序需要处理多个API,请查找其他版本。动态网址教程可能是一个好的开始。

最后,请测试您是否可以使用应用程序简单切换环境。例如,如果在服务器端存储用户和认证信息,切换环境可能会导致问题。您的生产数据库很可能不包含与您的开发数据库相同的用户,是否正确?在我们的应用中,我们删除所有相关的用户数据,并在测试人员通过新的基本网址更改环境后强制进行全新的登录。

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

推荐阅读更多精彩内容