App Extensions - 处理常见情况

当您编写执行应用扩展程序任务的自定义代码时,您可能需要处理许多类型扩展所共有的一些方案。使用本章中的代码和建议来帮助您实施解决方案。

使用嵌入式框架共享代码

您可以创建一个嵌入式框架,以在您的应用扩展程序及其包含应用之间共享代码。例如,如果您开发了一个图像过滤器以用于您的照片编辑扩展以及其包含的应用程序,请将过滤器的代码放在框架中并将框架嵌入到两个目标中。

确保您的嵌入式框架不包含应用程序扩展不可用的API,如某些API不适用于App Extensions中所述。如果您有一个包含此类API的自定义框架,您可以安全地从包含的应用程序链接到它,但不能与应用程序包含的扩展共享该代码。App Store拒绝链接到此类框架或以其他方式使用不可用API的任何应用程序扩展。

要配置应用程序扩展目标以使用嵌入式框架,请将目标的“仅限应用程序扩展安全API”构建设置设置为“是”。如果不这样做,Xcode会提醒您这样做,方法是显示警告“链接到dylib不安全,无法在应用程序扩展中使用”

重要

链接到嵌入式框架的包含应用程序必须包含arm64(iOS)或x86_64(OS X)体系结构构建设置,否则App Store将拒绝该应用程序。(如创建应用程序扩展中所述,所有应用程序扩展必须包含相应的64位体系结构构建设置。)

配置Xcode项目时,必须在“复制文件”构建阶段选择“框架”作为嵌入式框架的目标。

重要

始终选择“框架”作为“复制文件”构建阶段目标。如果您改为选择“SharedFramework”目的地,App Store将拒绝您的提交。

您可以为运行iOS 7或更早版本的用户提供包含应用程序,但在iOS 8或更高版本中运行时必须采取预防措施以安全地链接嵌入式框架。有关详细信息,请阅读将包含应用程序部署到旧版iOS的内容。

有关创建和使用嵌入式框架的更多信息,请观看WWDC 2014视频“构建现代框架”,网址https://developer.apple.com/videos/wwdc/2014

与您的应用程序共享数据

即使应用程序扩展包嵌套在其包含的应用程序包中,运行的应用程序扩展和包含应用程序也无法直接访问彼此的容器。

背景

要了解容器,阅读关于iOS的文件系统中的文件系统编程指南

但是,您可以启用数据共享。例如,您可能希望允许您的应用扩展程序及其包含的应用程序共享一组大量数据,例如预渲染资产。

要启用数据共享,请使用Xcode或Developer门户为包含应用及其包含的应用扩展启用应用组。接下来,在门户中注册应用程序组,并指定要在包含的应用程序中使用的应用程序组。要了解如何使用应用程序组,请参阅应用程序组添加应用程序

启用应用程序组后,应用程序扩展及其包含的应用程序都可以使用[NSUserDefaults](https://developer.apple.com/library/archive/documentation/LegacyTechnologies/WebObjects/WebObjects_3.5/Reference/Frameworks/ObjC/Foundation/Classes/NSUserDefaults/Description.html#//apple_ref/occ/cl/NSUserDefaults)API共享对用户首选项的访问权限。要启用此共享,请使用该[initWithSuiteName:](https://developer.apple.com/documentation/foundation/userdefaults/1409957-init)方法实例化新NSUserDefaults对象,并传入共享组的标识符。例如,共享扩展程序可能会使用以下代码更新用户最近使用的共享帐户:

  1. // Create and share access to an NSUserDefaults object

  2. NSUserDefaults *mySharedDefaults = [[NSUserDefaults alloc] initWithSuiteName: @"com.example.domain.MyShareExtension"];

  3. // Use the shared user defaults object to update the user's account

  4. [mySharedDefaults setObject:theAccountName forKey:@"lastAccountName"];

图4-1显示了扩展及其包含的应用程序如何使用共享容器来共享数据。

图4-1应用程序扩展的容器与其包含应用程序的容器不同[图片上传中...(image-b6cfaa-1559570465434-0)]

重要

如果您的应用扩展程序使用[NSURLSession](https://developer.apple.com/documentation/foundation/urlsession)该类执行后台上载或下载,则必须设置共享容器,以便扩展程序及其包含的应用程序都可以访问传输的数据。要了解如何在后台执行上载或下载,请参阅执行上载和下载

设置共享容器时,包含应用程序 - 以及允许参与数据共享的每个包含的应用程序扩展 - 都具有对共享容器的读写权限。要避免数据损坏,必须同步数据访问。

使用Core Data,SQLite或Posix锁来帮助协调共享容器中的数据访问。

版本说明

在iOS 8.2及更高版本中,您也可以使用[UIDocument](https://developer.apple.com/documentation/uikit/uidocument)该类来协调共享数据访问。

在iOS 9及更高版本中,您可以[NSFileCoordinator](https://developer.apple.com/documentation/foundation/nsfilecoordinator)直接使用该类进行共享数据访问,但是如果您这样做,则必须[NSFilePresenter](https://developer.apple.com/documentation/foundation/nsfilepresenter)在应用扩展转换为后台时删除对象。

访问网页

在共享扩展(在两个平台上)和动作扩展(仅限iOS)中,您可以通过要求Safari运行JavaScript文件并将结果返回到扩展名来授予用户访问Web内容的权限。您还可以在扩展程序运行之前(在两个平台上)使用JavaScript文件访问网页,或者在扩展程序完成其任务后访问或修改网页(仅限iOS)。例如,共享扩展可以帮助用户共享来自网页的内容,或者iOS中的Action扩展可以显示用户当前网页的翻译。

要向您的应用扩展程序添加网页访问和操作,请执行以下步骤:

  • 创建一个包含名为的全局对象的JavaScript文件ExtensionPreprocessingJS。将自定义JavaScript类的新实例分配给此对象。

  • NSExtensionActivationRule应用扩展程序Info.plist文件的字典中,为NSExtensionActivationSupportsWebPageWithMaxCount密钥指定非零值。(要了解有关激活规则字典的更多信息,请参阅为共享或操作扩展声明支持的数据类型。)

  • 当您的应用扩展程序启动时,使用[NSItemProvider](https://developer.apple.com/documentation/foundation/nsitemprovider)该类来获取执行JavaScript文件返回的结果。

  • 在iOS应用扩展程序中,如果您希望Safari在您的扩展程序完成其任务时修改网页,请将值传递给JavaScript文件。(您也可以NSItemProvider在此步骤中使用该类。)

要告诉Safari您的应用扩展程序包含JavaScript文件,请将该NSExtensionJavaScriptPreprocessingFile键添加到NSExtensionAttributes字典中。密钥的值应该是您希望Safari在扩展开​​始之前加载的文件。例如:

  1. <key>NSExtensionAttributes</key>
  2. <dict>
  3. <key>NSExtensionJavaScriptPreprocessingFile</key>
  4. <string>MyJavaScriptFile</string>
  5. </dict>

在这两个平台上,您的自定义JavaScript类可以定义run()Safari在加载JavaScript文件后立即调用的函数。在该run()函数中,Safari提供了一个名为的参数completionFunction,您可以使用该参数以键值对象的形式将结果传递给您的应用扩展。

在iOS中,您还可以定义finalize()Safari completeRequestReturningItems:completion:在其任务结束时调用时调用的函数。一个finalize()函数可以使用的物品的延长线在completeRequestReturningItems:completion:根据需要改变的网页。

例如,如果您的iOS应用扩展程序在启动时需要网页的基本URI,并且在停止时更改网页的背景颜色,您可以编写如清单4-1所示的JavaScript代码。

清单4-1示例run()finalize()函数

  1. var MyExtensionJavaScriptClass = function() {};

  2. MyExtensionJavaScriptClass.prototype = {

  3. run: function(arguments) {

  4. // Pass the baseURI of the webpage to the extension.

  5. arguments.completionFunction({"baseURI": document.baseURI});

  6. },

  7. // Note that the finalize function is only available in iOS.

  8. finalize: function(arguments) {

  9. // arguments contains the value the extension provides in [NSExtensionContext completeRequestReturningItems:completion:].

  10. // In this example, the extension provides a color as a returning item.

  11. document.body.style.backgroundColor = arguments["bgColor"];

  12. }

  13. };

  14. // The JavaScript file must contain a global object named "ExtensionPreprocessingJS".

  15. var ExtensionPreprocessingJS = new MyExtensionJavaScriptClass;

在这两个平台上,您需要编写代码来处理从run()函数传回的值。要获取结果字典,请kUTTypePropertyListNSItemProvider方法中指定类型标识符[loadItemForTypeIdentifier:options:completionHandler:](https://developer.apple.com/documentation/foundation/nsitemprovider/1403900-loaditemfortypeidentifier)。在字典中,使用NSExtensionJavaScriptPreprocessingResultsKey键来获取结果项。例如,要获取清单4-1中的run()函数传递的基URI ,可以使用如下代码:

  1. [imageProvider loadItemForTypeIdentifier:kUTTypePropertyList options:nil completionHandler:^(NSDictionary *item, NSError *error) {
  2. NSDictionary *results = (NSDictionary *)item;
  3. NSString *baseURI = [[results objectForKey:NSExtensionJavaScriptPreprocessingResultsKey] objectForKey:@"baseURI"];
  4. }];

finalize()在iOS应用扩展程序完成其任务时将值传递给函数,请使用该NSItemProvider``initWithItem:typeIdentifier:方法将字符串中的值打包为[NSExtensionJavaScriptFinalizeArgumentKey](https://developer.apple.com/documentation/foundation/nsextensionjavascriptfinalizeargumentkey)密钥。例如,要为清单4-1中的finalize()函数中使用的背景颜色指定红色,您的扩展可能会使用如下代码:

  1. NSExtensionItem *extensionItem = [[NSExtensionItem alloc] init];
  2. extensionItem.attachments = @[[[NSItemProvider alloc] initWithItem: @{NSExtensionJavaScriptFinalizeArgumentKey: @{@"bgColor":@"red"}} typeIdentifier:(NSString *)kUTTypePropertyList]];
  3. [[self extensionContext] completeRequestReturningItems:@[extensionItem] completion:nil];

执行上传和下载

用户在应用扩展程序中完成任务后,会立即返回主机应用。如果任务涉及可能冗长的上载或下载,则需要确保在您的扩展终止后它可以完成。要执行上载或下载,请使用[NSURLSession](https://developer.apple.com/documentation/foundation/urlsession)该类创建URL会话并启动后台上载或下载任务。

注意

回想一下,app扩展无法使用其他类型的后台任务,例如支持VoIP或播放后台音频。有关更多信息,请参阅响应主机应用程序的请求

在您的应用扩展程序启动上载或下载任务后,扩展程序可以完成主机应用程序的请求并终止,而不会影响任务的结果。要了解有关扩展如何处理来自主机应用程序的请求的详细信息,请参阅响应主机应用程序的请求。在iOS中,如果后台任务完成后您的扩展程序未运行,系统将在后台启动您的包含应用程序并调用[application:handleEventsForBackgroundURLSession:completionHandler:](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622941-application)应用程序委托方法。

重要

如果您的应用扩展程序启动后台[NSURLSession](https://developer.apple.com/documentation/foundation/urlsession)任务,您还必须设置扩展程序及其包含应用程序都可以访问的共享容器。使用类的[sharedContainerIdentifier](https://developer.apple.com/documentation/foundation/nsurlsessionconfiguration/1409450-sharedcontaineridentifier)属性NSURLSessionConfiguration指定共享容器的标识符,以便以后可以访问它。

有关设置共享容器的指导, 请参阅与您的应用程序共享数据

清单4-2显示了配置URL会话并使用它来启动下载的一种方法。

清单4-2配置NSURLSession对象并开始下载的示例

  1. NSURLSession *mySession = [self configureMySession];

  2. NSURL *url = [NSURL URLWithString:@"http://www.example.com/LargeFile.zip"];

  3. NSURLSessionTask *myTask = [mySession downloadTaskWithURL:url];

  4. [myTask resume];

  5. - (NSURLSession *) configureMySession {

  6. if (!mySession) {

  7. NSURLSessionConfiguration* config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@“com.mycompany.myapp.backgroundsession”];

  8. // To access the shared container you set up, use the sharedContainerIdentifier property on your configuration object.

  9. config.sharedContainerIdentifier = @“com.mycompany.myappgroupidentifier”;

  10. mySession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];

  11. }

  12. return mySession;

  13. }

由于一次只能有一个进程可以使用后台会话,因此您需要为包含应用及其每个应用扩展创建不同的后台会话。(每个后台会话都应该有一个唯一的标识符。)建议您的包含应用程序仅在后台启动应用程序时使用由其中一个扩展创建的后台会话来处理该扩展的事件。如果您需要在包含应用程序中执行其他与网络相关的任务,请为它们创建不同的URL会话。

如果您需要在启动后台URL会话之前完成主机应用程序的请求,请确保创建和使用会话的代码有效。在您的应用扩展程序调用[completeRequestReturningItems:completionHandler:](https://developer.apple.com/documentation/foundation/nsextensioncontext/1411301-completerequest)以告知主机应用程序其请求已完成后,系统可以随时终止您的扩展程序。

声明共享或操作扩展的支持数据类型

在您的“共享”或“操作”扩展程序中,您可能可以使用某些类型的数据,但不能处理其他类型的数据。要确保主机应用仅在用户选择了您支持的类型的数据时才提供您的扩展,请将该NSExtensionActivationRule密钥添加到您的扩展的Info.plist属性列表文件中。您还可以使用此键指定扩展程序可以处理的每种类型的最大项目数。

当您的分机运行时,系统会将NSExtensionActivationRule密钥的值与分机项[attachments](https://developer.apple.com/documentation/foundation/nsextensionitem/1416690-attachments)属性中的信息进行比较。有关可以使用此键的完整键列表,请参阅操作扩展键

例如,要声明您的共享扩展程序最多可以支持十个图像,一个电影和一个网页URL,您可以使用以下字典作为该NSExtensionAttributes键的值:

  1. <key>NSExtensionAttributes</key>
  2. <dict>
  3. <key>NSExtensionActivationRule</key>
  4. <dict>
  5. <key>NSExtensionActivationSupportsImageWithMaxCount</key>
  6. <integer>10</integer>
  7. <key>NSExtensionActivationSupportsMovieWithMaxCount</key>
  8. <integer>1</integer>
  9. <key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
  10. <integer>1</integer>
  11. </dict>
  12. </dict>

如果您不支持特定数据类型,请使用0相应键的值或从NSExtensionActivationRule字典中删除键。

注意

如果您的共享或iOS操作扩展程序需要访问网页,则必须包含NSExtensionActivationSupportsWebPageWithMaxCount非零值的密钥。(要了解如何使用JavaScript从您的扩展程序访问网页,请参阅访问网页。)

NSExtensionActivationRule字典中 的键足以满足典型应用扩展的过滤需求。如果您需要执行更复杂或更具体的过滤,例如区分public.urlpublic.image,则可以创建谓词语句。然后,使用表示谓词的裸字符串作为NSExtensionActivationRule键的值。(在运行时,系统将此字符串编译为[NSPredicate](https://developer.apple.com/documentation/foundation/nspredicate)对象。)

例如,应用扩展程序项的attachments属性可以指定PDF文件,如下所示:

  1. {extensionItems = ({
  2. attachments = ({
  3. registeredTypeIdentifiers = (
  4. "com.adobe.pdf",
  5. "public.file-url"
  6. );
  7. });
  8. })}

要指定您的应用扩展程序只能处理一个PDF文件,您可以创建一个谓词字符串,如下所示:

  1. SUBQUERY (
  2. extensionItems,
  3. $extensionItem,
  4. SUBQUERY (
  5. $extensionItem.attachments,
  6. $attachment,
  7. ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.adobe.pdf"
  8. ).@count == $extensionItem.attachments.@count
  9. ).@count == 1

以下是更复杂的谓词语句的示例:

  1. SUBQUERY (
  2. extensionItems,
  3. $extensionItem,
  4. SUBQUERY (
  5. $extensionItem.attachments,
  6. $attachment,
  7. ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "org.appextension.action-one" ||
  8. ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "org.appextension.action-two"
  9. ).@count == $extensionItem.attachments.@count
  10. ).@count == 1

此语句遍历一个[NSExtensionItem](https://developer.apple.com/documentation/foundation/nsextensionitem)对象数组,其次[attachments](https://developer.apple.com/documentation/foundation/nsextensionitem/1416690-attachments)是每个扩展项中的数组。对于每个附件,谓词评估附件中每个表示的统一类型标识符(UTI)。当附件表示UTI符合两个不同的指定UTI中的任何一个(您在每个UTI-CONFORMS-TO操作员的右侧看到)时,收集该UTI以进行最终比较测试。TRUE如果应用程序扩展名仅提供了一个支持UTI的扩展项附件,则返回最后一行。

仅在开发期间,您可以使用TRUEPREDICATE常量(始终求值为true)作为存根谓词语句,以在实现谓词语句之前测试代码路径。

重要

在将包含应用程序提交到App Store之前,请确保使用TRUEPREDICATE功能谓词语句或NSExtensionActivationRule键替换存根谓词的所有使用。如果您的包含应用中的任何应用扩展程序包含该字符串TRUEPREDICATE,则该应用将被拒绝。

要了解更多关于断言语句的语法,请参阅谓词格式化字符串的语法谓词编程指南

将包含的应用程序部署到iOS的旧版本

如果从包含的应用程序链接到嵌入式框架,您仍然可以将其部署到8.0之前的iOS版本,即使这些版本中没有嵌入式框架。

允许您执行此操作的机制是dlopen命令,您可以使用该命令有条件地链接和加载框架包。您可以使用此命令替代您可以在Xcode General或Build Phases目标编辑器中指定的构建时链接。主要思想是仅在iOS 8.0或更高版本中运行时将嵌入式框架链接到包含应用程序。

您必须在有条件地加载框架包的代码语句中使用Objective-C而不是Swift。您的应用程序的其余部分可以用任何一种语言编写,嵌入式框架本身也可以用任何一种语言编写。

调用之后dlopen,使用以下类型的语句访问嵌入式框架类:

  1. MyLoadedClass *loadedClass = [[NSClassFromString (@"MyClass") alloc] init];

重要

如果您的包含应用目标链接到嵌入式框架,它必须包含arm64架构,否则它将被App Store拒绝。

设置应用扩展Xcode项目以利用条件链接

  1. 对于每个包含的应用扩展程序,像往常一样将部署目标设置为iOS 8.0或更高版本。

    在Xcode目标编辑器的“常规”选项卡的“部署信息”部分中执行此操作。

  2. 对于包含应用程序,请将部署目标设置为您要支持的最旧版本的iOS。

  3. 在您的包含应用程序中,dlopen使用该[systemVersion](https://developer.apple.com/documentation/uikit/uidevice/1620043-systemversion)方法在运行时检查iOS版本的条件下调用该命令。

    dlopen仅当您的包含应用程序在iOS 8.0或更高版本中运行时才 调用该命令。进行此调用时,请务必使用Objective-C,而不是Swift。

某些iOS API通过dlopen命令使用嵌入式框架。您必须像dlopen直接调用一样使用这些API 。这些API来自[CFBundleRef](https://developer.apple.com/documentation/corefoundation/cfbundle)opaque类型:

  • [CFBundleGetFunctionPointerForName](https://developer.apple.com/documentation/corefoundation/1537143-cfbundlegetfunctionpointerfornam)

  • [CFBundleGetFunctionPointersforNames](https://developer.apple.com/documentation/corefoundation/1537109-cfbundlegetfunctionpointersforna)

[NSBundle](https://developer.apple.com/library/archive/documentation/LegacyTechnologies/WebObjects/WebObjects_3.5/Reference/Frameworks/ObjC/Foundation/Classes/NSBundle/Description.html#//apple_ref/occ/cl/NSBundle)课堂上讲:

  • [load](https://developer.apple.com/library/archive/documentation/LegacyTechnologies/WebObjects/WebObjects_3.5/Reference/Frameworks/ObjC/Foundation/Classes/NSBundle/Description.html#//apple_ref/occ/instm/NSBundle/load)

  • [loadAndReturnError:](https://developer.apple.com/documentation/foundation/nsbundle/1411819-loadandreturnerror)

  • [classNamed:](https://developer.apple.com/library/archive/documentation/LegacyTechnologies/WebObjects/WebObjects_3.5/Reference/Frameworks/ObjC/Foundation/Classes/NSBundle/Description.html#//apple_ref/occ/instm/NSBundle/classNamed:)

在您正在部署到8.0之前的iOS版本的应用程序中,仅在运行时检查中调用这些API,以确保您在iOS 8.0或更高版本中运行,并使用Objective-C调用这些API。

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

推荐阅读更多精彩内容

  • 最近一直忙于公司项目、今天抽空总结了一下关于上线审核的一些变动!和大家分享一下相互学习。 这两天发现苹果在审核指南...
    _VisitorsZsl阅读 1,019评论 0 1
  • 附规则地址:https://developer.apple.com/app-store/review/guidel...
    iOS_大菜鸟阅读 8,506评论 1 3
  • 感谢今天早起安排工地施工~ 感谢师傅帮我把车安排好 感谢胶水刚好够~不用另外再去拿 感谢师傅做好墙布还给我拍好照片...
    爱眉小札夏大宝阅读 241评论 0 0
  • 文/游庄薇 第一次在异国他乡过年 不能陪在他们身边 只能抱着浓浓的思念抵御孤独 翻看相册 彩色照片里 爷爷身着军装...
    ZYH庄园阅读 396评论 0 0
  • 又是一个星期一,绿城今天很冷,虽然有五六度,但那种湿冷的狂风会渗到人的骨头里,杀伤力不比北方的冰雪天气小。星期一总...
    昵称等我想好了再写阅读 256评论 0 0