今天参加了 Adobe 和 CSDN 组织的一个关于 PhoneGap 的开发讲座 ,而 PhoneGap 在 iOS 设备上的实现就是经过 UIWebView 控件来展现 html 内容,而且与 native 代码进行交互的。html
正好咱们在作有道云笔记的 iPad 版,由于咱们也是使用 UIWebView 来展现笔记内容,因此也须要作 js 与 native 代码相互调用的事情。因此在这儿顺便总结一下 UIWebView 在使用上的细节,以及谈谈我对 PhoneGap 的见解。前端
机制
首先咱们须要让 UIWebView 加载本地 HTML。使用以下代码完成:java
NSString * path = [[NSBundle mainBundle] bundlePath]; |
接着,咱们须要让 js 可以调用 native 端。iOS SDK 并无原生提供 js 调用 native 代码的 API。可是 UIWebView 的一个 delegate 方法使咱们能够作到让 js 须要调用时,通知 native。在 native 执行完相应调用后,能够用 stringByEvaluatingJavaScriptFromString 方法,将执行结果返回给 js。这样,就实现了 js 与 native 代码的相互调用。node
如下是 PhoneGap 相关调用的示例代码:android
// Objective-C 语言 |
具体让 js 通知 native 的方法是让 js 发起一次特殊的网络请求。这里,咱们和 PhoneGap 都是使用加载一个隐藏的 iframe 来实现的,经过将 iframe 的 src 指定为一个特殊的 URL,实如今 delegate 方法中截获此次请求。git
如下是 PhoneGap 相关调用的示例代码:github
// Javascript 语言
// 通知 iPhone UIWebView 加载 url 对应的资源
// url 的格式为: gap:something
function loadURL(url) {
var iFrame;
iFrame = document.createElement("iframe");
iFrame.setAttribute("src", url);
iFrame.setAttribute("style", "display:none;");
iFrame.setAttribute("height", "0px");
iFrame.setAttribute("width", "0px");
iFrame.setAttribute("frameborder", "0");
document.body.appendChild(iFrame);
// 发起请求后这个 iFrame 就没用了,因此把它从 dom 上移除掉
iFrame.parentNode.removeChild(iFrame);
iFrame = null;
} |
在这里,可能有些人说,经过改 document.location 也能够达到相同的效果。关于这个,我和 zyc 专门试过,通常状况下,改 document.location 是能够,可是改 document.location 有一个很严重的问题,就是若是咱们连续 2 个 js 调 native,连续 2 次改 document.location 的话,在 native 的 delegate 方法中,只能截获后面那次请求,前一次请求因为很快被替换掉,因此被忽略掉了。web
我也专门去 Github 上查找相关的开源代码,它们都是用过 iframe 来实现调用的,例如这个:https://github.com/marcuswestin/WebViewJavascriptBridge编程
关于这个,我也作了一个 Demo 来简单示例,地址以下:https://github.com/tangqiaoboy/UIWebViewSample
参数的传递
以上的示例代码为了讲清楚机制,因此只是示例了最简单的相互调用。但实际上 js 和 native 相互调用时,经常须要传递参数。
例如,有道云笔记 iPad 版用 UIWebView 显示笔记的内容,当用户点击了笔记中的附件,这个时候,js 须要通知 native 到后台下载这个笔记附件,同时通知 js 当前的下载进度。对于这个需求,js 层得到用户点击事件后,就须要把当前点击的附件的 ID 传递给 native,这样 native 才能知道下载哪一个附件。
参数传递最简单的方式是将参数做为 url 的一部分,放到 iFrame 的 src 里面。这样 UIWebView 经过截取分析 url 后面的内容便可得到参数。可是这样的问题是,该方法只能传递简单的参数信息,若是参数是一个很复杂的对象,那么这个 url 的编解码将会很复杂。对此,咱们的有道云笔记和 PhoneGap 采用了不一样的技术方案。
- 咱们的技术方案是将参数以 JSON 的形式传递,可是由于要附加在 url 以后,因此咱们将 JSON 进行了 Base64 编码,以保证 url 中不会出现一些非法的字符。
- PhoneGap 的技术方案是,也是用 JSON 传递参数,可是将 JSON 放在 UIWebView 中的一个全局数组中,UIWebView 当须要读取参数时,经过读取这个全局数组来得到相应的参数。
相比之下,应该说 PhoneGap 的方案更加全面,适用于多种场景。而咱们的方案简洁高效,知足了咱们本身产品的需求。
同步和异步
由于 iOS SDK 没有天生支持 js 和 native 相互调用,你们的技术方案都是本身实现的一套调用机制,因此这里面有同步异步的问题。细心的同窗就能发现,js 调用 native 是经过插入一个 iframe,这个 iframe 插入完了就完了,执行的结果须要 native 另外用 stringByEvaluatingJavaScriptFromString 方法通知 js,因此这是一个异步的调用。
而 stringByEvaluatingJavaScriptFromString 方法自己会直接返回一个 NSString 类型的执行结果,因此这显然是一个同步调用。
因此 js call native 是异步,native call js 是同步。在处理一些逻辑的时候,不可避免须要考虑这个特色。
这里顺便说一个 android,其实在 android 开发中,js 调 native 是同步的,可是 PhoneGap 为了将本身作成一个跨平台的框架,因此在 android 的 js call native 的 native 端,用 new Thread 新建了一个执行线程,这样把 android 的 js call native 也变成了异步调用。
UIWebView 的问题
线程阻塞问题
咱们在开发中发现,当在 native 层调用 stringByEvaluatingJavaScriptFromString 方法时,可能因为 javascript 是单线程的缘由,会阻塞原有 js 代码的执行。这里咱们的解决办法是在 js 端用 defer 将 iframe 的插入延后执行。
主线程的问题
UIWebView 的 stringByEvaluatingJavaScriptFromString 方法必须是主线程中执行,而主线程的执行时间过长就会 block UI 的更新。因此咱们应该尽可能让 stringByEvaluatingJavaScriptFromString 方法执行的时间短。
有道云笔记在保存的时候,须要调用 js 得到笔记的完整 html 内容,这个时候若是笔记内容很复杂,就会执行很长一段时间,而由于这个操做必须是主线程执行,因此咱们显示 “正在保存” 的 UIAlertView 彻底没法正常显示,整个 UI 界面彻底卡住了。在新的编辑器里,咱们更新了得到 html 内容的代码,才将这个问题解决。
键盘控制
作 iOS 开发的都知道,当咱们须要键盘显示在某个控件上时,能够调用 [obj becomeFirstResponder] 方法来让键盘出来,而且光标输入焦点出如今该控件上。
可是这个方法对于 UIWebView 并不可用。也就是说,咱们没法经过程序控制让光标输入焦点出如今 UIWebView 上。
关于这个问题,我在 stackoverflow 上专门 问了一下,仍是没有获得很好的解决办法。
CommonJS 规范
commonJS 是一个模块块加载的规范。而 AMD 是该规范的一个草案,CommonJS AMD 规范描述了模块化的定义,依赖关系,引用关系以及加载机制,其规范原文在 这里 。它被 requireJS,NodeJs,Dojo,jQuery 等开源框架普遍使用。这里 还有一篇不错的中文介绍文章。
AMD 规范须要用目录层级看成包层次,这一点就象 java 同样。以前我觉得 iOS 打包后的 ipa 资源文件中不能有资源目录层级关系,今天在会上问了一下,原来是我本身弄错了。若是须要将目录层级带入 ipa 资源文件中,只须要将该目录拖入工程中,而后选择 “Create groups for any added folders”。以下图所示,这样目录层级可以打包到 ipa 文件中。

调试
在 iOS 设备中调试 javascript 是一件至关苦逼的事情,拿 pw 的话来讲:“一会儿回到了 ie6 时代”。固然,业界也有一些调试工具能够用的。
咱们在开发时主要采用的是 weinre 这个框架。用这个框架,能够作一些基本的调试工做,可是它如今功能尚未象 pc 上的 js 调试器那么强大,例如它不能下断点,另外若是有 js 执行错误,它也没法正确的将错误信息报出。它还有一些 bug,例如在 mac 机下,若是你同时链接了有线网和无线网,那么 weinre 将没法正确地链接到调试页面。
但终究,它是如今业界现存的惟一相对可用的调试工具了。本次的 PhoneGap 讲座的第一位演讲者董龙飞有一篇博客很好地介绍了 weinre 的使用,地址是 这里,推荐感兴趣的同窗看看。即便不用 PhoneGap,weinre 也能给你在移动设备上设计网页带来方便。
(2013 年 10 月 22 日更新):关于调试这一起,从 WWDC2012 开始,苹果已经支持用 safari 来链接 iPhone 模拟器里面的 UIWebView 进行调试了,因此调试上已经方便了不少。详细的教程能够查看: WWDC2012 Session 600《Debuging UIWebViews and Websites on iOS》
我对 PhoneGap 的见解
今天的大会上,2 位演讲者把 PhoneGap 吹得至关牛。可是其实真正用过的人才能知道,PhoneGap 仍是有至关多的问题的。至少我知道在网易就有一个使用 PhoneGap 而失败的项目,因此我认为 PhoneGap 仍是有它至关大的局限性的。
我认为 PhoneGap 有如下 3 大问题:
-
首先,PhoneGap 的编程语言实际上是 javascript,这对于非前端工做者来讲,其实学习起来和学习原生的 objective-C 或 Java 编程语言难度差很少,并且因为历史缘由,javascript 语言自己的问题比其它语言都多。要想精通 javascript,至关不易。
-
而后,PhoneGap 的目标是方便地建立跨平台的应用。可是其实苹果和 google 都发布了本身的人机交互指南。有些状况下,苹果的程序和 android 程序有着不一样的交互原则的。象有道云笔记的 iPhone 版 和 android 版,就有着彻底不一样的界面和交互。使用 PhoneGap 就意味着你的程序在 UI 和交互上,既不象原生的 iphone 程序,又不象原生的 android 程序。
-
最后,性能问题。Javascript 终究没法和原生的程序比运行效率,这一点在当你要作一些动画效果的时候,就能显现得很明显。
固然,PhoneGap 的优点也很明显,若是你是作图书类,查询类,小工具类应用的话,这些应用 UI 交互不复杂,也不占用很高的 cpu 资源,PhoneGap 将很好地发挥出它的优点。对于这类应用:
-
你只须要编写一次,则能够同时完成 iOS, android, windows phone 等版本的开发。
-
若是改动不大,只是内容升级,那它升级时只须要更新相应的 js 文件,而不须要提交审核,而通常正常提交苹果的 app store 审核的话,经常须要一周时间。
因此 PhoneGap 不是万能的,但也不是没有用,它有它擅长的领域,一切都看你是否合理地使用它。
最后,推荐 PhoneGap 中国网站 ,在这里,你能够找到为数很少的中文资料。
对 js 的感想
如今前端工程师至关牛逼啊。前端工程师不但能够写前端网页,还能够用 Flex 写桌面端程序,能够用 nodejs 写 server 端程序,能够用 PhoneGap 写移动端程序,这一切,都是基于 javascript 语言的,还有最新出的 windows 8,原生支持用 js 来写 Metro 程序,世界已经没法阻止前端工程师了。