Glide4.9图片框架源码(五)之存储缓存和回调显示

上一节我们说到了如何获取磁盘缓存和开启图片请求的,需要回顾的同学可以继续查看:

Glide4.9图片框架源码(四)之获取磁盘缓存和图片请求

HttpUrlFetcher loadData()

  @Override
  public void loadData(@NonNull Priority priority,
      @NonNull DataCallback<? super InputStream> callback) {
    long startTime = LogTime.getLogTime();
    try {
      InputStream result = loadDataWithRedirects(glideUrl.toURL(), 0, null, glideUrl.getHeaders());
      callback.onDataReady(result);
    } catch (IOException e) {
      if (Log.isLoggable(TAG, Log.DEBUG)) {
        Log.d(TAG, "Failed to load data for url", e);
      }
      callback.onLoadFailed(e);
    } finally {
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        Log.v(TAG, "Finished http url fetcher fetch in " + LogTime.getElapsedMillis(startTime));
      }
    }
  }

上一节我们讲述了HttpUrlFetcher调用loadData请求图片数据并且返回图片对应的InputStream,那么这一节我们着重看下是如何将InputStream回调到最初的ViewTarget并且显示的,所以这里我们要看仔细每一个回调监听。这里我们看到调用了callback.onDataReady(result)方法,那么callback是由loaddata传进来的,我们返回再看看是怎么入参的:

SourceGenerator

  @Override
  public boolean startNext() {
    .........
    .........
    while (!started && hasNextModelLoader()) {
      loadData = helper.getLoadData().get(loadDataListIndex++);
      if (loadData != null
          && (helper.getDiskCacheStrategy().isDataCacheable(loadData.fetcher.getDataSource())
          || helper.hasLoadPath(loadData.fetcher.getDataClass()))) {
        started = true;
        loadData.fetcher.loadData(helper.getPriority(), this);
      }
    }
    return started;
  }

可以看到SourceGenerator传入的是this,所以上面的onDataReady方法回调到SourceGenerator的onDataReady方法,我们继续看回调:

  @Override
  public void onDataReady(Object data) {
    DiskCacheStrategy diskCacheStrategy = helper.getDiskCacheStrategy();
    if (data != null && diskCacheStrategy.isDataCacheable(loadData.fetcher.getDataSource())) {
      dataToCache = data;
      // We might be being called back on someone else's thread. Before doing anything, we should
      // reschedule to get back onto Glide's thread.
      cb.reschedule();
    } else {
      cb.onDataFetcherReady(loadData.sourceKey, data, loadData.fetcher,
          loadData.fetcher.getDataSource(), originalKey);
    }
  }

这里isDataCacheable是去判读是否能缓存到磁盘,如果不能,就继续往上回调,如果可以缓存,那么将当前的inputstream赋值给dataToCache,然后调用cb.reschedule(),我们来看看这个cb监听是什么时候传进来的:

  private final FetcherReadyCallback cb;

  private int loadDataListIndex;
  private DataCacheGenerator sourceCacheGenerator;
  private Object dataToCache;
  private volatile ModelLoader.LoadData<?> loadData;
  private DataCacheKey originalKey;

  SourceGenerator(DecodeHelper<?> helper, FetcherReadyCallback cb) {
    this.helper = helper;
    this.cb = cb;
  }

可以看到FetcherReadyCallback是在SourceGenerator初始化的时候传进来的,那么看下SourceGenerator的初始化方法:

DecodeJob
  private DataFetcherGenerator getNextGenerator() {
    switch (stage) {
      case RESOURCE_CACHE:
        return new ResourceCacheGenerator(decodeHelper, this);
      case DATA_CACHE:
        return new DataCacheGenerator(decodeHelper, this);
      case SOURCE:
        return new SourceGenerator(decodeHelper, this);
      case FINISHED:
        return null;
      default:
        throw new IllegalStateException("Unrecognized stage: " + stage);
    }
  }

可以看到是在DecodeJob的getNextGenerator方法中去初始化的,所以我们看看他的reschedule方法:

  @Override
  public void reschedule() {
    runReason = RunReason.SWITCH_TO_SOURCE_SERVICE;
    callback.reschedule(this);
  }
 @Override
  public void reschedule(DecodeJob<?> job) {
    getActiveSourceExecutor().execute(job);
  }

可以看到还是重新执行了DecodeJob线程,直接去加载SourceGenerator,我们跟进去看一下源码:

  @Override
  public boolean startNext() {
    if (dataToCache != null) {
      Object data = dataToCache;
      dataToCache = null;
      cacheData(data);
    }

    if (sourceCacheGenerator != null && sourceCacheGenerator.startNext()) {
      return true;
    }
    sourceCacheGenerator = null;

    loadData = null;
    boolean started = false;
    while (!started && hasNextModelLoader()) {
      loadData = helper.getLoadData().get(loadDataListIndex++);
      if (loadData != null
          && (helper.getDiskCacheStrategy().isDataCacheable(loadData.fetcher.getDataSource())
          || helper.hasLoadPath(loadData.fetcher.getDataClass()))) {
        started = true;
        loadData.fetcher.loadData(helper.getPriority(), this);
      }
    }
    return started;
  }
  private void cacheData(Object dataToCache) {
    long startTime = LogTime.getLogTime();
    try {
      Encoder<Object> encoder = helper.getSourceEncoder(dataToCache);
      DataCacheWriter<Object> writer =
          new DataCacheWriter<>(encoder, dataToCache, helper.getOptions());
      originalKey = new DataCacheKey(loadData.sourceKey, helper.getSignature());
      helper.getDiskCache().put(originalKey, writer);
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        Log.v(TAG, "Finished encoding source to cache"
            + ", key: " + originalKey
            + ", data: " + dataToCache
            + ", encoder: " + encoder
            + ", duration: " + LogTime.getElapsedMillis(startTime));
      }
    } finally {
      loadData.fetcher.cleanup();
    }

    sourceCacheGenerator =
        new DataCacheGenerator(Collections.singletonList(loadData.sourceKey), helper, this);
  }

由于我们之前已经分析过这一块的startNext方法,所以这里调用的是SourceGenerator的startNext,可以看到第一行直接判读按dataToCache非空,dataToCache在刚才获取input stream的时候已经赋值给dataToCache,所以这里直接执行cacheData方法去缓存数据。cacheData里面的helper.getSourceEncoder是通过传入的data类型,返回一个Encoder对应的解码器。然后又生成了一个DataCacheWriter包装类,传入了解码器和dataToCache流数据,调用 helper.getDiskCache().put方法,我们跟进去看下put方法:

@Override
  public void put(Key key, Writer writer) {
    String safeKey = safeKeyGenerator.getSafeKey(key);
    writeLocker.acquire(safeKey);
    try {
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        Log.v(TAG, "Put: Obtained: " + safeKey + " for for Key: " + key);
      }
      try {
        DiskLruCache diskCache = getDiskCache();
        Value current = diskCache.get(safeKey);
        if (current != null) {
          return;
        }
        DiskLruCache.Editor editor = diskCache.edit(safeKey);
        if (editor == null) {
          throw new IllegalStateException("Had two simultaneous puts for: " + safeKey);
        }
        try {
          File file = editor.getFile(0);
          if (writer.write(file)) {
            editor.commit();
          }
        } finally {
          editor.abortUnlessCommitted();
        }
      } catch (IOException e) {
        if (Log.isLoggable(TAG, Log.WARN)) {
          Log.w(TAG, "Unable to put to disk cache", e);
        }
      }
    } finally {
      writeLocker.release(safeKey);
    }
  }

可以看到传进来的writer调用了write(file),这里我们就不跟进去了,就是一般的读取流然后存储到文件,这里就把数据缓存到了硬盘缓存,回到哦最初的cacheData方法,重新去new DataCacheGenerator并且传入的监听是this,sourceCacheGenerator = new DataCacheGenerator(Collections.singletonList(loadData.sourceKey), helper, this),然后继续调用startStage中的sourceCacheGenerator.startNext,则去调用了DataCacheGenerator的startNext方法,继续看:

@Override
  public boolean startNext() {
    while (modelLoaders == null || !hasNextModelLoader()) {
      sourceIdIndex++;
      if (sourceIdIndex >= cacheKeys.size()) {
        return false;
      }

      Key sourceId = cacheKeys.get(sourceIdIndex);
      @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
      Key originalKey = new DataCacheKey(sourceId, helper.getSignature());
      cacheFile = helper.getDiskCache().get(originalKey);
      if (cacheFile != null) {
        this.sourceKey = sourceId;
        modelLoaders = helper.getModelLoaders(cacheFile);
        modelLoaderIndex = 0;
      }
    }
    loadData = null;
    boolean started = false;
    while (!started && hasNextModelLoader()) {
      ModelLoader<File, ?> modelLoader = modelLoaders.get(modelLoaderIndex++);
      loadData =
          modelLoader.buildLoadData(cacheFile, helper.getWidth(), helper.getHeight(),
              helper.getOptions());
      if (loadData != null && helper.hasLoadPath(loadData.fetcher.getDataClass())) {
        started = true;
        loadData.fetcher.loadData(helper.getPriority(), this);
      }
    }
    return started;
  }

这里为什么又去执行一次呢,其实这里是从磁盘缓存中去读取刚刚我们缓存的数据,那么这里的loadData.fetcher.loadData则去缓存中加载缓存图片文件并且再去回调,我们看下FileFetcher的loadData方法:

    @Override
    public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super Data> callback) {
      try {
        data = opener.open(file);
      } catch (FileNotFoundException e) {
        if (Log.isLoggable(TAG, Log.DEBUG)) {
          Log.d(TAG, "Failed to open file", e);
        }
        callback.onLoadFailed(e);
        return;
      }
      callback.onDataReady(data);
    }

直接通过opener.open去获取文件,加载的回调就在DataCacheGenerator的onDataReady中

@Override
  public void onDataReady(Object data) {
    cb.onDataFetcherReady(sourceKey, data, loadData.fetcher, DataSource.DATA_DISK_CACHE, sourceKey);
  }
 @Override
  public void onDataFetcherReady(Key sourceKey, Object data, DataFetcher<?> fetcher,
      DataSource dataSource, Key attemptedKey) {
    cb.onDataFetcherReady(sourceKey, data, fetcher, loadData.fetcher.getDataSource(), sourceKey);
  }
 @Override
  public void onDataFetcherReady(Key sourceKey, Object data, DataFetcher<?> fetcher,
      DataSource dataSource, Key attemptedKey) {
    this.currentSourceKey = sourceKey;
    this.currentData = data;
    this.currentFetcher = fetcher;
    this.currentDataSource = dataSource;
    this.currentAttemptingKey = attemptedKey;
    if (Thread.currentThread() != currentThread) {
      runReason = RunReason.DECODE_DATA;
      callback.reschedule(this);
    } else {
      GlideTrace.beginSection("DecodeJob.decodeFromRetrievedData");
      try {
        decodeFromRetrievedData();
      } finally {
        GlideTrace.endSection();
      }
    }
  }

继续回调依然是到上层的sourceCacheGenerator的onDataFetcherReady中,然后sourceCacheGenerator回调到Decode中的onDataFetcherReady方法,这里的currentThread从开启子线程执行DecodeJobs后没有做过线程切换,所以依然是当前线程,所以执行callback.reschedule(this)方法并且赋值runReason为DECODE_DATA准备再次执行Decode线程去解析磁盘缓存回调的数据:

  private void runWrapped() {
    switch (runReason) {
      case INITIALIZE:
        stage = getNextStage(Stage.INITIALIZE);
        currentGenerator = getNextGenerator();
        runGenerators();
        break;
      case SWITCH_TO_SOURCE_SERVICE:
        runGenerators();
        break;
      case DECODE_DATA:
        decodeFromRetrievedData();
        break;
      default:
        throw new IllegalStateException("Unrecognized run reason: " + runReason);
    }
  }
private void decodeFromRetrievedData() {
    if (Log.isLoggable(TAG, Log.VERBOSE)) {
      logWithTimeAndKey("Retrieved data", startFetchTime,
          "data: " + currentData
              + ", cache key: " + currentSourceKey
              + ", fetcher: " + currentFetcher);
    }
    Resource<R> resource = null;
    try {
      resource = decodeFromData(currentFetcher, currentData, currentDataSource);
    } catch (GlideException e) {
      e.setLoggingDetails(currentAttemptingKey, currentDataSource);
      throwables.add(e);
    }
    if (resource != null) {
      notifyEncodeAndRelease(resource, currentDataSource);
    } else {
      runGenerators();
    }
  }

由于是 runReason = RunReason.DECODE_DATA,所以直接执行decodeFromRetrievedData,这里调用了decodeFromData去解析数据,并返回一个Resource,这里的Resource就是加载和解析过后的数据,不去细究,直接看回调notifyEncodeAndRelease方法:

  private void notifyEncodeAndRelease(Resource<R> resource, DataSource dataSource) {
    if (resource instanceof Initializable) {
      ((Initializable) resource).initialize();
    }
    Resource<R> result = resource;
    LockedResource<R> lockedResource = null;
    if (deferredEncodeManager.hasResourceToEncode()) {
      lockedResource = LockedResource.obtain(resource);
      result = lockedResource;
    }
    notifyComplete(result, dataSource);
    stage = Stage.ENCODE;
    try {
      if (deferredEncodeManager.hasResourceToEncode()) {
        deferredEncodeManager.encode(diskCacheProvider, options);
      }
    } finally {
      if (lockedResource != null) {
        lockedResource.unlock();
      }
    }
    onEncodeComplete();
  }

  private void notifyComplete(Resource<R> resource, DataSource dataSource) {
    setNotifiedOrThrow();
    callback.onResourceReady(resource, dataSource);
  }

这里直接看回调notifyComplete方法,调用了callback.onResourceReady方法,可能我们已经忘了这里的callback到底是什么时候传进来的,来看一下:

 DecodeJob<R> init(
      GlideContext glideContext,
      Object model,
      EngineKey loadKey,
      Key signature,
      int width,
      int height,
      Class<?> resourceClass,
      Class<R> transcodeClass,
      Priority priority,
      DiskCacheStrategy diskCacheStrategy,
      Map<Class<?>, Transformation<?>> transformations,
      boolean isTransformationRequired,
      boolean isScaleOnlyOrNoTransform,
      boolean onlyRetrieveFromCache,
      Options options,
      Callback<R> callback,
      int order) {
    ......
    this.callback = callback;
    this.order = order;
    this.runReason = RunReason.INITIALIZE;
    this.model = model;
    return this;
  }

可以看到调用的是DecodeJob的initial方法,最终发现这里的callback其实就是EngineJob本身,所以看一看EngineJob的onResourceReady方法:

  @Override
  public void onResourceReady(Resource<R> resource, DataSource dataSource) {
    synchronized (this) {
      this.resource = resource;
      this.dataSource = dataSource;
    }
    notifyCallbacksOfResult();
  }

这里分别赋值了resource和dataSource,这两个变量就是我们说到的磁盘缓存的两种类型,继续看notifyCallbacksOfResult方法:

void notifyCallbacksOfResult() {
    ResourceCallbacksAndExecutors copy;
    Key localKey;
    EngineResource<?> localResource;
    synchronized (this) {
      stateVerifier.throwIfRecycled();
      if (isCancelled) {
        resource.recycle();
        release();
        return;
      } else if (cbs.isEmpty()) {
        throw new IllegalStateException("Received a resource without any callbacks to notify");
      } else if (hasResource) {
        throw new IllegalStateException("Already have resource");
      }
      engineResource = engineResourceFactory.build(resource, isCacheable);
      hasResource = true;
      copy = cbs.copy();
      incrementPendingCallbacks(copy.size() + 1);
      localKey = key;
      localResource = engineResource;
    }
    listener.onEngineJobComplete(this, localKey, localResource);
    for (final ResourceCallbackAndExecutor entry : copy) {
      entry.executor.execute(new CallResourceReady(entry.cb));
    }
    decrementPendingCallbacks();
  }
  @Override
  public synchronized void onEngineJobComplete(
      EngineJob<?> engineJob, Key key, EngineResource<?> resource) {
    // A null resource indicates that the load failed, usually due to an exception.
    if (resource != null && resource.isMemoryCacheable()) {
      activeResources.activate(key, resource);
    }

    jobs.removeIfCurrent(key, engineJob);
  }

  synchronized void activate(Key key, EngineResource<?> resource) {
    ResourceWeakReference toPut =
        new ResourceWeakReference(
            key, resource, resourceReferenceQueue, isActiveResourceRetentionAllowed);
    ResourceWeakReference removed = activeEngineResources.put(key, toPut);
    if (removed != null) {
      removed.reset();
    }
  }

ResourceCallbacksAndExecutors 这里存储的是回调的线程池执行器,我们先看后面,有一个EngineResource对象,还记得之前我们的内存缓存吗,对了,这里就是新建一个缓存对象,下面的engineResourceFactory.build方法,就是通过传入已经加载完成的resource生成EngineResource内存缓存。继续看listener的onEngineJobComplete方法,传入了key和engineResource,这里我们看到了两个熟悉的变量,isMemoryCacheable和activeResources,就是判断是否缓存到内存,如果是,那么直接通过activeResources的activate方法构造出ResourceWeakReference并且存储到弱引用缓存。

回到上面的notifyCallbacksOfResult,存储到内存缓存后,应该开始回调到最初的加载的地方并且设置图片了,我们继续看下entry.executor.execute(new CallResourceReady(entry.cb))方法,首先entry里存储的就是copy的ResourceCallbackAndExecutor,而copy是通过调用cbs的copy方法获取的,我们跟进去看一下

 static final class ResourceCallbacksAndExecutors
      implements Iterable<ResourceCallbackAndExecutor> {
    private final List<ResourceCallbackAndExecutor> callbacksAndExecutors;

    ResourceCallbacksAndExecutors() {
      this(new ArrayList<ResourceCallbackAndExecutor>(2));
    }

    void add(ResourceCallback cb, Executor executor) {
      callbacksAndExecutors.add(new ResourceCallbackAndExecutor(cb, executor));
    }

    void remove(ResourceCallback cb) {
      callbacksAndExecutors.remove(defaultCallbackAndExecutor(cb));
    }

    ResourceCallbacksAndExecutors copy() {
      return new ResourceCallbacksAndExecutors(new ArrayList<>(callbacksAndExecutors));
    }

发现callbacksAndExecutors就是ResourceCallbacksAndExecutors的一个变量,通过调用add方法添加Executor,那么我们就跟进去看在什么时候添加了这个回调执行器。

 synchronized void addCallback(final ResourceCallback cb, Executor callbackExecutor) {
    stateVerifier.throwIfRecycled();
    cbs.add(cb, callbackExecutor);
    if (hasResource) {
      incrementPendingCallbacks(1);
      callbackExecutor.execute(new CallResourceReady(cb));
    } else if (hasLoadFailed) {
      incrementPendingCallbacks(1);
      callbackExecutor.execute(new CallLoadFailed(cb));
    } else {
      Preconditions.checkArgument(!isCancelled, "Cannot add callbacks to a cancelled EngineJob");
    }
  }

    jobs.put(key, engineJob);
    engineJob.addCallback(cb, callbackExecutor);
    engineJob.start(decodeJob);

我们发现,callbackExecutor回调执行器在请求图片资源,开启decodeJob的时候就添加进来了,继续往上跟,看清楚callbackExecutor的真正对象是什么:

  <Y extends Target<TranscodeType>> Y into(
      @NonNull Y target,
      @Nullable RequestListener<TranscodeType> targetListener,
      Executor callbackExecutor) {
    return into(target, targetListener, /*options=*/ this, callbackExecutor);
  }

  public <Y extends Target<TranscodeType>> Y into(@NonNull Y target) {
    return into(target, /*targetListener=*/ null, Executors.mainThreadExecutor());
  }

 private static final Executor MAIN_THREAD_EXECUTOR =
      new Executor() {
        private final Handler handler = new Handler(Looper.getMainLooper());

        @Override
        public void execute(@NonNull Runnable command) {
          handler.post(command);
        }
      };

跟到最初始的地方我们发现,原来在调用into方法的时候就已经传入了一个主线程的执行器,所以回到我们存储内存缓存即将回调的地方:

 for (final ResourceCallbackAndExecutor entry : copy) {
      entry.executor.execute(new CallResourceReady(entry.cb));
    }

public void run() {
      synchronized (cb.getLock()) {
        synchronized (EngineJob.this) {
          if (cbs.contains(cb)) {
            engineResource.acquire();
            callCallbackOnResourceReady(cb);
            removeCallback(cb);
          }
          decrementPendingCallbacks();
        }
      }
    }

  void callCallbackOnResourceReady(ResourceCallback cb) {
    try {
      cb.onResourceReady(engineResource, dataSource);
    } catch (Throwable t) {
      throw new CallbackException(t);
    }
  }

所以这里的executor,必定存在主线程的MAIN_THREAD_EXECUTOR,所以在这里实现了线程切换,将数据回调到了主线程,我们直接看CallResourceReady的run方法,engineResource.acquire()开启了引用计数,表示当前内存缓存已经引用了,继续调用callCallbackOnResourceReady方法,这里的cb.onResourceReady方法就回调到了singleRequest中,我们跟进去看:

public void onResourceReady(Resource<?> resource, DataSource dataSource) {
    stateVerifier.throwIfRecycled();
    Resource<?> toRelease = null;
    try {
      synchronized (requestLock) {
        loadStatus = null;
          ......
          ......
        if (!canSetResource()) {
          toRelease = resource;
          this.resource = null;
          status = Status.COMPLETE;
          return;
        }
        onResourceReady((Resource<R>) resource, (R) received, dataSource);
      }
    } finally {
      if (toRelease != null) {
        engine.release(toRelease);
      }
    }
  }

private void onResourceReady(Resource<R> resource, R result, DataSource dataSource) {
    boolean isFirstResource = isFirstReadyResource();
    status = Status.COMPLETE;
    this.resource = resource;
    ......
    isCallingCallbacks = true;
    try {
      boolean anyListenerHandledUpdatingTarget = false;
      if (requestListeners != null) {
        for (RequestListener<R> listener : requestListeners) {
          anyListenerHandledUpdatingTarget |=
              listener.onResourceReady(result, model, target, dataSource, isFirstResource);
        }
      }
      anyListenerHandledUpdatingTarget |=
          targetListener != null
              && targetListener.onResourceReady(result, model, target, dataSource, isFirstResource);
      if (!anyListenerHandledUpdatingTarget) {
        Transition<? super R> animation = animationFactory.build(dataSource, isFirstResource);
        target.onResourceReady(result, animation);
      }
    } finally {
      isCallingCallbacks = false;
    }
    notifyLoadSuccess();
  }

onResourceReady省略的地方做了一些判断和失败的回调,可以忽略,我们直接看下面的onResourceReady方法,requestListeners和targetListener是最初我们自己是否添加过加载监听,如果设置过监听,这里就会回调,我们没有设置直接看target.onResourceReady方法,还记得我们的imageview设置到哪里了么,就是target中,因此这里就将数据源回调到了image view中,因此这里调用imageviewtarget的onResourceReady方法:

public void onResourceReady(@NonNull Z resource, @Nullable Transition<? super Z> transition) {
    if (transition == null || !transition.transition(resource, this)) {
      setResourceInternal(resource);
    } else {
      maybeUpdateAnimatable(resource);
    }
  }
 private void setResourceInternal(@Nullable Z resource) {
    // non-null Callback before starting it.
    setResource(resource);
    maybeUpdateAnimatable(resource);
  }

DrawableImageViewTarget
protected void setResource(@Nullable Drawable resource) {
    view.setImageDrawable(resource);
  }

这里的setResource方法也就调用了DrawableImageViewTarget的setResource方法,可以看到又调用了view的setImageDrawable方法,将图片设置到view上了,到这里从加载数据结束到存储到缓存并加载到view上的流程就分析结束了。

总结

HttpUrlFetcher 请求玩图片后返回给SourceGenerator图片的input stream,然后重新执行DecodeJob,调用SourceGenerator的startNext的cacheData方法去缓存数据到磁盘然后调用DataCacheGenerator的startNext方法又去磁盘缓存中加载数据,加载完成后再次执行DecodeJob方法,去decode解析磁盘缓存得到resource对象,完成后回调到EngineJob中,然后将resource存储到弱引用内存缓存中,引用计数加一,调用主线程的handler将数据回调到主线程的Target中加载并显示图片。执行流程就是SourceGenerator-->DataCacheGenerator-->DecodeJob-->EngineJob-->SingleRequest-->Target。到这里整个图片的加载流程就分析结束了,下一节我们再讲讲面试中Glide源码可能关注的一些问题~

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