目前移动应用商店中的大部分的App,都存在iOS和Android两个平台的版本,并且两个版本的UI,底层逻辑大致都相同。而相同的部分,却需要不同平台的开发人员实现两次。
通过对比现有的方案,我们也设计了一个跨平台的解决方案,基于C++实现,来解决不同平台上的逻辑需要重复实现的问题。 该方案命名为Core Component(以下简称CC)。
明确了我们的目的,我们开始选择CC核心的功能需要涉及到的技术,选择过程中,主要考虑到跨平台性,同类技术横向对比中,性能处于前列等方面。
我们使用C++来编写CC核心模块,然后增加一层适配层,用来连接各个平台和CC。在iOS中,可以使用Objective-C++来做适配层;在Android中,可以通过NDK来调到C++中。
由于适配层大多是处理一些类型转换,线程切换,api调用等操作,因此适配层的代码其实是可以自动生成的,后面会介绍我们自己实现的适配层代码自动生成器。
我们最终选择的C++11,已经包含了很多新的特性(”C++11 feels like a whole new language” -Bjarne Stroustrup, creator of C++),例如lambdas,smart pointers等等,能够在大多数场景下满足我们的需求。
CC层最核心的一部分,即是数据的逻辑以及存储,因此在数据存储上,我们使用了在移动端普遍使用的SQLite。SQLite的C api不是那么容易使用,不过现在已经有很多库将SQLite封装成面向对象的接口(就像Objective-C中的FMDB)。
在网络库方面,我们选择了cURL,cURL强大的网络处理能力,使得我们能够很容易的与Server进行交互,以及监控相应的网络数据流量,耗时等信息,方便后续的调整优化。
在CC层与Client,Server之间的数据传递方面,我们挑选了几种候选方案,最终选择了利用 Thrift 来传递数据的方案。
Wrapper
类似Dropbox使用的技术,需要CC层的每个数据对象,在平台层都有相对应的对象(二者的成员变量也需要相对应),然后在平台层对象的构造函数中(initWith*,以Objective-C为例),传入一个CC层的对象指针,然后在构造函数内部,将CC层对象的属性,转换成平台类型的属性(如下所示)。
这种方案的缺点在于,需要维护大量的适配层的代码。
– (id)initWithPhotoItemStruct:(const struct dbx_photo_item *)item {
if ((self = [super init])) {
_itemId = [[NSString alloc] initWithUTF8String:item->dpi_id];
_fileInfo = [[DBFileInfo alloc] initWithInfoStruct:&(item->dpi_file_metadata)];
_timeTaken = [DBUtilDateFromISO8601String(item->dpi_time_taken) retain];
// …
}
return self;
}
共享内存
同样需要CC层的每个数据对象,在平台层都有相对应的对象(二者的成员变量也需要相对应),与Wrapper方式不同之处在于,这种方案在平台层的对象中,封装一个C++的对象,然后重载平台层对象的getter方法,当Client需要访问某些属性时,实际上是将C++对象的属性转换成平台的类型,然后返回给调用者(如下所示)。
这种方案与Wrapper方案类似,缺点也是需要维护大量的适配层的代码,以及内存管理的问题,类型频繁转换的性能开销问题。
– (NSString *)itemId
// _obj为CC层的对象指针
return [[NSString alloc] initWithUTF8String:_obj->item_id];
}
bool convertCCThriftToOCThrift(apache::thrift::TBase *ori, id<TBase> dst) { if (!ori) return false; std::shared_ptr<CTMemoryBuffer> trans(new CTMemoryBuffer()); std::shared_ptr<CTProtocol> proto(new CTBinaryProtocol(trans)); ori->write(proto.get()); std::string binaryStr = trans->getBufferAsString(); NSData *bin = [NSData dataWithBytes:binaryStr.c_str() length:binaryStr.size()]; TMemoryBuffer *buf = [[TMemoryBuffer alloc] initWithData:bin]; TBinaryProtocol *pro = [[TBinaryProtocol alloc] initWithTransport:buf]; [dst read:pro]; return true; }
由于CC层只处理逻辑相关的部分,与UI无关,因此CC层不需要使用主线程来进行相关操作。反而需要避免一些耗时操作在主线程上执行,导致UI卡顿。因此我们在封装模块时,集成了各自的线程池,在api的入口处,切换到模块的线程,然后再执行任务,最后异步返回结果(由于使用了支持lambdas的C++11,异步返回变得很好实现)。目前模块中的线程池主要有:
线程池的实现,网上开源的库有很多,我们采用了C++11实现的一个开源的 ThreadPool 。
存储部分算是CC的核心模块之一,需要负责与Server的数据同步,数据缓存的更新。根据数据的获取形式不同,我们数据存储形式主要有数据库存储,以及文件存储。
db storage
由于SQL查询的便利,因此数据库中,我们主要存储需要条件查询的数据,例如需要分页加载的数据。
实现方面,我们将SQLite的C api封装成面向对象的接口,供各个模块调用,例如:
“` C++
std::stringstream sql;
sql << "DELETE FROM " << table_name
<< " WHERE "
<< kColId << " = :id;";
// 由于使用了线程池,因此我们封装的api均为异步调用。
db_->ExecuteStatementAsync(sql.str(), [=](sqlite::database* db, sqlite::statement* stmt) {
if (!stmt || !db) {
LOGE("execute statement failed");
return;
}
stmt->bind(":id", obj_id);
db->execute(*stmt);
});</li>
</ul>
<br />- file storage 文件存储中,主要存储一些配置/记录相关的数据。 实现方面,我们将需要保存的Thrift对象序列化,然后写入文件。每次更新配置/记录时,同时更新文件存储。 ### 网络 > CC的网络模块,是整个App业务相关的网络请求的出口。 cURL库本身也是纯C的库,因此我们首先对cURL进行了封装,同样将其api封装成面向对象的接口。 因为Client和Server交互是利用Thrift(部分api利用JSON),因此网络交互过程中,还涉及到Thrift的序列化/反序列化。因此我们根据需要解析的数据类型(Thrift,JSON等)不同,将其封装成多种Request对象(例如ThriftRequest,JsonRequest等)。 在使用过程中,只需要创建一个Request对象,设置相关参数,然后丢到请求队列中即可,例如: ``` C++ std::string url = "***"; std::shared_ptr<ThriftRequest<thrift::Comment>> request = std::make_shared<ThriftRequest<thrift::Comment>>(http::POST, url); request->SetBody(comment); // listener 为解析请求结果的回调 request->OnResponse(listener); ServerRequestManager::GetInstance().AddRequest(request);
网络模块中,有时候还需要统计相关的数据,或者做一些容错处理。因此我们在网络模块中,增加了一个类似于Hub的功能,作为所有的Request的出入口。
业务模块中,可以编写相应的出口/入口检测函数,然后注册到Hub中,Hub在发出Request/收到Response时,调用函数对其进行检测,例如:
“` C++
http::ResponseDetectFunc userid_detect_func = [=](const http::Request* const request, const http::ResponseData& response) {
if (!request) {
return true;
}
if (response.curl_code != CURLE_OK || response.status_code != http::HTTP_OK) {
return true;
}
<pre><code>bool do_request_without_auth = request->GetTag<bool>(REQUEST_TAG_DO_REQUEST_WITHOUT_AUTH);
if (!do_request_without_auth) {
int64_t user_id = request->GetTag<int64_t>(REQUEST_TAG_KEY_USER_ID);
if (user_id == 0 ||
Config::GetInstance().GetUserId() != user_id) {
LOGW("UserId detect failed. user_id in config: %lld, user_id in request: %lld, request url: %s", Config::GetInstance().GetUserId(), user_id, request->url().c_str());
return false;
}
}
return true;
</code></pre>
};http::RequestHub::GetInstance().RegisterResponseDetectHook(userid_detect_func);
<br />## 一些细节 ### 适配层的代码自动生成 由于适配层多是一些数据类型的转换,api调用等操作,这部分代码的构成大致相同,以iOS-CC为例: ``` Objective-C + (void)useCoupon:(KPCouponRequestParam*)arg0 finished:(void(^)(KPErrorInfo*, KPCoupon*))arg1 { std::shared_ptr<cc::thrift::CouponRequestParam> cpp_arg0 = std::make_shared<cc::thrift::CouponRequestParam>(); if (!convertOCThriftToCCThrift(arg0, cpp_arg0.get())) { arg1(nil, nil); return ; } cc::CouponManager::GetInstance().UseCoupon(cpp_arg0, [=](cc::ErrorInfoPtr err, const std::shared_ptr<cc::thrift::Coupon>& ret) { KPCoupon *value = [[KPCoupon alloc] init]; if (!convertCCThriftToOCThrift(ret.get(), value)) { value = nil; } dispatch_async(dispatch_get_main_queue(), ^{ arg1(convertCCErrorToOCError(err), value); }); }); return; }
L75~79:实现了Objective-C的数据结构转换成C++的数据结构;
L80:调用CC层的api;
L81~L88:将调用的结果的C++数据结构转换成Objective-C的数据结构。
如上述例子所示,不同的api,在适配层只是api名称,参数类型等不同,为了避免重复工作,我们实现了一个代码生成器(CodeGenerator),用于生成这部分代码。CodeGenerator的思路如下:
我们在CC层统一了数据更新的模型,利用Observer模式,所有的数据变动,都由CC层发送一个通知,通知给各个注册的Observer。例如下拉刷新,Client只需要预先注册一个Observer,然后在刷新的时候,调用Refresh,不需要传递任何pageSize,offset等参数,CC层自己从内存中保存的数据获取相关信息,然后调用Server的api刷新数据,然后发送通知,Client通过注册的Observer收到通知,然后再刷新UI。
由于CC层的限制,平台SDK级别的api是CC层访问不了的,例如扫描通讯录。针对这种方式,我们在CC层实现一个纯虚类:
C++
class SystemContactLoader
{
public:
virtual ~SystemContactLoader() {}
virtual void LoadAllSystemContacts(const StringSetPtr& loaded_phones, ContactLoadedListener listener) = 0;
};
Client层负责继承此类,然后在程序启动的时候,实例化子类,然后将实例化的对象注册到CC层中,这样CC层便可以调用到系统级的api。
某些api调用可能需要在主线程中,因此平台在实现的时候,可能需要切换到主线程,执行完毕之后,再开启一个新线程,将执行结果传递给回调函数。
CC这部分,主要工作在于整个模块框架的搭建与完善,后期的开发工作量基本上都比较少。因此如果一个App的逻辑部分不是很多的话,其实没有必要引入CC这种模块。
##长按关注猫头鹰技术公众号(mtydev)