一周前 简单分析了一下支付宝钱包插件的结构 ,得到了很多朋友的支持转发。一来纸上得来终觉浅,二来上篇 Blog 里也立言要写个 Demo,就花了点时间更深入地研究了一下支付宝钱包的插件。研究之后发现,这篇文章很可能会变成 关于“如何分析一个 App 的实现方式”和“如何写 PhoenGap Plugin”的教程 ,结果不一定对开发有帮助,但是分析的过程可以帮到一些刚接触 iOS 开发的朋友了解如何逆向思考一些优秀 App 的实现方式,下面基本上赤裸裸地记录了我整个思考和分析的过程,会显得有些啰嗦。
如果你对下文没兴趣,可以直接去 github 下 Demo 看,其实很简单,实际内容不超过 200 行代码。
https://github.com/allenhsu/PortalDemo
class-dump
略有 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
于是对 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
的定义,思路涌上心头。
Demo
https://github.com/allenhsu/PortalDemo
具体的实现看 github 上的代码,下面我简单提几个实现过程中遇到的问题、思考过程和解决方法。
根据 dump 到的头文件,我简单实现了一个 HtmlViewController
和PLNavigation
插件的- (void)pushWindow:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options
方法,后来发现,这个方法很有用,有它就基本能跑了。
问题一:如何注入 JS,如何触发 onDeviceReady()
上文提到 过初始化过程由 onDeviceReady()
触发,当然你可以直接 eval 这个方法,但看过 PhoneGap 的 Demo 得知这样 不专业 , 专业 的做法是:
1 2 3 | |
所以我在 HtmlViewController
的- (void) webViewDidFinishLoad:(UIWebView*) theWebView
加入了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
先注入 cordovaios.txt
的内容,然后添加 ondeviceready
的事件监听,这些事情要在 call super 之前,这样才能响应到 super 触发的事件。
问题二:如何提取页面标题和 rightBarButtonItem
这里有两种方法,可以由前端 JS 的初始化函数触发 Plugin 来修改标题和 rightBarButtonItem,也可以在 webViewDidFinishLoad
时主动提取,方便起见我选择了后者,问题一中引用的 extractMetaInfoFromWebView
就是简单地从页面中用 JS 提取内容,他的实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
其中大部分是前端知识,例如 document.title
取 title,jsToGetContentOfMetaNamed
中的@"$(\"meta[name='%@']\").attr('content')"
则是用按 name 取 meta 信息,取到后我直接用变量保存了这些信息,也可以考虑用 block。当点击rightBarButtonItem
时触发 didClickOnRightBarItem
再根据之前提取的 onclick
信息去 webView
里 eval(这个函数虽然是可定义的,但大部分页面里是 rightBar()
,可以在 console 中查看相关定义)。
问题三:如何处理相对路径
pushWindow 传入的路径是相对路径,例如 zqdc
子目录下zqdcmatch.html
中 push zqdcfilter.html
传入的是zqdcfilter.html
不带目录,所以要在 pushWindow 方法中处理相对路径:
1 | |
问题四:如何触发从 filter 页面确定的事件
实现以上功能后发现,足球单场的过滤页面可以推入和选择,但是返回后没有触发列表内容更新,查看 zqdcfilter.html
页面的 rightBar
看到有localStorage.setItem('__tbcp__filter__change', 'true');
,顺藤摸瓜找到了 match.js
中响应 frompop 事件的时候用到了这个变量。frompop 则是支付宝在 cordovaios.txt
中自定义的事件。
我比较弱地如下处理:
1 2 3 4 5 6 7 8 9 | |
依然是一些前端知识,用闭包是为了减少变量冲突。
其他
你可以根据 dump 的头文件和 Cordova.plist 实现一些其他的插件,比如 WebAppContext 可以用来做登录(我简单写了一下)。
总结
至此,Demo 基本完成,授人以鱼不如授人以渔,所以我终点记录了过程而非结果,希望能给到大家帮助。
此外,我只是简单的实现了一些最基础的方法,但不足以展现支付宝插件架构的全貌,也不一定是真正的实现方式,比如我简化了 HtmlViewController
直接继承自 CDVViewController
,而不是包含关系。通过 dump 到的信息你能得到更多,有很多问题值得更深入的思考,比如 WebappRuntime
的作用,BundleLoader
的使用, HtmlViewController
和CDVViewController
的包含关系等。如果你有更深入的研究,可以留言或 @许小帅_allen 一起分享。
转载请保留原文链接和作者信息,谢谢。