? ? ? ? 現(xiàn)在業(yè)內(nèi)基本上都在使用WaxPatch方案,由于Wax框架已經(jīng)停止維護(hù)四五年了,所以waxPatch在使用過程中還是存在不少坑(比如參數(shù)轉(zhuǎn)化過程中的問題,如果繼承類沒有實(shí)例化修改繼承類的方法無效, wax_gc中對(duì)oc中instance的持有延遲釋放...)。另外蘋果對(duì)于Wax使用的態(tài)度也處于模糊狀態(tài),這也是一個(gè)潛在的使用風(fēng)險(xiǎn)。
隨著FaceBook開源React Native框架,利用JavaScriptCore.framework直接建立JavaScript(JS)和Objective-C(OC)之間的bridge成為可能,JSPatch也在這個(gè)時(shí)候應(yīng)運(yùn)而生。開始還以為是在React Native的基礎(chǔ)上進(jìn)行的封裝,不過最近仔細(xì)研究了源代碼,跟React Native半毛錢關(guān)系都沒有。
深入了解JSPatch之后,第一感覺是這個(gè)方案小巧,易懂,維護(hù)成本低,直接通過OC代碼去調(diào)用runtime的API,作為一個(gè)IOS開發(fā)者,很快就能看明白,不用花大精力去了解學(xué)習(xí)lua。另外在建立JS和OC的Bridge時(shí),作者很巧妙的利用JS和OC兩種語(yǔ)言的消息轉(zhuǎn)發(fā)機(jī)制做了很優(yōu)雅的實(shí)現(xiàn),稍顯不足的是JSPatch只能支持ios7及以上。
下面我們?cè)賮砜纯碕SPatch對(duì)比WaxPatch的優(yōu)勢(shì)吧!
1.JS語(yǔ)言: JS比Lua在應(yīng)用開發(fā)領(lǐng)域有更廣泛的應(yīng)用,目前前端開發(fā)和終端開發(fā)有融合的趨勢(shì),作為擴(kuò)展的腳本語(yǔ)言,JS是不二之選。
2.符合Apple規(guī)則: JSPatch更符合Apple的規(guī)則。iOS Developer Program License Agreement里3.3.2提到不可動(dòng)態(tài)下發(fā)可執(zhí)行代碼,但通過蘋果JavaScriptCore.framework或WebKit執(zhí)行的代碼除外,JS正是通過JavaScriptCore.framework執(zhí)行的。
3.小巧: 使用系統(tǒng)內(nèi)置的JavaScriptCore.framework,無需內(nèi)嵌腳本引擎,體積小巧。
4.支持block: wax在幾年前就停止了開發(fā)和維護(hù),不支持Objective-C里block跟Lua程序的互傳,雖然一些第三方已經(jīng)實(shí)現(xiàn)block,但使用時(shí)參數(shù)上也有比較多的限制。
什么東西都不止是有優(yōu)點(diǎn)沒有缺點(diǎn)的吧!我們來說說JSPatch的劣勢(shì):相對(duì)于WaxPatch,JSPatch劣勢(shì)在于不支持iOS6,因?yàn)樾枰隞avaScriptCore.framework。另外目前內(nèi)存的使用上會(huì)高于wax,持續(xù)改進(jìn)中。
講到這里了那么就有人會(huì)問了!JSPatch的實(shí)現(xiàn)原理是什么呢?
那我們就來談?wù)凧SPatch的實(shí)現(xiàn)原理吧:JSPatch以小巧的體積做到了讓JS調(diào)用/替換任意OC方法,讓iOS APP具備熱更新的能力,接下來我們就從基礎(chǔ)原理、方法調(diào)用和方法替代來介紹整個(gè) JSPatch 的實(shí)現(xiàn)原理吧!
基礎(chǔ)原理:能做到通過JS調(diào)用和改寫OC方法最根本的原因是 Objective-C 是動(dòng)態(tài)語(yǔ)言,OC上所有方法的調(diào)用/類的生成都通過 Objective-C Runtime 在運(yùn)行時(shí)進(jìn)行,我們可以通過類名/方法名反射得到相應(yīng)的類和方法;也可以替換某個(gè)類的方法為新的實(shí)現(xiàn);還可以注冊(cè)一個(gè)類為類添加方法。理論上你可以在運(yùn)行時(shí)通過類名/方法名調(diào)用到任何OC方法,替換任何類的實(shí)現(xiàn)以及新增任意類。所以 JSPatch 的原理就是:JS傳遞字符串給OC,OC通過 Runtime 接口調(diào)用和替換OC方法。這是最基礎(chǔ)的原理了。
方法的調(diào)用:引入JSPatch后,可以通過以上JS代碼創(chuàng)建了一個(gè) UIView 實(shí)例,并設(shè)置背景顏色和透明度,涵蓋了require引入類,JS調(diào)用接口,消息傳遞,對(duì)象持有和轉(zhuǎn)換,參數(shù)轉(zhuǎn)換這五個(gè)方面,接下來逐一看看具體實(shí)現(xiàn)。
1.require
調(diào)用require(‘UIView’)后,就可以直接使用UIView這個(gè)變量去調(diào)用相應(yīng)的類方法了,require 做的事很簡(jiǎn)單,就是在JS全局作用域上創(chuàng)建一個(gè)同名變量,變量指向一個(gè)對(duì)象,對(duì)象屬性__isCls表明這是一個(gè) Class,__clsName保存類名,在調(diào)用方法時(shí)會(huì)用到這兩個(gè)屬性。
2.JS接口
接下來看看UIView.alloc()是怎樣調(diào)用的。
舊的實(shí)現(xiàn)方式我們就不說了我們直接說說新的實(shí)現(xiàn)方式吧!CoffieScript/JSX都可以用JS實(shí)現(xiàn)一個(gè)解釋器實(shí)現(xiàn)自己的語(yǔ)法,我也可以通過類似的方式做到,再進(jìn)一步想到其實(shí)我想要的效果很簡(jiǎn)單,就是調(diào)用一個(gè)不存在方法時(shí),能轉(zhuǎn)發(fā)到一個(gè)指定函數(shù)去執(zhí)行,就能解決一切問題了,這其實(shí)可以用簡(jiǎn)單的字符串替換,把JS腳本里的方法調(diào)用都替換掉。最后的解決方案是,在OC執(zhí)行JS腳本前,通過正則把所有方法調(diào)用都改成調(diào)用__c()函數(shù),再執(zhí)行這個(gè)JS腳本,做到了類似OC/Lua/Ruby等的消息轉(zhuǎn)發(fā)機(jī)制;給JS對(duì)象基類 Object 的 prototype 加上__c成員,這樣所有對(duì)象都可以調(diào)用到__c,根據(jù)當(dāng)前對(duì)象類型判斷進(jìn)行不同操作;__methodFunc()就是把相關(guān)信息傳給OC,OC用 Runtime 接口調(diào)用相應(yīng)方法,返回結(jié)果值,這個(gè)調(diào)用就結(jié)束了。
這樣做不用去OC遍歷對(duì)象方法,不用在JS對(duì)象保存這些方法,內(nèi)存消耗直降99%,這一步是做這個(gè)項(xiàng)目最爽的時(shí)候,用一個(gè)非常簡(jiǎn)單的方法解決了嚴(yán)重的問題,替換之前又復(fù)雜效果又差的實(shí)現(xiàn)。
3.消息傳遞
解決了JS接口問題,接下來看看JS和OC是怎樣互傳消息的。這里用到了 JavaScriptCore 的接口,OC端在啟動(dòng)JSPatch引擎時(shí)會(huì)創(chuàng)建一個(gè) JSContext 實(shí)例,JSContext 是JS代碼的執(zhí)行環(huán)境,可以給 JSContext 添加方法,JS就可以直接調(diào)用這個(gè)方法;JS通過調(diào)用 JSContext 定義的方法把數(shù)據(jù)傳給OC,OC通過返回值傳會(huì)給JS。調(diào)用這種方法,它的參數(shù)/返回值 JavaScriptCore 都會(huì)自動(dòng)轉(zhuǎn)換,OC里的 NSArray, NSDictionary, NSString, NSNumber, NSBlock 會(huì)分別轉(zhuǎn)為JS端的數(shù)組/對(duì)象/字符串/數(shù)字/函數(shù)類型。上述__methodFunc() 方法就是這樣把要調(diào)用的類名和方法名傳遞給OC的。
4.對(duì)象持有/轉(zhuǎn)換
UIView.alloc通過上述消息傳遞后會(huì)到OC執(zhí)行[UIView alloc],并返回一個(gè)UIView實(shí)例對(duì)象給JS,這個(gè)OC實(shí)例對(duì)象在JS是怎樣表示的呢?怎樣可以在JS拿到這個(gè)實(shí)例對(duì)象后可以直接調(diào)用它的實(shí)例方法(UIView.alloc().init())?
對(duì)于一個(gè)自定義id對(duì)象,JavaScriptCore 會(huì)把這個(gè)自定義對(duì)象的指針傳給JS,這個(gè)對(duì)象在JS無法使用,但在回傳給OC時(shí)OC可以找到這個(gè)對(duì)象。對(duì)于這個(gè)對(duì)象生命周期的管理,按我的理解如果JS有變量引用時(shí),這個(gè)OC對(duì)象引用計(jì)數(shù)就加1 ,JS變量的引用釋放了就減1,如果OC上沒別的持有者,這個(gè)OC對(duì)象的生命周期就跟著JS走了,會(huì)在JS進(jìn)行垃圾回收時(shí)釋放。
傳回給JS的變量是這個(gè)OC對(duì)象的指針,如果不經(jīng)過任何處理,是無法通過這個(gè)變量去調(diào)用實(shí)例方法的。所以在返回對(duì)象時(shí),JSPatch 會(huì)對(duì)這個(gè)對(duì)象進(jìn)行封裝。
5.類型轉(zhuǎn)換
JS把要調(diào)用的類名/方法名/對(duì)象傳給OC后,OC調(diào)用類/對(duì)象相應(yīng)的方法是通過 NSInvocation 實(shí)現(xiàn),要能順利調(diào)用到方法并取得返回值,要做兩件事:
1.取得要調(diào)用的OC方法各參數(shù)類型,把JS傳來的對(duì)象轉(zhuǎn)為要求的類型進(jìn)行調(diào)用。
2.根據(jù)返回值類型取出返回值,包裝為對(duì)象傳回給JS。
例如開頭例子的view.setAlpha(0.5), JS傳遞給OC的是一個(gè) NSNumber,OC需要通過要調(diào)用OC方法的NSMethodSignature得知這里參數(shù)要的是一個(gè) float 類型值,于是把NSNumber轉(zhuǎn)為float值再作為參數(shù)進(jìn)行OC方法調(diào)用。這里主要處理了 int/float/bool 等數(shù)值類型,并對(duì) CGRect/CGRange 等類型進(jìn)行了特殊轉(zhuǎn)換處理,剩下的就是實(shí)現(xiàn)細(xì)節(jié)了。
方法替換
JSPatch 可以用defineClass接口任意替換一個(gè)類的方法,方法替換的實(shí)現(xiàn)過程也是頗為曲折,一開始是用 va_list 的方式獲取參數(shù),結(jié)果發(fā)現(xiàn) arm64 下不可用,只能轉(zhuǎn)而用另一種hack方式繞道實(shí)現(xiàn)。
整個(gè) JSPatch 實(shí)現(xiàn)原理就大致描述完了,剩下的一些小點(diǎn),例如GCD接口,block實(shí)現(xiàn),方法名下劃線處理等就不細(xì)說了,可以直接看相關(guān)的代碼。
說完了JSPatch原理后我們?cè)賮碚f說怎么學(xué)習(xí)和理解吧!
(1)OC的動(dòng)態(tài)語(yǔ)言特性
不管是WaxPatch框架還是JSPatch的方案,其根本原理都是利用OC的動(dòng)態(tài)語(yǔ)言特性去動(dòng)態(tài)修改類的方法實(shí)現(xiàn)。
OC的動(dòng)態(tài)語(yǔ)言特性是在runtime system(全部用C實(shí)現(xiàn),Apple維護(hù)了一份開源代碼)上實(shí)現(xiàn)的,面向?qū)ο蟮腃lass和instance機(jī)制都是基于消息機(jī)制。我們平時(shí)認(rèn)為的[object method],正確的理解應(yīng)該是[receiver sendMsg], 所有的消息發(fā)送會(huì)在編譯階段編譯為runtime c函數(shù)的調(diào)用:_obj_sendMsg(id, SEL).
2)JS如何調(diào)用OC
在JS運(yùn)行環(huán)境中,需要解決兩個(gè)問題,一個(gè)是OC類對(duì)象(objc_class)的獲取,另一個(gè)就是使用對(duì)象提供的接口方法。
3)JS如何替換OC方法
JSPatch的主要作用還是通過腳本修復(fù)一些線上bug,希望能夠達(dá)到替換OC方法的目標(biāo)。JSPatch的實(shí)現(xiàn)巧妙之處在于:利用了OC的消息轉(zhuǎn)發(fā)機(jī)制。
Patch現(xiàn)場(chǎng)復(fù)原的補(bǔ)充:
Patch現(xiàn)場(chǎng)恢復(fù)的功能主要用于連續(xù)更新腳本的應(yīng)用場(chǎng)景。由于IOS的App應(yīng)用按Home鍵或者被電話中斷的時(shí)候,應(yīng)用實(shí)際上是首先進(jìn)入到后臺(tái)運(yùn)行階段(applicationWillResignActive),當(dāng)我們下次再次使用App的時(shí)候,如果后臺(tái)應(yīng)用沒有被終止(applicationWillTerminate),那么App不會(huì)走appliation:didFinishLaunchingWithOptions方法,而是會(huì)走(applicationWillEnterForeground)。 對(duì)于這種場(chǎng)景如果我們連續(xù)更新線上腳本,那么第二次腳本更新則無法保留最開始的方法實(shí)現(xiàn),另外恢復(fù)現(xiàn)場(chǎng)功能也有助于我們撤銷線上腳本能夠恢復(fù)應(yīng)用的本身代碼功能。
了解了這些后知道熱更新的重要性了吧!那么問題就來了,如何實(shí)現(xiàn)熱更新呢?目前為止能夠?qū)崿F(xiàn)熱更新的方法,總結(jié)起來有以下三種:
第一種:便是使用FaceBook 的開源框架 reactive native,用JS來寫原生的iOS應(yīng)用,iOS APP可以在運(yùn)行時(shí)從服務(wù)器拉取最新的JS文件到本地,然后再執(zhí)行,因?yàn)镴S是一門動(dòng)態(tài)的腳本語(yǔ)言,所以可以在運(yùn)行時(shí)直接讀取js文件執(zhí)行,也因此能夠?qū)崿F(xiàn)iOS的熱更新。
第二種:就是使用lua 腳本了。lua腳本如同JS 一樣,也能在動(dòng)態(tài)時(shí)被加載。比如之前憤怒的小鳥使用lua腳本做的一個(gè)插件 wax,可以實(shí)現(xiàn)使用lua寫iOS應(yīng)用。熱更新時(shí),從服務(wù)器拉去lua腳本,然后動(dòng)態(tài)的執(zhí)行就可以了。遺憾的是 wax目前已經(jīng)不更新了。
第三種:是在xcode 6 之后,蘋果自己開放了 iOS 的動(dòng)態(tài)庫(kù)編譯權(quán)限。什么是動(dòng)態(tài)庫(kù)?所謂的動(dòng)態(tài)庫(kù),其實(shí)就是可以在運(yùn)行時(shí)進(jìn)行加載。正好利用這一個(gè)特性,用來做ios的熱更新。
一款超簡(jiǎn)單的社會(huì)化分享的demo:https://github.com/XHXSS/XHXSS-