關於HotfixPatch
在IOS開發領域,由於Apple嚴格的稽核標準和低效率,IOS應用的發版速度極慢,稍微大型的app發版基本上都在一個月以上,所以程式碼熱更新(HotfixPatch)對於IOS應用來說就顯得尤其重要。
現在業內基本上都在使用WaxPatch方案,由於Wax框架已經停止維護四五年了,所以waxPatch在使用過程中還是存在不少坑(比如引數轉化過程中的問題,如果繼承類沒有例項化修改繼承類的方法無效, wax_gc中對oc中instance的持有延遲釋放…)。另外蘋果對於Wax使用的態度也處於模糊狀態,這也是一個潛在的使用風險。
隨著FaceBook開源React Native框架,利用JavaScriptCore.framework直接建立JavaScript(JS)和Objective-C(OC)之間的bridge成為可能,JSPatch也在這個時候應運而生。最開始是從唐巧的微信公眾號推送上了解到,開始還以為是在React Native的基礎上進行的封裝,不過最近仔細研究了原始碼,跟React Native半毛錢關係都沒有,這裡先對JSPatch的作者(不是唐巧,是Bang,部落格地址)贊一個。
深入瞭解JSPatch之後,第一感覺是這個方案小巧,易懂,維護成本低,直接通過OC程式碼去呼叫runtime的API,作為一個IOS開發者,很快就能看明白,不用花大精力去了解學習lua。另外在建立JS和OC的Bridge時,作者很巧妙的利用JS和OC兩種語言的訊息轉發機制做了很優雅的實現,稍顯不足的是JSPatch只能支援ios7及以上。
由於現在公司的部分應用還在支援ios6,完全取代Wax也不現實,但是一些新上應用已經直接開始支援ios7。個人覺得ios6和ios7的介面風格差別較大,相信應用最低支援版本會很快升級到ios7. 還考慮到JSPatch的成熟度不夠,所以決定把JSPatch和WaxPatch結合在一起,相互補充進行使用。下面給大家說一些學習使用體會。
JSPatch和WaxPatch對比
關於JSPatch對比WaxPatch的優勢,下面摘抄一下JSPatch作者的話:
方案對比
目前已經有一些方案可以實現動態打補丁,例如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).
詳細介紹參考博文:
runtime提供了一些執行時的API
- 反射類和選擇器
1 2 |
Class class = NSClassFromString("UIViewController"); SEL selector = NSSelectorFromString("viewDidLoad"); |
- 為某個類新增或者替換方法選擇器(SEL)的實現(IMP)
1 2 |
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中動態註冊類
1 2 3 |
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程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
//請求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建立的橋接函式傳入引數和返回引數即完成了呼叫;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
//字元替換 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的初始實現進行儲存;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
//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方法(全域性快取並沒有多大用途,但是對於後面恢復現場比較有用);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
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 中新增了兩個啟動和結束的呼叫函式如下:
1 2 |
void js_start(NSString* initScript); void js_end(); |
(2) JPEngine.m 中呼叫函式的實現以及恢復現場對部分程式碼的修改:主要是利用了替換方法和新增方法的cache(_JSOverideMethods, 主要是這個)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
//處理替換方法,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方法,則可以調起;
- …
先寫這麼多了,本來想寫一下我們的patch管理方案,覺得沒有什麼可說了,就不寫了~