24. AOP面向切面编程

AOP(Aspect Oriented Programming)面向切面编程

相比传统的OOP来说,OOP的特点在于它可以很好的将系统横向分为很多个模块(比如通讯录模块,聊天模块,发现模块),每个子模块可以横向的衍生出更多的模块,用于更好的区分业务逻辑。而AOP其实相当于是纵向的分割系统模块,将每个模块里的公共部分提取出来(即那些与业务逻辑不相关的部分,如日志,用户行为等等),与业务逻辑相分离开来,从而降低代码的耦合度。

AOP主要是被使用在日志记录,性能统计,安全控制,事务处理,异常处理几个方面。由于本人并不是位大神(正在成长中),所以目前只在项目里用到了日志记录和事物处理这两个方面,另外几个方面会在以后陆陆续续更新。

注意:AOP和OOP一样,并不是一种技术,而是一种编程思想,所有实现了AOP编程思想的都算是AOP的实现。

在iOS中我们通常使用Method Swizzling(俗称iOS黑魔法)来实现AOP,Method Swizzling其实就是一种在Runtime的时候把一个方法的实现与另一个方法的实现互相替换。具体详见Method Swizzling

Aspects 一个基于Objective-C的AOP开发框架,封装了 Runtime ,是我们平时比较常用到实现Method Swizzling的一个类库,它提供了如下两个API:

typedef NS_OPTIONS(NSUInteger, AspectOptions) {
    AspectPositionAfter   = 0,            /// Called after the original implementation (default)
    AspectPositionInstead = 1,            /// Will replace the original implementation.
    AspectPositionBefore  = 2,            /// Called before the original implementation.
    AspectOptionAutomaticRemoval = 1 << 3 /// Will remove the hook after the first execution.
};

/// Adds a block of code before/instead/after the current `selector` for a specific class.
///
/// @param block Aspects replicates the type signature of the method being hooked.
/// The first parameter will be `id<AspectInfo>`, followed by all parameters of the method.
/// These parameters are optional and will be filled to match the block signature.
/// You can even use an empty block, or one that simple gets `id<AspectInfo>`.
///
/// @note Hooking static methods is not supported.
/// @return A token which allows to later deregister the aspect.
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;

/// Adds a block of code before/instead/after the current `selector` for a specific instance.
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;

AOP iOS中的事务处理之版本控制

我之前维护过一个项目Toast系列(Mac下一个比较有名的刻盘软件),它有很多的版本比如Pro版本,LT(lite)版本。顾名思义LT其实就是关闭了很多功能的一个版本,很多对应的事件在LT不会被触发,所以为了阻止事件的触发,每个用户事件中都会出现一段如下的宏判断

- (void)detailBottomBtnEvent:(id)sender {
//if we not use AOP, we must write this code in project
#ifdef LITE_VERSION
    //do nothing
#else
   //do all thing
#endif
}

这显然不是我们想要看到的结果,每个用户事件里都会去判断LT_VERSION的宏,然后在做对应的事件处理。LT的版本不是一个主版本,我们的业务逻辑里主要还是需要触发用户对应的事件,所以这个时候我们就可以用到AOP的思想

//
//  AppDelegate+LiteEvent.m
//  AOPTransactionIntactDemo
//
//  Created by wuyike on 16/5/19.
//  Copyright © 2016年 bongmi. All rights reserved.
//

#import "AppLiteDelegate+LiteEvent.h"

#import "Aspects.h"

typedef void (^AspectHandlerBlock)(id<AspectInfo> aspectInfo);

@implementation AppLiteDelegate (LiteEvent)

- (void)setLiteEvent {
#ifdef LITE_VERSION

    NSDictionary *configs = @{
                             @"AOPTopViewController": @{
                                     UserTrackedEvents: @[
                                             @{
                                                 UserEventName: @"detailBtn",
                                                 UserEventSelectorName: @"detailTopBtnEvent:",
                                                 UserEventHandlerBlock: ^(id<AspectInfo> aspectInfo) {
                                                     NSLog(@"Top detailBtn clicked, this is lite version");
                                                 },
                                                 },
                                             ],
                                     },

                             @"AOPBottomViewController": @{
                                     UserTrackedEvents: @[
                                             @{
                                                 UserEventName: @"detailBtn",
                                                 UserEventSelectorName: @"detailBottomBtnEvent:",
                                                 UserEventHandlerBlock: ^(id<AspectInfo> aspectInfo) {
                                                     NSLog(@"Bottom detailBtn clicked this is lite version");
                                                 },
                                                 },
                                             ],
                                     },

                             @"AOPLeftViewController": @{
                                     UserTrackedEvents: @[
                                             @{
                                                 UserEventName: @"detailBtn",
                                                 UserEventSelectorName: @"detailLeftBtnEvent:",
                                                 UserEventHandlerBlock: ^(id<AspectInfo> aspectInfo) {
                                                     NSLog(@"Left detailBtn clicked this is lite version");
                                                 },
                                                 },
                                             ],
                                     },

                             @"AOPRightViewController": @{
                                     UserTrackedEvents: @[
                                             @{
                                                 UserEventName: @"detailBtn",
                                                 UserEventSelectorName: @"detailRightBtnEvent:",
                                                 UserEventHandlerBlock: ^(id<AspectInfo> aspectInfo) {
                                                     NSLog(@"Right detailBtn clicked this is lite version");
                                                 },
                                                 },
                                             ],
                                     },
                             };

    for (NSString *className in configs) {
        Class clazz = NSClassFromString(className);
        NSDictionary *config = configs[className];

        if (config[UserTrackedEvents]) {
            for (NSDictionary *event in config[UserTrackedEvents]) {
                SEL selekor = NSSelectorFromString(event[UserEventSelectorName]);
                AspectHandlerBlock block = event[UserEventHandlerBlock];

                [clazz aspect_hookSelector:selekor
                               withOptions:AspectPositionInstead
                                usingBlock:^(id<AspectInfo> aspectInfo) {
                                    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                                        block(aspectInfo);
                                    });
                                  } error:NULL];

            }
        }
    }
#endif
}

我们有Top,Buttom,Left,Right四个ViewController,每个ViewController中都有一个对应的用户触发事件,我们只需要在LT版本下替换对应的事件就可以,每个模块的业务逻辑不需要任何改动。

Demo 下载

AOP iOS中的事务处理之安全可变容器

OC中任何以NSMutable开头的类都是可变容器,它们一般都具有(insert 、remove、replace)等操作,所以我们经常需要判断容器是否为空,以及指针越界等问题。为了避免我们在每次操作这些容器的时候都去判断,一般有以下几种解决方法:

  1. 派生类
  2. Category
  3. Method Swizzling

使用派生类肯定不是好的方法,Category可以解决我们的问题,但是导致项目中所有用到容器操作的地方都需要显示的调用我们新加的方法,所以也不是很优雅。所以这个时候用Method Swizzling就是一个不错的选择。

#import "NSMutableArray+SafeArray.h"
#import <objc/runtime.h>

@implementation NSMutableArray (SafeArray)

+ (void)load {
    [[self class] swizzleMethod:@selector(addObject:) withMethod:@selector(safeAddObject:)];
    [[self class] swizzleMethod:@selector(objectAtIndex:) withMethod:@selector(safeObjectAtIndex:)];
    [[self class] swizzleMethod:@selector(insertObject:atIndex:) withMethod:@selector(safeInsertObject:atIndex:)];
    [[self class] swizzleMethod:@selector(removeObjectAtIndex:) withMethod:@selector(safeRemoveObjectAtIndex:)];
    [[self class] swizzleMethod:@selector(replaceObjectAtIndex:withObject:) withMethod:@selector(safeReplaceObjectAtIndex:withObject:)];
    NSLog(@"%@ %@", @"SafeArray", [self class]);
}

#pragma mark - magic

- (void)safeAddObject:(id)anObject {
    //do safe operate
    if (anObject) {
        [self safeAddObject:anObject];
    } else {
        NSLog(@"safeAddObject: anObject is nil");
    }
}

- (id)safeObjectAtIndex:(NSInteger)index {
    //do safe operate
    if (index >= 0 && index <= self.count) {
        return [self safeObjectAtIndex:index];
    }
    NSLog(@"safeObjectAtIndex: index is invalid");
    return nil;
}

- (void)safeInsertObject:(id)anObject
                 atIndex:(NSUInteger)index {
   //do safe operate
    if (anObject && index >= 0 && index <= self.count) {
        [self safeInsertObject:anObject atIndex:index];
    } else {
        NSLog(@"safeInsertObject:atIndex: anObject or index is invalid");
    }
}

- (void)safeRemoveObjectAtIndex:(NSUInteger)index {
  //do safe operate
    if (index >= 0 && index <= self.count) {
        [self safeRemoveObjectAtIndex:index];
    } else {
        NSLog(@"safeRemoveObjectAtIndex: index is invalid");
    }
}

- (void)safeReplaceObjectAtIndex:(NSUInteger)index
                      withObject:(id)anObject {
   //do safe operate
    if (anObject && index >= 0 && index <= self.count) {
        [self safeReplaceObjectAtIndex:index withObject:anObject];
    } else {
        NSLog(@"safeReplaceObjectAtIndex:withObject: anObject or index is invalid");
    }
}

- (void)swizzleMethod:(SEL)origSelector
           withMethod:(SEL)newSelector {
    Class class = [self class];

    Method originalMethod = class_getInstanceMethod(class, origSelector);
    Method swizzledMethod = class_getInstanceMethod(class, newSelector);

    BOOL didAddMethod = class_addMethod(class,
                                        origSelector,
                                        method_getImplementation(swizzledMethod),
                                        method_getTypeEncoding(swizzledMethod));
    if (didAddMethod) {
        class_replaceMethod(class,
                            newSelector,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

以上就是我用AOP思想在事件处理方面的两个具体的应用。

AOP iOS中的日志记录

通常我们会在项目中收集用户的日志,以及用户行为,以用来分析Bug,以及提升产品质量。项目往往包含很多的模块,以及下面会有更多的子模块,所以如果把这些操作具体加载每个事件中,显然这种做法是不可取的,第一所有收集用户行为的操作不属于业务逻辑范畴,我们不需要分散到各个业务中。第二这种方式的添加不利于后期维护,而且改动量是巨大的。所以这里使用上面提到的版本控制事件处理相同的方式,这里抄袭一个Demo

- (void)setupLogging
{
    NSDictionary *config = @{
        @"MainViewController": @{
              GLLoggingPageImpression: @"page imp - main page",
              GLLoggingTrackedEvents: @[
                      @{
                          GLLoggingEventName: @"button one clicked",
                          GLLoggingEventSelectorName: @"buttonOneClicked:",
                          GLLoggingEventHandlerBlock: ^(id<AspectInfo> aspectInfo) {
                              NSLog(@"button one clicked");
                          },
                        },
                      @{
                          GLLoggingEventName: @"button two clicked",
                          GLLoggingEventSelectorName: @"buttonTwoClicked:",
                          GLLoggingEventHandlerBlock: ^(id<AspectInfo> aspectInfo) {
                              NSLog(@"button two clicked");
                          },
                        },
                      ],
        },

        @"DetailViewController": @{
              GLLoggingPageImpression: @"page imp - detail page",
        }
    };

    [GLLogging setupWithConfiguration:config];
}

typedef void (^AspectHandlerBlock)(id<AspectInfo> aspectInfo);

+ (void)setupWithConfiguration:(NSDictionary *)configs
{
    // Hook Page Impression
    [UIViewController aspect_hookSelector:@selector(viewDidAppear:)
                              withOptions:AspectPositionAfter
                               usingBlock:^(id<AspectInfo> aspectInfo) {
                                   dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                                       NSString *className = NSStringFromClass([[aspectInfo instance] class]);
                                       NSString *pageImp = configs[className][GLLoggingPageImpression];
                                       if (pageImp) {
                                           NSLog(@"%@", pageImp);
                                       }
                                   });
                               } error:NULL];

    // Hook Events
    for (NSString *className in configs) {
        Class clazz = NSClassFromString(className);
        NSDictionary *config = configs[className];

        if (config[GLLoggingTrackedEvents]) {
            for (NSDictionary *event in config[GLLoggingTrackedEvents]) {
                SEL selekor = NSSelectorFromString(event[GLLoggingEventSelectorName]);
                AspectHandlerBlock block = event[GLLoggingEventHandlerBlock];

                [clazz aspect_hookSelector:selekor
                               withOptions:AspectPositionAfter
                                usingBlock:^(id<AspectInfo> aspectInfo) {
                                    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                                        block(aspectInfo);
                                    });
                                } error:NULL];

            }
        }
    }
}

我们通过上述的操作可以在每个用户事件触发后都追加一个用户的行为记录,同时又不需要修改业务逻辑。

总结

这里反馈一个关于Aspect的问题,貌似Aspect不支持不同类中含有相同名称的方法时,会出现不能正确替换方法的情况,详细可以参见https://github.com/steipete/Aspects/issues/48 https://github.com/steipete/Aspects/issues/24

作者:北辰明
链接://www.greatytc.com/p/f58a9e4be184
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

统计打点是 App 开发里很重要的一个环节,App 的运行状态、用户的各种行为等都需要打点,有不少关于统计的第三方库(如友盟统计)。但是,如果要求在整个项目的所有button里统计用户的点击事件,假如一个项目里面有1000个button,你就要设1000个地方设置统计代码,显然不科学。
有没有一种办法在一个地方进行统计打点,检测整个应用的点击事件?

方案一:使用Runtime的方式追踪点击的按钮

特点:需要对每个button进行tag编号,对手势点击、tableView的点击要单独配置,比较繁琐

方案二:使用面向切面编程AOP对按钮或者页面进行追踪(无需在任何详情页面中做相应配置)

特点:

1、在不修改源代码的情况下,通过运行时给程序添加统一功能的技术,可以用作日志记录,性能统计等
2、无需对每个button进行tag编号,创建button后只需在新建的plist中配置button对应的方法名和对应的事件 ID就行
3、适用于Tap点击手势,使用时设置事件ID,和button的使用方法一样
4、button不支持直接在block里面写事件的方式,但可以在block里面调用方法或者需要统一写成下面的方式

[button addTarget:self  action:@selector(click)forControlEvents:UIControlEventTouchUpInside];

5、适用于tableview的didSelectRowAtIndexPath点击事件,可获取tableView对应的类名、section和row
6、如果是统计tableview的点击事件,根据需要在获取到section和row后加个判断埋点统计

if (section == 0 && row == 1) {
   [MobClick event:eventID];
}

7、如果有特殊需求:某个按钮登录前和登录后记录的事件不一样,需要加判断

if ([eventID isEqualToString:@"xxx"]) {
    [EJServiceUserInfo isLogin]?[MobClick event:eventID]:[MobClick event:@"???"];
   }else{
   [MobClick event:eventID];
 }

以下是具体代码:

(不得不吐槽一下,网上很多博客文章都是转载的,很少有能直接运行的,研究了一整天才弄出来)

这里用到了第三方库:Aspects,用cocoaPods进行集成 pod 'Aspects'

1、创建一个继承与NSObject的EJAspectManager类

EJAspectManager.h

@interface EJAspectManager : NSObject
+(void)trackAspectHooks;
@end

EJAspectManager.m

#import "EJAspectManager.h"
#import "Aspects/Aspects.h"
@implementation EJAspectManager

+(void)trackAspectHooks{

    [EJAspectManager trackViewAppear];
    [EJAspectManager trackBttonEvent];
}

#pragma mark -- 监控统计用户进入此界面的时长,频率等信息
+ (void)trackViewAppear{

    [UIViewController aspect_hookSelector:@selector(viewWillAppear:)
                              withOptions:AspectPositionBefore
                               usingBlock:^(id<AspectInfo> info){

                                   //用户统计代码写在此处
                                   DDLogDebug(@"[打点统计]:%@ viewWillAppear",NSStringFromClass([info.instance class]));
                                   NSString *className = NSStringFromClass([info.instance class]);
                                   DLog(@"className-->%@",className);
                                   [MobClick beginLogPageView:className];//(className为页面名称

                               }
                                    error:NULL];

    [UIViewController aspect_hookSelector:@selector(viewWillDisappear:)
                              withOptions:AspectPositionBefore
                               usingBlock:^(id<AspectInfo> info){

                                   //用户统计代码写在此处
                                   DDLogDebug(@"[打点统计]:%@ viewWillDisappear",NSStringFromClass([info.instance class]));
                                   NSString *className = NSStringFromClass([info.instance class]);
                                   DLog(@"className-->%@",className);
                                   [MobClick endLogPageView:className];

                               }
                                    error:NULL];

    //other hooks ... goes here
    //...
}

#pragma mark --- 监控button的点击事件
+ (void)trackBttonEvent{

    __weak typeof(self) ws = self;

    //设置事件统计
    //放到异步线程去执行
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //读取配置文件,获取需要统计的事件列表
        NSString *path = [[NSBundle mainBundle] pathForResource:@"EventList" ofType:@"plist"];
        NSDictionary *eventStatisticsDict = [[NSDictionary alloc] initWithContentsOfFile:path];
        for (NSString *classNameString in eventStatisticsDict.allKeys) {
            //使用运行时创建类对象
            const char * className = [classNameString UTF8String];
            //从一个字串返回一个类
            Class newClass = objc_getClass(className);

            NSArray *pageEventList = [eventStatisticsDict objectForKey:classNameString];
            for (NSDictionary *eventDict in pageEventList) {
                //事件方法名称
                NSString *eventMethodName = eventDict[@"MethodName"];
                SEL seletor = NSSelectorFromString(eventMethodName);
                NSString *eventId = eventDict[@"EventId"];

                [ws trackEventWithClass:object_getClass(newClass) selector:seletor eventID:eventId];
                [ws trackTableViewEventWithClass:object_getClass(newClass) selector:seletor eventID:eventId];
                [ws trackParameterEventWithClass:object_getClass(newClass) selector:seletor eventID:eventId];
            }
        }
    });
}

#pragma mark -- 监控button和tap点击事件(不带参数)
+ (void)trackEventWithClass:(Class)klass selector:(SEL)selector eventID:(NSString*)eventID{

    [klass aspect_hookSelector:selector withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) {

        NSString *className = NSStringFromClass([aspectInfo.instance class]);
        NSLog(@"className--->%@",className);
        NSLog(@"event----->%@",eventID);
        if ([eventID isEqualToString:@"xxx"]) {
            [EJServiceUserInfo isLogin]?[MobClick event:eventID]:[MobClick event:@"???"];
        }else{
            [MobClick event:eventID];
        }
    } error:NULL];
}

#pragma mark -- 监控button和tap点击事件(带参数)
+ (void)trackParameterEventWithClass:(Class)klass selector:(SEL)selector eventID:(NSString*)eventID{

    [klass aspect_hookSelector:selector withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo,UIButton *button) {

        NSLog(@"button---->%@",button);
        NSString *className = NSStringFromClass([aspectInfo.instance class]);
        NSLog(@"className--->%@",className);
        NSLog(@"event----->%@",eventID);

    } error:NULL];
}

#pragma mark -- 监控tableView的点击事件
+ (void)trackTableViewEventWithClass:(Class)klass selector:(SEL)selector eventID:(NSString*)eventID{

    [klass aspect_hookSelector:selector withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo,NSSet *touches, UIEvent *event) {

        NSString *className = NSStringFromClass([aspectInfo.instance class]);
        NSLog(@"className--->%@",className);
        NSLog(@"event----->%@",eventID);
        NSLog(@"section---->%@",[event valueForKeyPath:@"section"]);
        NSLog(@"row---->%@",[event valueForKeyPath:@"row"]);
        NSInteger section = [[event valueForKeyPath:@"section"]integerValue];
        NSInteger row = [[event valueForKeyPath:@"row"]integerValue];

        //统计事件
        if (section == 0 && row == 1) {
            [MobClick event:eventID];
        }

    } error:NULL];
}
@end

2、这样我们在appDelegate里面直接调用下面的代码就可以达到全局统计的目的了!

[EJAspectManager trackAspectHooks];

3、上面涉及到的EventPlist是新创建的plist文件,设置方式如下:

图片.png

作者:来宝
链接://www.greatytc.com/p/6211704a4b22
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

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

推荐阅读更多精彩内容