【Android开发高级系列】内存管理专题

Android进程管理三部曲[3]-内存的回收

https://www.jianshu.com/p/c170f173de01


1 概述

        对于内存回收,主要可以分为两个层次:

    进程内的内存回收:通过释放进程中的资源进行内存回收;

    进程级的内存回收:通过杀死进程来进行内存回收;

        这其中,进程内的内存回收主要分为两个方面:

    虚拟机自身的垃圾回收机制;

    在系统内存状态发生变化时,通知应用程序,让开发者进行内存回收;

        而进程级的内存回收主要是依靠系统中的两个模块,它们是:

    Linux OOM Killer;

    LowMemoryKiller;

        在特定场景下,他们都会通过杀死进程来进行内存回收。

1.1 Android系统的内存管理简介

        在Android系统中,进程可以大致分为系统进程应用进程两大类。

        系统进程是系统内置的(例如:init,zygote,system_server进程),属于操作系统必不可少的一部分。系统进程的作用在于:

    管理硬件设备;

    提供访问设备的基本能力;

    管理应用进程;

        应用进程是指应用程序运行的进程。这些应用程序可能是系统出厂自带的(例如Launcher,电话,短信等应用),也可能是用户自己安装的(例如:微信,支付宝等)。

        Android中应用进程通常都运行在Java虚拟机中。在Android 5.0之前的版本,这个虚拟机是Dalvik,5.0及之后版本,Android引入了新的虚拟机,称作AndroidRuntime,简称“ART”。

        关于ART和Dalvik可以参见这里:ART and Dalvik。无论是Dalvik还是ART,本身都具有垃圾回收的能力,关于这一点,我们在后面专门讲解。

        Android的应用程序都会依赖一些公共的资源,例如:Android SDK提供的类和接口,以及Framework公开的图片,字符串等。为了达到节省内存的目的,这些资源在内存中并不是每个应用进程单独一份拷贝。而是会在所有应用之间共享,因为所有应用进程都是作为Zygote进程fork出来的子进程。关于这部分内容,我们已经在Android系统中的进程管理:进程的创建一文中讲解过。

        在Java语言中,通过new创建的对象都会在堆中分配内存。应用程序堆的大小是有限的。系统会根据设备的物理内存大小来确定每个应用程序所允许使用的内存大小,一旦应用程序使用的内存超过这个大小,便会发生OutOfMemoryError

        因此开发者需要关心应用的内存使用状况。关于如何监测应用程序的内存使用,可以参见这里:Investigating Your RAM Usage

2 开发者相关的API

        下面是一些与内存相关的开发者API,它们是AndroidSDK的一部分。

2.1 ComponentCallbacks2

        Android系统会根据当前的系统内存状态和应用的自身状态对应用进行通知。这种通知的目的是希望应用能够感知到系统和自身的状态变化,以便开发者可以更准确的把握应用的运行。

        例如:在系统内存充足时,为了提升响应性能,应用可以缓存更多的资源。但是当系统内存紧张时,开发者应当释放一定的资源来缓解内存紧张的状态。

        ComponentCallbacks2接口中的void onTrimMemory(int level)回调函数用来接收这个通知。关于这一点,在“开发者的内存回收”一节,我们会详细讲解。

2.2 ActivityManager

        ActivityManager,从名称中就可以看出,这个类是用来管理Activity的系统服务。但这个类中也包含了很多运行时状态查询的接口,这其中就包括与内存相关的几个:

    1、int getMemoryClass ()获取当前设备上,单个应用的内存大小限制,单位是M。注意,这个函数的返回值只是一个大致的值。

    2、void getMemoryInfo (ActivityManager.MemoryInfo outInfo)获取系统的内存信息,具体结构可以查看ActivityManager.MemoryInfo,开发者最关心的可能就是availMem以及totalMem。

    3、void getMyMemoryState (ActivityManager.RunningAppProcessInfo outState)获取调用进程的内存信息;

    4、MemoryInfo[] getProcessMemoryInfo (int[] pids)通过pid获取指定进程的内存信息;

    5、boolean isLowRamDevice()查询当前设备是否是低内存设备;

2.3 Runtime

        Java应用程序都会有一个Runtime接口的实例,通过这个实例可以查询运行时的一些状态,与内存相关的接口有:

    freeMemory()获取Java虚拟机的剩余内存;

    maxMemory()获取Java虚拟机所能使用的最大内存;

    totalMemory()获取Java虚拟机拥有的最大内存;

3 虚拟机的垃圾回收

        垃圾回收是指:虚拟机会监测应用程序的对象创建和使用,并在一些特定的时候销毁无用的对象以回收内存。

        垃圾回收的基本想法是要找出虚拟机中哪些对象已经不会再被使用然后将其释放。其最常用的算法有下面两种:

3.1 引用计数算法

        引用计数算法是为每个对象维护一个被引用的次数:对象刚创建时的初始引用计数为0,每次被一个对象引用时,引用计数加1,反之减1。当一个对象的引用计数重新回到0时便可以认为是不会被使用的,这些对象便可以被垃圾回收。

        读者可能马上会想到,当有两个对象互相引用时,这时引用计数该如何计算。关于这部分内容,这里不再展开讲解。有兴趣的读者可以查询Google或者维基百科:Garbage collection

3.2 对象追踪算法

        对象追踪算法是通过GC root类型的对象为起点,追踪所有被这些对象所引用的对象,并顺着这些被引用的对象继续往下追踪,在追踪的过程中,对所有被追踪到的对象打上标记。而剩下的那些没有被打过标记的对象便可以认为是没有被使用的,因此这些对象可以将其释放。

        这里提到的的GC root类型的对象有四类:

    1、栈中的local变量,即方法中的局部变量

    2、活动的线程(例如主线程或者开发者创建的线程)

    3、static变量

    4、JNI中的引用

        下面这幅图描述了这种算法:

        a)表示算法开始时,所有对象的标记为false,然后以GC root为起点开始追踪和打标记,b)中被追踪到的对象打上了标记。剩下的没有打上标记的对象便可以释放了。算法结束之后,c)中将所有对象的标记全部置为false。下一轮计算时,重新以GC root开始追踪。

        Dalvik虚拟机主要用的就是对象追踪算法,这里是其Source:MarkSweep.cpp

4 开发者的内存回收处理

        内存回收并不是仅仅是系统的事情,作为开发者,也需要在合适的场合下进行内存释放。无节制的消耗内存将导致应用程序OutOfMemoryError

        上文中提到,虚拟机的垃圾回收会回收那些不会再被使用到的对象。因此,开发者所需要做的就是:当确定某些对象不会再被使用时,要主动释放对其引用,这样虚拟机才能将其回收。对于不再被用到对象,仍然保持对其引用导致其无法释放,将导致内存泄漏的发生。

        为了更好的进行内存回收,系统会一些场景下会通知应用,希望应用能够配合进行一些内存的释放。ComponentCallbacks2接口中的 void onTrimMemory(intlevel)回调就是用来接收这个事件的。Activity, Service, ContentProvider和Application都实现了这个接口,因此这些类的子类都可以接收这个事件。onTrimMemory回调的参数是一个级别,系统会根据应用本身的状态以及系统的内存状态发送不同的级别,具体的包括:

    • 应用处于Running状态可能收到的级别

        ◦ TRIM_MEMORY_RUNNING_MODERATE表示系统内存已经稍低;

        ◦ TRIM_MEMORY_RUNNING_LOW表示系统内存已经相当低;

        ◦ TRIM_MEMORY_RUNNING_CRITICAL表示系统内存已经非常低,你的应用程序应当考虑释放部分资源;

    • 应用的可见性发生变化时收到的级别

        ◦ TRIM_MEMORY_UI_HIDDEN表示应用已经处于不可见状态,可以考虑释放一些与显示相关的资源;

    • 应用处于后台时可能收到的级别

        ◦ TRIM_MEMORY_BACKGROUND表示系统内存稍低,你的应用被杀的可能性不大。但可以考虑适当释放资源;

        ◦ TRIM_MEMORY_MODERATE表示系统内存已经较低,当内存持续减少,你的应用可能会被杀死;

        ◦ TRIM_MEMORY_COMPLETE表示系统内存已经非常低,你的应用即将被杀死,请释放所有可能释放的资源;

        这里是这个方法实现的示例代码:Release memory in response to events

        在前面的文章中我们提到过:ActivityManagerService负责管理所有的应用进程。而这里的通知也是来自ActivityManagerService。在updateOomAdjLocked的时候,ActivityManagerService会根据系统内存以及应用的状态通过app.thread.scheduleTrimMemory发送通知给应用程序。这里的app是ProcessRecord,即描述应用进程的对象,thread是应用的主线程。而scheduleTrimMemory是通过Binder IPC的方式将消息发送到应用进程上。这些内容在前面的文章中已经介绍过,如果觉得陌生,可以阅读一下前面两篇文章。

        在ActivityThread中(这个是应用程序的主线程),接受到这个通知之后,便会遍历应用进程中所有能接受这个通知的组件,然后逐个回调通知。

相关代码如下:

final void handleTrimMemory(int level) {

    if (DEBUG_MEMORY_TRIM)

        Slog.v(TAG, "Trimming memory to level:" + level);

   ArrayList callbacks = collectComponentCallbacks(true, null);


   final int N = callbacks.size();

   for (int i = 0; i < N; i++) {

       callbacks.get(i).onTrimMemory(level);

   }


   WindowManagerGlobal.getInstance().trimMemory(level);

}

4.1 Linux OOM Killer

        前面提到的机制都是在进程内部通过释放对象来进行内存回收。而实际上,系统中运行的进程数量,以及每个进程所消耗的内存都是不确定的。在极端的情况下,系统的内存可能处于非常严峻的状态,假设这个时候所有进程都不愿意释放内存,系统将会卡死。为了使系统能够继续运转不至于卡死,系统会尝试杀死一些不重要的进程来进行内存回收,这其中涉及的模块主要是:Linux OOM Killer和LowMemoryKiller。

        Linux OOM Killer是Linux内核的一部分,其源码可以在这里查看:/mm/oom_kill.c

        Linux OOM Killer的基本想法是:

        当系统已经没法再分配内存的时候,内核会遍历所有的进程,对每个进程计算badness值,得分(badness)最高的进程将会被杀死。即:badness得分越低表示进程越重要,反之表示不重要。

        Linux OOM Killer的执行流程如下:

_alloc_pages -> out_of_memory() -> select_bad_process() -> oom_badness()

        这其中,_alloc_pages是内核在分配内存时调用的函数。当内核发现无法再分配内存时,便会计算每个进程的badness值,然后选择最大的(系统认为最不重要的)将其杀死。

        那么,内核是如何计算进程的badness值的呢?请看下面的代码:

unsigned long oom_badness(struct task_struct *p, struct mem_cgroup *memcg, const nodemask_t *nodemask, unsigned long totalpages)

{

    long points;

    long adj;

    ...


    points = get_mm_rss(p->mm) + p->mm->nr_ptes + get_mm_counter(p->mm, MM_SWAPENTS);

    task_unlock(p);


    if (has_capability_noaudit(p, CAP_SYS_ADMIN))

        points -= (points * 3) / 100;


    adj *= totalpages / 1000;

    points += adj;

    return points > 0 ? points : 1;

}

        从这段代码中,我们可以看到,影响进程badness值的因素主要有三个:

    • 进程的oom_score_adj值;

    • 进程的内存占用大小;

    • 进程是否是root用户的进程;

        即,oom_score_adj(关于oom_score_adj,在Android系统中的进程管理:进程的优先级一文中我们专门讲解过。)值越小,进程占用的内存越小,并且如果是root用户的进程,系统就认为这个进程越重要。反之则被认为越不重要,越容易被杀死。

4.2 LowMemoryKiller

        OOM Killer是在系统内存使用情况非常严峻的时候才会起作用。但直到这个时候才开始杀死进程来回收内存是有点晚的。因为在进程被杀死之前,其他进程都无法再申请内存了。因此,Google在Android上新增了一个LowMemoryKiller模块。LowMemoryKiller通常会在Linux OOMKiller工作之前,就开始杀死进程。

          LowMemoryKiller的做法是:

        提供6个可以设置的内存级别,当系统内存每低于一个级别时,将oom_score_adj大于某个指定值的进程全部杀死。这么说会有些抽象,但具体看一下LowMemoryKiller的配置文件我们就好理解了。

        LowMemoryKiller在sysfs上暴露了两个文件来供系统调整参数,这两个文件的路径是:

    /sys/module/lowmemorykiller/parameters/minfree

    /sys/module/lowmemorykiller/parameters/adj

        如果你手上有一个Android设备,你可以通过adb shell连上去之后,通过cat命令查看这两个文件的内容。这两个文件是配对使用的,每个文件中都是由逗号分隔的6个整数值。

        在某个设备上,这两个文件的值可能分别是下面这样:

18432, 23040, 27648, 32256, 55296, 80640

0, 100, 200, 300, 900, 906

        这组配置的含义是;当系统内存低于80640k时,将oom_score_adj值大于906的进程全部杀死;当系统内存低于55296k时,将oom_score_adj值大于900的进程全部杀死,其他类推。

        LowMemoryKiller杀死进程的时候会在内核留下日志,你可以通过dmesg命令中看到。这个日志可能是这样的:

lowmemorykiller: Killing 'gnunet-service-' (service adj 0, to free 327224kB on behalf of 'kswapd0' (21) because cache 6064kB is below limit 6144kB foroom_score_adj 0

        从这个日志中,我们可以看到被杀死进程的名称,进程pid和oom_score_adj值。另外还有系统在杀死这个进程之前系统内存还剩多少,以及杀死这个进程释放了多少。

        LowMemoryKiller的源码也在内核中,路径是:kernel/drivers/staging/android/lowmemorykiller.c。

        lowmemorykiller.c中定义了如下几个函数:

    • lowmem_shrink

    • lowmem_init

    • lowmem_exit

    • lowmem_oom_adj_to_oom_score_adj

    • lowmem_autodetect_oom_adj_values

    • lowmem_adj_array_set

    • lowmem_adj_array_get

    • lowmem_adj_array_free

        LowMemoryKiller本身是一个内核驱动程序的形式存在,lowmem_init和lowmem_exit

分别负责模块的初始化和退出清理工作。

        在lowmem_init函数中,就是通过register_shrinker向内核中注册了register_shrinker函数:

static int lowmem_init(void)

{

    register_shrinker(&lowmem_shrinker);

    return 0;

}

        register_shrinker函数就是LowMemoryKiller的算法核心,这个函数的代码和说明如下:

static int lowmem_shrink(structshrinker *s, struct shrink_control *sc){

    struct task_struct *tsk;

    struct task_struct *selected= NULL;

    int rem = 0;

    int tasksize;

    int i;

    short min_score_adj = OOM_SCORE_ADJ_MAX+ 1;

    int minfree = 0;

    int selected_tasksize = 0;

    short selected_oom_score_adj;

    int array_size = ARRAY_SIZE(lowmem_adj);

    int other_free =global_page_state(NR_FREE_PAGES) - totalreserve_pages;

    int other_file =global_page_state(NR_FILE_PAGES) - global_page_state(NR_SHMEM) -total_swapcache_pages();

    if(lowmem_adj_size < array_size)

       array_size= lowmem_adj_size;


    if(lowmem_minfree_size < array_size)

        array_size= lowmem_minfree_size;

    // lowmem_minfree 和lowmem_adj记录了两个配置文件中配置的数据


    for(i = 0; i < array_size; i++) {

        minfree = lowmem_minfree[i];//确定当前系统处于低内存的第几档

        if(other_free< minfree && other_file < minfree) {

            //确定需要杀死的进程的oom_score_adj的上限

            min_score_adj= lowmem_adj[i];

            break;

        }

    }


    if(sc->nr_to_scan > 0)

        lowmem_print(3, "lowmem_shrink %lu,%x, ofree %d %d, ma %hd\n", sc->nr_to_scan, sc->gfp_mask,other_free, other_file, min_score_adj);


    rem = global_page_state(NR_ACTIVE_ANON) +global_page_state(NR_ACTIVE_FILE) + global_page_state(NR_INACTIVE_ANON) +global_page_state(NR_INACTIVE_FILE);


    if(sc->nr_to_scan <= 0||min_score_adj == OOM_SCORE_ADJ_MAX + 1) {

        lowmem_print(5, "lowmem_shrink %lu,%x, return %d\n", sc->nr_to_scan, sc->gfp_mask, rem);

        return rem;

    }

    selected_oom_score_adj = min_score_adj;

    rcu_read_lock();      // 遍历所有进程


    for_each_process(tsk) {

        struct task_struct *p;

        short oom_score_adj;

        if(tsk->flags & PF_KTHREAD)

            continue;

        p = find_lock_task_mm(tsk);


        if(!p) continue;

        if(test_tsk_thread_flag(p, TIF_MEMDIE)&& time_before_eq(jiffies, lowmem_deathpending_timeout)) {

            task_unlock(p);

            rcu_read_unlock();

            return0;

        }

        oom_score_adj = p->signal->oom_score_adj;// 跳过那些oom_score_adj值比目标值小的


        if(oom_score_adj < min_score_adj) {

            task_unlock(p);continue;

        }

        tasksize = get_mm_rss(p->mm);

        task_unlock(p);

        if(tasksize <= 0) continue;


        // selected 是将要杀死的备选进程

        if(selected) {

            // 跳过那些oom_score_adj比备选的小的

            if(oom_score_adj < selected_oom_score_adj)

                continue;//如果oom_score_adj一样,跳过那些内存消耗更小的


            if(oom_score_adj == selected_oom_score_adj&& tasksize <= selected_tasksize)

                continue;

        }


        // 更换备选的目标,因为又发现了一个oom_score_adj更大,

        // 或者内存消耗更大的进程

        selected = p;

        selected_tasksize = tasksize;

        selected_oom_score_adj = oom_score_adj;

        lowmem_print(2, "select '%s' (%d),adj %hd, size %d, to kill\n", p->comm, p->pid, oom_score_adj, tasksize);

    }


    // 已经选中目标,记录日志并杀死进程

    if(selected) {

        long cache_size = other_file * (long)(PAGE_SIZE / 1024);

        long cache_limit = minfree * (long)(PAGE_SIZE / 1024);

        long free = other_free * (long)(PAGE_SIZE / 1024);

        trace_lowmemory_kill(selected, cache_size, cache_limit, free);

        lowmem_print(1, "Killing'%s' (%d), adj %hd,\n"\" to free %ldkB on behalf of '%s' (%d)because\n"\" cache %ldkB is below limit %ldkB for oom_score_adj%hd\n"\" Free memory is %ldkB above reserved\n", selected->comm, selected->pid, selected_oom_score_adj, selected_tasksize* (long)(PAGE_SIZE / 1024), current->comm, current->pid, cache_size,cache_limit, min_score_adj, free);

        lowmem_deathpending_timeout = jiffies + HZ;

        set_tsk_thread_flag(selected,TIF_MEMDIE);

        send_sig(SIGKILL,selected, 0);

        rem -= selected_tasksize;

    }


    lowmem_print(4, "lowmem_shrink %lu,%x, return %d\n", sc->nr_to_scan, sc->gfp_mask, rem);

    rcu_read_unlock();

    return rem;

}


4.3 进程的死亡处理

        在任何时候,应用进程都可能死亡,例如被OOM Killer或者LowMemoryKiller杀死,自身crash死亡又或者被用户手动杀死。无论哪种情况,作为应用进程的管理者ActivityManagerService都需要知道。在应用进程死亡之后,ActivityManagerService需要执行如下工作:

        • 执行清理工作 ActivityManagerService内部的ProcessRecord以及可能存在的四大组件的相关结构需要全部清理干净

        • 重新程的级 上文已经提到过,进程的优先级是有关联性的,有其中一个进程死亡了,可能会连到影响到其他进程的优先级需要调整。

        ActivityManagerService是利用Binder提供的死亡通知机制来进行进程的死亡处理的。关于Binder请参阅其他资料,限于篇幅关系,这里不再展开讲解。

        简单来说,死亡通知机制就提供了进程间的一种死亡监听的能力:当目标进程死亡的时候,监听回调会执行。

        ActivityManagerService中的AppDeathRecipient监听了应用进程的死亡消息,该类代码如下:

private final class AppDeathRecipient implements IBinder.DeathRecipient {

   final ProcessRecord mApp;

   final int mPid;

   final ApplicationThread mAppThread;


   AppDeathRecipient(ProcessRecord app, int pid, IApplicationThread thread) {

       mApp = app;

       mPid = pid;

       mAppThread = thread;

   }


   @Override

   public void binderDied() {

       synchronized(ActivityManagerService.this) {

           appDiedLocked(mApp, mPid, mAppThread, true);

       }

   }

}

        每一个应用进程在启动之后,都会attach到ActivityManagerService上通知它自己的进程已经启动完成了。这时ActivityManagerService便会为其创建一个死亡通知的监听器。在这之后如果进程死亡了,ActivityManagerService便会收到通知。

private final boolean attachApplicationLocked(IApplicationThread thread, int pid) {

    ...

    try{

        AppDeathRecipient adr = newAppDeathRecipient(app, pid, thread);

        thread.asBinder().linkToDeath(adr,0);

        app.deathRecipient = adr;

    } catch(RemoteException e) {

        app.resetPackageList(mProcessStats);

        startProcessLocked(app,"link fail", processName);

        return false;

    }

    ...

}

        进程死亡之后的处理工作是appDiedLocked这个方法中处理的,这部分还是比较容易理解的,这里就不过多讲解了。

5 参考链接

Android进程管理三部曲[3]-内存的回收

https://www.jianshu.com/p/c170f173de01


Overview of Android Memory Management

Understanding Java Garbage Collection

Processes and Threads

Java Memory Management

Debugging ART Garbage Collection

Android Runtime

Taming the OOM killer

OOM Killer

Out Of Memory Management

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

推荐阅读更多精彩内容