关于iOS混合应用的初学者指南

亚伦·布雷洛斯特,2012年7月6日

简介

上周,纽约时报透露,Facebook正在对其实用的iOS应用程序进行一次重大更新。[url="http://bits.blogs.nytimes.com/2012/06/27/facebook-plans-to-speedup-its-iphone-app/"]这一事实本身并不让人惊讶。当然,Facebook会对其iOS应用进行重大更新。然而,这次更新的新闻意义重大。Facebook计划对其不断增长的移动应用套件进行重大的方向调整。[url="http://itunes.apple.com/us/artist/facebook-inc./id284882218"]

到目前为止,Facebook公开宣称的移动战略是在主要平台上发布混合应用,以避免‘写四次相同的代码’(关于这一点,我们稍后会详细讨论)。从理论上讲,这完全合理:谁愿意维护几个作用相同的代码库,而您只需一个即可?然而,实际上,Facebook应用在任何平台上都没有‘本地化’的感觉,性能问题和用户体验普遍遭到诟病。

本文将进一步探讨混合应用的主题。我们将首先解释什么构成了混合应用。然后,我们将探讨混合应用可以提供的优势,并详细讨论其负面方面。我们将探讨一些使得混合应用更加接近本地应用的方法,并提供具体建议以改善性能和外观感受。最后,我们将回到Facebook,详细分析其iOS应用的历史,以了解他们如何到达今天的位置。

个人而言,除非我不得不这么做,否则我不会为iOS构建混合应用。我认为混合应用在iOS上感觉并不好:它们运行速度更慢,操作更笨拙,并且永远不会拥有本地应用提供的外观和感觉。然而,在适当的条件下,它们可能提供的优势可能会超过其缺点。请继续阅读,以了解有关混合应用的所有信息,包括什么有效,什么无效,以及它们是否适合您的需求。

什么是混合应用?

通常,iPhone应用程序完全使用Objective C和Cocoa Touch框架构建。您可能有一个ukt tab bar controller来托管多个视图控制器。这些视图控制器可以是table view controller的子类,也可以是使用xibs或在代码中定义其ui的 PER ions或 browe ions。

混合应用使用部分或全部的客户端代码在html、css和javascript中实现,而不是Objective C。给定的应用程序屏幕实际上可能包含一个显示服务器端标记并解析服务器端代码的uiwebview,而不是Objective C。

一些应用,如Instapaper和Pocket,高度依赖网页浏览器,使用它们进行关键的应用功能。Instapaper和Pocket对网页浏览器的使用采取了实用主义方法:这两个应用的主要功能是显示重新格式化的网页内容,因此使用网页浏览器仅显示html是有道理的。

在光谱的更下面,Facebook 在其 iOS 应用程序中尽可能地使用了 HTML,本质上是将一个版本http://touch.facebook.com 嵌套到 Objective C 的壳中。他们使用 Objective C 进行限制性的应用程序功能,如登录界面和曾一度独特的滑动菜单界面。

最后,还有移动网站,这些网站可以被添加到 iPhone 的主屏幕上。这些网站是 100% HTML,并完全在 Mobile Safari 中运行。

应用程序光谱

类型 示例
纯本地化 Path 2.0 和其他完全使用 Objective C 和 Cocoa Touch 实现的应用程序。
一些网页视图 Instapaper 和 Pocket 都使用了网页视图来处理应用程序功能的关键部分,但其他所有内容都使用 Objective C 实现。
最小化 Objective C 例如 Facebook。尽可能地使用 HTML/JS/CSS 来实现应用程序,只有少数组件是本地的。
HTML 就是这样。

混合应用程序优势

为什么你想构建一个混合应用程序?有许多原因,例如执行速度和更新、进行 A/B 测试的便利性,以及通过允许前端 Web 开发者参与 iOS 应用程序来扩大可用的开发者群体。

执行速度

虽然开发一个完美地看起来和表现如同原生的移动 Web 体验几乎是不可能的,但相对简单地在短时间内提供一个相当不错的体验。与在 Objective C 中构建 JSON API 和相应的前端相比,构建一个不错的 Web 体验并将其加载到 UIWebView 中要容易得多。因此,一些开发者可能会倾向于先用 HTML 提供概念证明功能,然后在功能的价值得到证实后用本地代码替换。

市场上市速度可能是公司的一个关键区别因素,能够从提供时间表中削减几周甚至几个月的时间可能意味着对于有限 跑道 的初创公司来说是生死存亡之别。

发布更新速度

通过 HTML 提供应用程序功能的一个优势是,您可以在不向 Apple 交付应用程序更新的情况下,也不必等待 Apple 一周一次的审批流程,就可以更新应用程序的功能集。能够快速更改、修复和扩展应用程序的功能,可以为您相对于必须经历 Apple 审批流程才能对每个细微修改进行重审的竞争对手提供显著的竞争优势。

能够在早上设想一个新功能,在当天结束时交付它,并在夜间观察用户如何与之互动,以便在早上对其进行磨光、微调或删除,这是聚焦于应用程序在市场上成功所需经验的一种极其强大的方法。

A/B 测试

与执行速度和更新交付相关的是,您能够快速对用户进行 A/B 或分割测试。例如,您可能想测试当您的新的应用推荐功能在其本身包含“点赞”按钮时与只在推荐项详情页上包含“点赞”按钮相比,用户参与度是否会更高。通过在 HTML 中实现您的推荐功能,您可以轻松地进行这一变更的分割测试,向一个用户群仅显示按钮,以便查看用户行为是否发生了统计学上显著的变化。

在本地代码中进行A/B测试并非不可能,但设置测试会更具挑战性,并且与混合应用相比,你需要花费大量时间来响应测试中揭示的新数据。如果您想知道如何在Objective C中实现,可以在Little Big Thinkers网站上找到一篇优秀的文章,它展示了如何在iPhone上执行A/B测试:如何在内iOS应用中运行A/B测试

民主化的应用开发

如果您曾经尝试雇佣iOS开发者(或者您是iOS承包商),您就会知道,对有能力的iOS开发者的需求远大于可用的才能池。混合应用程序可以大大增加您可用的发展者的数量,因为如果您的前端开发者已经了解JavaScript、HTMLCSS以及您在服务器上使用的任何后端语言,那么您应该能够再次利用他们来为您的iOS应用程序开发功能。

混合应用的不利因素

当然,如果混合应用程序没有任何缺点,那就没有人会构建本地应用了。这显然不是事实,而且有多个原因。简而言之,混合应用程序响应较慢,操作笨拙,外观不够本地化,最重要的是,它们的感觉也不像本地应用。

缺少Nitro

Mobile Safari的JavaScript引擎Nitro非常快。它相当快。遗憾的是,由于安全方面的考虑,UIWebViews无法利用这一速度提升

可能Nitro相比WebKit先前JavaScript引擎性能提升的最大原因是使用了JIT——“即时”编译……JIT需要将内存页标记为可执行,但是,iOS作为一项安全措施,不允许内存页被标记为可执行。这是一个重大而严肃的安全策略。大多数现代操作系统都允许内存页被标记为可执行——包括Mac OS X、Windows以及(我相信)Android。iOS 4.3对此策略做出了例外,但这个例外仅限于Mobile Safari。

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

为了明确起见,您可以用CSS3完成一些真正惊人的效果,就像Hakim El Hattab最近在他的 stroll.js 项目中展示的那样。-click链接。但即便如此,总的来说,没有Nitro是任何混合iOS应用成功的严重障碍,尤其是那些想模仿原生iOS应用外观和感觉的应用。

模仿原生UI的挑战

除了上述性能问题之外,模仿原生Cocoa Touch用户界面也是通过实现混合应用所带来的另一个挑战。正如无数次被指出的那样,iPhone应用具有非常独特的风格和感觉。例如,表格单元格具有标准的字体大小、填充和间距要求、渐变选择高亮、展开箭头,以及许多其他难以复制的功能。

另一种选择是放弃复制iOS系统控件的要求,而为您自己的应用构建一个独特的UI,就像Facebook在他们的新闻源和时间线功能中所做的那样。然而,即使您选择走这条路,也必须解决一些WebKit特性,以确保您的混合应用感觉不像一个网页。

有时事情只是出了问题

推测你的混合视图的CSS或JavaScript文件迟早会加载失败,这是合情合理的。也许你的用户是在纽约或旧金山的AT&T客户,他们的GSM网络出了名的不稳定。也许今天就是运气不好的日子。不管怎样,你可能会向用户交付一个看起来像这样的UI

…更糟糕的是,你对此无能为力,除了将所有的JS和CSS直接挤压到HTML页面本身之外。这对你和你的应用都非常不利,而且无法控制。至少在一个完全本地应用中,您可以通过弹窗更优雅地处理服务器错误。

(来自@timanrebel的截图,Snowciety社交滑雪和滑雪板应用的共同创造者Snowciety.)

如何尝试让混合应用感觉更本地化

UIWebView 阴影和背景颜色

默认情况下,UIWebView显示渲染页面下的阴影和背景颜色或图案(取决于iOS版本和硬件)。这种感觉和看起来都不像本地应用,因此你需要去除它们。幸运的是,这相当容易做到:在UIWebView子类中添加几行代码就可以使视图‘平坦’。查看我的MIT许可项目FlatWebView,看看如何实现这个例子。

链接突出显示

为了帮助用户看到他们确切地点击的位置,WebKit在链接被点击时,在链接周围突出显示一个大的半透明矩形。

尽管这在网上很有用,但它感觉很不本地。要消除这种行为,你可以在你的Web应用中包含这个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毫秒的延迟。这实际上是一个特性,而不是一个错误,因为有时在网页上不小心点击错误的链接十分常见,给用户一个稍微延迟来纠正他们的错误会使体验比其他的选择要好得多。然而,当你在构建混合应用时,你应该只使用足够大的击中目标UI组件来防止意外点击。(你确实使用了至少40×40像素的较大击中目标,对吧?)

因此,当你构建混合应用时,你必须禁用WebKit的触摸延迟特性,以确保你的应用的UI感觉像Cocoa Touch UI一样响应。Matteo Spinelli提供了一个很有用的JavaScript代码(不是jQuery!)来修复这个‘问题’:Remove onclick delay on Webkit for iPhone

滚动

过去,构建混合应用的最大挑战之一是复制UIWebView中本地滚动行为。具体来说,在UIWebView中复制正常UIScrollView子类的滚动速度、惯性和‘rubberbanding’几乎是不可能的。幸运的是,Apple在iOS 5中为UIWebView公开了一个scrollView属性,这使得支持这些功能变得非常简单。

你所需要做的就是将你的webView的scrollView上的decelerationRate属性设置为UIScrollViewDecelerationRateNormal


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

桥接Objective C和JavaScript

如果你不打算利用iOS的一些平台功能,开发混合应用就完全没有意义。例如,尽管Facebook今天大部分是基于HTML实现的,但它依然使用原生UI来在其他用户的动态上发布帖子、上传照片、下拉刷新等功能。显然,你的应用中的原生功能必须能够与你的UIWebView-托管视图进行通信。然而,鉴于这些层次之间存在较差的通信渠道,在原生视图和网页视图之间创建无缝体验可能是一个挑战。

Objective C到JavaScript

为了响应原生代码的更改而更新WebView,你有三种选择

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

重新加载@UIWebView@的内容——或者加载一个全新的页面——是很糟糕的,因为它会留下一个空白且无法使用的页面一段时间。你总是可以显示一个加载HUD视图(例如SVProgressHUD),但这依然不是一种很好的体验。

通过在UIWebView内执行JavaScript代码来更新UIWebView是一种相对较好的选择:你可以进行部分更新,例如。然而,它也提出了自己的挑战,比如当某些事情不正常时,你会经历非常糟糕的调试体验。

JavaScript到Objective C

为了响应WebView的更改来更新原生代码,在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框架从核心中提取出来。如果你曾经使用过三个20编写过应用,你会知道它有点难以驾驭。据推测,乔在退出iOS开发后,Facebook的权力人物认为现有的实现太难维护,是时候重新考虑方向了。

Facebook平台工程经理Dave Fetterman在去年的F8大会上详细描述了这次内部变革。

你实际上需要为四个不同的平台构建同样的东西。你想要为所有这些群体构建吗?你将不得不构建四次。然后还有所有的功能——群组、优惠、新的个人资料。所有这些东西和不平衡矩阵变得真的很糟糕。所以我们不得不四次构建东西,这意味着代码会变慢。代码会过时。存在的版本不一致,事情无法相互配合,这对像Facebook这样的快速移动公司来说极具挑战性。

因此,当Facebook 4.0版本适配iOS系统时,它代表了这种新的“一次编写,到处运行”的心态。我个人必须说,在理论上,这是一个非常好的想法。没有人愿意四次构建和维护相同的特性集,尤其是像Facebook这样的公司,它的用户与工程师的比率惊人地高。不幸的是,实际情况是,UIWebViews根本不足以处理像Facebook这样的丰富应用程序提出的需要,而且用户也都知道这一点

结论

总的来说,混合应用程序在iOS上感觉并不十分合适:它们运行较慢,操作笨拙,可能会遭受无法恢复的错误,而且它们的感觉并没有本机应用程序好。但是,它们提供了一些优点,在某些情况下,这些优点可能是值得交换的。我个人建议您始终计划完全使用本机代码构建您的iOS应用程序的用户界面,并在需要时选择性地将其部分实现为混合功能,无论这种需求是纯粹的执行速度、能够即时推送更新,还是能够在短时间内测试用户对功能变体的反馈。

最后,但并非最不重要的是,只有你才知道什么最适合你的用户和你的业务。确保你的选择是出于正确的理由,而不是仅仅为了方便。

你对混合应用程序的看法如何?你认为什么时候优点会超过缺点?你有没有其他提高混合应用程序体验的小贴士和技巧?需要更多从混合到完全本机的建议?请在评论区告诉我们,我们很乐意听听你的意见!