使用NSSecureCoding协议进行对象编解码(转)

http://blog.jobbole.com/67655/
NSCoding是把数据存储在iOS和Mac

OS上的一种极其简单和方便的方式,它把模型对象直接转变成一个文件,然后再把这个文件重新加载到内存里,并不需要任何文件解析和序列化的逻辑。如果要把对象保存到一个数据文件中(假设这个对象实现了NSCoding协议),那么你可以像下面这样做:

C++

Foo *someFoo = [[Foo alloc] init];

[NSKeyedArchiver archiveRootObject:someFoo toFile:someFile];

1

2Foo*someFoo=[[Fooalloc]init];

[NSKeyedArchiverarchiveRootObject:someFootoFile:someFile];

稍后再加载它:

C++

Foo *someFoo = [NSKeyedUnarchiver unarchiveObjectWithFile:someFile];

1

Foo*someFoo=[NSKeyedUnarchiverunarchiveObjectWithFile:someFile];

这样做对于编译进APP里的资源来说是可以的(例如nib文件,它在底层使用了NSCoding),但是使用NSCoding来读写用户数据文件的问题在于,把全部的类编码到一个文件里,也就间接地给了这个文件访问你APP里面实例类的权限。

虽然你不能在一个NSCoded文件里(至少在iOS中的)存储可执行代码,但是一名黑客可以使用特制地文件骗过你的APP进入到实例化类中,这是你从没打算做的,或者是你想要在另一个不同的上下文时才做的。尽管以这种方式造成实际性的破坏很难,但是无疑会导致用户的APP崩溃掉或者数据丢失。

在iOS6中,苹果引入了一个新的协议,是基于NSCoding的,叫做NSSecureCoding。NSSecureCoding和NSCoding是一样的,除了在解码时要同时指定key和要解码的对象的类,如果要求的类和从文件中解码出的对象的类不匹配,NSCoder会抛出异常,告诉你数据已经被篡改了。

大部分支持NSCoding的系统对象都已经升级到支持NSSecureCoding了,所以能安全地写有关归档的代码,你可以确保正在加载的数据文件是安全的。实现的方式如下:

C++

// Set up NSKeyedUnarchiver to use secure coding

NSData *data = [NSData dataWithContentsOfFile:someFile];

NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];

[unarchiver setRequiresSecureCoding:YES];

// Decode object

Foo *someFoo = [unarchiver decodeObjectForKey:NSKeyedArchiveRootObjectKey];

1

2

3

4

5

6

7// Set up NSKeyedUnarchiver to use secure coding

NSData*data=[NSDatadataWithContentsOfFile:someFile];

NSKeyedUnarchiver*unarchiver=[[NSKeyedUnarchiveralloc]initForReadingWithData:data];

[unarchiversetRequiresSecureCoding:YES];

// Decode object

Foo*someFoo=[unarchiverdecodeObjectForKey:NSKeyedArchiveRootObjectKey];

注意一下,如果要让编写归档的代码是安全的,那么存储在文件中的每一个对象都要实现NSSecureCoding协议,否则会有异常抛出。如果要告诉框架自定义的类支持NSSecureCoding协议,那么你必须在initWithCoder:

method方法中实现新的解码逻辑,并且supportsSecureCodin方法要返回YES。encodeWithCoder:方法没有变化,因为与安全相关的事是围绕加载进行的,而不是保存:

C++

@interface Foo : NSObject

@property (nonatomic, strong) NSNumber *property1;

@property (nonatomic, copy) NSArray *property2;

@property (nonatomic, copy) NSString *property3;

@end

@implementation Foo

+ (BOOL)supportsSecureCoding

{

return YES;

}

- (id)initWithCoder:(NSCoder *)coder

{

if ((self = [super init]))

{

// Decode the property values by key, specifying the expected class

_property1 = [coder decodeObjectOfClass:[NSNumber class] forKey:@"property1"];

_property2 = [coder decodeObjectOfClass:[NSArray class] forKey:@"property2"];

_property3 = [coder decodeObjectOfClass:[NSString class] forKey:@"property3"];

}

return self;

}

- (void)encodeWithCoder:(NSCoder *)coder

{

// Encode our ivars using string keys as normal

[coder encodeObject:_property1 forKey:@"property1"];

[coder encodeObject:_property2 forKey:@"property2"];

[coder encodeObject:_property3 forKey:@"property3"];

}

@end

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57@interfaceFoo:NSObject

@property(nonatomic,strong)NSNumber*property1;

@property(nonatomic,copy)NSArray*property2;

@property(nonatomic,copy)NSString*property3;

@end

@implementationFoo

+(BOOL)supportsSecureCoding

{

returnYES;

}

-(id)initWithCoder:(NSCoder*)coder

{

if((self=[superinit]))

{

// Decode the property values by key, specifying the expected class

_property1=[coderdecodeObjectOfClass:[NSNumberclass]forKey:@"property1"];

_property2=[coderdecodeObjectOfClass:[NSArrayclass]forKey:@"property2"];

_property3=[coderdecodeObjectOfClass:[NSStringclass]forKey:@"property3"];

}

returnself;

}

-(void)encodeWithCoder:(NSCoder*)coder

{

// Encode our ivars using string keys as normal

[coderencodeObject:_property1forKey:@"property1"];

[coderencodeObject:_property2forKey:@"property2"];

[coderencodeObject:_property3forKey:@"property3"];

}

@end

几周前,我写了一篇关于如何自动实现NSCoding的文章,它利用反射机制确定运行时类的属性。

这是一种给所有的模型对象添加NSCoding支持的很好的方式,在initWithCoder:/encodeWithCoder:

方法中,你不再需要写重复的并且容易出错的代码了。但是我们使用的方法没有支持NSSecureCoding,因为我们不打算在对象被加载时校验其类型。

那么怎么改善这个自动NSCoding系统,使其以正确的方式支持NSSecureCoding呢?

回想一下,最开始的实现原理是利用class_copyPropertyList() 和 property_getName()这样两个运行时方法,产生属性名称列表,我们再把它们在数组中排序:

C++

// Import the Objective-C runtime headers

#import

- (NSArray *)propertyNames

{

// Get the list of properties

unsigned int propertyCount;

objc_property_t *properties = class_copyPropertyList([self class],

&propertyCount);

NSMutableArray *array = [NSMutableArray arrayWithCapacity:propertyCount];

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

{

// Get property name

objc_property_t property = properties[i];

const char *propertyName = property_getName(property);

NSString *key = @(propertyName);

// Add to array

[array addObject:key];

}

// Remember to free the list because ARC doesn't do that for us

free(properties);

return array;

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43// Import the Objective-C runtime headers

#import

-(NSArray*)propertyNames

{

// Get the list of properties

unsignedintpropertyCount;

objc_property_t*properties=class_copyPropertyList([selfclass],

&propertyCount);

NSMutableArray*array=[NSMutableArrayarrayWithCapacity:propertyCount];

for(inti=0;i

{

// Get property name

objc_property_tproperty=properties[i];

constchar*propertyName=property_getName(property);

NSString*key=@(propertyName);

// Add to array

[arrayaddObject:key];

}

// Remember to free the list because ARC doesn't do that for us

free(properties);

returnarray;

}

使用KVC(键-值编码),我们能够利用名称设置和获取一个对象的所有属性,并且在一个NSCoder对象中对这些属性进行编码/解码。

为了要实现NSSecureCoding,我们要遵循同样的原则,但是不仅仅是获取属性名,还需要获取它们的类型。幸运地是,Objective C运行时存储了类的属性类型的详细信息,所以可以很容易和名字一起取到这些数据。

一个类的属性可以是基本数据类型(例如整型、布尔类型和结构体),或者对象(例如字符串、数组等等)。KVC中的valueForKey: and

setValue:forKey:方法实现了对基本类型的自动“装箱”,也就是说它们会把整型、布尔型和结构体各自转变成NSNumber和NSValue对象。这使事情变得简单了很多,因为我们只要处理装箱过的类型(对象)即可,所以我们可以声明属性类型为类,而不用为不同的属性类型调用不同的解码方法。

尽管运行时方法没有提供已装箱的类名,但是它们提供了类型编码—一种特殊格式化的C风格的字符串,它包含了类型信息(与@encode(var);返回的形式一样)。因为没有方法自动获取到基本类型对应的装箱过的类,所以我们需要解析这个字符串,然后指定其合适的类型。

类型编码字符串形式的文档说明在这里

第一个字母代表了基本类型。Objective

C使用一个唯一的字母表示每一个支持的基本类型,例如’i’表示integer,’f’表示float,’d’表示double,等等。对象用’@’表示(紧跟着的是类名),还有其他一些不常见的类型,例如’:’表示selectors,’#’表示类。

结构体和联合体表示为大括号里面的表达式。只有几种类型是KVC机制所支持的,但是支持的那些类通常被装箱为NSValue对象,所以可用一种方式处理以’{’开头的任何值。

如果根据字符串的首字母来转换,那么我们可以处理所有已知的类型:

C++

Class propertyClass = nil;

char *typeEncoding = property_copyAttributeValue(property, "T");

switch (typeEncoding[0])

{

case 'c': // Numeric types

case 'i':

case 's':

case 'l':

case 'q':

case 'C':

case 'I':

case 'S':

case 'L':

case 'Q':

case 'f':

case 'd':

case 'B':

{

propertyClass = [NSNumber class];

break;

}

case '*': // C-String

{

propertyClass = [NSString class];

break;

}

case '@': // Object

{

//TODO: get class name

break;

}

case '{': // Struct

{

propertyClass = [NSValue class];

break;

}

case '[': // C-Array

case '(': // Enum

case '#': // Class

case ':': // Selector

case '^': // Pointer

case 'b': // Bitfield

case '?': // Unknown type

default:

{

propertyClass = nil; // Not supported by KVC

break;

}

}

free(typeEncoding);

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99ClasspropertyClass=nil;

char*typeEncoding=property_copyAttributeValue(property,"T");

switch(typeEncoding[0])

{

case'c':// Numeric types

case'i':

case's':

case'l':

case'q':

case'C':

case'I':

case'S':

case'L':

case'Q':

case'f':

case'd':

case'B':

{

propertyClass=[NSNumberclass];

break;

}

case'*':// C-String

{

propertyClass=[NSStringclass];

break;

}

case'@':// Object

{

//TODO: get class name

break;

}

case'{':// Struct

{

propertyClass=[NSValueclass];

break;

}

case'[':// C-Array

case'(':// Enum

case'#':// Class

case':':// Selector

case'^':// Pointer

case'b':// Bitfield

case'?':// Unknown type

default:

{

propertyClass=nil;// Not supported by KVC

break;

}

}

free(typeEncoding);

如果要处理’@’类型,则需要提去出类名。类名可能包括协议(实际上我们并不需要用到),所以划分字符串拿准确的类名,然后使用NSClassFromString得到类:

C++

case '@':

{

// The objcType for classes will always be at least 3 characters long

if (strlen(typeEncoding) >= 3)

{

// Copy the class name as a C-String

char *cName = strndup(typeEncoding + 2, strlen(typeEncoding) - 3);

// Convert to an NSString for easier manipulation

NSString *name = @(cName);

// Strip out and protocols from the end of the class name

NSRange range = [name rangeOfString:@"<"];

if (range.location != NSNotFound)

{

name = [name substringToIndex:range.location];

}

// Get class from name, or default to NSObject if no name is found

propertyClass = NSClassFromString(name) ?: [NSObject class];

free(cName);

}

break;

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40case'@':

{

// The objcType for classes will always be at least 3 characters long

if(strlen(typeEncoding)>=3)

{

// Copy the class name as a C-String

char*cName=strndup(typeEncoding+2,strlen(typeEncoding)-3);

// Convert to an NSString for easier manipulation

NSString*name=@(cName);

// Strip out and protocols from the end of the class name

NSRangerange=[namerangeOfString:@"<"];

if(range.location!=NSNotFound)

{

name=[namesubstringToIndex:range.location];

}

// Get class from name, or default to NSObject if no name is found

propertyClass=NSClassFromString(name)?:[NSObjectclass];

free(cName);

}

break;

}

最后,把上面的解析过程和前面实现的propertyNames方法结合起来,创建一个方法返回属性类的字典,属性名称作为字典的键。下面是完成的实现过程:

- (NSDictionary *)propertyClassesByName

{

// Check for a cached value (we use _cmd as the cache key,

// which represents @selector(propertyNames))

NSMutableDictionary *dictionary = objc_getAssociatedObject([self class], _cmd);

if (dictionary)

{

return dictionary;

}

// Loop through our superclasses until we hit NSObject

dictionary = [NSMutableDictionary dictionary];

Class subclass = [self class];

while (subclass != [NSObject class])

{

unsigned int propertyCount;

objc_property_t *properties = class_copyPropertyList(subclass,

&propertyCount);

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

{

// Get property name

objc_property_t property = properties[i];

const char *propertyName = property_getName(property);

NSString *key = @(propertyName);

// Check if there is a backing ivar

char *ivar = property_copyAttributeValue(property, "V");

if (ivar)

{

// Check if ivar has KVC-compliant name

NSString *ivarName = @(ivar);

if ([ivarName isEqualToString:key] ||

[ivarName isEqualToString:[@"_" stringByAppendingString:key]])

{

// Get type

Class propertyClass = nil;

char *typeEncoding = property_copyAttributeValue(property, "T");

switch (typeEncoding[0])

{

case 'c': // Numeric types

case 'i':

case 's':

case 'l':

case 'q':

case 'C':

case 'I':

case 'S':

case 'L':

case 'Q':

case 'f':

case 'd':

case 'B':

{

propertyClass = [NSNumber class];

break;

}

case '*': // C-String

{

propertyClass = [NSString class];

break;

}

case '@': // Object

{

//TODO: get class name

break;

}

case '{': // Struct

{

propertyClass = [NSValue class];

break;

}

case '[': // C-Array

case '(': // Enum

case '#': // Class

case ':': // Selector

case '^': // Pointer

case 'b': // Bitfield

case '?': // Unknown type

default:

{

propertyClass = nil; // Not supported by KVC

break;

}

}

free(typeEncoding);

// If known type, add to dictionary

if (propertyClass) dictionary[propertyName] = propertyClass;

}

free(ivar);

}

}

free(properties);

subclass = [subclass superclass];

}

// Cache and return dictionary

objc_setAssociatedObject([self class], _cmd, dictionary,

OBJC_ASSOCIATION_RETAIN_NONATOMIC);

return dictionary;

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193-(NSDictionary *)propertyClassesByName

{

// Check for a cached value (we use _cmd as the cache key,

// which represents @selector(propertyNames))

NSMutableDictionary *dictionary=objc_getAssociatedObject([selfclass],_cmd);

if(dictionary)

{

returndictionary;

}

// Loop through our superclasses until we hit NSObject

dictionary=[NSMutableDictionarydictionary];

Classsubclass=[selfclass];

while(subclass!=[NSObjectclass])

{

unsignedintpropertyCount;

objc_property_t *properties=class_copyPropertyList(subclass,

&propertyCount);

for(inti=0;i<propertyCount;i++)

{

// Get property name

objc_property_tproperty=properties[i];

constchar*propertyName=property_getName(property);

NSString *key=@(propertyName);

// Check if there is a backing ivar

char*ivar=property_copyAttributeValue(property,"V");

if(ivar)

{

// Check if ivar has KVC-compliant name

NSString *ivarName=@(ivar);

if([ivarNameisEqualToString:key]||

[ivarNameisEqualToString:[@"_"stringByAppendingString:key]])

{

// Get type

ClasspropertyClass=nil;

char*typeEncoding=property_copyAttributeValue(property,"T");

switch(typeEncoding[0])

{

case'c': // Numeric types

case'i':

case's':

case'l':

case'q':

case'C':

case'I':

case'S':

case'L':

case'Q':

case'f':

case'd':

case'B':

{

propertyClass=[NSNumberclass];

break;

}

case'*': // C-String

{

propertyClass=[NSStringclass];

break;

}

case'@': // Object

{

//TODO: get class name

break;

}

case'{': // Struct

{

propertyClass=[NSValueclass];

break;

}

case'[': // C-Array

case'(': // Enum

case'#': // Class

case':': // Selector

case'^': // Pointer

case'b': // Bitfield

case'?': // Unknown type

default:

{

propertyClass=nil;// Not supported by KVC

break;

}

}

free(typeEncoding);

// If known type, add to dictionary

if(propertyClass)dictionary[propertyName]=propertyClass;

}

free(ivar);

}

}

free(properties);

subclass=[subclasssuperclass];

}

// Cache and return dictionary

objc_setAssociatedObject([selfclass],_cmd,dictionary,

OBJC_ASSOCIATION_RETAIN_NONATOMIC);

returndictionary;

}

最难的部分已经完成了。现在,要实现NSSecureCoding,只要将initWithCoder:方法中之前写的自动编码实现的部分,改为在解析时考虑到属性的类就可以了。此外,还需让supportsSecureCoding方法返回YES:

C++

+ (BOOL)supportsSecureCoding

{

return YES;

}

- (id)initWithCoder:(NSCoder *)coder

{

if ((self = [super init]))

{

// Decode the property values by key, specifying the expected class

[[self propertyClassesByName] enumerateKeysAndObjectsUsingBlock:(void (^)(NSString *key, Class propertyClass, BOOL *stop)) {

id object = [aDecoder decodeObjectOfClass:propertyClass forKey:key];

if (object) [self setValue:object forKey:key];

}];

}

return self;

}

- (void)encodeWithCoder:(NSCoder *)aCoder

{

for (NSString *key in [self propertyClassesByName])

{

id object = [self valueForKey:key];

if (object) [aCoder encodeObject:object forKey:key];

}

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47+(BOOL)supportsSecureCoding

{

returnYES;

}

-(id)initWithCoder:(NSCoder*)coder

{

if((self=[superinit]))

{

// Decode the property values by key, specifying the expected class

[[selfpropertyClassesByName]enumerateKeysAndObjectsUsingBlock:(void(^)(NSString*key,ClasspropertyClass,BOOL*stop)){

idobject=[aDecoderdecodeObjectOfClass:propertyClassforKey:key];

if(object)[selfsetValue:objectforKey:key];

}];

}

returnself;

}

-(void)encodeWithCoder:(NSCoder*)aCoder

{

for(NSString*keyin[selfpropertyClassesByName])

{

idobject=[selfvalueForKey:key];

if(object)[aCoderencodeObject:objectforKey:key];

}

}

这样就得到了一个用于描述模型对象的简单的基类,并且它以正确的方式支持NSSecureCoding。此外,你可以使用我的AutoCoding扩展,它利用这种方法自动给没有实现NSCoding 和 NSSecureCoding协议的对象添加对它们的支持。

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,678评论 0 9
  • 1、禁止手机睡眠[UIApplication sharedApplication].idleTimerDisabl...
    DingGa阅读 1,114评论 1 6
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,594评论 18 139
  • Chapters: The Basics - Properties Excerpt From: Apple Inc...
    碧波浮沉阅读 595评论 0 1
  • 实话生活 体悟人生 今天是2016年8月20日 天气晴天 温度25-34度 今天早上五点多起床 起来了以后先按柔腹...
    木风恒阅读 119评论 0 1