笔记-更深层次的了解iOS内存管理

01.jpg

在研究Hash表的过程中,想看iOS当中有哪些场景应用,最为大家所知的应该就是weak关键字的底层原理,利用网上的资料深究了一下,同时更进一步了解到了iOS内存管理方面的知识,所以希望自己能够保留这份记忆,就记录一下。

Hash

笔记-数据结构之 Hash(OC的粗略实现)

Hash或者说散列表,它是一种基础数据结构,这里为什么会说到它,因为我感觉理解了Hash对weak关键字底层的理解有很大的帮助。

Hash表是一种特殊的数据结构,它同数组、链表以及二叉树等相比有很明显的区别,但是它又是在数组和链表的基础上演化而来。

Hash表的本质是一个数组,数组中每一个元素称为一个箱子,箱子中存放元素。
存储过程如下:

  • 根据key计算出它的哈希值h。
  • 假设箱子的个数为n,那么这个键值对应该放在第(h % n)个箱子中。
  • 如果该箱子中已经有了键值对,就使用方法解决冲突(这里值说分离链接法解决冲突,还有一个方法是开放定址法)。

Hash表采用一个映射函数f:key->address将关键字映射到该记录在表中存储位置,从而想要查找该记录时,可以直接根据关键字和映射关系计算出该记录在表中的存储位置,通常情况下,这种映射关系称作Hash函数,而通过Hash函数和关键字计算出来的存储位置(这里的存储位置只是表中的存储位置,并不是实际的物理地址)称作Hash地址。

先看一个列子:
假如联系人信息采用Hash表存储,当想要找到“lisi”的信息时,直接根据“lisi”和Hash函数计算出Hash地址即可。
因为我们是用数组大小对哈希值进行取模,有可能不同的键值产生的索引值相同,这就是所谓的冲突。


image

显然这里“sizhang”元素和“zhangsi”元素产生了冲突,解决该冲突的方法就是改变数据结构,将数组内的元素改变为一个链表,这样就能容下足够多的元素。

在使用分离链接法解决哈希冲突时,每个箱子其实是一个链表,将属于同一个箱子里的元素存储在一张线性表中,而每张表的表头的序号即为计算得到的Hash地址,如下图最左边是数组结构,数组内的元素为链表结构。


image

这里的Hash表我们只做简单的了解,想要详细了解的请参考:
笔记-数据结构之 Hash(OC的粗略实现)
深入理解哈希表
哈希算法详解

内存管理的思考

ARC的核心思想:

  • 自己生成的对象,自己持有
  • 非自己生成的对象,自己也可以持有
  • 自己持有的对象不需要时,需要对其进行释放
  • 非自己持有的对象无法释放

其实不论ARC还是MRC都遵循该方式,只是在ARC模式下这些工作被编译器做了

引用计数

retain、release、etainCount

苹果的实现:(这部分内容是根据 《Objective-C高级编程 iOS与OS X多线程和内存管理》 来的)

- retainCount
__CFDoExternRefOperation
CFBasicHashGetCountOfKey
- retain
__CFDoExternRefOperation
CFBasicHashAddValue
- release
__CFDoExternRefOperation
CFBasicHashRemoveValue
(CFBasicHashRemoveValue返回0时,-release调用dealloc)

各个方法都通过同一个调用来__CFDoExternRefOperation函数,调用来一系列名称相似的函数。如这些函数名的前缀“CF”所示,它们包含于Core Foundation框架源代码中,即是CFRuntime.c__CFDoExternRefOperation函数。

__CFDoExternRefOperation函数按retainCount/retain/release操作进行分发,调用不同的函数,NSObject类的retainCount/retain/release实例方法也许如下面代码所示:

- (NSUInteger)retainCount  {
    return (NSUInteger)__CFDoExternRefOperation(OPERATION_retainCount,self);
}

- (id)retain  {
    return (id)__CFDoExternRefOperation(OPERATION_retain,self);
}

- (void)release  {
    return __CFDoExternRefOperation(OPERATION_release,self);
}
int __CFDoExternRefOperation(uintptr_r op,id obj) {
        CFBasicHashRef table = 取得对象对应的散列表(obj);
        int count;

        switch(op) {
            case OPERATION_retainCount: 
                count = CFBasicHashGetCountOfKey(table,obj);
                return count; 
            case OPERATION_retain: 
                CFBasicHashAddValue(table,obj);
                return obj; 
            case OPERATION_release: 
                count = CFBasicHashRemoveValue(table,obj):
                return 0 == count;
        }
    }

从上面代码可以看出,苹果大概就是采用散列表(引用计数表)来管理引用计数,当我们在调用retain、retainCount、release时,先调用_CFDoExternRefOperation()从而获取到引用计数表的内存地址以及本对象的内存地址,然后根据对象的内存地址在表中查询获取到引用计数值。

若是retain则加1,若是retainCount就直接返回值,若是release则减1。(在CFBasechashRemoveValue中将引用计数减少到0时会调用dealloc废弃对象)

Autorelease

作用: autorelease作用是将对象放入自动释放池中,当自从释放池销毁时对自动释放池中的对象都进行一次release操作。

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain]; 

原理: ARC下,使用@autoreleasepool{}来使用一个AutoreleasePool,随后编译器会改成下面的样子:

void *context = objc_autoreleasePoolPush();
// 执行的代码
objc_autoreleasePoolPop(context);

而这两个函数都是对AutoreleasePoolPage的简单的封装,所以自动释放机制的核心就在于这个类。
AutoreleasePoolPage是一个C++实现的类

image

  • AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage以双链表的形式组合而成(分别对应结构中的parent指针和child指针)
  • AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程)
  • AutoreleasePoolPage每个对象开辟一个虚拟内存一页的大小,除了上面实例变量所占空间,剩下的空间全部用来存储autorelease对象的地址
  • 上面的id *next指针作为游标指向栈顶最新add进来的autorelease对象的下一个位置
  • 一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,连接链表,后来的autorelease对象在新的page加入

所以,若当前线程中只有一个AutoreleasePoolPage对象,并记录了很多autorelease对象地址时内存如下:

image

图中的情况,这一页再加入一个autorelease对象就要满了(也就是next指针马上指向栈顶),这时就要执行上面说的操作,建立下一页page对象,与这一页链表链接完成后,新page的next指针被初始化在栈底(begin的位置),然后继续向栈顶添加新对象。

所以,向一个对象发送- autorelease消息,就是将这个对象加入到当前AutoreleasePoolPage的栈顶next指针指向的位置

每当执行一个objc_autoreleasePoolPush调用时,runtime向当前的AutoreleasePoolPageadd进一个哨兵对象,值为0(也就是nil),那么page就变成了下面的样子:

image

objc_autoreleasePoolPush的返回值正式这个哨兵对象的地址,被objc_autoreleasePoolPop(哨兵对象)作为入参,

  • 根据传入的哨兵对象地址找到哨兵对象所处的page
  • 在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次- release消息,并向回移动next指针到正确位置
  • 从最新加入的对象一直向前清理,可以向前跨越若干个page,知道哨兵所在的page

刚才的objc_autoreleasePoolPop执行后,最终变成了下面样子:

image

关键字

__strong

__strong表示强引用,指向并持有该对象。该对象只要引用计数不为0,就不会被销毁。如果在声明引用时,不加修饰符,那么引用将默认为强引用。

  • 对象通过alloc、new、copy、mutableCopy来分配内存的
id __strong obj = [[NSObject alloc] init];

编译器会转换成下面代码:

id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));

// ...
objc_release(obj);

当使用alloc、new、copy、mutableCopy进行对象内存分配时,强指针直接指向一个引用计数为1的对象

  • 对象不是自身生成,但是自身持有
id __strong obj = [NSMutableArray array];

在这种情况下,obj也指向一个引用计数为1的对象内存。编译器会转换成下面代码:

id obj = objc_msgSend(NSMutableArray, @selector(array));

//替代我们调用retain方法,是obj持有该对象
objc_retainAutoreleaseReturnValue(obj);
objc_release(obj);

从而使得obj指向了一个引用计数为1的对象,不过,
objc_retainAutoreleaseReturnValue有一个成对的函数objc_autoreleaseReturnValue,这两个函数可以用于最优化程序的运行,代码如下:

+ (id)array {
    return [[NSMutableArray alloc] init];
}

编译器转换如下:

+ (id)array {
    id obj = objc_msgSend(NSMutableArray,@selector(alloc));
    objc_msgSend(obj,@selector(init));
    
    // 代替我们调用autorelease方法
    return objc_autoreleaseReturnValue(obj);
}

其实autorelease这个开销不小,runtime机制解决了这个问题。

优化

Thread Local Storage(TLS)线程局部存储,目的很简单,将一块内存作为某个线程专有的存储,以key-value的形式进行读写,比如在非arm架构下,使用pthread提供的方法实现:

void *pthread_getspecific(pthread_key_t);
int pthread_setspecific(pthread_key_t, const void *);

在返回值身上调用objc_autoreleaseReturnValue方法时,runtime将这个返回值object储存在TLS中,然后直接返回这个object(不调用autorelease),同时,在外部接收这个返回值的objc_retainAutoreleaseReturnValue里,发现TLS中正好存在这个对象,那么直接返回这个object(不调用retain)。
于是乎,调用方和被调用利用TLS做中转,很有默契的免去了对返回值的内存管理。
关系图如下:

image

__weak

__weak表示弱引用,弱引用不会影响对象的释放,而当对象被释放时,所有指向它的弱引用都会自动被置为nil,这样可以防止野指针。

id __weak obj = [[NSObject alloc] init];

根据我们的了解,可以知道obj对象在生成之后立马就会被释放,主要原因是因为__weak修饰的指针没有引起对象内部的引用计数发生变化。

__weak的几个使用场景:

  • 在Delegate关系中防止循环引用
  • 在Block中防止循环引用
  • 用来修饰指向有Interface Builder创建的控件

weak实现原理的概括:
Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其实是一个Hash(哈希)表(这就是为什么在本文开始我要简单介绍一下Hash表的原因),Key是所指对象的地址,Valueweak指针的地址(这个地址的值是所指对象的地址)数组。

weak的实现原理可以概括成三步:

  • 初始化时,runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。
  • 添加引用时,objc_initWeak函数会调用objc_storeWeak()函数,objc_storeWeak()的作用是更新指针指向,创建对应的弱引用表。
  • 释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entryweak表中删除,最后清理对象的记录。

weak表

weak表是一个弱引用表,实现为一个weak_table结构体

struct weak_table_t {
    weak_entry_t *weak_entries;     // 保存来所有指向指定对象的weak指针     weak_entries的对象
    size_t num_entries;             // weak对象的存储空间
    uintptr_t mask;                 // 参与判断引用计数辅助量
    uintptr_t max_hash_displacement;// hash key 最大偏移值
};

这是一个全局弱引用Hash表。使用不定类型对象的地址作为key,用weak_entry_t类型结构体对象作为value,其中的weak_entries成员,从字面意思上看,即为弱引用表的入口。

weak全局表中的存储weak定义的对象的表结构weak_entry_tweak_entry_t是存储在弱引用表中的一个内部结构体,它负责维护和存储指向一个对象的所有弱引用Hash表。定义如下:

typedef objc_object ** weak_referrer_t;
struct weak_entry_t {
    DisguisedPtr<objc_object> referent;  //范型
    union {
        struct {
            weak_referrer_t *referrers;
            uintptr_t        out_of_line : 1;
            uintptr_t        num_refs : PTR_MINUS_1;
            uintptr_t        mask;
            uintptr_t        max_hash_displacement;
        };
        struct {
            // out_of_line=0 is LSB of one of these (don't care which)
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];
        };
    }
};

即:

  • weak_table_tweak全局表):采用Hash表的方式把所有weak引用的对象,存储所有引用weak对象。
  • weak_entry_tweak_table_t表中Hash表的value值,weak对象体):用于记录Hash表中weak对象。
  • objc_objct(weak_entry_t对象中的范型对象,用于标记对象weak对象):用于标示weak引用对象。

下面详细看下weak底层实现原理:

id __weak obj = [[NSObject alloc] init];

编译器转换后代码如下:

id obj;
id tmp = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(tmp,@selector(init));
objc_initWeak(&obj,tmp);
objc_release(tmp);
objc_destroyWeak(&obj);

对于objc_initWeak()的实现:

id objc_initWeak(id *location, id newObj) {
    // 查看对象实例是否有效,无效对象直接导致指针释放
    if (!newObj) { 
        *location = nil;
        return nil;
    }
    
    // 存储weak对象
    return storeWeak(location, newObj);
}

存储weak对象的方法:

/** 
 * This function stores a new value into a __weak variable. It would
 * be used anywhere a __weak variable is the target of an assignment.
 * 
 * @param location The address of the weak pointer itself
 * @param newObj The new object this weak ptr should now point to
 * 
 * @return \e newObj
 */
id
objc_storeWeak(id *location, id newObj)
{
    // 更新弱引用指针的指向
    id oldObj;
    SideTable *oldTable;
    SideTable *newTable;
    spinlock_t *lock1;
#if SIDE_TABLE_STRIPE > 1
    spinlock_t *lock2;
#endif

    // Acquire locks for old and new values.
    // Order by lock address to prevent lock ordering problems. 
    // Retry if the old value changes underneath us.
    
    /**
    获取新值和旧值的锁存位置(用地址作为唯一标示)
    通过地址来建立索引标志,防止桶重复
    下面指向操作会改变旧值
    */
 retry:
    // 更改指针,获得以oldObj为索引所存储的值地址
    oldObj = *location;
    oldTable = SideTable::tableForPointer(oldObj);
    // 更改新值指针,获得以newObj为索引所存储的值地址
    newTable = SideTable::tableForPointer(newObj);
    
    // 加锁操作,防止多线程中竞争冲突
    lock1 = &newTable->slock;
#if SIDE_TABLE_STRIPE > 1
    lock2 = &oldTable->slock;
    if (lock1 > lock2) {
        spinlock_t *temp = lock1;
        lock1 = lock2;
        lock2 = temp;
    }
    if (lock1 != lock2) spinlock_lock(lock2);
#endif
    spinlock_lock(lock1);

    if (*location != oldObj) {
        spinlock_unlock(lock1);
#if SIDE_TABLE_STRIPE > 1
        if (lock1 != lock2) spinlock_unlock(lock2);
#endif
        goto retry;
    }
    // 旧对象解除注册操作
    weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    // 新对象添加注册操作
    newObj = weak_register_no_lock(&newTable->weak_table, newObj, location);
    // weak_register_no_lock returns nil if weak store should be rejected

    // Set is-weakly-referenced bit in refcount table.
    if (newObj  &&  !newObj->isTaggedPointer()) {
        // 弱引用位初始化操作
        // 引用计数那张散列表的weak引用对象的引用计数中标识为weak的引用
        newObj->setWeaklyReferenced_nolock();
    }

    // Do not set *location anywhere else. That would introduce a race.
    // 前面不要设置location对象,这里需要更改指针指向
    *location = newObj;
    
    spinlock_unlock(lock1);
#if SIDE_TABLE_STRIPE > 1
    if (lock1 != lock2) spinlock_unlock(lock2);
#endif

    return newObj;
}

这里同样引用一个比较直观的初始化弱引用对象流程图:


image

总之根据以上对weak进行的存储过程,可以通过下面流程图帮助理解:


image

weak释放为nil的过程

释放对象基本流程如下:

  • 调用objc_release
  • 因为对象的引用计数为0,所以执行dealloc
  • dealloc中,调用来_objc_rootDealloc函数
  • _objc_rootDealloc中,调用来object_dispose函数
  • 调用objc_destructInstance
  • 最后调用objc_clear_deallocating

clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entryweak表中删除,最后清理对象的记录。

void objc_clear_deallocating(id obj) {
    assert(obj);
    assert(!UseGC);
    if (obj->isTaggedPointer()) return;
    obj->clearDeallocating();
}

//执行 clearDeallocating方法
inline void objc_object::clearDeallocating() {
    sidetable_clearDeallocating();
}
// 执行sidetable_clearDeallocating,找到weak表中的value值
void  objc_object::sidetable_clearDeallocating() {
    SideTable *table = SideTable::tableForPointer(this);
    // clear any weak table items
    // clear extra retain count and deallocating bit
    // (fixme warn or abort if extra retain count == 0 ?)
    spinlock_lock(&table->slock);
    RefcountMap::iterator it = table->refcnts.find(this);
    if (it != table->refcnts.end()) {
        if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
            weak_clear_no_lock(&table->weak_table, (id)this);
        }
        table->refcnts.erase(it);
    }
    spinlock_unlock(&table->slock);
}

最终通过调用weak_clear_no_lock方法,将weak指针置空,函数实现如下:

/** 
 * Called by dealloc; nils out all weak pointers that point to the 
 * provided object so that they can no longer be used.
 * 
 * @param weak_table 
 * @param referent The object being deallocated. 
 */
void 
weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
{
    objc_object *referent = (objc_object *)referent_id;

    weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
    if (entry == nil) {
        // XXX should not happen, but does with mismatched CF/objc
        //printf("XXX no entry for clear deallocating %p\n", referent);
        return;
    }

    // zero out references
    weak_referrer_t *referrers;
    size_t count;
    
    if (entry->out_of_line) {
        referrers = entry->referrers;
        count = TABLE_SIZE(entry);
    } 
    else {
        referrers = entry->inline_referrers;
        count = WEAK_INLINE_COUNT;
    }
    
    for (size_t i = 0; i < count; ++i) {
        objc_object **referrer = referrers[I];
        if (referrer) {
            if (*referrer == referent) {
                *referrer = nil;
            }
            else if (*referrer) {
                _objc_inform("__weak variable at %p holds %p instead of %p. "
                             "This is probably incorrect use of "
                             "objc_storeWeak() and objc_loadWeak(). "
                             "Break on objc_weak_error to debug.\n", 
                             referrer, (void*)*referrer, (void*)referent);
                objc_weak_error();
            }
        }
    }
    
    weak_entry_remove(weak_table, entry);
}

objc_clear_deallocating函数的操作如下:

  • weak表中获取废弃对象的地址为键值的记录
  • 将包含在记录中的所有附有weak修饰符变量的地址,置为nil
  • weak表中该记录删除
  • 从引用计数表中删除废弃对象的地址为键值的记录

说了这么多,还是为了说明一开始说的那句话:
Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其实是一个Hash(哈希)表,Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象的地址)数组。

__unsafe_unretained

__unsafe_unretained作用需要和weak对比,它不会引起对象的内部引用计数的变化,但是,当其指向的对象被销毁是__unsafe_unretained修饰的指针不会置为nil。是不安全的所有权修饰符,它不纳入ARC的内存管理。

__autoreleasing

将对象赋值给附有__autoreleasing修饰符的变量等同于MRC时调用对象的autorelease方法。

@autoeleasepool {
    // 如果看了上面__strong的原理,就知道实际上对象已经注册到自动释放池里面了 
    id __autoreleasing obj = [[NSObject alloc] init];
}

编译器转换如下代码:

id pool = objc_autoreleasePoolPush(); 
id obj = objc_msgSend(NSObject,@selector(alloc));
objc_msgSend(obj,@selector(init));
objc_autorelease(obj);
objc_autoreleasePoolPop(pool);
@autoreleasepool {
    id __autoreleasing obj = [NSMutableArray array];
}

编译器转换上述代码如下:

id pool = objc_autoreleasePoolPush();
id obj = objc_msgSend(NSMutableArray,@selector(array));
objc_retainAutoreleasedReturnValue(obj);
objc_autorelease(obj);
objc_autoreleasePoolPop(pool);

上面两种方式,虽然第二种持有对象的方法从alloc方法变为了objc_retainAutoreleasedReturnValue函数,都是通过objc_autorelease,注册到autoreleasePool中。

篇幅太长了,很多底层上面的东西,网上都有相关的资料,以前看不是很懂,现在回过头来细细研读,感觉还是能理解的,所以参考了网络上的资料整理出来了,增加自己的印象,也希望我的理解能够帮助到小伙伴们,如有错误,希望指出,共同进步,谢谢

参考资料:
《Objective-C高级编程 iOS于OS X多线程和内存管理》
iOS 底层解析weak的实现原理(包含weak对象的初始化,引用,释放的分析
黑幕后的Autorelease

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

推荐阅读更多精彩内容