Skip to content

Commit 8e8830a

Browse files
committed
Merge pull request #2 from objccn/master
创建issue-15-3
2 parents dbe64e2 + 7ef2170 commit 8e8830a

File tree

138 files changed

+30267
-148
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

138 files changed

+30267
-148
lines changed

.bookignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
draft/
2+
fake_lang/
3+
origin/
4+
tools/

SUMMARY.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# 目录
2+
3+
* [更轻量的 View Controllers](publish/issue1/issue-1-0-tang3w.md)
4+
* [更轻量的 View Controllers](publish/issue1/issue-1-1-tang3w.md)
5+
* [整洁的 Table View 代码](publish/issue1/issue-1-2-tang3w.md)
6+
* [并发编程](publish/issue2/issue-2-0-FangYiXiong.md)
7+
* [并发编程:API 及挑战](publish/issue2/issue-2-1-beyondvincent.md)

draft/issue1/issue-1-2-tang3w.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Table view controllers 的 view 属性永远都是一个 table view。如果你
2020

2121
最后,你需要把迁移后丢失的 table view controller 的特性给补回来。大多数都是 `viewWillAppear:``viewDidAppear:` 中简单的一条语句。切换编辑模式需要实现一个 action 方法,用来切换 table view 的 `editing` 属性。大多数工作来自重新创建对键盘的支持。
2222

23-
在选择条路之前,其实还有一个更轻松的选择,它可以通过分离我们需要关心的功能(关注点分离),让你获得额外的好处:
23+
在选择这条路之前,其实还有一个更轻松的选择,它可以通过分离我们需要关心的功能(关注点分离),让你获得额外的好处:
2424

2525
### 使用Child View Controllers
2626

@@ -233,4 +233,4 @@ Table view controllers(以及其他的 controller 对象!)应该在 model
233233

234234
原文[Clean table view code](http://www.objc.io/issue-1/table-views.html)
235235

236-
译文[整理 Table View 的代码 - 言无不尽](http://tang3w.com/translate/objective-c/objc.io/2013/10/23/整理-table-view-的代码.html)
236+
译文[整理 Table View 的代码 - 言无不尽](http://tang3w.com/translate/objective-c/objc.io/2013/10/23/整理-table-view-的代码.html)

draft/issue10/issue-10-1-xinjixjz.md

Lines changed: 227 additions & 0 deletions
Large diffs are not rendered by default.

draft/issue10/issue-10-3-migrant.md

Lines changed: 194 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
几乎每一个应用开发者都需要经历的就是将从 web service 获取到的数据转变到 Core Data 中。这篇文章阐述了如何去做。我们在这里讨论的每一个问题在之前的文章中都已经描述过了,并且 Apple 在他们的文档中也提过。然而,从头到尾回顾一遍对我们来说还是很有益的。
2+
3+
程序所有的代码都[在 GitHub 上](https://github.com/objcio/issue-10-core-data-network-application)
4+
5+
## 计划
6+
7+
我们将会建立一个简单、只读的应用程序,用来显示 CocoaPods 说明的完整列表。这些说明都显示在 table view 中,所有 pod 的说明都是以分页的形式,从 web service 取得,并以 JSON 对象返回。
8+
9+
我们这样来做
10+
11+
1. 首先,我们创建一个 `PodsWebservice` 类,用来从 web service 请求所有的说明。
12+
2. 接着,创建一个 `Importer` 对象取出说明并将他们导入 Core Data。
13+
3. 最终,我们展示如何让最重要的工作在后台线程中运行。
14+
15+
## 从 Web Service 取得对象
16+
17+
首先,创建一个单独的类从 web service 取得数据是很不错的。我们已经写了一个简单的 [web server 示例](https://gist.github.com/chriseidhof/725946f0d02b17ced209),用来获取 CocoaPods 说明并将它们生成 JSON;请求 `/specs` 这个 URL 会返回一个按字母排序的 pod 说明列表。web service 是分页的,所以我们需要分开请求每一页。一个响应的示例如下:
18+
19+
{
20+
"number_of_pages": 559,
21+
"result": [{
22+
"authors": { "Ash Furrow": "[email protected]" },
23+
"homepage": "https://github.com/500px/500px-iOS-api",
24+
"license": "MIT",
25+
"name": "500px-iOS-api",
26+
...
27+
28+
我们想要创建只有一个 `fetchAllPods:` 方法的类,它有一个回调 block,这将会被每一个页面调用。这也可以通过代理实现;但为什么我们选择用 block,你可以读一读这篇[有关消息传递机制的文章](http://www.objccn.io/issue-7-4/)
29+
30+
@interface PodsWebservice : NSObject
31+
- (void)fetchAllPods:(void (^)(NSArray *pods))callback;
32+
@end
33+
34+
这个回调会被每个页面调用。实现这个方法很简单。我们创建一个帮助方法,`fetchAllPods:page:`,它会为一个页面取得所有的 pods,一旦加载完一页就让它再调用自己。注意一下,为了简洁,我们这里不考虑处理错误,但是你可以在 GitHub 上完整的项目中看到。处理错误总是很重要的,至少打印出错误,这样你可以很快检查到哪些地方没有像预期一样工作:
35+
36+
- (void)fetchAllPods:(void (^)(NSArray *pods))callback page:(NSUInteger)page
37+
{
38+
NSString *urlString = [NSString stringWithFormat:@"http://localhost:4567/specs?page=%d", page];
39+
NSURL *url = [NSURL URLWithString:urlString];
40+
[[[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:
41+
^(NSData *data, NSURLResponse *response, NSError *error) {
42+
id result = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL];
43+
if ([result isKindOfClass:[NSDictionary class]]) {
44+
NSArray *pods = result[@"result"];
45+
callback(pods);
46+
NSNumber* numberOfPages = result[@"number_of_pages"];
47+
NSUInteger nextPage = page + 1;
48+
if (nextPage < numberOfPages.unsignedIntegerValue) {
49+
[self fetchAllPods:callback page:nextPage];
50+
}
51+
}
52+
}] resume];
53+
}
54+
55+
要做的就是这些了。我们解析 JSON,做一些非常粗糙的检查(验证结果是一个字典),然后调用回调函数。
56+
57+
## 将对象装进 Core Data
58+
59+
现在我们可以将 JSON 装进我们的 Core Data store 中了。为了分清,我们创建一个 `Importer` 对象来调用 web service,并且创建或者更新对象。将这些放到一个单独的类中很不错,因为这样我们的 web service 和 Core Data 部分完全解耦。如果我们想要给 store 提供一个不同的 web service 或者在别的某个地方重用 web service,我们现在并不需要手动处理这两种情况。同时,不要在 view controller 中编写逻辑代码,以后我们可以在别的 app 中更容易复用这些组件。
60+
61+
我们的 `Importer` 有两个方法:
62+
63+
@interface Importer : NSObject
64+
- (id)initWithContext:(NSManagedObjectContext *)context
65+
webservice:(PodsWebservice *)webservice;
66+
- (void)import;
67+
@end
68+
69+
通过初始化方法将 context 注入到对象中是一个非常强有力的技巧。当编写测试的时候,我们可以很容易的注入一个不同的 context。同样适用于 web service:我们可以很容易的用一个不同的对象模拟 web service。
70+
71+
`import` 方法负责处理逻辑。我们调用 `fetchAllPods:` 方法,并且对于每一批 pod 说明,我们都会将它们导入到 context 中。通过将逻辑代码包装到 `performBlock:`,context 会确保所有的事情都在正确的线程中执行。然后我们迭代这些说明,并且会为每一个说明生成一个唯一标识符(这些标识符可以是任何独一无二的,只要能确定到唯一一个 model object,正如在 [Drew 的文章](http://objccn.io/issue-10-1/)中解释那样。然后我们试着找到 model object,如果不存在则创建一个。`loadFromDictionary:` 方法需要一个 JSON 字典,并根据字典中的值更新 model object:
72+
73+
- (void)import
74+
{
75+
[self.webservice fetchAllPods:^(NSArray *pods)
76+
{
77+
[self.context performBlock:^
78+
{
79+
for(NSDictionary *podSpec in pods) {
80+
NSString *identifier = [podSpec[@"name"] stringByAppendingString:podSpec[@"version"]];
81+
Pod *pod = [Pod findOrCreatePodWithIdentifier:identifier inContext:self.context];
82+
[pod loadFromDictionary:podSpec];
83+
}
84+
}];
85+
}];
86+
}
87+
88+
上面的代码中有很多地方要注意。首先,查找或创建方法的效率是非常低下的。在生产环境的代码中,你需要批量处理 pods 并且同时找到他们,正如在《导入大数据集》中[「高效地导入数据」](http://objccn.io/issue-4-5/#efficient-importing)这一节中所解释的那样。
89+
90+
第二,我们直接在 `Pod` 类(managed object 的子类)中创建 `loadFromDictionary:`。这意味着我们的 model object 知道 web service。在真实的代码中,我们很有可能将这些放到一个类别中,这样这两个很完美的分开了。对于这个示例,这无关要紧。
91+
92+
## 创建一个独立的后台堆栈
93+
94+
在写上面的代码时,我们会先在在主 managed object context 中拥有一切需要的数据。我们的应用在 table view 控制器中使用一个 fetched results controller 来显示所有的 pods。当 managed object context 中的数据改变时,fetched results controller 自动更新 data model。然而,在主 managed object context 中处理导入数据并不是最优的。主线程可能被堵塞,UI 可能没有反应。大多数时候,在主线程中处理的工作应该是最小限度的,并且造成的延迟应当难以察觉。如果你的情况正是这样,那非常好。然而,如果我们想要做些额外的努力,我们可以在后台线程中处理导入操作。
95+
96+
Apple 在 WWDC 会议以及官方的《Core Data 编程指南》文档的[「Concurrency with Core Data」](https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/CoreData/Articles/cdConcurrency.html) 一节中,对于并发的 Core Data,推荐给开发者两种选择。这两种都需要独立的 managed object contexts,它们要么共享同样的 persistent store coordinator,要么不共享。在处理很多改变时,拥有独立的 persistent store coordinators 提供更出色的性能,因为仅需要的锁只是在 sqlite 级别。拥有共享的 persistent store coordinator 也就意味着拥有共享缓存,当你没有做出很多改变时,这会很快。所以,根据你的情况而定,你需要衡量哪种方案更好,然后选择是否需要一个共享的 persistent store coordinator。当主 context 是只读的情况下,根本不需要锁,因为 iOS 7 中的 sqlite 有写前记录功能并且支持多重读取和单一写入。然而,对于我们的示范目的,我们会使用完全独立堆栈的处理方式。我们使用下面的代码设置一个 managed object context:
97+
98+
- (NSManagedObjectContext *)setupManagedObjectContextWithConcurrencyType:(NSManagedObjectContextConcurrencyType)concurrencyType
99+
{
100+
NSManagedObjectContext *managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:concurrencyType];
101+
managedObjectContext.persistentStoreCoordinator =
102+
[[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.managedObjectModel];
103+
NSError* error;
104+
[managedObjectContext.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType
105+
configuration:nil
106+
URL:self.storeURL
107+
options:nil
108+
error:&error];
109+
if (error) {
110+
NSLog(@"error: %@", error.localizedDescription);
111+
}
112+
return managedObjectContext;
113+
}
114+
115+
然后我们调用这个方法两次,一次是为主 managed object context,一次是为后台 managed object context:
116+
117+
self.managedObjectContext = [self setupManagedObjectContextWithConcurrencyType:NSMainQueueConcurrencyType];
118+
self.backgroundManagedObjectContext = [self setupManagedObjectContextWithConcurrencyType:NSPrivateQueueConcurrencyType];
119+
120+
注意传递的参数 `NSPrivateQueueConcurrencyType` 告诉 Core Data 创建一个独立队列,这将确保后台 managed object context 的运行发生在一个独立的线程中。
121+
122+
现在就剩一步了:每当后台 context 保存后,我们需要更新主线程。我们在[之前第 2 期的这篇文章](http://objccn.io/issue-2-2/)中描述了如何操作。我们注册一下,当 context 保存时得到一个通知,如果是后台 context,调用 `mergeChangesFromContextDidSaveNotification:` 方法。这就是我们要做的所有事情:
123+
124+
[[NSNotificationCenter defaultCenter]
125+
addObserverForName:NSManagedObjectContextDidSaveNotification
126+
object:nil
127+
queue:nil
128+
usingBlock:^(NSNotification* note) {
129+
NSManagedObjectContext *moc = self.managedObjectContext;
130+
if (note.object != moc) {
131+
[moc performBlock:^(){
132+
[moc mergeChangesFromContextDidSaveNotification:note];
133+
}];
134+
}
135+
}];
136+
137+
这儿还有一个小忠告:`mergeChangesFromContextDidSaveNotification:` 是在 `performBlock:`中发生的。在我们这个情况下,`moc` 是主 managed object context,因此,这将会阻塞主线程。
138+
139+
注意你的 UI(即使是只读的)必须有能力处理对象的改变,或者事件的删除。Brent Simmons 最近写了两篇文章,分别是 [《Why Use a Custom Notification for Note Deletion》](http://inessential.com/2014/02/25/why_use_a_custom_notification_for_note_d)[《Deleting Objects in Core Data》](http://inessential.com/2014/02/25/more_about_deleting_objects_in_core_data)。这些文章解释说明了如何面对这些情况,如果你在你的 UI 中显示一个对象,这个对象有可能会发生改变或者被删除。
140+
141+
## 实现从 UI 进行的写入
142+
143+
你可能觉得上面讲的看起来非常简单,这是因为仅有的写操作是在后台线程进行的。在我们当前的应用中,我们没有处理其他方面的合并;并没有来自主 managed object context 中的改变。为了增加这个,你可以采用不少策略。[Drew 的这篇文章](http://objccn.io/issue-10-1/)很好的阐述了相关的方法。
144+
145+
根据你的需求,一个非常简单的模式或许是这样:不管用户何时改变 UI 中的某些东西,你并不改变 managed object context。相反,你去调用 web service。如果成功了,你可以从 web service 中得到改变,然后更新你的后台 context。这些改变随后回被传送到主 context。这样做有两个弊端:用户可能需要一段时间才能看到 UI 的改变,并且如果用户未联网,他将不能改变任何东西。在 [Florian 的文章](http://objccn.io/issue-10-4/)中,描述了我们如何使用不同策略让应用在离线时也能工作。
146+
147+
如果你正在处理合并,你也需要定义一个合并原则。这又是根据特定使用情况而定的。如果合并失败了你可能需要抛出一个错误,或者总是给某一个 managed object context 优先权。[NSMergePolicy](https://developer.apple.com/library/mac/documentation/CoreData/Reference/NSMergePolicy_Class/Reference/Reference.html) 类描述出了可能的选择。
148+
149+
## 结论
150+
151+
我们已经看到如何实现一个简单的只读应用,这个应用能将从 web service 取得的大量数据导入到 Core Data。通过使用后台 managed object context,我们已经建立了一个不会阻塞 UI(除非正在处理合并)的 Core Data 程序。
152+
153+
---
154+
155+
[话题 #10 下的更多文章](http://objccn.io/issue-10)
156+
157+
原文 [A Networked Core Data Application](http://www.objc.io/issue-10/networked-core-data-application.html)

0 commit comments

Comments
 (0)