ASDK的使用(一)

在这个AsyncDisplayKit 2.0教程中,学习如何通过异步渲染的强大功能使用户界面及其流畅的滚动。

paper

AsyncDisplayKit是最初由Facebook的Paper应用程序开发的UI框架。这就是Paper团队面临的核心问题之一:如何尽可能缓解主线程的压力?

现在,许多应用程序的用户体验很大程度上依赖于持续手势和物理动画。至少,你的UI大部分依赖于某种形式的 scrollView

这些类型的用户界面完全依赖于主线程且对主线程阻塞非常敏感。主线程阻塞将导致丢帧,降低用户的体验。

主线程开销较大的任务一般分为:

  • 1: 计算尺寸和布局:比如 -heightForRowAtIndexPath:,或者在UILbel中调用 -sizeThatFits以及指数上升AutoLayout布局计算
  • 2: 图像解码:想要在一个 imageView 中使用 UIImage,首先要进行图形解码。
  • 3: 绘图:复杂的文本以及手动绘制渐变和阴影。
  • 4: 对象生命周期:创建,操纵和销毁系统对象(即创建一个UIView)

在正确使用中, AsyncDisplayKit允许您在默认情况下异步执行所有计算、布局和渲染。在没有任何额外优化的情况下,应用程序可以在主线程上完成的工作量大约减少一个数量级

除了这些性能优势,酷炫的 AsyncDisplayKit 还为开发者提供的便利接口,用简洁的代码就能完成复杂的功能。

在这两部分 AsyncDisplayKit 2.0 教程中,你将掌握使用ASDK构建一个实用的和动态的应用程序的所有要素。 在第一部分中,你将要学习一些在你构建应用程序时可以用到的宏观思想。 在第二部分中,你将学习如何构建自己 nodesubclass,以及如何使用ASDK强大的布局引擎。为了更好的完成本教程,你需要会使用 Xcode 以及 熟悉 Objective-C

特别声明:ASDK不兼容Interface Builder和AutoLayout,因此,您将不会在本教程中使用它们,虽然ASDK完全支持Swift(除了ComponentKit),许多开发者仍在使用.
Objective-C。免费App排行榜前100大多数都没有使用Swift(至少6个使用ASDK)。出于这些原因,本系列将重点介绍 Objective-C。话虽这么说,我们已经包括了一个Swift版本的实例项目。(嘴上说没有,代码还是很诚实的😂~)

一: 开始

请下载项目实例

该项目使用CocoaPods来拉入AsyncDisplayKit。 因此,所以,在正常的 CocoaPods 体系下,继续打开RainforestStarter.xcworkspace而不是RainforestStarter.xcodeproj

构建并运行以查看包含 UITableView 动物列表的应用程序。如果你看过了代码,AnimalTableController你会发现这是一个正常且熟悉的UITableViewController类。

注意:确保在真机上运行本教程中的代码,而不是在模拟器中运行。

向上滑动你将看到帧数丢失引起的卡顿。你不需要启动控制台,以便能发现到这个应用程序需要在性能方面上的一些优化。

[图片上传失败...(image-c8c6db-1547448512256)]

您可以通过AsyncDisplayKit的强大功能来解决这个问题。

二: 介绍ASDisplayNode

ASDisplayNode是ASDK的核心类,它只是一个类似于 MVC 中的 “View”。认识一个 node 的最佳方法是参照你已经熟悉的UIViewsCALayers 之间的关系。

记住,iOS应用程序中的所有在屏幕上的显示都通过CALayer对象表示的。UIViews 创建并且拥有一个底层的 CALayer,并为他们添加触摸处理和其他交互功能。UIView 并不是 CALayer 的子类,它们环绕着一个图层对象,扩展了它的功能.

image

这种抽象的情况下扩展 ASDisplayNode 您可以将它们视为包装一个view,就像在 view 包装着layer 一样。

通过常规视图将哪些节点带到表中的事实是它们可以在后台队列上创建和配置,并且默认情况下同时呈现


image

幸运的是,用于处理 Node 的 API 对于任何使用过的 UIViews 或者 CALayers 的人来说应该异常的熟悉。所有 View 的属性都可以等效为 Node 类。你可以访问基础的 view 或者 layer 本身,就像是访问 view.layer 一样

三: 节点容器(The Node Containers)

虽然 Node 本身提供了巨大的性能改进的可能,但真正的强大的是它们与四个容器类结合使用时产生的黑魔法。

这些类包括:
-ASViewController:一个UIViewController的子类,允许你提供要管理的 Node。
-ASCollectionNodeASTableNode:与UICollectionViewUITableView的等价,其子类实际上保留在底层。

  • ASCollectionNodeASTableNode:与UICollectionViewUITableView的等价,其子类实际上是在框架维护的。

  • ASPagerNodeASCollectionNode的一个子类,与UIKit的UIPageViewController相比,可以提供很好的刷卡性能。

说得好,但真正的黑魔法来自ASRangeController 这些类用于影响所包含的 Node 的行为。现在,跟着我并把你们的脑袋放空吧~

四: TableNode

你要做的第一件事就是将当前 TableView 替换为 TableNode。这个没什么难度。

4.1: tableNode来替换tableView

首先,导航到AnimalTableController.m。 在该类的其他导入下方添加以下行:

#import <AsyncDisplayKit/AsyncDisplayKit.h>

导入ASDK为了使用框架。
然后,我们继续,替换 tableView 的声明属性 :

@property (strong, nonatomic) ASTableNode *tableNode;

来替换

@property (strong, nonatomic) UITableView *tableView;

这将导致这个类中很多地方报错,但不要慌张!

别担心。这些错误和警告将作为你实现相关功能的引导。

-viewDidLoad 中的报错是理所当然,因为 tableView 已经被替换掉。我不会让你通过tableNode 替换 所有的tableView实例(我的意思是,查找和替换并非那么难),但是如果你做了,你会看到:

  • 1: 你应该为ASTableNode分配一个属性。
  • 2: table Node 没有调用 -registerClass:forCellReuseIdentifier: 方法。
  • 3: 你不能添加一个 nodesubview

此时,你应该将 -viewDidLoad 中的方法替换为:

- (void)viewDidLoad {
  [super viewDidLoad];
  [self.view addSubnode:self.tableNode];
  [self applyStyle];
}

这里要注意一个有趣的情况,你调用的是 UIView的一个 -addSubnode: 方法,该方法是通过 category 添加到 UIView 上的,等效于:

[self.view addSubview:self.tableNode.view];

接下来,修改 -viewWillLayoutSubviews 中的代码:

- (void)viewWillLayoutSubviews {
  [super viewWillLayoutSubviews];
  self.tableNode.frame = self.view.bounds;
}

所有这一切都是用self.tableNode替换self.tableView来设置表的框架。

继续修改 -applyStyle 方法中的代码为:

- (void)applyStyle {
  self.view.backgroundColor = [UIColor blackColor];
  self.tableNode.view.separatorStyle = UITableViewCellSeparatorStyleNone;
}

这是唯一设置 tableseparatorStyle 的一行代码。注意 tableNodeview 是如何访问 tableseparatorStyle 属性的。ASTableNode 不会暴露所有UITableView的的属性,所以你必须通过tableNode底层的 UITableView 实例去设置 UITableView 的特殊属性。

然后,在 -initWithAnimals: 方法中添加。

_tableNode = [[ASTableNode alloc] initWithStyle:UITableViewStylePlain];

并且在 return 之前,调用:

[self wireDelegation];

这就会在初始化 AnimalTableController 的时候,创建了一个 tableNode 并且调用 -wireDelegation 方法 设置 tableNode 的 代理。

4.2: 设置 TableNode 的 DataSource & Delegate

类似于 UITableViewASTableNode 也使用 DataSourceDelegate 来设置本身。TableNodeASTableDataSourceASTableDelegate protocols 非常类似于 UITableViewDataSourceUITableViewDelegate。

事实上,虽然他们定义了一些完全相同的方法,如 -tableNode:numberOfRowsInSection:,但两组协议也不完全相同,因为 ASTableNode 行为和 UITableView 还以所有不同的。

找到 -wireDelegation 方法, 并用 tableNode 替换 tableView

- (void)wireDelegation {
  self.tableNode.dataSource = self;
  self.tableNode.delegate = self;
}

现在, 你会收到警告, AnimalTableController 实际上不符合协议。目前,AnimalTableController 仅遵循 UITableViewDataSourceUITableViewDelegate 协议。在下面的章节中,我们将遵循这些协议,使我们能够使用 tableNode 的功能。

4.3: 遵循 ASTableDataSource

AnimalTableController.m 开头的地方找到 AnimalTableControllerDataSource 扩展声明:

@interface AnimalTableController (DataSource)<UITableViewDataSource>
@end

ASTableDataSource 替换 UITableViewDataSource为:

@interface AnimalTableController (DataSource)<ASTableDataSource>
@end

现在,AnimalTableController 已经遵循了 AnimalTableController 协议。本就该如此了。

导航到 AnimalTableController.m 的底部并找到 DataSource category 的实现。

首先,将 UITableViewDataSource-tableView:numberOfRowsInSection:方法,
更改为 ASTableDataSource 的版本。

- (NSInteger)tableNode:(ASTableNode *)tableNode numberOfRowsInSection:(NSInteger)section {
  return self.animals.count;
}

接着, ASTableNodescells 会以不同于 UITableView 的方式返回。用下面的代码替换 -tableView:cellForRowAtIndexPath: 以适应新的规则。

// 1
- (ASCellNodeBlock)tableNode:(ASTableView *)tableView nodeBlockForRowAtIndexPath:(NSIndexPath *)indexPath {
  
  // 2
  RainforestCardInfo *animal = self.animals[indexPath.row];
 
  // 3 return ASCellNodeBlock
  return ^{
    // 4
    CardNode *cardNode = [[CardNode alloc] initWithAnimal:animal];
 
    //You'll add something extra here later...
    return cardNode;
  };
}

让我们回顾一下:

  • 1: ASDK 中的 ASCellNode 等价于 UITableViewCell 或者 UICollectionViewCell 。要注意的是这个方法返回的是一个 ASCellNodeBlockASTableNode 维持着内部所有的 Cell,每个 indexPath 对应一个 block,并且随时准备进行初始化。
  • 2: 你的首要任务是通过数据模型构建cell。这是非常重要的一步,要注意!你获取数据后在 下面的 block 处理。不要在 block 里引用indexPath,以防止 block 运行前的数据变动。
  • 3: 然后返回一个 block,其返回值必须为 ASCellNode
  • 4: 没有必要担心Cell的复用以及初始化一个Cell的方法。您可能会注意到您现在返回了CardNode,而不是 CardCell

这让我想到一个重要的点。或许你已经了解到,使用 ASDK 不需要复用 cell,好吧,我已经说了两遍了,但能记住就好。请随意删除顶部kCellReuseIdentifier的定义吧

static NSString *kCellReuseIdentifier = @"CellReuseIdentifier";

你不必再担心 -prepareForReuse

4.3: 遵循 ASTableDelegate

AnimalTableController.m 顶部,找到以下Delegate类别接口声明:

@interface AnimalTableController (Delegate)<UITableViewDelegate>
@end

ASTableDelegate 替换 UITableViewDelegate

@interface AnimalTableController (Delegate)<ASTableDelegate>
@end

现在 AnimalTableController 已经遵循了 ASTableDelegate,是时候做处理了。在 AnimalTableController.m 底部找到 Delegate 分类的实现。

我们都知道,每个 UITableView 至少都要提供一个 -tableView:heightForRowAtIndexPath:实现方法,因为每个 cell 的高度都由代理计算和返回。

ASTableDelegate 中没有 -tableView:heightForRowAtIndexPath:。再 ASDK 中,所有的 ASCellNode 都负责确定自己的大小。你可以选择为单元格定义最小和最大尺寸,而不是提供静态高度。这种情况下,你希望每个cell的高度至少为屏幕的 2/3。

现在不用担心太多,这个会在第二部分中介绍。

现在只需要替换 -tableView:heightForRowAtIndexPath:为:

- (ASSizeRange)tableView:(ASTableView *)tableNode 
  constrainedSizeForRowAtIndexPath:(NSIndexPath *)indexPath {
  CGFloat width = [UIScreen mainScreen].bounds.size.width;
  CGSize min = CGSizeMake(width, ([UIScreen mainScreen].bounds.size.height/3) * 2);
  CGSize max = CGSizeMake(width, INFINITY);
  return ASSizeRangeMake(min, max);
}

经过我们的辛勤劳动,重新编译、运行项目,看看发生了什么。
真是一个流畅的 tableView!一旦你开始做了,那就让我们做的更好吧!

五: 无限滚动

在大多数应用中,服务器的数据个数往往会多于当前 tableView 中显示的数量。这意味着,你必须通过某些手段做无缝处理,以便用户刷完当前数据列表时从服务端加载新的数据。

很多时候,这是通过手动观察滚动视图方法中的内容偏移来处理 scrollViewDidScroll:, 使用 ASDK, 有一种更具说明性的处理方式。相反的,你可以预先确定好你需要加载的页数。

你要做的第一件事是取消已经存在的方法的注释。在 AnimalTableController.m的结尾,取消 Helpers 分类中的两个方法。你可以认为 -retrieveNextPageWithCompletion: 是你的网络调用,而 -insertNewRowsInTableNode: 是个非常典型的再表中添加新的元素的方法。

接下来,在 -viewDidLoad 添加:

self.tableNode.view.leadingScreensForBatching = 1.0;  // overriding default of 2.0

设置 leadingScreensForBatching 为 1.0 意味着当用户滑动到列表只剩下一个屏幕的数据的到达结尾的时候,就会载入新的数据。

继续,在 Delegate 分类中实现:

- (BOOL)shouldBatchFetchForTableNode:(ASTableNode *)tableNode {
  return YES;
}

此方法用于告知表是否应该在此之后继续请求新批次。如果您知道自己已经到达API数据的末尾,请返回NO,不再发出请求。

因为你希望无限滚动,那就返回 YES,以确保总是请求新的数据。

接下来,还要添加:

- (void)tableNode:(ASTableNode *)tableNode willBeginBatchFetchWithContext:(ASBatchContext *)context {
  //1
  [self retrieveNextPageWithCompletion:^(NSArray *animals) {
    //2
    [self insertNewRowsInTableNode:animals];
    //3
    [context completeBatchFetching:YES];
  }];
}

该方法在用户滑动到 table 的末端并且 -shouldBatchFetchForTableNode: 方法返回 YES 时被调用。

让我们回顾下上面的章节:

  • 1: 首先,你要请求新的 animals 数据来展示。通常是通过 API 来获取的一组array。
  • 2: 完成后,用新下载的数据更新 tableView
  • 3: 最后,确保 -completeBatchFetching:返回的是YES,即大功告成。在完成操作之前,不会进行新的数据请求。

Build and Run,并且不停的滚呀滚。你将会看到不停的看到一只鸟,他们是无限的。


2Gif.gif

六: 智能预加载

你在工作中是否曾经遇到需要预先加载内容到 scrollView 或者 pageView 控制器中?也许你正在处理一个充满屏幕 image ,并且总是希望在接下来的几张图片加载时处于等待状态,所以用户很少看到占位符。

当你再这样的体系下工作时,你很快就会意识到有很多问题要考虑。

  • 1: 你占用了多少内存
  • 2: 你应该提前多久加载内容
  • 3: 你决定什么时候忽略用户的交互反映

并且当你考虑到多个维度的内容时,将些问题将会变得更加复杂。假设你有一个pageViewController,里面每个 viewController 都带有一个 collectionView。现在,你就需要考虑如何在两个方向上动态加载内容。同时,还要对每个设备进行优化。

image

还记得告诉你 ASRangeController 是不重要的吗?现在,这将是我们的重点。

在每个容器类中,所有包含的 node 都有一个接口状态的概念。在任何给定的时间,一个 node 可以是下面的任意组合:


image
  • Preload Range(预载范围):通常最远的范围从可见区域。这是当cell的每个 subNode (例如ASNetworkImageNode) 的内容从外源加载,例如API和本地缓存。这与批量获取时,使用用模型对象代表cell本身形成对比。
  • Display Range(显示范围):在这里进行显示任务,例如文本绘制和进行图像解码。
  • Visible Range(可见范围):此时,node 至少有一个像素在屏幕上。

这些范围也适用于 screenfuls 的度量,并且可以使用 ASRangeTuningParameters 属性轻松调整。

例如:你正在使用一个 ASNetworkImageNode 在 gallery 的每个页面中展示图像,当每个cell进入 Preload Range 时,会发送网络请求,对进入显示范围时检索到的图像进行解码.

一般来说,如果你不愿意,你不必过多考虑这些范围。内置组件(如ASNetworkImageNodeASTextNode)充分利用它们,这意味着默认情况下您将看到巨大的好处

注意: 有件不明显的事,这些 Ranges 不是堆栈的。相反,它们会在
Visible Range 上重叠和汇聚。如果将显示和预取都设置为一个屏幕,则它们将完全相同。通常数据需要存在才能显示,所以一般预取范围应该稍大一点。那么在 node 到达该范围时,就可以开始显示。

通常,该范围的前侧大于后侧。当用户改变他们的滚动方向时,范围的大小也反转,以便有利于用户实际移动的内容。

七: Node接口的状态回调

你可能会疑惑:这些 Ranges 是如何正确工作的?很高兴你这样问~

系统中的每个 node 都有一个 interfaceState 属性,是一个带有字段((NS_OPTION)ASInterfaceState类型。 ASRangeController 负责管理 ASCellNodescrolView 上的移动,每个 subNode 都由一个 interfaceState 属性做对应的更新。这意味着即使时 tree 中最深的 nodes 也可以相应 interfaceState 的变化。

幸运的是,我们很少需要直接去操作 nodeinterfaceState 上的 二进制位。更常见的做法时,你只需要对某 node 的特定的状态进行更改。这就是接口的状态回调。

7.1: Node 命名

为了看到一个 node 的各种状态,给它命名时很有必要的。这样,你就可以监测每个 node 的数据加载、内容成、屏幕展示以及所以的事情。

回到代码 -tableNode:nodeBlockForRowAtIndexPath:,添加一句注释

//You'll add something extra here later...

在它的下面,给 cardNode 添加一个 debugName:

cardNode.debugName = [NSString stringWithFormat:@"cell %zd", indexPath.row];

现在,您将能够跟踪cell在范围内的进展。

7.2: 监听Cells

进入 CardNode_InterfaceCallbacks.m 中,你可以找到六种追踪 noderanges 中的状态的方法。取消注释,Build and Run。打开你的控制台,然后慢慢滑动 table。对照你的滑动,观察cell在对应的状态变化。

201901140549031547459343377_small.gif

注意: 大多数情况下,你只要关心 -didEnterVisibleState-didExitVisibleState 方法对 ASInterfaceState 的改变。或者说,已经为你做好了许多引擎。你可以查看 ASNetworkImageNode 中的代码,看看你集成的通过 PreloadDisplay 状态实现的功能。 所有 node 网络图片的请求和解码,以及内存的释放都是自动完成,不费吹灰之力。

八: 智能预加载(续)

在 2.0 版本中,已经介绍了多个维度上智能与加载的概念。假设你有一个竖直滚动的tableView,在其中某些Cell包含了水平滚动的 collectionView

proaldGif.gif

虽然这个集合现在在技术上处于可见区域,但您不希望预先加载整个集合。相反,两个滚动视图都有自己的ASRangeController,并带有可单独配置的范围调整参数。

8.1: 来到二次元

现在,你已经有了完整的 AnimalTableController , 你可以把它做为 ASPagerNode 的一个page。

项目已经提前写好了控制器的代码,首先进入 AppDelegate.m

找到 -installRootViewController 的下面代码:

AnimalTableController *vc = [[AnimalTableController alloc] 
                              initWithAnimals:[RainforestCardInfo allAnimals]];

替换为:

AnimalPagerController *vc = [[AnimalPagerController alloc] init];

然后,跳到 AnimalPagerController.m-init 方法中添加创建 pager 方法以及 dataSource 的数据源:

_pagerNode = [[ASPagerNode alloc] init];
_pagerNode.dataSource = self;

pagerNode 是 ASCollectionNode 的子类,使用方法与 UIPageViewController 一样。API 实际上比 UIPageViewController 要简单的多。

接下来要实现 pager 的 dataSource 方法,在底部找到 ASPagerDataSource 分类.

首先,告诉 pager 有几个页面。实际上当前的 animal 数组中有三组不同动物,我们需要重写 -numberOfPagesInPagerNode:方法:

- (NSInteger)numberOfPagesInPagerNode:(ASPagerNode *)pagerNode {
  return self.animals.count;
}

然后,你需要实现 -pagerNode:nodeAtIndex 方法,类似于先前实现的 ASTableNodedataSource 方法。

- (ASCellNode *)pagerNode:(ASPagerNode *)pagerNode nodeAtIndex:(NSInteger)index {
    // 1
    NSArray *animals = self.animals[index];
    // 2
    ASCellNode *node = [[ASCellNode alloc] initWithViewControllerBlock:^UIViewController * _Nonnull{
        return  [[AnimalTableController alloc] initWithAnimals:animals];
    } didLoadBlock:nil];
    
    return node;
}

我们来总结下这部分:

  • 1: 尽管这个版本中没有进行模块化分,但是首先获取数据模型是个好习惯。
  • 2: 这一次,你使用的正是强大的 -initWithViewControllerBlock: 构造器。你所要做的就是返回一个block,这个 block 返回你提前设置好的 tableNodeController,它将自动展示在pager 的 页面中。真是太酷了😏~

一旦你添加了这个方法,你将拥有一个完整功能的 Pagar,其中的 cell 是从你原先创建的 tableNodeController 生成的。现在,就可以在用户的垂直和水平滑动下,充分发挥二维预加载的功能!

要查看这个 AsyncDisplayKit 2.0 教程完整的项目,点击这里进行下载。如果你想查看swift版本,这里也有

准备好之后,请转到该项目的第2部分,了解 AsyncDisplayKit 2.0 引入的强大的新的布局系统。

如果你想先进行深入了解,你可以阅读 AsyncDisplayKit主页 的文档。Scott Goodson(AsyncDisplayKit的原创作者)也有几个你可能会感兴趣的话题。最近的话题很好的概述了一些框架对处理大图片存在问题的的尝试。

你可能会对 Paper的构建感兴趣。虽然当时并没有开源,并且有许多地方发生了变化,但看到这一切的开始还是挺有意思的。

这里有一个 public Skack channel,欢迎来提问~

著作权声明

AsyncDisplayKit 2.0 Tutorial: Getting Started
转载AsyncDisplayKit 2.0 教程:入门「译」

相关资料:
iOS 开发一定要尝试的 Texture(ASDK)

TextTure文档l

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

推荐阅读更多精彩内容

  • 这一系列的文章会从几个方面对 ASDK 在性能调优方面策略的实现进行分析,帮助读者理解 ASDK 如何做到使复杂的...
    Draveness阅读 7,602评论 8 110
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,016评论 4 62
  • 演讲者Tim Urban,读政府专业,拥有写不完的论文,别人都是刚开始就把第一时间写完,而他总在最后一刻惊慌怪兽出...
    karmen123阅读 314评论 0 0
  • 今午,又是修复实验,我依然没能做出东西来,准确地说,我根本没有什么思路,包括有什么要求,我要做些什么,只是按照稍有...
    cqmudhw阅读 146评论 0 0