iOS hybrid App 的實(shí)現原理及性能監測
作者董一凡自述:作為一名寫(xiě)了十年代碼的程序員,目前我最擅長(cháng)的領(lǐng)域是移動(dòng)平臺的客戶(hù)端開(kāi)發(fā),在移動(dòng)領(lǐng)域的開(kāi)發(fā)時(shí)間超過(guò)七年,前前后后涉獵過(guò)很多個(gè)平臺。隨著(zhù)大部分移動(dòng)平臺自己走向死亡,現在我也主要專(zhuān)注在了iOS和Android兩大移動(dòng)平臺,偶爾也會(huì )客串下Windows這個(gè)不知道是移動(dòng)還是桌面的平臺。 十年前,我剛入行的時(shí)候,曾經(jīng)認為自己將會(huì )永遠做一個(gè)C++程序員,于是花了大量時(shí)間在C++上?,F在C++也是我工作所用的主力語(yǔ)言之一,工作之外也會(huì )偶爾寫(xiě)點(diǎn)什么娛樂(lè )一下。 寫(xiě)了一些年程序后,終于意識到了之前定位的狹隘,于是開(kāi)始廣泛的學(xué)習各種技術(shù),各種各樣的語(yǔ)言也學(xué)了很多,值得慶幸的是,幾年折騰下來(lái),我一直也沒(méi)有對寫(xiě)代碼這件事感到厭倦,于是我又認為自己將會(huì )永遠把開(kāi)發(fā)做下去。 現在,我也覺(jué)得開(kāi)發(fā)是一個(gè)可以終身做下去的事業(yè),不過(guò)除了事業(yè)我還想追求更多的東西,從這些年的經(jīng)歷來(lái)看,其中貫穿始終的就是在不停的學(xué)習,想明白這一點(diǎn)后,我開(kāi)始除技術(shù)之外更廣領(lǐng)域的學(xué)習,比如日語(yǔ),畫(huà)畫(huà),設計,鋼琴等等,給自己的定位也變成了在今后作為一名終身學(xué)習者。
一 iOS hybrid App 簡(jiǎn)單介紹
大家應該多少都知道,iOS 設備上有兩種入口,一是通過(guò) App Strore 下載一個(gè)個(gè)的 App,另一個(gè)是用系統瀏覽器去訪(fǎng)問(wèn)網(wǎng)頁(yè)。前者我們一般稱(chēng)為原生應用,后者就是傳統意義上的網(wǎng)頁(yè)。兩者各有特點(diǎn),開(kāi)發(fā)一個(gè)原生應用,一般是使用 Apple 給我們提供的開(kāi)發(fā)工具和 Cocoa 框架。優(yōu)勢就是可以利用到系統的所有特性,做出很酷的特性而不損失任何的性能,而缺點(diǎn)就是每次 App 提供新功能都必須重新打包 App,提交給 Apple 進(jìn)行審核,通過(guò)以后再上架 App Store,最后用戶(hù)再升級,平均需要兩周的時(shí)間。相反,寫(xiě)一個(gè)網(wǎng)頁(yè)則完全沒(méi)有這個(gè)限制,服務(wù)器做一次升級,用戶(hù)通過(guò)瀏覽器再訪(fǎng)問(wèn),就是最新的了,而寫(xiě)網(wǎng)頁(yè)的缺點(diǎn)則是受到很大的限制,很多系統特性是無(wú)法訪(fǎng)問(wèn)的,而且性能往往不高,以至于很難實(shí)現一些很酷的效果。
鑒于原生應用和網(wǎng)頁(yè)各有優(yōu)勢,所以就衍生出了一種介于兩者之間的開(kāi)發(fā)方式--混合應用(hybrid App)。其特點(diǎn)是在原生應用中嵌入一個(gè)瀏覽器組件,然后通過(guò)某種方式,讓原生代碼和網(wǎng)頁(yè)能夠雙向通訊,結果就是可以在需要原生功能的時(shí)候使用原生功能,而適合放在網(wǎng)頁(yè)端的部分就放在服務(wù)器上。某種程度上利用到了兩者的優(yōu)勢。另一個(gè)優(yōu)勢就是,由于網(wǎng)頁(yè)技術(shù)在 iOS 和 Android 上是一樣的,所以網(wǎng)頁(yè)的這部分也就天然可以跨平臺了。
二 如何實(shí)現 hybrid App
實(shí)現一個(gè) hybrid App 最簡(jiǎn)單的方法就是使用 Apache Cordova 開(kāi)源框架。Cordova 已經(jīng)幫你做好了所有的網(wǎng)頁(yè)和原生應用之間的橋接工作,你需要做的就是根據他的文檔去寫(xiě)對應的網(wǎng)頁(yè)代碼和原生代碼就行了。具體請參考官方網(wǎng)站
可惜的是,我們總有些場(chǎng)景無(wú)法使用 Cordava,比如我曾經(jīng)的一個(gè)項目,項目主要是要提供一個(gè) SDK ,SDK 本身要使用 hybrid 的技術(shù)。但是 SDK 的用戶(hù)可能也會(huì )用到 Cordova,有些情況下,兩者用的 Cordova 為不同版本,正好無(wú)法兼容。于是就需要自己去實(shí)現 hybrid App 的底層了。
三 iOS hybrid App 的底層實(shí)現
1. 原生代碼調用網(wǎng)頁(yè)中的 JavaScript 函數
假設我們的網(wǎng)頁(yè)中有如下代碼
[scripttype=text/javascript]functionmyFunc(){returnTextfromweb}[/script]
原生代碼可以用如下方式調用 myFunc()
NSString*result=[self.webViewstringByEvaluatingJavaScriptFromString:@myFunc()];
在這里 result 就等于 Text from web
2. 網(wǎng)頁(yè)中的 JavaScript 調用系統的原生代碼
這一步比上邊的要復雜一些,iOS 不像 Android 可以直接給網(wǎng)頁(yè)中的 JavaScript 函數注入一個(gè)原生代碼的接口。這里我們會(huì )用一個(gè)比較曲折的方式來(lái)實(shí)現。
假設 Objective-C 的類(lèi)里有一個(gè)方法
-(void)nativeFunction:(NSString*)args{}
JavaScript 里我們用下邊的方法來(lái)最終調用到上邊這個(gè)方法
window.JSBridge.callFunction(callNativeFunction,somedata);
在我們的頁(yè)面里,是沒(méi)有 JSBridge.callFunction 存在的,這一步我們要在原生代碼端注入。
在 webView 的 delegate 的 - (void)webViewDidFinishLoad:(UIWebView *)webView 里我們用下邊的方式注入 JavaScript
NSString*js=@(function(){window.JSBridge={};window.JSBridge.callFunction=function(functionName,args){varurl=bridge-js://invoke?;varcallInfo={};callInfo.functionname=functionName;if(args){callInfo.args=args;}url+=JSON.stringify(callInfo);varrootElm=document.documentElement;variFrame=document.createElement(IFRAME);iFrame.setAttribute(src,url);rootElm.appendChild(iFrame);iFrame.parentNode.removeChild(iFrame);};returntrue;})();;[webViewstringByEvaluatingJavaScriptFromString:js];
簡(jiǎn)單解釋一下,首先我們在 window 里創(chuàng )建一個(gè)叫 JSBridge 的對象,然后在里邊定義一個(gè)方法 callFunction,這個(gè)方法的作用是把兩個(gè)參數打包為 JSON 字符串,然后附帶到我們自定義的 URL bridge-js://invoke? 后邊,最后用 IFRAME 的方式來(lái)加載這個(gè) URL
這么做的原因是,當加載 IFRAME 的時(shí)候,就會(huì )調用 webView 的 delegate 的 - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType 方法,其中 request 就是我們剛才自定義的那個(gè) URL,在這個(gè)方法里我們做如下處理
NSURL*url=[requestURL];NSString*urlStr=url.absoluteString;return[selfprocessURL:urlStr];
processURL 函數如下
-(BOOL)processURL:(NSString*)url{NSString*urlStr=[NSStringstringWithString:url];NSString*protocolPrefix=@bridge-js://invoke?;if([[urlStrlowercaseString]hasPrefix:protocolPrefix]){urlStr=[urlStrsubstringFromIndex:protocolPrefix.length];urlStr=[urlStrstringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];NSError*jsonError;NSDictionary*callInfo=[NSJSONSerializationJSONObjectWithData:[urlStrdataUsingEncoding:NSUTF8StringEncoding]options:kNilOptionserror:jsonError];NSString*functionName=[callInfoobjectForKey:@functionname];NSString*args=[callInfoobjectForKey:@args];if([functionNameisEqualToString:@callNativeFunction]){[selfnativeFunction:args];}returnNO;}returnYES;}
從 bridge-js://invoke? 這個(gè)自定義的 URL 里邊把附帶在后邊 JSON 字符串解析出來(lái),然后判斷 functionname key 的值如果是 callNativeFunction 那么就去調用原生方法 nativeFunction, 如果需要實(shí)現更多的方法調用,只要添加這個(gè)映射關(guān)系就行了。
至此,JavaScript 和 Objective-C 代碼的雙向調用就都實(shí)現了。
四 性能監測
Hybrid 和原生應用之間的爭論一直以來(lái)都不少,其核心問(wèn)題其實(shí)就是如何平衡開(kāi)發(fā)成本和用戶(hù)體驗之間的關(guān)系。Hybrid的開(kāi)發(fā)成本一般來(lái)說(shuō)要低于原生應用,然后其體驗總是要差一些。為了讓 Hybrid 的用戶(hù)體驗能更可能的接近原生應用,性能監測就顯的更為重要了。
影響 App 使用體驗一般來(lái)講有兩個(gè)主要方面
第一方面是 UI 的響應速度,UI 的流暢與否給用戶(hù)的體驗是非常不一樣的。對這方面的性能監測,一般的做法就是在主要的交互函數里打上時(shí)間戳,而對于系統的 View,也可以采用 Method Swizzle 的方法對所有的系統函數的調用時(shí)間進(jìn)行統計。
二是網(wǎng)絡(luò ),而由于現在的大部分 App 都多少有了網(wǎng)絡(luò )請求,所以網(wǎng)絡(luò )的請求速度也會(huì )很大程度上影響用戶(hù)體驗。網(wǎng)絡(luò )問(wèn)題在 Hybrid App 就體現的更明顯。Hybrid App 總是會(huì )去加載服務(wù)器端的頁(yè)面,在頁(yè)面加載出來(lái)之前,很可能整個(gè)手機屏幕是空白的,如果空白時(shí)間太長(cháng),將是一個(gè)很糟糕的事情,所以實(shí)時(shí)的監測請求網(wǎng)頁(yè)的時(shí)間,以及頁(yè)面的加載速度就非常有必要了。針對 webView,建議在它的 delegate 的幾個(gè)方法里打上時(shí)間戳,以此來(lái)統計頁(yè)面請求和加載的時(shí)間。
總之實(shí)現起來(lái),并不是一個(gè)非常復雜的工作。然而性能監測的工作,實(shí)現只是其中的一個(gè)方面,由于用戶(hù)的使用習慣,實(shí)際的網(wǎng)絡(luò )環(huán)境各種問(wèn)題,性能監測并不是在開(kāi)發(fā)階段監測一下就算完了的,一般來(lái)說(shuō),總是得把監測工作部署到最終用戶(hù)的手機上去的,如果是一個(gè)用戶(hù)量不小的 App,那么如何把收集到的大量數據很好的統計顯示出來(lái),這完全是另一回事了,把這事做好,要牽扯到很多的數據組織,前端展示的工作,實(shí)際的實(shí)施絕對不是個(gè)簡(jiǎn)單的工作。
所幸,現在已經(jīng)有很多公司幫我們完成了這個(gè)工作,比如 New Relic,App dynamics,Compuware,聽(tīng)云等。這次我們就以聽(tīng)云為例,看看他們是怎么來(lái)做性能監測這件事的。
五 聽(tīng)云探針(iOS App版)的使用
聽(tīng)云對 App 的性能監測使用起來(lái)還是比較簡(jiǎn)單的,簡(jiǎn)單步驟如下
申請完聽(tīng)云的賬戶(hù)后,在添加 App 的地方填寫(xiě)相關(guān)信息
之后就會(huì )得到一個(gè)唯一的 App Key
然后下載聽(tīng)云的 iOS SDK 的 Framework,拷貝到項目中,注意添加以下 4 個(gè)額外的系統庫
CoreTelephony.framework
Security.framework
SystemConfiguration.framework
libz.dylib
然后在 App 的 pch 文件中包含聽(tīng)云 App 探針的頭文件
#import
最后將 main.m 中加入
[NBSAppAgentstartWithAppID:App_Key];
代碼一般為下邊的樣子
intmain(intargc,char*argv[]){@autoreleasepool{[NBSAppAgentstartWithAppID:App_Key];returnUIApplicationMain(argc,argv,nil,NSStringFromClass([AppDelegateclass]));}}
這樣整個(gè)集成工作就完成了。啟動(dòng) App,如果在 Log 日志中有如下內容顯示就表示代碼集成成功
NBSAppAgent2.2.2.1---->start!SuccesstoconnecttoNBSSERVER
六 聽(tīng)云監測數據觀(guān)察
1. 匯總數據
登錄到聽(tīng)云的后臺管理頁(yè)面,首先我們可以看到匯總的監測數據,圖表的效果還是不錯的,鼠標放到每個(gè)數據點(diǎn)上會(huì )顯示詳細的數據。
總體看來(lái),分為兩大類(lèi),一類(lèi)是應用交互性能,這類(lèi)主要是監測 UI 響應情況,會(huì )給出 view 加載,以及 layout 的時(shí)間匯總。如果發(fā)現某一項參數出現異常,那也許就是需要重構 UI 的信號了。另一類(lèi)是網(wǎng)絡(luò )性能,包含網(wǎng)絡(luò )請求的響應時(shí)間等。
2. Web View
重要的東西最先講,這個(gè)部分是聽(tīng)云目前最有特色的部分(好像是首家這么做的,目前還沒(méi)在其他的類(lèi)似服務(wù)里看到這個(gè)功能)。通常我們進(jìn)行網(wǎng)絡(luò )性能監測的時(shí)候,給出的是整個(gè)網(wǎng)絡(luò )請求的情況,這在瀏覽器里邊來(lái)說(shuō),整個(gè)網(wǎng)絡(luò )請求其實(shí)也就是頁(yè)面的請求,兩者沒(méi)有區別。而到了 App 里,同樣是 http 請求,有可能是來(lái)自 web service 的調用,也可能是來(lái)自 web view 加載頁(yè)面。而后者正好是我們講的 Hybrid App 的主要實(shí)現方式。聽(tīng)云的這個(gè)條目就是完全只給出 web view 所進(jìn)行的請求情況,換句話(huà)說(shuō),這是我們用來(lái)監測 Hybrid App 網(wǎng)絡(luò )性能的最好數據。
(1)HTTP請求
這里有所有 web view 所加載的頁(yè)面的匯總數據。
(2)頁(yè)面加載
聽(tīng)云除了給出網(wǎng)絡(luò )性能的數據,這里還很貼心的給出了頁(yè)面加載的匯總數據,要知道現在的網(wǎng)頁(yè)是有可能非常復雜,包含很多頁(yè)面元素的,在桌面端問(wèn)題也許不明顯,但在移動(dòng)端,太復雜的效果也許會(huì )大大的拖慢加載速度,影響用戶(hù)體驗。根據這里給出的頁(yè)面加載數據,就可以有針對性的去優(yōu)化網(wǎng)頁(yè)了。
3. 網(wǎng)絡(luò )
這個(gè)條目主要是 App 的所有網(wǎng)絡(luò )請求的數據。
(1)拓補圖
這主要是一個(gè)分類(lèi)匯總的數據,可以分別查看是自己的 App 所以及第三方服務(wù)所發(fā)送的網(wǎng)絡(luò )請求的匯總數據。
(2)HTTP請求
顧名思義,這里是所有 http 請求的詳細數據,分別顯示了響應最慢的主機,以及吞吐量最高的主機。
(3)地域
這個(gè)條目比較有意思,用顏色的方式標示出了世界各國的平均響應時(shí)間,點(diǎn)擊對應的國家還可以繼續進(jìn)入到下一級,最后可以進(jìn)入到詳細的網(wǎng)絡(luò )數據分析頁(yè)面。
(4)組合分析
這個(gè)頁(yè)面在中國的網(wǎng)絡(luò )環(huán)境下是非常有用的,它可以根據運營(yíng)商,地域,接入方式來(lái)給出匯總數據。要知道中國的網(wǎng)絡(luò )環(huán)境非常復雜,不同運營(yíng)商之間,甚至同一運營(yíng)商在不同的地區網(wǎng)絡(luò )互通情況會(huì )相差非常大。有了這里的數據,就可以有針對性的去部署服務(wù)器,優(yōu)化網(wǎng)絡(luò )體驗。
4. 交互
進(jìn)入交互分析具體項
在這里我們可以詳細的看到 ViewController 以及 View 的每一個(gè)系統函數的調用時(shí)間,通過(guò)這個(gè)數據就可以非常好的分析是哪個(gè)一個(gè) ViewController 出了問(wèn)題,對應的去重構就可以了。
交互分析下邊的幾項是通過(guò)一定的條件來(lái)看 UI 交互的具體數據,可以通過(guò)版本進(jìn)行過(guò)濾,這樣就可以方便的過(guò)濾掉已經(jīng)把問(wèn)題修改掉了的版本。還可以通過(guò)操作系統(iOS/Android)和設備來(lái)看各自的交互響應的數據。
5. 其他
除了性能分析,聽(tīng)云的數據里也有常見(jiàn)的崩潰數據,活躍數以及事件監測等。這里就不詳細展開(kāi)了
七 總結
Hybrid App 在某些特定場(chǎng)景是非常有用的,然而也確實(shí)有它的局限性,特別是對交互要求很高的地方,使用它是不太合適,畢竟它還是基于網(wǎng)頁(yè)技術(shù)。不過(guò)html5在移動(dòng)端的發(fā)展也非常迅速,也許會(huì )有更好的未來(lái)也說(shuō)不定??傊莆者@個(gè)技術(shù)是不會(huì )錯的。另外由于網(wǎng)頁(yè)端很可能會(huì )成為我們性能瓶頸,所以要時(shí)時(shí)注意測試相關(guān)部分的性能表現,也建議使用一些應用性能監測的第三方服務(wù)。這樣能夠更好的定位產(chǎn)品環(huán)境的問(wèn)題。
評論