baohuanlove 2019-06-10
跨平台vs.原生
在日常研发过程中,人们经常会对跨平台技术抱有成见。他们认为:由于并非原生,而是基于网络,因此其整体效率会较为缓慢,当然也就注定会出错。
但是,他们殊不知:如今,那些原生架构能够实现的功能,跨平台的应用框架基本都能实现,它们之间的运行效率通常取决于用户是如何实现其程序代码的。在实际项目中,我们经常会看到有的团队使用Swift/Java,开发出了速度缓慢且时常崩溃的原生应用;而其他团队则能够使用Titanium之类的跨平台技术,开发出了流畅的产品。
另外,无需用到混合式跨平台工具,跨平台的框架本身就能够生成真正意义上的原生UI。因此,作为原生应用的开发人员,您过去遇到过的诸如内存泄漏等问题,在如今大多数跨平台框架中已得到了完美解决,而它们能够协助开发人员实现了大部分的重量级加载任务。
可见,只有理解了您所使用的框架局限性,才是避免应用程序出现缓慢状况的关键所在。下面,我将试着和您分析跨平台应用在移动设备上运行缓慢、甚至无法响应的原因,并且帮助您找到各种加速的办法。作为代码示例,我选用的是Titanium作为框架。当然,这些改进技巧也适用于其他类型的框架。
第1类原因:设计
通常情况下,在开发跨平台的应用程序时,开发团队往往趋向于将两大平台的版本合二为一。而这种设计理念恰恰是导致应用程序在长期运行后,变得缓慢的根本原因之一。
首先,您需要了解的是:不同平台所对应的默认UI与UX(更为重要)是截然不同的。例如,iOS为每一个窗体“栈(stack)”都配置了打开/关闭动画、以及幻灯片手势。这些虽然看似微不足道,但是动画的触发,能够给用户带来交互式使用的体验。这种“让用户通过在手机屏幕上滑动手指,在窗体栈中切换应用内与应用间不同页面”的想法,缓冲了跳转的时间。用户不会直观地体会到点击后退按钮所可能碰到的缓慢问题。
因此,在为某个窗体设计不同的外观时,开发人员应酌情考虑是否采用原生的显示效果。如果想自定义的话,那么开发人员就需要通过一个事件侦听器,来捕捉用户是否在屏幕上滑动了手指,进而关闭相应的窗体。否则,动画手势的突然消失,会让用户生硬地感觉到整个UX的下降,进而产生应用变慢的感受。
除了上述动画效果与UI上的不同之外,开发人员还应当注意跨平台框架的垃圾回收问题,以防止出现自定义UI在运行过程中出现内存泄漏的漏洞。
同时,随着用户的广泛使用、以及移动设备硬件性能的提升,他们会设置更多的自定义手势与快捷方式,而您的移动应用需要捕捉、跟踪与计算更多的手指轨迹。因此,您会发现数据的加载与计算的阻塞会相互影响,并形成恶性循环。以至于某些应用在上线一年之后,就失去了可维护性与可使用性,而开发团队则不得不对程序进行重写与重构。
解决方案
首先,在应用的设计阶段,我们应当充分了解与接受平台之间的差异性。通过针对两套系统的产品设计,来发现不同平台的用户对于界面与效果的期待。在公司内部,您可以邀请长期使用Android手机和iPhone的两类人,进行各种使用效果的实测。
其次,利用内置的UX/UI特征,尝试着从原生平台的角度,实现应用界面上的各项功能。例如:在大多数iOS应用中,按钮一般会被放置在标题的左右两侧;而Android版本则通常会把按钮放置在右边,或者直接隐藏到菜单图标的里面。另外,Android应用会内置有后退按钮,而iOS则会有一个返回的手势。通过支持这些基于不同移动平台的特征,用户就能够直观地理解您的应用,并产生一定的认同感。
第2类原因:加载太多
应用程序变慢的另一个主要原因是:在UI中一次性加载的元素太多。毕竟,移动设备的性能不及用户电脑,很难同时处理多项任务需求。而且,不是所有的人都能使用最新版本的iPhone、以及高端Android设备。其中仍在使用Android 4.4的用户也不在少数。因此,要想在各类应用商店里脱颖而出,您的移动产品就应该具备在低端配置和老旧版本的设备上仍有不俗性能的能力。
例如,在iOS的应用商店内,如果您点开并选中“Today”标签的话,它会迅速地载入五款应用标签。接着,您在向下滑动时,请注意滚动条的位置和大小。您会不难发现:滚动条会以原来的大小滑向屏幕的底部,当接近屏底的20%处时,它会迅速缩小、甚至弹回到上方的某个位置。而屏幕上则开始显示“第二页”的数据。
可见,该应用商店并非一次性加载了所有数据。因此,在实际应用的设计与构建中,我们应该扪心自问:我们在让用户首次看到推送内容时,是否一并加载了后续内容?您是否针对图片进行了延迟加载?在预加载的数据中,有多少应该在ListVIEW中就显示出来?100项还是200项?在应用启动之处,您需要调用多少个API?我在TiSlack论坛里,有看过“在APP的启动时,如何最有效地调用50个API”的帖子,以及“如何在ListVIEW中有效加载10000条信息”的留言。
在此,我的答案是:“不要这样做!”没有人能够一次性查看这么多条信息。如果您希望用户能够滚动列表的话,那么请使用延迟加载(lazy-loading)以及分页,哪怕数据已存放在设备的本地空间里。另外,如果想让用户进行搜索的话?那么也请您在服务端本地搜索完毕之后,再推送到用户设备上。
解决方案
从上面苹果应用商店的例子,我们可以了解到:在应用被首次打开之后,数据内容才被进行加载。例如在默认的TabGroup中:
...
另外,我在首页选项卡的窗体中添加了一个postlayout事件侦听器。该函数的作用是调用窗体的初始化器,在为选项卡获取相关数据之后,再进行加载。通过等待postlayout事件,您可以确保用户看到的不是加载页面,而是正常的应用内容。下面便是用来滞后进行初始化内容的简单函数--handlePostlayout:
function handlePostlayout() { $.todayWindow.add( Alloy.createController('todayContent').getView() ); }
如您所见,在Today标签中,真实请求的内容、以及相关依赖项,并没有马上被初始化或加载,此举无疑加快了应用程序的整体性能。
同时,对于其他选项卡而言,我们可以监控屏幕窗体的“焦点(focus)”事件,只在窗体被聚焦时进行加载。同理,您也可以将该事件用在当前窗体被再次聚焦时,及时刷新数据;以及运用到Firebase Analytics(Android集成埋点分析)中。
记住:相对于那些用分页或延迟加载的方式,来重新初始化整个页面而言,仅下载数据的方式在量级上会更“轻”、速度会更快、也更节约CPU的计算力。
第3类原因:桥
此处的“桥”是一个源自Titanium和React Native之类跨平台框架的概念。它表示:JavaScript代码和原生代码之间的每一次交互,都会产生开销。例如:您在服务器进行API调用的时候,显然,相对于花费100次调用API,而每次仅取回1条数据而言,我们更愿意仅调用1次API,并一次性地取回100条数据。
那么何为“过桥”呢?其实,我们在添加UI元素、更新UI元素、以及触发动画时都会产生调用。例如:在Titanium中的Ti.App.fireEvent流就需要用到过桥的概念。该类事件通常被用于在应用程序内触发某些操作,进而实现初始化。另外,此类事件也会触发UI的更新操作。因此,一个事件可能会触发两次过桥。
那么当所有的事情都同时发生,尤其处于循环触发的状态时,过桥进程就会变得“拥挤不堪”。而如果桥的带宽容量又比较“狭窄”的话,您的应用就会产生大面积的延迟,甚至可能发生中断事故,进而直接影响了用户的使用体验。
解决此类问题其实非常简单。您只需要将UI的批处理组合在一起便可。因此,当需要修改ListTimes的整张表时,我们可以采用ListTeal.RePateTimeSt,来轻松地一次性重新插入整个数据集。而当您需要更改某个UI元素的一组属性时,则完全可以使用applyProperties,而无需更改每个属性的具体顺序。
同时,您可以使用Backbone Events,来触发整个应用程序中的各种事件,而不必采取过桥的方式。而且,我们很容易将当前的各种Ti.App事件迁移到Backbone Events上。
如下所示,我们首先将Backbone Events包括到alloy.js中。
Alloy.Globals.events = _.clone(Backbone.Events);
然后,您将应用里任何曾经用到了Ti.App.fireEvent()的地方,替换为如下函数:
Alloy.Globals.events.trigger();
接着,以同样的方式,您可以继续将Ti.App.addEventListener()替换为:
Alloy.Globals.events.on()
您甚至可以对自己的项目进行全局搜索与替换,以获得立竿见影的性能提高效果。
结论