扒虫篇-崩溃日志解读及Crash收集

Paste_Image.png

前言

崩溃是让发人员比较头痛的事情,app崩溃了,说明代码写的有问题,这时如何快速定位到崩溃的地方很重要。调试阶段是比较容易找到出问题的地方的,但是已经上线的app并分析崩溃报告就比较麻烦了。最终,我们可以通过iOS崩溃日志在大多数情况下,你能从中了解到关于闪退的详尽、有用的信息。线上崩溃可以通过 iTunesConnect 中心的Cash收集,也可以通过第三方Cash收集工具,亦或自己在工程中手动收集崩溃日志上传到服务器中,本文做个小结,希望对初入者能有些帮助。

崩溃

崩溃是由于程序抛出异常,系统异常结束的一种现象。我们可以先了解一下异常 NSException,这对于我们理解崩溃有帮助。NSException掌控着程序的生命,程序的崩溃就是NSException来控制的。其实主要的出发点是让开发者认识到哪里的代码有问题。

** NSException**

这个样子

其实控制台输出的日志信息就是NSException产生的,一旦程序抛出异常,程序就会崩溃,控制台就会有这些崩溃日志。

下面代码就会让你的程序崩溃(下面代码出自别人的文章,文末有原文出处)

//异常的名称   
NSString *exceptionName = @"自定义异常";   
//异常的原因    
NSString *exceptionReason = @"我长得太帅了,所以程序崩溃了";    
//异常的信息   
NSDictionary *exceptionUserInfo = nil; 
NSException *exception = [NSException exceptionWithName:exceptionName reason:exceptionReason userInfo:exceptionUserInfo];       
NSString *aboutMe = @"太帅了";      
if ([aboutMe isEqualToString:@"太帅了"])     
{   //抛异常        
  @throw exception;    
}

崩溃截图如下

1478827158887712.png

NSException的实用技巧

  • 1、 若自己封装一套SDK,若要提示哪里出错,那么就可以使用NSException。就像上面NSException的基本用法中的代码一样。

  • 2、可以用来捕获异常,防止程序的崩溃。当你意识到某段代码可能存在崩溃的危险,那么你就可以通过捕获异常来防止程序的崩溃。代码如下

    @try {        
      //如果@try中的代码会导致程序崩溃,就会来到@catch
      //将一个nil插入到可变数组中,这行代码肯定有问题
      [arrayM addObject:nilStr];
     }    
    @catch (NSException *exception) {        
      //如果@try中的代码有问题(导致崩溃),就会来到@catch
      //在这里你可以进行相应的处理操作
      //如果你要抛出异常(让程序崩溃),就写上 @throw exception
    }    
    @finally {        
      //@finally中的代码是一定会执行的
      //你可以在这里进行一些相应的操作
    }
    

崩溃日志

关于修复崩溃的Bug,如果你凭借自己的经验,有时候可能会遇到问题卡住,我想最快的方式就是通过分析崩溃日志来解决崩溃。

什么是崩溃日志,从哪里能得它
iOS设备上的应用闪退时,操作系统会生成一个崩溃报告,也叫崩溃日志,保存在设备上。

崩溃日志上有很多有用的信息,包括应用是什么情况下闪退的。通常,上面有每个正在执行线程的完整堆栈跟踪信息,所以你能从中了解到闪退发生时各线程都在做什么,并分辨出闪退发生在哪个线程上。

有几种方法可以从设备上获取崩溃日志。

  • xcode中查看崩溃信息
    xcode->Window->Organizer->Crashes


  • 通过Xcode查看设备崩溃信息
    除了上面的系统分析工具来进行分析,如果是我们自己直接使用手机连接崩溃或者崩溃之后连接手机,选择window-> devices -> 选择自己的手机 -> view device logs 就可以查看我们的崩溃信息了。


  • 使用第三方软件:itools
    如果你平时不用iTunes,而是使用itools这类第三方的软件对iPhone设备进行管理,也是没问题的。


    打开itools,在你的设备下,找到“高级功能”,点击“崩溃日志”,然后将需要的日志导出到电脑里面就可以了!

  • 应用提交到App Store后,你也能从 iTunes Connect 获取到用户的崩溃日志. 登录到 iTunes Connect 上, 选择 Manage Your Applications, 点击相应的应用, 点击应用图标下面的 View Details 按钮, 然后点击右栏Links部分的 Crash Reports 。

什么时候不会产生崩溃日志
以下情况不会有崩溃信息产生:

  • 内存访问错误(不是野指针错误)
  • 低内存,当程序内存使用过多会造成系统低内存的问题,系统会将程序内存回收
  • 因为某种原因触发看门狗机制

一般Xcode不输出Crash日志有一下几个可能:

  1. NSSetUncaughtExceptionHandler() 可能被重写了,(比如你引用了一些第三方库, 它的SDK里面可能包含了把Crash的日志上传到服务器, 这样这个日志可能被重写了, 就不打印本地的崩溃信息了) 尽量把它放在didFinishLaunchingWithOptions 最后面的一行代码块里.

  2. 还一种崩溃的情况是 EXC_BAD_ACCESS ,EXC_BAD_ACCESS异常的本意是指访问不到内存中这个地址的值,可能是由于些变量已经被回收了,亦可能是由于使用栈内存的基本类型的数据赋值给了id类型的变量。当遇到这种错误, 控制一般不会给你很多关于崩溃的信息, 这种崩溃你开启僵尸对象模式即可, 不过记住你在正式发布的时候记得把这个勾取消, 不然会造成内存泄漏。*

解析崩溃日志

.dSYM 文件

.dSYM 文件称为符号表,是指在Xcode项目编译后,在编译生成的二进制文件.app的同级目录下生成的同名的.dSYM文件。

.dSYM文件其实是一个目录,在子目录中包含了一个16进制的保存函数地址映射信息的中转文件,所有Debug的symbols都在这个文件中(包括文件名、函数名、行号等),所以也称之为调试符号信息文件。符号表就是用来符号化 crash log(崩溃日志)。crash log中有一些方法16进制的内存地址等,通过符号表就能找到对应的能够直观看到的方法名之类。

符号集是我们对ipa文件进行打包之后,和.app文件同级的后缀名为.dSYM的文件,这个文件必须使用Xcode进行打包才有。每一个.dSYM文件都有一个UUID,和.app文件中的UUID对应,代表着是一个应用。而.dSYM文件中每一条崩溃信息也有一个单独的UUID,用来和程序的UUID进行校对。这些UUID一致时才可以解析出当前APP的崩溃信息.

我们在Archive的时候会生成.xcarchive文件,然后显示包内容就能够在里面找到.dsYM文件和.app文件。

所以 为了更好的分析崩溃原因,在每次上架APP的时候,应该保留对应的app文件和dsym文件。

当获得一份crash日志时,我们需要将初始展示的十六进制地址等原始信息映射为源代码级别的方法名称和代码行数,使其对开发人员可读。这个过程称为符号化解析。要成功地符号化解析一份crash日志,我们需要有对应的应用程序二进制文件以及符号(.dSYM)文件。

当程序崩溃的时候,我们可以获得到崩溃的错误堆栈,但是这个错误堆栈都是0x开头的16进制地址,需要我们使用Xcode自带的symbolicatecrash工具来将.Crash和.dSYM文件进行符号化,就可以得到详细崩溃的信息。

Symbolicatecrash
Symbolicatecrash是Xcode自带的一个分析工具,可以通过机器上的崩溃日志和应用的.dSYM文件定位发生崩溃的位置,把crash日志中的地址替换成代码相应位置。

使用效果:

分析前:
Thread 0 name: Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0 CoreFoundation 0x3723b870 0x37180000 + 768112
1 CoreFoundation 0x37196648 0x37180000 + 91720
2 CoreFoundation 0x37181e90 0x37180000 + 7824
3 CoreFoundation 0x3718bb74 0x37180000 + 47988
4 CoreFoundation 0x3718ba8e 0x37180000 + 47758
5 UIKit 0x30f0f866 0x30f0a000 + 22630

分析后:
0 CoreFoundation 0x3723b870 ___forwarding___ + 136
1 CoreFoundation 0x37196648 _CF_forwarding_prep_0 + 40
2 CoreFoundation 0x37181e90 CFRetain + 76
3 CoreFoundation 0x3718bb74 +[__NSArrayI __new::] + 48
4 CoreFoundation 0x3718ba8e -[__NSPlaceholderArray initWithObjects:count:] + 294
5 UIKit 0x30f0f866 -[UIView(Hierarchy) _makeSubtreePerformSelector:withObject:withObject:copySublayers:] + 70

在这个路径下你可以得到系统自带的Symbolicatecrash,把它拷贝到指定的文件下

/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources

获取.dSYM文件

选中archive的版本右击,选择Show in Finder就可以选中archived 文件然后显示包内容,就可以找到dSYM文件了。


解析步骤

  • 我在解析崩溃信息的时候,首先在桌面上建立一个Crash文件夹,然后将.Crash、app、.dSYM、symbolicatecrash放在这个文件夹中。

Paste_Image.png

注意:这里的 .crash 必须是真机安装的打包的那个 sometwo 产生的崩溃日志才行,运行其他的版本产生的崩溃日志,以下的解析会失败。

如何把这个打包的应用安装到测试机上呢?注意这里的应用不是 ipa文件,而且这个手机也可以没被加入到当前的开发者账号中。

手机连上 itunes,在itunes中打开 手机的应用, 文件->添加到资料库 把桌面是上的那个应用添加进入,再同步更新到测试机器中即可。

Paste_Image.png

如果你一直解析失败,那么可能你的 .Crash、app、.dSYM、的UUID不一致,通过终端工具可以查看 app、 .dSYM文件的UUID:

cd到文件夹
dwarfdump --uuid Sometwo.app/Sometwo
dwarfdump --uuid Sometwo.app.dSYM
三者一致才能还原符号表。
Paste_Image.png

Paste_Image.png

由上图可以看出三折的UUID是不一致的,所以会一直解析失败,无法符号化 .Crash文件。

在终端中输入以下命令, iOS002 换成你自己的用户名称

  • cd /Users/iOS002/Desktop/Cash/
  • export DEVELOPER_DIR="/Applications/XCode.app/Contents/Developer"
  • ./symbolicatecrash /Users/iOS002/Desktop/Cash/SomeTwo.crash /Users/iOS002/Desktop/Cash/SomeTwo.app.dSYM > Control_symbol.crash

一切正常的话这样就完成了一个崩溃日志的解析工作。解析完成后会生成一个新的.Crash文件,这个文件中就是崩溃详细信息。图中红色标注的部分就是我们代码崩溃的部分。

收集崩溃日志

获取崩溃信息方式
在iOS中获取崩溃信息的方式有很多,比较常见的是使用友盟、云测、百度、Crashlytics等第三方分析工具,或者自己收集崩溃信息并上传公司服务器。下面列举一些我们常用的崩溃分析方式:

  • 自己实现应用内崩溃收集,并上传服务器。
  • 使用友盟、云测、百度、Crashlytics等第三方崩溃统计工具。

自己收集崩溃信息

苹果给我们提供了异常处理的类,NSException类。这个类可以创建一个异常对象,也可以通过这个类获取一个异常对象。这个类中我们最常用的还是一个获取崩溃信息的C函数,我们可以通过这个函数在程序发生异常的时候收集这个异常。然后把收集到的崩溃信息发送到自己的服务器。

我们也可以通过下面方法获取崩溃统计的函数指针:

 NSUncaughtExceptionHandler *handler = NSGetUncaughtExceptionHandler();
 NSSetUncaughtExceptionHandler (&UncaughtExceptionHandler);
//  收集崩溃信息的调用方法
void UncaughtExceptionHandler(NSException *exception) {
    NSArray *arr = [exceptioncallStackSymbols];//得到当前调用栈信息
    NSString *reason = [exceptionreason];//非常重要,就是崩溃的原因
    NSString *name = [exceptionname];//异常类型
    NSLog(@"exception type : %@ \n crash reason : %@ \n call stack info : %@", name, reason, arr);
}

获取到了崩溃的发送给开发者有以下两种方式:

  1. 将崩溃信息持久化在本地,下次程序启动时,将崩溃信息作为日志发送给开发者。

  2. 通过邮件发送给开发者。不过此种方式需要得到用户的许可,因为iOS不能后台发送短信或者邮件,会弹出发送邮件的界面,只有用户点击了发送才可发送。不过,此种方式最符合苹果的以用户至上的原则。

    发送邮件代码:
    NSString *crashLogInfo = [NSString stringWithFormat:@"exception type : %@ \n crash reason : %@ \n callstack info : %@", name, reason, arr];
    NSString*urlStr = [NSString stringWithFormat:@"mailto://tianranwuwai@yeah.net?subject=bug报告&body=感谢您的配合! 错误详情:%@",crashLogInfo];
    NSURL *url =[NSURL URLWithString:[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
    [[UIApplication sharedApplication] openURL:url];
    

我们是否也可以在程序崩溃时,将崩溃信息写入本地,APP再次启动时,将崩溃信息上传到我们的服务器。这里就要用到apple的一个函数:NSSetUncaughtExceptionHandler。上代码:

   application didFinishLaunchingWithOptions中调用
   [self catchCrashLogs];

    - (void)catchCrashLogs{
        NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);
    }
  
   void UncaughtExceptionHandler(NSException *exception){
      if (exception ==nil)return;
        NSArray *array = [exception callStackSymbols];
        NSString *reason = [exception reason];
        NSString *name  = [exception name];
        NSDictionary *dict = @{@"appException":@{@"exceptioncallStachSymbols":array,@"exceptionreason":reason,@"exceptionname":name}};
    if([SDFileToolClass writeCrashFileOnDocumentsException:dict]){
        NSLog(@"Crash logs write ok!");
    }
   }
    //写入缓存中: 以下提供三个API,分别是:写入,获取,清空
    NSString * const SDCrashFileDirectory = @"SDMapHomeCrashFileDirectory"; //你的项目中自定义文件夹名
    + (NSString *)sd_getCachesPath{
        return [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
    }
    + (BOOL)writeCrashFileOnDocumentsException:(NSDictionary *)exception{
    NSString *time = [[NSDate date] formattedDateWithFormat:@"yyyyMMddHHmmss" locale:[NSLocale currentLocale]];
    NSDictionary *infoDictionary = [[NSBundle mainBundle] infoDictionary];
    NSString *crashname = [NSString stringWithFormat:@"%@_%@Crashlog.plist",time,infoDictionary[@"CFBundleName"]];
    NSString *crashPath = [[self sd_getCachesPath] stringByAppendingPathComponent:SDCrashFileDirectory];
    NSFileManager *manager = [NSFileManager defaultManager];
    //设备信息
    NSMutableDictionary *deviceInfos = [NSMutableDictionary dictionary];
    [deviceInfos setObject:[infoDictionary objectForKey:@"DTPlatformVersion"] forKey:@"DTPlatformVersion"];
    [deviceInfos setObject:[infoDictionary objectForKey:@"CFBundleShortVersionString"] forKey:@"CFBundleShortVersionString"];
    [deviceInfos setObject:[infoDictionary objectForKey:@"UIRequiredDeviceCapabilities"] forKey:@"UIRequiredDeviceCapabilities"];

    BOOL isSuccess = [manager createDirectoryAtPath:crashPath withIntermediateDirectories:YES attributes:nil error:nil];
    if (isSuccess) {
        NSLog(@"文件夹创建成功");
        NSString *filepath = [crashPath stringByAppendingPathComponent:crashname];
        NSMutableDictionary *logs = [NSMutableDictionary dictionaryWithContentsOfFile:filepath];
        if (!logs) {
            logs = [[NSMutableDictionary alloc] init];
        }
        //日志信息
        NSDictionary *infos = @{@"Exception":exception,@"DeviceInfo":deviceInfos};
        [logs setObject:infos forKey:[NSString stringWithFormat:@"%@_crashLogs",infoDictionary[@"CFBundleName"]]];
        BOOL writeOK = [logs writeToFile:filepath atomically:YES];
        NSLog(@"write result = %d,filePath = %@",writeOK,filepath);
        return writeOK;
    }else{
        return NO;
      }
    }
    + (nullable NSArray *)sd_getCrashLogs{
     NSString *crashPath = [[self sd_getCachesPath] stringByAppendingPathComponent:SDCrashFileDirectory];
     NSFileManager *manager = [NSFileManager defaultManager];
     NSArray *array = [manager contentsOfDirectoryAtPath:crashPath error:nil];
     NSMutableArray *result = [NSMutableArray array];
    if (array.count == 0) return nil;
    for (NSString *name in array) {
        NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:[crashPath stringByAppendingPathComponent:name]];
        [result addObject:dict];
    }
    return result;
    }

    + (BOOL)sd_clearCrashLogs{
     NSString *crashPath = [[self sd_getCachesPath] stringByAppendingPathComponent:SDCrashFileDirectory];
    NSFileManager *manager = [NSFileManager defaultManager];
    if (![manager fileExistsAtPath:crashPath]) return YES; //如果不存在,则默认为删除成功
    NSArray *contents = [manager contentsOfDirectoryAtPath:crashPath error:NULL];
    if (contents.count == 0) return YES;
    NSEnumerator *enums = [contents objectEnumerator];
    NSString *filename;
    BOOL success = YES;
    while (filename = [enums nextObject]) {
        if(![manager removeItemAtPath:[crashPath stringByAppendingPathComponent:filename] error:NULL]){
            success = NO;
            break;
        }
    }
    return success;
    }

使用工具Crashlytics统计Crash

市场上有多种移动应用Crash收集工具, 如友盟,MTJ等。在iOS中, 收集Crash主要通过两种方式, 一种是信号量机制,因为crash通常会发出信号量,标明某某应用崩溃了, 另一种方式是每一个应用都有一个crash handle, 即崩溃钩子, 每当程序崩溃时, 都会执行这个回调。信号量比起崩溃句柄的区别有点像ios开发中的通知和delegate。 信号量抛出后,可以被多个捕获crash的工具获取到,然后取当前的堆栈信息, 再利用该堆栈信息与原app的dsym文件进行比对, 就可以找到崩溃的代码行。
理论上讲, 这个信号量机制优秀于crash句柄, 因为这样的话,可以有多个收集工具并行收集, 前提是,每个收集工具收集后,继续抛出这个异常,而不是截断这个异常,当截断后后续的其它工具就收集不到这个异常了, 会导致其它工具收集不全的问题。 而友盟正是这样做的.
利用crash 句柄这种方式使得crash信息只能被一个收集工具所收集到,因为句柄只有一个。如果一个应用中有多个收集工具都设置了这个句柄, 这里就得看谁最后设置这个句柄, 谁就有效。

上面是收集crash的方式说明, 现在说说Crashlytics这个工具。 原理和上面的一样。 不一样的是, 这个工具被twitter收购, 既然有这么一根大树, 那就保证了这个工具的稳定性。 所以建议使用, 目前是免费的。
使用步骤基本上可以分为如下:

  • 注册,
  • 收到邀请信, 然后一步步按其说明完成注册。
  • 根据其提示,下载一个mac app配合进行使用。
  • 当有崩溃发生时,会给注册的邮件发送崩溃统计,方便查看。

在crash信息收集时, 如果正在进行debug调试,是收集不到信息的。

使用Crashlytics的好处:

  • Crashlytics不会漏掉任何应用崩溃信息(就这两个字让我决定使用crashlytics)
  • Crashlytics可以象Bug管理工具那样,管理这些崩溃日志, 可以根据频率及影响用户量来自动设置优先级
  • 可以每天和每周将崩溃信息汇总发送到邮箱中。

具体使用,可以参照这篇文章Crashlytics

小结

有关应用Crash的处理工作任重而道远,后续会持续更新,先写这些吧。


本文参考文章:
关于崩溃日志解读很详细很棒的的一篇文章
iOS被开发者遗忘在角落的NSException-其实它很强大
iOS崩溃调试(收集不同用户的崩溃信息)

Paste_Image.png

Paste_Image.png

模拟器打印不出来 malloc stock的信息,需要真机。

1.unrecognized seletor。错误:这种情况很简单,给一个对象发送了一条它不认识的消息。比如说你的.h中声明了某一个方法,但是.m中却没有实现,而且你没有对异常消息处理(消息转发)就会造成这种现象。解决办法:首先排查自己的某一些方法是否实现,其次看一下哪些对象接收了它不该接收的消息。

2.index 1 beyond NSArraMu [0,0]数组越界:数组越界这个不多说。

3.NSNul length 这个异常以可以归类为第一种,也是给某一个对象发送了不识别的消息。常见原因有:给UILabel对象设置了text,此时的text内容为空字符串null,然后你在取text的length的时候就会抛出异常。

4.EXC_BAD_ACCESS异常:这种大多数是对象提前释放,访问了野指针的错误。解决办法:排查所有声明为weak对象的使用,是否在没有持有的情况下再次访问了该对象(该对象已经被释放),第二在MRC情况下,排查一下所以已经release的对象(声明一点,MRC中全局变量最好在dealloc方法中进行释放),第三排查一下所有block,是否block被正常赋值等。

5.崩溃在main函数。这种情况最苦逼也是最难找到bug所在,这种情况下,用@try @catch将main函数包裹起来,这样会抛出异常堆栈信息等,或者通过添加全局breakPoint来追踪bug。(扯淡)

@try @catch 是最后的大招

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

推荐阅读更多精彩内容