關(guān)于HotfixPatch
在IOS開(kāi)發(fā)領(lǐng)域,由于Apple嚴(yán)格的審核標(biāo)準(zhǔn)和低效率,IOS應(yīng)用的發(fā)版速度極慢,稍微大型的app發(fā)版基本上都在一個(gè)月以上,所以代碼熱更新(HotfixPatch)對(duì)于IOS應(yīng)用來(lái)說(shuō)就顯得尤其重要。
現(xiàn)在業(yè)內(nèi)基本上都在使用WaxPatch方案,由于Wax框架已經(jīng)停止維護(hù)四五年了,所以waxPatch在使用過(guò)程中還是存在不少坑(比如參數(shù)轉(zhuǎn)化過(guò)程中的問(wèn)題,如果繼承類(lèi)沒(méi)有實(shí)例化修改繼承類(lèi)的方法無(wú)效, wax_gc中對(duì)oc中instance的持有延遲釋放...)。另外蘋(píng)果對(duì)于Wax使用的態(tài)度也處于模糊狀態(tài),這也是一個(gè)潛在的使用風(fēng)險(xiǎn)。
隨著FaceBook開(kāi)源React Native框架,利用JavaScriptCore.framework直接建立JavaScript(JS)和Objective-C(OC)之間的bridge成為可能,JSPatch也在這個(gè)時(shí)候應(yīng)運(yùn)而生。最開(kāi)始是從唐巧的微信公眾號(hào)推送上了解到,開(kāi)始還以為是在React Native的基礎(chǔ)上進(jìn)行的封裝,不過(guò)最近仔細(xì)研究了源代碼,跟React Native半毛錢(qián)關(guān)系都沒(méi)有,這里先對(duì)JSPatch的作者(不是唐巧,是Bang,博客地址)贊一個(gè)。
深入了解JSPatch之后,第一感覺(jué)是這個(gè)方案小巧,易懂,維護(hù)成本低,直接通過(guò)OC代碼去調(diào)用runtime的API,作為一個(gè)IOS開(kāi)發(fā)者,很快就能看明白,不用花大精力去了解學(xué)習(xí)lua。另外在建立JS和OC的Bridge時(shí),作者很巧妙的利用JS和OC兩種語(yǔ)言的消息轉(zhuǎn)發(fā)機(jī)制做了很優(yōu)雅的實(shí)現(xiàn),稍顯不足的是JSPatch只能支持ios7及以上。
由于現(xiàn)在公司的部分應(yīng)用還在支持ios6,完全取代Wax也不現(xiàn)實(shí),但是一些新上應(yīng)用已經(jīng)直接開(kāi)始支持ios7。個(gè)人覺(jué)得ios6和ios7的界面風(fēng)格差別較大,相信應(yīng)用最低支持版本會(huì)很快升級(jí)到ios7. 還考慮到JSPatch的成熟度不夠,所以決定把JSPatch和WaxPatch結(jié)合在一起,相互補(bǔ)充進(jìn)行使用。下面給大家說(shuō)一些學(xué)習(xí)使用體會(huì)。
JSPatch和WaxPatch對(duì)比
關(guān)于JSPatch對(duì)比WaxPatch的優(yōu)勢(shì),下面摘抄一下JSPatch作者的話:
方案對(duì)比
目前已經(jīng)有一些方案可以實(shí)現(xiàn)動(dòng)態(tài)打補(bǔ)丁,例如WaxPatch,可以用Lua調(diào)用OC方法,相對(duì)于WaxPatch,JSPatch的優(yōu)勢(shì):
1.JS語(yǔ)言: JS比Lua在應(yīng)用開(kāi)發(fā)領(lǐng)域有更廣泛的應(yīng)用,目前前端開(kāi)發(fā)和終端開(kāi)發(fā)有融合的趨勢(shì),作為擴(kuò)展的腳本語(yǔ)言,JS是不二之選。
2.符合Apple規(guī)則: JSPatch更符合Apple的規(guī)則。iOS Developer Program License Agreement里3.3.2提到不可動(dòng)態(tài)下發(fā)可執(zhí)行代碼,但通過(guò)蘋(píng)果JavaScriptCore.framework或WebKit執(zhí)行的代碼除外,JS正是通過(guò)JavaScriptCore.framework執(zhí)行的。
3.小巧: 使用系統(tǒng)內(nèi)置的JavaScriptCore.framework,無(wú)需內(nèi)嵌腳本引擎,體積小巧。
4.支持block: wax在幾年前就停止了開(kāi)發(fā)和維護(hù),不支持Objective-C里block跟Lua程序的互傳,雖然一些第三方已經(jīng)實(shí)現(xiàn)block,但使用時(shí)參數(shù)上也有比較多的限制。
JSPatch的劣勢(shì):
- 相對(duì)于WaxPatch,JSPatch劣勢(shì)在于不支持iOS6,因?yàn)樾枰隞avaScriptCore.framework。另外目前內(nèi)存的使用上會(huì)高于wax,持續(xù)改進(jìn)中。
JSPatch的實(shí)現(xiàn)原理理解
JSPatch的實(shí)現(xiàn)原理作者的博文已經(jīng)很詳細(xì)的介紹了,我這里就不多說(shuō)了,貼一下學(xué)習(xí)之處:
- JSPatch實(shí)現(xiàn)原理詳解 http://blog.cnbang.net/tech/2808/
- JSPatch Git源碼和使用說(shuō)明 https://github.com/bang590/JSPatch
看實(shí)現(xiàn)原理詳解的時(shí)候?qū)φ罩创a看,比較好理解,我在這里說(shuō)一下我對(duì)JSPatch的學(xué)習(xí)和理解:
(1)OC的動(dòng)態(tài)語(yǔ)言特性
不管是WaxPatch框架還是JSPatch的方案,其根本原理都是利用OC的動(dòng)態(tài)語(yǔ)言特性去動(dòng)態(tài)修改類(lèi)的方法實(shí)現(xiàn)。
OC的動(dòng)態(tài)語(yǔ)言特性是在runtime system(全部用C實(shí)現(xiàn),Apple維護(hù)了一份開(kāi)源代碼)上實(shí)現(xiàn)的,面向?qū)ο蟮腃lass和instance機(jī)制都是基于消息機(jī)制。我們平時(shí)認(rèn)為的[object method],正確的理解應(yīng)該是[receiver sendMsg], 所有的消息發(fā)送會(huì)在編譯階段編譯為runtime c函數(shù)的調(diào)用:_obj_sendMsg(id, SEL).
詳細(xì)介紹參考博文:
runtime提供了一些運(yùn)行時(shí)的API
- 反射類(lèi)和選擇器
Class class = NSClassFromString("UIViewController");
SEL selector = NSSelectorFromString("viewDidLoad");
- 為某個(gè)類(lèi)新增或者替換方法選擇器(SEL)的實(shí)現(xiàn)(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中動(dòng)態(tài)注冊(cè)類(lèi)
Class superCls = NSClassFromString(superClassName);
cls = objc_allocateClassPair(superCls, className.UTF8String, 0);
objc_registerClassPair(cls);
(2)JS如何調(diào)用OC
在JS運(yùn)行環(huán)境中,需要解決兩個(gè)問(wèn)題,一個(gè)是OC類(lèi)對(duì)象(objc_class)的獲取,另一個(gè)就是使用對(duì)象提供的接口方法。
對(duì)于第一個(gè)問(wèn)題,JSPatch在實(shí)現(xiàn)中是通過(guò)Require調(diào)用在JS環(huán)境下創(chuàng)建一個(gè)class同名對(duì)象(js形式),當(dāng)向OC發(fā)送alloc接收消息之后,會(huì)將OC環(huán)境中創(chuàng)建的對(duì)象地址保存到這個(gè)這個(gè)js同名對(duì)象中,js本身并不完成任何對(duì)象的初始化。關(guān)于JS持有OC對(duì)象的引用,其回收的解釋在JSPatch作者的博文中有介紹,沒(méi)有具體測(cè)試。詳見(jiàn)JSPatch.js代碼:
//請(qǐng)求OC類(lèi)對(duì)象
UIView = require("UIView");
//緩存JS class同名對(duì)象
var _require = function(clsName) {
if (!global[clsName]) {
global[clsName] = {
__isCls: 1,
__clsName: clsName
}
}
return global[clsName]
}
//調(diào)用class方法,返回OC實(shí)例化對(duì)象進(jìn)行封裝
var ret = instance ? _OC_callI(instance, selectorName, args, isSuper):
_OC_callC(clsName, selectorName, args)
//OC創(chuàng)建后返回對(duì)象
return@{@"__clsName": NSStringFromClass([obj class]), @"__obj": obj};
//JS中解析OC對(duì)象
return _formatOCToJS(ret)
//_formatOCToJS
if (obj instanceof Object) {
var ret = {}
for (var key in obj) {
ret[key] = _formatOCToJS(obj[key])
}
return ret
}
對(duì)于第二個(gè)問(wèn)題,JSPatch在JS環(huán)境中通過(guò)中心轉(zhuǎn)發(fā)方式,所有OC方法的調(diào)用均是通過(guò)新增Object(js)原型方法_c(methodName)完成調(diào)用,在通過(guò)JavaScriptCore執(zhí)行JS腳本之前,先將所有的方法調(diào)用字符替換
_c('method')的方式; 在_c函數(shù)中通過(guò)JSContex建立的橋接函數(shù)傳入?yún)?shù)和返回參數(shù)即完成了調(diào)用;
//字符替換
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轉(zhuǎn)發(fā)調(diào)用參數(shù)
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調(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ù)都是通過(guò)這樣定義
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的主要作用還是通過(guò)腳本修復(fù)一些線上bug,希望能夠達(dá)到替換OC方法的目標(biāo)。JSPatch的實(shí)現(xiàn)巧妙之處在于:利用了OC的消息轉(zhuǎn)發(fā)機(jī)制。
- 1:替換原有selector的IMP實(shí)現(xiàn)為一個(gè)空的IMP實(shí)現(xiàn),這樣當(dāng)objc_class接受到消息之后,就會(huì)進(jìn)行消息轉(zhuǎn)發(fā), 另外需要將selector的初始實(shí)現(xiàn)進(jìn)行保存;
//selector指向空實(shí)現(xiàn)
IMP msgForwardIMP = getEmptyMsgForwardIMP(typeDescription, methodSignature);
class_replaceMethod(cls, selector, msgForwardIMP, typeDescription);
//保存原有實(shí)現(xiàn),這里進(jìn)行了修改,增加了恢復(fù)現(xiàn)場(chǎng)的支持
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方法構(gòu)造一個(gè)JPSelector及其IMP實(shí)現(xiàn)(根據(jù)返回參數(shù)構(gòu)造),添加到當(dāng)前class中,并通過(guò)cls+selecotr全局緩存JS方法(全局緩存并沒(méi)有多大用途,但是對(duì)于后面恢復(fù)現(xiàn)場(chǎng)比較有用);
if (!_JSOverideMethods[clsName][JPSelectorName]) {
_initJPOverideMethods(clsName);
_JSOverideMethods[clsName][JPSelectorName] = function;
const char *returnType = [methodSignature methodReturnType];
IMP JPImplementation = NULL;
//根據(jù)返回類(lèi)型構(gòu)造
switch (returnType[0]){
...
}
if(!class_respondsToSelector(cls, JPSelector)){
class_addMethod(cls, JPSelector, JPImplementation, typeDescription);
} else {
class_replaceMethod(cls, JPSelector, JPImplementation,typeDescription);
}
}
- 3:然后改寫(xiě)每個(gè)替換方法類(lèi)的forwadInvocation的實(shí)現(xiàn)進(jìn)行攔截,如果攔截到的Invocation的selctor轉(zhuǎn)化成JPSelector能夠響應(yīng),說(shuō)明是一個(gè)替換方法,則從Invocation中取參數(shù)后調(diào)用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++) {
...
}
//獲取參數(shù)之后invoke JPSector調(diào)用JSFunction的實(shí)現(xiàn)
@synchronized(_context) {
_TMPInvocationArguments = formatOCToJSList(argList);
[invocation setSelector:JPSelector];
[invocation invoke];
_TMPInvocationArguments = nil;
}
}
Patch現(xiàn)場(chǎng)復(fù)原的補(bǔ)充
Patch現(xiàn)場(chǎng)恢復(fù)的功能主要用于連續(xù)更新腳本的應(yīng)用場(chǎng)景。由于IOS的App應(yīng)用按Home鍵或者被電話中斷的時(shí)候,應(yīng)用實(shí)際上是首先進(jìn)入到后臺(tái)運(yùn)行階段(applicationWillResignActive),當(dāng)我們下次再次使用App的時(shí)候,如果后臺(tái)應(yīng)用沒(méi)有被終止(applicationWillTerminate),那么App不會(huì)走appliation:didFinishLaunchingWithOptions方法,而是會(huì)走(applicationWillEnterForeground)。 對(duì)于這種場(chǎng)景如果我們連續(xù)更新線上腳本,那么第二次腳本更新則無(wú)法保留最開(kāi)始的方法實(shí)現(xiàn),另外恢復(fù)現(xiàn)場(chǎng)功能也有助于我們撤銷(xiāo)線上腳本能夠恢復(fù)應(yīng)用的本身代碼功能。
JSPatch的現(xiàn)場(chǎng)恢復(fù)
本文在JSPatch基礎(chǔ)上添加了現(xiàn)場(chǎng)恢復(fù)功能;源碼地址參考:
- 增加現(xiàn)場(chǎng)恢復(fù)的JSPatchDemo:
https://github.com/philonpang/JSPatch.git
說(shuō)明如下:
(1)在JPEngine.h 中添加了兩個(gè)啟動(dòng)和結(jié)束的調(diào)用函數(shù)如下:
void js_start(NSString* initScript);
void js_end();
(2) JPEngine.m 中調(diào)用函數(shù)的實(shí)現(xiàn)以及恢復(fù)現(xiàn)場(chǎng)對(duì)部分代碼的修改:主要是利用了替換方法和新增方法的cache(_JSOverideMethods, 主要是這個(gè))
//處理替換方法,selector指回最初的IMP,JPSelector和ORIGSelector都指向未實(shí)現(xiàn)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在使用過(guò)程中也會(huì)遇到不少坑,所以雖然這兩個(gè)框架現(xiàn)在雖然都能夠做到新增可執(zhí)行代碼,但是將其應(yīng)用到開(kāi)發(fā)功能組件還不太可取。
比如說(shuō)我在第一次使用JSPatch遇到了一個(gè)坑:(后面想單寫(xiě)一個(gè)博客收集一下我們團(tuán)隊(duì)使用Patch遇到的坑~~)
在JS腳本改寫(xiě)派生類(lèi)中未實(shí)現(xiàn)的繼承類(lèi)的 optional protocol方法時(shí),tableView reload的時(shí)候不會(huì)調(diào)用JS的補(bǔ)丁方法,但是在tableView中顯式調(diào)用可以調(diào)用替換的selector方法;另外如果在派生類(lèi)中重寫(xiě)這個(gè)protocol方法,則可以調(diào)起;
...
先寫(xiě)這么多了,本來(lái)想寫(xiě)一下我們的patch管理方案,覺(jué)得沒(méi)有什么可說(shuō)了,就不寫(xiě)了~