Google组件化方案 - SPI之AutoService

市面上的组件化通信框架可谓是眼花缭乱,参差不齐。像阿里的ARouter, 美团的WMRouter, 还有个人开发者提供的CC框架,他们各有优缺点。今天我们介绍一款轻量级的组件化通信框架,谷歌的亲儿子 - AutoService。

使用篇

项目结构

在Android项目架构中,最底层一个Base层,然后是Common,所有的业务模块都依赖Common层,app被称为壳工程,它依赖所有的业务模块。

1.png

图很简陋,但整体就是这样一个结构了。我们就以一个简单的demo举例,创建一个kotlin项目:

2.png

common为通用模块,app和weblibrary为业务模块。我们要做的就是从app的MainActivity跳转到weblibrary的WebViewActivity。

依赖

首先需要在公共模块进行依赖:

api "com.google.auto.service:auto-service:1.0-rc7"

需要注意的是,如果某个业务模块需要对外开放,也就是需要其他模块进行访问,那么必须添加注解处理器,我们在weblibrary的build.gradle添加以下依赖项:

kapt "com.google.auto.service:auto-service:1.0-rc7"

HowUse

1. 接口下沉

在公共模块定义需要开放的接口,这个接口用于app模块的访问,从而实现跳转,我们叫做接口下沉。

interface AutoServiceInterface {

    fun toWebActivity(context: Context, url: String, title: String)
}

2. 接口实现

因为要访问的是 web模块,所以要在web模块进行接口的实现

@AutoService(AutoServiceInterface::class)
class AutoServiceImpl : AutoServiceInterface {

    override fun toWebActivity(context: Context, url: String, title: String?) {

        context.startActivity(Intent(context, WebViewActivity::class.java).apply {
            putExtra("url", url)
            putExtra("title", title)
        })

    }


}

需要注意的是,接口实现类需要使用AutoService注解,值为接口的类对象。这里通过context跳转到WebViewActivity,并传入url参数和title参数。

来看看WebViewActivity,接收到url参数和title参数,并进行展示和加载

class WebViewActivity : AppCompatActivity() {
    private lateinit var mUrl: String
    private lateinit var mTitle: String
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_web_view)
        mWebView.settings.javaScriptEnabled = true
        mUrl = intent?.getStringExtra("url") ?: ""
        mTitle = intent?.getStringExtra("title") ?: ""
        if (mTitle.isEmpty()) {
            mHeaderBar.visibility = View.GONE
        } else {
            mHeaderBar.visibility = View.VISIBLE
            mHeaderBar.setTitle(mTitle)
        }
        mWebView.loadUrl(mUrl)
    }

}

3. 访问

接下来就可以直接在app模块的MainActivity访问了

mBtnToWeb.setOnClickListener {

    val load = ServiceLoader.load(AutoServiceInterface::class.java)?.toList()

    if (load.isNullOrEmpty()) {
        Log.i("kangf", "load == null")
        return@setOnClickListener
    }
    load[0].toWebActivity(this, "https://www.baidu.com", "百度")
    load.forEach {
        Log.i("kangf", it.javaClass.canonicalName ?: it.javaClass.name)
    }


}

通过ServiceLoader的load方法,传入接口类类型,返回的是一个Itrator对象,因为接口的实现类可能不止一个。我们这里为了方便,获取第1个实现类,直接调用其方法即可。

我们先点击按钮,打印出来的是实现类的全名:

[图片上传失败...(image-8a891f-1701756859877)]

实际运行效果:

<img src="https://img-blog.csdnimg.cn/20200810164309640.gif#pic_center" alt="在这里插入图片描述" style="zoom:50%;" />

6不6,就是这么简单,模块间所有的交互都可以通过接口来实现。

原理篇

至于原理,也是非常简单,AutoService源码:https://github.com/google/auto

SPI

SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。这么说可能有些抽象,我们在源码中去理解。

注解

从AutoService注解来入手:

@Documented
@Retention(CLASS)
@Target(TYPE)
public @interface AutoService {
  /** Returns the interfaces implemented by this service provider. */
  Class<?>[] value();
}

这个注解的参数是一个Class, 看注释可以知道,这个注解使用在接口的实现类上面。对于注解不了解的同学,可以看一下这篇文章哦:

https://blog.csdn.net/qq_22090073/article/details/104476822

注解处理器

这个注解规定被标记在一个类上面,且在jvm加载class时被抛弃,所以这是一个在编译时处理的注解,必定是有注解处理器了。

注解处理器就是在编译时检测所有的注解,在处理器中查找我们需要的住进进行处理 (通过注解找到类元信息生成java代码或其他文件),然后同时被打包到jar文件中

@SupportedOptions({ "debug", "verify" })
public class AutoServiceProcessor extends AbstractProcessor {

  // 省略不重要的东东。。。。。。。。。。。。。。。。。。。。

  /**
   * <ol>
   *  <li> 遍历所有带有AutoService注解的类<ul>
   *      <li> 验证AutoService的值是否正确
   *      <li> 按服务接口对class进行分类
   *      </ul>
   *
   *  <li> For each {@link AutoService} interface <ul>
   *       <li> 创建一个文件 在META-INF/services/路径下
   *       <li> 遍历所有带有AutoService注解的类 <ul>
   *           <li>在文件里面创建一个实体(也就是类的全路径)
   *           </ul>
   *       </ul>
   * </ol>
   */
    
    /**
     * 相当于main函数,开始处理注解
     * 注解处理器的核心方法,处理具体的注解,生成Java文件
     *
     * @param set              使用了支持处理注解的节点集合
     * @param roundEnvironment 当前或是之前的运行环境,可以通过该对象查找的注解。
     * @return true 表示后续处理器不会再处理(已经处理完成)
     */
  @Override
  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    try {
      // 调用了processImpl方法
      return processImpl(annotations, roundEnv);
    } catch (Exception e) {
      // 如果在写入的过程中发生了错误,就把错误写入到文件中
      StringWriter writer = new StringWriter();
      e.printStackTrace(new PrintWriter(writer));
      fatalError(writer.toString());
      return true;
    }
  }

  private boolean processImpl(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    // 查找所有的带注解的class,生成配置文件
    // 1. 如果注解处理完成,生成文件
    if (roundEnv.processingOver()) {
      
      generateConfigFiles();
    } else { // 2. 否则处理注解
      processAnnotations(annotations, roundEnv);
    }

    return true;
  }

  // 处理注解
  private void processAnnotations(Set<? extends TypeElement> annotations,
      RoundEnvironment roundEnv) {

    // 找到所有带注解的节点   elements,每个带AutoService注解的class就是一个节点
    // 其实就是把class封装成了一个Element对象
    Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(AutoService.class);

    log(annotations.toString());
    log(elements.toString());

    // 遍历节点
    for (Element e : elements) {
      // TODO(gak): check for error trees?
      // 类节点就是TypeElement
      TypeElement providerImplementer = (TypeElement) e;
      // 将找到的实现了放到providers中,因为可能有多
      AnnotationMirror annotationMirror = getAnnotationMirror(e, AutoService.class).get();
      Set<DeclaredType> providerInterfaces = getValueFieldOfClasses(annotationMirror);
      if (providerInterfaces.isEmpty()) {
        error(MISSING_SERVICES_ERROR, e, annotationMirror);
        continue;
      }
      for (DeclaredType providerInterface : providerInterfaces) {
        TypeElement providerType = MoreTypes.asTypeElement(providerInterface);

        log("provider interface: " + providerType.getQualifiedName());
        log("provider implementer: " + providerImplementer.getQualifiedName());

        if (checkImplementer(providerImplementer, providerType)) {
          providers.put(getBinaryName(providerType), getBinaryName(providerImplementer));
        } else {
          String message = "ServiceProviders must implement their service provider interface. "
              + providerImplementer.getQualifiedName() + " does not implement "
              + providerType.getQualifiedName();
          error(message, e, annotationMirror);
        }
      }
    }
  }

  // 生成配置文件
  private void generateConfigFiles() {
    // 使用Filer对象生成文件,所以先创建出来
    Filer filer = processingEnv.getFiler();

    // 遍历找到的接口
    for (String providerInterface : providers.keySet()) {
      String resourceFile = "META-INF/services/" + providerInterface;
      log("Working on resource file: " + resourceFile);
      try {
        // 1. 创建一个set集合
        SortedSet<String> allServices = Sets.newTreeSet();
        try {
          // would like to be able to print the full path
          // before we attempt to get the resource in case the behavior
          // of filer.getResource does change to match the spec, but there's
          // no good way to resolve CLASS_OUTPUT without first getting a resource.
          FileObject existingFile = filer.getResource(StandardLocation.CLASS_OUTPUT, "",
              resourceFile);
          log("Looking for existing resource file at " + existingFile.toUri());
          // 2. 找到原来文件中存在的实现类,
          Set<String> oldServices = ServicesFiles.readServiceFile(existingFile.openInputStream());
          log("Existing service entries: " + oldServices);
          // 3. 放到新的集合中,保证allServices中数据是文件中的最新数据
          allServices.addAll(oldServices);
            
          
        } catch (IOException e) {
          // According to the javadoc, Filer.getResource throws an exception
          // if the file doesn't already exist.  In practice this doesn't
          // appear to be the case.  Filer.getResource will happily return a
          // FileObject that refers to a non-existent file but will throw
          // IOException if you try to open an input stream for it.
          log("Resource file did not already exist.");
        }
   
        Set<String> newServices = new HashSet<String>(providers.get(providerInterface));
        if (allServices.containsAll(newServices)) {
          log("No new service entries being added.");
          return;
        }

        allServices.addAll(newServices);
        log("New service file contents: " + allServices);
        FileObject fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT, "",
            resourceFile);
        OutputStream out = fileObject.openOutputStream();
        // 在文件中一次性写入
        ServicesFiles.writeServiceFile(allServices, out);
        out.close();
        log("Wrote to: " + fileObject.toUri());
      } catch (IOException e) {
        fatalError("Unable to create " + resourceFile + ", " + e);
        return;
      }
    }
  }

  /**
   * Verifies {@link ServiceProvider} constraints on the concrete provider class.
   * Note that these constraints are enforced at runtime via the ServiceLoader,
   * we're just checking them at compile time to be extra nice to our users.
   */
  private boolean checkImplementer(TypeElement providerImplementer, TypeElement providerType) {

    String verify = processingEnv.getOptions().get("verify");
    if (verify == null || !Boolean.valueOf(verify)) {
      return true;
    }

    // TODO: We're currently only enforcing the subtype relationship
    // constraint. It would be nice to enforce them all.

    Types types = processingEnv.getTypeUtils();

    return types.isSubtype(providerImplementer.asType(), providerType.asType());
  }

  /**
   * Returns the binary name of a reference type. For example,
   * {@code com.google.Foo$Bar}, instead of {@code com.google.Foo.Bar}.
   *
   */
  private String getBinaryName(TypeElement element) {
    return getBinaryNameImpl(element, element.getSimpleName().toString());
  }

  private String getBinaryNameImpl(TypeElement element, String className) {
    Element enclosingElement = element.getEnclosingElement();

    if (enclosingElement instanceof PackageElement) {
      PackageElement pkg = (PackageElement) enclosingElement;
      if (pkg.isUnnamed()) {
        return className;
      }
      return pkg.getQualifiedName() + "." + className;
    }

    TypeElement typeElement = (TypeElement) enclosingElement;
    return getBinaryNameImpl(typeElement, typeElement.getSimpleName() + "$" + className);
  }

  /**
   * Returns the contents of a {@code Class[]}-typed "value" field in a given {@code annotationMirror}.
   */
  private ImmutableSet<DeclaredType> getValueFieldOfClasses(AnnotationMirror annotationMirror) {
    return getAnnotationValue(annotationMirror, "value")
        .accept(
            new SimpleAnnotationValueVisitor8<ImmutableSet<DeclaredType>, Void>() {
              @Override
              public ImmutableSet<DeclaredType> visitType(TypeMirror typeMirror, Void v) {
                // TODO(ronshapiro): class literals may not always be declared types, i.e. int.class,
                // int[].class
                return ImmutableSet.of(MoreTypes.asDeclared(typeMirror));
              }

              @Override
              public ImmutableSet<DeclaredType> visitArray(
                  List<? extends AnnotationValue> values, Void v) {
                return values
                    .stream()
                    .flatMap(value -> value.accept(this, null).stream())
                    .collect(toImmutableSet());
              }
            },
            null);
  }

  private void log(String msg) {
    if (processingEnv.getOptions().containsKey("debug")) {
      processingEnv.getMessager().printMessage(Kind.NOTE, msg);
    }
  }

  private void error(String msg, Element element, AnnotationMirror annotation) {
    processingEnv.getMessager().printMessage(Kind.ERROR, msg, element, annotation);
  }

  private void fatalError(String msg) {
    processingEnv.getMessager().printMessage(Kind.ERROR, "FATAL ERROR: " + msg);
  }
}



final class ServicesFiles {
  public static final String SERVICES_PATH = "META-INF/services";

  private ServicesFiles() { }

  static String getPath(String serviceName) {
    return SERVICES_PATH + "/" + serviceName;
  }

  /**
   * 读取文件
   */
  static Set<String> readServiceFile(InputStream input) throws IOException {
    HashSet<String> serviceClasses = new HashSet<String>();
    Closer closer = Closer.create();
    try {
      // TODO(gak): use CharStreams
      BufferedReader r = closer.register(new BufferedReader(new InputStreamReader(input, UTF_8)));
      String line;
      while ((line = r.readLine()) != null) {
        int commentStart = line.indexOf('#');
        if (commentStart >= 0) {
          line = line.substring(0, commentStart);
        }
        line = line.trim();
        if (!line.isEmpty()) {
          serviceClasses.add(line);
        }
      }
      return serviceClasses;
    } catch (Throwable t) {
      throw closer.rethrow(t);
    } finally {
      closer.close();
    }
  }

  /**
   * 按接口实现类全名和接口全名写入文件
   *
   * @param output not {@code null}. Not closed after use.
   * @param services a not {@code null Collection} of service class names.
   * @throws IOException
   */
  static void writeServiceFile(Collection<String> services, OutputStream output)
      throws IOException {
    BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(output, UTF_8));
    for (String service : services) {
      writer.write(service);
      writer.newLine();
    }
    writer.flush();
  }
}

以上就是注解处理器的全部关键代码,总共分为以下几步:

  • 1.遍历找到所有带有AutoService注解的类
  • 2.验证AutoService注解的值是否正确
  • 3.遍历所有的下沉接口
  • 3.在META-INF/services/路径下创建文件,文件名以类的接口类全路径命名
  • 4.在文件里写入内容,实现类(当前注解类)的全路径

第三、四步可能大家不太明白,拿我们上面的例子来说:

其实就是在META-INF/services/路径下生成了一个名字为com.kagnf.webview.autoservice.AutoServiceInterface的文件

文件内容是com.kangf.webview.autoservice.AutoServiceImpl,大家打包后可以在apk里面看到:

<img src="https://img-blog.csdnimg.cn/20200825160553694.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIyMDkwMDcz,size_16,color_FFFFFF,t_70#pic_center" alt="在这里插入图片描述" style="zoom:50%;" />

如果有多个下沉接口,肯定就会有多个文件了。源码看起来复杂,但是处理器具体就这几步的操作。

ServiceLoader

注解处理器只生成了这样一个文件,可是怎么用呢?这时候Java API的作用就体现出来了,在上面的demo中,我们通过ServiceLoader.load(AutoServiceInterface::class.java)?.toList()实现了接口的实现类的查找,因为可能不止一个实现类,所以这里返回了一个Iterator对象,使用toList方法转成List。

那么ServiceLoader是怎么实现具体查找的?走进去看看吧

// ServiceLoader.java
public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

创建了一个类加载器,调用自己的load方法():

public static <S> ServiceLoader<S> load(Class<S> service,
                                        ClassLoader loader)
{
    return new ServiceLoader<>(service, loader);
}

然后直接通过new的方式,创建了一个ServiceLoader对象,并返回,上面我们知道它返回是一个Iterator,所以它是实现了Iteratable接口的:

public final class ServiceLoader<S>
    implements Iterable<S>
{
// 。。。。。。。。。。。
}

那么不用想,它的主要逻辑就在构造方法里面了:

public void reload() {
    providers.clear();
    lookupIterator = new LazyIterator(service, loader);
}

private ServiceLoader(Class<S> svc, ClassLoader cl) {
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    // Android改变了:不要使用旧的安全代码。
    // 在Android中, System.getSecurityManager() 永远为null
    // acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    reload();
}

构造方法有两个参数,svc是我们传进来的接口的class对象,cl是内部创建的一个ClassLoader。首先给全局的service变量赋值为class对象,然后调用了reload()方法。

reload()方法中,创建了一个懒加载的迭代器:lookupIterator = new LazyIterator(service, loader);,并传入了class对象和类加载器,这里其实相当于一个门面模式,当调用ServiceLoader.iterator()时创建迭代器时,会使用到lookupIterator的hasNext()和next()

public Iterator<S> iterator() {
    return new Iterator<S>() {

        Iterator<Map.Entry<String,S>> knownProviders
            = providers.entrySet().iterator();

        // 返回懒加载迭代器的hasNext
        public boolean hasNext() {
            if (knownProviders.hasNext())
                return true;
            return lookupIterator.hasNext();
        }

        // 返回懒加载迭代器的next
        public S next() {
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            return lookupIterator.next();
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }

    };
}

接下来我们看看这个懒加载迭代器怎么创建的:

private class LazyIterator
    implements Iterator<S>
{

    Class<S> service;
    ClassLoader loader;
    // 当前接口对应的文件节点
    Enumeration<URL> configs = null;
    // 文件中的具体实现类的全路径,因为可能不止一个实现类,所以使用Iterator
    Iterator<String> pending = null;
    // 下一个实现类的文件名
    String nextName = null;

    private LazyIterator(Class<S> service, ClassLoader loader) {
        this.service = service;
        this.loader = loader;
    }

    private boolean hasNextService() {
        // 第一次进入,nextName为Null
        if (nextName != null) {
            return true;
        }
        // configs为null,进入if语句
        if (configs == null) {
            try {
                // private static final String PREFIX = "META-INF/services/";
                // fullname就是找到META-INF/services/目录下的,以接口名全名命名的文件
                // demo举例就是 META-INF/services/com.kagnf.webview.autoservice.AutoServiceInterface
                String fullName = PREFIX + service.getName();
                if (loader == null)
                    configs = ClassLoader.getSystemResources(fullName);
                else // classloader加载这个资源文件,返回一个Enumeration节点对象
                    configs = loader.getResources(fullName);
            } catch (IOException x) {
                fail(service, "Error locating configuration files", x);
            }
        }
        while ((pending == null) || !pending.hasNext()) {
            // 判断有没有更多的节点
            if (!configs.hasMoreElements()) {
                return false;
            }
            // 解析节点,返回文件内的实现类集合
            pending = parse(service, configs.nextElement());
        }
        // 给nextName赋值
        nextName = pending.next();
        return true;
    }

    private S nextService() {
        if (!hasNextService())
            throw new NoSuchElementException();
        String cn = nextName;
        nextName = null;
        Class<?> c = null;
        try {
            c = Class.forName(cn, false, loader);
        } catch (ClassNotFoundException x) {
            // 。。。。。。。。。。。。。
        }
        if (!service.isAssignableFrom(c)) {
            // 抛异常,这里不用管 。。。。。。。。。。
          
        }
        try {
            S p = service.cast(c.newInstance());
            providers.put(cn, p);
            return p;
        } catch (Throwable x) {
            //。。。。。。。。。。。。。。。。。。。。。。。。。
        }
        throw new Error();          // This cannot happen
    }

    public boolean hasNext() {
            return hasNextService();
    }

    public S next() {
            return nextService();
      
    }

    public void remove() {
        throw new UnsupportedOperationException();
    }

}

以上代码并不难, 重写hasNext(),和next()方法,分别调用了hasNextService()nextService(),顾名思义,nextService()方法就是返回具体的实现类

nextService()主要分为以下几步:

  • 首先调用hasNextService()方法,判断有没有更多的接口对应的文件
  • hasNextService中,找到下一个文件,并解析内容,把文件的class名放到Iterator中,给nextName赋值
  • 使用Class.forName加载文件中的类,并使用c.newInstance()创建并返回类的实体

这样就完成了实现类的创建!

有同学有疑问,我们并没有用到ServiceLoader的next方法啊,因为kotlin的语法糖可以直接toList, 在java中正确的用法应该是:

val load = ServiceLoader.load(AutoServiceInterface::class.java)?.iterator().next();

这样是不是就一目了然了?这里获取的就是com.kagnf.webview.autoservice.AutoServiceInterface文件中的第一个实现类

总结

是不是很简单,一句话就能总价,先通过注解处理器在META-INF/services/目录下生成对应的文件,再通过ServiceLoader加载文件中的类并返回。这就是java中大名鼎鼎的SPI机制。
对于注解处理器不太了解的同学可以看下这篇文章:
手写ARouter

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

推荐阅读更多精彩内容