iOS中的HotFix方案總結詳解

fcs9191 8年前發布 | 26K 次閱讀 iOS開發 移動開發 JavaScript

相信HotFix大家應該都很熟悉了,今天主要對于最近調研的一些方案做一些總結。iOS中的HotFix方案大致可以分為四種:

  • WaxPatch(Alibaba)
  • Dynamic Framework(Apple)
  • React Native(非死book)
  • 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空對象

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 {
             // 通過OC回調回來執行,獲取參數 
            var args = _formatOCToJS(Array.prototype.slice.call(arguments))
            var lastSelf = global.self
            global.self = args[0]
            if (global.self) global.self.__realClsName = realClsName
            // 刪除前兩個參數:在OC中進行消息轉發的時候,前兩個參數是self和selector,
            // 我們在實際調用js的具體實現的時候,需要把這兩個參數刪除。
            args.splice(0,1)
            var ret = originMethod.apply(originMethod, args)
            global.self = lastSelf
            return ret
          } catch(e) {
            _OC_catch(e.message, e.stack)
          }
        }]
      })()
    }
  }

可以發現,具體實現是遍歷方法列表對象的屬性(方法名),然后往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

  1. 使用NSScanner分離classDeclaration,分離成三部分

    • 類名 : className
    • 父類名 : superClassName
    • 實現的協議名 : protocalNames
  2. 使用NSClassFromString(className)獲得該Class對象。

    • 若該Class對象為nil,則說明JS端要添加一個新的類,使用objc_allocateClassPair與objc_registerClassPair注冊一個新的類。
    • 若該Class對象不為nil,則說明JS端要替換一個原本已存在的類
  3. 根據從JS端傳遞來的實例方法與類方法參數,為這個類對象添加/替換實例方法與類方法

    • 添加實例方法時,直接使用上一步得到class對象; 添加類方法時需要調用objc_getMetaClass方法獲得元類。
    • 如果要替換的類已經定義了該方法,則直接對該方法替換和實現消息轉發。
    • 否則根據以下兩種情況進行判斷
      • 遍歷protocalNames,通過objc_getProtocol方法獲得協議對象,再使用protocol_copyMethodDescriptionList來獲得協議中方法的type和name。匹配JS中傳入的selectorName,獲得typeDescription字符串,對該協議方法的實現消息轉發。
      • 若不是上述兩種情況,則js端請求添加一個新的方法。構造一個typeDescription為”@@:\@*”(返回類型為id,參數值根據JS定義的參數個數來決定。新增方法的返回類型和參數類型只能為id類型,因為在JS端只能定義對象)的IMP。將這個IMP添加到類中。
  4. 為該類添加setProp:forKey和getProp:方法,使用objc_getAssociatedObject與objc_setAssociatedObject讓JS腳本擁有設置property的能力

  5. 返回{className:cls}回JS腳本。

不過其中還包括一個overrideMethod方法,不管是替換方法還是新增方法,都是使用overrideMethod方法。它的目的主要在于進行method swizzing操作,也就是方法的重定向。我們把所有的消息全部都轉發到ForwardInvocation函數里去執行(不知道的同學請自行補消息轉發機制),這樣做的目的在于,我們可以在NSInvocation中獲取到所有的參數,這樣就可以實現一個通用的IMP,任意方法任意參數都可以通過這個IMP中轉,拿到方法的所有參數回調JS的實現。于是overrideMethod其實就是做了如下這件事情:

具體實現,以替換 UIViewController 的 -viewWillAppear: 方法為例:

  1. 把UIViewController的-viewWillAppear:方法通過class_replaceMethod()接口指向_objc_msgForward這是一個全局 IMP,OC 調用方法不存在時都會轉發到這個IMP上,這里直接把方法替換成這個IMP,這樣調用這個方法時就會走到-forwardInvocation:。

  2. 為UIViewController添加-ORIGviewWillAppear:和-_JPviewWillAppear: 兩個方法,前者指向原來的IMP實現,后者是新的實現,稍后會在這個實現里回調JS函數。

  3. 改寫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&lt;-&gt;JS的互傳了。

4. 類型轉換

還是引用作者的一段話:

JS把要調用的類名/方法名/對象傳給OC后,OC調用類/對象相應的方法是通過NSInvocation實現,要能順利調用到方法并取得返回值,要做兩件事:

  1. 取得要調用的 OC 方法各參數類型,把 JS 傳來的對象轉為要求的類型進行調用。
  2. 根據返回值類型取出返回值,包裝為對象傳回給 JS。

例如舉例子的來講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 = 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)
      }
    }

其實_c函數就是一個消息轉發中心,它根據傳入的參數,這里可以分為兩種類型來講述:

  • 對于實例方法和類方法,最后會調用_methodFunc方法
  • 對于自定義的屬性,set和get操作。

對于自定義的屬性,其實它并不會將這些屬性真正添加到OC中的對象里去,它只會添加一個_ocProps對象,然后在JS中,通過_ocProps對象來保存我們所有定義的屬性,要獲取值的只要從這個屬性里通過name獲取就可以了。

對于_methodFunc方法,其實就是將OC方法的名字還原,帶上參數,然后轉發給類方法或者實例方法處理。

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)
  }

對于callSelector方法來講:

  1. 初始化
    • 將JS封裝的instance對象進行拆裝,得到OC的對象;
    • 根據類名與selectorName獲得對應的類對象與selector;
    • 通過類對象與selector構造對應的NSMethodSignature簽名,再根據簽名構造NSInvocation對象,并為invocation對象設置target與Selector
  2. 根據方法簽名,獲悉方法每個參數的實際類型,將JS傳遞過來的參數進行對應的轉換(比如說參數的實際類型為int類型,但是JS只能傳遞NSNumber對象,需要通過[[jsObj toNumber] intValue]進行轉換)。轉換后使用setArgument方法為NSInvocation對象設置參數。
  3. 執行invoke方法。
  4. 通過getReturnValue方法獲取到返回值。
  5. 根據返回值類型,封裝成JS中對應的對象(因為JS并不識別OC對象,所以返回值為OC對象的話需封裝成{className:className, obj:obj})返回給JS端。

總結

好,到現在為止,我們所有的流程就已經走完了,我們的js文件也已經生效了。當然,我所說JSPatch原理只是基礎的一部份原理,可以使我們的基本流程可以實現,還有一些復雜的操作功能,還需要再深入的學習,才可以掌握,JSPatch對于學習Runtime也是一個不錯的例子,就像Aspects一樣,大家可以去好好研究一下。

 

來自:http://www.jianshu.com/p/66dad614b905

 

 本文由用戶 fcs9191 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!