iOS中的HotFix方案总结详解
iOS中的HotFix方案總結(jié)詳解
相信HotFix大家應(yīng)該都很熟悉了,今天主要對(duì)于最近調(diào)研的一些方案做一些總結(jié)。iOS中的HotFix方案大致可以分為四種:
- WaxPatch(Alibaba)
- Dynamic Framework(Apple)
- React Native(Facebook)
- JSPatch(Tencent)
WaxPatch
WaxPatch是一個(gè)通過Lua語言編寫的iOS框架,不僅允許用戶使用 Lua 調(diào)用 iOS SDK和應(yīng)用程序內(nèi)部的 API, 而且使用了 OC runtime 特性調(diào)用替換應(yīng)用程序內(nèi)部由 OC 編寫的類方法,從而達(dá)到HotFix的目的。
WaxPatch的優(yōu)點(diǎn)在于它支持iOS6.0,同時(shí)性能上比較的優(yōu)秀,但是缺點(diǎn)也是非常的明顯,不符合Apple3.2.2的審核規(guī)則即不可動(dòng)態(tài)下發(fā)可執(zhí)行代碼,但通過蘋果JavaScriptCore.framework或WebKit執(zhí)行的代碼除外;同時(shí)Wax已經(jīng)長期沒有人維護(hù)了,導(dǎo)致很多OC方法不能用Lua實(shí)現(xiàn),比如Wax不支持block;最后就是必須要內(nèi)嵌一個(gè)Lua腳本的執(zhí)行引擎才能運(yùn)行Lua腳本;Wax并不支持arm64框架。
Dynamic Framework
動(dòng)態(tài)的Framework,其實(shí)就是動(dòng)態(tài)庫;首先我介紹一下關(guān)于動(dòng)態(tài)庫和靜態(tài)庫的一些特性以及區(qū)別。
不管是靜態(tài)庫還是動(dòng)態(tài)庫,本質(zhì)上都是一種可執(zhí)行的二進(jìn)制格式,可以被載入內(nèi)存中執(zhí)行。
iOS上的靜態(tài)庫可以分為.a文件和.framework,動(dòng)態(tài)庫可以分為.dylib(xcode7以后變成了.tdb)和.framework。
靜態(tài)庫: 鏈接時(shí)完整地拷貝至可執(zhí)行文件中,被多次使用就有多份冗余拷貝。
動(dòng)態(tài)庫: 鏈接時(shí)不復(fù)制,程序運(yùn)行時(shí)由系統(tǒng)動(dòng)態(tài)加載到內(nèi)存,供程序調(diào)用,系統(tǒng)只加載一次,多個(gè)程序共用,節(jié)省內(nèi)存。
靜態(tài)庫和動(dòng)態(tài)庫是相對(duì)編譯期和運(yùn)行期的:靜態(tài)庫在程序編譯時(shí)會(huì)被鏈接到目標(biāo)代碼中,程序運(yùn)行時(shí)將不再需要改靜態(tài)庫;而動(dòng)態(tài)庫在程序編譯時(shí)并不會(huì)被鏈接到目標(biāo)代碼中,只是在程序運(yùn)行時(shí)才被載入,因?yàn)樵诔绦蜻\(yùn)行期間還需要?jiǎng)討B(tài)庫的存在。
總結(jié):同一個(gè)靜態(tài)庫在不同程序中使用時(shí),每一個(gè)程序中都得導(dǎo)入一次,打包時(shí)也被打包進(jìn)去,形成一個(gè)程序。而動(dòng)態(tài)庫在不同程序中,打包時(shí)并沒有被打包進(jìn)去,只在程序運(yùn)行使用時(shí),才鏈接載入(如系統(tǒng)的框架如UIKit、Foundation等),所以程序體積會(huì)小很多。
好,所以Dynamic Framework其實(shí)就是我們可以通過更新App所依賴的Framework方式,來實(shí)現(xiàn)對(duì)于Bug的HotFix,但是這個(gè)方案的缺點(diǎn)也是顯而易見的它不符合Apple3.2.2的審核規(guī)則,使用了這種方式是上不了Apple Store的,它只能適用于一些越獄市場(chǎng)或者公司內(nèi)部的一些項(xiàng)目使用,同時(shí)這種方案其實(shí)并不適用于BugFix,更適合App線上的大更新。所以其實(shí)我們項(xiàng)目中的引入的那些第三方的Framework都是靜態(tài)庫,我們可以通過file這個(gè)命令來查看我們的framework到底是屬于static還是dynamic。
React Native
React Native支持用JavaScript進(jìn)行開發(fā),所以可以通過更改JS文件實(shí)現(xiàn)App的HotFix,但是這種方案的明顯的缺點(diǎn)在于它只適合用于使用了React Native這種方案的應(yīng)用。
JSPatch
JSPatch是只需要在項(xiàng)目中引入極小的JSPatch引擎,就可以使用JavaScript語言調(diào)用Objective-C的原生接口,獲得腳本語言的能力:動(dòng)態(tài)更新iOS APP,替換項(xiàng)目原生代碼、快速修復(fù)bug。但是JSPatch也有它的自己的缺點(diǎn),主要在由于它要依賴javascriptcore,framework,而這個(gè)framework是在iOS7.0以后才引入進(jìn)來,所以JSPatch是不支持iOS6.0的,同時(shí)由于使用的是JS的腳本技術(shù),所以在內(nèi)存以及性能上面是要低于Wax的。
所以最后當(dāng)然還是采用了JSPatch這種方案,但是實(shí)際過程中還是出現(xiàn)了一些問題的,所以掌握J(rèn)SPatch的核心原理對(duì)于我們解決問題是非常有幫助的。
關(guān)于JSPatch的核心原理講解
預(yù)加載部分
關(guān)于核心原理的講解,網(wǎng)上有不少,但是幾乎都是差不多,很多都還是引用了作者bang自己寫的文檔的內(nèi)容,所以我采用一個(gè)例子方式進(jìn)行講解JSPatch的主要運(yùn)行流程,其實(shí)當(dāng)然也會(huì)引用一些作者的簡述,大家可以參照我寫的流程講述,在配合源碼或者官方文檔的介紹,應(yīng)該就可以了解JSPatch。
[JPEngine startEngine];NSString *sourcePath = [[NSBundle mainBundle] pathForResource:@"demo" ofType:@"js"];NSString *script = [NSString stringWithContentsOfFile:sourcePath encoding:NSUTF8StringEncoding error:nil];[JPEngine evaluateScript:script];首先是運(yùn)行[JPEngine startEngine]啟動(dòng)JSPatch,啟動(dòng)過程分為一下兩部分:
-
通過JSContext,聲明了一些JS方法到內(nèi)存,這樣我們之后就可以在JS中調(diào)用這些方法,主要常用到的包括以下幾個(gè)方法,同時(shí)會(huì)監(jiān)聽一個(gè)內(nèi)存告警的通知。
context[@"_OC_defineClass"] = ^(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods) {return defineClass(classDeclaration, instanceMethods, classMethods);};context[@"_OC_callI"] = ^id(JSValue *obj, NSString *selectorName, JSValue *arguments, BOOL isSuper) {return callSelector(nil, selectorName, arguments, obj, isSuper);};context[@"_OC_callC"] = ^id(NSString *className, NSString *selectorName, JSValue *arguments) {return callSelector(className, selectorName, arguments, nil, NO);};context[@"_OC_formatJSToOC"] = ^id(JSValue *obj) {return formatJSToOC(obj);};context[@"_OC_formatOCToJS"] = ^id(JSValue *obj) {return formatOCToJS([obj toObject]);};context[@"_OC_getCustomProps"] = ^id(JSValue *obj) {id realObj = formatJSToOC(obj);return objc_getAssociatedObject(realObj, kPropAssociatedObjectKey);};context[@"_OC_setCustomProps"] = ^(JSValue *obj, JSValue *val) {id realObj = formatJSToOC(obj);objc_setAssociatedObject(realObj, kPropAssociatedObjectKey, val,OBJC_ASSOCIATION_RETAIN_NONATOMIC);};
- 加載JSPatch.js文件,JSPatch文件的主要內(nèi)容在于定義一些我們之后會(huì)用在的JS函數(shù),數(shù)據(jù)結(jié)構(gòu)以及變量等信息,之后我會(huì)在用到的時(shí)候詳細(xì)介紹。
腳本運(yùn)行
我們定義如下的腳本:
require('UIAlertView') defineClass('AppDelegate',['name', 'age', 'temperatureDatas'],{testFuncationOne: function(index) {self.setName('wuyike')self.setAge(21)self.setTemperatureDatas(new Array(37.10, 36.78, 36.56))var alertView = UIAlertView.alloc().initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles("title", self.name(), self, "OK", null)alertView.show()}},{testFuncationTwo: function(datas) {var alertView = UIAlertView.alloc().initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles("title", "wwww", self, "OK", null)alertView.show()}});然后就是執(zhí)行我們上面說的[JPEngine evaluateScript:script]了,程序開始執(zhí)行我們的腳本,但是在這之前,JSpatch會(huì)對(duì)我們的腳本做一些處理,這一步同樣也包括兩個(gè)方面:
- 需要給我們的程序加上try catch的部分代碼,主要目的是當(dāng)我們的JS腳本有錯(cuò)誤的時(shí)候,可以catch到錯(cuò)誤信息
- 將所有的函數(shù)都改成通過__c原函數(shù)的形式進(jìn)行調(diào)用。
也就是最后我們調(diào)用的腳本已經(jīng)變成如下的形式了:
;(function(){try{require('UIAlertView') defineClass('AppDelegate',['name', 'age', 'temperatureDatas'],{testFuncationOne: function(index) {self.__c("setName")('wuyike')self.__c("setAge")(21)self.__c("setTemperatureDatas")(new Array(37.10, 36.78, 36.56))var alertView = UIAlertView.__c("alloc")().__c("initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles")("title", self.__c("name")(), self, "OK", null)alertView.__c("show")()}},{testFuncationTwo: function(datas) {var alertView = UIAlertView.__c("alloc")().__c("initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles")("title", "wwww", self, "OK", null)alertView.__c("show")()}});那么為什么需要用函數(shù)__c來替換我們的函數(shù)呢,因?yàn)镴S語法的限制對(duì)于沒有定義的函數(shù)JS是無法調(diào)用的,也就是調(diào)用UIAlertView.alloc()其實(shí)是非法的,因?yàn)樗捎玫牟⒉皇窍⑥D(zhuǎn)發(fā)的形式,所以作者原來是想把一個(gè)類的所有函數(shù)都定義在JS上,也就是如下形式:
{__clsName: "UIAlertView",alloc: function() {…},beginAnimations_context: function() {…},setAnimationsEnabled: function(){…},... }但是這種形式就必須要遍歷當(dāng)前類的所有方法,還要循環(huán)找父類的方法直到頂層,這種方法直接導(dǎo)致的問題就是內(nèi)存暴漲,所以是不可行的,所以最后作者采用了消息轉(zhuǎn)發(fā)的思想,定義了一個(gè)_c的原函數(shù),所有的函數(shù)都通過_c來轉(zhuǎn)發(fā),這樣就解決了我們的問題。
值得一提的是我們的__c函數(shù)就是在我們執(zhí)行JSPatch.js的時(shí)候聲明到j(luò)s里的Object方法里去的,就是下面這個(gè)函數(shù),_customMethods里面聲明了很多需要追加在Object上的函數(shù)。
for (var method in _customMethods) {if (_customMethods.hasOwnProperty(method)) {Object.defineProperty(Object.prototype, method, {value: _customMethods[method], configurable:false, enumerable: false})}}1. require
調(diào)用 require('UIAlertView') 后,就可以直接使用UIAlertView這個(gè)變量去調(diào)用相應(yīng)的類方法了,require做的事很簡單,就是在JS全局作用域上創(chuàng)建一個(gè)同名變量,變量指向一個(gè)對(duì)象,對(duì)象屬性 __clsName 保存類名,同時(shí)表明這個(gè)對(duì)象是一個(gè) OC Class。
var _require = function(clsName) {if (!global[clsName]) {global[clsName] = {__clsName: clsName}}return global[clsName] }這樣我們?cè)诮酉聛碚{(diào)用UIAlertView.__c()方法的時(shí)候系統(tǒng)就不會(huì)報(bào)錯(cuò)了,因?yàn)樗呀?jīng)是JS中一個(gè)全局的Object對(duì)象了。
{__clsName: "UIAlertView" }2.defineClass
接下來我們就要執(zhí)行defineClass函數(shù)了
global.defineClass = function(declaration, properties, instMethods, clsMethods)defineClass函數(shù)可接受四個(gè)參數(shù):
字符串:”需要替換或者新增的類名:繼承的父類名 <實(shí)現(xiàn)的協(xié)議1,實(shí)現(xiàn)的協(xié)議2>”
[屬性]
{實(shí)例方法}
{類方法}
當(dāng)我調(diào)用這個(gè)函數(shù)以后主要是做三件事情:
- 執(zhí)行_formatDefineMethods方法,主要目的是修改傳入的function函數(shù)的的格式,以及在原來實(shí)現(xiàn)上追加了從OC回調(diào)回來的參數(shù)解析。
- 然后執(zhí)行_OC_defineClass方法,也就是調(diào)用OC的方法,解析傳入類的屬性,實(shí)例方法,類方法,里面會(huì)調(diào)用overrideMethod方法,進(jìn)行method swizzing操作,也就是方法的重定向。
- 最后執(zhí)行_setupJSMethod方法,在js中通過_ocCls記錄類實(shí)例方法,類方法。
關(guān)于_formatDefineMethods
_formatDefineMethods方法接收的參數(shù)是一個(gè)方法列表js對(duì)象,加一個(gè)新的js空對(duì)象
可以發(fā)現(xiàn),具體實(shí)現(xiàn)是遍歷方法列表對(duì)象的屬性(方法名),然后往js空對(duì)象中添加相同的屬性,它的值對(duì)應(yīng)的是一個(gè)數(shù)組,數(shù)組的第一個(gè)值是方法名對(duì)應(yīng)實(shí)現(xiàn)函數(shù)的參數(shù)個(gè)數(shù),第二個(gè)值是一個(gè)函數(shù)(也就是方法的具體實(shí)現(xiàn))。_formatDefineMethods作用,簡單的說,它把defineClass中傳遞過來的js對(duì)象進(jìn)行了修改:
原來的形式是:{testFuncationOne:function(){...}} 修改之后是:{testFuncationOne: [argCount, function (){...新的實(shí)現(xiàn)}]}傳遞參數(shù)個(gè)數(shù)的目的是,runtime在修復(fù)類的時(shí)候,無法直接解析原始的js實(shí)現(xiàn)函數(shù),那么就不知道參數(shù)的個(gè)數(shù),特別是在創(chuàng)建新的方法的時(shí)候,需要根據(jù)參數(shù)個(gè)數(shù)生成方法簽名,也就是還原方法名字,所以只能在js端拿到j(luò)s函數(shù)的參數(shù)個(gè)數(shù),傳遞到OC端。
// js 方法 initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles// oc 方法 initWithTitle:message:delegate:cancelButtonTitle:otherButtonTitles:關(guān)于_OC_defineClass
使用NSScanner分離classDeclaration,分離成三部分
- 類名 : className
- 父類名 : superClassName
- 實(shí)現(xiàn)的協(xié)議名 : protocalNames
使用NSClassFromString(className)獲得該Class對(duì)象。
- 若該Class對(duì)象為nil,則說明JS端要添加一個(gè)新的類,使用objc_allocateClassPair與objc_registerClassPair注冊(cè)一個(gè)新的類。
- 若該Class對(duì)象不為nil,則說明JS端要替換一個(gè)原本已存在的類
根據(jù)從JS端傳遞來的實(shí)例方法與類方法參數(shù),為這個(gè)類對(duì)象添加/替換實(shí)例方法與類方法
- 添加實(shí)例方法時(shí),直接使用上一步得到class對(duì)象; 添加類方法時(shí)需要調(diào)用objc_getMetaClass方法獲得元類。
- 如果要替換的類已經(jīng)定義了該方法,則直接對(duì)該方法替換和實(shí)現(xiàn)消息轉(zhuǎn)發(fā)。
- 否則根據(jù)以下兩種情況進(jìn)行判斷
- 遍歷protocalNames,通過objc_getProtocol方法獲得協(xié)議對(duì)象,再使用protocol_copyMethodDescriptionList來獲得協(xié)議中方法的type和name。匹配JS中傳入的selectorName,獲得typeDescription字符串,對(duì)該協(xié)議方法的實(shí)現(xiàn)消息轉(zhuǎn)發(fā)。
- 若不是上述兩種情況,則js端請(qǐng)求添加一個(gè)新的方法。構(gòu)造一個(gè)typeDescription為”@@:\@*”(返回類型為id,參數(shù)值根據(jù)JS定義的參數(shù)個(gè)數(shù)來決定。新增方法的返回類型和參數(shù)類型只能為id類型,因?yàn)樵贘S端只能定義對(duì)象)的IMP。將這個(gè)IMP添加到類中。
為該類添加setProp:forKey和getProp:方法,使用objc_getAssociatedObject與objc_setAssociatedObject讓JS腳本擁有設(shè)置property的能力
返回{className:cls}回JS腳本。
不過其中還包括一個(gè)overrideMethod方法,不管是替換方法還是新增方法,都是使用overrideMethod方法。它的目的主要在于進(jìn)行method swizzing操作,也就是方法的重定向。我們把所有的消息全部都轉(zhuǎn)發(fā)到ForwardInvocation函數(shù)里去執(zhí)行(不知道的同學(xué)請(qǐng)自行補(bǔ)消息轉(zhuǎn)發(fā)機(jī)制),這樣做的目的在于,我們可以在NSInvocation中獲取到所有的參數(shù),這樣就可以實(shí)現(xiàn)一個(gè)通用的IMP,任意方法任意參數(shù)都可以通過這個(gè)IMP中轉(zhuǎn),拿到方法的所有參數(shù)回調(diào)JS的實(shí)現(xiàn)。于是overrideMethod其實(shí)就是做了如下這件事情:
具體實(shí)現(xiàn),以替換 UIViewController 的 -viewWillAppear: 方法為例:
把UIViewController的-viewWillAppear:方法通過class_replaceMethod()接口指向_objc_msgForward這是一個(gè)全局 IMP,OC 調(diào)用方法不存在時(shí)都會(huì)轉(zhuǎn)發(fā)到這個(gè)IMP上,這里直接把方法替換成這個(gè)IMP,這樣調(diào)用這個(gè)方法時(shí)就會(huì)走到-forwardInvocation:。
為UIViewController添加-ORIGviewWillAppear:和-_JPviewWillAppear: 兩個(gè)方法,前者指向原來的IMP實(shí)現(xiàn),后者是新的實(shí)現(xiàn),稍后會(huì)在這個(gè)實(shí)現(xiàn)里回調(diào)JS函數(shù)。
改寫UIViewController的-forwardInvocation: 方法為自定義實(shí)現(xiàn)。一旦OC里調(diào)用 UIViewController 的-viewWillAppear:方法,經(jīng)過上面的處理會(huì)把這個(gè)調(diào)用轉(zhuǎn)發(fā)到-forwardInvocation:,這時(shí)已經(jīng)組裝好了一個(gè)NSInvocation,包含了這個(gè)調(diào)用的參數(shù)。在這里把參數(shù)從 NSInvocation反解出來,帶著參數(shù)調(diào)用上述新增加的方法 -JPviewWillAppear:,在這個(gè)新方法里取到參數(shù)傳給JS,調(diào)用JS的實(shí)現(xiàn)函數(shù)。整個(gè)調(diào)用過程就結(jié)束了,整個(gè)過程圖示如下:
1.png
關(guān)于_setupJSMethod
if (properties) {properties.forEach(function(o){_ocCls[className]['props'][o] = 1_ocCls[className]['props']['set' + o.substr(0,1).toUpperCase() + o.substr(1)] = 1})}var _setupJSMethod = function(className, methods, isInst, realClsName) {for (var name in methods) {var key = isInst ? 'instMethods': 'clsMethods',func = methods[name]_ocCls[className][key][name] = _wrapLocalMethod(name, func, realClsName)}}是最后的一步是把之前所有的方法以及屬性放入 _ocCls中保存起來,最后再調(diào)用require把類保存到全局變量中。
到這一步為止,我們的JS腳本中的所有對(duì)象已經(jīng),通過runtime替換到我們的程序中去了,也就是說,剩下的就是如何在我們出觸發(fā)函數(shù)以后,能正確的去執(zhí)行JS中函數(shù)的內(nèi)容。
3. 對(duì)象持有/轉(zhuǎn)換
下面引用作者的一段話:
require('UIView')這句話在JS全局作用域生成了UIView這個(gè)對(duì)象,它有個(gè)屬性叫 __isCls,表示這代表一個(gè)OC類。調(diào)用UIView這個(gè)對(duì)象的alloc()方法,會(huì)去到_c()函數(shù),在這個(gè)函數(shù)里判斷到調(diào)用者_(dá)isCls 屬性,知道它是代表OC類,把方法名和類名傳遞給OC完成調(diào)用。調(diào)用類方法過程是這樣,那實(shí)例方法呢?UIView.alloc()會(huì)返回一個(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ì)象的指針,這個(gè)指針也可以重新傳回OC,要在JS調(diào)用這個(gè)對(duì)象的某個(gè)實(shí)例方法,根據(jù)第2點(diǎn)JS接口的描述,只需在_c()函數(shù)里把這個(gè)對(duì)象指針以及它要調(diào)用的方法名傳回給OC就行了,現(xiàn)在問題只剩下:怎樣在_c()函數(shù)里判斷調(diào)用者是一個(gè)OC對(duì)象指針?目前沒找到方法判斷一個(gè)JS對(duì)象是否表示 OC 指針,這里的解決方法是在OC把對(duì)象返回給JS之前,先把它包裝成一個(gè)NSDictionary:
static NSDictionary *_wrapObj(id obj) {return @{@"__obj": obj}; }讓 OC 對(duì)象作為這個(gè) NSDictionary 的一個(gè)值,這樣在 JS 里這個(gè)對(duì)象就變成:
{__obj: [OC Object 對(duì)象指針]}這樣就可以通過判斷對(duì)象是否有_obj屬性得知這個(gè)對(duì)象是否表示 OC 對(duì)象指針,在_c函數(shù)里若判斷到調(diào)用者有_obj屬性,取出這個(gè)屬性,跟調(diào)用的實(shí)例方法一起傳回給OC,就完成了實(shí)例方法的調(diào)用。
但是:
JS無法調(diào)用 NSMutableArray / NSMutableDictionary / NSMutableString 的方法去修改這些對(duì)象的數(shù)據(jù),因?yàn)檫@三者都在從OC返回到JS時(shí) JavaScriptCore 把它們轉(zhuǎn)成了JS的Array/Object/String,在返回的時(shí)候就脫離了跟原對(duì)象的聯(lián)系,這個(gè)轉(zhuǎn)換在JavaScriptCore里是強(qiáng)制進(jìn)行的,無法選擇。
若想要在對(duì)象返回JS后,回到OC還能調(diào)用這個(gè)對(duì)象的方法,就要阻止JavaScriptCore的轉(zhuǎn)換,唯一的方法就是不直接返回這個(gè)對(duì)象,而是對(duì)這個(gè)對(duì)象進(jìn)行封裝,JPBoxing 就是做這個(gè)事情的。
把NSMutableArray/NSMutableDictionary/NSMutableString對(duì)象作為JPBoxing的成員保存在JPBoxing實(shí)例對(duì)象上返回給JS,JS拿到的是JPBoxing對(duì)象的指針,再傳回給OC時(shí)就可以通過對(duì)象成員取到原來的NSMutableArray/NSMutableDictionary/NSMutableString對(duì)象,類似于裝箱/拆箱操作,這樣就避免了這些對(duì)象被JavaScriptCore轉(zhuǎn)換。
實(shí)際上只有可變的NSMutableArray/NSMutableDictionary/NSMutableString這三個(gè)類有必要調(diào)用它的方法去修改對(duì)象里的數(shù)據(jù),不可變的NSArray/NSDictionary/NSString是沒必要這樣做的,直接轉(zhuǎn)為JS對(duì)應(yīng)的類型使用起來會(huì)更方便,但為了規(guī)則簡單,JSPatch讓NSArray/NSDictionary/NSString也同樣以封裝的方式返回,避免在調(diào)用OC方法返回對(duì)象時(shí)還需要關(guān)心它返回的是可變還是不可變對(duì)象。最后整個(gè)規(guī)則還是挺清晰:NSArray/NSDictionary/NSString 及其子類與其他 NSObject 對(duì)象的行為一樣,在JS上拿到的都只是其對(duì)象指針,可以調(diào)用它們的OC方法,若要把這三種對(duì)象轉(zhuǎn)為對(duì)應(yīng)的JS類型,使用額外的.toJS()的接口去轉(zhuǎn)換。
對(duì)于參數(shù)和返回值是C指針和 Class 類型的支持同樣是用 JPBoxing 封裝的方式,把指針和Class作為成員保存在JPBoxing對(duì)象上返回給JS,傳回OC時(shí)再解出來拿到原來的指針和Class,這樣JSPatch就支持所有數(shù)據(jù)類型OC<->JS的互傳了。
4. 類型轉(zhuǎn)換
還是引用作者的一段話:
JS把要調(diào)用的類名/方法名/對(duì)象傳給OC后,OC調(diào)用類/對(duì)象相應(yīng)的方法是通過NSInvocation實(shí)現(xiàn),要能順利調(diào)用到方法并取得返回值,要做兩件事:
例如舉例子的來講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)換處理。
5. callSelector
callSelector這個(gè)就是我們最后執(zhí)行函數(shù)了!但是在執(zhí)行這個(gè)函數(shù)之前,前面還有不少東西。
關(guān)于 _c函數(shù)
__c: function(methodName) {var slf = thisif (slf instanceof Boolean) {return function() {return false}}if (slf[methodName]) {return slf[methodName].bind(slf);}if (!slf.__obj && !slf.__clsName) {throw new Error(slf + '.' + methodName + ' is undefined')}if (slf.__isSuper && slf.__clsName) {slf.__clsName = _OC_superClsName(slf.__obj.__realClsName ? slf.__obj.__realClsName: slf.__clsName);}var clsName = slf.__clsNameif (clsName && _ocCls[clsName]) {var methodType = slf.__obj ? 'instMethods': 'clsMethods'if (_ocCls[clsName][methodType][methodName]) {slf.__isSuper = 0;return _ocCls[clsName][methodType][methodName].bind(slf)}if (slf.__obj && _ocCls[clsName]['props'][methodName]) {if (!slf.__ocProps) {var props = _OC_getCustomProps(slf.__obj)if (!props) {props = {}_OC_setCustomProps(slf.__obj, props)}slf.__ocProps = props;}var c = methodName.charCodeAt(3);if (methodName.length > 3 && methodName.substr(0,3) == 'set' && c >= 65 && c <= 90) {return function(val) {var propName = methodName[3].toLowerCase() + methodName.substr(4)slf.__ocProps[propName] = val}} else {return function(){ return slf.__ocProps[methodName]}}}}return function(){var args = Array.prototype.slice.call(arguments)return _methodFunc(slf.__obj, slf.__clsName, methodName, args, slf.__isSuper)}}其實(shí)_c函數(shù)就是一個(gè)消息轉(zhuǎn)發(fā)中心,它根據(jù)傳入的參數(shù),這里可以分為兩種類型來講述:
- 對(duì)于實(shí)例方法和類方法,最后會(huì)調(diào)用_methodFunc方法
- 對(duì)于自定義的屬性,set和get操作。
對(duì)于自定義的屬性,其實(shí)它并不會(huì)將這些屬性真正添加到OC中的對(duì)象里去,它只會(huì)添加一個(gè)_ocProps對(duì)象,然后在JS中,通過_ocProps對(duì)象來保存我們所有定義的屬性,要獲取值的只要從這個(gè)屬性里通過name獲取就可以了。
對(duì)于_methodFunc方法,其實(shí)就是將OC方法的名字還原,帶上參數(shù),然后轉(zhuǎn)發(fā)給類方法或者實(shí)例方法處理。
var _methodFunc = function(instance, clsName, methodName, args, isSuper, isPerformSelector) {var selectorName = methodNameif (!isPerformSelector) {methodName = methodName.replace(/__/g, "-")selectorName = methodName.replace(/_/g, ":").replace(/-/g, "_")var marchArr = selectorName.match(/:/g)var numOfArgs = marchArr ? marchArr.length : 0if (args.length > numOfArgs) {selectorName += ":"}}var ret = instance ? _OC_callI(instance, selectorName, args, isSuper):_OC_callC(clsName, selectorName, args)return _formatOCToJS(ret)}對(duì)于callSelector方法來講:
- 將JS封裝的instance對(duì)象進(jìn)行拆裝,得到OC的對(duì)象;
- 根據(jù)類名與selectorName獲得對(duì)應(yīng)的類對(duì)象與selector;
- 通過類對(duì)象與selector構(gòu)造對(duì)應(yīng)的NSMethodSignature簽名,再根據(jù)簽名構(gòu)造NSInvocation對(duì)象,并為invocation對(duì)象設(shè)置target與Selector
總結(jié)
好,到現(xiàn)在為止,我們所有的流程就已經(jīng)走完了,我們的js文件也已經(jīng)生效了。當(dāng)然,我所說JSPatch原理只是基礎(chǔ)的一部份原理,可以使我們的基本流程可以實(shí)現(xiàn),還有一些復(fù)雜的操作功能,還需要再深入的學(xué)習(xí),才可以掌握,JSPatch對(duì)于學(xué)習(xí)Runtime也是一個(gè)不錯(cuò)的例子,就像Aspects一樣,大家可以去好好研究一下。
文/北辰明(簡書作者)
原文鏈接:http://www.jianshu.com/p/66dad614b905 http://www.jianshu.com/p/66dad614b905?utm_campaign=hugo&utm_medium=reader_share&utm_content=note
總結(jié)
以上是生活随笔為你收集整理的iOS中的HotFix方案总结详解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 学习笔记900天总结
- 下一篇: 【SSH网上商城项目实战05】完成数据库