leakcanary源码学习随笔

leakcanary是什么?

leakcanary是Square开源的一个内存泄露自动探测神器,它是一个Android和Java的内存泄露检测库,可以大幅度减少了开发中遇到的OOM问题。

下图为官方应用图标:

image

简介

Github地址:leakcanary

特点

  • 使用简单
  • 实时检测
  • 及时的消息提醒及log日志

内存泄漏

本文主要介绍leakcanary的实现及原理。关于内存泄漏的定义及Android常见的内存泄漏及解决方法,推荐参考如下文章:

Android性能优化-内存泄漏(上)

Android性能优化-内存泄漏(下)

leakcanary怎么用?

1、gradle引入库

 debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.1'
 releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.1'
 // Optional, if you use support library fragments:
 debugImplementation 'com.squareup.leakcanary:leakcanary-support-fragment:1.6.1'

2、自定义Application并且在onCreate中进行初始化

public class ExampleApplication extends Application {

  @Override public void onCreate() {
    super.onCreate();
    if (LeakCanary.isInAnalyzerProcess(this)) {
      // This process is dedicated to LeakCanary for heap analysis.
      // You should not init your app in this process.
      return;
    }
    LeakCanary.install(this);
    // Normal app init code...
  }
}

leakcanary核心执行流程是怎样?

leakcanary主要是通过弱引用持有监听的实例对象,每个弱引用生成唯一的id,内部维护一个retainedKeys。通过触发gc,循环获取ReferenceQueue中的对象,若已被gc回收,则把id从keys中移除,最后keys维护了一个内存泄漏的实例集合。接着通过dump堆内存信息,用Square的haha库对hprof文件进行解析,最终获取内存泄漏实例的引用链,推送到前台通知。

关键类功能说明

说明
LeakCanary 外观模式,统一的注册类,install会返回RefWatcher对象,用于监听内存泄漏
RefWatcher 用于整体调度监听对象的内存泄漏,是核心的类。内部维护了watchExecutor(异步分析线程池)、debuggerControl(用于判断是否debug模式)、gcTrigger(触发gc)、heapDumper(实现为AndroidHeapDumper,用于dump内存堆快照信息)、retainedKeys(用于记录内存泄漏的对象)、queue(弱引用被gc回收会加入到该队列);提供watch()方法对对象进行内存泄漏的判断分析。
ActivityRefWatcher 用于监听Activity的生命周期、执行watch()
FragmentRefWatcher 用于监听Fragment的生命周期、执行watch()
KeyedWeakReference 将检测的对象转换为虚引用持有,便于监听GC的回收,内部维护了一个key属性,用作唯一的标识(UUID.randomUUID()生成)
DebuggerControl 实现为AndroidDebuggerControl,用于判断是否属于调试模式(Debug.isDebuggerConnected())
GcTrigger 用于触发GC操作,(Runtime.getRuntime().gc())
HeapDump 堆内存信息的包装类,包含heapDumpFile、excludedRefs、referenceKey等对象
ExcludedRefs 用于排除一些已知的系统内存泄漏问题,避免再次提醒
HeapAnalyzerService IntentService,用于开始执行内存泄漏的堆信息分析
HeapAnalyzer 堆内存分析器,用于堆信息分析的整体调度
HprofParser haha库中用于解析hprof文件
Snapshot haha库中,堆信息内存快照
AnalysisResult 分析结果包装类,包含className、leakTrace(泄漏的引用链)、retainedHeapSize(内存泄漏的大小)等

代码执行流程

leakcanary的核心流程主要包含5个步骤。

1、install-注册

2、bind-设置监听

3、watch-触发监听,分析泄漏

4、dump-堆内存信息

5、analyze-分析,获取引用链

这里先上一下整体的流程图,建议结合源码进行查看。

image

下面我们通过上诉5个步骤相关的源码来进行分析。

install

我们知道,使用leakcanary主要在application中进行注册,调用LeakCanary.install(this)。相关的实现代码如下:

 public static RefWatcher install(Application application) {
    //创建了RefWatcher(也就是检测内存泄漏的类)
    return refWatcher(application)
        //设置了内存泄漏的通知Service(通知)
        .listenerServiceClass(DisplayLeakService.class)
        //设置了需要忽略的已知的系统级别的内存泄漏(可以自定义)
        .excludedRefs(AndroidExcludedRefs.createAppDefaults().build())
        //开始监听
        .buildAndInstall();
  }

bind

leakcanary主要是监听应用的Activity、Fragment的内存泄漏。那么bind的这个环节,主要是通过监听Activity、Fragment相关的生命周期,当对象onDestry的时候,开始执行watch操作。
首先我们先看buildAndInstall()方法,实现如下:

1、buildAndInstall

public RefWatcher buildAndInstall() {
    if (LeakCanaryInternals.installedRefWatcher != null) {
      throw new UnsupportedOperationException("buildAndInstall() should only be called once.");
    }
    //初始化RefWatcher对象
    RefWatcher refWatcher = build();
    if (refWatcher != DISABLED) {
      if (watchActivities) {//默认true
        //开始绑定Activity的监听
        ActivityRefWatcher.install(context, refWatcher);
      }
      if (watchFragments) {//默认true
         //开始绑定Fragment的监听
        FragmentRefWatcher.Helper.install(context, refWatcher);
      }
    }
    LeakCanaryInternals.installedRefWatcher = refWatcher;
    return refWatcher;
  }

2、ActivityRefWatcher.install(context, refWatcher)

  public static void install(Context context, RefWatcher refWatcher) {
    Application application = (Application) context.getApplicationContext();
    ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);
    //通过Application的ActivityLifecycleCallbacks监听Activity的生命周期
    application.registerActivityLifecycleCallbacks(activityRefWatcher.lifecycleCallbacks);
  }

  private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
      new ActivityLifecycleCallbacksAdapter() {
        @Override public void onActivityDestroyed(Activity activity) {
          //当Activity销毁的时候,开始触发watch进行分析
          refWatcher.watch(activity);
        }
      };

3、 FragmentRefWatcher.Helper.install(context, refWatcher)

 public static void install(Context context, RefWatcher refWatcher) {
        //初始化FragmentRefWatcher对象及集合
      List<FragmentRefWatcher> fragmentRefWatchers = new ArrayList<>();
        
      if (SDK_INT >= O) {
        fragmentRefWatchers.add(new AndroidOFragmentRefWatcher(refWatcher));
      }

      try {
        Class<?> fragmentRefWatcherClass = Class.forName(SUPPORT_FRAGMENT_REF_WATCHER_CLASS_NAME);
        Constructor<?> constructor =
            fragmentRefWatcherClass.getDeclaredConstructor(RefWatcher.class);
        FragmentRefWatcher supportFragmentRefWatcher =
            (FragmentRefWatcher) constructor.newInstance(refWatcher);
        fragmentRefWatchers.add(supportFragmentRefWatcher);
      } catch (Exception ignored) {
      }

      if (fragmentRefWatchers.size() == 0) {
        return;
      }

      Helper helper = new Helper(fragmentRefWatchers);
     //首先开始监听Activity的生命周期
      Application application = (Application) context.getApplicationContext();
      application.registerActivityLifecycleCallbacks(helper.activityLifecycleCallbacks);
    }

    private final Application.ActivityLifecycleCallbacks activityLifecycleCallbacks =
        new ActivityLifecycleCallbacksAdapter() {
          @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
           //当Activity创建的时候,进行Fragment的绑定监听
            for (FragmentRefWatcher watcher : fragmentRefWatchers) {
              watcher.watchFragments(activity);
            }
          }
        };
        
  @Override public void watchFragments(Activity activity) {
    if (activity instanceof FragmentActivity) {
      FragmentManager supportFragmentManager =
          ((FragmentActivity) activity).getSupportFragmentManager();
     //通过FragmentManager注册FragmentLifecycleCallbacks,监听Fragment的生命周期
      supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true);
    }
  } 
  
 private final FragmentManager.FragmentLifecycleCallbacks fragmentLifecycleCallbacks =
      new FragmentManager.FragmentLifecycleCallbacks() {
        @Override public void onFragmentDestroyed(FragmentManager fm, Fragment fragment) {
        //当Fragment销毁的时候,开始触发watch进行分析
          refWatcher.watch(fragment);
        }
      };

 

watch

watch主要是通过弱引用持有监听的实例对象,每个弱引用生成唯一的id,内部维护一个retainedKeys。通过触发gc,循环获取ReferenceQueue中的对象,若已被gc回收,则把id从keys中移除,最后keys维护了一个内存泄漏的实例集合。触发下一步dump操作。关键的实现代码如下:

1、watch

  public void watch(Object watchedReference, String referenceName) {
    if (this == DISABLED) {
      return;
    }
    checkNotNull(watchedReference, "watchedReference");
    checkNotNull(referenceName, "referenceName");
    final long watchStartNanoTime = System.nanoTime();
    //生成唯一的key
    String key = UUID.randomUUID().toString();
    retainedKeys.add(key);
    //生成虚引用
    final KeyedWeakReference reference =
        new KeyedWeakReference(watchedReference, key, referenceName, queue);
    //开始触发线程进行分析
    ensureGoneAsync(watchStartNanoTime, reference);
  }

2、ensureGone

 private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
    watchExecutor.execute(new Retryable() {
      @Override public Retryable.Result run() {
        return ensureGone(reference, watchStartNanoTime);
      }
    });
  }
  
Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
    long gcStartNanoTime = System.nanoTime();
    long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
    //刷新keys
    removeWeaklyReachableReferences();
    //判断是否在debug模式
    if (debuggerControl.isDebuggerAttached()) {
      // The debugger can create false leaks.
      return RETRY;
    }
    //如果已回收不用处理
    if (gone(reference)) {
      return DONE;
    }
    //没回收,触发gc,提高准确性
    gcTrigger.runGc();
    //再次刷新keys
    removeWeaklyReachableReferences();
    //如果keys中还是存在该虚引用,则判定为内存泄漏
    if (!gone(reference)) {
      long startDumpHeap = System.nanoTime();
      long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
      File heapDumpFile = heapDumper.dumpHeap();
      if (heapDumpFile == RETRY_LATER) {
        // Could not dump the heap.
        return RETRY;
      }
      long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);

      HeapDump heapDump = heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key)
          .referenceName(reference.name)
          .watchDurationMs(watchDurationMs)
          .gcDurationMs(gcDurationMs)
          .heapDumpDurationMs(heapDumpDurationMs)
          .build();

      heapdumpListener.analyze(heapDump);
    }
    return DONE;
  }

 //判断对象是否已被gc回收
 private boolean gone(KeyedWeakReference reference) {
    return !retainedKeys.contains(reference.key);
  }
 //循环获取ReferenceQueue中的对象,若被gc回收,从keys中移除
 private void removeWeaklyReachableReferences() {
    // WeakReferences are enqueued as soon as the object to which they point to becomes weakly
    // reachable. This is before finalization or garbage collection has actually happened.
    KeyedWeakReference ref;
    while ((ref = (KeyedWeakReference) queue.poll()) != null) {
      retainedKeys.remove(ref.key);
    }
  }

dump

在watch中的ensureGone方法中,当监听到内存泄漏的方法时,会开始执行dump,下载堆内存快照文件。具体的实现如下:

    //如果keys中还是存在该虚引用,则判定为内存泄漏
    if (!gone(reference)) {
      long startDumpHeap = System.nanoTime();
      long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
      //下载对内存文件
      File heapDumpFile = heapDumper.dumpHeap();
      if (heapDumpFile == RETRY_LATER) {
        // Could not dump the heap.
        return RETRY;
      }
      long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
     //构建堆内存包装类
      HeapDump heapDump = heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key)
          .referenceName(reference.name)
          .watchDurationMs(watchDurationMs)
          .gcDurationMs(gcDurationMs)
          .heapDumpDurationMs(heapDumpDurationMs)
          .build();

      heapdumpListener.analyze(heapDump);
    }

关键的heapDumper.dumpHeap()实现如下:

public File dumpHeap() {
    File heapDumpFile = leakDirectoryProvider.newHeapDumpFile();

    if (heapDumpFile == RETRY_LATER) {
      return RETRY_LATER;
    }

    FutureResult<Toast> waitingForToast = new FutureResult<>();
    //显示冻结弹层
    showToast(waitingForToast);

    if (!waitingForToast.wait(5, SECONDS)) {
      CanaryLog.d("Did not dump heap, too much time waiting for Toast.");
      return RETRY_LATER;
    }

    Notification.Builder builder = new Notification.Builder(context)
        .setContentTitle(context.getString(R.string.leak_canary_notification_dumping));
    Notification notification = LeakCanaryInternals.buildNotification(context, builder);
    NotificationManager notificationManager =
        (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
    int notificationId = (int) SystemClock.uptimeMillis();
    notificationManager.notify(notificationId, notification);

    Toast toast = waitingForToast.get();
    try {
        //关键的实现,dump堆内存信息
      Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
      //隐藏冻结弹层
      cancelToast(toast);
      notificationManager.cancel(notificationId);
      return heapDumpFile;
    } catch (Exception e) {
      CanaryLog.d(e, "Could not dump heap");
      // Abort heap dump
      return RETRY_LATER;
    }
  }

analyze

当dump堆内存信息成功后,会执行如下方法heapdumpListener.analyze(heapDump),开始执行分析,具体的实现如下:

  @Override public void analyze(HeapDump heapDump) {
    checkNotNull(heapDump, "heapDump");
    HeapAnalyzerService.runAnalysis(context, heapDump, listenerServiceClass);
  }
  
    public static void runAnalysis(Context context, HeapDump heapDump,
      Class<? extends AbstractAnalysisResultService> listenerServiceClass) {
    setEnabledBlocking(context, HeapAnalyzerService.class, true);
    setEnabledBlocking(context, listenerServiceClass, true);
    Intent intent = new Intent(context, HeapAnalyzerService.class);
    intent.putExtra(LISTENER_CLASS_EXTRA, listenerServiceClass.getName());
    intent.putExtra(HEAPDUMP_EXTRA, heapDump);
    ContextCompat.startForegroundService(context, intent);
  }

通过源码可知,会开启一个IntentService进行分析的操作。接下来看,HeapAnalyzerService的onHandleIntentInForeground实现,如下:

  @Override protected void onHandleIntentInForeground(@Nullable Intent intent) {
    if (intent == null) {
      CanaryLog.d("HeapAnalyzerService received a null intent, ignoring.");
      return;
    }
    String listenerClassName = intent.getStringExtra(LISTENER_CLASS_EXTRA);
    HeapDump heapDump = (HeapDump) intent.getSerializableExtra(HEAPDUMP_EXTRA);
    //构建了堆解析器
    HeapAnalyzer heapAnalyzer =
        new HeapAnalyzer(heapDump.excludedRefs, this, heapDump.reachabilityInspectorClasses);
   //开始执行分析
    AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey,
        heapDump.computeRetainedHeapSize);
    AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);
  }

接下来是使用了Square的开源库haha去分析hprof文件,获取泄漏对象的引用链,相关的实现如下:

 public AnalysisResult checkForLeak(File heapDumpFile, String referenceKey,
      boolean computeRetainedSize) {
    long analysisStartNanoTime = System.nanoTime();

    if (!heapDumpFile.exists()) {
      Exception exception = new IllegalArgumentException("File does not exist: " + heapDumpFile);
      return failure(exception, since(analysisStartNanoTime));
    }

    try {
      listener.onProgressUpdate(READING_HEAP_DUMP_FILE);
      HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
      HprofParser parser = new HprofParser(buffer);
      listener.onProgressUpdate(PARSING_HEAP_DUMP);
      Snapshot snapshot = parser.parse();
      listener.onProgressUpdate(DEDUPLICATING_GC_ROOTS);
      deduplicateGcRoots(snapshot);
      listener.onProgressUpdate(FINDING_LEAKING_REF);
      Instance leakingRef = findLeakingReference(referenceKey, snapshot);

      // False alarm, weak reference was cleared in between key check and heap dump.
      if (leakingRef == null) {
        return noLeak(since(analysisStartNanoTime));
      }
      return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, computeRetainedSize);
    } catch (Throwable e) {
      return failure(e, since(analysisStartNanoTime));
    }
  }
 private AnalysisResult findLeakTrace(long analysisStartNanoTime, Snapshot snapshot,
      Instance leakingRef, boolean computeRetainedSize) {

    listener.onProgressUpdate(FINDING_SHORTEST_PATH);
    ShortestPathFinder pathFinder = new ShortestPathFinder(excludedRefs);
    ShortestPathFinder.Result result = pathFinder.findPath(snapshot, leakingRef);

    // False alarm, no strong reference path to GC Roots.
    if (result.leakingNode == null) {
      return noLeak(since(analysisStartNanoTime));
    }

    listener.onProgressUpdate(BUILDING_LEAK_TRACE);
    LeakTrace leakTrace = buildLeakTrace(result.leakingNode);

    String className = leakingRef.getClassObj().getClassName();

    long retainedSize;
    if (computeRetainedSize) {

      listener.onProgressUpdate(COMPUTING_DOMINATORS);
      // Side effect: computes retained size.
      snapshot.computeDominators();

      Instance leakingInstance = result.leakingNode.instance;

      retainedSize = leakingInstance.getTotalRetainedSize();

      // TODO: check O sources and see what happened to android.graphics.Bitmap.mBuffer
      if (SDK_INT <= N_MR1) {
        listener.onProgressUpdate(COMPUTING_BITMAP_SIZE);
        retainedSize += computeIgnoredBitmapRetainedSize(snapshot, leakingInstance);
      }
    } else {
      retainedSize = AnalysisResult.RETAINED_HEAP_SKIPPED;
    }

    return leakDetected(result.excludingKnownLeaks, className, leakTrace, retainedSize,
        since(analysisStartNanoTime));
  }

关于haha库的具体实现,可参考如下链接。

haha github

最后在onHandleIntentInForeground的方法最后,会执行AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);将最终分析的结果推送到前台的通知。这就完成了leakcanary对于内存泄漏的关键步骤分析。

leakcanary是如何进行内存泄漏的监听?

leakcanary的内存泄漏监听包含2个方面,一个是对Activity的监听,一个是对Fragment的监听。

1、 Activity内存泄漏监听

通过上述的流程分析,我们可知。leakcanary通过对 application注册ActivityLifecycleCallbacks的监听,当回调onDestroy的时候,开始对Activity进行watch操作

2、Fragment内存泄漏监听

通过上述的流程分析,我们可知。leakcanary通过对 application注册ActivityLifecycleCallbacks的监听,当回调onActivityCreate的时候,通过FragmentManager注册FragmentLifecycleCallbacks的监听,当回调onDestroy的时候,开始对Fragment进行watch操作

leakcanary是如何进行内存泄漏的分析?

leakcanary主要是通过监听Activity及Fragment的生命周期,当对象onDestroy的时候,使用弱引用持有该实例对象,每个弱引用生成唯一的id,内部维护一个retainedKeys。通过触发gc,循环获取ReferenceQueue中的对象,若已被gc回收,则把id从keys中移除,表示已正常被gc回收,最后keys维护了一个内存泄漏的集合。主要是通过弱引用和ReferenceQueue机制来进行内存泄漏的分析。

leakcanary是如何dump内存信息?

通过上述的dump流程,leakcanary主要是通过Android系统提供的api进行堆内存信息的保存。具体的api如下:

  Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
  

leakcanary是如何进行堆信息的分析?

leakcanary主要是使用haha库对hprof的文件进行解析,最后分析出内存泄漏对象的引用链,及泄漏的内存大小。关于haha库的详细使用,可参考:

haha github

总结

思考

Square真是一家牛逼的公司,而且热衷于对开源社区的贡献。在Android的生态中,贡献了诸如okhttp、picasso、leakcanary、javapoet、retrofit等主流的框架。

Squaregithub

参考资料

Android性能优化-内存泄漏(上)

Android性能优化-内存泄漏(下)

LeakCanary源码解析-很值得我们学习的一款框架

关于

欢迎关注我的个人公众号

微信搜索:一码一浮生,或者搜索公众号ID:life2code

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

推荐阅读更多精彩内容