Ensembles:一个 CoreData 同步框架
Ensembles拓展了苹果的Core Data框架,为Mac OS和iOS添加了P2P同步。多个SQLite持久化存储可以通过类似iCloud或者Dropbox的文件同步平台耦合在一起。该框架可容易地被拓展以支持任何可在设备间移动文件的服务,包括自定义服务器。
下载
使用以下命令将 Ensembles 下载到本地
1 |
git clone https://github.com/drewmccormack/ensembles.git |
Ensemble 使用了 Git 子模块(submodules)。如果想获取子模块,首先进入 ensembles 根目录
1 |
cd ensembles |
然后 issue 下命令
1 |
git submodule update --init |
将Ensembles 添加到 iOS工程中
使用CocoaPods向Xcode工程添加Ensembles
1.向Podfile中写入以下内容
1 2 |
platform :ios, '7.0' pod "Ensembles", "~> 1.0" |
手动添加Ensembles 静态库到Xcode工程中
- 在Finder中,将Framework目录下的 Ensembles iOS.xcodeproj 文件拖入到你的项目中。
- 在左侧资源列表中选择应用资源列表等根文件,然后选择 app target。
- 在 General 标签下,点击 Linked Frameworks and Libraries 下方的“+”。
- 选择添加 libensembles.a 。
- 切换到“Build Settings”标签下“Locate the Other Linker Flags“并添加”-ObjC“。
- 切换到”Build Phases“标签下,展开“Target Dependencies”后点击“+”。
- 找到“Ensembles Resources iOS”并添加。
- 在左侧资源列表中打开工程“Ensembles iOS.xcodeproj”,并展开其下的 Products 文件夹,找到 Ensembles.bundle。
- 将Ensembles.bundle 拖拽到你项目中的 Copy Bundle Resources 下。
- 在预编译头文件中,或其他任何需要使用Ensembles的文件中添加以下代码。
1 |
#import <Ensembles/Ensembles.h> |
以模块方式使用Ensembles
- 在Finder中,将Framework目录下的 Ensembles iOS.xcodeproj 文件拖入到你的项目中。
- 在左侧资源列表中选择应用的工程根文件,然后选择应用的target。
- 在“General”标签下,点击“Linked Frameworks and Libraries”下方的“+”。
- 选择并添加 Ensebles.framework。(注意不要选成Mac framework)
- 在文件中导入Ensembles
Objective-C
1 |
#import <Ensembles/Ensembles.h> |
Swift
1 |
import Ensembles |
向 OS X 工程中导入Ensembles
使用CocoaPods向Xcode 工程中添加 Ensembles
1.向 Podfile 中添加以下内容
1 2 |
platform :osx, '10.9' pod "Ensembles", "~> 1.0" |
2.选择项目左侧资源列表根文件,并选择应用的target。
3.在“General”标签中,点击“Linked Frameworks and Libraries” 下的“+”。
4.选择并添加Ensembles.framework。
5.如果你的app bundle中没有build phase,则按以下步骤新建:
- 选择工程资源列表根文件,然后选择应用的target。
- 打开Build Phases标签。
- 点击顶部的+按钮。
- 在弹出菜单中选择New Copy Files Build Phase。
- 展开新的Copy Files,Destination 改为 Frameworks。
- 点击Copy Files底部的+按钮,选择并添加Ensembles.framework。
6.切换到 build setting中的“Runpath Search Path”下,添加@loader_path/../Frameworks
。
7.在预编译头文件或需要使用Ensembles的地方,添加以下代码
1 |
#import <Ensembles/Ensembles.h> |
导入可选的云服务
默认情况下,Ensembles 只支持 iCloud。如果需要使用其他云服务,例如 Dropbox,你需要再按以下步骤进行。
如果你使用了CocoaPods,将可选的 subspec 添加到 Podfile。例如你想使用Dropbox,那么添加
1 |
pod "Ensembles/Dropbox", "~> 1.0" |
如果你没使用CocoaPods,而是手动添加的Ensembles,对于你想支持的服务,你需要找到其相关的源文件和框架。你可以在 Vendor文件夹下找到需要框架,在Framework/Extensions文件夹下找到其相关源文件。
例如,如果你想支持Dropbox,你需要将DropboxSDK Xcode项目添加为依赖,并链接到相应的产品库(product library),同时在你的项目中包含文件CDEDropboxCloudFileSystem.h 和 CDEDropboxCloudFileSystem.m。
Idiomatic App
Idiomatic 是一款使用了Ensembles开发的App,使用iCloud或者Dropbox可以在不同设备间进行同步。该应用可以帮助你记录你的想法,包括照片,并添加标签进行分组。应用的Core Data model 有三个实体,并包含多对多的关系(relationship)。
可以通过Idiomatic来理解如何在Core Data 应用中集成Ensembles。如果你想了解Idiomatic如何工作,可以从 App Store 下载安装。也可以按一下步骤进行编译安装;
- 在Xcode左侧资源树选择Idiomatic项目,并选择Idiomatic target。
- 选择Capabilities标签,并打开iCloud开关。
- 在你的设备和模拟器中编译安装,并登陆相同的iCloud账号。
添加一个note,并为其添加标签。当打开app时会进行同步,不过你也可以通过点击Groups表格下的按钮强制同步。
对于Dropbox,可以通过 The Mental Faculty 账号进行同步,如果要使用自己的开发者账号,需要做一下几步:
1.在developer.dropbox.com 注册Dropbox 开发者账号。
2.在App Console中点击 Create app 按钮。
3.API类型选择Dropbox API。
4.选择保存类型 ‘Files and Datastores’。
5.选择‘Yes — My app only needs access to files it creates’。
6.输入应用名称(例如Idiomatic)。
7.点击创建应用。
8.找到Idiomatic项目中的IDMSyncManager类,找到以下代码,并将AppKey 和 AppSecret 内容替换为刚刚在Dropbox网站创建的应用。
1 2 |
NSString * const IDMDropboxAppKey = @"xxxxxxxxxxxxxxx"; NSString * const IDMDropboxAppSecret = @"xxxxxxxxxxxxxxx"; |
9.在Xcode中选择Idiomatic project,再选择Idiomatic iOS target。
10.切换到 Info 标签。
11.打开URL Types 小节,并将 URL Schemes 入口更改为
1 |
db-<你的Dropbox App Key> |
Idiomatic还支持另一个同步服务:IdioSync。IdioSync是一个基于Node.js服务器定制服务,利用 Amazon S3存储。在ensembles.io 购买 Priority Support Package 即可获取服务器源码。
初步了解Ensembles
在使用Ensembles之前,需要现在预编译头文件或代码源文件中导入库的头文件。
1 |
#import <Ensembles/Ensembles.h> |
类 CDEPersistentStoreEnsemble
是Ensembles框架中最重要的类。你需要为每个需要同步的NSPersistentStore
实例化一个CDEPersistentStoreEnsemble
对象。这个类将监听SQLite保存操作,并与其他设备进行同步。
一般情况下,当你创建Core Data栈(stack)时就要初始化CDEPersistentStoreEnsemble
实例。在数据保存前初始化ensemble非常必要。
同时你还要注意遵从CDECloudFileSystem协议的类。任何遵从该协议的类,都可以将ensemble进行后端同步,在不同设备之间传输数据。你可以使用现有的类(如 CDEICloudFileSystem
),也可以自己开发。
初始化ensemble只需要以下几行代码:
1 2 3 4 5 6 7 8 9 10 |
// 创建 Ensemble cloudFileSystem = [[CDEICloudFileSystem alloc] initWithUbiquityContainerIdentifier:@"P7BXV6PHLD.com.mentalfaculty.idiomatic"]; ensemble = [[CDEPersistentStoreEnsemble alloc] initWithEnsembleIdentifier:@"MainStore" persistentStorePath:storeURL.path managedObjectModelURL:modelURL cloudFileSystem:cloudFileSystem]; ensemble.delegate = self; |
在 cloud file system 初始化后,它将和饱含NSManagedObjectModel
文件的URL、NSPersistentStore
的路径(path),一同传入到 CDEPersistentStoreEnsemble
的初始化方法中。ensemble的identifier用来在不同设备间匹配相同存储文件,因此它尤为重要。
一旦CDEPersistentStoreEnsemble
完成初始化,就可以与其绑定。创建一个ensemble实例并使用本地持久化存储进行初始化,只要一次就够了。一旦绑定完成,除非你明确的解绑、或者云文件系统出现严重问题(如切换账户),即使重启都不会影响ensemble。
可以通过isLeeched
属性来查询ensemble对象是否绑定成功,leechPersistentStoreWithCompletion:
方法查看初始化进程(如果尝试绑定一个已经绑定过的ensemble对象,将出错)
1 2 3 4 5 |
if (!ensemble.isLeeched) { [ensemble leechPersistentStoreWithCompletion:^(NSError *error) { if (error) NSLog(@"Could not leech to ensemble: %@", error); }]; } |
因为Ensembles包含大量网络操作和长时间操作,所以大部分方法均为异步方法,这些方法拥有错误参数(error parameter)的回调块(block)。如果error 为 nil
,那么任务执行成功。这些方法需要在主线程(main thread)中调用,同时回调方法也将在主队列(main queue)中执行。
当ensemble对象绑定完成后,可以使用mergeWithCompletion
方法进行同步操作:
1 2 3 |
[ensemble mergeWithCompletion:^(NSError *error) { if (error) NSLog(@"Error merging: %@", error); }]; |
一次合并包括从云端下载其他设备中的最新数据,后台的NSManagedObjectedContenxt
读取数据后与本地最新数据进行合并,再通过NSPersistentStore
保存。
当执行合并操作时,将更改的数据合并到NSManagedObjectedContenxt
是非常重要的一步。此操作可以在代理方法persistentStoreEnsemble:didSaveMergeChangesWithNotification:
中进行。
1 2 3 4 5 6 |
- (void)persistentStoreEnsemble:(CDEPersistentStoreEnsemble *)ensemble didSaveMergeChangesWithNotification:(NSNotification *)notification { [managedObjectContext performBlock:^{ [managedObjectContext mergeChangesFromContextDidSaveNotification:notification]; }]; } |
要注意此处调用的上下文为后台执行的上下文(context)。你需要确保调用mergeChangesFromContextDidSaveNotification:
方法的线程与主上下文(main context)进行通信。
你可以通过下面的代理方法来为managed objects 提供全局标示(identifiers)。
1 2 3 4 5 |
- (NSArray *)persistentStoreEnsemble:(CDEPersistentStoreEnsemble *)ensemble globalIdentifiersForManagedObjects:(NSArray *)objects { return [objects valueForKeyPath:@"uniqueIdentifier"]; } |
这个方法同样在后台线程中调用,注意只能访问在此线程中传递的对象。
并不强制提供全局标识符(global identifier),如果提供了,框架将自动确保对象不会因为不同设备中多次导入而重复。如果不提供全局标识符,那么框架将自动为对象提供一个新的唯一标识符。
如果你决定提供全局标识符,它们如何生成、如何保存都有你决定。一个常见的选择是对数据模型(data model)的实体(entity)添加一个额外的属性(attribute),将其设为UUID进行保存。
排除错误
Ensembles 有内置的日志系统,默认只记录错误。开发过程中,使用详细日志记录,可以看到框架做了什么。只需要在启动过程中调用以下方法:
1 |
CDESetCurrentLoggingLevel(CDELoggingLevelVerbose); |
单元测试
每个平台的Ensembles框架都饱含单元测试,运行测试:打开Xcode工作空间,在顶部工具栏选择 Ensembles Mac 或者Ensembles iOS target,选择菜单项 Product > Test。