iOS熱更新-JSPatch實現(xiàn)原理+Patch現(xiàn)場恢復

關(guān)于HotfixPatch

在iOS開發(fā)領(lǐng)域,由于Apple嚴格的審核標準和低效率,iOS應(yīng)用的發(fā)版速度極慢,稍微大型的app發(fā)版基本上都在一個月以上,所以代碼熱更新(HotfixPatch)對于iOS應(yīng)用來說就顯得尤其重要造成。

現(xiàn)在業(yè)內(nèi)基本上都在使用WaxPatch方案,由于Wax框架已經(jīng)停止維護四五年了雄嚣,所以waxPatch在使用過程中還是存在不少坑(比如參數(shù)轉(zhuǎn)化過程中的問題晒屎,如果繼承類沒有實例化修改繼承類的方法無效, wax_gc中對oc中instance的持有延遲釋放...)。另外蘋果對于Wax使用的態(tài)度也處于模糊狀態(tài)缓升,這也是一個潛在的使用風險鼓鲁。

隨著FaceBook開源React Native框架,利用JavaScriptCore.framework直接建立JavaScript(JS)和Objective-C(OC)之間的bridge成為可能港谊,JSPatch也在這個時候應(yīng)運而生骇吭。最開始是從唐巧的微信公眾號推送上了解到,開始還以為是在React Native的基礎(chǔ)上進行的封裝歧寺,不過最近仔細研究了源代碼燥狰,跟React Native半毛錢關(guān)系都沒有,這里先對JSPatch的作者(不是唐巧斜筐,是Bang龙致,博客地址)贊一個。

深入了解JSPatch之后顷链,第一感覺是這個方案小巧目代,易懂,維護成本低嗤练,直接通過OC代碼去調(diào)用runtime的API榛了,作為一個IOS開發(fā)者,很快就能看明白潭苞,不用花大精力去了解學習lua。另外在建立JS和OC的Bridge時真朗,作者很巧妙的利用JS和OC兩種語言的消息轉(zhuǎn)發(fā)機制做了很優(yōu)雅的實現(xiàn)此疹,稍顯不足的是JSPatch只能支持ios7及以上。

由于現(xiàn)在公司的部分應(yīng)用還在支持ios6遮婶,完全取代Wax也不現(xiàn)實蝗碎,但是一些新上應(yīng)用已經(jīng)直接開始支持ios7。個人覺得ios6和ios7的界面風格差別較大旗扑,相信應(yīng)用最低支持版本會很快升級到ios7. 還考慮到JSPatch的成熟度不夠蹦骑,所以決定把JSPatch和WaxPatch結(jié)合在一起,相互補充進行使用臀防。下面給大家說一些學習使用體會眠菇。

JSPatch和WaxPatch對比

關(guān)于JSPatch對比WaxPatch的優(yōu)勢边败,下面摘抄一下JSPatch作者的話:

來源: JSPatch – 動態(tài)更新iOS APP

方案對比

目前已經(jīng)有一些方案可以實現(xiàn)動態(tài)打補丁,例如WaxPatch捎废,可以用Lua調(diào)用OC方法笑窜,相對于WaxPatch,JSPatch的優(yōu)勢:

1.JS語言:JS比Lua在應(yīng)用開發(fā)領(lǐng)域有更廣泛的應(yīng)用登疗,目前前端開發(fā)和終端開發(fā)有融合的趨勢排截,作為擴展的腳本語言,JS是不二之選辐益。

2.符合Apple規(guī)則:JSPatch更符合Apple的規(guī)則断傲。iOS Developer Program License Agreement里3.3.2提到不可動態(tài)下發(fā)可執(zhí)行代碼,但通過蘋果JavaScriptCore.framework或WebKit執(zhí)行的代碼除外智政,JS正是通過JavaScriptCore.framework執(zhí)行的认罩。

3.小巧:使用系統(tǒng)內(nèi)置的JavaScriptCore.framework,無需內(nèi)嵌腳本引擎女仰,體積小巧猜年。

4.支持block:wax在幾年前就停止了開發(fā)和維護,不支持Objective-C里block跟Lua程序的互傳疾忍,雖然一些第三方已經(jīng)實現(xiàn)block乔外,但使用時參數(shù)上也有比較多的限制。

JSPatch的劣勢:

相對于WaxPatch一罩,JSPatch劣勢在于不支持iOS6杨幼,因為需要引入JavaScriptCore.framework。另外目前內(nèi)存的使用上會高于wax聂渊,持續(xù)改進中差购。

JSPatch的實現(xiàn)原理理解

JSPatch的實現(xiàn)原理作者的博文已經(jīng)很詳細的介紹了,我這里就不多說了汉嗽,貼一下學習之處:

JSPatch實現(xiàn)原理詳解http://blog.cnbang.net/tech/2808/

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

看實現(xiàn)原理詳解的時候?qū)φ罩创a看欲逃,比較好理解,我在這里說一下我對JSPatch的學習和理解:

(1)OC的動態(tài)語言特性

不管是WaxPatch框架還是JSPatch的方案饼暑,其根本原理都是利用OC的動態(tài)語言特性去動態(tài)修改類的方法實現(xiàn)稳析。

OC的動態(tài)語言特性是在runtime system(全部用C實現(xiàn),Apple維護了一份開源代碼)上實現(xiàn)的弓叛,面向?qū)ο蟮腃lass和instance機制都是基于消息機制彰居。我們平時認為的[object method],正確的理解應(yīng)該是[receiver sendMsg], 所有的消息發(fā)送會在編譯階段編譯為runtime c函數(shù)的調(diào)用:_obj_sendMsg(id, SEL).

詳細介紹參考博文:

Objective-C Runtime詳細介紹

Objective-C Runtime源碼_Apple

runtime提供了一些運行時的API

反射類和選擇器

Class class =NSClassFromString("UIViewController");? ? SEL selector =NSSelectorFromString("viewDidLoad");

為某個類新增或者替換方法選擇器(SEL)的實現(xiàn)(IMP)

BOOLclass_addMethod(Class cls, SEL name, IMP imp,constchar*types);IMPclass_replaceMethod(Class cls, SEL name, IMP imp,constchar*types);

在runtime中動態(tài)注冊類

Class superCls = NSClassFromString(superClassName);cls = objc_allocateClassPair(superCls, className.UTF8String,0);objc_registerClassPair(cls);

(2)JS如何調(diào)用OC

在JS運行環(huán)境中撰筷,需要解決兩個問題陈惰,一個是OC類對象(objc_class)的獲取,另一個就是使用對象提供的接口方法毕籽。

對于第一個問題抬闯,JSPatch在實現(xiàn)中是通過Require調(diào)用在JS環(huán)境下創(chuàng)建一個class同名對象(js形式)井辆,當向OC發(fā)送alloc接收消息之后,會將OC環(huán)境中創(chuàng)建的對象地址保存到這個這個js同名對象中画髓,js本身并不完成任何對象的初始化掘剪。關(guān)于JS持有OC對象的引用,其回收的解釋在JSPatch作者的博文中有介紹奈虾,沒有具體測試夺谁。詳見JSPatch.js代碼:

//請求OC類對象UIView= require("UIView");//緩存JS class同名對象var_require = function(clsName) {if(!global[clsName]) {? ? ? ? ? global[clsName] = {? ? ? ? ? ? __isCls:1,? ? ? ? ? ? __clsName: clsName? ? ? ? ? }? ? ? ? }returnglobal[clsName]? ? ? }//調(diào)用class方法,返回OC實例化對象進行封裝varret = instance ? _OC_callI(instance, selectorName, args, isSuper):? ? ? ? ? ? ? ? ? ? ? ? _OC_callC(clsName, selectorName, args)//OC創(chuàng)建后返回對象return@{@"__clsName":NSStringFromClass([objclass]),@"__obj":obj};//JS中解析OC對象return_formatOCToJS(ret)//_formatOCToJSif(obj instanceofObject) {varret = {}for(varkey in obj) {? ? ? ? ? ret[key] = _formatOCToJS(obj[key])? ? ? ? }returnret? ? }

對于第二個問題肉微,JSPatch在JS環(huán)境中通過中心轉(zhuǎn)發(fā)方式匾鸥,所有OC方法的調(diào)用均是通過新增Object(js)原型方法_c(methodName)完成調(diào)用,在通過JavaScriptCore執(zhí)行JS腳本之前碉纳,先將所有的方法調(diào)用字符替換

_c('method')的方式勿负; 在_c函數(shù)中通過JSContex建立的橋接函數(shù)傳入?yún)?shù)和返回參數(shù)即完成了調(diào)用;

//字符替換staticNSString*_regexStr =@"\\.\\s*(\\w+)\\s*\\(";staticNSString*_replaceStr =@".__c(\"$1\")(";NSString*formatedScript = [NSStringstringWithFormat:@"try{@}catch(e){_OC_catch(e.message, e.stack)}", [_regex stringByReplacingMatchesInString:script options:0range:NSMakeRange(0, script.length) withTemplate:_replaceStr]];//__c()向OC轉(zhuǎn)發(fā)調(diào)用參數(shù)Object.prototype.__c= function(methodName) {? ? ? ? ...returnfunction(){? ? ? ? ? var args = Array.prototype.slice.call(arguments)return_methodFunc(self.__obj,self.__clsName, methodName, args,self.__isSuper)? ? ? ? }? ? }//_methodFunc調(diào)用橋接函數(shù)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中的橋接函數(shù)劳曹,JS和OC的橋接函數(shù)都是通過這樣定義context[@"_OC_callI"] = ^id(JSValue *obj,NSString*selectorName, JSValue *arguments,BOOLisSuper) {returncallSelector(nil, selectorName, arguments, obj, isSuper);? ? };? ? context[@"_OC_callC"] = ^id(NSString*className,NSString*selectorName, JSValue *arguments) {returncallSelector(className, selectorName, arguments,nil,NO);? ? };

(3)JS如何替換OC方法

JSPatch的主要作用還是通過腳本修復一些線上bug奴愉,希望能夠達到替換OC方法的目標。JSPatch的實現(xiàn)巧妙之處在于:利用了OC的消息轉(zhuǎn)發(fā)機制铁孵。

1:替換原有selector的IMP實現(xiàn)為一個空的IMP實現(xiàn)锭硼,這樣當objc_class接受到消息之后,就會進行消息轉(zhuǎn)發(fā), 另外需要將selector的初始實現(xiàn)進行保存蜕劝;

//selector指向空實現(xiàn)IMP msgForwardIMP = getEmptyMsgForwardIMP(typeDescription, methodSignature);? ? class_replaceMethod(cls, selector, msgForwardIMP, typeDescription);//保存原有實現(xiàn)檀头,這里進行了修改,增加了恢復現(xiàn)場的支持NSString*originalSelectorName = [NSStringstringWithFormat:@"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方法構(gòu)造一個JPSelector及其IMP實現(xiàn)(根據(jù)返回參數(shù)構(gòu)造)岖沛,添加到當前class中暑始,并通過cls+selecotr全局緩存JS方法(全局緩存并沒有多大用途,但是對于后面恢復現(xiàn)場比較有用);

if(!_JSOverideMethods[clsName][JPSelectorName]) {? ? ? ? _initJPOverideMethods(clsName);? ? ? ? _JSOverideMethods[clsName][JPSelectorName] =function;constchar *returnType = [methodSignature methodReturnType];? ? ? ? IMP JPImplementation =NULL;//根據(jù)返回類型構(gòu)造switch(returnType[0]){? ? ? ? ...? ? ? ? }if(!class_respondsToSelector(cls, JPSelector)){? ? ? ? ? ? class_addMethod(cls, JPSelector, JPImplementation, typeDescription);? ? ? ? }else{? ? ? ? ? ? class_replaceMethod(cls, JPSelector, JPImplementation,typeDescription);? ? ? ? }? ? }

3:然后改寫每個替換方法類的forwadInvocation的實現(xiàn)進行攔截婴削,如果攔截到的Invocation的selctor轉(zhuǎn)化成JPSelector能夠響應(yīng)廊镜,說明是一個替換方法,則從Invocation中取參數(shù)后調(diào)用JPSelector的IMP唉俗;

staticvoidJPForwardInvocation(idslf, SEL selector,NSInvocation*invocation)? ? {NSMethodSignature*methodSignature = [invocation methodSignature];NSIntegernumberOfArguments = [methodSignature numberOfArguments];NSString*selectorName =NSStringFromSelector(invocation.selector);NSString*JPSelectorName = [NSStringstringWithFormat:@"_JP@", selectorName];? ? ? ? SEL JPSelector =NSSelectorFromString(JPSelectorName);if(!class_respondsToSelector(object_getClass(slf), JPSelector)) {? ? ? ? ? ? ...? ? ? ? }NSMutableArray*argList = [[NSMutableArrayalloc] init];? ? ? ? [argList addObject:slf];for(NSUIntegeri =2; i < numberOfArguments; i++) {? ? ? ? ? ? ...? ? ? ? }//獲取參數(shù)之后invoke JPSector調(diào)用JSFunction的實現(xiàn)@synchronized(_context) {? ? ? ? ? ? _TMPInvocationArguments= formatOCToJSList(argList);? ? ? ? ? ? [invocation setSelector:JPSelector];? ? ? ? ? ? [invocation invoke];? ? ? ? ? ? _TMPInvocationArguments=nil;? ? ? ? }? ? }

Patch現(xiàn)場復原的補充

Patch現(xiàn)場恢復的功能主要用于連續(xù)更新腳本的應(yīng)用場景嗤朴。由于IOS的App應(yīng)用按Home鍵或者被電話中斷的時候,應(yīng)用實際上是首先進入到后臺運行階段(applicationWillResignActive)互躬,當我們下次再次使用App的時候播赁,如果后臺應(yīng)用沒有被終止(applicationWillTerminate)颂郎,那么App不會走appliation:didFinishLaunchingWithOptions方法吼渡,而是會走(applicationWillEnterForeground)。 對于這種場景如果我們連續(xù)更新線上腳本乓序,那么第二次腳本更新則無法保留最開始的方法實現(xiàn)寺酪,另外恢復現(xiàn)場功能也有助于我們撤銷線上腳本能夠恢復應(yīng)用的本身代碼功能坎背。

JSPatch的現(xiàn)場恢復

本文在JSPatch基礎(chǔ)上添加了現(xiàn)場恢復功能;源碼地址參考:

增加現(xiàn)場恢復的JSPatchDemo:

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

說明如下:

(1)在JPEngine.h 中添加了兩個啟動和結(jié)束的調(diào)用函數(shù)如下:

voidjs_start(NSString* initScript);voidjs_end();

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

//處理替換方法,selector指回最初的IMP寄雀,JPSelector和ORIGSelector都指向未實現(xiàn)IMPif([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的那些坑

JSPatch在使用過程中也會遇到不少坑得滤,雖然這兩個框架現(xiàn)在雖然都能夠做到新增可執(zhí)行代碼,但是將其應(yīng)用到開發(fā)功能組件還不太可取盒犹。比如說我在第一次使用JSPatch遇到了一個坑:

在JS腳本改寫派生類中未實現(xiàn)的繼承類的 optional protocol方法時懂更,tableView reload的時候不會調(diào)用JS的補丁方法,但是在tableView中顯式調(diào)用可以調(diào)用替換的selector方法急膀;另外如果在派生類中重寫這個protocol方法沮协,則可以調(diào)起;



詳情參考:iOS熱更新-JSPatch實現(xiàn)原理+Patch現(xiàn)場恢復 - iOS知識庫

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末卓嫂,一起剝皮案震驚了整個濱河市慷暂,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌晨雳,老刑警劉巖行瑞,帶你破解...
    沈念sama閱讀 211,561評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異餐禁,居然都是意外死亡血久,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評論 3 385
  • 文/潘曉璐 我一進店門坠宴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來洋魂,“玉大人,你說我怎么就攤上這事喜鼓「笨常” “怎么了?”我有些...
    開封第一講書人閱讀 157,162評論 0 348
  • 文/不壞的土叔 我叫張陵庄岖,是天一觀的道長豁翎。 經(jīng)常有香客問我,道長隅忿,這世上最難降的妖魔是什么心剥? 我笑而不...
    開封第一講書人閱讀 56,470評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮背桐,結(jié)果婚禮上优烧,老公的妹妹穿的比我還像新娘。我一直安慰自己链峭,他們只是感情好畦娄,可當我...
    茶點故事閱讀 65,550評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般熙卡。 火紅的嫁衣襯著肌膚如雪杖刷。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,806評論 1 290
  • 那天驳癌,我揣著相機與錄音滑燃,去河邊找鬼。 笑死颓鲜,一個胖子當著我的面吹牛表窘,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播甜滨,決...
    沈念sama閱讀 38,951評論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼蚊丐,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了艳吠?” 一聲冷哼從身側(cè)響起麦备,我...
    開封第一講書人閱讀 37,712評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎昭娩,沒想到半個月后凛篙,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,166評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡栏渺,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,510評論 2 327
  • 正文 我和宋清朗相戀三年呛梆,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片磕诊。...
    茶點故事閱讀 38,643評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡填物,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出霎终,到底是詐尸還是另有隱情滞磺,我是刑警寧澤,帶...
    沈念sama閱讀 34,306評論 4 330
  • 正文 年R本政府宣布莱褒,位于F島的核電站击困,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏广凸。R本人自食惡果不足惜阅茶,卻給世界環(huán)境...
    茶點故事閱讀 39,930評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望谅海。 院中可真熱鬧脸哀,春花似錦、人聲如沸扭吁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,745評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至谅摄,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間系馆,已是汗流浹背送漠。 一陣腳步聲響...
    開封第一講書人閱讀 31,983評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留由蘑,地道東北人闽寡。 一個月前我還...
    沈念sama閱讀 46,351評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像尼酿,于是被迫代替她去往敵國和親爷狈。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,509評論 2 348

推薦閱讀更多精彩內(nèi)容