zhenby's blog


  • 首页

  • 归档

  • 我读

  • 关于

BLink V1.5 兼容iOS 7了

发表于 2013-10-10   |   分类于 作品   |  

BLink in iOS 7

之前为什么不兼容 iOS 7

我在上一篇博客第一个收费应用-BLink中提到过,BLink 不兼容 iOS 7 是因为 iOS 7 的 bug,那时候因为 iOS 7 还在测试阶段,因为 NDA 的原因,我没细说那个 bug:

在 iOS 7 中,在 Web App 的全屏模式下(standlone),所有会离开当前浏览会话的 JS 执行都无效。比如,在全屏模式下,执行一个 JS 的 alert 语句无效,使用 JS 打开一个应用的 URL Scheme 也会无效。

因为 BLink 的快捷方式实际上就是一个桌面书签,会通过是否全屏模式做不同的操作:非全屏模式下,提示用户添加书签到桌面上;全屏模式下,直接打开一个 URL Scheme。
因为 iOS 7 的这个 bug,在全屏模式下打开一个应用的 URL Scheme 无效了,所以 BLink 在 iOS 7 下的快捷方式打开后只会是一个白屏。

别对苹果修 bug 抱太大希望,要有 Plan B

这个 bug 从 iOS 7 测试版本就一直存在,具体从 beta 几开始我没测试,我从 iOS 7 beta 4 开始试用 iOS 7,就发现了这个 bug,我一直认为现在是 iOS 7 的测试版本,有 bug 很正常,看到苹果开发者论坛上也有人已经报 bug 给苹果了,所以我是幻想着苹果能在 iOS 7 正式版本之前解决这个 bug 的。
但是到了 beta 6,再到9月10号苹果发布会后发布的 GM版,这个 bug 还是一直存在。
我这个时候还是幻想着苹果能在正式版的时候解决这个 bug,毕竟这是个大问题阿,所有全屏 Web App 都会受影响,连 alert 都 alert 不了。
所以在这段 iOS 7 测试版本期间,我没有做任何的行动,也没想好一直幻想着正式版本能正常工作。
但是在9月18号的时候,iOS 7 正式版发布,我测试后,问题依旧阿。我X,而且发现正式版根本就是 GM 版,一个星期一个问题都没解决阿,顿时对苹果失望至极。

因为之前对苹果修 bug 抱太大希望,我根本没考虑 Plan B,所以正式版发布后,一天几个一星差评,几封反馈的邮件。
这是教训阿,以后对这种大版本的更新,在最后的 GM 版本,如果还有问题,就应该尽快想 Plan B,并尽快上线新版本,并且,再加根据系统版本的远程开关,这样即使正式版本修复了,也可以远程控制是否启用 Plan B。

怎么解决的

解决的过程比较曲折,我尝试了使用 Local Storage 以及 Web SQL Database 来保存「是否将快捷方式添加到桌面」的标识,由于我的快捷方式的书签地址为 data:text/html 类型的,都无法使用 Local Storeage 及 Web SQL Database。
一开始一筹莫展,想不到好的办法解决,最后参考了一个同类型的 App,他们是使用安装「描述文件」的方式来生成桌面快捷方式。这种方式我一开始是抗拒的,因为用户不了解什么是描述文件,也不确定安装完后会不会对设备造成其他影响,而且由于我没买电子签名证书,所以我生成的描述文件在设备上安装时,会显示「未签名」的,这也会让用户疑惑。但是实在找不到其他的解决办法,最后只能在安装过程中提示用户「安装此描述文件只会在你的设备主屏幕上添加一个快捷方式,不会修改你设备上的任何设置,所以无需担心描述文件未签名的问题。」

Install the Profile

然后,解决了这个问题后,我又发现了一个 bug,在 iOS 7 中,从原生的应用中尝试打开电话的 URL Scheme 时,会卡个10秒,拨号的界面才会出现,电话已经拨通了,但是界面却一直卡在打开的原生应用中。
这是恶心的问题阿,最后只能直接通过 Safari 打开电话的 URL Scheme,这就导致了每次都会出现一个确认框,用户体验又下降一大截了。

Confirmation box

所以在 iOS 7 中,现在的 BLink 只能算个半成品,只是达到勉强可用的状态,希望苹果尽快发布新的 iOS 7 版本,可能修复这些 bug。

感受

这件事后,我思考了一下,为什么 BLink 会遇到这样的窘境,我发现是因为 BLink 太依赖 iOS 中原生的其他应用了,整个应用流程不能在 BLink 里面单独完成,在 BLink 中完成一部分操作,跳出 BLink,再在其他地方完成另一部分操作。而依赖越多,那么限制也会越多,毕竟依赖的这部分你是无法控制的。
所以我的下个应用,我希望能做一个独立的,拥有完整用户体验的一个应用。

第一个收费App——BLink

发表于 2013-09-12   |   分类于 作品   |  

最近新上架了第一个收费作品:BLink

BLink 0BLink 1

做什么的

这是一个用来创建联系人快捷方式的 App,用户可以用照片,或者内置的头像在桌面上创建一个用于打电话、发短信、发邮件的快捷图标。

原理很简单

如果大家用过 BLink 的话,就会知道它的工作原理还是很简单的,生成图标必须依靠 Safari,所以每个快捷方式其实就是一个 Web Clip,判断到是全屏启动的,则直接跳转到一个 URL Scheme。

限免

由于是第一个收费作品,所以没经验,早上六点多审核通过上线的,中午十二点我就把他设置成限免了,后来 bang 提醒我,设置的太快了,收费的时间太短,这样限免的网站爬不到价格变化,就不会推荐了。

果然,我搜了又搜,限免网站上都找不到推荐,只能自己在爱应用上自推了一下,编辑觉的不错,就推荐了。其他中文网站看到后,也做了不少推荐,所以量还不错。但是都是大中华地区的,我本来定位的主要付费市场是在欧美的,由于限免时机设置的不好,欧美的量少的可怜。

所以第一次上线的时候,可以收费几天,然后再设置限免,这样限免的效果比较好。如果本来已经有收费版本上线了,只是更新版本而已,那审核通过后,可以马上设置成限免。

收费后,BLink 最高的时候冲到了中国区付费总榜单的第30名左右。第二天出销售报告的时候才知道,付费总榜单第30名的钱也没多少钱,单靠这点钱,那些主要市场在中国区的付费应用的公司真没法活。

问题

不兼容 iOS 7

由于 iOS 7 当前的测试版本有 bug,BLink 创建的快捷方式在 iOS 7 上打开后,一片空白,不能打电话,也不能调出发信息、发邮件的界面。因为这个 bug,中国区的用户有几个上来就直接给一星,写说不能兼容 iOS 7,中国区的用户就喜欢尝鲜啊。
对于这个 bug,我试了几种办法,都不能规避它,只能让苹果解决了,已经有国外开发者把这个 bug 提交给苹果了(Apple Developer 论坛对此 bug 的讨论),但是在苹果发布会上发布的 iOS 7 GM 版本上,此 bug 依然存在。希望18号发布的 iOS 7 正式版能解决此问题。

快捷方式启动慢

这个问题我只能尽量的优化,因为是使用 Web Clip 作为快捷方式的,所以快捷方式启动的快慢主要取决于 Web Clip 的加载速度,其他耗时的过程,包括从 Safari 切换到具体应用,具体应用的打开,我不能干预。所以我能做的,只能是缩小 Web Clip 的体积。测试了一天,终于大大缩小了 Web Clip 的体积。优化后的版本为 V1.1 版本,很快就能上线。

iOS 屏幕方向那点事儿

发表于 2013-08-20   |   分类于 编程技术   |  

一般的应用,只会支持竖屏正方向一个方向,支持多个屏幕方向的应用还是比较少的。
不过我在工作的项目中,跟这个屏幕方向接触比较多,因为我们是一个有界面的 SDK,要让接入方接入的,一开始做没什么经验,考虑到接入方本身的屏幕方向可能是多种的,所以我们直接上来就支持四个方向,然后就是各种转屏的问题,90度旋转、180读旋转、270度旋转,测试手都快转断了。
后来觉的根本没必要,浪费了很多时间在解决屏幕方向的问题上,后来就简化到让接入方直接设置支持某个方向了。

一般的应用不用搞的这么的复杂,只要支持一两个屏幕方向就可以了。我也做一下跟屏幕方向有关的几点总结,希望能帮到一些开发者!

系统屏幕方向枚举

通过查看文档,用于控制系统屏幕方向的枚举如下:

// iOS 6 之前用于控制屏幕方向的枚举
typedef enum {
UIInterfaceOrientationPortrait = UIDeviceOrientationPortrait,
UIInterfaceOrientationPortraitUpsideDown = UIDeviceOrientationPortraitUpsideDown,
UIInterfaceOrientationLandscapeLeft = UIDeviceOrientationLandscapeRight,
UIInterfaceOrientationLandscapeRight = UIDeviceOrientationLandscapeLeft
} UIInterfaceOrientation;

// iOS 6 及之后版本用于控制屏幕方向的枚举
typedef enum {
UIInterfaceOrientationMaskPortrait = (1 << UIInterfaceOrientationPortrait),
UIInterfaceOrientationMaskLandscapeLeft = (1 << UIInterfaceOrientationLandscapeLeft),
UIInterfaceOrientationMaskLandscapeRight = (1 << UIInterfaceOrientationLandscapeRight),
UIInterfaceOrientationMaskPortraitUpsideDown = (1 << UIInterfaceOrientationPortraitUpsideDown),
UIInterfaceOrientationMaskLandscape = (UIInterfaceOrientationMaskLandscapeLeft | UIInterfaceOrientationMaskLandscapeRight),
UIInterfaceOrientationMaskAll = (UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskLandscapeLeft |
UIInterfaceOrientationMaskLandscapeRight | UIInterfaceOrientationMaskPortraitUpsideDown),
UIInterfaceOrientationMaskAllButUpsideDown = (UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskLandscapeLeft |
UIInterfaceOrientationMaskLandscapeRight),
} UIInterfaceOrientationMask;

可以发现:

  • iOS 6 及之后版本使用的 UIInterfaceOrientationMask 类型来控制屏幕屏幕方向,该类型也新增加了几个枚举取值,可用一个枚举取值来代表多个屏幕方向。
  • 四个基本屏幕方向(上、下、左、右)中,UIInterfaceOrientationMask = (1 << UIInterfaceOrientation),所以,如果你的应用中需要动态的将 UIInterfaceOrientation 类型转换成 UIInterfaceOrientationMask 类型的话,只需做一下上面的转换即可,不需要通过 switch 来判断再转换。

怎么控制屏幕方向

在 iOS 的应用中,有多种方式可以控制界面的屏幕方向,有全局的,有针对 UIWindow 中界面的控制,也有针对单个界面。

单个界面控制

iOS 6之前

在 iOS 6 之前,单个界面的屏幕方向控制,都使用 UIViewController 类中的这个方法:

// 是否支持旋转到某个屏幕方向
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
{
return ((toInterfaceOrientation == UIInterfaceOrientationLandscapeRight) |
(toInterfaceOrientation == UIInterfaceOrientationLandscapeLeft));
}

默认情况下,此方法只有参数为 UIInterfaceOrientationPortrait 时,返回值才为真,即默认只支持竖屏向上。上面的例子中,表示支持横屏向右及横屏向左两个方向。

iOS 6及之后的版本

在 iOS 6 及之后的版本,单个界面的屏幕方向控制,要使用 UIViewController 在 iOS 6.0 中新增加的两个方法:

// 是否支持转屏
- (BOOL)shouldAutorotate
{
return YES;
}

// 支持的屏幕方向,此处可直接返回 UIInterfaceOrientationMask 类型
// 也可以返回多个 UIInterfaceOrientationMask 取或运算后的值
- (NSUInteger)supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskLandscape;
}

其中 - supportedInterfaceOrientations 方法在 iPad 中默认取值为 UIInterfaceOrientationMaskAll,即默认支持所有屏幕方向;而 iPhone 跟 iPod Touch 的默认取值为 UIInterfaceOrientationMaskAllButUpsideDown,即支持除竖屏向下以外的三个方向。
在设备屏幕旋转时,系统会调用 - shouldAutorotate 方法检查当前界面是否支持旋转,只有 - shouldAutorotate 返回 YES 的时候,- supportedInterfaceOrientations 方法才会被调用,以确定是否需要旋转界面。

UIWindow中的界面控制(iOS 6及以上版本才有效)

在 iOS 6 中,UIApplicationDelegate 协议中添加了一个可以指定 UIWindow 中的界面的屏幕方向的方法:

- (NSUInteger)application:(UIApplication *)application
supportedInterfaceOrientationsForWindow:(UIWindow *)window
{
return UIInterfaceOrientationMaskLandscape;
}

此方法的默认值为 Info.plist 中配置的 Supported interface orientations 项的值。
一般我们都不会创建其他的 UIWindow,所以通过这个方法,也可以达到全局控制。

全局控制

在应用的 Info.plist 文件中,有一个 Supported interface orientations 的配置,可以配置整个应用的屏幕方向,如下图:
Supported interface orientations

此配置其实跟工程中 Target 的 Summary 界面中的 Supported interface orientations 配置是一致的,修改任意一边,另一个边都会同步的修改。
Target Summary

并且,应用在启动时,会使用 Info.plist 中的 Supported interface orientations 项中的第一个值作为启动动画的屏幕方向。按照此处截图的取值,第一个取值为 Portrait(top home button),即竖屏反方向,所以此应用在启动时,会使用竖屏反方向显示启动动画。

多种控制共存的规则

  • 一个界面最后支持的屏幕方向,是取 (全局控制 ∩ UIWindow 中的界面控制 ∩ 单个界面控制) 的交集,如果全局控制支持所有屏幕方向,UIWindow 中的界面控制支持横屏,当个界面中只是支持横屏向右,那么最后界面只会以横屏向右显示,并且不支持旋转到其他的方向。
  • 如果以上三种控制支持的屏幕方向最后的交集为空,iOS 5 跟 iOS 6 的处理有点不同,在 iOS 6 下,甚至会直接抛出 UIApplicationInvalidInterfaceOrientationException 的异常,然后直接崩溃,所以还是要保持这三个值的交集为非空。

浅析 Cordova for iOS

发表于 2013-05-16   |   分类于 编程技术   |  

Cordova,对这个名字大家可能比较陌生,大家肯定听过 PhoneGap 这个名字,Cordova 就是 PhoneGap 被 Adobe 收购后所改的名字。

Cordova 是一个可以让 JS 与原生代码(包括 Android 的 java,iOS 的 Objective-C 等)互相通信的一个库,并且提供了一系列的插件类,比如 JS 直接操作本地数据库的插件类。

这些插件类都是基于 JS 与 Objective-C 可以互相通信的基础的,这篇文章说说 Cordova 是如何做到 JS 与 Objective-C 互相通信的,解释如何互相通信需要弄清楚下面三个问题:

  1. JS 怎么跟 Objective-C 通信
  2. Objective-C 怎么跟 JS 通信
  3. JS 请求 Objective-C,Objective-C 返回结果给 JS,这一来一往是怎么串起来的

Cordova 现在最新版本是 2.7.0,本文也是基于 2.7.0 版本进行分析的。

JS 怎么跟 Objective-C 通信

JS 与 Objetive-C 通信的关键代码如下:(点击代码框右上角的文件名链接,可直接跳转该文件在 github 的地址)

JS 发起请求cordova.js
function iOSExec() {
...
if (!isInContextOfEvalJs && commandQueue.length == 1) {
// 如果支持 XMLHttpRequest,则使用 XMLHttpRequest 方式
if (bridgeMode != jsToNativeModes.IFRAME_NAV) {
// This prevents sending an XHR when there is already one being sent.
// This should happen only in rare circumstances (refer to unit tests).
if (execXhr && execXhr.readyState != 4) {
execXhr = null;
}
// Re-using the XHR improves exec() performance by about 10%.
execXhr = execXhr || new XMLHttpRequest();
// Changing this to a GET will make the XHR reach the URIProtocol on 4.2.
// For some reason it still doesn't work though...
// Add a timestamp to the query param to prevent caching.
execXhr.open('HEAD', "/!gap_exec?" + (+new Date()), true);
if (!vcHeaderValue) {
vcHeaderValue = /.*\((.*)\)/.exec(navigator.userAgent)[1];
}
execXhr.setRequestHeader('vc', vcHeaderValue);
execXhr.setRequestHeader('rc', ++requestCount);
if (shouldBundleCommandJson()) {
// 设置请求的数据
execXhr.setRequestHeader('cmds', iOSExec.nativeFetchMessages());
}
// 发起请求
execXhr.send(null);
} else {
// 如果不支持 XMLHttpRequest,则使用透明 iframe 的方式,设置 iframe 的 src 属性
execIframe = execIframe || createExecIframe();
execIframe.src = "gap://ready";
}
}
...
}

JS 使用了两种方式来与 Objective-C 通信,一种是使用 XMLHttpRequest 发起请求的方式,另一种则是通过设置透明的 iframe 的 src 属性,下面详细介绍一下两种方式是怎么工作的:

XMLHttpRequest bridge

JS 端使用 XMLHttpRequest 发起了一个请求:execXhr.open('HEAD', "/!gap_exec?" + (+new Date()), true); ,请求的地址是 /!gap_exec;并把请求的数据放在了请求的 header 里面,见这句代码:execXhr.setRequestHeader('cmds', iOSExec.nativeFetchMessages()); 。

而在 Objective-C 端使用一个 NSURLProtocol 的子类来检查每个请求,如果地址是 /!gap_exec 的话,则认为是 Cordova 通信的请求,直接拦截,拦截后就可以通过分析请求的数据,分发到不同的插件类(CDVPlugin 类的子类)的方法中:

UCCDVURLProtocol 拦截请求UCCDVURLProtocol.m
+ (BOOL)canInitWithRequest:(NSURLRequest*)theRequest
{
NSURL* theUrl = [theRequest URL];
NSString* theScheme = [theUrl scheme];

// 判断请求是否为 /!gap_exec
if ([[theUrl path] isEqualToString:@"/!gap_exec"]) {
NSString* viewControllerAddressStr = [theRequest valueForHTTPHeaderField:@"vc"];
if (viewControllerAddressStr == nil) {
NSLog(@"!cordova request missing vc header");
return NO;
}
long long viewControllerAddress = [viewControllerAddressStr longLongValue];
// Ensure that the UCCDVViewController has not been dealloc'ed.
UCCDVViewController* viewController = nil;
@synchronized(gRegisteredControllers) {
if (![gRegisteredControllers containsObject:
[NSNumber numberWithLongLong:viewControllerAddress]]) {
return NO;
}
viewController = (UCCDVViewController*)(void*)viewControllerAddress;
}

// 获取请求的数据
NSString* queuedCommandsJSON = [theRequest valueForHTTPHeaderField:@"cmds"];
NSString* requestId = [theRequest valueForHTTPHeaderField:@"rc"];
if (requestId == nil) {
NSLog(@"!cordova request missing rc header");
return NO;
}
...
}
...
}

Cordova 中优先使用这种方式,Cordova.js 中的注释有提及为什么优先使用 XMLHttpRequest 的方式,及为什么保留第二种 iframe bridge 的通信方式:

// XHR mode does not work on iOS 4.2, so default to IFRAME_NAV for such devices.
// XHR mode’s main advantage is working around a bug in -webkit-scroll, which
// doesn’t exist in 4.X devices anyways

iframe bridge

在 JS 端创建一个透明的 iframe,设置这个 ifame 的 src 为自定义的协议,而 ifame 的 src 更改时,UIWebView 会先回调其 delegate 的 webView:shouldStartLoadWithRequest:navigationType: 方法,关键代码如下:

UIWebView拦截加载CDVViewController.m
// UIWebView 加载 URL 前回调的方法,返回 YES,则开始加载此 URL,返回 NO,则忽略此 URL
- (BOOL)webView:(UIWebView*)theWebView
shouldStartLoadWithRequest:(NSURLRequest*)request
navigationType:(UIWebViewNavigationType)navigationType
{
NSURL* url = [request URL];

/*
* Execute any commands queued with cordova.exec() on the JS side.
* The part of the URL after gap:// is irrelevant.
*/

// 判断是否 Cordova 的请求,对于 JS 代码中 execIframe.src = "gap://ready" 这句
if ([[url scheme] isEqualToString:@"gap"]) {
// 获取请求的数据,并对数据进行分析、处理
[_commandQueue fetchCommandsFromJs];
return NO;
}
...
}

Objective-C 怎么跟 JS 通信

熟悉 UIWebView 用法的同学都知道 UIWebView 有一个这样的方法 stringByEvaluatingJavaScriptFromString:,这个方法可以让一个 UIWebView 对象执行一段 JS 代码,这样就可以达到 Objective-C 跟 JS 通信的效果,在 Cordova 的代码中多处用到了这个方法,其中最重要的两处如下:

  • 获取 JS 的请求数据

    获取 JS 的请求数据CDVCommandQueue.m
    - (void)fetchCommandsFromJs
    {
    // Grab all the queued commands from the JS side.
    NSString* queuedCommandsJSON = [_viewController.webView
    stringByEvaluatingJavaScriptFromString:
    @"cordova.require('cordova/exec').nativeFetchMessages()"];

    [self enqueCommandBatch:queuedCommandsJSON];
    if ([queuedCommandsJSON length] > 0) {
    CDV_EXEC_LOG(@"Exec: Retrieved new exec messages by request.");
    }
    }
  • 把 JS 请求的结果返回给 JS 端

    把 JS 请求的结果返回给 JS 端CDVCommandDelegateImpl.m
    - (void)evalJs:(NSString*)js scheduledOnRunLoop:(BOOL)scheduledOnRunLoop
    {
    js = [NSString stringWithFormat:
    @"cordova.require('cordova/exec').nativeEvalAndFetch(function(){ %@ })",
    js];
    if (scheduledOnRunLoop) {
    [self evalJsHelper:js];
    } else {
    [self evalJsHelper2:js];
    }
    }

    - (void)evalJsHelper2:(NSString*)js
    {
    CDV_EXEC_LOG(@"Exec: evalling: %@", [js substringToIndex:MIN([js length], 160)]);
    NSString* commandsJSON = [_viewController.webView
    stringByEvaluatingJavaScriptFromString:js];
    if ([commandsJSON length] > 0) {
    CDV_EXEC_LOG(@"Exec: Retrieved new exec messages by chaining.");
    }

    [_commandQueue enqueCommandBatch:commandsJSON];
    }

    - (void)evalJsHelper:(NSString*)js
    {
    // Cycle the run-loop before executing the JS.
    // This works around a bug where sometimes alerts() within callbacks can cause
    // dead-lock.
    // If the commandQueue is currently executing, then we know that it is safe to
    // execute the callback immediately.
    // Using (dispatch_get_main_queue()) does *not* fix deadlocks for some reaon,
    // but performSelectorOnMainThread: does.
    if (![NSThread isMainThread] || !_commandQueue.currentlyExecuting) {
    [self performSelectorOnMainThread:@selector(evalJsHelper2:)
    withObject:js
    waitUntilDone:NO];
    } else {
    [self evalJsHelper2:js];
    }
    }

怎么串起来

先看一下 Cordova JS 端请求方法的格式:

// successCallback : 成功回调方法
// failCallback : 失败回调方法
// server : 所要请求的服务名字
// action : 所要请求的服务具体操作
// actionArgs : 请求操作所带的参数
cordova.exec(successCallback, failCallback, service, action, actionArgs);

传进来的这五个参数并不是直接传送给原生代码的,Cordova JS 端会做以下的处理:

  1. 会为每个请求生成一个叫 callbackId 的唯一标识:这个参数需传给 Objective-C 端,Objective-C 处理完后,会把 callbackId 连同处理结果一起返回给 JS 端
  2. 以 callbackId 为 key,{success:successCallback, fail:failCallback} 为 value,把这个键值对保存在 JS 端的字典里,successCallback 与 failCallback 这两个参数不需要传给 Objective-C 端,Objective-C 返回结果时带上 callbackId,JS 端就可以根据 callbackId 找到回调方法
  3. 每次 JS 请求,最后发到 Objective-C 的数据包括:callbackId, service, action, actionArgs

关键代码如下:

JS 端处理请求cordova.js
function iOSExec() {
...
// 生成一个 callbackId 的唯一标识,并把此标志与成功、失败回调方法一起保存在 JS 端
// Register the callbacks and add the callbackId to the positional
// arguments if given.
if (successCallback || failCallback) {
callbackId = service + cordova.callbackId++;
cordova.callbacks[callbackId] =
{success:successCallback, fail:failCallback};
}

actionArgs = massageArgsJsToNative(actionArgs);

// 把 callbackId,service,action,actionArgs 保持到 commandQueue 中
// 这四个参数就是最后发给原生代码的数据
var command = [callbackId, service, action, actionArgs];
commandQueue.push(JSON.stringify(command));
...
}

// 获取请求的数据,包括 callbackId, service, action, actionArgs
iOSExec.nativeFetchMessages = function() {
// Each entry in commandQueue is a JSON string already.
if (!commandQueue.length) {
return '';
}
var json = '[' + commandQueue.join(',') + ']';
commandQueue.length = 0;
return json;
};

原生代码拿到 callbackId、service、action 及 actionArgs 后,会做以下的处理:

  1. 根据 service 参数找到对应的插件类
  2. 根据 action 参数找到插件类中对应的处理方法,并把 actionArgs 作为处理方法请求参数的一部分传给处理方法
  3. 处理完成后,把处理结果及 callbackId 返回给 JS 端,JS 端收到后会根据 callbackId 找到回调方法,并把处理结果传给回调方法

关键代码:

Objective-C 返回结果给 JS 端CDVCommandDelegateImpl.m
- (void)sendPluginResult:(CDVPluginResult*)result callbackId:(NSString*)callbackId
{
CDV_EXEC_LOG(@"Exec(%@): Sending result. Status=%@", callbackId, result.status);
// This occurs when there is are no win/fail callbacks for the call.
if ([@"INVALID" isEqualToString : callbackId]) {
return;
}
int status = [result.status intValue];
BOOL keepCallback = [result.keepCallback boolValue];
NSString* argumentsAsJSON = [result argumentsAsJSON];

// 将请求的处理结果及 callbackId 通过调用 JS 方法返回给 JS 端
NSString* js = [NSString stringWithFormat:
@"cordova.require('cordova/exec').nativeCallback('%@',%d,%@,%d)",
callbackId, status, argumentsAsJSON, keepCallback];

[self evalJsHelper:js];
}

JS 端根据 callbackId 回调cordova.js
// 根据 callbackId 及是否成功标识,找到回调方法,并把处理结果传给回调方法
callbackFromNative: function(callbackId, success, status, args, keepCallback) {
var callback = cordova.callbacks[callbackId];
if (callback) {
if (success && status == cordova.callbackStatus.OK) {
callback.success && callback.success.apply(null, args);
} else if (!success) {
callback.fail && callback.fail.apply(null, args);
}

// Clear callback if not expecting any more results
if (!keepCallback) {
delete cordova.callbacks[callbackId];
}
}
}

通信效率

Cordova 这套通信效率并不算低。我使用 iPod Touch 4 与 iPhone 5 进行真机测试:JS 做一次请求,Objective-C 收到请求后不做任何的处理,马上把请求的数据返回给 JS 端,这样能大概的测出一来一往的时间(从 JS 发出请求,到 JS 收到结果的时间)。每个真机我做了三组测试,每组连续测试十次,每组测试前我都会把机器重启,结果如下:

  • iPod Touch 4(时间单位:毫秒):
组\序号 第1次 第2次 第3次 第4次 第5次 第6次 第7次 第8次 第9次 第10次 组平均时间
第一组 10 11 8 13 11 9 14 13 9 12 11.0
第二组 33 13 9 13 11 8 14 12 15 37 15.2
第三组 20 19 9 16 11 17 13 9 10 8 13.2

这三十次测试的平均时间是:(11.0 + 15.2 + 13.2) / 3 = 13.13 毫秒

  • iPhone 5(时间单位:毫秒)
组\序号 第1次 第2次 第3次 第4次 第5次 第6次 第7次 第8次 第9次 第10次 组平均时间
第一组 3 3 4 2 3 2 3 2 2 3 2.7
第二组 7 2 2 2 2 3 2 2 2 4 2.8
第三组 6 3 2 3 2 2 2 3 2 2 2.7

这三十次测试的平均时间是:(2.7 + 2.8 + 2.7) / 3 = 2.73 毫秒

这通信的效率虽然比不上原生调原生,但是也是属于可接受的范围了。

UIWindow的一个兼容性问题

发表于 2013-04-28   |   分类于 编程技术   |  

最近发现了一个奇怪的 UIWindow 的兼容性问题,通过一种比较取巧的方式解决了,如果你有更好的解决办法,请在回复中告诉我。

问题

描述

如果应用中使用了额外的 UIWindow,并且此 UIWindow 中包含了 UIWebView,那么在iOS 5.1或者以下的系统版本中,可能会出现在 UIWebView中触发键盘时,键盘处于不可见的状态。

前提

  • 项目中使用了另一个 UIWindow (在这里给这个额外的 UIWindow 取个代号:HighLevelWindow)
  • 设置此 HighLevelWindow 的 windowLevel 大于 UIWindowLevelNormal,确保在显示时能覆盖在应用默认的 UIWindow 上面 (UIWindowLevelNormal 级别为应用默认的 UIWindow 的 windowLevel 取值)
  • HighLevelWindow 中的 rootViewController 里面包含了一个 UIWebView
  • 在显示此 HighLevelWindow 时,已经调用makeKeyAndVisible方法将 HighLevelWindow 设置成应用的keyWindow,确保能正常的接收到触摸事件

重现步骤

  1. 使用 iOS 5.1或者以下版本的设备运行应用,在应用默认的 UIWindow 中触发过键盘,如触发过一个输入框(UITextField),见下图:

在默认UIWindow中触发键盘

2.调出 HighLevelWindow,将 HighLevelWindow 中的 UIWebView 载入 google.com,点击 google 的搜索输入框,会发现网页虽然上推了,但是键盘处于不可见的状态,见下图:

键盘处于不可见的状态

分析

上 google 搜了很久,给出的解决方案都是说没把 HighLevelWindow 设置成应用的 KeyWindow,导致接收不到屏幕的触摸事件,但是从 google 网页上推可以看出,其实 HighLevelWindow 是可以接收到触摸事件的,而且我已经确定把 HightLevelWindow 设置成应用的 KeyWindow 了。

现在的症状就是键盘在默认的 UIWindow 中触发过后,在 HinhLevelWindow 中再次触发,键盘其实是被挡在了 HighLevelWindow 后面,处于不可见,不可点击的状态。(此症状可通过将 HighLevelWindow 的 frame.size 设置成只有屏幕一半大小来确定)

尝试了各种方法,都是不行的,而且从 iOS 6.0 开始,此 bug 是不存在的,那更可以确定是一个兼容性的问题了。

解决

我突然想起,就在之前项目还没改用 UIWebView 前,使用一般的 UITextField 输入框是可以正常的显示键盘的,于是,就有以下这种取巧的解决方案:
在包含了 UIWebView 的 UIViewController 显示时(viewDidAppear: 方法调用时),插入一个 UITextField,迅速的获取焦点,然后取消焦点,删除此 UITextFiled ,关键代码如下:

- FixedBugWindowViewController.m
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];

// 在系统版本低于 iOS 6.0 时才做此操作
if ([[[UIDevice currentDevice] systemVersion] floatValue] < 6.0)
{
// 新建一个 UITextField,并添加到视图中
UITextField *textFieldFixKeyBoardBug = [[UITextField alloc] initWithFrame:self.view.bounds];
[self.view addSubview:textFieldFixKeyBoardBug];

// 获取焦点,释放焦点
[textFieldFixKeyBoardBug becomeFirstResponder];
[textFieldFixKeyBoardBug resignFirstResponder];

// 将 UITextField 从视图中移除,并释放
[textFieldFixKeyBoardBug removeFromSuperview];
[textFieldFixKeyBoardBug release];
}
}

加上以上代码后,可以正常的显示键盘了。

可正常显示键盘

不单单是UIWebView,不单单是键盘

  • 不单单是 UIWebView,我在使用 MFMessageComposeViewController 时也遇过这个问题,类似的 MFMailComposeViewController 应该也会出现这个问题,可以使用相同的方法修复
  • 不单单是键盘,在 UIWebView 中,页面有一个下拉框,会显示一个 UIPickerView,此 UIPickerView 也处于被隐藏,不可见的状态,也可以使用此方法修复

Demo源码

Demo源码地址:https://github.com/zhenby/UIWindowKeyboardBug

函数vs方法

发表于 2013-03-05   |   分类于 编程技术   |  

函数(function),方法(method),之前没细究它们的不同,随心所欲的想说哪个就说哪个,“这个初始化函数…”,“这个初始化方法…”,看着都差不多,没什么区别。
直到前几天,一个新来的同事,在看我整理的 Objective-C代码规范文档,里面有一段是这样的:

初始化函数

  • (void)init
    {
    …
    }
    …

他看到后,疑惑的跟我说:“你这表达方式不对吧,你标题说的是函数,但是内容却说的是方法。”
哦?原来函数跟方法是不一样的。

函数

一个代码块,完成特定的功能,然后将结果返回给调用方,常见的函数的格式是这样的:

<return type> <function name> (<arg1 type> <arg1 name>, <arg2 type> <arg2 name>, ... )
{
// Code here
}

一个函数声明与调用的例子:
// 实现函数
int plus(int x, int y)
{
return x * y;
}

int main (int argc, const char *argv[])
{
int x = 2;
int y = 3;
// 调用函数
int result = plus(x, y);

return 0;
}

方法

也是一个代码块,不过方法是需要写在类里面的,调用时需要类或者对象才可以调用,一个 Objective-C 的方法例子如下:

@implementation Person

// 实现方法
- (void)setName:(NSString *)name
{
//code here
}

@end

int main (int argc, const char *argv[])
{
// 使用对象调用方法
[[Person new] setName:@"zhenby"];

return 0;
}

有什么不同

那说到底,函数跟方法的不同就是:方法是属于类或者对象的,而函数则不一定,可以独立于类与对象之外,独立调用,所以可以说 函数 >= 方法,因此方法也可以叫 member function。

Objective-C中的函数

Objective-C 中一般的函数是全局有效的(可在函数前加 static 关键字使得该函数只在该文件中有效),即在一个文件中实现了一个函数,在同个项目中的其他代码中都可以直接调用此函数,所以定义函数时,函数名需要唯一,重复的函数名(不管参数是否一致)是编译不过的。
知道这个特性后,就可以把一些常用的代码块,比如获取当前时间戳这样的功能的整理成了一个函数,这样的好处是项目中的代码在需要时都可以直接调用,而不需要类或者对象,类似于 NSLog 函数。

而我在实现函数的时候,遇到了一个这样的警告“no previous prototype for function xxx ”,这个警告的意思是没找到一个前置的函数原型,在文件的顶端,或者头文件(如果有的话)加上你所加的函数原型就可以了,例如:

// 函数原型
int plus(int, int);
/*
如果参数为空的话,在函数原型中需要传 void,在函数原型中参数为空的话,
在C中表示此函数可以接受任意个参数,在 Objective-C 中也有一样的规则
*/

long timestamp(void);


// 实现函数
int plus(int x, int y)
{
return x * y;
}

long timestamp()
{
// 返回当前的时间戳
return (long)[[NSDate date] timeIntervalSince1970];
}

Objective-C中的方法

在 Objective-C 中,方法的调用是通过消息传递来进行的,需要在运行时才能确定方法的地址(只要知道一个类的方法名,不管这个方法是否公开,都可以调用到,这也是为啥苹果的私有 API 会被挖出来,所以也没有受保护方法这样的说法,方法要么是公开的,要么是不公开的,无论公不公开,通过方法名都可以调用到方法),而消息传递就是通过id objc_msgSend(id theReceiver, SEL theSelector, ...)这个函数来达到目的的,可以说 Objective-C 中的方法,其实相当于固定前两个参数的 objc_msgSend 函数。比如:

@implementation Test

- (long)timestamp
{
NSLog(@"in timestamp");
// 返回当前时间戳
return (long)[[NSDate date] timeIntervalSince1970];
}

@end

// 调用 timestamp 方法
Test *test = [Test new];
[test timestamp];
[test release];

//----------------------------------------------------------

/*
上面 [test timestamp] 这句代码就相当于以下的函数调用,
直接执行下面的代码,也可以在控制台中打印出 in timestamp。
*/

#import <objc/message.h>

objc_msgSend(test, @selector(timestamp));

2012

发表于 2013-02-10   |   分类于 生活   |  

大家新年好!大年初一,窝在家写博客,写写我的2012。

工作

2012春节后,开始去公司的游戏中心,从服务端转成客户端,开始搞 iOS。这一年我感觉很不错,比之前搞服务端进步大很多。
之前搞服务端,主要提供的是接口跟数据,而客户端,是用户可以直接接触到的产品,我更喜欢客户端,离用户更近一点。

作品

今年开始才有自己个人的作品。
做了一个叫剪贴箱的 app,下载量很一般,用户量很少;我也没做推广,发版本的时候就在微博上发一下。用户量跟用户评价都很少,所以让我没了更新版本的动力, 到现在为止也只是更新了两个版本而已。不过v1.1 版本发了之后,每天的新增用户跟活跃用户都有明显的提高,现在能稳定排在中国区效率类热门榜单五六十名左右的位置。这是我第一次做一个完整的产品,从一个小想法,到一个产品。
其实还做了另一个娱乐类的 app,想着年前能上线的,不过就前两天被苹果的审核拒了,也发现了一些 bugs,所以等着年后修复完再重新提交审核。

开源

开始 iOS 开发后,我还有另一个改变,就是开始大量的使用开源项目,自己也有意识的开源一些代码了。
想想以前做服务端,做的都是比较简单的东西,对数据进行简单的处理,把数据保持在内存里,找出符合条件的数据返回……很少需要用到开源的项目,所以自己也不会有意识的把某些模块独立出来,作为一个开源项目。
做了 iOS 后开始接触到了大量的开源项目,一遇到一个系统没提供的功能或者控件,先上 github 找开源库,找不到那就自己尝试写一个。Three20,JSONKit,ASIHttpRequest,EGO等这些优秀的开源项目给 iOS 开发者很大的帮助。
我慢慢的也会自己开源一些东西,我的 github 主页:http://github.com/zhenby。

我读

今年开始阅读,读了大概20本书,书单在这里。之前也会零散的阅读,一本书放在床头,三个月都没读完,读到最后都忘了前面讲什么了。
2012年年初买了 Kindle,也在手机,iTouch 上装上各种阅读软件,买电子书看。睡觉前看,上厕所的时候也看,公车地铁上也看,感觉读一本没之前想的那么困难,有时候一天就可以读完一本书了。我越来越喜欢读电子书了,随时随地想看就看,做书摘,做笔记,再导出来放到 Evernote 中,多方便。很多人还是钟情于实体书,我也买实体书,不过相对于电子书,我读实体书的效率低很多,这跟我没有大片连续的阅读时间有关,所以我现在都首选电子书,想读的书实在没电子版,再考虑买实体书。2012年真应该对电子书说一声我爱你。

运动

2012年的运动频率明显增多了。
夏天的时候,每个工作日基本能保持一天一跑步,跑个两三公里。现在冬天懒了,一三五才去跑。亏了一群跟我一起跑步的同事,六点一到,几个人就开始动身准备跑步,几个人互相问一问去不去跑步,今天穿牛仔裤,不方便跑?拿不跑也去走走,吊一下单杠嘛。好,走!
2012我还去参加了一次10Km的马拉松跑,2013年继续参加。对了,2013年还有一个运动的目标就是骑单车去珠海。

2013

  • 读书、运动的好习惯要继续保持
  • 开源多点东西
  • 继续参加广州马拉松,骑单车去珠海

印象普吉岛

发表于 2013-02-09   |   分类于 生活   |  

梦

除夕夜了,这两天天气冷了不少。就三天前我从广州回来的那天,广州的天气热到就穿一件薄长袖还会流汗,所以我就带了一件薄外套回来。把这件外套穿上,扣子扣紧,开着摩托车出去,浑身发抖。
被冷了这么几天后,昨晚做梦的时候,我梦到了我在海边晒太阳。梦里我穿着一条沙滩裤,太阳很猛,海风徐徐,近处大家正在踏着海浪;远处拖拽伞刚被水上摩托拉起,在蓝天下飞的跟白云一样高。多舒服,我躺下,直接睡在沙滩上,让阳光洒满全身,正想舒服的睡去的时候,我突然想起,我靠,我还没擦防晒霜。这可不成,会被晒伤的阿!然后就起来准备去买防晒霜,就醒了!买什么防晒霜!买什么防晒霜!
醒来后才想起梦中海边的景色不就跟当时在普吉岛一摸一样,才想起我还没写这篇游记呢。
普吉岛

行程

11月底从吉隆坡去的普吉岛,待了四天,就逛了两个海滩,巴东跟卡伦。大部分时间都在巴东上,巴东很热闹,人很多,一条海鲜街,晚上还有一条小吃街。卡伦的海滩人比较少,很舒服,适合去那里晒着太阳睡觉。

吃

巴东有很多饭店,海鲜一条街,是吃海鲜的好地方,推荐 NO.6 饭店,价格公道,味道也不错。
巴东夜市晚上的时候,很多路边摊出动,里面有各式各样的小吃。
不过肠胃不好的,就少吃生的海鲜咯,我们吃了个不熟的生蚝,然后我女朋友中招了,闹了一天肠胃炎。
NO.6的海鲜

骑摩托车

直接用护照去抵押,就可以租到摩托车,价格不贵,一天就三四十块。骑着摩托车很方便的,可以到处逛,吹着海风,看着海景,逛去远一点的地方。我租了一天,从巴东海滩骑到了卡伦海滩,那段山路很多的上下坡,开起来有飙车的感觉。不过有点危险,而且泰国是靠左行驶的,一开始不习惯。那天我开着摩托车去买东西,转弯后一时忘了靠左行驶,迎面骑来的老外对我大喊:“You should drive left!!”,我才意识到我骑错边了,“I sorry, i forgot”,我小声的应答。
摩托车头盔

人妖

没去看人妖表演,不过在饭店吃饭的时候遇到一个会讲普通话的人妖,骨架很大,男生的声音,女生的外貌,不漂亮,显得有点粗狂。
她有很多女性化的动作:说话激动时用手捂着脸,不耐烦时给出一个 小S 的白眼,还有一个比较夸张的是,空闲下来时在旁边做两腿张开的深蹲动作,我猜是想身材更好点吧。

欧美老年人

普吉岛有超多的欧美人,而且还有很多欧美老年人。女的深受肥胖毒害,站在沙滩上,浪花打过来纹丝不动;而男的一副大好肯德基上校的样子,站在老婆旁边,望着老婆,是不是的说上几句话。我为什么一直强调欧美老年人,因为真的没看见其他脸孔的老年人在度假。心态不一样,中国这个时期的老年人,忙于担心儿女的婚事,忙于带孙子孙女。
欧美老年人

再乱扯一下

  • 在泰国的电视上看到微信的广告,赞助了一个节目,那个节目让大家通过微信进行投票,用微信查看附近的人,发现也有不少人,原来微信已经进军泰国市场了
  • 观察了一下泰国人拿的手机,大部分的本地人,拿的基本都是 Nokia 之类的功能机,少量的 Android 机,欧美人大部分都拿 iPhone
  • 坑爹的酒店竟然没 wifi,感觉普吉岛那边的 wifi 不是很普及,搜出来一些免费热点,也上不了
1234
zhenby

zhenby

28 日志
5 分类
13 标签
© 2016 zhenby
由 Hexo 强力驱动
主题 - NexT.Mist