知识点2

27、ViewController的didReceiveMemoryWarning是在什么时候调用的?默认的操作是什么?

当程序接到内存警告时View Controller将会收到这个消息:didReceiveMemoryWarning

从iOS3.0开始,不需要重载这个函数,把释放内存的代码放到viewDidUnload中去。

这个函数的默认实现是:检查controller是否可以安全地释放它的view(这里加粗的view指的是controller的view属性),比如view本身没有superview并且可以被很容易地重建(从nib或者loadView函数)。

如果view可以被释放,那么这个函数释放view并调用viewDidUnload。

你可以重载这个函数来释放controller中使用的其他内存。但要记得调用这个函数的super实现来允许父类(一般是UIVIewController)释放view。

如果你的ViewController保存着view的子view的引用,那么,在早期的iOS版本中,你应该在这个函数中来释放这些引用。而在iOS3.0或更高版本中,你应该在viewDidUnload中释放这些引用.

viewDidUnload 被废弃

在iOS4和iOS5系统中,当内存不足,应用受到MemoryWarning时,系统就会自动调用当前没有在界面上得ViewController的viewDidUnload方法。通常情况下,未显示在界面的ViewController是UINavigationController Push栈中未在栈顶的ViewController,以及UITabBarController中未显示的子ViewController。这些ViewController都在MemoryWarning事件发生时,让系统自动调用viewDidUnload 方法。

在iOS6中,由于viewDidUnload 事件任何情况下都不会被触发,所以苹果在文档中建议,应该将回收内存的相关操作移到另一个函数didReceiveMemoryWarning中。但是如果仅仅写成:(以下为错误示例)

- (void)didReceiveMemoryWarning {

[super didReceiveMemoryWarning];

if(self.isViewLoaded&&!self.view.window)

{

self.view=nil;

}

}

在iOS6以后,不建议将view置为nil的原因如下:

1,UIView有一个CALayer成员变量,CALayer用于将自己画到屏幕上,如下图:

2,CALayer是一个bitmap图像的容器类,当UIView调用自身的drawRect时,CALayer才会创建bitmap图像类。

3,具体占内存的其实是一个bitmap图像类,CALayer只占48Bytes,UiView只占96Bytes。而一个iPad的全屏UIView的bitmap类会占到12MB的大小。

4,在iOS6,当系统发出MemoryWarning时,系统会自动回收bitmap类,但是不回收UIView和CALayer类。这样既能回收大部分内存,又能在需要bitmap类时,通过调用UIView的drawRect:方法重建。

内存优化:

苹果系统对上面的内存回收做了一个优化:

1,当一段内存被分配时,它会被标记成“In User”,以防止被重复使用。当内存被释放时,这段内存被标记为“Not in use”,这样有新的内存申请时,这块内存就可能被分配给其他变量。

2,CALayer包括的具体的bitmap内容的私有成员变量类型为CABackingStore,当收到MemoryWarning时,CABackingStore类型的内存会被标记为Volatile类型,表示这块内存可能再次被原变量使用。

这样,有了上面优化后,当收到MemoryWarning时,虽然所有的CALayer所包含的bitmap内存被标记成volatile了,但是只要这块内存没有被复用,当需要重建bitmap内存时,可以直接被复用,避免了再次调用UIView的drawRect:方法。

简单说:对于iOS6,不需要做任何以前viewDidUnload的事情,更不需要把以前viewDidUnload的代码移到didReceiveMemoryWarning方法中。



30. frame和bounds有什么不同?

31.ViewController生命周期

按照执行顺序排列

- initWithCoder:通过nib文件初始化时触发

- awakeFromNib:nib文件被加载的时候,会发送一个awakeFromNib的消息到nib文件中的每个对象

- loadView:开始加载视图控制器自带的view

- viewDidLoad:视图控制器的view被加载完成

- viewWillAppear:视图控制器的view将要显示在window上

- updateViewConstraints:视图控制器的view开始更新AutoLayout约束

- viewWillLayoutSubviews:视图控制器的view将要更新内容视图的位置

- viewDidLayoutSubviews:视图控制器的view已经更新视图的位置

- viewDidAppear:视图控制器的view已经展现到window上

- viewWillDisappear:视图控制器的view将要从window上消失

- viewDidDisappear:视图控制器的view已经从window上消失


为什么不建议重载loadView?

永远不要主动调用这个函数。view controller会在view的property被请求并且当前view值为nil时调用这个函数。如果你手动创建view,你应该重载这个函数,且不要在重载的时候调用[super loadview]。如果你用IB创建view并初始化view controller,那就意味着你使用initWithNibName:bundle:方法,这时,你不应该重载loadView函数。

这个方法系统的默认实现是这样:

1;寻找有关可用的nib文件的信息,根据这个信息来加载nib文件       //所以,nib的加载过程是在loadview中完成的哦。

2;如果没有有关nib文件的信息,默认创建一个空白的UIView对象,然后把对象成赋值给view controller的主view。

所以,如果你决定重载这个函数时,你也应该完成这些步骤:

把子类的view赋给view属性(property)(你create的view必须是唯一的实例,并且不被其他任何controller共享),而且你重载的这个函数不应该调用super,这个也是为了保持主view与controller的单一映射关系。


一、结构

按结构可以对ios的所有ViewController分成两类:

1、主要用于展示内容的ViewController,这种ViewController主要用于为用户展示内容,并与用户交互,如UITableViewController,UIViewController。

2、用于控制和显示其他ViewController的ViewController。这种ViewController一般都是一个ViewController的容器。如UINavigationController,UITabbarController。它们都有一个属性:viewControllers。其中UINavigationController表示一种Stack式结构,push一个ViewController或pop一次,因此后一个ViewController一般会依赖前一个ViewController。而UITabbarController表示一个Array结构,各个ViewController是并列的。

第一种ViewController会经常被继承,用来显示不同的数据给用户。而第二种很少被继承,除非你真的需要自定义它。

二、Controller和View的生命周期

这里指的View是指Controller的View。它作为Controler的属性,生命周期在Controller的生命周期内。就是说你的Controller不能在view释放前就释放了。

图2 ViewController生命周期


当你alloc并init了一个ViewController时,这个ViewController应该是还没有创建view的。ViewController的view是使用了lazyInit方式创建,就是说你调用的view属性的getter:[self view]。在getter里会先判断view是否创建,如果没有创建,那么会调用loadView来创建view。loadView完成时会继续调用viewDidLoad。loadView和viewDidLoad的一个区别就是:loadView时还没有view。而viewDidLoad时view以及创建好了。

当view被添加其他view中之前时,会调用viewWillAppear,而之后会调用viewDidAppear。

当view从其他view中移出之前时,会调用viewWillDisAppear,而之后会调用viewDidDisappear。

当view不在使用,而且是disappeared,受到内存警告时,那么viewController会将view释放并将其指向nil。

三、代码组织(如何设计良好的viewcontroller)

ViewController生命周期中有那么多函数,一个重要问题就是什么代码该写在什么地方。

1、init里不要出现创建view的代码。良好的设计,在init里应该只有相关数据的初始化,而且这些数据都是比较关键的数据。init里不要掉self.view,否则会导致viewcontroller创建view。(因为view是lazyinit的)。

2、loadView中只初始化view,一般用于创建比较关键的view如tableViewController的tabView,UINavigationController的navgationBar,不可掉用view的getter(在掉super loadView前),最好也不要初始化一些非关键的view。如果你是从nib文件中创建的viewController在这里一定要首先调用super的loadView方法,但建议不要重载这个方法。

3、viewDidLoad 这时候view已经有了,最适合创建一些附加的view和控件了。有一点需要注意的是,viewDidLoad会调用多次(viewcontroller可能多次载入view,参见图2)。

4、viewWillAppear 这个一般在view被添加到superview之前,切换动画之前调用。在这里可以进行一些显示前的处理。比如键盘弹出,一些特殊的过程动画(比如状态条和navigationbar颜色)。

5、viewDidAppear 一般用于显示后,在切换动画后,如果有需要的操作,可以在这里加入相关代码。

6、viewDidUnload 这时候viewController的view已经是nil了。由于这一般发生在内存警告时,所以在这里你应该将那些不在显示的view释放了。比如你在viewcontroller的view上加了一个label,而且这个label是viewcontroller的属性,那么你要把这个属性设置成nil,以免占用不必要的内存,而这个label在viewDidLoad时会重新创建。


ViewController是iOS开发中MVC模式中的C,ViewController是view的controller,ViewController的职责主要包括管理内部各个view的加载显示和卸载,同时负责与其他ViewController的通信和协调。在ios中,有两类ViewController,一类是显示内容的,比如UIViewController、UITableViewController等,同时还可以自定义继承自UIViewController的ViewController;另一类是ViewController容器,UINavigationViewController和UITabBarController等,UINavigationController是以Stack的形式来存储和管理ViewController,UITabBarController是以Array的形式来管理ViewController。和Android中Activity一样,IOS开发中,ViewController也有自己的生命周期(Lifecycle)。

首先来看看View的加载过程,如下图:


从图中可以看到,在view加载过程中首先会调用loadView方法,在这个方法中主要完成一些关键view的初始化工作,比如UINavigationViewController和UITabBarController等容器类的ViewController;接下来就是加载view,加载成功后,会接着调用viewDidLoad方法,这里要记住的一点是,在loadView之前,是没有view的,也就是说,在这之前,view还没有被初始化。完成viewDidLoad方法后,ViewController里面就成功的加载view了,如上图右下角所示。

在Controller中创建view有两种方式,一种是通过代码创建、一种是通过Storyboard或Interface Builder来创建,后者可以比较直观的配置view的外观和属性,Storyboard配合IOS6后推出的AutoLayout,应该是Apple之后主推的一种UI定制解决方案,后期我会专门介绍一篇使用AutoLayout进行UI制作的文章。言归正传,通过IB或Storyboard创建view,在Controller中创建view后,会在Controller中对view进行一些操作,会出现如下代码:

@interface MyViewController()

@property (nonatomic) IBOutlet id myButton;

@property (nonatomic) IBOutlet id myTextField;

- (IBAction)myAction:(id)sender;

@end

这里用IBOutlet标记了一个UIButton和一个UITextField,用IBAction来标记UIButton的响应事件,IBOutlet和IBAction都是一个整形常量,用来标记控件,通过一张图能比较清晰的看清他们之间的关系:


上图中,MyViewController是继承自UIViewController的一个自定义ViewController,它包含两个View,一个是UIButton,一个是UITextField,从箭头的指向性上就可以比较好的理解IBOutlet和IBAction了。IBOutlet是告诉Interface Builder,此实例变量被连接到nib文件中的view对象,IBOutlet本身不做任何操作,只是一个标记作用。IBAction同样是个标记关键字,它只能标记方法,它告诉IB用IBAction标记的方法可以被某个控件触发。

通过编程的方式创建view,如下代码:

- (void)loadView

{

CGRect applicationFrame = [[UIScreen mainScreen] applicationFrame];

UIView *contentView = [[UIView alloc] initWithFrame:applicationFrame];

contentView.backgroundColor = [UIColor blackColor];

self.view = contentView;

levelView = [[LevelView alloc] initWithFrame:applicationFrame viewController:self];

[self.view addSubview:levelView];

}

上述代码首先得到屏幕的frame,然后根据该frame生成了一个contentView,并指定当前ViewController的root view为contentView,然后生成了一个LevelView的自定义View并将它通过addSubview:方法添加到当前ViewController当中,完成view的初始化加载。

关于loadView方法的重写,官方文档中有一个明显的注释,原文如下:

Note: When overriding the loadView method to create your views programmatically, you should not call super. Doing so initiates the default view-loading behavior and usually just wastes CPU cycles. Your own implementation of the loadView method should do all the work that is needed to create a root view and subviews for your view controller.

意思是当通过代码方式去创建你自己的view时,在loadView方法中不应该调用super,如果调用[super loadView]会影响CPU性能。

接下来我们看看ViewController中的view是如何被卸载的:


从图中可以看到,当系统发出内存警告时,会调用didReceiveMemoeryWarning方法,如果当前有能被释放的view,系统会调用viewWillUnload方法来释放view,完成后调用viewDidUnload方法,至此,view就被卸载了。此时原本指向view的变量要被置为nil,具体操作是在viewDidUnload方法中调用self.myButton = nil;

小结一下:

loadView和viewDidLoad的区别就是,loadView时view还没有生成,viewDidLoad时,view已经生成了,loadView只会被调用一次,而viewDidLoad可能会被调用多次(View可能会被多次加载),当view被添加到其他view中之前,会调用viewWillAppear,之后会调用viewDidAppear。当view从其他view中移除之前,调用viewWillDisAppear,移除之后会调用viewDidDisappear。当view不再使用时,受到内存警告时,ViewController会将view释放并将其指向为nil。

ViewController的生命周期中各方法执行流程如下:

init—>loadView—>viewDidLoad—>viewWillApper—>viewDidApper—>viewWillDisapper—>viewDidDisapper—>viewWillUnload->viewDidUnload—>dealloc


33. OC中是如何实现线程同步的?


@synchronized: 添加同步锁

NSLock:加锁

NSCondition:加条件锁

dispatch_async(dispatch_get_main_queue(), ^{}); :异步主线程

NSOperationQueue:添加线程依赖

NSOperationQueue:设置最大并发数为1


36. 编程中,保存数据有哪几种方式?

NSKeyedArchiver:

1.1关于数据的持久化存储的几种方式

说到NSKeyedArchiver,也就先要了解下iOS开发中关于数据持久化存储的几种方式:1.属性列表 2.对象归档 3.数据库存储(SQLite) 4.Apple提供的CoreData存储工具,关于以上存储方式的使用场景和各自的优缺点,我在此就不再赘述了,今天主要谈一谈关于第二类存储方式-对象归档的使用方法和特点。

1.2什么是对象归档

归档是一种很常用的文件储存方法,几乎任何类型的对象都能够被归档储存(实际上是一种文件保存的形式)。

苹果提供了NSKeyedArchiver和NSKeyedUnarchiver两个类以供我们把对象序列化和反序列化,在存储之前使用NSKeyedArchiver进行序列化操作,并且写入本地文件,在使用之前使用NSKeyedUnarchiver进行反序列化的操作,以供提取使用!

1.3什么场景下会使用到对象归档

在实际的开发过程中,我们会使用各种数据存储的方式。

如果是简单的进行一些系统提供的类型,例如NSArray,NSDictionary,NSString,BOOL,NSInteger,NSfloat等基本数据类型或者对象,我们可以选择系统提供的NSUserDefault这个单例,使用简单方便,但是仅仅只能对以上这些特定的数据格式进行存储,是否有些局限性?而且属性属性列表这种方式又是否安全呢?可能这些这些条件NSUserDefault都无法满足!

对于一些规律性的,量级比较大的数据,又有规律可循的数据,我们可以选择建表或者使用Apple提供的CoreData进行持久化的存储!

那么如果数据的量级不是很大,没有必要动用数据库或者是CoreData这种大规模的杀伤性武器的时候,而且又对数据的安全性和持久性有那么些要求的时候,我们最好去选择对象序列化这种中等杀伤性工具了!

1.4对象归档的使用方法

使用归档的方法对系统提供的基本类型和基本对象进行归档的操作,在这里不再阐述了,如果明白了对自定义对象的归档和解档,那么系统的基本数据类型和基本对象的归档和解档也就相对很easy了!

具体的使用方法我就借用目前我正在开发维护的代码进行以下说明:

目前我们产品的需求是在用户登录之后,就持久化存储用户的登陆相关信息,在后续使用中不需要再次登陆。当用户没有退出登录,卸载程序后,重新从App Store中现在app后,同样保持登陆状态。

如果仅仅是登陆之后的登陆状态,使用NSUserDefault完全可以实现,只是安全性不是太好而已,但是当用户在登陆状态卸载并且重新安装app后,仍然要保持登陆状态的话,那这个问题就值得思考下了!

基于以上需求,也就是说我们要把用户的登陆信息存储在一个地方,这个地方要满足的条件是1.持久化存储,用户登录后,再次打开app不需要重新进行登录      2.用户在登录的状态下卸载app,再次重新安装后,仍然保持卸载前的登陆状态,也就是要完美重现卸载前的状态!

基于以上的条件,我们自然而然地联想到苹果的sandBox机制,关于苹果的sandBox机制,我们不再详述,最关键的一点是:在sandBox中的Document目录下存储的文件,会根据用户的appleID同步到apple的服务端,也就是说如果再次安装app的时候,此app中的沙盒(sandBox)的Document目录下的文件会被再次还原(用户的app购买信息是和用户的appleID绑定的),那么需求就被完美的满足了,具体的代码实现以及注意事项请继续向下阅读:

@interface IHomeSession : NSObject

@property (nonatomic, strong) NSString *sessionId;              //会话Id

@property (nonatomic, strong) NSDate   *lastSessionDate;    //记录上一次请求时间

@property (nonatomic, strong) NSString *token;                   //ut值

@property (nonatomic, strong) NSString *alias;                    //设备别名

@property (nonatomic, strong) NSString *username;            //用户名

//序列化对象的单例

+ (IHomeSession *)sharedMemory;

//保存

- (void)save;

//获取ssesionId

- (NSString *)getSessionId;

//重置

- (void)reset;

@end

以上是.h文件

具体属性可以根据自己的需求进行添加

提供获取序列化单例的方法,方便在项目全局进行获取和使用

提供保存(save),重置(reset),获取sessionId(getSessionId)的api接口以供使用,也可以根据自己的需求添加api

最重要的一点,是当前类需要遵循NSCoding协议,NSCoding协议中有两个方法,都是requred方法,遵循该协议后,必须实现。

以下是.m文件中NSCoding协议的具体实现

#pragma mark - NSCoding

- (void)encodeWithCoder:(NSCoder *)aCoder

{

[aCoder encodeObject:_sessionId forKey:@"_sessionId"];

[aCoder encodeObject:_lastSessionDate forKey:@"_lastSessionDate"];

[aCoder encodeObject:_token forKey:@"_token"];

[aCoder encodeObject:_username forKey:@"_username"];

[aCoder encodeObject:_alias forKey:@"_alias"];

}

通过以上编码的方法,对当前类中的属性进行逐一的键值编码!

- (id)initWithCoder:(NSCoder *)aDecoder

{

if (self = [super init])

{

_sessionId = [aDecoder decodeObjectForKey:@"_sessionId"];

_alias = [aDecoder decodeObjectForKey:@"_alias"];

_lastSessionDate = [aDecoder decodeObjectForKey:@"_lastSessionDate"];

_token = [aDecoder decodeObjectForKey:@"_token"];

_username = [aDecoder decodeObjectForKey:@"_username"];

}

return self;

}

通过以上解码的方法,对当前类中的属性,根据键进行逐一的逆向编码,并返回一个当前类的实例!

实现了以上的协议方法后,我们就可对当前的类对象进行归档和解档的操作了:

首先我们规定一个归档文件在沙盒中的存储路径,写在一个类方法中,方便取用,为了满足app重新安装后仍然可以获取到最后一次登陆信息的需求,我们把文件存储在沙盒中的第一个文件夹(Document)中,这样可以在程序重新安装后自动回复,原理我在需求分析上已经做了阐述!

+ (NSString *)path

{

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);

NSString *documentDir = [paths objectAtIndex:0];

NSString *dstPath = [documentDir stringByAppendingPathComponent:@"user.data"];

return dstPath;

}

归档的方法,我们集成在save的接口中:

- (void)save

{

[NSKeyedArchiver archiveRootObject:self toFile:[IHomeSession path]];

}

解档的方法我们集成在单例的获取中,一定要先查找对应路径下的文件是否存在,如果存在进行解档操作,不存在的话重新生成一个单例,这样会增强程序的健壮性,防止误取单例,造成程序崩溃!

+ (IHomeSession *)sharedMemory

{

static dispatch_once_t onceToken;

dispatch_once(&onceToken, ^{

if ([[NSFileManager defaultManager] fileExistsAtPath:[IHomeSession path]]) {

instance = [NSKeyedUnarchiver unarchiveObjectWithData:

[NSData dataWithContentsOfFile:[IHomeSession path]]];

}

else

{

instance = [[IHomeSession alloc] init];

}

});

return instance;

}

重置的接口中,我们需要删除本地文件,同时将单例中的各种属性恢复到初始状态,然后将初始状态下的对象保存归档!

- (void)reset

{

[[NSFileManager defaultManager] removeItemAtPath:[IHomeSession path] error:nil];

instance = [[IHomeSession alloc] init];

instance.sessionId = nil;

instance.lastSessionDate = nil;

instance.token = @"default";

instance.username = nil;

instance.alias = nil;

[instance save];

}

例2:

@implementation person

#pragma mark 写入文件

-(void)encodeWithCoder:(NSCoder *)encoder{

[super encodeWithCoder:encoder];//不要忘了这个

[encoder encodeInt:self.age forKey:@"age"];

[encoder encodeObject:self.name forKey:@"name"];

[encoder encodeFloat:self.height forKey:@"height"];

}

#pragma mark 从文件中读取

-(id)initWithCoder:(NSCoder *)decoder{

self = [super initWithCoder:decoder];//不要忘了这个

self.age = [decoder decodeIntForKey:@"age"];

self.name = [decoder decodeObjectForKey:@"name"];

self.height = [decoder decodeFloatForKey:@"height"];

return self;

}

//创建

-(void)createPerson{

person *p = [[[person alloc] init] autorelease];

p.age = 20;

p.name = @"Rio";

p.height =1.75f;

//获得Document的路径

NSString *documents = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];

NSString *path = [documents stringByAppendingPathComponent:@"person.archiver"];//拓展名可以自己随便取

[NSKeyedArchiver archiveRootObject:p toFile:path];

}

//读取

-(void)readPerson{

NSString *documents = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];

NSString *path = [documents stringByAppendingPathComponent:@"person.archiver"];

person *person1 = [NSKeyedUnarchiver unarchiveObjectWithFile:path];

NSLog(@"%@",person1);

}


特点:

可以存储自定义模型对象

NSKeyedArchiver归档相对较plist存储而言,它可以直接存储自定义模型对象,而plist文件需要将模型转为字典才可以存储自定义对象模型;

2.归档不能存储大批量数据(相比较Sqlite而言),存储数据到文件是将所有的数据一下子存储到文件中,从文件中读取数据也是一下子读取所有的数据;

缺点:

假如你的文件中有100个对象了,然后你想在利用归档添加一个对象,你需要先把所有的数据解档出来,然后再加入你想添加的那个对象,同理,你想删除一个文件中的一个对象也是,需要解档出所有的对象,然后将其删除。性能低这样处理

4.1 基本使用:需要归档的模型类必须要遵守NSCoding协议,然后模型实现类中必须实现两个方法:1>encodeWithCoder ->归档;2> initWithCoder:  - >解档

4.2 使用注意:

如果父类也遵守了NSCoding协议,请注意:

应该在encodeWithCoder:方法中加上一句[superencodeWithCode:encode];// 确保继承的实例变量也能被编码,即也能被归档应该在initWithCoder:方法中加上一句self = [superinitWithCoder:decoder];// 确保继承的实例变量也能被解码,即也能被恢复

plist文件读与写:

通过代码来创建plist文件,代码如下:

//建立文件管理

NSFileManager *fm = [NSFileManager defaultManager];

//找到Documents文件所在的路径

NSArray *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUSErDomainMask, YES);

//取得第一个Documents文件夹的路径

NSString *filePath = [path objectAtIndex:0];

//把TestPlist文件加入

NSString *plistPath = [filePath stringByAppendingPathComponent:@"test.plist"];

//开始创建文件

[fm createFileAtPath:plistPath contents:nil attributes:nil];

//删除文件

[fm removeItemAtPath:plistPath error:nil];

在写入数据之前,需要把要写入的数据先写入一个字典中,创建一个dictionary:

//创建一个字典

NSDictionary *dic = [NSDictionary dictionaryWithObjectsAndKeys:@"zhangsan",@"1",@"lisi",@"2", nil];

//把数据写入plist文件

[dic writeToFile:plistPath atomically:YES];

读取plist中的数据,形式如下:

//读取plist文件,首先需要把plist文件读取到字典中

NSDictionary *dic2 = [NSDictionary dictionaryWithContentsOfFile:plistPath];

//打印数据

NSLog(@"key1 is %@",[dic2 valueForKey:@"1"]);

NSLog(@"dic is %@",dic2);

关于plist中的array读写,代码如下:

//把TestPlist文件加入

NSString *plistPaths = [filePath stringByAppendingPathComponent:@"tests.plist"];

//开始创建文件

[fm createFileAtPath:plistPaths contents:nil attributes:nil];

//创建一个数组

NSArray *arr = [[NSArray alloc] initWithObjects:@"1",@"2",@"3",@"4", nil];

//写入

[arr writeToFile:plistPaths atomically:YES];

//读取

NSArray *arr1 = [NSArray arrayWithContentsOfFile:plistPaths];

//打印

NSLog(@"arr1is %@",arr1);

偏好设置:

//1.获取NSUserDefaults对象

NSUserDefaults *defaults=[NSUserDefaults standardUserDefaults];

//2保存数据(如果设置数据之后没有同步, 会在将来某一时间点自动将数据保存到Preferences文件夹下面)

[defaults setObject:@"yangyong" forKey:@"name"];

[defaults setInteger:23 forKey:@"age"];

[defaults setDouble:1.73f forKey:@"height"];

[defaults setObject:@"man" forKey:@"gender"];

//3.强制让数据立刻保存

[defaults synchronize];

(1)偏好设置是专门用来保存应用程序的配置信息的, 一般情况不要在偏好设置中保存其他数据。如果利用系统的偏好设置来存储数据, 默认就是存储在Preferences文件夹下面的,偏好设置会将所有的数据都保存到同一个文件中。

(2)使用偏好设置对数据进行保存之后, 它保存到系统的时间是不确定的,会在将来某一时间点自动将数据保存到Preferences文件夹下面,如果需要即刻将数据存储,可以使用[defaults synchronize];

设置数据时,synchornize方法强制写入

UserDefaults设置数据时,不是立即写入,而是根据时间戳定时地把缓存中的数据写入本地磁盘。所以调用了set方法之后数据有可能还没有写入磁盘应用程序就终止了。出现以上问题,可以通过调用synchornize方法强制写入

(3)注意点:所有的信息都写在一个文件中,对比简单的plist可以保存和读取基本的数据类型。

好处:

1.存储数据不需要关心文件名称

2.快速存储键值对

底层实现:

它其实就是一个字典

Write写入方式:永久保存在磁盘中。具体方法为:

第一步:获得文件即将保存的路径:

NSArray *documentPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask,YES);//使用C函数NSSearchPathForDirectoriesInDomains来获得沙盒中目录的全路径。该函数有三个参数,目录类型、User domain mask、布尔值。其中布尔值表示是否需要通过~扩展路径。而且第一个参数是不变的,即为NSSearchPathDirectory 。在iOS中后两个参数也是不变的,即为:NSUserDomainMask 和 YES。

NSString *ourDocumentPath =[documentPaths objectAtIndex:0];

还有一种方法是使用NSHomeDirectory函数获得sandbox的路径。具体的用法为:

NSString *sandboxPath = NSHomeDirectory();

// Once you have the full sandbox path, you can create a path from it,但是不能在sandbox的本文件层上写文件也不能创建目录,而应该是此基础上创建一个新的可写的目录,例如Documents,Library或者temp。

NSString *documentPath = [sandboxPath stringByAppendingPathComponent:@"Documents"];//将Documents添加到sandbox路径上,具体原因前面分析了!

这两者的区别就是:使用NSSearchPathForDirectoriesInDomains比在NSHomeDirectory后面添加Document更加安全。因为该文件目录可能在未来发送的系统上发生改变。

第二步:生成在该路径下的文件:

NSString *FileName=[documentDirectory stringByAppendingPathComponent:fileName];//fileName就是保存文件的文件名

第三步:往文件中写入数据:

[data writeToFile:FileName atomically:YES];//将NSData类型对象data写入文件,文件名为FileName

最后:从文件中读出数据:

NSData data=[NSData dataWithContentsOfFile:FileName options:0 error:NULL];//从FileName中读取出数据

Sqlite:


生成路径

+(NSString*)path{

NSArray*documentArr =NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES);NSString*documentPath = [documentArr firstObject];// crylown.db 为数据库的名字NSString*path = [NSString stringWithFormat:@"%@/crylown.db",documentPath];returnpath;}

创建/打开数据库

sqlite3 *database;

int databaseResult = sqlite3_open([[selfpath] UTF8String], &database);

if(databaseResult != SQLITE_OK) {

NSLog(@"创建/打开数据库失败,%d",databaseResult);

}

创建表

char*error;//    建表格式: create table if not exists 表名 (列名 类型,....)    注: 如需生成默认增加的id: id integer primary key autoincrement

const char*createSQL ="create table if not exists list(id integer primary key autoincrement,name char,sex char)";

int tableResult = sqlite3_exec(database, createSQL, NULL, NULL, &error);

if(tableResult != SQLITE_OK) {   

     NSLog(@"创建表失败:%s",error); 

   }

添加数据

// 对SQL语句执行预编译intsqlite3_prepare(sqlite3 *db,constchar*sql,intbyte,sqlite3_stmt **stmt,constchar**tail)

1.db代表打开的数据库连接

2.sql代表的sql语句

3.byte代表SQL语句的最大长度

4.传出参数,指向预编译SQL语句产生的sqlite3_stmt

5.指向SQL语句中未使用的部分

int sqlite3_prapare_v2()版本,代表该函数的最新版本。

//  添加//  sql语句格式: insert into 表名 (列名)values(值)

constchar*insertSQL ="insert into haha (name,sex)values('iosRunner','male')";

int insertResult = sqlite3_prepare_v2(database, insertSQL,-1, &stmt,nil);

if(insertResult != SQLITE_OK) {

NSLog(@"添加失败,%d",insertResult);      

  }else{//        

  执行sql语句sqlite3_step(stmt);     

   }

查找数据

//返回sqlite3_stmt(预编译SQL语句产生的结果)const char* sqlite3_colum_int/text...(sqlite3_stmt *,intN)

根据结果返回的值的类型不同选择int/text等,N代表列名在表中的位置。

//    查找//  sql语句格式: select 列名from表名 where 列名 = 参数      注:前面的列名为查询结果里所需要看到的 列名,后面的 列名 = 参数 用于判断删除哪条数据

const char*searchSQL ="select id,name,sex from haha where name = 'puyun2'";

intsearchResult = sqlite3_prepare_v2(database, searchSQL, -1, &stmt,nil);

if(searchResult !=SQLITE_OK) {

NSLog(@"查询失败,%d",searchResult);       

 }else{

while(sqlite3_step(stmt) ==SQLITE_ROW) {          // 查询的结果可能不止一条,直到 sqlite3_step(stmt) !=SQLITE_ROW,查询结束。

int idWord = sqlite3_column_int(stmt,0);

char *nameWord = (char*) sqlite3_column_text(stmt,1);

char*sexWord = (char*)sqlite3_column_text(stmt,2);

NSLog(@"%d,%s,%s",idWord,nameWord,sexWord);        

    }       

 }

修改数据

// 修改      // sql语句格式: update 表名set列名 = 新参数 where 列名 = 参数  注:前面的 列名 = 新参数 是修改的值, 后面的 列名 = 参数 用于判断删除哪条数据

const char*changeSQL ="update haha set name = 'buhao' where name = 'iosRunner'";

int updateResult = sqlite3_prepare_v2(database, changeSQL, -1, &stmt,nil);

if(updateResult !=SQLITE_OK) {

NSLog(@"修改失败,%d",updateResult);     

   }else{            

sqlite3_step(stmt);    

    }

删除数据

//        删除//        sql语句格式: deletefrom表名 where 列名 = 参数    注:后面的 列名 = 参数 用于判断删除哪条数据

const char*deleteSQL ="delete from haha where name = 'iosRunner'";

intdeleteResult = sqlite3_prepare_v2(database, deleteSQL, -1, &stmt,nil);

if(deleteResult !=SQLITE_OK) {

NSLog(@"删除失败,%d",deleteResult);      

  }else{        

    sqlite3_step(stmt); 

       }

结束处理

//        销毁stmt,回收资源

sqlite3_finalize(stmt);

//    关闭数据库

sqlite3_close(database);

注意:写入数据库,字符串可以采用char方式,而从数据库中取出char类型,当char类型有表示中文字符时,会出现乱码。这是因为数据库默认使用ascII编码方式。所以要想正确从数据库中取出中文,需要用NSString来接收从数据库取出的字符串。

NSfileManager:

/**

获取Documents路径

@return 返回Documents路径

*/

- (NSString *)getDocumentsPath {

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);

NSString *path = [paths objectAtIndex:0];

return path;

}

/**

创建文件夹

@param folderName 文件夹的名字

*/

- (void)createDirectoryWithFolderName:(NSString *)folderName {

NSString *documentsPath =[self getDocumentsPath];

NSFileManager *fileManager = [NSFileManager defaultManager];

NSString *iOSDirectory = [documentsPath stringByAppendingPathComponent:folderName];

BOOL isSuccess = [fileManager createDirectoryAtPath:iOSDirectory withIntermediateDirectories:YES attributes:nil error:nil];

if (isSuccess) {

[self remindMessage:CreateFolderSuccess];

} else {

[self remindMessage:CreateFolderFail];

}

}

/**

创建文件

@param name 文件的名字

*/

- (void)createFileWithName:(NSString *)name {

NSString *documentsPath =[self getDocumentsPath];

NSFileManager *fileManager = [NSFileManager defaultManager];

NSString *iOSPath = [documentsPath stringByAppendingPathComponent:name];

BOOL isSuccess = [fileManager createFileAtPath:iOSPath contents:nil attributes:nil];

if (isSuccess) {

[self remindMessage:CreateFileSuccess];

} else {

[self remindMessage:CreateFileFail];

}

}

/**

往文件中写内容

@param name 文件的名字

@param content 要写入的内容

*/

- (void)writeFileWithName:(NSString *)name withContent:(NSString *)content {

NSString *documentsPath = [self getDocumentsPath];

NSString *iOSPath = [documentsPath stringByAppendingPathComponent:name];

BOOL isSuccess = [content writeToFile:iOSPath atomically:YES encoding:NSUTF8StringEncoding error:nil];

if (isSuccess) {

[self remindMessage:WriteFileSuccess];

} else {

[self remindMessage:WriteFileFail];

}

}

/**

读取文件内容

@param name 需要读取的文件的名字

@return 返回读取的内容

*/

- (NSString *)readFileContentWithName:(NSString *)name {

NSString *documentsPath =[self getDocumentsPath];

NSString *iOSPath = [documentsPath stringByAppendingPathComponent:name];

NSString *content = [NSString stringWithContentsOfFile:iOSPath encoding:NSUTF8StringEncoding error:nil];

return content;

}

/**

判断文件是否存在

@param filePath 文件路径

@return 返回BOOL值

*/

- (BOOL)isExistAtPath:(NSString *)filePath {

NSFileManager *fileManager = [NSFileManager defaultManager];

BOOL isExist = [fileManager fileExistsAtPath:filePath];

if (!isExist) {

[self remindMessage:NoFileExist];

}

return isExist;

}

/**

判断文件是否存在,如果不存在,则拷贝

@param fileName 文件的名字

*/

- (void)isExistFileWithName:(NSString *)fileName {

NSFileManager *fileManager = [NSFileManager defaultManager];

NSString *filePath = [[self getDocumentsPath] stringByAppendingPathComponent:fileName];

if(![fileManager fileExistsAtPath:filePath]) { //如果不存在

NSLog(@"xxx.txt is not exist");

NSString *nameStr = [NSString stringWithFormat:@"/%@",fileName];

NSString *dataPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingString:nameStr];//获取程序包中相应文件的路径

NSError *error;

if ([fileManager copyItemAtPath:dataPath toPath:filePath error:&error]) { //拷贝

[self remindMessage:CopyFileSuccess];

} else {

[self remindMessage:CopyFileFail];

}

}

}

/**

计算文件大小

@param filePath 文件路径

@return 返回文件大小

*/

- (unsigned long long)fileSizeAtPath:(NSString *)filePath {

NSFileManager *fileManager = [NSFileManager defaultManager];

BOOL isExist = [fileManager fileExistsAtPath:filePath];

if (isExist) {

unsigned long long fileSize = [[fileManager attributesOfItemAtPath:filePath error:nil] fileSize];

return fileSize;

} else {

[self remindMessage:NoFileExist];

return 0;

}

}

/**

计算整个文件夹中所有文件大小

@param folderPath 文件夹路径

@return 返回文件夹中所有文件大小

*/

- (unsigned long long)folderSizeAtPath:(NSString*)folderPath {

NSFileManager *fileManager = [NSFileManager defaultManager];

BOOL isExist = [fileManager fileExistsAtPath:folderPath];

if (isExist) {

NSEnumerator *childFileEnumerator = [[fileManager subpathsAtPath:folderPath] objectEnumerator];

unsigned long long folderSize = 0;

NSString *fileName = @"";

while ((fileName = [childFileEnumerator nextObject]) != nil) {

NSString *fileAbsolutePath = [folderPath stringByAppendingPathComponent:fileName];

folderSize += [self fileSizeAtPath:fileAbsolutePath];

}

return folderSize / (1024.0 * 1024.0);

} else {

[self remindMessage:NoFileExist];

return 0;

}

}

/**

删除文件

@param name 需要删除的文件名字

*/

- (void)deleteFileWithName:(NSString *)name {

NSString *documentsPath =[self getDocumentsPath];

NSFileManager *fileManager = [NSFileManager defaultManager];

NSString *iOSPath = [documentsPath stringByAppendingPathComponent:name];

BOOL isSuccess = [fileManager removeItemAtPath:iOSPath error:nil];

if (isSuccess) {

[self remindMessage:DeleteFileSuccess];

} else {

[self remindMessage:DeleteFileFail];

}

}

/**

移动文件

@param name 需要移动的文件名字

*/

- (void)moveFileWithName:(NSString *)name {

NSString *documentsPath =[self getDocumentsPath];

NSFileManager *fileManager = [NSFileManager defaultManager];

NSString *filePath = [documentsPath stringByAppendingPathComponent:name];

NSString *moveToPath = [documentsPath stringByAppendingPathComponent:name];

BOOL isSuccess = [fileManager moveItemAtPath:filePath toPath:moveToPath error:nil];

if (isSuccess) {

[self remindMessage:MoveFileSuccess];

} else {

[self remindMessage:MoveFileFail];

}

}

/**

文件重命名

@param name1 需要重命名的文件名字

@param name2 作为重命名的文件名字

*/

- (void)renameFileName:(NSString *)name1 willChangeFileName:(NSString *)name2 {

//通过移动该文件对文件重命名

NSString *documentsPath = [self getDocumentsPath];

NSFileManager *fileManager = [NSFileManager defaultManager];

NSString *filePath = [documentsPath stringByAppendingPathComponent:name1];

NSString *moveToPath = [documentsPath stringByAppendingPathComponent:name2];

BOOL isSuccess = [fileManager moveItemAtPath:filePath toPath:moveToPath error:nil];

if (isSuccess) {

[self remindMessage:RenameFileSuccess];

} else {

[self remindMessage:RenameFileFail];

}

}


38. OC中创建线程的方法是什么?如果在主线程中执行代码,方法是什么?如果想延时执行代码,方法又是什么?

方法一:

NSThread *t = [[NSThread alloc] initWithTarget:self selector:@selector(mutableThread) object:nil];

方法二:

[NSThread detachNewThreadSelector:@selector(mutableThread) toTarget:self withObject:nil];

方法三:

[self performSelectorInBackground:@selector(mutableThread) withObject:nil];

方法四:多线程blog创建

NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];

//会开启一个多线程

[operationQueue addOperationWithBlock:^{

for(int i = 0; i < 50 ;i++)

{

NSLog(@"多线程:%d",i);

}

}];

方法五:

//相当于是一个线程池

NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];

operationQueue.maxConcurrentOperationCount = 1;//设置并发数

//创建线程

NSInvocationOperation *opertion1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(thread1) object:nil];

//设置线程的优先级

[opertion1 setQueuePriority:NSOperationQueuePriorityVeryLow];

//创建另一个线程

NSInvocationOperation *opertion2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(thread2) object:nil];

[opertion2 setQueuePriority:NSOperationQueuePriorityHigh];

//NSOperation就是一个操作单元,用来执行方法,是一个抽象类,必须子类化或者使用系统创建好的子类(NSInvocationOperation or NSBlockOperation)

// //NSOperation是最小的操作单元;只能够执行一次;

// //NSInvocationOperation第一步:创建

NSInvocationOperation *invocation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(banZhuanPlus) object:nil];

// //第二步:(不设置的话不添加到队列)在主线程中执行

// [invocation start];

//NSBlockOperation第一步:创建

NSBlockOperation *block = [NSBlockOperation blockOperationWithBlock:^{

[self banZhuanPlus];

}];

// //第二步:执行(在主线程中执行)

// [block start];//如果添加到队列就不要start了,如果不添加,当前线程就是在主线程中执行,如果添加,就不是在主线程了

// 这个队列会自动帮咱们创建一个辅助的线程,这个时候当前线程就不是主线程了

//这个队列里面只能够添加NSOperation以及子类的对象;

NSOperationQueue *queue = [[NSOperationQueue alloc] init];

[queue setMaxConcurrentOperationCount:2];//设置最大并行数;

[queue addOperation:block];//只要把操作队列添加到队列中就会执行;

[queue addOperation:invocation];

方法六:

dispatch_queue_t queue = dispatch_queue_create("test",NULL);

dispatch_async(queue,^{

for(int i=0;i<50;i++)

{

NSLog(@"多线程:%d",i);

});

创建线程的方法:

- [NSThread detachNewThreadSelector:nil toTarget:nil withObject:nil]

- [self performSelectorInBackground:nil withObject:nil];

- [[NSThread alloc] initWithTarget:nil selector:nil object:nil];

- dispatch_async(dispatch_get_global_queue(0, 0), ^{});

- dispatch_sync(dispatch_get_global_queue(0, 0), ^{});

- [[NSOperationQueue new] addOperation:nil];

主线程中执行代码的方法:

-[self performSelectorOnMainThread:@selector() withObject:nil waitUntilDone:NO];

- dispatch_async(dispatch_get_main_queue(), ^{});//还有异步

-[[NSOperationQueue mainQueue] addOperationWithBlock:^{

// UI更新代码

}];

延迟执行代码

NSTimer启动定时器

sleep(2)  睡两秒钟

+ (void)sleepUntilDate:(NSDate *)date;

+ (void)sleepForTimeInterval:(NSTimeInterval)ti;

[NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]];

[NSThread sleepForTimeInterval:1.0f];


1.performSelector方法

[self performSelector:@selector(delayMethod) withObject:nil afterDelay:1.0f];

此方式要求必须在主线程中执行,否则无效。

是一种非阻塞的执行方式,

暂时未找到取消执行的方法。

2.定时器:NSTimer

[NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(delayMethod) userInfo:nil repeats:NO];

此方式要求必须在主线程中执行,否则无效。

是一种非阻塞的执行方式,

可以通过NSTimer类的- (void)invalidate;取消执行。

3. sleep方式

[NSThread sleepForTimeInterval:1.0f]; [self delayMethod];

此方式在主线程和子线程中均可执行。

是一种阻塞的执行方式,建方放到子线程中,以免卡住界面

没有找到取消执行的方法。

GCD延迟执行

double delayInSeconds = 1.0;

__block ViewController* bself = self;

dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));

dispatch_after(popTime, dispatch_get_main_queue(), ^(void){

[bself test1]; });

此方式在可以在参数中选择执行的线程。

是一种非阻塞的执行方式,

没有找到取消执行的方法。

39. iOS中有哪些多线程方案?

常用的有三种: NSThread NSOperationQueue GCD。

1、NSThread 是这三种范式里面相对轻量级的,但也是使用起来最负责的,

你需要自己管理thread的生命周期,线程之间的同步。线程共享同一应用程序的部分内存空间,

它们拥有对数据相同的访问权限。你得协调多个线程对同一数据的访问,

一般做法是在访问之前加锁,这会导致一定的性能开销。

2、NSOperationQueue 以面向对象的方式封装了用户需要执行的操作,

我们只要聚焦于我们需要做的事情,而不必太操心线程的管理,同步等事情,

因为NSOperation已经为我们封装了这些事情。

NSOperation 是一个抽象基类,我们必须使用它的子类。

3、 GCD: iOS4 才开始支持,它提供了一些新的特性,以及运行库来支持多核并行编程,

它的关注点更高:如何在多个cpu上提升效率。

总结:

- NSThread是早期的多线程解决方案,实际上是把C语言的PThread线程管理代码封装成OC代码。

- GCD是取代NSThread的多线程技术,C语法+block。功能强大。

- NSOperationQueue是把GCD封装为OC语法,额外比GCD增加了几项新功能。

* 最大线程并发数

* 取消队列中的任务

* 暂停队列中的任务

* 可以调整队列中的任务执行顺序,通过优先级

* 线程依赖

* NSOperationQueue支持KVO。 这就意味着你可以观察任务的状态属性。

但是NSOperationQueue的执行效率没有GCD高,所以一半情况下,我们使用GCD来完成多线程操作。

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

推荐阅读更多精彩内容