转自 Allenjavascript
昨天看到唐巧分析了支付宝钱包插件的实现原理,今天也趁热打铁研究了一下支付宝插件的结构和代码,不少时候逆向思考也能够为本身积累不少有用的经验(即使实际实现方式和本身所想有出入)。css
承接唐巧的上文,本文一样以彩票插件为例,若是你没有下载该插件的压缩包,用以下命令下载。html
wget http://download.alipay.com/mobilecsprod/alipay.mobile/20130601021432806/xlarge/10000011.amr
并改成 zip 后缀,解压。前端
Manifest.xml
描述,index-alipay-native.html
为插件入口页面唐巧打开 index-alipay-native.html
给你们看过了,你会发现 Cells 都是不能点的,由于页面还没有初始化,代码中能够看到形似 <a rel="ssq/ssqbet.html">
的代码。显然这表明双色球的子页面,我简单猜想一下用 rel 的缘由:编程
首先,点击 Cell 在客户端触发的必然是 Native 的 pushViewController:
,用 href 一样能够经过webView:shouldStartLoadWithRequest:navigationType:
触发,但这样会在某些意外状况下形成点击后页面直接跳转。此外若是最终是用形如 push://10000011/ssq/ssqbet.html
的协议来实现 Native 跳转,用 rel 再注入 JS 处理能够防止客户端逻辑的显式暴露(如长按、拷贝,曾经的新浪微博客户端长按评论能够看到相似 comment:// 的协议,不见得有多大危害,可是值得避免)
页面底部有以下代码:
1 2 3 4 5 6 7 8 9 10 11 |
//配置时间戳,解决浏览器或者webview cache问题 seajs.config({ map: [ [ /^(.*\.(?:css|js))(.*)$/i, '$1?000021'] ] }); function onDeviceReady(){ seajs.use('../js/appnav/nav',function(Nav){ Nav.initialize(); }); } |
略懂前端开发的话,果断猜想 onDeviceReady()
是在页面加载完毕后,由设备调用的初始化方法,因而尝试在 Console 中输入 onDeviceReady()
执行,点击 Cell,报错以下:
跟到 nav.js
里能够看到是在 pushWindow
中报错的,错误为 alipay
未定义,粗略判断 alipay
为客户端注入的变量,这里看到的调用形如 alipay.navigation.pushWindow(obj.attr('rel'))
用到了上述 rel
属性,最终应该会触发客户端相似 [self pushWebControllerWithURL:url]
的代码来推入下一个 VC,这位咱们最终尝试实现能跑这个插件的 Demo 提供了一些思路。
用 onDeviceReady
能够完成大部分页面的初始化工做,如 ssq/ssqbet.html
,在浏览器里也能响应下拉机选操做,你会发现,这些插件的屏幕尺寸兼容性和浏览器兼容性极佳:
再如 jczq/jczqmatch.html
,甚至能够读到场次数据并完成数据初始化了
在这里咱们又发现了两个有意思的信息,它们被写在 meta 信息里,根据字面,很容易理解,它们分别定义了navigationBar
的 title
和 rightBarButtonItem
,以及点击 rightBarButtonItem
触发的 JS 函数。此时你若是比对着使用客户端就知道这个筛选对应着怎样的表现和交互,这些都为咱们写 Demo 提供了线索。
尝试在 Console 输入 rightBar
(不带括号)查看其定义,又能获得不少有用的线索,好比点击rightBarButtonItem
触发的是推入 filterpage
,这个 filterpage
则是在 js/jczq/jczqmatch.js
中定义的。
此外,结合 jczqfilter.html
的内容和 rightBar
函数,能够看到主页面利用 HTML5 的 localStorage 为 filter 页面提供了数据(这为纠结于 UIWebView 如何给 push 进来的下一个 View 提供数据的我,提供了很好的思路,值得借鉴)
1
|
localStorage.setItem('__tbcp__filter', JSON.stringify(obj)) |
不知不觉写了 2 个多小时了,发现经过文字表达出来要比分析自己还费时。不过,至此,支付宝插件的大致框架已经比较清晰了,和 Native Code 的交互方式、数据传递方式也有了必定的思路。
接下来的几天我会尝试写一个 Demo 让这样一个插件基本能跑起来(除了核心的下单、支付环节,这显然不是我力所能及的)。跟着个人分析,相信读者对 WebView + Native Code 的混合编程模式应该也有了一些新的认识。
但愿我有足够的动力和时间写 Demo,由于这个 Demo 能够帮助你们更清楚地理解支付宝插件中用到的 WebView + Native Code 的 Hybrid 开发方式。
一周前简单分析了一下支付宝钱包插件的结构,获得了不少朋友的支持转发。一来纸上得来终觉浅,二来上篇 Blog 里也立言要写个 Demo,就花了点时间更深刻地研究了一下支付宝钱包的插件。研究以后发现,这篇文章极可能会变成关于“如何分析一个 App 的实现方式”和“如何写 PhoenGap Plugin”的教程,结果不必定对开发有帮助,可是分析的过程能够帮到一些刚接触 iOS 开发的朋友了解如何逆向思考一些优秀 App 的实现方式,下面基本上赤裸裸地记录了我整个思考和分析的过程,会显得有些啰嗦。
若是你对下文没兴趣,能够直接去 github 下 Demo 看,其实很简单,实际内容不超过 200 行代码。
https://github.com/allenhsu/PortalDemo
略有 obj-c 开发经验的同窗应该都知道利器 class-dump。由于 obj-c 的动态特性致使 obj-c 的二进制代码中会保留类名和方法名,因此能够用 otool
获得这些信息,而 class-dump 所作的就是把 otool -ov
获得的信息组织成结构更清晰的信息输出,比 otool
的输出更易读。class-dump 在个人工做中为我带来不少便利,经过查看类的定义,能够帮助我了解一个好的 App 的架构和部分逻辑的实现思路。
首先,从任意网站获得支付宝钱包的 .ipa 文件,改后缀为 .zip,解压获得 Portal.app(或者直接从机器里获得 Portal.app),右键 Show Package Content,其中最大的 Portal 就是二进制文件,能够从 app 目录拷贝到一个干净的目录。而后我通常会分别执行如下两条命令:
class-dump Portal > class-dump.txt class-dump -H -o ./ Portal
其中第一行把全部 dump 的信息输出到一个文件,方便 ctrl-f,第二行则把全部信息以头文件的形式输出到当前目录,每一个类一个文件。
上篇文章提到过点击连接时,console 中提示未定义的方法是 alipay.navigation.pushWindow
,因此直接在 class-dump.txt
中尝试搜索了 pushWindow
,找到两个相关类:HtmlViewController
和PLNavigation
。HtmlViewController
是内嵌 Web 的 VC,其中还包含了一个 CDVViewController
的变量,而 PLNavigation
则继承自 CDVPlugin
,虽然没实际用过 PhoneGap,但这些信息足以说明支付宝钱包的插件是基于 PhoneGap 实现 JS 和 Native 代码通讯的(huangzhi 也在给个人留言中提到了这点)
注:方法的变量能够适当修改成合适的类型和名称,二进制代码不保留变量类型和名称。
因而对 PhoneGap 作了一些学习(此处略去学习过程),果断在支付宝的 Package 里搜索 cordova,俩文件,Cordova.plist
和 cordovaios.txt
,看起来有用就 cp 出来。
先下载了最新的 PhoneGap 2.9.0,发现升级文档中提到了 2.7.0 中 Cordova.plist
变成了config.xml
,回头扫了一眼 cordovaios.txt
,所幸没有加混淆,这就是 cordova.js
,虽去掉了大部分版本信息,可是看到了相似 TODO: remove in 2.0.
的注释,因此用的是 1.x 版本,经过 JS 文件的比对和升级文档的提示,肯定 cordovaios.txt
来自 1.6.1 版本的 PhoneGap。(注:PhoneGap 1.6.1 不支持 ARC)
在 cordovaios.txt
的尾部咱们也看到了 alipay
的定义,Native 暴露给 JS 的方法尽收眼底(未混淆)。
Cordova.plist
则是 PhoneGap 的配置文件,其中也包含全部 Plugin 的映射关系,好比NavigationClass => PLNavigation
,配合刚才 dump 出来的头文件和 alipay
的定义,思路涌上心头。
https://github.com/allenhsu/PortalDemo
具体的实现看 github 上的代码,下面我简单提几个实现过程当中遇到的问题、思考过程和解决方法。
根据 dump 到的头文件,我简单实现了一个 HtmlViewController
和 PLNavigation
插件的 - (void)pushWindow:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options
方法,后来发现,这个方法颇有用,有它就基本能跑了。
上文提到过初始化过程由 onDeviceReady()
触发,固然你能够直接 eval 这个方法,但看过 PhoneGap 的 Demo 得知这样不专业,专业的作法是:
1 2 3 |
document.addEventListener("deviceready", onDeviceReady, false); function onDeviceReady() { } |
因此我在 HtmlViewController
的 - (void) webViewDidFinishLoad:(UIWebView*) theWebView
加入了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
- (void) webViewDidFinishLoad:(UIWebView*) theWebView { static NSString* jsString = nil; if (!jsString) { NSString *jsFile = [[NSBundle mainBundle] pathForResource:@"cordovaios" ofType:@"txt"]; jsString = [[NSString alloc] initWithContentsOfFile:jsFile encoding:NSUTF8StringEncoding error:NULL]; } [theWebView stringByEvaluatingJavaScriptFromString:jsString]; [theWebView stringByEvaluatingJavaScriptFromString:@"document.addEventListener(\"deviceready\", onDeviceReady, false)"]; [self extractMetaInfoFromWebView:theWebView]; return [super webViewDidFinishLoad:theWebView]; } |
先注入 cordovaios.txt
的内容,而后添加 ondeviceready
的事件监听,这些事情要在 call super 以前,这样才能响应到 super 触发的事件。
这里有两种方法,能够由前端 JS 的初始化函数触发 Plugin 来修改标题和 rightBarButtonItem,也能够在webViewDidFinishLoad
时主动提取,方便起见我选择了后者,问题一中引用的extractMetaInfoFromWebView
就是简单地从页面中用 JS 提取内容,他的实现以下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
- (void)extractMetaInfoFromWebView:(UIWebView *)theWebView { self.title = [theWebView stringByEvaluatingJavaScriptFromString:@"document.title"]; NSString *jsString = [self jsToGetContentOfMetaNamed:@"right-bar-item"]; NSString *rightBarItemMeta = [theWebView stringByEvaluatingJavaScriptFromString:jsString]; if (rightBarItemMeta.length > 0) { self.rightItemDict = [self dictionaryFromMetaString:rightBarItemMeta]; if ([self.rightItemDict objectForKey:@"title"]) { NSString *title = [self.rightItemDict objectForKey:@"title"]; self.storeRightItem = [[[UIBarButtonItem alloc] initWithTitle:title style:UIBarButtonItemStyleBordered target:self action:@selector(didClickOnRightBarItem:)] autorelease]; [self.navigationItem setRightBarButtonItem:self.storeRightItem animated:YES]; } } } |
其中大部分是前端知识,例如 document.title
取 title,jsToGetContentOfMetaNamed
中的@"$(\"meta[name='%@']\").attr('content')"
则是用按 name 取 meta 信息,取到后我直接用变量保存了这些信息,也能够考虑用 block。当点击 rightBarButtonItem
时触发didClickOnRightBarItem
再根据以前提取的 onclick
信息去 webView
里 eval(这个函数虽然是可定义的,但大部分页面里是 rightBar()
,能够在 console 中查看相关定义)。
pushWindow 传入的路径是相对路径,例如 zqdc
子目录下 zqdcmatch.html
中 pushzqdcfilter.html
传入的是 zqdcfilter.html
不带目录,因此要在 pushWindow 方法中处理相对路径:
1
|
NSString *startPage = [[currentPage stringByDeletingLastPathComponent] stringByAppendingPathComponent:[arguments objectAtIndex:1]]; |
实现以上功能后发现,足球单场的过滤页面能够推入和选择,可是返回后没有触发列表内容更新,查看zqdcfilter.html
页面的 rightBar
看到有localStorage.setItem('__tbcp__filter__change', 'true');
,顺藤摸瓜找到了 match.js
中响应 frompop 事件的时候用到了这个变量。frompop 则是支付宝在 cordovaios.txt
中自定义的事件。
我比较弱地以下处理:
1 2 3 4 5 6 7 8 9 |
- (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; if (firstTime) { firstTime = NO; } else { [self.webView stringByEvaluatingJavaScriptFromString:@"(function() { var channel = cordova.require(\"cordova/channel\"); channel.onPop.fire(); }())"]; } } |
依然是一些前端知识,用闭包是为了减小变量冲突。
你能够根据 dump 的头文件和 Cordova.plist 实现一些其余的插件,好比 WebAppContext 能够用来作登陆(我简单写了一下)。
至此,Demo 基本完成,授人以鱼不如授人以渔,因此我终点记录了过程而非结果,但愿能给到你们帮助。
此外,我只是简单的实现了一些最基础的方法,但不足以展示支付宝插件架构的全貌,也不必定是真正的实现方式,好比我简化了 HtmlViewController
直接继承自 CDVViewController
,而不是包含关系。经过 dump 到的信息你能获得更多,有不少问题值得更深刻的思考,好比 WebappRuntime
的做用,BundleLoader
的使用,HtmlViewController
和 CDVViewController
的包含关系等。若是你有更深刻的研究,能够留言或 @许小帅_allen 一块儿分享。
转载请保留原文连接和做者信息,谢谢。