Objective-C 運行時(Runtime)解析

Objective-C基于C語言加入了面向對象特性和消息轉發,Objective-C 的消息轉發需要運行時系統來動態創建類和對象,從而進行消息發送和轉發.

objc_msgSend

C語言在編譯階段決定運行時調用的函數,會生成直接調用函數的指令,相當于靜態綁定.作為C語言的超集所有調用的函數直到運行期才能確定,對象收到消息后,具體調用哪個方法由運行期決定,甚至可以在運行的時候改變,所以Objective-C也被稱之為動態語言.

Objective-C正常調用實例對象方法:

    [course fetchData];

以上代碼會被轉換成標準的C語言函數調用,等價于以下代碼:

Course *courseClass = objc_msgSend([Course class], sel_registerName("alloc"));
    courseClass = objc_msgSend(courseClass, sel_registerName("init"));
    SEL sel = sel_registerName("fetchData");
    objc_msgSend(courseClass,sel);

頭文件導入:

#import <objc/message.h>
#import <objc/runtime.h>

編譯出錯,需要修改設置選項:


image.png

objc_msgSend定義如下:

/** 
 * Sends a message with a simple return value to an instance of a class.
 * 
 * @param self A pointer to the instance of the class that is to receive the message.
 * @param op The selector of the method that handles the message.
 * @param ... 
 *   A variable argument list containing the arguments to the method.
 * 
 * @return The return value of the method.
 * 
 * @note When it encounters a method call, the compiler generates a call to one of the
 *  functions \c objc_msgSend, \c objc_msgSend_stret, \c objc_msgSendSuper, or \c objc_msgSendSuper_stret.
 *  Messages sent to an object’s superclass (using the \c super keyword) are sent using \c objc_msgSendSuper; 
 *  other messages are sent using \c objc_msgSend. Methods that have data structures as return values
 *  are sent using \c objc_msgSendSuper_stret and \c objc_msgSend_stret.
 */
OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0);
/** 

SEL是映射到方法的C字符串,可以通過Objc 編譯器命令@selector()或者 Runtime 系統的sel_registerName函數來獲得一個SEL類型的方法選擇器.

所以下面的代碼和的代碼執行結果一致:

Course *courseClass = objc_msgSend([Course class], @selector(alloc));
    courseClass = objc_msgSend(courseClass, @selector(init));
    SEL sel = @selector(fetchData);
    objc_msgSend(courseClass,sel);

Objective-C類是由Class類型來表示的,它是一個指向objc_class結構體的指針,objc_class定義如下:

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

類對象也是一個實例,類對象中的isa指針指向的是元類(meta class),當我們進行類方法查詢的時候就會通過isa指針找到元類查詢.元類最終會指向根元類,形成查找閉環.

super_class指向父類,如果類對象本身無法實現,可以通過父類進行查找實現.

如果每次方法查詢都需要通過methodlist來查找實現,那么效率很低,所以查找過一次會通過cache保存其實現,提升方法查找效率.

struct objc_cache {
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
    unsigned int occupied                                    OBJC2_UNAVAILABLE;
    Method buckets[1]                                        OBJC2_UNAVAILABLE;
};
經典查找.jpg

消息轉發

Objective-C中經常遇到找到實現方法的問題,解決這個問題有三種方案,動態方法解析,重定向接受者和完整的消息轉發.

動態方法解析,可以通過系統提供resolveClassMethod和resolveInstanceMethod來實現.

+ (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
+ (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);

Course類中定義一個calculatData方法,但是不實現.

@interface Course : NSObject

- (void)fetchData;

- (void)calculatData;

@end

調用:

Course *course = [Course new];
    [course calculatData];

報錯信息:

[Course calculatData]: unrecognized selector sent to instance 0x608000013570
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Course calculatData]: unrecognized selector sent to instance 0x608000013570'

重寫resolveInstanceMethod方法:

+ (BOOL)resolveClassMethod:(SEL)sel {
    return [super resolveClassMethod:sel];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    
    if (sel == @selector(calculatData)) {
        class_addMethod([self class], sel, (IMP)resolveMethodIMP, "v@:");
        return YES;
    }
    
    return [super resolveInstanceMethod:sel];
}
void resolveMethodIMP(id self, SEL _cmd) {
    NSLog(@"resolveInstanceMethod---resolveMethodIMP");
}

重置接收者,當前接受者如果不能處理對象,可以交由其他對象處理,類似于web端的重定向,也可以通過這種組合的方式實現多繼承.

定義未實現的方法:

- (void)fetchMethod;

定義新的接收者:

@implementation Teacher

- (void)fetchMethod {
    NSLog(@"Teacher---fetchMethod");
}

@end

forwardingTargetForSelector實現:

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(fetchMethod)) {
        return [Teacher new];
    }
    return [super forwardingTargetForSelector:aSelector];
}

調用:

Course *course = [Course new];
    [course calculatData];
    
    [course fetchMethod];

結果:

Teacher---fetchMethod

最后一步保證是完成的消息轉發機制,需要重寫methodSignatureForSelector和forwardInvocation方法.

- (void)forwardInvocation:(NSInvocation *)anInvocation OBJC_SWIFT_UNAVAILABLE("");
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector OBJC_SWIFT_UNAVAILABLE("");

Course類中的方法簽名重寫和執行:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"%@",NSStringFromSelector(_cmd));
    NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
    
    if (!signature) {
        if ([Teacher instancesRespondToSelector:aSelector]) {
            signature = [Teacher instanceMethodSignatureForSelector:aSelector];
        }
    }
    
    return signature;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"%@",NSStringFromSelector(_cmd));
    if ([Teacher instanceMethodForSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:[Teacher new]];
    } else {
        [super forwardInvocation:anInvocation];
    }
}

Teacher類中重寫forwardMethod方法:

- (void)forwardMethod {
    NSLog(@"Teacher---forwardMethod");
}

方法調用:

 Course *course = [Course new];
    [course forwardMethod];

屬性與方法

Runtime中最常見的方法就是屬性和方法的獲取,屬性獲取通過class_copyPropertyList實現.

/** 
 * Describes the properties declared by a class.
 * 
 * @param cls The class you want to inspect.
 * @param outCount On return, contains the length of the returned array. 
 *  If \e outCount is \c NULL, the length is not returned.        
 * 
 * @return An array of pointers of type \c objc_property_t describing the properties 
 *  declared by the class. Any properties declared by superclasses are not included. 
 *  The array contains \c *outCount pointers followed by a \c NULL terminator. You must free the array with \c free().
 * 
 *  If \e cls declares no properties, or \e cls is \c Nil, returns \c NULL and \c *outCount is \c 0.
 */
OBJC_EXPORT objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
 Course *course = [[Course alloc] init];
    Class  class = [course class];
    
    unsigned int procount = 0;
    objc_property_t * properties = class_copyPropertyList(class, &procount);
    for (int i = 0; i < procount; i++) {
        objc_property_t property = properties[i];
        const char *propertyName =  property_getName(property);
        NSLog(@"屬性名稱: %s", propertyName);
    }
    free(properties);

class_copyIvarList獲取對象的變量:

/** 
 * Describes the instance variables declared by a class.
 * 
 * @param cls The class to inspect.
 * @param outCount On return, contains the length of the returned array. 
 *  If outCount is NULL, the length is not returned.
 * 
 * @return An array of pointers of type Ivar describing the instance variables declared by the class. 
 *  Any instance variables declared by superclasses are not included. The array contains *outCount 
 *  pointers followed by a NULL terminator. You must free the array with free().
 * 
 *  If the class declares no instance variables, or cls is Nil, NULL is returned and *outCount is 0.
 */
OBJC_EXPORT Ivar *class_copyIvarList(Class cls, unsigned int *outCount) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
 unsigned int varcount = 0;
    Ivar *varlist = class_copyIvarList(class, &varcount);
    for (int i = 0; i < varcount; i++) {
        Ivar var = varlist[i];
        const char *varname =  ivar_getName(var);
        NSLog(@"變量名稱: %s", varname);
    }
    free(varlist);

class_copyMethodList獲取方法列表:

/** 
 * Describes the instance methods implemented by a class.
 * 
 * @param cls The class you want to inspect.
 * @param outCount On return, contains the length of the returned array. 
 *  If outCount is NULL, the length is not returned.
 * 
 * @return An array of pointers of type Method describing the instance methods 
 *  implemented by the class—any instance methods implemented by superclasses are not included. 
 *  The array contains *outCount pointers followed by a NULL terminator. You must free the array with free().
 * 
 *  If cls implements no instance methods, or cls is Nil, returns NULL and *outCount is 0.
 * 
 * @note To get the class methods of a class, use \c class_copyMethodList(object_getClass(cls), &count).
 * @note To get the implementations of methods that may be implemented by superclasses, 
 *  use \c class_getInstanceMethod or \c class_getClassMethod.
 */
OBJC_EXPORT Method *class_copyMethodList(Class cls, unsigned int *outCount) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
 unsigned int methodcount = 0;
    Method *methods = class_copyMethodList(class, &methodcount);
    for (int i = 0; i < methodcount; i++) {
        Method method = methods[i];
        SEL method_name = method_getName(method);
        NSLog(@"方法名稱:%@",NSStringFromSelector(method_name));
    }
    free(methods);

方法交換

Objective-C中方法交換(Method Swizzling)發生在運行時,在運行時將兩個Method進行交換,可以通過方法交換的特性實現AOP(面向切面編程),可以在埋點統計中執行.

交換UIViewController的viewDidLoad方法,執行完新的方法之后,繼續執行交換之前的方法,交換代碼在load方法中執行.Method Swizzling本質上就是對IMP和SEL進行交換.

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(viewDidLoad);
        SEL swizzledSelector = @selector(fe_viewDidload);
        
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        BOOL didAddMethod = class_addMethod(class,
                                            originalSelector,
                                            method_getImplementation(swizzledMethod),
                                            method_getTypeEncoding(swizzledMethod));
        if (didAddMethod) {
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (void)fe_viewDidload {
    NSLog(@"fe_viewDidload執行");
    [self fe_viewDidload];
}

原始方法:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    NSLog(@"viewDidload執行");
}

執行結果:

fe_viewDidload執行
viewDidload執行

isa-swizzling

iOS開發中常用的KVO(Key-Value Observing)鍵值觀察是典型的isa
swizzling,官方文檔介紹如下:

Automatic key-value observing is implemented using a technique called isa-swizzling.
The isa
pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.
When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
You should never rely on the isa
pointer to determine class membership. Instead, you should use the class
method to determine the class of an object instance.

鍵值觀察的主要方法:

@interface NSObject(NSKeyValueObserverRegistration)

/* Register or deregister as an observer of the value at a key path relative to the receiver. The options determine what is included in observer notifications and when they're sent, as described above, and the context is passed in observer notifications as described above. You should use -removeObserver:forKeyPath:context: instead of -removeObserver:forKeyPath: whenever possible because it allows you to more precisely specify your intent. When the same observer is registered for the same key path multiple times, but with different context pointers each time, -removeObserver:forKeyPath: has to guess at the context pointer when deciding what exactly to remove, and it can guess wrong.
*/
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context NS_AVAILABLE(10_7, 5_0);
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

@end

KVO在調用addObserver方法之后,將isa指向到另外一個類,在新類里面重寫被觀察的對象的class,setter,dealloc,_isKVOA方法.

定義Teacher類:

@interface Teacher : NSObject

@property (copy, nonatomic) NSString *teacherName;

- (void)fetchMethod;

- (void)forwardMethod;

@end
Teacher *teacher = [Teacher new];
    
    NSLog(@"Teacher->isa:%@",object_getClass(teacher));
    NSLog(@"Teacher class:%@",[teacher class]);
    NSLog(@"Teacher方法列表 = %@",ClassMethodNames(object_getClass(teacher)));
    [teacher addObserver:self forKeyPath:@"courseName" options:NSKeyValueObservingOptionNew context:nil];
    
    NSLog(@"Teacher->isa:%@",object_getClass(teacher));
    NSLog(@"Teacher class:%@",[teacher class]);
    NSLog(@"Teacher方法列表 = %@",ClassMethodNames(object_getClass(teacher)));
    
    self.teacher = teacher;
static NSArray * ClassMethodNames(Class c)
{
    NSMutableArray * array = [NSMutableArray array];
    unsigned int methodCount = 0;
    Method * methodList = class_copyMethodList(c, &methodCount);
    unsigned int i;
    for(i = 0; i < methodCount; i++) {
        [array addObject: NSStringFromSelector(method_getName(methodList[i]))];
    }
    
    free(methodList);
    return array;
}

執行結果isa指向了新的類NSKVONotifying_Teacher.

Teacher->isa:Teacher
Teacher class:Teacher
Teacher方法列表 = (
    fetchMethod,
    forwardMethod,
    teacherName,
    "setTeacherName:",
    ".cxx_destruct"
)
Teacher->isa:NSKVONotifying_Teacher
Teacher class:Teacher
Teacher方法列表 = (
    class,
    dealloc,
    "_isKVOA"
)

重寫setter方法:

/* Given a key that identifies a property (attribute, to-one relationship, or ordered or unordered to-many relationship), send -observeValueForKeyPath:ofObject:change:context: notification messages of kind NSKeyValueChangeSetting to each observer registered for the key, including those that are registered with other objects using key paths that locate the keyed value in this object. Invocations of these methods must always be paired.

The change dictionaries in notifications resulting from use of these methods contain optional entries if requested at observer registration time:
    - The NSKeyValueChangeOldKey entry, if present, contains the value returned by -valueForKey: at the instant that -willChangeValueForKey: is invoked (or an NSNull if -valueForKey: returns nil).
    - The NSKeyValueChangeNewKey entry, if present, contains the value returned by -valueForKey: at the instant that -didChangeValueForKey: is invoked (or an NSNull if -valueForKey: returns nil).
*/
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;

關聯對象

關聯對象在類別中比較頻繁,假設擴展UITextField,新加屬性placeHolderColor.

@property (strong,nonatomic) UIColor *placeHolderColor;
-(UIColor *)placeHolderColor {
    return objc_getAssociatedObject(self, placeHolderColorKey);
}

-(void)setPlaceHolderColor:(UIColor *)placeHolderColor {
    [self setValue:placeHolderColor forKeyPath:@"_placeholderLabel.textColor"];
    objc_setAssociatedObject(self,placeHolderColorKey, placeHolderColor, OBJC_ASSOCIATION_RETAIN);
}

歸檔和解檔

Objective-C中實現自定義對象的歸檔和解檔需要實現NSCoding協議.

@protocol NSCoding

- (void)encodeWithCoder:(NSCoder *)aCoder;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder; // NS_DESIGNATED_INITIALIZER

@end

以Teacher類為例:

// 解檔
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    NSLog(@"initWithCoder");
    self = [super init];
    if (self) {
        self.teacherName = [aDecoder decodeObjectForKey:@"teacherName"];
        self.age = [[aDecoder decodeObjectForKey:@"age"] integerValue];
    }
    return self;
}

// 歸檔
- (void)encodeWithCoder:(NSCoder *)aCoder {
    
    NSLog(@"encodeWithCoder");
    [aCoder encodeObject:self.teacherName forKey:@"teacherName"];
    [aCoder encodeObject:@(self.age) forKey:@"age"];
}

歸檔和解檔保存和讀取:

- (IBAction)saveAction:(UIButton *)sender {
    Teacher *teacher = [Teacher new];
    teacher.teacherName = @"FlyElephant";
    teacher.age = 27;
    
    NSString *filePath = [NSHomeDirectory() stringByAppendingPathComponent:@"teacher.archiver"];
    BOOL success = [NSKeyedArchiver archiveRootObject:teacher toFile:filePath];
    if(success){
        NSLog(@"歸檔保存成功");
    }
}

- (IBAction)readAction:(UIButton *)sender {
    NSString *filePath = [NSHomeDirectory() stringByAppendingPathComponent:@"teacher.archiver"];
    Teacher *teacher = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
    NSLog(@"Teacher:%@---age:%ld",teacher.teacherName,teacher.age);
}
歸檔保存成功
Teacher:FlyElephant---age:27

屬性如果過多會寫的很煩躁,通過runtime來解決:

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super init];
    if (self) {
        unsigned int varCount = 0;
        Ivar *vars = class_copyIvarList([self class], &varCount);
        for (int i = 0; i < varCount; i ++) {
            Ivar var = vars[i];
            const char *name = ivar_getName(var);
            NSString *key = [NSString stringWithUTF8String:name];
            id value = [aDecoder decodeObjectForKey:key];
            [self setValue:value forKey:key];
        }
    }
    return self;
}

// 歸檔
- (void)encodeWithCoder:(NSCoder *)aCoder{
    unsigned int varCount = 0;
    Ivar *vars = class_copyIvarList([self class], &varCount);
    for (int i = 0; i < varCount; i ++) {
        Ivar var = vars[i];
        const char *name = ivar_getName(var);
        NSString *key = [NSString stringWithUTF8String:name];
        
        id value = [self valueForKey:key];
        [aCoder encodeObject:value forKey:key];
    }
}

同樣的Runtime可以通過屬性和變量的讀取,實現字典和模型之間的轉換,簡易版比較容易。

參考鏈接:
動態綁定、objc_msgSend、消息轉發機制
Objective-C Runtime 運行時之一:類與對象
Objective-C Runtime Programming Guide
KVO官方文檔
神經病院Objective-C Runtime出院第三天——如何正確使用Runtime

http://yulingtianxia.com/blog/2014/11/05/objective-c-runtime/

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

推薦閱讀更多精彩內容