load/initalize/MethodSwizzling/Block

前言

2016年6月7號開始load/initalize/KVO/KVC/Block,并通過代碼實現

load/initalize

  • NSObject是大多數Objective-C類層次結構的根類,通過繼承NSObject對象,擁有Objective-C運行時對象能力基本的接口
  • load/initalize 兩個方法,由每個類Objective - C運行時自動調用

load 簡介

當一個類或分類被添加到Objective-C運行時;會調用load方法,實現這個方法來加載執行特定類的行為。

Discussion:

  • load方法消息會被發送到動態加載和靜態鏈接的類和類別,只有當被加載新的類或類別實現該方法才可以響應。

初始化時執行順序如下:

  • 對所有鏈接框架初始化 (#import)
  • 加載所有load方法
  • 初始化所有C++ 靜態變量和靜態方法和使用attribute(constructor) 關鍵字聲明的函數
  • 初始化所有鏈接到你的框架 (在Link Binary With Libraries導入的框架)

注意

  • 該類的load方法調用在所有父類的load方法之后
  • 該分類的load方法調用在對應類的load方法之后

使用場景

在自定義實現load方法時,可以很安全地(線程安全)提前獲取其他毫無相關類消息,并且在消息還沒實現時,手動去實現該消息(也就是說load方法常常應用于Swizzling Method
操作)

initalize 簡介

在初始化類之前收到第一個消息,也就是說,對該類進行初始化時被調用

Discussion

  • 在程序中向類或者與其繼承的任何子類發送第一條消息前,runtime會以線程安全的方式來向類發起initialize消息。
  • 父類會在子類之前收到這條消息。如果子類沒有實現initialize方法,由于Runtime機制
    將按順序調用實現該類繼承而來的父類initialize方法,父類的initialize可能會被調用多次,針對這種情況解決方法代碼:
+ (void)initialize {
  if (self == [ClassName self]) {
    // ... do the initialization ...
  }
}

使用該方法注意點:

在一個線程安全的方式下進行初始化時,在不同的類initialize被調用的順序是不保證的,環,使用initialize方法時應盡量做少量且必要的工作。具體而言,initialize方法內部實現了同步鎖如果在方法內實現復雜的邏輯,容易會導致死鎖。因此,不應該依賴于initialize復雜的初始化,而應該限制它的簡單,可以使用init、new等方法實現初始化。

特別注意

initialize每個類只會被調用一次,如果你想為類或分類實現自定義初始化方法可以使用load方法進行Swizzling Method操作,后面會簡單講下FDTemplateLayoutCell框架如何使用Swizzling Method

Method Swizzling 簡介

  • 使用Method Swizzling可以改變現有的Selector的實現一項處理技術,方法的調用在Objective - C中運行時,通過改變Selector映射到該類的底層函數調度表,來修改方法的實現.
  • 在Objective-C實現擴展方法可以使用Category來覆蓋系統方法,系統會優先調用Category中的代碼,然后在調用原類中的代碼,如果我們在已有的Category想實現UITableViewDelegate/UITableViewSource代理方法,往往就會使用Method Swizzling,而且很輕松做到這一點,可以通過新建UITableView Category,在其分類使用+(load)Method Swizzling替換方法代理,例如調用reloadData等方法,FDTemplateLayoutCell框架就是這么做的。

Method Swizzling應用場景

  • 跟蹤視圖控制器ViewDidLoad、viewWillAppear 、viewDidAppear實現,如果多個ViewController重復使用代碼,可以在UIViewController分類在load方法中替換其實現方法

  • 實現UITableViewDelegate、UITableViewDataSource自定義代理方法,例如在編寫框架時,用到UITableViewDelegate、UITableViewDataSource代理,替換具體實現名稱

Swizzling應該總是在dispatch_once中執行

  • 因為Method Swizzling會改變全局狀態,需要提供一切預防措施給Runtime。GCD的dispatch_once原子性就是一個很好的預防措施,無論有多少個線程,確保代碼塊恰好執行一次。

  • Selectors, Methods, & Implementations
    在Objective - C中,實現 Selectors, Methods, Implementations是指在Runtime的特定方面,實現消息發送時往往會用到他們

Selectors(typedef struct objc_selector *SEL):用于表示在運行時方法的名稱。
Method(typedef struct objc_method Method
):表示類的方法
Implementation(typedef id (
IMP)(id, SEL, ...)
): 這個數據類型是指向一個指針函數實現方法

Method Swizzling使用

//
//  UIViewController+MethodSwizzlingCategoryExample.m
//  LoadInitalizeKVOKVCBlockExample
//
//  Created by lmj  on 16/6/8.
//  Copyright ? 2016年 linmingjun. All rights reserved.
//

#import "UIViewController+MethodSwizzlingCategoryExample.h"
#import <objc/runtime.h>
@implementation UIViewController (MethodSwizzlingCategoryExample)


+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken,^{
        Class class= [self class];
        
        SEL originalSelector = @selector(viewDidLoad);
        SEL swizzledSelector = @selector(swizzledViwDidLoad);
        // 通過class_getInstanceMethod()函數從當前class對象中的method list獲取method結構體
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        // 如果是類方法:
        // Class class = object_getClass((id)self);
        // ...
        // Method originalMethod = class_getClassMethod(class, originalSelector);
        // Method swizzledMethod = class_getClassMethod(class, swizzledSelector);
        
        // 使用class_addMethod()函數對Method Swizzling做了一層驗證,如果self沒有實現swizzledSelector交換的方法,會導致失敗
        BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        
        if (didAddMethod) { // 如果self沒有實現swizzledViwDidLoad方法,class_replaceMethod向對象所屬的類動態添加所需的selector:,如果swizzledSelector沒有實現,
            
            
            
            // class_replaceMethod,它有兩種不同的行為。當類中沒有想替換的原方法時,該方法會調用class_addMethod來為該類增加一個新方法,也因為如此,class_replaceMethod在調用時需要傳入types參數,而method_exchangeImplementations和method_setImplementation卻不需要。
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {// 通過class_addMethod()的驗證,如果self實現了swizzledViwDidLoad這個方法,class_addMethod()函數將會返回NO,進行交換了。
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
        
        
    });
    
}

#pragma mark - Method Swizzling

- (void)swizzledViwDidLoad {
    // 在swizzledViwDidLoad方法內部調用[self swizzledViwDidLoad];時,執行的是UIViewController的viewDidLoad方法。
    // 該方法調用 [self swizzledViwDidLoad]; 不會造成死循環;因為swizzledViwDidLoad已經被指定為viewDidLoad方法,所以這里實質調用的是[self viewDidLoad];方法,經過調試如果不使用這行代碼程序運行結果相同,但是 Mattt Thompson指出這行代碼是為了防止潛在的危險或麻煩,體現一個優秀程序員的良好素養.
    [self swizzledViwDidLoad];
    NSLog(@"swizzledViwDidLoad: %@", self);
}

@end

 SEL originalSelector = @selector(viewDidLoad);
 SEL swizzledSelector = @selector(swizzledViwDidLoad);

 Method originalMethod = class_getInstanceMethod(class, originalSelector)
 Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

1.通過class_getInstanceMethod()函數從當前class對象中的method list獲取method結構體

BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));

2.使用class_addMethod()函數對Method Swizzling做了一層驗證,如果ViewController沒有viewDidLoad(書寫過程可能寫成viewDidLoadd等錯誤),swizzledSelector交換的方法,會導致失敗,返回YES

method_exchangeImplementations(originalMethod, swizzledMethod);

3.通過class_addMethod()的驗證,如果self實現了swizzledViwDidLoad這個方法,class_addMethod()函數將會返回NO,進行交換了。

swizzledViwDidLoad方法內調用[self swizzledViwDidLoad]原因:

  • 在swizzledViwDidLoad方法內部調用[self swizzledViwDidLoad];時,執行的是UIViewController的viewDidLoad方法。
  • 該方法調用 [self swizzledViwDidLoad]時候不會造成死循環;因為swizzledViwDidLoad已經被指定為viewDidLoad方法,所以這里實質調用的是[self viewDidLoad];方法,經過調試如果不使用這行代碼程序運行結果相同,但是 Mattt Thompson指出這行代碼是為了防止潛在的危險或麻煩,體現一個優秀程序員的良好素養.

FDTemplateLayoutCell使用Method Swizzling

  • FDIndexPathHeightCacheInvalidation(UIViewTableView Category)

+ (void)load {
    // All methods that trigger height cache's invalidation(觸發高度緩存失效的所有方法)
    SEL selectors[] = {
        @selector(reloadData),
        @selector(insertSections:withRowAnimation:),
        @selector(deleteSections:withRowAnimation:),
        @selector(reloadSections:withRowAnimation:),
        @selector(moveSection:toSection:),
        @selector(insertRowsAtIndexPaths:withRowAnimation:),
        @selector(deleteRowsAtIndexPaths:withRowAnimation:),
        @selector(reloadRowsAtIndexPaths:withRowAnimation:),
        @selector(moveRowAtIndexPath:toIndexPath:)
    };
    // sizeof(selectors) / sizeof(SEL) =  (9(數組大小))* SEL  / SEL; 一種用C語言計算個數的常用的技巧
    for (NSUInteger index = 0; index < sizeof(selectors) / sizeof(SEL); ++index) {
        SEL originalSelector = selectors[index];
        
        SEL swizzledSelector = NSSelectorFromString([@"fd_" stringByAppendingString:NSStringFromSelector(originalSelector)]);
        Method originalMethod = class_getInstanceMethod(self, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector);
     
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}
  • 為什么在項目調用reloadData調用自動計算高度以及緩存,原因就在這里,在Objective-C實現擴展方法可以使用Category來覆蓋系統方法,系統會優先調用Category中的代碼,然后在調用原類中的代碼,如果我們在已有的Category想實現UITableViewDelegate/UITableViewSource代理方法,往往就會使用Method Swizzling,可以通過新建UITableView Category,在其分類使用+(load)Method Swizzling替換方法代理(內部計算cell高度,緩存高度),再執行項目當中UITableViewDelegate/UITableViewDataSource代理方法
  • 寫到這里,先寫其他內容,有空再對FDTemplateLayoutCell框架所有類的源碼進行分析,并且實現擴展

block

本章介紹了塊和變量之間的相互作用,包括內存管理

block變量的類型

在block作為對象代碼中,變量可以用五種不同的方式處理,其中有是三種標準類型的變量,如同一個函數:

  • 全局變量,包括靜態局部變量
  • 全局函數(在技術上不是變量)
  • 在函數內的局部變量

block還支持其他兩種類型的變量

  • 以__block修飾的變量,block代碼塊是可變的,任何引用快內的代碼內存會從棧區復制到堆區

  • 通過const來修飾的變量

以下規則適用于block中使用變量

  • 全局變量是可訪問的,包括在同一Scope(范圍)里的靜態變量

  • 傳遞給block參數都可以訪問(就像函數 的參數)

  • 局部棧區(非靜態)在同一Scope(范圍)里以const修飾的局部變量(在于block外的變量可以無縫地直接在block內部使用,而且在block內變量發生變化的不會體現到block外)

  • 局部變量聲明在該block的范圍內,它的行為完全像一個函數的局部變量,每次調用block時,都會對變量進行一次copy操作(從棧區Copy到堆區),這些變量可以作為常量或在block內作為引用變量

  • 下面的示例演示了局部非靜態變量的使用:

- (void)blockExample1 {

    int x = 123;
     
    void (^printXAndY)(int) = ^(int y) {
     
        printf("%d %d\n", x, y);
    };
     
    printXAndY(456); 

}
// prints: 123 456

局部非靜態變量試圖將一個的值賦給塊內作為一個新值會導致一個錯誤:

- (void)blockExample2 {

    int x = 123;
     
    void (^printXAndY)(int) = ^(int y) {
     
        x = x + y; // error
        printf("%d %d\n", x, y);
    };

}
// Error
  • 你可以指定一個可變的具有讀寫功能的輸入變量,通常?應用__block存儲類型來聲明變量。也可以通過與__block數據類型相似的、但他們又是排斥的數據類型來聲明,例如寄存器、自動、靜態局部變量,
    詳見地址:寄存器、自動、靜態關鍵字詳細說明

  • 要允許一個變量在block內改變,使用__block存儲類型來修飾變量,如果在棧區聲明的block,進行任何的拷貝操作造成超出frame的末尾存儲時,將會造成棧區的破壞
    下面的例子演示了在一個給定的詞法范圍內的多個塊可以同時使用一個共享變量

  • 作為一次優化,block開始存儲在棧區,可以使用Block_copy對block進行copy操作(或在Objective-C會進行一次copy操作(ARC機制)),變量將從棧區拷貝到堆區,因此,一個__block聲明的變量地址是由時間而改變的。在這里有兩__block變量進一步的限制:他們不能改變數組長度,并且不能包含C99可變長度的數組結構。

下面的例子說明了如何使用一個__block變量:

__block int x = 123; //  x lives in block storage
 
void (^printXAndY)(int) = ^(int y) {
 
    x = x + y;
    printf("%d %d\n", x, y);
};
printXAndY(456); // prints: 579 456
// x is now 579

下面的例子顯示了幾個類型的變量的相互作用:

對象和block變量

作為block變量提供Objective-C和C++對象和其他block的支持

Objective-C 對象

  • 當一個block被拷貝時,它會對block內使用的對象變量創建一個強引用。
  • 如果在一個方法中實現并使用一個block:

1.如果你通過引用訪問的實例變量,那么這個block會對self產生強引用。
2.如果你通過值來訪問實例變量,那么這個block會對這個變量本身產生強引用。

下面的例子說明了這兩種不同的情況

dispatch_async(queue, ^{
    // instanceVariable is used by reference, a strong reference is made to self
    doSomethingWithObject(instanceVariable);
});
 
 
id localVariable = instanceVariable;
dispatch_async(queue, ^{
    /*
      localVariable is used by value, a strong reference is made to localVariable
      (and not to self).
    */
    doSomethingWithObject(localVariable);
});


dispatch_async(queue, ^{
    // instanceVariable is used by reference, a strong reference is made to self
    doSomethingWithObject(instanceVariable);
});
 
 
id localVariable = instanceVariable;
dispatch_async(queue, ^{
    /*
      localVariable is used by value, a strong reference is made to localVariable
      (and not to self).
    */
    doSomethingWithObject(localVariable);
});

dispatch_async(queue, ^{

    // instanceVariable is used by reference, a strong reference is made to self
    doSomethingWithObject(instanceVariable);
});
 
 
id localVariable = instanceVariable;
dispatch_async(queue, ^{
    /*
      localVariable is used by value, a strong reference is made to localVariable
      (and not to self).
    */
    doSomethingWithObject(localVariable);
});

C ++對象

  • 一般在block內可以使用c++對象。

  • 在一個成員函數,成員變量和函數的引用是通過隱含的this指針,從而出現可變進口。
    指針,從而出現可變。 如果一個塊被復制,則有兩點考慮:

  • 如果你有一個__block
    存儲類的東西是一個基于堆棧的c++對象,那么平常copy
    使用構造函數。

  • 如果您使用任何其他的c++基于堆棧的對象在一塊,它必須有一個const copy
    構造函數。 然后復制使用c++對象的構造函數。

  • 如果你有一個__block存儲器修飾的類而且是基于堆棧的C++對象,則通常使用Copy構造函數。如果你在block內使用其他基于堆棧C++對象,它必須有一個const拷貝構造函數。然后,對C ++對象的構造函數使用一次Copy操作。

調用一個block

如果你聲明一個block作為一個變量,可以使用它作為一個函數,如下兩個例子的使用

int (^oneFrom)(int) = ^(int anInt) {
    return anInt - 1;
};
 
printf("1 from 10 is %d", oneFrom(10));
// Prints "1 from 10 is 9"
 
float (^distanceTraveled)(float, float, float) =
                         ^(float startingSpeed, float acceleration, float time) {
 
    float distance = (startingSpeed * time) + (0.5 * acceleration * time * time);
    return distance;
};
 
float howFar = distanceTraveled(0.0, 9.8, 1.0);
printf:  howFar:4.9

通常,你將一個block作為一個函數或一個方法的參數傳遞給一個block。在這些情況下,通常創建一個“inline(內聯)”塊。

使用block作為函數參數

你可以通過一個block作為一個函數參數,如同其他參數一樣。然而,在很多情況下,你不需要聲明block;相反,你只需簡單地實現它們,然后使用inline(內聯)它們作為一個參數。下面的例子使用了qsort_b功能。qsort_b類似于標準的qsort_r功能,以最后參數作為一個block。

char *myCharacters[3] = { "TomJohn", "George", "Charles Condomine" };
 
qsort_b(myCharacters, 3, sizeof(char *), ^(const void *l, const void *r) {
    char *left = *(char **)l;
    char *right = *(char **)r;
    return strncmp(left, right, 1);
});
// Block implementation ends at "}"
 
// myCharacters is now { "Charles Condomine", "George", "TomJohn" }

下面的例子演示了如何使用block與dispatch_apply功能。 dispatch_apply聲明如下:

void dispatch_apply(size_t iterations, dispatch_queue_t queue, void (^block)(size_t));

功能塊用于提交多次調用調度隊列。它需要三個參數,第一個指定的迭代次數來執行;二是指定一個被提交的隊列,第三個是block本身,而這個block本身又需要一個單一的參數作為當前的迭代次數。
可以使用dispatch_apply打印出迭代索引

#include <dispatch/dispatch.h>
size_t count = 10;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
 
dispatch_apply(count, queue, ^(size_t i) {
    printf("%u\n", i);
});
printf: 0
        2
        1
        3
        4
        5
        6
        7
        8
        9

使用block作為方法參數

Cocoa提供了一種方法使用block的數量。 你通過block作為一個方法的參數就像任何其他參數。

NSArray *array = @[@"A", @"B", @"C", @"A", @"B", @"Z", @"G", @"are", @"Q"];
NSSet *filterSet = [NSSet setWithObjects: @"A", @"Z", @"Q", nil];
 
BOOL (^test)(id obj, NSUInteger idx, BOOL *stop);
 
test = ^(id obj, NSUInteger idx, BOOL *stop) {
 
    if (idx < 5) {
        if ([filterSet containsObject: obj]) {
            return YES;
        }
    }
    return NO;
};
 
NSIndexSet *indexes = [array indexesOfObjectsPassingTest:test];
 
NSLog(@"indexes: %@", indexes);
 
/*
Output:
indexes: <NSIndexSet: 0x10236f0>[number of indexes: 2 (in 2 ranges), indexes: (0 3)]
*/

拷貝block方法

你可以copy和release模塊:
也可以使用C函數,如下兩個函數

Block_copy();

Block_release();

為了避免內存泄漏,必須始終平衡Block_copy()
與Block_release()兩種操作

避免兩種block書寫

避免block在for/while循環和判斷語句內 (that is, ^{ ... }),,表示該block的是局部數據結構的地址,棧的局部數據結構的范圍是封閉的復合語句,所以你應該避免在下面的例子中顯示的模式:

面試提問:

load 與 initialize 的區別
答:load :
(1)load方法在這個文件被程序裝載時調用 (2)如果一個類實現了load
方法,在調用這個方法前會首先調用父類的load (3)如果一個類沒有實現load
方法,那么就不會調用它父類的load方法(4)添加一個子類的分類時,在分類添加load方法時,調用順序parent->child->category(5)在Compile Sources中,文件的排放順序就是其裝載順序,自然也就是load方法調用的順序(除了父子類關系的描述)
(6)load方法是線程安全的,它內部使用了鎖,所以我們應該避免線程阻塞在load
方法中。
initialize:(1)實例化一個對象時調用,只會調用一次(2)在initialize
方法內部也會調用父類的方法,即使子類沒有實現initialize方法,也會調用父類的方法

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,646評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,595評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,560評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,035評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,814評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,224評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,444評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,988評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,804評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,998評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,237評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,665評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,927評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,706評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,993評論 2 374

推薦閱讀更多精彩內容