iOS-OC底层-AutoReleasePool分析

概念

autoreleasepool,即自动释放池。在aotureleasepool中创建的对象,会将对象添加到当前的aoturelease pool中,当自动释放池销毁时,对所有的对象做release操作。那具体是个什么样的操作流程呐?我们开始今天的探索。

开始

先上一段代码片段:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSObject *objc = [NSObject alloc];
    }
    return 0;
}

拿到这么一段代码后,要探索它的本质,基本上也就2个方式入手:

  1. 通过Clang来查看下编译后的源码;
  2. 通过Xcode汇编跟踪流程;

首先我们通过Clang来看一下编译后的源码,查看main:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        NSObject *objc = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc"));
    }
    return 0;
}

这里可以很直观的看到@autoreleasepool最终转换为了__AtAutoreleasePool __autoreleasepool;

@autoreleasepool{
}
转换为:
{
  __AtAutoreleasePool __autoreleasepool; 
  
}

接一下看一下:__AtAutoreleasePool是个什么?

struct __AtAutoreleasePool {
  //构造函数
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  //析构函数
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

__AtAutoreleasePool是一个结构体,并包含构造函数析构函数和一个void *的变量;这种语法格式,我在之前的文章中也有介绍,
在作用域内部,声明一个变量时,会自动执行构造函数,出作用域时,会自动执行析构函数,最终的代码大致如下:

{
  __AtAutoreleasePool __autoreleasepool;  
}
转换为=>
{
   //进入作用域
   atautoreleasepoolobj = objc_autoreleasePoolPush();
   // 其他代码...
   // 其他代码...
   //即将出作用域
   objc_autoreleasePoolPop(atautoreleasepoolobj);
}

当进入作用域时会调用objc_autoreleasePoolPush(),
出作用域时会执行objc_autoreleasePoolPop(atautoreleasepoolobj);

接下来,再通过Xcode 汇编跟踪一下:

@autoreleasepool汇编跟踪

通过Symbolic Breakpoint添加一个objc_autoreleasePoolPush符号,来跟踪一下这个函数来自哪里:
Symbolic breakpoint 查看符号信息

这里可以很清楚的看到来自熟悉的libobjc.A.dylib中。

源码分析
void *
objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}
NEVER_INLINE
void
objc_autoreleasePoolPop(void *ctxt)
{
    AutoreleasePoolPage::pop(ctxt);
}

从源码可以看到,objc_autoreleasePoolPushobjc_autoreleasePoolPop内部是调用了AutoreleasePoolPage类的push()pop()。下一步我们先来看下这个AutoreleasePoolPage:

class AutoreleasePoolPage : private AutoreleasePoolPageData
{
    friend struct thread_data_t;
  public:
    static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
        PAGE_MAX_SIZE;  // must be multiple of vm page size
#else
        PAGE_MIN_SIZE;  // size and alignment, power of 2
#endif
private:
  //省略....
}

AutoreleasePoolPage继承自私有的AutoreleasePoolPageData:

struct AutoreleasePoolPageData
{
    magic_t const magic;  // 16
    __unsafe_unretained id *next;  //8
    pthread_t const thread;   //  8
    AutoreleasePoolPage * const parent; //8
    AutoreleasePoolPage *child;   // 8
    uint32_t const depth;        // 4
    uint32_t hiwat;             // 4
    // 16 + 8 + 8 + 8 + 8 + 4 + 4 = 56;
    // begin() = this + sizeof(*this) = 56;
    AutoreleasePoolPageData(__unsafe_unretained id* _next, pthread_t _thread, AutoreleasePoolPage* _parent, uint32_t _depth, uint32_t _hiwat)
        : magic(), next(_next), thread(_thread),
          parent(_parent), child(nil),
          depth(_depth), hiwat(_hiwat)
    {
    }
};

AutoreleasePoolPageData 是一个 包含多个成员变量和一个构造函数的 结构体,而且是一个双向链表的结构;
magic: 校验AutoreleasePoolPage的完整性;
next : 指向最新添加的autorelease对象的下一个位置;
thread: 当前的线程;
parent: 指向父节点,第一个节点的parent = nil;
child: 指向子节点,最后一个节点的chile = nil;
depth: 深度,从0开始递增;
hiwat: high water mark 最大入栈数量标记;

来看一段源码中关于Autorelease pool implementation的注释:

/***********************************************************************
   Autorelease pool implementation

   A thread's autorelease pool is a stack of pointers.
   Each pointer is either an object to release, or POOL_BOUNDARY which is
     an autorelease pool boundary.
   A pool token is a pointer to the POOL_BOUNDARY for that pool. When
     the pool is popped, every object hotter than the sentinel is released.
   The stack is divided into a doubly-linked list of pages. Pages are added
     and deleted as necessary.
   Thread-local storage points to the hot page, where newly autoreleased
     objects are stored.
**********************************************************************/

线程的自动释放池是指针的堆栈。
    每个指针都是要释放的对象,或者是POOL_BOUNDARY,它是自动释放池的边界。
    池令牌是指向该池的POOL_BOUNDARY的指针。 弹出池时,将释放比哨点更热的每个对象。
    堆栈分为两个双向链接的页面列表。 根据需要添加和删除页面。
    线程本地存储指向热页面,该页面存储新自动释放的对象。

a stack of pointer : 通过压栈的方式存入指针(也就是需要自动释放的对象)
POOL_BOUNDARY : 边界/哨兵/栈底标识
token: 用来存储POOL_BOUNDARY
a doubly-linked list: 双向链表
the hot page: 通过tls方式存储当前hotPage
这里的AutoreleasePoolPageData 就是每一张AutoreleasePoolPage的结构了,自动释放的对象都会压入到这个数据结构;

接下来,我们就具体看下对象是如何压入的,回到我们最开始的调用:
AutoreleasePoolPage::push()

AutoreleasePoolPage::push();
// push() 内部源码
   static inline void *push() 
    {
        id *dest;
        if (slowpath(DebugPoolAllocation)) {
            // Each autorelease pool starts on a new pool page.
            // 每个自动释放池从一个新的池页面开始。
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {
            dest = autoreleaseFast(POOL_BOUNDARY);
        }
        ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }

通常看到这里通常忽略掉slowpath的情况,也就是说这里push(),会执行autoreleaseFast(POOL_BOUNDARY); 这里可以看到传入了一个POOL_BOUNDARY边界标识

static inline id *autoreleaseFast(id obj)
    {
        //获取当前hotPage
        AutoreleasePoolPage *page = hotPage();
        //如果hotPage存在且不满!full
        if (page && !page->full()) {
            //添加[自动释放对象obj]
            return page->add(obj);
        } else if (page) { //存在hotPage且满了full
            return autoreleaseFullPage(obj, page);
        } else {//不存在hotPage
            return autoreleaseNoPage(obj);
        }
    }
//获取hotPage的源码
static inline AutoreleasePoolPage *hotPage() 
    {
        //通过tls_get_direct()获取 (getValueFromKey:)
        AutoreleasePoolPage *result = (AutoreleasePoolPage *)
            tls_get_direct(key);  
        if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
        if (result) result->fastcheck();
        return result;
    }

这里的代码还是非常清晰的,首先是读取hotPage(),然后分3中情况来处理:

  1. 如果hotPage存在且不满!full
  2. 存在hotPage且满了full
  3. 不存在hotPage

首次进来一定是先执行第3种情况,先看一下autoreleaseNoPage(obj)源码:

id *autoreleaseNoPage(id obj)
    {
        // "No page" could mean no pool has been pushed
        // or an empty placeholder pool has been pushed and has no contents yet
        ASSERT(!hotPage());

        bool pushExtraBoundary = false;
        if (haveEmptyPoolPlaceholder()) {
            // We are pushing a second pool over the empty placeholder pool
            // or pushing the first object into the empty placeholder pool.
            // Before doing that, push a pool boundary on behalf of the pool 
            // that is currently represented by the empty placeholder.
            pushExtraBoundary = true;
        }
        else if (obj != POOL_BOUNDARY  &&  DebugMissingPools) {
            // We are pushing an object with no pool in place, 
            // and no-pool debugging was requested by environment.
            _objc_inform("MISSING POOLS: (%p) Object %p of class %s "
                         "autoreleased with no pool in place - "
                         "just leaking - break on "
                         "objc_autoreleaseNoPool() to debug", 
                         objc_thread_self(), (void*)obj, object_getClassName(obj));
            objc_autoreleaseNoPool(obj);
            return nil;
        }
        else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) {
            // We are pushing a pool with no pool in place,
            // and alloc-per-pool debugging was not requested.
            // Install and return the empty pool placeholder.
            return setEmptyPoolPlaceholder();
        }

        // We are pushing an object or a non-placeholder'd pool.

        // Install the first page.
        AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
        setHotPage(page);
        
        // Push a boundary on behalf of the previously-placeholder'd pool.
        if (pushExtraBoundary) {
            page->add(POOL_BOUNDARY);
        }
        
        // Push the requested object or pool.
        return page->add(obj);
    }

首先要知道这个方法目前传入的参数obj = POOL_BOUNDARY,
这里一进来了的3个if判断可以忽略,都是一些容错判断。核心就3行:

  // Install the first page.
        // 新建一个AutoreleasePoolPage
        AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
        // 设置当前的hotPage 为 page
        setHotPage(page);
        // Push a boundary on behalf of the previously-placeholder'd pool.
        if (pushExtraBoundary) {
            page->add(POOL_BOUNDARY);
        }
        // Push the requested object or pool.
        // 把obj压入page
        return page->add(obj);
}

新建一个AutoreleasePoolPage ->设置当前的hotPage 为 page -> 把obj压入page;
这里着重看下new AutoreleasePoolPage(nil);的构造函数:

AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
        AutoreleasePoolPageData(begin(),
                                objc_thread_self(),
                                newParent,
                                newParent ? 1+newParent->depth : 0,
                                newParent ? newParent->hiwat : 0)
    { 
        if (parent) {
            parent->check();
            ASSERT(!parent->child);
            parent->unprotect();
            parent->child = this;
            parent->protect();
        }
        protect();
    }

该构造函数接收一个newParent参数,这里新的page, newParent = nil, 内部又调用了AutoreleasePoolPageData ()构造函数,在看下AutoreleasePoolPageData的构造函数:

AutoreleasePoolPageData(__unsafe_unretained id* _next, pthread_t _thread, AutoreleasePoolPage* _parent, uint32_t _depth, uint32_t _hiwat)
        : magic(), next(_next), thread(_thread),
          parent(_parent), child(nil),
          depth(_depth), hiwat(_hiwat)
    {
    }

这里重点看下赋值:
_next = begin() : 初始化将begin()赋值给了_next, 看下 begin()

id * begin() {
        return (id *) ((uint8_t *)this+sizeof(*this));
}

这里其实就是AutoreleasePoolPageData结构体的末端,新的对象将从这个位置作为起始位置开始压入(这里其实就是偏移56个字节长度,即AutoreleasePoolPageData默认的大小);
thread = objc_thread_self():当前线程;
parent = newParent = nil:第一个page 节点的父节点为nil;
depth = 0: 初始化深度为0;
hiwat = 0:最大入栈数量标记为0;
最终初始化完的AutoreleasePoolPage就是这样一个结构:

一个新的AutoreleasePoolPage

page->add(obj)后:
压入一个自动释放对象

这里要知道的是如果是首次创建的AutoreleasePoolPage第一个压入的是POOL_BOUNDARY边缘标识,之后才是其他对象。

首次进入的情况到这里也就完了,接下来看一下如果hotPage存在且不满!full的情况:
这种情况比较简单,直接将新的autorelease对象直接压入就可以了
page->add(obj);

存在hotPage且满了full的情况:
此时会进入autoreleaseFullPage(obj, page);,这里传入了要压入的obj和hotPage page:

id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
    {
        // The hot page is full. 
        // Step to the next non-full page, adding a new page if necessary.
        // Then add the object to that page.
        ASSERT(page == hotPage());
        ASSERT(page->full()  ||  DebugPoolAllocation);

        do {
            if (page->child) page = page->child;
            else page = new AutoreleasePoolPage(page);
        } while (page->full());

        setHotPage(page);
        return page->add(obj);
    }

这里的核心就是这个do while循环
判断当前page是否存在子节点,如果存在,把当前子节点赋值给page,判断当前page是否已经存满,如果满了,继续查询子节点,知道找到没有满的节点;如果不存在,重新开辟新的AutoreleasePoolPage。 最终将拿到的page设置为HotPage,然后将obj压入page
最终就是这样的一个结构:

autoreleasepool内部数据结构

这里再看一下page->full()的临界值是多少

bool full() { 
   return next == end();
}
id * end() {
  return (id *) ((uint8_t *)this+SIZE);
}

static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
        PAGE_MAX_SIZE;  // must be multiple of vm page size
#else
        PAGE_MIN_SIZE;  // size and alignment, power of 2
#endif

#define PAGE_MAX_SIZE           PAGE_SIZE
#define PAGE_SIZE               I386_PGBYTES
#define I386_PGBYTES            4096            /* bytes per 80386 page */

this就是AutoreleasePoolPage -> AutoreleasePoolPageData结构体,最大字节数为4096,去处结构体本身的大小56,剩余4040字节,4040/8 = 505,所以每一个Page 最多可压入 505个对象,要注意的是首张page只能放入504个对象,因为被POOL_BOUNDARY标识占用了8个字节。

通过MRC直观的看下对象入栈情况
通过Xcode->build Setting,关闭ARC,通过手动调用autorelease入栈,并通过_objc_autoreleasePoolPrint()方法打印出入栈信息:

//extern 关键字:该函数的实现在其他模块中
extern void _objc_autoreleasePoolPrint(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        for(int i = 0;i<510;i++){
            NSObject *objc = [[NSObject alloc]autorelease];
        }
        _objc_autoreleasePoolPrint();
    }
    return 0;
}

我这里截取部分输出信息:


控制台输出

控制台输出

当压入的对象个数超过505的时候,会开辟新的AutoreleasePoolPage。这里创建了510个对象,所以开辟了第2个Page。
观察一下每个AutoreleasePoolPage的起始地址和第一个值存放地址:

//AutoreleasePoolPage0
objc[58092]: [0x101012000]  ................  PAGE (full)  (cold)
objc[58092]: [0x101012038]  ################  POOL 0x101012038
0x101012038 = 0x101012000 + 0x38(56) = 0x101012000 + begin()
//AutoreleasePoolPage1
objc[58092]: [0x10100a000]  ................  PAGE  (hot) 
objc[58092]: [0x10100a038]       0x100742e50  NSObject
0x10100a038 = 0x10100a000 + 0x38(56) = 0x10100a000 + begin()

以上就是关于autoreleasepool_objc_autoreleasePoolPush(void)的探索。

接下来我们在看一下_objc_autoreleasePoolPop(void * _Nonnull context) 出栈释放都是怎么做的

void
objc_autoreleasePoolPop(void *ctxt)
{
    AutoreleasePoolPage::pop(ctxt);
}
static inline void
    pop(void *token)
    {
        AutoreleasePoolPage *page;
        id *stop;
        if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
            // Popping the top-level placeholder pool.
            page = hotPage();
            if (!page) {
                // Pool was never used. Clear the placeholder.
                return setHotPage(nil);
            }
            // Pool was used. Pop its contents normally.
            // Pool pages remain allocated for re-use as usual.
            page = coldPage();
            token = page->begin();
        } else {
            page = pageForPointer(token);
        }

        stop = (id *)token;
        if (*stop != POOL_BOUNDARY) {
            if (stop == page->begin()  &&  !page->parent) {
                // Start of coldest page may correctly not be POOL_BOUNDARY:
                // 1. top-level pool is popped, leaving the cold page in place
                // 2. an object is autoreleased with no pool
            } else {
                // Error. For bincompat purposes this is not 
                // fatal in executables built with old SDKs.
                return badPop(token);
            }
        }

        if (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
            return popPageDebug(token, page, stop);
        }

        return popPage<false>(token, page, stop);
    }

上面的代码主要是取出codePage,赋值给了page,这里的page实际上就是最开始的那个包含POOL_BOUNDARY标识的pagetoken = page->begin, stop = token; stop存着标识符,为后面的对象是否全部出栈做判断。这里面核心的调用其实是最后一行:
return popPage<false>(token, page, stop);

template<bool allowDebug>
    static void
    popPage(void *token, AutoreleasePoolPage *page, id *stop)
    {
        if (allowDebug && PrintPoolHiwat) printHiwat();

        page->releaseUntil(stop);

        // memory: delete empty children
        if (allowDebug && DebugPoolAllocation  &&  page->empty()) {
            // special case: delete everything during page-per-pool debugging
            AutoreleasePoolPage *parent = page->parent;
            page->kill();
            setHotPage(parent);
        } else if (allowDebug && DebugMissingPools  &&  page->empty()  &&  !page->parent) {
            // special case: delete everything for pop(top)
            // when debugging missing autorelease pools
            page->kill();
            setHotPage(nil);
        } else if (page->child) {
            // hysteresis: keep one empty child if page is more than half full
            if (page->lessThanHalfFull()) {
                page->child->kill();
            }
            else if (page->child->child) {
                page->child->child->kill();
            }
        }
    }

void releaseUntil(id *stop) 
    {
        // Not recursive: we don't want to blow out the stack 
        // if a thread accumulates a stupendous amount of garbage
        
        while (this->next != stop) {
            // Restart from hotPage() every time, in case -release 
            // autoreleased more objects
            AutoreleasePoolPage *page = hotPage();

            // fixme I think this `while` can be `if`, but I can't prove it
            while (page->empty()) {
                page = page->parent;
                setHotPage(page);
            }

            page->unprotect();
            id obj = *--page->next;
            memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
            page->protect();

            if (obj != POOL_BOUNDARY) {
                objc_release(obj);
            }
        }

        setHotPage(this);

#if DEBUG
        // we expect any children to be completely empty
        for (AutoreleasePoolPage *page = child; page; page = page->child) {
            ASSERT(page->empty());
        }
#endif
    }

这里可以看到核心的对象释放逻辑在void releaseUntil(id *stop)里面,
通过while循环 拿到每个子节点的所有obj,并最终调用了objc_release(obj);函数,来完成对象的释放,这里跟dealloc的实现是一致的。在page->releaseUntil(stop);之后,就是关于page相关的释放处理,核心方法就是page->kill()
这里allocDebug = false,所以会直接进入

else if (page->child) {
            // hysteresis: keep one empty child if page is more than half full
            if (page->lessThanHalfFull()) {
                page->child->kill();
            }
            else if (page->child->child) {
                page->child->child->kill();
            }
 }

void kill() 
    {
        // Not recursive: we don't want to blow out the stack 
        // if a thread accumulates a stupendous amount of garbage
        AutoreleasePoolPage *page = this;
        while (page->child) page = page->child;

        AutoreleasePoolPage *deathptr;
        do {
            deathptr = page;
            page = page->parent;
            if (page) {
                page->unprotect();
                page->child = nil;
                page->protect();
            }
            delete deathptr;
        } while (deathptr != this);
    }

通过delete 将节点page释放掉。
当push时,生成了page1,page2,page3;pop时,首先拿到page1,并通过stop保存page1->begin();进入releaseUntil(stop)后,通过一个while循环,从最深的节点,这里是page3,通过next指针查找obj,然后调用objc_release(obj)释放对象;对象释放完成后,开始对page->kill(),通过delete 释放page;

栈的先进后出

总结

以上就是关于@autoreleasepool的探索内容了。有什么问题,请在评论区指出哦!谢谢!

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

推荐阅读更多精彩内容