本教程适用于正在使用FMDB,并期望迁移到WCDB的开发者。
为什么要迁移到WCDB?
WCDB依托于微信上亿用户的实际场景,解决了许多在开发和线上遇到的共性问题,在性能、易用性、功能完整性以及兼容性上都有较好的表现。并且,开发者可以平滑地从FMDB升级到WCDB。
高效
WCDB在并发、ORM以及SQLite源码都做了许多针对性的优化,使得在写入、多线程并发、初始化等方面比FMDB有30%-280%的性能提升。
读操作性能对比
写操作性能对比
批量写操作性能对比
多线程读写操作性能对比
初始化性能对比
更多benchmark的数据和测试代码,请参考:性能数据与Benchmark。
易用
WCDB通过WINQ和ORM,使得从拼接SQL、获取数据、拼装Object的整个过程,只需要一行代码即可完成。
- /*
- FMDB Code
- */
- FMResultSet *resultSet = [fmdb executeQuery:@"SELECT * FROM message"];
- NSMutableArray<Message *> *messages = [[NSMutableArray alloc] init];
- while ([resultSet next]) {
- Message *message = [[Message alloc] init];
- message.localID = [resultSet intForColumnIndex:0];
- message.content = [resultSet stringForColumnIndex:1];
- message.createTime = [NSDate dateWithTimeIntervalSince1970:[resultSet doubleForColumnIndex:2]];
- message.modifiedTime = [NSDate dateWithTimeIntervalSince1970:[resultSet doubleForColumnIndex:3]];
- [messages addObject:message];
- }
- /*
- WCDB Code
- */
- NSArray<Message *> *messages = [wcdb getAllObjectsOfClass:Message.class fromTable:@"message"];
完整
WCDB基于微信的真实场景,对数据库的常见需求都提供了对应的解决方案,包括:
- 错误统计
- 性能统计
- 损坏修复
- 反注入
- 加密
- …
对比与迁移
安装
首先在工程的配置Build Phases
->Link Binary With Libraries
中,将FMDB以及SQLite的库移出工程。
然后参考安装教程选择适合方式链入WCDB的库。
创建数据库
WCTDatabase
通过指定路径进行创建。同时,该接口会自动创建路径中未创建的目录。
- NSString* path = @"intermediate/directory/will/be/created/automatically/wcdb";
- WCTDatabase* wcdb = [[WCTDatabase alloc] initWithPath:path];
临时数据库可以创建在iOS/macOS的临时目录上
- NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:@"tmp.db"];
- WCTDatabase* wcdb = [[WCTDatabase alloc] initWithPath:path];
WCDB暂不支持创建内存数据库。由于移动平台的磁盘介质大多为SSD,其性能与纯内存操作差别不大。同时内存数据库会占用大量内存,从而导致FOOM。
打开数据库
WCDB会在第一次访问数据库时,自动打开数据库,不需要开发者主动操作。
canOpen
接口可用于测试数据库能否正常打开,isOpened
接口可用于测试数据库是否已打开。
- if (![wcdb canOpen]) {
- NSLog(@"open failed");
- }
- if ([wcdb isOpened]) {
- NSLog(@"database is already opened");
- }
建表与ORM
FMDB不支持ORM,而WCDB可以通过绑定类与表绑定起来,从而大幅度减少代码量。
对于在FMDB已经定义的类:
- //Message.h
- @interface Message : NSObject
- @property int localID;
- @property(retain) NSString *content;
- @property(retain) NSDate *createTime;
- @property(retain) NSDate *modifiedTime;
- @end
和表:
- FMDatabase* fmdb = [[FMDatabase alloc] initWithPath:path];
- [fmdb executeUpdate:@"CREATE TABLE message(localID INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT, createTime INTEGER, db_modifiedTime INTEGER)"];
- [fmdb executeUpdate:@"CREATE INDEX message_index ON message(createTime)"];
可以将其建模为
- //Message.h
- @interface Message : NSObject <WCTTableCoding>
- @property int localID;
- @property(retain) NSString *content;
- @property(retain) NSDate *createTime;
- @property(retain) NSDate *modifiedTime;
- WCDB_PROPERTY(localID)
- WCDB_PROPERTY(content)
- WCDB_PROPERTY(createTime)
- WCDB_PROPERTY(modifiedTime)
- @end
- //Message.mm
- @implementation Message
- WCDB_IMPLEMENTATION(Message)
- WCDB_SYNTHESIZE(Message, localID)
- WCDB_SYNTHESIZE(Message, content)
- WCDB_SYNTHESIZE(Message, createTime)
- WCDB_SYNTHESIZE_COLUMN(Message, modifiedTime, "db_modifiedTime")
- WCDB_PRIMARY_AUTO_INCREMENT(Message, localID)
- WCDB_INDEX(Message, "_index", createTime)
- @end
其中:
WCDB_IMPLEMENTATION(className)
用于定义进行绑定的类WCDB_PROPERTY(propertyName)
和WCDB_SYNTHESIZE(className, propertyName)
用于声明和定义字段。WCDB_SYNTHESIZE(className, propertyName)
默认使用属性名作为数据库表的字段名。对于属性名与字段名不同的情况,可以使用WCDB_SYNTHESIZE_COLUMN(className, propertyName, columnName)
进行映射。- 对于在FMDB已经创建的表,若属性名与字段名不同,则可以用
WCDB_SYNTHESIZE_COLUMN
宏进行映射,如例子中的db_modifiedTime字段
- 对于在FMDB已经创建的表,若属性名与字段名不同,则可以用
WCDB_PRIMARY_AUTO_INCREMENT(className, propertyName)
用于定义主键且自增。WCDB_INDEX(className, indexNameSubfix, propertyName)
用于定义索引。
定义完成后,调用createTableAndIndexesOfName:withClass:
即可完成创建。
- WCTDatabase* wcdb = [[WCTDatabase alloc] initWithPath:path];
- [wcdb createTableAndIndexesOfName:@"message" withClass:Message.class]
注:该接口使用的是IF NOT EXISTS
的SQL,因此可以用重复调用。不需要在每次调用前判断表或索引是否已经存在。
更多关于ORM的定义,请参考:ORM使用方法。
数据库升级
createTableAndIndexesOfName:withClass:
会根据ORM的定义,创建表或索引。
当定义发生变化时,该接口也会对应的增加字段或索引。
因此,该接口可用于数据库表的升级。
- //Message.h
- @interface Message : NSObject <WCTTableCoding>
- @property int localID;
- @property(assign) const char *newContent;
- //@property(retain) NSDate *createTime;
- @property(retain) NSDate *modifiedTime;
- @property(retain) NSDate *newProperty;
- WCDB_PROPERTY(localID)
- WCDB_PROPERTY(newContent)
- //WCDB_PROPERTY(createTime)
- WCDB_PROPERTY(modifiedTime)
- WCDB_PROPERTY(newProperty)
- @end
- //Message.mm
- @implementation Message
- WCDB_IMPLEMENTATION(Message)
- WCDB_SYNTHESIZE(Message, localID)
- WCDB_SYNTHESIZE_COLUMN(Message, newContent, "content")
- //WCDB_SYNTHESIZE(Message, createTime)
- WCDB_SYNTHESIZE_COLUMN(Message, modifiedTime, "db_modifiedTime")
- WCDB_SYNTHESIZE(Message, newProperty)
- WCDB_PRIMARY_AUTO_INCREMENT(Message, localID)
- WCDB_INDEX(Message, "_index", createTime)
- WCDB_UNIQUE(Message, modifiedTime)
- WCDB_INDEX(Message, "_newIndex", newProperty)
- @end
- WCTDatabase* db = [[WCTDatabase alloc] initWithPath:path];
- [db createTableAndIndexesOfName:@"message" withClass:Message.class]
删除字段
如例子中的createTime
字段,删除字段只需直接将ORM中的定义删除即可。
注:由于SQLite不支持删除字段,因此该操作只是将对应字段忽略。
增加字段
如例子中的newProperty
字段,增加字段只需直接在ORM定义出添加,并再次调用createTableAndIndexesOfName:withClass:
。
修改字段
如例子中的newContent
字段,字段类型可以直接修改,但需要确保新类型与旧类型兼容;字段名称则需要通过WCDB_SYNTHESIZE_COLUMN(className, proeprtyName, columnName)
重新映射到旧字段。
注:由于SQLite不支持修改字段名,因此该操作只是将新的属性映射到原来的字段名。
增加约束
如例子中的WCDB_UNIQUE(Message, modifiedTime)
,新的约束只需直接在ORM中添加,并再次调用createTableAndIndexesOfName:withClass:
。
增加索引
如例子中的WCDB_INDEX(Message, "_newIndex", newProperty)
,新的索引只需直接在ORM添加,并再次调用createTableAndIndexesOfName:withClass:
。
访问数据库
得益于ORM的定义,开发者无需使用类似intForColumnIndex:
的接口手动组装Object。以下是增删查改的代码示例。
查询
- /*
- FMDB Code
- */
- FMResultSet *resultSet = [fmdb executeQuery:@"SELECT * FROM message"];
- NSMutableArray<Message *> *messages = [[NSMutableArray alloc] init];
- while ([resultSet next]) {
- Message *message = [[Message alloc] init];
- message.localID = [resultSet intForColumnIndex:0];
- message.content = [resultSet stringForColumnIndex:1];
- message.createTime = [NSDate dateWithTimeIntervalSince1970:[resultSet doubleForColumnIndex:2]];
- message.modifiedTime = [NSDate dateWithTimeIntervalSince1970:[resultSet doubleForColumnIndex:3]];
- [messages addObject:message];
- }
- /*
- WCDB Code
- */
- NSArray<Message *> *messages = [wcdb getAllObjectsOfClass:Message.class fromTable:@"message"];
插入
- /*
- FMDB Code
- */
- [fmdb executeUpdate:@"INSERT INTO message VALUES(?, ?, ?, ?)", @(message.localID), message.content, @(message.createTime.timeIntervalSince1970), @(message.modifiedTime.timeIntervalSince1970)];
- /*
- WCDB Code
- */
- [wcdb insertObject:message into:@"message"];
修改
- /*
- FMDB Code
- */
- [fmdb executeUpdate:@"UPDATE message SET modifiedTime=?", @(message.modifiedTime.timeIntervalSince1970)];
- /*
- WCDB Code
- */
- [wcdb updateAllRowsInTable:@"message" onProperties:Message.modifiedTime withObject:message];
删除
- /*
- FMDB Code
- */
- [fmdb executeUpdate:@"DELETE FROM message"];
- /*
- WCDB Code
- */
- [wcdb deleteAllObjects];
条件语句
WCDB通过WINQ完成条件语句,以减轻了拼装SQL的繁琐,并提供一系列优化和反注入等特性。
WINQ和字段映射
对于已定义ORM的类,通过className.propertyName
获取数据库字段的映射。
以下是SQL和WINQ之间转换的一些例子。
类型 | SQL示例 | WINQ示例 |
---|---|---|
排序 | ORDER BY localID ASC | Message.localID.order(WCTOrderedAscending) |
多字段排序 | ORDER BY localID ASC, content DESC | {Message.localID.order(WCTOrderedAscending), Message.content.order(WCTOrderedDescending)} |
聚合函数 | MAX(localID) | Message.localID.max() |
条件语句 | localID==2 AND content IS NOT NULL | Message.localID==2&&Message.content.isNotNull() |
多个字段组合 | localID, content | {Message.localID, Message.content} |
| COUNT() | Message.AnyProperty.count() |
所有ORM定义的字段 | (localID, content, createTime, modifiedTime) | Message.AllProperties |
指定table | myTable.localID | Message.localID.inTable("myTable") |
改写条件语句
了解了WINQ,就可以完成更复杂的增删查改操作了。
部分查询
- /*
- FMDB Code
- */
- NSMutableArray<Message *> *messages = [[NSMutableArray alloc] init];
- FMResultSet* resultSet = [fmdb executeQuery:@"SELECT localID, createTime FROM message WHERE localID>=1 OR modified!=createTime"];
- while (resultSet && [resultSet next]) {
- Message *message = [[Message alloc] init];
- message.localID = [resultSet intForColumnIndex:0];
- message.createTime = [NSDate dateWithTimeIntervalSince1970:[resultSet doubleForColumnIndex:2]];
- [messages addObject:message];
- }
- /*
- WCDB Code
- */
- NSArray *messages = [wcdb getObjectsOnResults:{Message.localID, Message.createTime}
- fromTable:@"message"
- where:Message.localID>1||Message.modifiedTime!=Message.createTime];
自增插入
- /*
- FMDB Code
- */
- [fmdb executeUpdate:@"INSERT INTO message(localID, content) VALUES(?, ?)", nil, message.content];
- /*
- WCDB Code
- */
- message.isAutoIncrement = YES;
- [wcdb insertObject:message
- onProperties:{Message.localID, Message.content}
- into:@"message"];
数值更新
- /*
- FMDB Code
- */
- [fmdb executeUpdate:@"UPDATE message SET modifiedTime=? WHERE localID==?", @([NSDate date].timeIntervalSince1970), @(1)];
- /*
- WCDB Code
- */
- [wcdb updateRowsInTable:@"message"
- onProperty:Message.modifiedTime
- withValue:[NSDate date]
- where:Message.localID==1];
部分删除
- /*
- FMDB Code
- */
- [fmdb executeUpdate:@"DELETE FROM message WHERE localID>0 AND content IS NULL LIMIT ?", @(1)];
- /*
- WCDB Code
- */
- [wcdb deleteObjectsFromTable:@"messsage"
- where:Message.localID>0&&Message.content!=nil
- limit:1];
更多关于增删查改接口的用法,可参考:CRUD教程
特殊语句和核心层接口
WCDB的ObjC层接口封装了绝大部分场景下适用的增删查改语句。但SQL千变万化,接口层不可能覆盖全部场景。对于这种情况,可以通过WINQ的核心层接口进行调用。
对于SQL:EXPLAIN QUERY PLAN CREATE TABLE message(localID INTEGER)
。
找到其对应的sql-stmt,然后通过以
WCDB::Statement
开头的类进行调用。如例子中,其对应的sql-stmt为WCDB::StatementExplain
和WCDB::StatementCreateTable
。获取字段映射。对于已经定义ORM的字段,可以通过
className.propertyName
获取,如:Message.localID
。对于未定义ORM的字段,可以通过WCDB::Column columnName("columnName")
创建,如WCDB::Column localID("localID")
。根据
Statement
内的定义,按照与SQL同名的函数调用获得完整的WINQ语句。如例子中,其对应的WINQ语句为
- WCDB::ColumnDefList columnDefList = {WCTSampleORM.identifier.def(WCTColumnTypeInteger32, true)};
- WCDB::StatementExplain statementExplain = WCDB::StatementExplain().explainQueryPlan(WCDB::StatementCreateTable().create("message", columnDefList));
- 通过
getDescription()
打印log,调试确保SQL正确
- NSLog(@"SQL: %s", statementExplain.getDescription().c_str());
执行WINQ
通过exec:
执行WINQ statement。
- [wcdb exec:statement];
获取WINQ运行结果
通过prepare:
运行WINQ statement,获得WCTStatement
,并以此获取返回值。
- WCTStatement *statement = [wcdb prepare:statementExplain];
- if (statement && [statement step]) {
- for (int i = 0; i < [statement getCount]; ++i) {
- NSString *columnName = [statement getNameAtIndex:i];
- WCTValue *value = [statement getValueAtIndex:i];
- NSLog(@"%@:%@", columnName, value);
- }
- }
该接口风格与FMDB类似。
更多关于示例代码,可以参考核心层接口
事务
WCDB的基础事务接口与FMDB的接口类似。
- /*
- FMDB Code
- */
- BOOL result = [fmdb beginTransaction];
- if (!result) {
- //failed
- }
- //do sth...
- if (![fmdb commit]) {
- //failed
- [fmdb rollback];
- }
- /*
- WCDB Code
- */
- BOOL result = [wcdb beginTransaction];
- if (!result) {
- //failed
- }
- //do sth...
- if (![wcdb commitTransaction]) {
- [wcdb rollbackTransaction];
- }
便捷事务接口
runTransaction:
接口会在commit失败时自动rollback事务。开发者也可以在BLOCK
结束时返回YES
或NO
来决定commit或rollback事务,以此减少代码量。
- [wcdb runTransaction:^BOOL{
- //do sth...
- return result;//YES to commit transaction and NO to rollback transaction
- }];
多重语句和批处理
WCDB不支持多重语句。多个语句需拆分单独写。
WCDB对于涉及批量操作的接口,都有内置的事务。如createTableAndIndexesOfName:withClass:
和insertObjects:into:
等,这类接口通常不止执行一条SQL,因此WCDB会自动嵌入事务,以提高性能。
线程安全与并发
FMDB通过FMDatabasePool
完成多线程任务。
而对于WCDB,WCTDatabase
、WCTTable
和WCTTransaction
的所有SQL操作接口都是线程安全,并且自动管理并发的。
WCDB的连接池会根据数据库访问所在的线程、是否处于事务、并发状态等,自动分发合适的SQLite连接进行操作,并在完成后回收以供下一次再利用。
因此,开发者既不需要使用一个新的类来完成多线程任务,也不需要过多关注线程安全的问题。同时,还能获得更高的性能表现。
- /*
- FMDB Code
- */
- //thread-1 read
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
- [fmdbPool inDatabase:^(FMDatabase *_Nonnull db) {
- NSMutableArray *messages = [[NSMutableArray alloc] init];
- FMResultSet *resultSet = [db executeQuery:@"SELECT * FROM message"];
- while ([resultSet next]) {
- Message *message = [[Message alloc] init];
- message.localID = [resultSet intForColumnIndex:0];
- message.content = [resultSet stringForColumnIndex:1];
- message.createTime = [NSDate dateWithTimeIntervalSince1970:[resultSet doubleForColumnIndex:2]];
- message.modifiedTime = [NSDate dateWithTimeIntervalSince1970:[resultSet doubleForColumnIndex:3]];
- [messages addObject:message];
- }
- //...
- }];
- });
- //thread-2 write
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
- [fmdbPool inDatabase:^(FMDatabase *_Nonnull db) {
- [db beginTransaction]
- for (Message *message in messages) {
- [db executeUpdate:@"INSERT INTO message VALUES(?, ?, ?, ?)", @(message.localID), message.content, @(message.createTime.timeIntervalSince1970), @(message.modifiedTime.timeIntervalSince1970)];
- }
- if (![db commit]) {
- [db rollback];
- }
- }];
- });
- /*
- WCDB Code
- */
- //thread-1 read
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
- NSArray *messages = [wcdb getAllObjectsOfClass:Message.class fromTable:@"message"];
- //...
- });
- //thread-2 write
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
- [wcdb insertObjects:messages into:@"message"];
- });
配置
在使用数据库时,通常会对其设置一些默认的配置,如cache_size
、journal_mode
等。
FMDB通过FMDatabasePoolDelegate
进行配置,但其只能在SQLite Handle创建时进行配置。对于已经产生的SQLite handle,很难再次更改配置。
而WCDB可以随时灵活地对其设置或变更。
- /*
- FMDB Code
- */
- - (BOOL)databasePool:(FMDatabasePool *)pool shouldAddDatabaseToPool:(FMDatabase *)database
- {
- FMResultSet* resultSet = [database executeQuery:@"PRAGMA cache_size=-2000"];
- [result next];
- }
- /*
- WCDB Code
- */
- [wcdb setConfig:^BOOL(std::shared_ptr<WCDB::Handle> handle, WCDB::Error &error) {
- return handle->exec(WCDB::StatementPragma().pragma(WCDB::Pragma::CacheSize, -2000));
- } forName:@"CacheSizeConfig"]'
关闭数据库
关闭数据库通常有两种场景:
- 数据库使用结束,回收对象。
- 数据库进行某些操作,需要临时关闭数据库。如移动、复制数据库文件。
回收对象
对于这种情况,开发者无需手动操作。WCDB会自动管理这个过程。对于某一路径的数据库,WCDB会在所有对其的引用释放时,自动关闭数据库,并回收资源。
对于iOS平台,当内存不足时,WCDB会自动关闭空闲的SQLite连接,以节省内存。开发者也可以手动调用[db purgeFreeHandles]
对清理单个数据库的空闲SQLite连接。或调用[WCTDatabase PurgeFreeHandlesInAllDatabases]
清理所有数据库的空闲SQLite连接。
手动关闭数据库
无论是WCDB的多线程管理,还是FMDB的FMDatabasePool,都存在多线程关闭数据库的问题。
即,当一个线程希望关闭数据库时,另一个线程还在继续执行操作。
而某些特殊的操作需要确保数据库完全关闭,例如移动、重命名、删除数据库等文件层面的操作。
例如,若在A线程进行插入操作的执行过程中,B线程尝试复制数据库,则复制后的新数据库很可能是一个损坏的数据库。
因此,WCDB提供了close:
接口确保完全关闭数据库,并阻塞其他线程的访问。
- [wcdb close:^(){
- //do something on this closed database
- }];
隔离Objective-C++代码
WCDB基于WINQ,引入了Objective-C++代码,因此对于所有引入WCDB的源文件,都需要将其后缀.m
改为.mm
。为减少影响范围,可以通过Objective-C的category特性将其隔离,达到只在model层使用Objective-C++编译,而不影响controller和view。
对于已有类WCTSampleAdvance
,
- //WCTSampleAdvance.h
- #import <Foundation/Foundation.h>
- #import "WCTSampleColumnCoding.h"
- @interface WCTSampleAdvance : NSObject
- @property(nonatomic, assign) int intValue;
- @property(nonatomic, retain) WCTSampleColumnCoding *columnCoding;
- @end
- //WCTSampleAdvance.mm
- @implementation WCTSampleAdvance
- @end
可以创建WCTSampleAdvance (WCTTableCoding)
专门用于定义ORM。
为简化定义代码,WCDB同样提供了文件模版
WCTTableCoding文件模版
为了简化定义,WCDB同样提供了Xcode文件模版来创建WCTTableCoding
的category。
首先需要安装文件模版。
- 安装脚本集成在WCDB的编译脚本中,只需编译一次WCDB,就会自动安装文件模版。
- 也可以手动运行
cd path-to-your-wcdb-dir/objc/templates; sh install.sh;
手动安装 文件模版。
- 安装完成后重启Xcode,选择新建文件,滚到窗口底部,即可看到对应的文件模版。
选择
WCTTableCoding
输入需要实现WCTTableCoding
的类这里以
WCTSampleAdvance
为例,Xcode会自动创建WCTSampleAdvance+WCTTableCoding.h
文件模版:
- #import "WCTSampleAdvance.h"
- #import <WCDB/WCDB.h>
- @interface WCTSampleAdvance (WCTTableCoding) <WCTTableCoding>
- WCDB_PROPERTY(<#property1 #>)
- WCDB_PROPERTY(<#property2 #>)
- WCDB_PROPERTY(<#property3 #>)
- WCDB_PROPERTY(<#property4 #>)
- WCDB_PROPERTY(<#... #>)
- @end
- 加上类的ORM实现即可。
- //WCTSampleAdvance.h
- #import <Foundation/Foundation.h>
- #import "WCTSampleColumnCoding.h"
- @interface WCTSampleAdvance : NSObject
- @property(nonatomic, assign) int intValue;
- @property(nonatomic, retain) WCTSampleColumnCoding *columnCoding;
- @end
- //WCTSampleAdvance.mm
- @implementation WCTSampleAdvance
- WCDB_IMPLEMENTATION(WCTSampleAdvance)
- WCDB_SYNTHESIZE(WCTSampleAdvance, intValue)
- WCDB_SYNTHESIZE(WCTSampleAdvance, columnCoding)
- WCDB_PRIMARY_ASC_AUTO_INCREMENT(WCTSampleAdvance, intValue)
- @end
- //WCTSampleAdvance+WCTTableCoding.h
- #import "WCTSampleAdvance.h"
- #import <WCDB/WCDB.h>
- @interface WCTSampleAdvance (WCTTableCoding) <WCTTableCoding>
- WCDB_PROPERTY(intValue)
- WCDB_PROPERTY(columnCoding)
- @end
此时,原来的WCTSampleAdvance.h
中不包含任何C++的代码。因此,其他文件对其引用时,不需要修改文件名后缀。只有Model层需要使用WCDB接口的类,才需要包含WCTSampleAdvance+WCTTableCoding.h
,并修改文件名后缀为.mm
。