IOS熱更新-JSPatch實現原理+Patch現場恢復

關於HotfixPatch前端

 

在IOS開發領域,因爲Apple嚴格的審覈標準和低效率,IOS應用的發版速度極慢,稍微大型的app發版基本上都在一個月以上,因此代碼熱更新(HotfixPatch)對於IOS應用來講就顯得尤爲重要。ios

 

如今業內基本上都在使用WaxPatch方案,因爲Wax框架已經中止維護四五年了,因此waxPatch在使用過程當中仍是存在很多坑(好比參數轉化過程當中的問題,若是繼承類沒有實例化修改繼承類的方法無效, wax_gc中對oc中instance的持有延遲釋放…)。另外蘋果對於Wax使用的態度也處於模糊狀態,這也是一個潛在的使用風險。git

 

隨着FaceBook開源React Native框架,利用JavaScriptCore.framework直接創建JavaScript(JS)和Objective-C(OC)之間的bridge成爲可能,JSPatch也在這個時候應運而生。最開始是從唐巧的微信公衆號推送上了解到,開始還覺得是在React Native的基礎上進行的封裝,不過最近仔細研究了源代碼,跟React Native半毛錢關係都沒有,這裏先對JSPatch的做者(不是唐巧,是Bang,博客地址)贊一個。github

 

深刻了解JSPatch以後,第一感受是這個方案小巧,易懂,維護成本低,直接經過OC代碼去調用runtime的API,做爲一個IOS開發者,很快就能看明白,不用花大精力去了解學習lua。另外在創建JS和OC的Bridge時,做者很巧妙的利用JS和OC兩種語言的消息轉發機制作了很優雅的實現,稍顯不足的是JSPatch只能支持ios7及以上。緩存

 

因爲如今公司的部分應用還在支持ios6,徹底取代Wax也不現實,可是一些新上應用已經直接開始支持ios7。我的以爲ios6和ios7的界面風格差異較大,相信應用最低支持版本會很快升級到ios7. 還考慮到JSPatch的成熟度不夠,因此決定把JSPatch和WaxPatch結合在一塊兒,相互補充進行使用。下面給你們說一些學習使用體會。微信

 

JSPatch和WaxPatch對比app

 

關於JSPatch對比WaxPatch的優點,下面摘抄一下JSPatch做者的話:框架

 

  • 來源: JSPatch – 動態更新iOS APPide

    http://blog.cnbang.net/works/2767/函數

 

方案對比

 

目前已經有一些方案能夠實現動態打補丁,例如WaxPatch,能夠用Lua調用OC方法,相對於WaxPatch,JSPatch的優點:

 

  • 1.JS語言: JS比Lua在應用開發領域有更普遍的應用,目前前端開發和終端開發有融合的趨勢,做爲擴展的腳本語言,JS是不二之選。

  • 2.符合Apple規則: JSPatch更符合Apple的規則。iOS Developer Program License Agreement裏3.3.2提到不可動態下發可執行代碼,但經過蘋果JavaScriptCore.framework或WebKit執行的代碼除外,JS正是經過JavaScriptCore.framework執行的。

  • 3.小巧: 使用系統內置的JavaScriptCore.framework,無需內嵌腳本引擎,體積小巧。

  • 4.支持block: wax在幾年前就中止了開發和維護,不支持Objective-C裏block跟Lua程序的互傳,雖然一些第三方已經實現block,但使用時參數上也有比較多的限制。

 

JSPatch的劣勢:

 

  • 相對於WaxPatch,JSPatch劣勢在於不支持iOS6,由於須要引入JavaScriptCore.framework。另外目前內存的使用上會高於wax,持續改進中。

 

JSPatch的實現原理理解

 

JSPatch的實現原理做者的博文已經很詳細的介紹了,我這裏就很少說了,貼一下學習之處:

 

  • JSPatch實現原理詳解 http://blog.cnbang.net/tech/2808/

  • JSPatch Git源碼和使用說明 https://github.com/bang590/JSPatch

 

看實現原理詳解的時候對照着源碼看,比較好理解,我在這裏說一下我對JSPatch的學習和理解:

 

(1)OC的動態語言特性

 

不論是WaxPatch框架仍是JSPatch的方案,其根本原理都是利用OC的動態語言特性去動態修改類的方法實現。

OC的動態語言特性是在runtime system(所有用C實現,Apple維護了一份開源代碼)上實現的,面向對象的Class和instance機制都是基於消息機制。咱們平時認爲的[object method],正確的理解應該是[receiver sendMsg], 全部的消息發送會在編譯階段編譯爲runtime c函數的調用:_obj_sendMsg(id, SEL).

 

詳細介紹參考博文:

 

  • Objective-C Runtime詳細介紹

    http://justsee.iteye.com/blog/2163777

  • Objective-C Runtime源碼_Apple

    http://www.opensource.apple.com/source/objc4/

 

runtime提供了一些運行時的API

 

  • 反射類和選擇器

 

 Class class = NSClassFromString("UIViewController");

    SEL selector = NSSelectorFromString("viewDidLoad");

 

  • 爲某個類新增或者替換方法選擇器(SEL)的實現(IMP)

 

    BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);

    IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types);

 

  • 在runtime中動態註冊類

 

    Class superCls = NSClassFromString(superClassName);

    cls = objc_allocateClassPair(superCls, className.UTF8String, 0);

    objc_registerClassPair(cls);

 

(2)JS如何調用OC

 

在JS運行環境中,須要解決兩個問題,一個是OC類對象(objc_class)的獲取,另外一個就是使用對象提供的接口方法。

 

對於第一個問題,JSPatch在實現中是經過Require調用在JS環境下建立一個class同名對象(js形式),當向OC發送alloc接收消息以後,會將OC環境中建立的對象地址保存到這個這個js同名對象中,js自己並不完成任何對象的初始化。關於JS持有OC對象的引用,其回收的解釋在JSPatch做者的博文中有介紹,沒有具體測試。詳見JSPatch.js代碼:

 

    //請求OC類對象

    UIView = require("UIView");

 

    //緩存JS class同名對象

    var _require = function(clsName) {

        if (!global[clsName]) {

          global[clsName] = {

            __isCls: 1,

            __clsName: clsName

          }

        }

        return global[clsName]

      }

 

    //調用class方法,返回OC實例化對象進行封裝

    var ret = instance ? _OC_callI(instance, selectorName, args, isSuper):

                         _OC_callC(clsName, selectorName, args)

 

    //OC建立後返回對象

    return@{@"__clsName": NSStringFromClass([obj class]), @"__obj": obj};

 

 

    //JS中解析OC對象

    return _formatOCToJS(ret)

 

    //_formatOCToJS

    if (obj instanceof Object) {

        var ret = {}

        for (var key in obj) {

          ret[key] = _formatOCToJS(obj[key])

        }

        return ret

     }

 

對於第二個問題,JSPatch在JS環境中經過中心轉發方式,全部OC方法的調用均是經過新增Object(js)原型方法_c(methodName)完成調用,在經過JavaScriptCore執行JS腳本以前,先將全部的方法調用字符替換

_c(‘method’)的方式; 在_c函數中經過JSContex創建的橋接函數傳入參數和返回參數即完成了調用;

 

    //字符替換

    static NSString *_regexStr = @"\\.\\s*(\\w+)\\s*\\(";

    static NSString *_replaceStr = @".__c(\"$1\")(";

 

    NSString *formatedScript = [NSString stringWithFormat:@"try{@}catch(e){_OC_catch(e.message, e.stack)}", [_regex stringByReplacingMatchesInString:script options:0 range:NSMakeRange(0, script.length) withTemplate:_replaceStr]];

 

 

    //__c()向OC轉發調用參數

    Object.prototype.__c = function(methodName) {

 

        ...

 

        return function(){

          var args = Array.prototype.slice.call(arguments)

          return _methodFunc(self.__obj, self.__clsName, methodName, args, self.__isSuper)

        }

     }

 

    //_methodFunc調用橋接函數

    var _methodFunc = function(instance, clsName, methodName, args, isSuper) {

 

        ...

 

        var ret = instance ? _OC_callI(instance, selectorName, args, isSuper):

                             _OC_callC(clsName, selectorName, args)

 

        return _formatOCToJS(ret)

     }

 

 

    //OC中的橋接函數,JS和OC的橋接函數都是經過這樣定義

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

    };

 

(3)JS如何替換OC方法

 

JSPatch的主要做用仍是經過腳本修復一些線上bug,但願可以達到替換OC方法的目標。JSPatch的實現巧妙之處在於:利用了OC的消息轉發機制。

 

1:替換原有selector的IMP實現爲一個空的IMP實現,這樣當objc_class接受到消息以後,就會進行消息轉發, 另外須要將selector的初始實現進行保存;

 

    //selector指向空實現

    IMP msgForwardIMP = getEmptyMsgForwardIMP(typeDescription, methodSignature);

    class_replaceMethod(cls, selector, msgForwardIMP, typeDescription);

 

 

    //保存原有實現,這裏進行了修改,增長了恢復現場的支持

    NSString *originalSelectorName = [NSString stringWithFormat:@"ORIG@", selectorName];

    SEL originalSelector = NSSelectorFromString(originalSelectorName);

    if(class_respondsToSelector(cls, selector)) {

        if(!class_respondsToSelector(cls, originalSelector)){

            class_addMethod(cls, originalSelector, originalImp, typeDescription);

        } else {

            class_replaceMethod(cls, originalSelector, originalImp, typeDescription);

        }

    }

 

2:將替換的JS方法構造一個JPSelector及其IMP實現(根據返回參數構造),添加到當前class中,並經過cls+selecotr全局緩存JS方法(全局緩存並無多大用途,可是對於後面恢復現場比較有用);

 

    if (!_JSOverideMethods[clsName][JPSelectorName]) {

        _initJPOverideMethods(clsName);

        _JSOverideMethods[clsName][JPSelectorName] = function;

        const char *returnType = [methodSignature methodReturnType];

        IMP JPImplementation = NULL;

 

        //根據返回類型構造

        switch (returnType[0]){

         ...

        }

 

        if(!class_respondsToSelector(cls, JPSelector)){

            class_addMethod(cls, JPSelector, JPImplementation, typeDescription);

        } else {

            class_replaceMethod(cls, JPSelector, JPImplementation,typeDescription);

        }

    }

 

3:而後改寫每一個替換方法類的forwadInvocation的實現進行攔截,若是攔截到的Invocation的selctor轉化成JPSelector可以響應,說明是一個替換方法,則從Invocation中取參數後調用JPSelector的IMP;

 

    static void JPForwardInvocation(id slf, SEL selector, NSInvocation *invocation)

    {

        NSMethodSignature *methodSignature = [invocation methodSignature];

        NSInteger numberOfArguments = [methodSignature numberOfArguments];

 

        NSString *selectorName = NSStringFromSelector(invocation.selector);

        NSString *JPSelectorName = [NSString stringWithFormat:@"_JP@", selectorName];

        SEL JPSelector = NSSelectorFromString(JPSelectorName);

 

        if (!class_respondsToSelector(object_getClass(slf), JPSelector)) {

            ...

        }

 

        NSMutableArray *argList = [[NSMutableArray alloc] init];

        [argList addObject:slf];

 

        for (NSUInteger i = 2; i < numberOfArguments; i++) {

            ...

        }

 

        //獲取參數以後invoke JPSector調用JSFunction的實現

        @synchronized(_context) {

            _TMPInvocationArguments = formatOCToJSList(argList);

 

            [invocation setSelector:JPSelector];

            [invocation invoke];

 

            _TMPInvocationArguments = nil;

        }

    }

 

Patch現場復原的補充

 

Patch現場恢復的功能主要用於連續更新腳本的應用場景。因爲IOS的App應用按Home鍵或者被電話中斷的時候,應用其實是首先進入到後臺運行階段(applicationWillResignActive),當咱們下次再次使用App的時候,若是後臺應用沒有被終止(applicationWillTerminate),那麼App不會走appliation:didFinishLaunchingWithOptions方法,而是會走

(applicationWillEnterForeground)。 對於這種場景若是咱們連續更新線上腳本,那麼第二次腳本更新則沒法保留最開始的方法實現,另外恢復現場功能也有助於咱們撤銷線上腳本可以恢復應用的自己代碼功能。

 

JSPatch的現場恢復

 

本文在JSPatch基礎上添加了現場恢復功能;源碼地址參考:

 

  • 增長現場恢復的JSPatchDemo:

    https://github.com/philonpang/JSPatch.git

 

說明以下:

 

(1)在JPEngine.h 中添加了兩個啓動和結束的調用函數以下:

 

   void js_start(NSString* initScript);

    void js_end();

 

(2) JPEngine.m 中調用函數的實現以及恢復現場對部分代碼的修改:主要是利用了替換方法和新增方法的cache(_JSOverideMethods, 主要是這個)

 

    //處理替換方法,selector指回最初的IMP,JPSelector和ORIGSelector都指向未實現IMP

     if([JPSelectorName hasPrefix:@"_JP"]){

         if (class_getMethodImplementation(cls, @selector(forwardInvocation:)) == (IMP)JPForwardInvocation) {

             SEL ORIGforwardSelector = @selector(ORIGforwardInvocation:);

             IMP ORIGforwardImp = class_getMethodImplementation(cls, ORIGforwardSelector);

             class_replaceMethod(cls, @selector(forwardInvocation:), ORIGforwardImp, "v@:@");

             class_replaceMethod(cls, ORIGforwardSelector, _objc_msgForward, "v@:@");

         }

 

 

         NSString *selectorName = [JPSelectorName stringByReplacingOccurrencesOfString:@"_JP" withString:@""];

         NSString *ORIGSelectorName = [JPSelectorName stringByReplacingOccurrencesOfString:@"_JP" withString:@"ORIG"];

 

         SEL JPSelector = NSSelectorFromString(JPSelectorName);

         SEL selector = NSSelectorFromString(selectorName);

         SEL ORIGSelector = NSSelectorFromString(ORIGSelectorName);

 

         if(class_respondsToSelector(cls, ORIGSelector) &&

            class_respondsToSelector(cls, selector) &&

            class_respondsToSelector(cls, JPSelector)){

             NSMethodSignature *methodSignature = [cls instanceMethodSignatureForSelector:ORIGSelector];

             Method method = class_getInstanceMethod(cls, ORIGSelector);

             char *typeDescription = (char *)method_getTypeEncoding(method);

             IMP forwardEmptyIMP = getEmptyMsgForwardIMP(typeDescription, methodSignature);

             IMP ORIGSelectorImp = class_getMethodImplementation(cls, ORIGSelector);

 

             class_replaceMethod(cls, selector, ORIGSelectorImp, typeDescription);

             class_replaceMethod(cls, JPSelector, forwardEmptyIMP, typeDescription);

             class_replaceMethod(cls, ORIGSelector, forwardEmptyIMP, typeDescription);

         }

     }

 

     //處理添加的新方法

     else {

         isClsNew = YES;

         SEL JPSelector = NSSelectorFromString(JPSelectorName);

         if(class_respondsToSelector(cls, JPSelector)){

             NSMethodSignature *methodSignature = [cls instanceMethodSignatureForSelector:JPSelector];

             Method method = class_getInstanceMethod(cls, JPSelector);

             char *typeDescription = (char *)method_getTypeEncoding(method);

             IMP forwardEmptyIMP = getEmptyMsgForwardIMP(typeDescription, methodSignature);

 

             class_replaceMethod(cls, JPSelector, forwardEmptyIMP, typeDescription);

         }

     }

 

HotfixPatch的那些坑

 

WaxPatch以前被一些同事抱怨有很多坑,JSPatch在使用過程當中也會遇到很多坑,因此雖然這兩個框架如今雖然都可以作到新增可執行代碼,可是將其應用到開發功能組件還不太可取。

 

好比說我在第一次使用JSPatch遇到了一個坑:(後面想單寫一個博客收集一下咱們團隊使用Patch遇到的坑~~)

 

  • 在JS腳本改寫派生類中未實現的繼承類的 optional protocol方法時,tableView reload的時候不會調用JS的補丁方法,可是在tableView中顯式調用能夠調用替換的selector方法;另外若是在派生類中重寫這個protocol方法,則能夠調起;

相關文章
相關標籤/搜索