iOS中的HotFix方案总结详解
iOS中的HotFix方案總結詳解
相信HotFix大家應該都很熟悉了,今天主要對于最近調研的一些方案做一些總結。iOS中的HotFix方案大致可以分為四種:
- WaxPatch(Alibaba)
- Dynamic Framework(Apple)
- React Native(Facebook)
- JSPatch(Tencent)
WaxPatch
WaxPatch是一個通過Lua語言編寫的iOS框架,不僅允許用戶使用 Lua 調用 iOS SDK和應用程序內部的 API, 而且使用了 OC runtime 特性調用替換應用程序內部由 OC 編寫的類方法,從而達到HotFix的目的。
WaxPatch的優點在于它支持iOS6.0,同時性能上比較的優秀,但是缺點也是非常的明顯,不符合Apple3.2.2的審核規則即不可動態下發可執行代碼,但通過蘋果JavaScriptCore.framework或WebKit執行的代碼除外;同時Wax已經長期沒有人維護了,導致很多OC方法不能用Lua實現,比如Wax不支持block;最后就是必須要內嵌一個Lua腳本的執行引擎才能運行Lua腳本;Wax并不支持arm64框架。
Dynamic Framework
動態的Framework,其實就是動態庫;首先我介紹一下關于動態庫和靜態庫的一些特性以及區別。
不管是靜態庫還是動態庫,本質上都是一種可執行的二進制格式,可以被載入內存中執行。
iOS上的靜態庫可以分為.a文件和.framework,動態庫可以分為.dylib(xcode7以后變成了.tdb)和.framework。
靜態庫: 鏈接時完整地拷貝至可執行文件中,被多次使用就有多份冗余拷貝。
動態庫: 鏈接時不復制,程序運行時由系統動態加載到內存,供程序調用,系統只加載一次,多個程序共用,節省內存。
靜態庫和動態庫是相對編譯期和運行期的:靜態庫在程序編譯時會被鏈接到目標代碼中,程序運行時將不再需要改靜態庫;而動態庫在程序編譯時并不會被鏈接到目標代碼中,只是在程序運行時才被載入,因為在程序運行期間還需要動態庫的存在。
總結:同一個靜態庫在不同程序中使用時,每一個程序中都得導入一次,打包時也被打包進去,形成一個程序。而動態庫在不同程序中,打包時并沒有被打包進去,只在程序運行使用時,才鏈接載入(如系統的框架如UIKit、Foundation等),所以程序體積會小很多。
好,所以Dynamic Framework其實就是我們可以通過更新App所依賴的Framework方式,來實現對于Bug的HotFix,但是這個方案的缺點也是顯而易見的它不符合Apple3.2.2的審核規則,使用了這種方式是上不了Apple Store的,它只能適用于一些越獄市場或者公司內部的一些項目使用,同時這種方案其實并不適用于BugFix,更適合App線上的大更新。所以其實我們項目中的引入的那些第三方的Framework都是靜態庫,我們可以通過file這個命令來查看我們的framework到底是屬于static還是dynamic。
React Native
React Native支持用JavaScript進行開發,所以可以通過更改JS文件實現App的HotFix,但是這種方案的明顯的缺點在于它只適合用于使用了React Native這種方案的應用。
JSPatch
JSPatch是只需要在項目中引入極小的JSPatch引擎,就可以使用JavaScript語言調用Objective-C的原生接口,獲得腳本語言的能力:動態更新iOS APP,替換項目原生代碼、快速修復bug。但是JSPatch也有它的自己的缺點,主要在由于它要依賴javascriptcore,framework,而這個framework是在iOS7.0以后才引入進來,所以JSPatch是不支持iOS6.0的,同時由于使用的是JS的腳本技術,所以在內存以及性能上面是要低于Wax的。
所以最后當然還是采用了JSPatch這種方案,但是實際過程中還是出現了一些問題的,所以掌握JSPatch的核心原理對于我們解決問題是非常有幫助的。
關于JSPatch的核心原理講解
預加載部分
關于核心原理的講解,網上有不少,但是幾乎都是差不多,很多都還是引用了作者bang自己寫的文檔的內容,所以我采用一個例子方式進行講解JSPatch的主要運行流程,其實當然也會引用一些作者的簡述,大家可以參照我寫的流程講述,在配合源碼或者官方文檔的介紹,應該就可以了解JSPatch。
[JPEngine startEngine];NSString *sourcePath = [[NSBundle mainBundle] pathForResource:@"demo" ofType:@"js"];NSString *script = [NSString stringWithContentsOfFile:sourcePath encoding:NSUTF8StringEncoding error:nil];[JPEngine evaluateScript:script];首先是運行[JPEngine startEngine]啟動JSPatch,啟動過程分為一下兩部分:
-
通過JSContext,聲明了一些JS方法到內存,這樣我們之后就可以在JS中調用這些方法,主要常用到的包括以下幾個方法,同時會監聽一個內存告警的通知。
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文件的主要內容在于定義一些我們之后會用在的JS函數,數據結構以及變量等信息,之后我會在用到的時候詳細介紹。
腳本運行
我們定義如下的腳本:
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()}});然后就是執行我們上面說的[JPEngine evaluateScript:script]了,程序開始執行我們的腳本,但是在這之前,JSpatch會對我們的腳本做一些處理,這一步同樣也包括兩個方面:
- 需要給我們的程序加上try catch的部分代碼,主要目的是當我們的JS腳本有錯誤的時候,可以catch到錯誤信息
- 將所有的函數都改成通過__c原函數的形式進行調用。
也就是最后我們調用的腳本已經變成如下的形式了:
;(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")()}});那么為什么需要用函數__c來替換我們的函數呢,因為JS語法的限制對于沒有定義的函數JS是無法調用的,也就是調用UIAlertView.alloc()其實是非法的,因為它采用的并不是消息轉發的形式,所以作者原來是想把一個類的所有函數都定義在JS上,也就是如下形式:
{__clsName: "UIAlertView",alloc: function() {…},beginAnimations_context: function() {…},setAnimationsEnabled: function(){…},... }但是這種形式就必須要遍歷當前類的所有方法,還要循環找父類的方法直到頂層,這種方法直接導致的問題就是內存暴漲,所以是不可行的,所以最后作者采用了消息轉發的思想,定義了一個_c的原函數,所有的函數都通過_c來轉發,這樣就解決了我們的問題。
值得一提的是我們的__c函數就是在我們執行JSPatch.js的時候聲明到js里的Object方法里去的,就是下面這個函數,_customMethods里面聲明了很多需要追加在Object上的函數。
for (var method in _customMethods) {if (_customMethods.hasOwnProperty(method)) {Object.defineProperty(Object.prototype, method, {value: _customMethods[method], configurable:false, enumerable: false})}}1. require
調用 require('UIAlertView') 后,就可以直接使用UIAlertView這個變量去調用相應的類方法了,require做的事很簡單,就是在JS全局作用域上創建一個同名變量,變量指向一個對象,對象屬性 __clsName 保存類名,同時表明這個對象是一個 OC Class。
var _require = function(clsName) {if (!global[clsName]) {global[clsName] = {__clsName: clsName}}return global[clsName] }這樣我們在接下來調用UIAlertView.__c()方法的時候系統就不會報錯了,因為它已經是JS中一個全局的Object對象了。
{__clsName: "UIAlertView" }2.defineClass
接下來我們就要執行defineClass函數了
global.defineClass = function(declaration, properties, instMethods, clsMethods)defineClass函數可接受四個參數:
字符串:”需要替換或者新增的類名:繼承的父類名 <實現的協議1,實現的協議2>”
[屬性]
{實例方法}
{類方法}
當我調用這個函數以后主要是做三件事情:
- 執行_formatDefineMethods方法,主要目的是修改傳入的function函數的的格式,以及在原來實現上追加了從OC回調回來的參數解析。
- 然后執行_OC_defineClass方法,也就是調用OC的方法,解析傳入類的屬性,實例方法,類方法,里面會調用overrideMethod方法,進行method swizzing操作,也就是方法的重定向。
- 最后執行_setupJSMethod方法,在js中通過_ocCls記錄類實例方法,類方法。
關于_formatDefineMethods
_formatDefineMethods方法接收的參數是一個方法列表js對象,加一個新的js空對象
可以發現,具體實現是遍歷方法列表對象的屬性(方法名),然后往js空對象中添加相同的屬性,它的值對應的是一個數組,數組的第一個值是方法名對應實現函數的參數個數,第二個值是一個函數(也就是方法的具體實現)。_formatDefineMethods作用,簡單的說,它把defineClass中傳遞過來的js對象進行了修改:
原來的形式是:{testFuncationOne:function(){...}} 修改之后是:{testFuncationOne: [argCount, function (){...新的實現}]}傳遞參數個數的目的是,runtime在修復類的時候,無法直接解析原始的js實現函數,那么就不知道參數的個數,特別是在創建新的方法的時候,需要根據參數個數生成方法簽名,也就是還原方法名字,所以只能在js端拿到js函數的參數個數,傳遞到OC端。
// js 方法 initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles// oc 方法 initWithTitle:message:delegate:cancelButtonTitle:otherButtonTitles:關于_OC_defineClass
使用NSScanner分離classDeclaration,分離成三部分
- 類名 : className
- 父類名 : superClassName
- 實現的協議名 : protocalNames
使用NSClassFromString(className)獲得該Class對象。
- 若該Class對象為nil,則說明JS端要添加一個新的類,使用objc_allocateClassPair與objc_registerClassPair注冊一個新的類。
- 若該Class對象不為nil,則說明JS端要替換一個原本已存在的類
根據從JS端傳遞來的實例方法與類方法參數,為這個類對象添加/替換實例方法與類方法
- 添加實例方法時,直接使用上一步得到class對象; 添加類方法時需要調用objc_getMetaClass方法獲得元類。
- 如果要替換的類已經定義了該方法,則直接對該方法替換和實現消息轉發。
- 否則根據以下兩種情況進行判斷
- 遍歷protocalNames,通過objc_getProtocol方法獲得協議對象,再使用protocol_copyMethodDescriptionList來獲得協議中方法的type和name。匹配JS中傳入的selectorName,獲得typeDescription字符串,對該協議方法的實現消息轉發。
- 若不是上述兩種情況,則js端請求添加一個新的方法。構造一個typeDescription為”@@:\@*”(返回類型為id,參數值根據JS定義的參數個數來決定。新增方法的返回類型和參數類型只能為id類型,因為在JS端只能定義對象)的IMP。將這個IMP添加到類中。
為該類添加setProp:forKey和getProp:方法,使用objc_getAssociatedObject與objc_setAssociatedObject讓JS腳本擁有設置property的能力
返回{className:cls}回JS腳本。
不過其中還包括一個overrideMethod方法,不管是替換方法還是新增方法,都是使用overrideMethod方法。它的目的主要在于進行method swizzing操作,也就是方法的重定向。我們把所有的消息全部都轉發到ForwardInvocation函數里去執行(不知道的同學請自行補消息轉發機制),這樣做的目的在于,我們可以在NSInvocation中獲取到所有的參數,這樣就可以實現一個通用的IMP,任意方法任意參數都可以通過這個IMP中轉,拿到方法的所有參數回調JS的實現。于是overrideMethod其實就是做了如下這件事情:
具體實現,以替換 UIViewController 的 -viewWillAppear: 方法為例:
把UIViewController的-viewWillAppear:方法通過class_replaceMethod()接口指向_objc_msgForward這是一個全局 IMP,OC 調用方法不存在時都會轉發到這個IMP上,這里直接把方法替換成這個IMP,這樣調用這個方法時就會走到-forwardInvocation:。
為UIViewController添加-ORIGviewWillAppear:和-_JPviewWillAppear: 兩個方法,前者指向原來的IMP實現,后者是新的實現,稍后會在這個實現里回調JS函數。
改寫UIViewController的-forwardInvocation: 方法為自定義實現。一旦OC里調用 UIViewController 的-viewWillAppear:方法,經過上面的處理會把這個調用轉發到-forwardInvocation:,這時已經組裝好了一個NSInvocation,包含了這個調用的參數。在這里把參數從 NSInvocation反解出來,帶著參數調用上述新增加的方法 -JPviewWillAppear:,在這個新方法里取到參數傳給JS,調用JS的實現函數。整個調用過程就結束了,整個過程圖示如下:
1.png
關于_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中保存起來,最后再調用require把類保存到全局變量中。
到這一步為止,我們的JS腳本中的所有對象已經,通過runtime替換到我們的程序中去了,也就是說,剩下的就是如何在我們出觸發函數以后,能正確的去執行JS中函數的內容。
3. 對象持有/轉換
下面引用作者的一段話:
require('UIView')這句話在JS全局作用域生成了UIView這個對象,它有個屬性叫 __isCls,表示這代表一個OC類。調用UIView這個對象的alloc()方法,會去到_c()函數,在這個函數里判斷到調用者_isCls 屬性,知道它是代表OC類,把方法名和類名傳遞給OC完成調用。調用類方法過程是這樣,那實例方法呢?UIView.alloc()會返回一個UIView實例對象給JS,這個OC實例對象在JS是怎樣表示的?怎樣可以在 JS 拿到這個實例對象后可以直接調用它的實例方法UIView.alloc().init()?
對于一個自定義id對象,JavaScriptCore 會把這個自定義對象的指針傳給JS,這個對象在JS無法使用,但在回傳給OC時,OC可以找到這個對象。對于這個對象生命周期的管理,按我的理解如果JS有變量引用時,這個OC對象引用計數就加1,JS變量的引用釋放了就減1,如果OC上沒別的持有者,這個OC對象的生命周期就跟著 JS走了,會在JS進行垃圾回收時釋放。傳回給JS的變量是這個OC對象的指針,這個指針也可以重新傳回OC,要在JS調用這個對象的某個實例方法,根據第2點JS接口的描述,只需在_c()函數里把這個對象指針以及它要調用的方法名傳回給OC就行了,現在問題只剩下:怎樣在_c()函數里判斷調用者是一個OC對象指針?目前沒找到方法判斷一個JS對象是否表示 OC 指針,這里的解決方法是在OC把對象返回給JS之前,先把它包裝成一個NSDictionary:
static NSDictionary *_wrapObj(id obj) {return @{@"__obj": obj}; }讓 OC 對象作為這個 NSDictionary 的一個值,這樣在 JS 里這個對象就變成:
{__obj: [OC Object 對象指針]}這樣就可以通過判斷對象是否有_obj屬性得知這個對象是否表示 OC 對象指針,在_c函數里若判斷到調用者有_obj屬性,取出這個屬性,跟調用的實例方法一起傳回給OC,就完成了實例方法的調用。
但是:
JS無法調用 NSMutableArray / NSMutableDictionary / NSMutableString 的方法去修改這些對象的數據,因為這三者都在從OC返回到JS時 JavaScriptCore 把它們轉成了JS的Array/Object/String,在返回的時候就脫離了跟原對象的聯系,這個轉換在JavaScriptCore里是強制進行的,無法選擇。
若想要在對象返回JS后,回到OC還能調用這個對象的方法,就要阻止JavaScriptCore的轉換,唯一的方法就是不直接返回這個對象,而是對這個對象進行封裝,JPBoxing 就是做這個事情的。
把NSMutableArray/NSMutableDictionary/NSMutableString對象作為JPBoxing的成員保存在JPBoxing實例對象上返回給JS,JS拿到的是JPBoxing對象的指針,再傳回給OC時就可以通過對象成員取到原來的NSMutableArray/NSMutableDictionary/NSMutableString對象,類似于裝箱/拆箱操作,這樣就避免了這些對象被JavaScriptCore轉換。
實際上只有可變的NSMutableArray/NSMutableDictionary/NSMutableString這三個類有必要調用它的方法去修改對象里的數據,不可變的NSArray/NSDictionary/NSString是沒必要這樣做的,直接轉為JS對應的類型使用起來會更方便,但為了規則簡單,JSPatch讓NSArray/NSDictionary/NSString也同樣以封裝的方式返回,避免在調用OC方法返回對象時還需要關心它返回的是可變還是不可變對象。最后整個規則還是挺清晰:NSArray/NSDictionary/NSString 及其子類與其他 NSObject 對象的行為一樣,在JS上拿到的都只是其對象指針,可以調用它們的OC方法,若要把這三種對象轉為對應的JS類型,使用額外的.toJS()的接口去轉換。
對于參數和返回值是C指針和 Class 類型的支持同樣是用 JPBoxing 封裝的方式,把指針和Class作為成員保存在JPBoxing對象上返回給JS,傳回OC時再解出來拿到原來的指針和Class,這樣JSPatch就支持所有數據類型OC<->JS的互傳了。
4. 類型轉換
還是引用作者的一段話:
JS把要調用的類名/方法名/對象傳給OC后,OC調用類/對象相應的方法是通過NSInvocation實現,要能順利調用到方法并取得返回值,要做兩件事:
例如舉例子的來講view.setAlpha(0.5),JS傳遞給OC的是一個NSNumber,OC需要通過要調用OC方法的 NSMethodSignature得知這里參數要的是一個float類型值,于是把NSNumber轉為float值再作為參數進行OC方法調用。這里主要處理了int/float/bool等數值類型,并對CGRect/CGRange等類型進行了特殊轉換處理。
5. callSelector
callSelector這個就是我們最后執行函數了!但是在執行這個函數之前,前面還有不少東西。
關于 _c函數
__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)}}其實_c函數就是一個消息轉發中心,它根據傳入的參數,這里可以分為兩種類型來講述:
- 對于實例方法和類方法,最后會調用_methodFunc方法
- 對于自定義的屬性,set和get操作。
對于自定義的屬性,其實它并不會將這些屬性真正添加到OC中的對象里去,它只會添加一個_ocProps對象,然后在JS中,通過_ocProps對象來保存我們所有定義的屬性,要獲取值的只要從這個屬性里通過name獲取就可以了。
對于_methodFunc方法,其實就是將OC方法的名字還原,帶上參數,然后轉發給類方法或者實例方法處理。
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)}對于callSelector方法來講:
- 將JS封裝的instance對象進行拆裝,得到OC的對象;
- 根據類名與selectorName獲得對應的類對象與selector;
- 通過類對象與selector構造對應的NSMethodSignature簽名,再根據簽名構造NSInvocation對象,并為invocation對象設置target與Selector
總結
好,到現在為止,我們所有的流程就已經走完了,我們的js文件也已經生效了。當然,我所說JSPatch原理只是基礎的一部份原理,可以使我們的基本流程可以實現,還有一些復雜的操作功能,還需要再深入的學習,才可以掌握,JSPatch對于學習Runtime也是一個不錯的例子,就像Aspects一樣,大家可以去好好研究一下。
文/北辰明(簡書作者)
原文鏈接:http://www.jianshu.com/p/66dad614b905 http://www.jianshu.com/p/66dad614b905?utm_campaign=hugo&utm_medium=reader_share&utm_content=note
總結
以上是生活随笔為你收集整理的iOS中的HotFix方案总结详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 学习笔记900天总结
- 下一篇: XTU 1250 Super Fast