Runtime学习-Category的学习

在平日编程中或阅读第三方代码时,category可以说是无处不在。category也可以说是OC作为一门动态语言的一大特色。category为我们动态扩展类的功能提供了可能,或者我们也可以把一个庞大的类进行功能分解,按照category进行组织。

数据结构

工程中添加分类,对分类进行编译,编译后的category结构如下:

struct _category_t {
    const char *name;
    struct _class_t *cls;
    const struct _method_list_t *instance_methods;
    const struct _method_list_t *class_methods;
    const struct _protocol_list_t *protocols;
    const struct _prop_list_t *properties;
};

category_t在源码中的数据结构

struct category_t {
    //分类名字
    const char *name;
    
    //对应的类
    classref_t cls;
    
    //实例对象的方法
    WrappedPtr<method_list_t, PtrauthStrip> instanceMethods;
    //类方法
    WrappedPtr<method_list_t, PtrauthStrip> classMethods;
    //协议
    struct protocol_list_t *protocols;
    //属性
    struct property_list_t *instanceProperties;
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;
  
  
    /****获取方法、属性、协议的方法****/
    //获取分类中的方法
    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    //获取分类中的属性
    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
    
    //获取分类中的协议
    protocol_list_t *protocolsForMeta(bool isMeta) {
        if (isMeta) return nullptr;
        else return protocols;
    }
};

从源码和编译后的结构来看,category包含了以下内容:
const char *name: 对应类的名字
struct _class_t *cls: 对应的类,在加载阶段,使用该参数查找对应的类

const struct _method_list_t *instance_methods: 实例方法列表,在分类中编写的实例方法,会存储在该列表中
const struct _method_list_t *class_methods: 类方法列表,分类中的类方法,会存储在该列表中
const struct _prop_list_t *properties: 属性列表,分类中定义的属性会存储在该列表
const struct _protocol_list_t *protocols: 协议列表,分类中的协议会存储在该列表

category的编译

category的编译.png

分类经过编译后
1)、生成了实例方法列表_OBJC_$_CATEGORY_INSTANCE_METHODS_MethodSend_$_cate和类方法列表_OBJC_$_CATEGORY_CLASS_METHODS_MethodSend_$_cate,两者的命名都遵循了公共前缀+类名+category名字的命名方式
2)、生成了category的结构_OBJC_$_CATEGORY_MethodSend_$_cate
3)、在最后执行了section ("__DATA, __objc_catlist,regular,no_dead_strip"),表明分类是存储在 __DATA 段的__objc_catlist section里面的。这部分是Mach-o的数据,在运行时加载category时,从这部分取出所有的category然后添加到对应类的方法列表中。
Mach-o

category的加载流程

当动态连接器(dyld)加载处理好的OC代码时,系统会调用objc_init()方法。

void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    environ_init();
    tls_init();
    static_init();
    runtime_init();
    exception_init();
#if __OBJC2__
    cache_t::init();
#endif
    _imp_implementationWithBlock_init();

  //向dyld注册监听Mach-O中OC相关section被加载入\载出内存的事件
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}

这里主要关注_dyld_objc_notify_register(&map_images, load_images, unmap_image);方法。该方法会向dyld注册监听Mach-O中OC相关section被加载入\载出内存的事件。

具体有三个事件:
_dyld_objc_notify_mapped(对应map_images回调):当dyld已将images加载入内存时。
_dyld_objc_notify_init(对应load_images回调):当dyld初始化image后。OC调用类的+load方法,就是在这时进行的。
_dyld_objc_notify_unmapped(对应unmap_image回调):当dyld将images移除内存时。

而category写入类的的方法列表,是在_dyld_objc_notify_mapped,也就是调用map_images方法时进行的写入操作,即将Mach-O相关sections都加载到内存之后所发生的。

(1)、map_images方法实现

// _dyld_objc_notify_register监听到dyld已将images加载入内存的通知 处理方法
void map_images(unsigned count, const char * const paths[],
           const struct mach_header * const mhdrs[])
{
    mutex_locker_t lock(runtimeLock);
    //调用map_images_nolock
    return map_images_nolock(count, paths, mhdrs);
}
//接受到dyld已将images加载入内存的通知后的操作
void map_images_nolock(unsigned mhCount, const char * const mhPaths[],
                  const struct mach_header * const mhdrs[])
{
    static bool firstTime = YES;
    header_info *hList[mhCount];
    uint32_t hCount;
    size_t selrefCount = 0;

  
  /*其他相关代码*/
  

    // Find all images with Objective-C metadata.
    hCount = 0;
    
    //计算class数量,根据总数调整各种表的大小。
    // Count classes. Size various table based on the total.
    int totalClasses = 0;
    int unoptimizedTotalClasses = 0;
    {
        uint32_t i = mhCount;
        while (i--) {
            const headerType *mhdr = (const headerType *)mhdrs[i];
                            
          //循环读取mach head 信息,从共享缓存中读取header_info
            auto hi = addHeader(mhdr, mhPaths[i], totalClasses, unoptimizedTotalClasses);
            if (!hi) {
                // no objc data in this entry
                continue;
            }
                        
            //将读取到的header_info信息添加到hList
            hList[hCount++] = hi;
        }
    }

  //从mach中读取到了head_info
    if (hCount > 0) {
        //直接开始image读取
        _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
    }

    firstTime = NO;
    
    // Call image load funcs after everything is set up.
    for (auto func : loadImageFuncs) {
        for (uint32_t i = 0; i < mhCount; i++) {
            func(mhdrs[i]);
        }
    }
}

map_images是调用了map_images_nolock方法,由map_images_nolock处理后续的逻辑。
map_images_nolock方法中,最主要的工作是遍历可执行文件,从共享缓存中读取header_info信息,并且把遍历出来的header_info信息存储到hList中,以供后面的处理流程使用。读取到所有的header_info后,调用_read_images方法。

(2)、_read_images方法实现

//从mach-o中加载类、协议、分类、selector等
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
    header_info *hi;
    uint32_t hIndex;
    size_t count;
    size_t i;
    Class *resolvedFutureClasses = nil;
    size_t resolvedFutureClassCount = 0;
    static bool doneOnce;
    bool launchTime = NO;
    TimeLogger ts(PrintImageTimes);

#define EACH_HEADER \
    hIndex = 0;         \
    hIndex < hCount && (hi = hList[hIndex]); \
    hIndex++

    // 开始遍历头文件,进行类与元类的读取操作并标记(旧类改动后会生成新的类,并重映射到新的类上)
    for (EACH_HEADER) {

        ////从头文件中拿到类的信息
        classref_t const *classlist = _getObjc2ClassList(hi, &count);

        bool headerIsBundle = hi->isBundle();
        bool headerIsPreoptimized = hi->hasPreoptimizedClasses();

        for (i = 0; i < count; i++) {
            Class cls = (Class)classlist[i];
            //!核心操作,readClass读取类的信息及类的更新
            Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);

                      if (newCls != cls  &&  newCls) {
                          // Class was moved but not deleted. Currently this occurs
                // only when the new class resolved a future class.
                // Non-lazily realize the class below.
                resolvedFutureClasses = (Class *)
                    realloc(resolvedFutureClasses, 
                            (resolvedFutureClassCount+1) * sizeof(Class));
                resolvedFutureClasses[resolvedFutureClassCount++] = newCls;
            }
        }
    }

    // 处理分类category,并rebuild重建这个类的方法列表method list
    
    // Discover categories. Only do this after the initial category
    // attachment has been done. For categories present at startup,
    // discovery is deferred until the first load_images call after
    // the call to _dyld_objc_notify_register completes. rdar://problem/53119145
    if (didInitialAttachCategories) {
        for (EACH_HEADER) { // for(hIndex = 0; hIndex < hCount && (hi = hList[hIndex]); hIndex++)
            load_categories_nolock(hi);
        }
    }

 
  /*
    其他相关代码
  */

#undef EACH_HEADER
}

_read_images方法是非常重要的方法,代码量也是很大的,大概三百多行,涉及到类、协议、方法和分类的加载等等。这里主要以分类的加载进行说明。
方法中,使用宏EACH_HEADER定义了一个for循环。对map_images_nolock方法中归集出来的header_info进行遍历,调用load_categories_nolock方法处理header_infocatlist

(3)、load_categories_nolock方法实现


static void load_categories_nolock(header_info *hi) {
    
    // hi是mach-o的头,表头

    // 这个count的数量是下面hi->catlist(&count)
    /*
     *processCatlist(hi->catlist(&count));
     *processCatlist(hi->catlist2(&count));
     */
    //有几个分类,就是cout就是多少
    size_t count;
    
    
    //获取全部类的全部的分类
    
    // C++ 闭包 [&](category_t * const *catlist) {};
    auto processCatlist = [&](category_t * const *catlist) {
        
        //遍历每个分类,处理类方法和对应类的绑定、对象方法和对应类的绑定
        for (unsigned i = 0; i < count; i++) {
            //取出i位置的category
            category_t *cat = catlist[i];
            //取出category中保存的类的信息
            Class cls = remapClass(cat->cls);
            locstamped_category_t lc{cat, hi};

            //处理这个分类
            // Process this category.
            
            //如果当前类是rootClass
            if (cls->isStubClass()) {
                
                
                // Stub classes are never realized. Stub classes
                // don't know their metaclass until they're
                // initialized, so we have to add categories with
                // class methods or properties to the stub itself.
                // methodizeClass() will find them and add them to
                // the metaclass as appropriate.
                if (cat->instanceMethods ||
                    cat->protocols ||
                    cat->instanceProperties ||
                    cat->classMethods ||
                    cat->protocols ||
                    (hasClassProperties && cat->_classProperties))
                {
                    objc::unattachedCategories.addForClass(lc, cls);
                }
            } else {

                //如果当前类是 非rootClass
                
                // First, register the category with its target class.
                // Then, rebuild the class's method lists (etc) if
                // the class is realized.
                
                //实例方法、协议、属性
                if (cat->instanceMethods ||  cat->protocols
                    ||  cat->instanceProperties)
                {
                    if (cls->isRealized()) {
                        //ATTACH_EXISTING = 1 << 3 = 0001 << 3 = 1000 = 8
                        attachCategories(cls, &lc, 1, ATTACH_EXISTING);
                    } else {
                        objc::unattachedCategories.addForClass(lc, cls);
                    }
                }

                //类方法
                if (cat->classMethods  ||  cat->protocols
                    ||  (hasClassProperties && cat->_classProperties))
                {
                    if (cls->ISA()->isRealized()) {
                        //ATTACH_EXISTING = 1 << 3 = 0001 << 3 = 1000 = 8
                        //ATTACH_METACLASS = 1 << 1 = 0001 << 1 = 0010 = 2
                        //ATTACH_EXISTING | ATTACH_METACLASS = 1010 = 10
                        attachCategories(cls->ISA(), &lc, 1, ATTACH_EXISTING | ATTACH_METACLASS);
                    } else {
                        objc::unattachedCategories.addForClass(lc, cls->ISA());
                    }
                }
            }
        }
    };

    
    
    // 就是从mach-o中获取分类,但是为什么分两个?_getObjc2CategoryList 不必关心,没意义,是编译时的东西
    // _getObjc2CategoryList
    processCatlist(hi->catlist(&count));
    // _getObjc2CategoryList2
    processCatlist(hi->catlist2(&count));
}

load_categories_nolock方法对header_info中的catlist做处理。
首先定义一个回调processCatlist(可以类比block),在回调中遍历catlist每一个category,并调用attachCategories方法处理category中的类方法和实例方法的绑定。然后调用这个回调,实现方法的绑定流程。

(4)、attachCategories方法实现


static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
                 int flags)
{

    constexpr uint32_t ATTACH_BUFSIZ = 64;
    //方法列表
    method_list_t   *mlists[ATTACH_BUFSIZ];
    //属性列表
    property_list_t *proplists[ATTACH_BUFSIZ];
    //协议列表
    protocol_list_t *protolists[ATTACH_BUFSIZ];

    uint32_t mcount = 0;
    uint32_t propcount = 0;
    uint32_t protocount = 0;
    
    bool fromBundle = NO;
    
    //实例对象,传值为8,即1000 ;类对象传值为10,即 1010
    //ATTACH_METACLASS = 1 << 1 = 0010
    //isMeta 在实例对象时,= 0; 类对象时,=1
    bool isMeta = (flags & ATTACH_METACLASS);
    
    /*
     *ro属于cleanmemory,在编译即确定的内存空间,只读,加载后不会改变内容的空间
     *rw属于dirtymemory,rw是运行时结构,可读可写,可以向类中添加属性、方法等,在运行时会改变的内存;
     *rwe相当于类的额外信息,因为在实际使用过程中,只有很少的类会真正的改变他们的内容,所以为避免资源的消耗就有了rwe;
     *
     *ro被复制一份到rw里面,这是因为在运行时分类可以添加方法,而程序员也可以动态添加方法或者属性到类里面,而ro是只读的,所以需要在rw里面来追踪这些东西。
     *不是每一个类都会动态添加,所以如果这片内存写在rw里面,那么就会对脏内存有影响,所以把这些东西放在rwe里面
     */
    auto rwe = cls->data()->extAllocIfNeeded();

    
    //拿出来类的全部分类,并获取到所有分类里面的方法、属性、和协议
    for (uint32_t i = 0; i < cats_count; i++) {
        
        //拿到一个分类category
        auto& entry = cats_list[i];

        //从分类中拿出来方法列表
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            
            //如果分类中方法超过64个,数组存满了,就先添加方法列表.
            if (mcount == ATTACH_BUFSIZ) {
                prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
              // 添加方法
                rwe->methods.attachLists(mlists, mcount);
                mcount = 0;
            }
            //最开始查到的分类中的方法,放到最后一个位置,从后向前插入到mlists中
            mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
            fromBundle |= entry.hi->isBundle();
        }
    }

    
    //把之前获取到的所有分类中的方法、属性、和协议,添加到类中
    if (mcount > 0) {
        //把分类的方法添加到方法列表
        prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
                           NO, fromBundle, __func__);
      
        //调用list_array_tt的方法attachLists
        rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
        
        //
        //ATTACH_EXISTING = 1 << 3 = 1000 = 8 = 1000
        //flags 对象方法是 1 << 3 = 8 = 1000;  类方法是ATTACH_EXISTING | ATTACH_METACLASS =  8 | 2 = 10 = 1010
        //在加载分类的流程中,这个缓存的刷新都会走到
        if (flags & ATTACH_EXISTING) {
            flushCaches(cls, __func__, [](Class c){
                // constant caches have been dealt with in prepareMethodLists
                // if the class still is constant here, it's fine to keep
                return !c->cache.isConstantOptimizedCache();
            });
        }
    }

    //属性:添加到类的属性列表
    rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
        //协议:添加到类的协议列表
    rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}

auto rwe = cls->data()->extAllocIfNeeded();:该方法获取到类的class_ro_t,使用这个变量支持修改类的信息。

rwe说明.png

该方法把分类中的方法、协议、属性添加到类中。
methods 类型为 method_array_t、properties 类型为 property_array_t、protocols 类型为 protocol_array_t。而这三个类型都是list_array_tt,所以添加到类的时候,都是调用的list_array_tt的方法attachLists

(5)、list_array_ttattachLists方法实现


    void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;

        if (hasArray()) {
            // many lists -> many lists
            
            //拿到原数组的count
            uint32_t oldCount = array()->count;
            //根据原数组的count和新加进来的count,得到新数组的size
            uint32_t newCount = oldCount + addedCount;
            //根据新数组的size,开辟空间,得到新的数组
            array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount));
            newArray->count = newCount;
            array()->count = newCount;

            //把原来的数据从后向前,添加到新数组中
            for (int i = oldCount - 1; i >= 0; i--)
                newArray->lists[i + addedCount] = array()->lists[i];
            
            //新来的数组,从前向后,插入到新数组中
            for (unsigned i = 0; i < addedCount; i++)
                newArray->lists[i] = addedLists[i];
            free(array());
            
            //给数组重新赋值
            setArray(newArray);
            validate();
        }
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list
            list = addedLists[0];
            validate();
        } 
        else {
            // 1 list -> many lists
            Ptr<List> oldList = list;
            uint32_t oldCount = oldList ? 1 : 0;
            uint32_t newCount = oldCount + addedCount;
            setArray((array_t *)malloc(array_t::byteSize(newCount)));
            array()->count = newCount; 
            if (oldList) array()->lists[addedCount] = oldList;
            for (unsigned i = 0; i < addedCount; i++)
                array()->lists[i] = addedLists[i];
            validate();
        }
    }

attachLists方法添加数据的时候,把新进来的数据添加到前面,原来的数据是排在后面。

经过以上方法的处理,category的方法、属性、协议就被加载到了类中。流程图如下:
category载入流程.png

category的一些问题

(1)、category和类定义相同的方法,方法的调用顺序

  • 分类与类的同名方法调用
    根据attachLists方法,可以看出,分类中的方法是插入在类原有方法前面,所以分类方法会优先调用。
    但是这种并不是简单的方法覆盖,只是按照顺序优先调用category中的方法。
  • 分类与分类中同名方法的调用

    根据编译顺序,最后编译的category优先调用。
    调用顺序.png

(2)、category中为什么不能添加属性
对属性的修饰符@property。使用@property的时候,系统会自动生成带“_”的成员变量和该变量的setter和getter方法。
属性归根结底是由成员变量和setter + gettr方法组成。而这部分的生成的时期是编译期。
经过category的学习,能清楚category的加载是在运行时。即使是使用@property修饰了,也已经过了编译期,不会生成成员变量和setter+getter方法。

如果有需要在分类中添加属性,可以借助关联对象的方法,手动创建属性的settergetter,并与属性的进行关联。
代码如下:

  /*
  *MethodSend (cate)分类的interface
  */
@interface MethodSend (cate)
//定义属性。这里是通过重写setter和getter方法,MethodSend实例和cate_Name进行的关联。
//和编译期的@property创建成员变量、setter、getter是不同的逻辑
//在分类中添加的属性,没有成员变量
@property (nonatomic, copy) NSString * cate_Name;

@end
  /*
  *MethodSend (cate)分类的实现
  */
@implementation MethodSend (cate)

- (void)setCate_Name:(NSString *)cate_Name{
    NSLog(@"setCate_Name  is cate_Name = %@",cate_Name);
    objc_setAssociatedObject(self,
                             "cate_Name",
                             cate_Name,
                             OBJC_ASSOCIATION_COPY);
}
- (NSString *)cate_Name{
    NSString*string = objc_getAssociatedObject(self, "cate_Name");
    NSLog(@"cate_Name  is cate_Name = %@",string);
    //只是MethodSend对象关联了cate_Name,并不能真正的生成成员变量、setter、getter
    //NSLog(@"cate_Name  is _cate_name = %@",_cate_name);
    return string;
}

(3)、category和extension的区别
加载时机:
category:在运行时加载
extension:在编译期加载

添加属性:
category:不能添加属性,准确点说添加了属性,但是没有成员变量、setter、getter,不能正常使用
extension:由于在编译期加载,所以extension可以添加属性,并声称对应的成员变量、setter、getter

添加方法:
category:可以添加方法
extension:只能是方法的定义,没有方法的实现

用处:
category:可以用来为系统类添加方法;或者使用category来对自定义类进行功能的拆分。
extension:一般用来隐藏类的私有信息,你必须有一个类的源码才能为一个类添加extension,所以你无法为系统的类比如NSString添加extension,除非创建子类再添加extension。而category不需要有类的源码,我们可以给系统提供的类添加category。

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

推荐阅读更多精彩内容