iOS上的离线模式及其在Realm上的实现功能



由DataArt强大的iOS开发者Ekaterina Semashko发布

有关项目的一些信息:用Swift编写的iOS平台移动应用程序。 该应用程序的目的是能够在公司员工及其朋友之间共享折扣卡。

该项目的目标之一是学习和实践流行的技术和图书馆。 选择Realm来存储本地数据,使用Alamofire来处理服务器,使用Google Sign-In进行身份验证,使用PINRemoteImage进行图像上传。

该应用程序的主要功能:

  • 添加地图,编辑和删除地图;
  • 查看他人的卡;
  • 通过商店名称/用户名搜索卡;
  • 将卡片添加到收藏夹以快速访问。

从一开始就假定可以在不连接网络的情况下使用应用程序,但是仅在读取模式下才可以。 即 我们可以查看有关卡的信息,但是如果没有Internet便无法修改它们。 为此,应用程序始终具有服务器上数据库的所有卡和品牌的副本,以及当前用户的收藏夹列表。 搜索也在本地实施。

后来,我们决定通过添加记录模式来离线扩展。 当出现Internet连接时,有关用户所做更改的信息已存储并同步。 将讨论这种读写离线模式的实现。



在移动应用程序中,完全脱机模式需要什么? 我们需要消除用户对Internet连接质量的依赖,尤其是:

  1. 从服务器上删除对用户的响应对其在UI中的操作的依赖。 首先,请求将与本地存储交互,然后将其发送到服务器。
  2. 标记并存储本地更改。
  3. 实施同步机制-当出现Internet连接时,您需要将更改发送到服务器。
  4. 向用户显示哪些更改已同步,哪些未同步。

离线优先方法


首先,我必须更改与服务器和数据库进行交互的现有机制。 目的是防止用户依赖于Internet的存在与否。 首先,它应该与本地数据仓库交互,并且服务器请求应该在后台进行。

在以前的版本中,数据存储层和网络层之间存在牢固的连接。 处理数据的机制如下:首先通过NetworkManager类向服务器发出请求,我们等待结果,然后通过Repository类将数据保存到数据库。 然后将结果提供给UI,如图所示。


为了实现离线优先方法,我分离了数据存储层和网络层,引入了一个新的Flow类,该类控制了调用NetworkManager和存储库的顺序。 现在,首先通过Repository类将数据保存到数据库,然后将结果发送到UI,然后用户继续使用该应用程序。 在后台,向服务器发出请求,响应后,数据库和UI中的信息将更新。


使用对象标识符


使用新的体系结构,出现了几个新任务,其中之一是使用id对象。 以前,我们是在创建对象时从服务器收到它们的。 但是现在对象是在本地创建的,因此,有必要生成一个id,并在同步后将其更新为当前的ID。 在这里,我遇到了Realm的第一个限制:创建对象后,您无法更改其主键。

第一种选择是放弃对象中的主键,将id设置为常规字段。 但是与此同时,失去了使用主键的优势:领域索引,这加快了对象的获取速度,具有使用create标志更新对象的能力(如果不存在则创建对象),以及对对象唯一性的遵守。

我想保存主键,但是它不能是服务器中对象的ID。 结果,有效的解决方案是有两个标识符,其中一个是服务器,可选字段,另一个是本地,这将是主键。

结果,在本地创建对象时,在客户端上会生成一个本地ID,并且在对象来自服务器的情况下,它等于服务器ID。 由于在单一来源的真理应用程序中有一个数据库,因此当从服务器接收数据时,将使用当前的本地标识符来更新对象,并且只能使用该标识符。 当向服务器发送数据时,服务器标识符被传输。

存储未同步的更改


对尚未发送到服务器的对象的更改必须存储在本地。 这可以通过以下方式实现:

  1. 将字段添加到现有对象
  2. 将未同步的对象存储在单独的表中;
  3. 以某种格式存储各个字段更改。


我没有在类中直接使用Realm对象,但我自己进行了映射以避免出现多线程问题。 界面自动更新是使用自动更新结果样本完成的,我在其中订阅了更新请求。 只有第一种方法适用于我当前的体系结构,所以选择落在向现有对象添加字段上。

地图对象发生了最多的变化:

  • 已同步-服务器上是否有数据;
  • 已删除-是,如果仅在本地删除卡,则需要同步。

上一部分中讨论的标识符:

  • localId-应用程序中实体的主键,等于服务器ID,或在本地生成;
  • serverId-服务器的ID。

值得一提的是图像的存储。 本质上,附件字段diskURL已添加到服务器上映像的serverURL字段中,该字段存储本地未同步映像的地址。 同步图像时,删除了本地图像,以免阻塞设备的内存。

服务器同步


为了与服务器同步,添加了使用可达性的功能,以便在出现Internet时启动同步机制。

首先,它检查是否需要对数据库进行任何更改。 然后,将请求发送到服务器以进行实际的数据转换,结果,筛选出不需要发送到客户端的更改(例如,更改已经在服务器上删除的对象)。 其余更改会将对服务器的请求排队。

要发送更改,可以实现批量更新,将更改发送到数组中或发出大量请求以同步所有数据。 但是到那时,后端开发人员已经在忙于另一个项目,并且仅在空闲时间为我们提供了帮助,因此我们为每种类型的更改创建了一个请求。

我通过OperationQueue实现了队列,并将每个请求包装在一个异步Operation中。 某些操作相互依赖,例如,我们无法在创建地图之前加载地图图像,因此我将图像操作的依赖项添加到了地图操作中。 另外,将图像上传到服务器的操作比其他所有人都具有较低的优先级,并且由于它们的沉重性,我最后将它们添加到队列中。

在计划脱机模式时,最大的问题是解决同步期间与服务器的冲突。 但是当我们在实现过程中达到这一点时,我们意识到用户在不同设备上更改相同数据的情况非常少见。 因此,对于我们来说,实施最后一个作家获胜机制就足够了。 在同步期间,始终优先处理客户端上未发送的更改,这些更改不会被擦除。

错误处理仍处于起步阶段,如果同步失败,则下次Internet出现时,该对象将被添加到更改队列中。 然后,如果合并后它仍然挂起不同步,则用户将决定是保留还是删除它。

使用Realm时的其他解决方法


与Realm合作时会遇到更多问题。 也许这种经验对某人也很有用。

当按字符串排序时,顺序将按照UTF-8中的字符顺序进行,不支持区分大小写的搜索。 我们面临的情况是小写字母的名称紧随大写字母的名称之后,例如:磁铁,Pyaterochka,Ribbon。 如果列表很大,则所有小写的名称都将位于底部,这非常不愉快。

为了保留排序顺序,无论大小写如何,我们必须引入一个新的lowercasedName字段,在更新名称时对其进行更新并按其排序。

此外,还添加了一个新字段,用于根据收藏夹中是否存在卡片进行排序,因为从本质上讲,这需要针对对象的关系进行子查询。

在Realm中搜索时,存在不区分大小写的CONTAINS [c]%@方法。 但是,a,它仅适用于拉丁字母。 对于俄罗斯品牌,我们还必须创建单独的字段并进行搜索。 但是后来发现在搜索时要排除特殊字符由我们掌握。



如您所见,对于移动应用程序来说,实现脱机模式非常有可能,它可以保存更改并进行少量同步,有时甚至在后端进行的更改也很少。

尽管有一些困难,您仍可以使用Realm来实现它,同时以实时更新,零拷贝体系结构和便捷的API的形式获得所有优势。

因此,无论连接的质量如何,都没有理由随时拒绝用户访问数据。

Source: https://habr.com/ru/post/zh-CN432422/


All Articles