原創(chuàng)文章轉(zhuǎn)載請(qǐng)注明出處,謝謝
相信HotFix大家應(yīng)該都很熟悉了,今天主要對(duì)于最近調(diào)研的一些方案做一些總結(jié)。iOS中的HotFix方案大致可以分為四種:
- WaxPatch(Alibaba)
- Dynamic Framework(Apple)
- React Native(Facebook)
- JSPatch(Tencent)
WaxPatch
WaxPatch是一個(gè)通過(guò)Lua語(yǔ)言編寫的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í)行代碼,但通過(guò)蘋果JavaScriptCore.framework或WebKit執(zhí)行的代碼除外;同時(shí)Wax已經(jīng)長(zhǎng)期沒(méi)有人維護(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)庫(kù);首先我介紹一下關(guān)于動(dòng)態(tài)庫(kù)和靜態(tài)庫(kù)的一些特性以及區(qū)別。
不管是靜態(tài)庫(kù)還是動(dòng)態(tài)庫(kù),本質(zhì)上都是一種可執(zhí)行的二進(jìn)制格式,可以被載入內(nèi)存中執(zhí)行。
iOS上的靜態(tài)庫(kù)可以分為.a文件和.framework,動(dòng)態(tài)庫(kù)可以分為.dylib(xcode7以后變成了.tdb)和.framework。
靜態(tài)庫(kù): 鏈接時(shí)完整地拷貝至可執(zhí)行文件中,被多次使用就有多份冗余拷貝。
動(dòng)態(tài)庫(kù): 鏈接時(shí)不復(fù)制,程序運(yùn)行時(shí)由系統(tǒng)動(dòng)態(tài)加載到內(nèi)存,供程序調(diào)用,系統(tǒng)只加載一次,多個(gè)程序共用,節(jié)省內(nèi)存。
靜態(tài)庫(kù)和動(dòng)態(tài)庫(kù)是相對(duì)編譯期和運(yùn)行期的:靜態(tài)庫(kù)在程序編譯時(shí)會(huì)被鏈接到目標(biāo)代碼中,程序運(yùn)行時(shí)將不再需要改靜態(tài)庫(kù);而動(dòng)態(tài)庫(kù)在程序編譯時(shí)并不會(huì)被鏈接到目標(biāo)代碼中,只是在程序運(yùn)行時(shí)才被載入,因?yàn)樵诔绦蜻\(yùn)行期間還需要?jiǎng)討B(tài)庫(kù)的存在。
總結(jié):同一個(gè)靜態(tài)庫(kù)在不同程序中使用時(shí),每一個(gè)程序中都得導(dǎo)入一次,打包時(shí)也被打包進(jìn)去,形成一個(gè)程序。而動(dòng)態(tài)庫(kù)在不同程序中,打包時(shí)并沒(méi)有被打包進(jìn)去,只在程序運(yùn)行使用時(shí),才鏈接載入(如系統(tǒng)的框架如UIKit、Foundation等),所以程序體積會(huì)小很多。
好,所以Dynamic Framework其實(shí)就是我們可以通過(guò)更新App所依賴的Framework方式,來(lái)實(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)庫(kù),我們可以通過(guò)file這個(gè)命令來(lái)查看我們的framework到底是屬于static還是dynamic。
React Native
React Native支持用JavaScript進(jìn)行開發(fā),所以可以通過(guò)更改JS文件實(shí)現(xiàn)App的HotFix,但是這種方案的明顯的缺點(diǎn)在于它只適合用于使用了React Native這種方案的應(yīng)用。
JSPatch
JSPatch是只需要在項(xiàng)目中引入極小的JSPatch引擎,就可以使用JavaScript語(yǔ)言調(diào)用Objective-C的原生接口,獲得腳本語(yǔ)言的能力:動(dòng)態(tài)更新iOS APP,替換項(xiàng)目原生代碼、快速修復(fù)bug。但是JSPatch也有它的自己的缺點(diǎn),主要在由于它要依賴javascriptcore,framework,而這個(gè)framework是在iOS7.0以后才引入進(jìn)來(lái),所以JSPatch是不支持iOS6.0的,同時(shí)由于使用的是JS的腳本技術(shù),所以在內(nèi)存以及性能上面是要低于Wax的。
所以最后當(dāng)然還是采用了JSPatch這種方案,但是實(shí)際過(guò)程中還是出現(xiàn)了一些問(wèn)題的,所以掌握J(rèn)SPatch的核心原理對(duì)于我們解決問(wèn)題是非常有幫助的。
關(guān)于JSPatch的核心原理講解
預(yù)加載部分
關(guān)于核心原理的講解,網(wǎng)上有不少,但是幾乎都是差不多,很多都還是引用了作者bang自己寫的文檔的內(nèi)容,所以我采用一個(gè)例子方式進(jìn)行講解JSPatch的主要運(yùn)行流程,其實(shí)當(dāng)然也會(huì)引用一些作者的簡(jiǎn)述,大家可以參照我寫的流程講述,在配合源碼或者官方文檔的介紹,應(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)過(guò)程分為一下兩部分:
- 通過(guò)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í)行我們上面說(shuō)的[JPEngine evaluateScript:script]了,程序開始執(zhí)行我們的腳本,但是在這之前,JSpatch會(huì)對(duì)我們的腳本做一些處理,這一步同樣也包括兩個(gè)方面:
- 需要給我們的程序加上try catch的部分代碼,主要目的是當(dāng)我們的JS腳本有錯(cuò)誤的時(shí)候,可以catch到錯(cuò)誤信息
- 將所有的函數(shù)都改成通過(guò)__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來(lái)替換我們的函數(shù)呢,因?yàn)镴S語(yǔ)法的限制對(duì)于沒(méi)有定義的函數(shù)JS是無(wú)法調(diào)用的,也就是調(diào)用UIAlertView.alloc()其實(shí)是非法的,因?yàn)樗捎玫牟⒉皇窍⑥D(zhuǎn)發(fā)的形式,所以作者原來(lái)是想把一個(gè)類的所有函數(shù)都定義在JS上,也就是如下形式:
{
__clsName: "UIAlertView",
alloc: function() {…},
beginAnimations_context: function() {…},
setAnimationsEnabled: function(){…},
...
}
但是這種形式就必須要遍歷當(dāng)前類的所有方法,還要循環(huán)找父類的方法直到頂層,這種方法直接導(dǎo)致的問(wèn)題就是內(nèi)存暴漲,所以是不可行的,所以最后作者采用了消息轉(zhuǎn)發(fā)的思想,定義了一個(gè)_c的原函數(shù),所有的函數(shù)都通過(guò)_c來(lái)轉(zhuǎn)發(fā),這樣就解決了我們的問(wèn)題。
值得一提的是我們的__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做的事很簡(jiǎn)單,就是在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è)诮酉聛?lái)調(diào)用UIAlertView.__c()方法的時(shí)候系統(tǒng)就不會(huì)報(bào)錯(cuò)了,因?yàn)樗呀?jīng)是JS中一個(gè)全局的Object對(duì)象了。
{
__clsName: "UIAlertView"
}
2.defineClass
接下來(lái)我們就要執(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ù)的的格式,以及在原來(lái)實(shí)現(xiàn)上追加了從OC回調(diào)回來(lái)的參數(shù)解析。
- 然后執(zhí)行_OC_defineClass方法,也就是調(diào)用OC的方法,解析傳入類的屬性,實(shí)例方法,類方法,里面會(huì)調(diào)用overrideMethod方法,進(jìn)行method swizzing操作,也就是方法的重定向。
- 最后執(zhí)行_setupJSMethod方法,在js中通過(guò)_ocCls記錄類實(shí)例方法,類方法。
關(guān)于_formatDefineMethods
_formatDefineMethods方法接收的參數(shù)是一個(gè)方法列表js對(duì)象,加一個(gè)新的js空對(duì)象
var _formatDefineMethods = function(methods, newMethods, realClsName) {
for (var methodName in methods) {
if (!(methods[methodName] instanceof Function)) return;
(function(){
var originMethod = methods[methodName]
newMethods[methodName] = [originMethod.length, function() {
try {
// 通過(guò)OC回調(diào)回來(lái)執(zhí)行,獲取參數(shù)
var args = _formatOCToJS(Array.prototype.slice.call(arguments))
var lastSelf = global.self
global.self = args[0]
if (global.self) global.self.__realClsName = realClsName
// 刪除前兩個(gè)參數(shù):在OC中進(jìn)行消息轉(zhuǎn)發(fā)的時(shí)候,前兩個(gè)參數(shù)是self和selector,
// 我們?cè)趯?shí)際調(diào)用js的具體實(shí)現(xiàn)的時(shí)候,需要把這兩個(gè)參數(shù)刪除。
args.splice(0,1)
var ret = originMethod.apply(originMethod, args)
global.self = lastSelf
return ret
} catch(e) {
_OC_catch(e.message, e.stack)
}
}]
})()
}
}
可以發(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作用,簡(jiǎn)單的說(shuō),它把defineClass中傳遞過(guò)來(lái)的js對(duì)象進(jìn)行了修改:
原來(lái)的形式是:
{
testFuncationOne:function(){...}
}
修改之后是:
{
testFuncationOne: [argCount, function (){...新的實(shí)現(xiàn)}]
}
傳遞參數(shù)個(gè)數(shù)的目的是,runtime在修復(fù)類的時(shí)候,無(wú)法直接解析原始的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,則說(shuō)明JS端要添加一個(gè)新的類,使用objc_allocateClassPair與objc_registerClassPair注冊(cè)一個(gè)新的類。
- 若該Class對(duì)象不為nil,則說(shuō)明JS端要替換一個(gè)原本已存在的類
-
根據(jù)從JS端傳遞來(lái)的實(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,通過(guò)objc_getProtocol方法獲得協(xié)議對(duì)象,再使用protocol_copyMethodDescriptionList來(lái)獲得協(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ù)來(lái)決定。新增方法的返回類型和參數(shù)類型只能為id類型,因?yàn)樵贘S端只能定義對(duì)象)的IMP。將這個(gè)IMP添加到類中。
為該類添加setProp:forKey和getProp:方法,使用objc_getAssociatedObject與objc_setAssociatedObject讓JS腳本擁有設(shè)置property的能力
返回{className:cls}回JS腳本。
不過(guò)其中還包括一個(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ù)都可以通過(guò)這個(gè)IMP中轉(zhuǎn),拿到方法的所有參數(shù)回調(diào)JS的實(shí)現(xiàn)。于是overrideMethod其實(shí)就是做了如下這件事情:
具體實(shí)現(xiàn),以替換 UIViewController 的 -viewWillAppear: 方法為例:
把UIViewController的-viewWillAppear:方法通過(guò)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è)方法,前者指向原來(lái)的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)過(guò)上面的處理會(huì)把這個(gè)調(diào)用轉(zhuǎn)發(fā)到-forwardInvocation:,這時(shí)已經(jīng)組裝好了一個(gè)NSInvocation,包含了這個(gè)調(diào)用的參數(shù)。在這里把參數(shù)從 NSInvocation反解出來(lái),帶著參數(shù)調(diào)用上述新增加的方法 -JPviewWillAppear:,在這個(gè)新方法里取到參數(shù)傳給JS,調(diào)用JS的實(shí)現(xiàn)函數(shù)。整個(gè)調(diào)用過(guò)程就結(jié)束了,整個(gè)過(guò)程圖示如下:
關(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中保存起來(lái),最后再調(diào)用require把類保存到全局變量中。
到這一步為止,我們的JS腳本中的所有對(duì)象已經(jīng),通過(guò)runtime替換到我們的程序中去了,也就是說(shuō),剩下的就是如何在我們出觸發(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)用類方法過(guò)程是這樣,那實(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無(wú)法使用,但在回傳給OC時(shí),OC可以找到這個(gè)對(duì)象。對(duì)于這個(gè)對(duì)象生命周期的管理,按我的理解如果JS有變量引用時(shí),這個(gè)OC對(duì)象引用計(jì)數(shù)就加1,JS變量的引用釋放了就減1,如果OC上沒(méi)別的持有者,這個(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)在問(wèn)題只剩下:怎樣在_c()函數(shù)里判斷調(diào)用者是一個(gè)OC對(duì)象指針? 目前沒(méi)找到方法判斷一個(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ì)象指針]}
這樣就可以通過(guò)判斷對(duì)象是否有_obj屬性得知這個(gè)對(duì)象是否表示 OC 對(duì)象指針,在_c函數(shù)里若判斷到調(diào)用者有_obj屬性,取出這個(gè)屬性,跟調(diào)用的實(shí)例方法一起傳回給OC,就完成了實(shí)例方法的調(diào)用。
但是:
JS無(wú)法調(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)行的,無(wú)法選擇。
若想要在對(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í)就可以通過(guò)對(duì)象成員取到原來(lái)的NSMutableArray/NSMutableDictionary/NSMutableString對(duì)象,類似于裝箱/拆箱操作,這樣就避免了這些對(duì)象被JavaScriptCore轉(zhuǎn)換
。
實(shí)際上只有可變的NSMutableArray/NSMutableDictionary/NSMutableString這三個(gè)類有必要調(diào)用它的方法去修改對(duì)象里的數(shù)據(jù),不可變的NSArray/NSDictionary/NSString是沒(méi)必要這樣做的,直接轉(zhuǎn)為JS對(duì)應(yīng)的類型使用起來(lái)會(huì)更方便,但為了規(guī)則簡(jiǎn)單,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í)再解出來(lái)拿到原來(lái)的指針和Class,這樣JSPatch就支持所有數(shù)據(jù)類型OC<->JS的互傳了。
4. 類型轉(zhuǎn)換
還是引用作者的一段話:
JS把要調(diào)用的類名/方法名/對(duì)象傳給OC后,OC調(diào)用類/對(duì)象相應(yīng)的方法是通過(guò)NSInvocation實(shí)現(xiàn),要能順利調(diào)用到方法并取得返回值,要做兩件事:
- 取得要調(diào)用的 OC 方法各參數(shù)類型,把 JS 傳來(lái)的對(duì)象轉(zhuǎn)為要求的類型進(jìn)行調(diào)用。
- 根據(jù)返回值類型取出返回值,包裝為對(duì)象傳回給 JS。
例如舉例子的來(lái)講view.setAlpha(0.5),JS傳遞給OC的是一個(gè)NSNumber,OC需要通過(guò)要調(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 = this
if (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.__clsName
if (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ù),這里可以分為兩種類型來(lái)講述:
- 對(duì)于實(shí)例方法和類方法,最后會(huì)調(diào)用_methodFunc方法
- 對(duì)于自定義的屬性,set和get操作。
對(duì)于自定義的屬性,其實(shí)它并不會(huì)將這些屬性真正添加到OC中的對(duì)象里去,它只會(huì)添加一個(gè)_ocProps對(duì)象,然后在JS中,通過(guò)_ocProps對(duì)象來(lái)保存我們所有定義的屬性,要獲取值的只要從這個(gè)屬性里通過(guò)name獲取就可以了。
對(duì)于_methodFunc方法,其實(shí)就是將OC方法的名字還原,帶上參數(shù),然后轉(zhuǎn)發(fā)給類方法或者實(shí)例方法處理。
var _methodFunc = function(instance, clsName, methodName, args, isSuper, isPerformSelector) {
var selectorName = methodName
if (!isPerformSelector) {
methodName = methodName.replace(/__/g, "-")
selectorName = methodName.replace(/_/g, ":").replace(/-/g, "_")
var marchArr = selectorName.match(/:/g)
var numOfArgs = marchArr ? marchArr.length : 0
if (args.length > numOfArgs) {
selectorName += ":"
}
}
var ret = instance ? _OC_callI(instance, selectorName, args, isSuper):
_OC_callC(clsName, selectorName, args)
return _formatOCToJS(ret)
}
對(duì)于callSelector方法來(lái)講:
- 初始化
- 將JS封裝的instance對(duì)象進(jìn)行拆裝,得到OC的對(duì)象;
- 根據(jù)類名與selectorName獲得對(duì)應(yīng)的類對(duì)象與selector;
- 通過(guò)類對(duì)象與selector構(gòu)造對(duì)應(yīng)的NSMethodSignature簽名,再根據(jù)簽名構(gòu)造NSInvocation對(duì)象,并為invocation對(duì)象設(shè)置target與Selector
- 根據(jù)方法簽名,獲悉方法每個(gè)參數(shù)的實(shí)際類型,將JS傳遞過(guò)來(lái)的參數(shù)進(jìn)行對(duì)應(yīng)的轉(zhuǎn)換(比如說(shuō)參數(shù)的實(shí)際類型為int類型,但是JS只能傳遞NSNumber對(duì)象,需要通過(guò)[[jsObj toNumber] intValue]進(jìn)行轉(zhuǎn)換)。轉(zhuǎn)換后使用setArgument方法為NSInvocation對(duì)象設(shè)置參數(shù)。
- 執(zhí)行invoke方法。
- 通過(guò)getReturnValue方法獲取到返回值。
- 根據(jù)返回值類型,封裝成JS中對(duì)應(yīng)的對(duì)象(因?yàn)镴S并不識(shí)別OC對(duì)象,所以返回值為OC對(duì)象的話需封裝成{className:className, obj:obj})返回給JS端。
總結(jié)
好,到現(xiàn)在為止,我們所有的流程就已經(jīng)走完了,我們的js文件也已經(jīng)生效了。當(dāng)然,我所說(shuō)JSPatch原理只是基礎(chǔ)的一部份原理,可以使我們的基本流程可以實(shí)現(xiàn),還有一些復(fù)雜的操作功能,還需要再深入的學(xué)習(xí),才可以掌握,JSPatch對(duì)于學(xué)習(xí)Runtime也是一個(gè)不錯(cuò)的例子,就像Aspects一樣,大家可以去好好研究一下。