轉自 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 一塊兒分享。
轉載請保留原文連接和做者信息,謝謝。