iOS混合应用入门

Aaron Brethorst, 2012年7月06日

简介

上周,《纽约时报》揭露,Facebook正在为其iOS应用程序进行重大更新。[http://bits.blogs.nytimes.com/2012/06/27/facebook-plans-to-speedup-its-iphone-app/] 这个事实本身没有什么有新闻价值的。当然,Facebook正在对它的iOS应用进行重大更新。然而,这次尤为重要。Facebook规划了对他们构建和维护其不断增长的移动应用套件进行的一次重大航向调整。

到目前为止,Facebook公开的移动战略一直是,为了让避免“编写四次相同的代码”(稍后详细说明)而在主要平台上交付混合应用。在理论上,这是完美的:谁想维护几个执行同样功能的核心库,而只需一个呢?然而,实际上,Facebook应用在所有平台上都不会感觉像是“原生”的,性能糟糕至极,通常受到用户的谴责。

本文将更深入地探讨混合应用的话题。我们首先解释一下什么构成了混合应用。然后,我们将查看混合应用可以提供的优势,并详细介绍其负面影响。我们会考察一些让混合应用更像原生应用的选项,并提供具体的建议来改进性能和外观感受。最后,我们会回到Facebook,更详细地检查他们iOS应用的历史,以了解他们是怎样到达今天的。

个人而言,除非我不得不面对,否则我不会为iOS构建混合应用。我不认为混合应用在iOS上看起来合适:它们速度较慢,使用起来笨拙,并且永远不会完全具备原生应用提供的外观和感受。然而,在适当的条件下,它们可以提供优势,这些优势可能超过了其负面因素。继续阅读,了解混合应用的所有信息,了解它们是否适合您的需求。

什么是混合应用?

通常,iPhone应用完全用Objective C和Cocoa Touch框架构建。您可能有UITabBarController,它托管多个视图控制器。这些视图控制器可能是UITableViewController的子类,或者可能是使用XIBs或定义UI的UIViewControllers。有时,一个视图控制器可能会在内部显示网页内容或长篇文本内容时托管UIWebView控件。

混合应用使用HTML、CSS和JavaScript来实现它们的部分或全部客户端代码,而不是使用Objective C。特定的应用程序屏幕实际上可能包含一个渲染服务器端标记和解释服务器端代码的UIWebView,而不是使用Objective C。

一些应用,比如Instapaper和Pocket,高度依赖WebView,使用它们来实现关键的应用程序功能。Instapaper和Pocket对WebView的使用采取了一种务实的方法:两个应用的主要功能是显示重新格式化的网页内容,那么使用WebView来显示HTML只是一种合理的方式。

在光谱的另一端,Facebook 在他们的 iOS 应用中尽可能地使用 HTML,本质上是在 Objective C 框架中嵌入了一个 http://touch.facebook.com 的版本。他们使用 Objective C 对一些应用功能进行限制,例如登录屏幕和他们曾经独特的侧滑菜单界面。

最后,还有一些移动网站,可以添加到 iPhone 的主屏幕。这些都是 100% 的 HTML,仅可以通过 Mobile Safari 运行。

应用类型谱系

类型 示例
纯原生 2.0 版 Path 等应用完全用 Objective C 和 Cocoa Touch 实现。
部分视图 Instapaper 和 Pocket 都使用视图来处理应用的关键部分功能,但其他一切都在 Objective C 中实现。
最少量 Objective C 例如 Facebook。越多的应用功能通过 HTML/JS/CSS 实现,只有少数组件是原生的。
HTML 正如其名。

混合应用的优势

为什么你想建立一个混合应用?有多个原因,比如执行速度和更新的速度、进行 A/B 测试的便捷性,以及通过让前端网页开发者为 iOS 应用做出贡献来扩大你的开发者阵容。

执行速度

尽管开发一个完美模拟原生外观和行为的移动网络体验几乎不可能,但在短时间内提供合理良好的体验是相对简单的。在 UIWebView 中构建适当的网络体验并将其加载,肯定比用 Objective C 建立一个 JSON API 以及前端要容易。因此,一些开发者可能更喜欢用 HTML 实现部分概念特性,然后再在特性价值得到证实后用原生代码替换。

上市时间对于公司来说可能是一个关键的区别因素,能够减少数周甚至数月的时间表意味着对于资源有限的初创公司来说可能是生存与否的区别。

发布更新的速度

通过 HTML 提供应用特性还有一个优势,就是你可以更新应用功能集,而无需提交应用到苹果审核,也无需等待苹果的一周审核流程。能够快速改变、修复和扩展您应用的能力,可以为您相对于必须通过苹果每项微小修改审核流程的竞争对手提供显著的竞争优势。

能够在早上构想一个新的特性,并在一天结束时实现它,然后在一夜之间观察用户如何与它互动,以便在第二天早上进一步完善、微调或删除它,这是一个在市场上取得成功所必需的体验的强大方式。

A/B 测试

与执行速度和更新发布相关的是,你可以迅速地对用户进行 A/B 或分割测试。例如,你可能想测试包括在应用流本身还是仅包括在流项目详情页面上的“赞”按钮,是否会导致更高的用户互动。通过在 HTML 中实现应用流特性,你可以轻松地进行分割测试,仅将按钮展示给一组用户,以观察是否存在统计学上显著的用户行为变化。

在本地代码中执行A/B测试并非不可能,但设置测试会更具挑战性,响应测试揭示的新数据将比混合应用需要更多的时间。如果你对如何在Objective C中实现它感兴趣,Little Big Thinkers有一篇非常棒的博客,展示了他们如何在iPhone上执行A/B测试

应用开发民主化

如果你曾经尝试雇佣一个iOS开发者(或者如果你是一个iOS承包商),你就会知道,有能力胜任的iOS开发者远远超过了可用人才的数量。混合应用可以大大增加可供你使用的有能力的开发者的数量,因为你在iOS应用程序中,应该能够重新利用任何前端开发者的技术,只要他们已经熟悉JavaScript、HTMLCSS以及你在服务器上使用的任何后端语言。

混合应用的缺点

当然,如果混合应用没有缺点,那就没有人会开发本地应用了。这显然不是事实,而且有几个原因。简而言之,混合应用速度较慢、笨拙,外观不自然,最重要的是,它们感觉也不自然。

缺少Nitro

Mobile Safari的JavaScript引擎Nitro非常快。但遗憾的是,由于安全考虑,UIWebViews无法利用这种速度提升

也许Nitro性能比WebKit earlier的JavaScript引擎有显著提高的最大原因就是使用了一种JIT——即时编译……JIT需要在RAM中将内存页面标记为可执行,但iOS为了安全考虑,不允许将内存页面标记为可执行。这是一个重要和严格的安全策略。大多数现代操作系统都允许在内存中将页面标记为可执行——包括Mac OS X、Windows以及(我相信)Android。iOS 4.3对这项政策做出了例外,但这个例外只是特别限于Mobile Safari。

从实际的角度来说,这意味着如果你的混合应用使用了JavaScript,它的感觉会比同样的UI在Mobile Safari中慢,或者慢于用Objective C实现的UI。不幸的是,除了尽可能减少或完全放弃在UIWebViews中使用JavaScript,转而使用CSS3动画和过渡(这应当能够利用GPU加速)之外,别无他法。

要清楚,你可以仅仅使用CSS3实现一些真正惊人的效果,例如Hakim El Hattab最近在他 stroll.js 项目中的展示一样。但是,总的来说,缺乏Nitro对于任何混合iOS应用的成功都是一个严重的障碍,特别是那些试图匹配原生iOS应用的外观和感觉的应用。

模仿原生UI的挑战

除了上面提到的性能问题外,模拟原生Cocoa Touch用户界面也是实现混合应用时遇到的另一个困难。正如无数人指出过的那样,iPhone应用有着非常独特的样式和感觉。例如,表格单元有标准的字体大小、填充和间距要求、渐变选择高亮、展开箭头等许多难以复制的功能。

一个替代方案是放弃复制iOS系统控件的要求,而是为你的应用构建一个独特的UI,就像Facebook在其新闻源和时间线功能上所做的那样。然而,即使你选择了这条路,也有几个WebKit特性需要加以考虑,以便让你的混合应用不感觉像是一个网页。

有时候事情确实会搞砸

期待你的混合视图的 CSS 或 Javascript 文件最终会无法加载,这并不奇怪。也许你的用户是纽约或旧金山的 AT&T 客户,他们那众所周知的易变 GSM 网络。也许今天神灵并不保佑你。不管怎样,最终你提供的用户界面可能像这样

……并且最糟糕的是,除了把所有的 JS 和 CSS 混入到 HTML 页面本身之外,你几乎无能为力。这对你和你的应用程序的印象极为不佳,而且完全无法控制。至少对于一个全原生应用,服务器错误可以通过弹窗方式更加优雅地处理。

(献给 @timanrebel,社交滑雪和滑雪板应用 Snowciety 的共同创造者。)

如何尝试让您的混合应用感觉像原生应用

UIWebView 阴影和背景颜色

默认情况下,UIWebViews 在渲染页面的下方显示阴影和背景颜色或图案(根据 iOS 版本和硬件)。这样的外观和感觉显然不属于原生应用,所以你需要去掉这两个属性。幸运的是,这很简单:在 UIWebView 子类中添加几行代码,就能使视图看起来扁平。查看我的 MIT 许可项目 FlatWebView,看看如何实现这个功能。

链接高亮

为了帮助用户看到他们点击的位置,当链接被点击时,WebKit 会在一个大半透明的矩形内高亮显示链接。

尽管这在网页上很方便,但它显然不像原生应用。要移除这种行为,你可以在你的网络应用中包含以下 CSS 片段

/* http://stackoverflow.com/questions/9157080/wrong-webkit-tap-highlight-color-behavior-when-page-as-web-standalone-app */ html { -webkit-tap-highlight-color: rgba(0,0,0,0); -webkit-user-select: none; }

触摸延迟

默认情况下,WebKit 在响应网页上的触摸链接时,会添加大约 300 毫秒的延迟。这实际上是一个功能,而不是一个错误,因为在一个网页上意外点击错误链接的情况很常见,给用户一个微小的延迟来纠正他们的错误,比起其他选择来说,用户体验要好得多。然而,在构建混合应用时,你应该只使用具有足够大的点击目标(至少 40×40 像素)的 UI 组件。(您确实使用的是这样的大点击目标,对吗?)

因此,在构建混合应用时,您必须禁用 WebKit 的触摸延迟功能,以确保您的应用程序界面感觉像 Cocoa Touch 的界面一样灵敏。Matteo Spinelli 提供了一些有用的 JavaScript 代码(不是 jQuery!)来解决这个“问题”:查看 在 iPhone 上移除 Webkit 的 onclick 延迟

滚动

构建混合应用时最大的挑战之一是将原生滚动行为复制到 UIWebView 中。特别是,在 UIWebView 中复制正常 UIScrollView 子类中的滚动速度、惯性和“拉橡皮筋”几乎是不可能的。幸运的是,Apple 在 iOS 5 中为 UIWebViews 暴露了 scrollView 属性,这使得支持这些功能变得非常简单。

您所要做的就是设置网页视图滚动视图的 decelerationRate 属性为 UIScrollViewDecelerationRateNormal


UIWebView *hybridView = [self somethingThatGetsOurWebView];
webView.scrollView.decelerationRate = UIScrollViewDecelerationRateNormal;

桥接 Objective C 和 Javascript

如果你不打算利用iOS平台的一些功能,那么构建一个混合应用程序就没什么意义。例如,尽管Facebook现在大部分是用HTML实现的,但它仍然使用原生UI来发布在其他用户的动态中,上传照片,下拉刷新以及其他功能。显然,但你的应用程序中的原生功能必须能与你的UIWebView-托管视图进行通信。由于这些层之间存在较差的通信渠道,因此创建原生和网页视图之间的无缝体验可能具有挑战性。

Objective-C到JavaScript

为了响应原生代码的变化来更新网页视图,你有三种选择

  1. 重新加载UIWebView的内容。
  2. 使用NSURLRequest加载新页面。
  3. 在UIWebView中调用JavaScript代码,使用API。

重新加载UIWebView的内容——或者加载一个全新的页面——都是很糟糕的,因为这将导致你在相当长的一段时间内拥有一个空白、无法操作的页面。你总是可以显示一个加载HUD视图(例如,SVProgressHUD),但这仍然不是一种很好的体验。

通过在UIWebView中执行JavaScript代码来更新UIWebView是一种稍微好一点的选项:它给了你执行部分更新的机会。然而,这也带来了它自己的挑战,比如当某些事情不起作用时,你将得到一个极其糟糕的调试体验。

JavaScript到Objective-C

为了响应你的网页视图中的变化来更新原生代码,iOS中的最佳选择是实现UIWebView委托方法-webView:shouldStartLoadWithRequest:navigationType:。你可以定义自定义方案(例如,使用myappname://而不是http://),以及自定义URI(例如,使用showImageCapture而不是apple.com),以便从UIWebView调用原生平台功能。不幸的是,这个解决方案常常会退化到即使是中等复杂的应用程序也是如此的大块的if-语句。

例如,Facebook可能会这样实现它们的委托方法

if ([request.url.scheme isEqual:@"showImagePicker"]) { // 显示UIImagePickerController } else if ([request.url.scheme isEqual:@"sendMessage"]) { // 显示发送消息视图控制器 } else if ([request.url.scheme isEqual:@"checkIn"]) { // 显示地点签到视图控制器 } else if ([request.url.scheme isEqual:@"postStatus"]) { // 显示发布状态视图控制器 }

幸运的是,有一些方法可以简化它。例如,你可以使用一个NSDictionary将方案映射到某种形式的即时调度系统的动作上。


- (void)viewDidLoad
{
    self.dispatch = [NSMutableDictionary dictionary];
    [dispatch setObject:[NSValue valueWithPointer:@selector(showImagePickerForURL:)] forKey:@"showImagePicker"];
}

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
    NSValue *selectorPointer = [self.dispatch objectForKey:request.url.scheme];
    if (selectorPointer)
    {
      [self performSelector:[selectorPointer pointerValue] withObject:request.url];
      return NO;
    }
    else
    {
      return YES;
    }
}

Facebook

当iPhone版Facebook首次推出时,它是一个几乎是完全原生的应用程序,据我所知,它是单枪匹马由Joe Hewitt编写的,他从其核心提取了Three20 iOS框架。如果你曾经使用过Three20编写应用程序,你会知道它有点难以驾驭。风险评估,在那位乔·休伊特退出iOS开发之后,Facebook的当权派决定,现有的实现实在难以维护,并且是时候重新考虑策略了。

这种内部的海量变革在去年F8大会上被Facebook平台的产品经理Dave Fetterman详细描述

你需要为基本上相同的事情构建四个不同的平台。你想要为所有这些群体构建吗?那么你必须把这个东西构建四次。然后还有所有这些功能——群组、 deals、新的个人资料。所有这些和矩阵都变得真的很坏。因此,我们必须把它构建四次,这意味着代码会变慢。代码会变旧。有不同版本的parity,事物不可能一起发挥作用,这对像Facebook这样快速发展的公司来说非常困难。

因此,当Facebook 4.0针对iOS发布时,它代表了这种新的“一次编写,到处运行”的理念。我必须说,就我个人而言,我认为这是一个在理论上的好主意。没有人愿意四次构建和维护相同的特性集,尤其是像Facebook这样拥有超高的用户与工程师比例的公司。不幸的是,现实情况是,UIWebViews根本不足以处理像Facebook这样的丰富应用所提出的需要,而且其用户也知道这一点

结论

总的来说,混合应用在iOS上感觉并不完美:它们速度更慢,操作更笨拙,可能会受到无法恢复的错误的影响,而且它们的感觉并不像原生应用那样好。但是,它们提供了一些优势,在合适的条件下,这些优势可能是一个值得权衡的好交易。就我个人而言,我建议您始终计划完全用原生代码构建iOS应用的界面,并在需要的情况下,选择性地实现其中的部分作为混合功能,无论是出于对执行速度的纯粹需求,还是需要即时推送更新,或者需要在短时间内对用户测试多个功能变体。

然而,最终,只有你知道什么对你的用户和你的业务才是真正正确的。只需确保你的选择是出于正确的原因,而不仅仅是出于便利的考虑。

你对混合应用有什么看法?你认为什么时候积极因素会超过消极因素?你对改善混合应用体验有其他的技巧和窍门吗?需要更多关于如何从混合到完全原生应用的建议吗?在评论中告诉我们,我们很高兴听到你的想法!