下面主要說兩個(gè)熱更新的原理:
第一個(gè)現(xiàn)在最多的實(shí)現(xiàn)思路,不管是OCRunner還是DynamicOC還是...
第二個(gè)是JSPatch.
這兩個(gè)原理好好看完基本就對(duì)熱更有很好的認(rèn)識(shí)了.
下面會(huì)把項(xiàng)目里面實(shí)現(xiàn)的方式進(jìn)行code.
https://github.com/frankKiwi/HotfixSolve.git
原理
熱修復(fù)的核心原理:
- 攔截目標(biāo)方法調(diào)用,讓其調(diào)用轉(zhuǎn)發(fā)到預(yù)先埋好的特定方法中
- 獲取目標(biāo)方法的調(diào)用參數(shù)
只要完成了上面兩步精偿,你就可以隨心所欲了。在肆意發(fā)揮前,你需要掌握一些 Runtime 的基礎(chǔ)理論,下面進(jìn)入 Runtime 理論速成教程。
Runtime 速成
Runtime 可以在運(yùn)行時(shí)去動(dòng)態(tài)的創(chuàng)建類和方法粪小,因此你可以通過字符串反射的方式去動(dòng)態(tài)調(diào)用OC方法趟畏、動(dòng)態(tài)的替換方法贡歧、動(dòng)態(tài)新增方法等等。下面簡(jiǎn)單介紹下熱修復(fù)所需要用到的 Runtime 知識(shí)點(diǎn)。
Class 反射創(chuàng)建
通過字符串創(chuàng)建類:Class
// 方式1
NSClassFromString(@"NSObject");
// 方式2
objc_getClass("NSObject");
SEL 反射創(chuàng)建
通過字符串創(chuàng)建方法 selector
// 方式1
@selector(init);
// 方式2
sel_registerName("init");
// 方式3
NSSelectorFromString(@"init");
方法替換/交換
- 方法替換:
class_replaceMethod
- 方法交換:
method_exchangeImplementations
// 方法替換
- (void)methodReplace
{
Method methodA = class_getInstanceMethod(self.class, @selector(myMethodA));
IMP impA = method_getImplementation(methodA);
class_replaceMethod(self.class, @selector(myMethodC), impA, method_getTypeEncoding(methodA));
// print: myMethodA
[self myMethodC];
}
// 方法交換
- (void)methodExchange
{
Method methodA = class_getInstanceMethod(self.class, @selector(myMethodA));
Method methodB = class_getInstanceMethod(self.class, @selector(myMethodB));
method_exchangeImplementations(methodA, methodB);
// print: myMethodB
[self myMethodA];
// print: myMethodA
[self myMethodB];
}
- (void)myMethodA
{
NSLog(@"myMethodA");
}
- (void)myMethodB
{
NSLog(@"myMethodB");
}
- (void)myMethodC
{
NSLog(@"myMethodC");
}
新增類
通過字符串動(dòng)態(tài)新增一個(gè)類
- 首先創(chuàng)建新類:
objc_allocateClassPair
- 然后注冊(cè)新創(chuàng)建的類:
objc_registerClassPair
這里有個(gè)小知識(shí)點(diǎn)利朵,為什么類創(chuàng)建的方法名是objc_allocateClassPair
律想,而不是objc_allocateClass
呢?這是因?yàn)樗瑫r(shí)創(chuàng)建了一個(gè)類(class)和元類(metaclass)绍弟。關(guān)于元類可以看這篇文章:What is a meta-class in Objective-C?
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[self addNewClassPair];
Class MyObject = NSClassFromString(@"MyObject");
NSObject *myObj = [[MyObject alloc] init];
[myObj performSelector:@selector(sayHello)];
return YES;
}
- (void)addNewClassPair
{
Class myCls = objc_allocateClassPair([NSObject class], "MyObject", 0);
objc_registerClassPair(myCls);
[self addNewMethodWithClass:myCls];
}
新增方法
新增方法:class_addMethod
這里也有個(gè)小知識(shí)點(diǎn)技即,就是使用特定字符串描述方法返回值和參數(shù)樟遣,例如:v@:
而叼。其具體映射關(guān)系請(qǐng)移步:Type Encodings
void sayHello(id self, SEL _cmd)
{
NSLog(@"%@ %s", self, __func__);
}
- (void)addNewMethodWithClass:(Class)targetClass
{
class_addMethod(targetClass, @selector(sayHello), (IMP)sayHello, "v@:");
}
消息轉(zhuǎn)發(fā)
當(dāng)給對(duì)象發(fā)送消息時(shí),如果對(duì)象沒有找到對(duì)應(yīng)的方法實(shí)現(xiàn)豹悬,那么就會(huì)進(jìn)入正常的消息轉(zhuǎn)發(fā)流程葵陵。其主要流程如下:
// 1.運(yùn)行時(shí)動(dòng)態(tài)添加方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
// 2.快速轉(zhuǎn)發(fā)
- (id)forwardingTargetForSelector:(SEL)aSelector
// 3.構(gòu)建方法簽名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
// 4.消息轉(zhuǎn)發(fā)
- (void)forwardInvocation:(NSInvocation *)anInvocation
其中最后的forwardInvocation:
會(huì)傳遞一個(gè)NSInvocation
對(duì)象(Ps:NSInvocation 可以理解為是消息發(fā)送objc_msgSend(void id self, SEL op, ... )
的對(duì)象)。NSInvocation 包含了這個(gè)方法調(diào)用的所有信息:selector瞻佛、參數(shù)類型埃难、參數(shù)值和返回值類型。此外涤久,你還可以去更改參數(shù)值和返回值涡尘。
除了上面的正常消息轉(zhuǎn)發(fā),我們還可以借助_objc_msgForward
方法讓消息強(qiáng)制轉(zhuǎn)發(fā)
Method methodA = class_getInstanceMethod(self.class, @selector(myMethodA));
IMP msgForwardIMP = _objc_msgForward;
// 替換 myMethodA 的實(shí)現(xiàn)后响迂,每次調(diào)用 myMethodA 都會(huì)進(jìn)入消息轉(zhuǎn)發(fā)
class_replaceMethod(self.class, @selector(myMethodA), msgForwardIMP, method_getTypeEncoding(methodA));
Method 調(diào)用方式
- 常規(guī)調(diào)用
- 反射調(diào)用
- objc_msgSend
- C 函數(shù)調(diào)用
- NSInvocation 調(diào)用
@interface People : NSObject
- (void)helloWorld;
@end
// 常規(guī)調(diào)用
People *people = [[People alloc] init];
[people helloWorld];
// 反射調(diào)用
Class cls = NSClassFromString(@"People");
id obj = [[cls alloc] init];
[obj performSelector:NSSelectorFromString(@"helloWorld")];
// objc_msgSend
((void(*)(id, SEL))objc_msgSend)(people, sel_registerName("helloWorld"));
// C 函數(shù)調(diào)用
Method initMethod = class_getInstanceMethod([People class], @selector(helloWorld));
IMP imp = method_getImplementation(initMethod);
((void (*) (id, SEL))imp)(people, @selector(helloWorld));
// NSInvocation 調(diào)用
NSMethodSignature *sig = [[People class] instanceMethodSignatureForSelector:sel_registerName("helloWorld")];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
invocation.target = people;
invocation.selector = sel_registerName("helloWorld");
[invocation invoke];
第五種 NSInvocation 調(diào)用 是熱修復(fù)調(diào)用任意 OC 方法的核心基礎(chǔ)考抄。通過 NSInvocation 不但可以自定義函數(shù)的參數(shù)值和返回值,而且還可以自定義方法選擇器(selector) 和消息接收對(duì)象(target)蔗彤。因此川梅,我們可以通過字符串的方式構(gòu)建任意 OC 方法調(diào)用。
實(shí)戰(zhàn)
掌握了理論知識(shí)后然遏,實(shí)踐起來就不難了贫途。上面說到熱修復(fù)的核心就是攔截目標(biāo)方法調(diào)用并且拿到方法的參數(shù)值,要實(shí)現(xiàn)這一點(diǎn)其實(shí)很容易待侵。具體步驟如下:
- 首先新增一個(gè)方法實(shí)現(xiàn)跟目標(biāo)方法一致的別名方法丢早,用來調(diào)用原目標(biāo)方法。
- 其次將目標(biāo)方法的函數(shù)實(shí)現(xiàn)(IMP)替換成
_objc_msgForward
秧倾,目的是讓目標(biāo)方法進(jìn)行強(qiáng)制轉(zhuǎn)發(fā) - 最后將目標(biāo)方法類的
forwardInvocation:
方法實(shí)現(xiàn)替換成通用的自定義實(shí)現(xiàn)怨酝,其目的是可以在這個(gè)自定義實(shí)現(xiàn)里面拿到目標(biāo)方法的NSInvocation
對(duì)象。
下面是熱修復(fù)核心代碼的簡(jiǎn)要實(shí)現(xiàn)那先。
實(shí)戰(zhàn)部分給出的示例代碼不考慮異常等情況农猬,只為闡明熱修復(fù)原理
typedef void(^OCDynamicBlock)(id self, NSInvocation *originalInvocation);
@implementation NSObject (OCDynamic)
+ (void)dy_hookSelector:(SEL)selector withBlock:(void(^)(id self, NSInvocation *originalInvocation))block
{
// 保存回調(diào) block
[dynamicBlockMap() setObject:block forKey:NSStringFromSelector(selector)];
// 1.獲取目標(biāo)方法的 IMP
Method targetMethod = class_getInstanceMethod(self, selector);
IMP targetMethodIMP = method_getImplementation(targetMethod);
// 2.新增一個(gè)目標(biāo)方法的別名方法
NSString *aliasSelString = [NSString stringWithFormat:@"oc_dynamic_%@", NSStringFromSelector(selector)];
const char *typeEncoding = method_getTypeEncoding(targetMethod);
BOOL isSuccessed = class_addMethod(self, NSSelectorFromString(aliasSelString), targetMethodIMP, typeEncoding);
NSLog(@"%@ add method successfully: %d", aliasSelString, isSuccessed);
// 3.將目標(biāo)方法實(shí)現(xiàn)替換成 _objc_msgForward
class_replaceMethod(self, selector, (IMP)_objc_msgForward, typeEncoding);
// 4.將目標(biāo)類的 forwardInvocation 替換為自定義 dy_forwardInvocation_center
class_replaceMethod(self, @selector(forwardInvocation:), (IMP)dy_forwardInvocation_center, "v@:@");
}
static NSMutableDictionary<NSString *, OCDynamicBlock>* dynamicBlockMap(void)
{
static NSMutableDictionary *_dynamicBlockMap;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_dynamicBlockMap = [NSMutableDictionary dictionary];
});
return _dynamicBlockMap;
}
static void dy_forwardInvocation_center(id self, SEL _cmd, NSInvocation *anInvocation)
{
// 獲取回調(diào) block
OCDynamicBlock targetBlock = [dynamicBlockMap() objectForKey:NSStringFromSelector(anInvocation.selector)];
// 將 anInvocation 的 sel 設(shè)置為別名 sel
NSString *aliasSelString = [NSString stringWithFormat:@"oc_dynamic_%@", NSStringFromSelector(anInvocation.selector)];
anInvocation.selector = NSSelectorFromString(aliasSelString);
// 調(diào)用回調(diào) block
targetBlock(self, anInvocation);
}
@end
下面是 MyClassC 的實(shí)現(xiàn)代碼
@implementation MyClassC
- (void)sayHelloTo:(NSString *)name
{
NSLog(@"%s: %@", __func__, name);
}
@end
下面是 MyClassC 的測(cè)試代碼
- (void)hookMyClassCMethod
{
[MyClassC dy_hookSelector:@selector(sayHelloTo:) withBlock:^(id _Nonnull self, NSInvocation * _Nonnull originalInvocation) {
__weak id value = nil;
[originalInvocation getArgument:&value atIndex:2];
NSLog(@"%@ %@", NSStringFromSelector(originalInvocation.selector), value);
}];
// 測(cè)試 MyClassC
[[MyClassC new] sayHelloTo:@"jack"];
}
雖然調(diào)用了 [[MyClassC new] sayHelloTo:@"jack"];
,但是你會(huì)發(fā)現(xiàn)并沒有對(duì)應(yīng)的sayHelloTo: jack
日志輸出售淡,而是輸出了:oc_dynamic_sayHelloTo: jack
斤葱。這說明了該方法調(diào)用被成功攔截并且回調(diào)到了對(duì)應(yīng)的 block 中慷垮。至此,我們簡(jiǎn)要的熱修復(fù)功能已實(shí)現(xiàn)了揍堕。是不是很簡(jiǎn)單换帜?
上面的示例代碼都是本地 Hard Code,下面就來聊聊如何動(dòng)態(tài)的 Hook 指定類的方法及改變修改目標(biāo)方法的調(diào)用行為鹤啡。從 MyClassC 的測(cè)試代碼中可以看出,我們可以用字符串反射的方式實(shí)現(xiàn)動(dòng)態(tài) Hook蹲嚣。
[self dy_hookMethodWithHookMap:@{
@"cls": @"MyClassC",
@"sel": @"sayHelloTo:"
}];
// 測(cè)試 MyClassC
[[MyClassC new] sayHelloTo:@"jack"];
- (void)dy_hookMethodWithHookMap:(NSDictionary *)hookMap {
Class cls = NSClassFromString([hookMap objectForKey:@"cls"]);
SEL sel = NSSelectorFromString([hookMap objectForKey:@"sel"]);
[cls dy_hookSelector:sel withBlock:^(id _Nonnull self, NSInvocation * _Nonnull originalInvocation) {
__weak id value = nil;
[originalInvocation getArgument:&value atIndex:2];
NSLog(@"%@ %@", NSStringFromSelector(originalInvocation.selector), value);
}];
}
上面的示例代碼中递瑰,我們只需要構(gòu)建指定規(guī)則的 hookMap 即可實(shí)現(xiàn)動(dòng)態(tài) Hook,我們可以根據(jù)實(shí)際項(xiàng)目實(shí)現(xiàn)一套適合自己的 DSL 語法隙畜。然后解析對(duì)應(yīng)的 DSL 生成 hookMap抖部。
由于我們拿到了目標(biāo)方法調(diào)用的 NSInvocation 對(duì)象,所以我們可以任意的修改方法的參數(shù)值议惰、返回值慎颗、selector 及 target。下面簡(jiǎn)單介紹下如何實(shí)現(xiàn)上面的目標(biāo)言询。
一俯萎、方法替換為空實(shí)現(xiàn)
替換為空實(shí)現(xiàn)其實(shí)很簡(jiǎn)單,就是不處理回調(diào)中的 originalInvocation
即可运杭。
[weakSelf dy_hookMethodWithHookMap:@{
@"cls": @"ViewController",
@"sel": @"myEmptyMethod",
@"isReplcedEmpty": @(YES)
}];
// 將不會(huì)打印 -[ViewController myEmptyMethod]
[weakSelf myEmptyMethod];
[cls dy_hookSelector:sel withBlock:^(id _Nonnull self, NSInvocation * _Nonnull originalInvocation) {
if ([hookMap[@"isReplcedEmpty"] boolValue]) {
NSLog(@"[%@ %@] replace into empty IMP", cls, NSStringFromSelector(sel));
return;
}
}];
二夫啊、方法參數(shù)修改
通過 NSInvocation 的 - (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx
即可修改方法參數(shù)值。例如動(dòng)態(tài)的把 sayHelloTo:
方法的參數(shù)值jack
改為 Lili
辆憔。
知識(shí)點(diǎn):
所有 OC 方法都有兩個(gè)隱藏的參數(shù):第一個(gè)是
self
, 第二個(gè)是selector
撇眯,所以我們?cè)谠O(shè)置參數(shù)值時(shí) index 是從 2 開始的
[weakSelf dy_hookMethodWithHookMap:@{
@"cls": @"MyClassC",
@"sel": @"sayHelloTo:",
@"parameters": @[@"Lili"]
}];
// 打印信息是-[MyClassC sayHelloTo:]: Lili ,而不是 jack
[[MyClassC new] sayHelloTo:@"jack"];
[cls dy_hookSelector:sel withBlock:^(id _Nonnull self, NSInvocation * _Nonnull originalInvocation) {
if ([hookMap[@"isReplcedEmpty"] boolValue]) {
NSLog(@"[%@ %@] replace into empty IMP", cls, NSStringFromSelector(sel));
return;
}
[parameters enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[originalInvocation setArgument:&obj atIndex:idx + 2];
}];
[originalInvocation invoke];
}];
三虱咧、方法返回值修改
通過 NSInvocation 的 - (void)setReturnValue:(void *)retLoc
即可修改方法返回值熊榛。例如將 MyClassC
的 className
方法的返回值改為 Return value had change
- (NSString *)className {
return @"MyClassC";
}
[weakSelf dy_hookMethodWithHookMap:@{
@"cls": @"MyClassC",
@"sel": @"className",
@"returnValue": @"Return value had change"
}];
// 打印信息是 Return value had change ,而不是 MyClassC
[NSLog(@"%@", [[MyClassC new] className]);
[cls dy_hookSelector:sel withBlock:^(id _Nonnull self, NSInvocation * _Nonnull originalInvocation) {
if ([hookMap[@"isReplcedEmpty"] boolValue]) {
NSLog(@"[%@ %@] replace into empty IMP", cls, NSStringFromSelector(sel));
return;
}
[parameters enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[originalInvocation setArgument:&obj atIndex:idx + 2];
}];
[originalInvocation invoke];
id returnValue = [hookMap objectForKey:@"returnValue"];
if (returnValue) {
[originalInvocation setReturnValue:&returnValue];
}
}];
四腕巡、方法調(diào)用前后插入自定義代碼
我們可以在回調(diào) block 中做一些自定義調(diào)用玄坦,等這些完成后再調(diào)用[originalInvocation invoke]
。例如在 myMethod
調(diào)用前調(diào)用 dynamicCallMethod
方法
- (void)dynamicCallMethod {
NSLog(@"%s Dynamic call", __func__);
}
[weakSelf dy_hookMethodWithHookMap:@{
@"cls": @"MyClassC",
@"sel": @"myMethod",
@"customMethods": @[@"self.dynamicCallMethod"]
}];
// 會(huì)先打印 -[MyClassC dynamicCallMethod] Dynamic call绘沉,然后再打印 -[MyClassC myMethod]
[[MyClassC new] myMethod];
[cls dy_hookSelector:sel withBlock:^(id _Nonnull self, NSInvocation * _Nonnull originalInvocation) {
if ([hookMap[@"isReplcedEmpty"] boolValue]) {
NSLog(@"[%@ %@] replace into empty IMP", cls, NSStringFromSelector(sel));
return;
}
[customMethods enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSArray<NSString *> *targets = [obj componentsSeparatedByString:@"."];
id target = nil;
if ([targets.firstObject isEqualToString:@"self"]) {
target = self;
}
SEL sel = NSSelectorFromString(targets.lastObject);
NSMethodSignature *targetSig = [[target class] instanceMethodSignatureForSelector:sel];
NSInvocation *customInvocation = [NSInvocation invocationWithMethodSignature:targetSig];
customInvocation.target = target;
customInvocation.selector = sel;
[customInvocation invoke];
target = nil;
}];
[parameters enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[originalInvocation setArgument:&obj atIndex:idx + 2];
}];
[originalInvocation invoke];
id returnValue = [hookMap objectForKey:@"returnValue"];
if (returnValue) {
[originalInvocation setReturnValue:&returnValue];
}
}];
上面簡(jiǎn)單的闡述了如何通過字符串方式調(diào)用 OC 方法营搅,如果要實(shí)現(xiàn)可以調(diào)用任意 OC 方法,還需要繼續(xù)完善上面的解析邏輯梆砸,但其中核心點(diǎn)都是通過構(gòu)建 NSInvocation
转质。這里算是拋磚引玉吧。
OCDynamic 只是簡(jiǎn)單的實(shí)現(xiàn)了熱修復(fù)的核心邏輯帖世,這是遠(yuǎn)遠(yuǎn)不夠的休蟹。雖然我們可以不斷完善沸枯,但是業(yè)界已經(jīng)有了完善的開源庫(kù):Aspects。Aspects
庫(kù)是OCDynamic
的加強(qiáng)完善版赂弓。因此绑榴,我們只需要站在巨人的肩膀上即可,就沒有必要重復(fù)造輪子了盈魁。下面就來分析下Aspects
的基本原理及其可以優(yōu)化的點(diǎn)翔怎。
Aspects
Aspects 可以攔截目標(biāo)方法調(diào)用,并且將目標(biāo)方法調(diào)用以 NSInvocation 形式返回杨耙。 下面簡(jiǎn)單介紹下其主要構(gòu)成赤套、Hook 流程、Invoke 流程及該庫(kù)存在的一些問題珊膜。
- AspectsContainer:Tracks all aspects for an object/class
- AspectIdentifier:Tracks a single aspect
一容握、Hook 流程
- 檢查 selector 是否可以替換,里面涉及一些黑名單等判斷
- 獲取 AspectsContainer车柠,如果為空則創(chuàng)建并綁定目標(biāo)類
- 創(chuàng)建 AspectIdentifier剔氏,用來保存回調(diào)
block
和AspectOptions
等信息 - 將目標(biāo)類
forwardInvocation:
方法替換為自定義方法(__ASPECTS_ARE_BEING_CALLED__) - 目標(biāo)類新增一個(gè)帶有
aspects_
前綴的方法,新方法(aliasSelector)實(shí)現(xiàn)跟目標(biāo)方法相同 - 將目標(biāo)方法實(shí)現(xiàn)替換為
_objc_msgForward
// 將目標(biāo)類 forwardInvocation: 方法替換為自定義方法
IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
if (originalImplementation) {
class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
}
// 目標(biāo)類新增一個(gè)帶有 aspects_ 前綴的方法竹祷,新方法(aliasSelector)實(shí)現(xiàn)跟目標(biāo)方法相同
Method targetMethod = class_getInstanceMethod(klass, selector);
IMP targetMethodIMP = method_getImplementation(targetMethod);
const char *typeEncoding = method_getTypeEncoding(targetMethod);
SEL aliasSelector = NSSelectorFromString([AspectsMessagePrefix stringByAppendingFormat:@"_%@", NSStringFromSelector(selector)]);
class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding);
// 將目標(biāo)方法實(shí)現(xiàn)替換為 _objc_msgForward
class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
二谈跛、Invoke 流程
- 調(diào)用目標(biāo)方法進(jìn)入消息轉(zhuǎn)發(fā)流程
- 調(diào)用自定義
__ASPECTS_ARE_BEING_CALLED__
方法 - 獲取對(duì)應(yīng) invocation,將 invocation.selector 設(shè)置為 aliasSelector
- 通過 aliasSelector 獲取對(duì)應(yīng) AspectsContainer
- 根據(jù) AspectOptions 調(diào)用用戶自定實(shí)現(xiàn)(目標(biāo)方法調(diào)用前/后/替換)
三塑陵、Aspects 優(yōu)化
- 使用了自旋鎖币旧,存在優(yōu)先級(jí)反轉(zhuǎn)問題,使用
pthread_mutex_lock
代替即可 - 特殊
struct
判斷邏輯不夠全面猿妈,例如 NSRange, NSPoint 等在 x86-64 位架構(gòu)下有問題吹菱,需要自行兼容
#if defined(__LP64__) && __LP64__
if (valueSize == 16) {
methodReturnsStructValue = NO;
}
#endif
- 類方法無法直接 hook, 不過可以 hook 其
Meta class
元類方式進(jìn)行解決
object_getClass(targetCls)
- 無法同時(shí) hook 一個(gè)類的實(shí)例方法和類方法,原因是使用了相同的
swizzledClasse
key, 解決如下:
static Class aspect_swizzleClassInPlace(Class klass) {
NSCParameterAssert(klass);
NSString *className = [NSString stringWithFormat:@"%@_%p", NSStringFromClass(klass), klass];
_aspect_modifySwizzledClasses(^(NSMutableSet *swizzledClasses) {
if (![swizzledClasses containsObject:className]) {
aspect_swizzleForwardInvocation(klass);
[swizzledClasses addObject:className];
}
});
return klass;
}
static void aspect_undoSwizzleClassInPlace(Class klass) {
NSCParameterAssert(klass);
NSString *className = [NSString stringWithFormat:@"%@_%p", NSStringFromClass(klass), klass];
_aspect_modifySwizzledClasses(^(NSMutableSet *swizzledClasses) {
if ([swizzledClasses containsObject:className]) {
aspect_undoSwizzleForwardInvocation(klass);
[swizzledClasses removeObject:className];
}
});
}
NSInvocation 的坑
NSInvocation 在取其參數(shù)值和返回值的時(shí)候需要注意內(nèi)存管理的問題彭则,下面介紹下在實(shí)際開發(fā)中所遇到的問題鳍刷。
一、EXC_BAD_ACCESS
從 -forwardInvocation:
里的 NSInvocation
對(duì)象取參數(shù)值時(shí)俯抖,若參數(shù)值是id類型输瓜,一般會(huì)這樣取:
id value = nil;
[invocation getArgument:&value atIndex:2];
但是這種寫法存在 EXC_BAD_ACCESS
風(fēng)險(xiǎn)。例如:Hook NSMutableArray 的 insertObject:atIndex: 方法。你會(huì)發(fā)現(xiàn)在有些系統(tǒng)調(diào)用會(huì)出現(xiàn)野指針崩潰
[NSClassFromString(@"__NSArrayM") aspect_hookSelector:@selector(insertObject:atIndex:) withOptions:AspectPositionInstead usingBlock:^(id<AspectInfo> info){
id value = nil;
[info.originalInvocation getArgument:&value atIndex:2];
if (value) {
[info.originalInvocation invoke];
}
} error:NULL];
開啟 Zombie objects
下的異常打印
-[UITraitCollection retain]: message sent to deallocated instance 0x6000007cde00
原因分析:
- NSInvocation 不會(huì)引用參數(shù)捻悯,詳情可以看官方文檔(This class does not retain the arguments for the contained invocation by default)
- ARC 在隱式賦值不會(huì)自動(dòng)插入 retain 語句。在
[info.originalInvocation getArgument:&value atIndex:2];
中北戏,因?yàn)?value 是通過指針賦值(隱式賦值),所以 ARC 機(jī)制并不生效(具體可以參考:ARC - Retainable object pointers section)漫蛔,這也導(dǎo)致了 value 沒有調(diào)用retain
方法 - ARC 下
id value
相當(dāng)于__strong id vaule
嗜愈,__strong
類型的變量會(huì)在當(dāng)前作用域結(jié)束后自動(dòng)調(diào)用release
方法進(jìn)行釋放旧蛾。其實(shí)現(xiàn)如下所示:
void objc_storeStrong(id *object, id value) {
id oldValue = *object;
value = [value retain];
*object = value;
[oldValue release];
}
綜上所述可以得出:value 并沒有持有參數(shù)對(duì)象但又對(duì)參數(shù)對(duì)象進(jìn)行釋放,這導(dǎo)致參數(shù)對(duì)象被提前釋放蠕嫁。如果此時(shí)再對(duì)該對(duì)象發(fā)送消息則會(huì)發(fā)生野指針崩潰
解決辦法:
1锨天、將 value 變成 __unsafe_unretained
或 __weak
,讓 ARC 在它退出作用域時(shí)不插入 release 語句
__unsafe_unretained id value = nil;
2剃毒、通過 __bridge
轉(zhuǎn)換讓 value 持有返回對(duì)象病袄,顯示賦值
id value = nil;
void *result;
[invocation getArgument:&result atIndex:2];
value = (__bridge id)result;
二、Memory Leak
使用 NSInvocation
調(diào)用alloc/new/copy/mutableCopy
方法時(shí)會(huì)發(fā)生內(nèi)存泄漏赘阀,示例如下:
- (void)memoryLeakA
{
NSMethodSignature *signature = [NSObject methodSignatureForSelector:@selector(new)];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.target = MyClassA.class;
invocation.selector = @selector(new);
[invocation invoke];
}
- (void)memoryLeakB
{
[MyClassB performSelector:@selector(new)];
}
使用 Memory Graph 查看對(duì)象內(nèi)存時(shí)會(huì)發(fā)現(xiàn) MyClassA
和 MyClassB
都被標(biāo)記為內(nèi)存泄漏了
原因分析:
ARC 機(jī)制中益缠,當(dāng)調(diào)用 alloc/new/copy/mutableCopy
方法返回的對(duì)象是直接持有的,其引用計(jì)數(shù)為1
纤壁。在常規(guī)的方法調(diào)用時(shí)編譯器會(huì)自動(dòng)調(diào)用 release,而使用NSInvocation
或performSelector:
動(dòng)態(tài)調(diào)用alloc/new/copy/mutableCopy
方法時(shí)捺信,ARC 并不會(huì)自動(dòng)調(diào)用release
酌媒,所以導(dǎo)致內(nèi)存泄漏。
謹(jǐn)記:
ARC 對(duì)動(dòng)態(tài)方法調(diào)用是無能為力的??
溫馨提示:
有興趣的可以 Xcode 看看這兩種方式的匯編實(shí)現(xiàn)?? (Product -> Perform Action -> Assemble)
解決辦法:
- 使用
__bridge_transfer
修飾符將返回對(duì)象的內(nèi)存管理權(quán)移交出來迄靠,讓外部對(duì)象管理其內(nèi)存
// 方法1
id resultObj = nil;
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.target = [NSObject class];
invocation.selector = @selector(new);
[invocation invoke];
void *result;
[invocation getReturnValue:&result];
if ([selName isEqualToString:@"alloc"] ||
[selName isEqualToString:@"new"] ||
[selName isEqualToString:@"copy"] ||
[selName isEqualToString:@"mutableCopy"]) {
resultObj = (__bridge_transfer id)result;
} else {
resultObj = (__bridge id)result;
}
- 采用常規(guī)方法調(diào)用代替 NSInvocation
// 方法2
id resultObj = nil;
if ([selName isEqualToString:@"alloc"]) {
resultObj = [[target class] alloc];
} else if ([selName isEqualToString:@"new"]) {
resultObj = [[target class] new];
} else if ([selName isEqualToString:@"copy"]) {
resultObj = [target copy];
} else if ([selName isEqualToString:@"mutableCopy"]) {
resultObj = [target mutableCopy];
} else {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.target = [NSObject class];
invocation.selector = @selector(new);
[invocation invoke];
void *result;
[invocation getReturnValue:&result];
resultObj = (__bridge id)result;
}
審核分析
其實(shí)能不能成功上線是熱修復(fù)的首要前提秒咨,我們辛辛苦苦開的框架如果上不了線,那一切都是徒勞無功掌挚。下面就來分析下其審核風(fēng)險(xiǎn)雨席。
- 首先這個(gè)是我們自研的,所以蘋果審核無法通過靜態(tài)代碼掃描識(shí)別吠式。
- 其次系統(tǒng)庫(kù)內(nèi)部也大量使用了消息轉(zhuǎn)發(fā)機(jī)制陡厘。可以通過符號(hào)斷點(diǎn)驗(yàn)證
_objc_msgForward
和forwardInvocation:
特占。所以不存在風(fēng)險(xiǎn)糙置。 - 蘋果無法采用動(dòng)態(tài)檢驗(yàn)消息轉(zhuǎn)發(fā),非系統(tǒng)調(diào)用都不能使用是目,這個(gè)成本太大了谤饭,幾乎不可能。
- Aspects 庫(kù)目前線上有大量使用懊纳,為此不用擔(dān)心揉抵。就算 Aspects 被禁用,參考 Aspects 自己實(shí)現(xiàn)也不難嗤疯。
綜上所述:無審核風(fēng)險(xiǎn)冤今。
當(dāng)然熱修復(fù)框架只是為了更好的控制線上 bug 影響范圍和給用戶更好的體驗(yàn)。
后記
隨著項(xiàng)目的業(yè)務(wù)復(fù)雜度增加茂缚,線上問題可能存在一些 C 函數(shù)的動(dòng)態(tài)調(diào)用和 block 參數(shù)的修改辟汰,這邊介紹一個(gè)強(qiáng)大的庫(kù)列敲,外部函數(shù)接口:libffi,它也可以攔截函數(shù)和獲取函數(shù)調(diào)用參數(shù)帖汞。相比 Aspects戴而,其功能更加強(qiáng)大,不但可以動(dòng)態(tài)調(diào)用 C 函數(shù)翩蘸,而且還可以用 libffi 實(shí)現(xiàn)一套基于 IMP 替換(擁有更好的性能)的熱修復(fù)框架所意。有興趣的童鞋請(qǐng)參考:libffi doc 和 如何動(dòng)態(tài)調(diào)用 C 函數(shù)
一.JSPatch (可用于企業(yè)發(fā)包,TF發(fā)包使用,App Store可以自己試著魔改一下)
下面下說下原理:
JSPatch 實(shí)現(xiàn)原理詳解
大綱
基礎(chǔ)原理
方法調(diào)用
1.require
2.JS接口
i.封裝 JS 對(duì)象
ii.__c()
元函數(shù)
3.消息傳遞
4.對(duì)象持有/轉(zhuǎn)換
5.類型轉(zhuǎn)換
方法替換
1.基礎(chǔ)原理
2.va_list實(shí)現(xiàn)(32位)
3.ForwardInvocation實(shí)現(xiàn)(64位)
4.新增方法
i.方案
ii.Protocol
5.Property實(shí)現(xiàn)
6.self關(guān)鍵字
7.super關(guān)鍵字
擴(kuò)展
1.Struct 支持
2.C 函數(shù)支持
細(xì)節(jié)
1.Special Struct
2.內(nèi)存問題
i.Double Release
ii.內(nèi)存泄露
3.‘_’的處理
4.JPBoxing
5.nil 的處理
i.區(qū)分NSNull/nil
ii.鏈?zhǔn)秸{(diào)用
總結(jié)
基礎(chǔ)原理
JSPatch 能做到通過 JS 調(diào)用和改寫 OC 方法最根本的原因是 Objective-C 是動(dòng)態(tài)語言,OC 上所有方法的調(diào)用/類的生成都通過 Objective-C Runtime 在運(yùn)行時(shí)進(jìn)行催首,我們可以通過類名/方法名反射得到相應(yīng)的類和方法:
Class class = NSClassFromString("UIViewController");
id viewController = [[class alloc] init];
SEL selector = NSSelectorFromString("viewDidLoad");
[viewController performSelector:selector];
也可以替換某個(gè)類的方法為新的實(shí)現(xiàn):
static void newViewDidLoad(id slf, SEL sel) {}
class_replaceMethod(class, selector, newViewDidLoad, @"");
還可以新注冊(cè)一個(gè)類扶踊,為類添加方法:
Class cls = objc_allocateClassPair(superCls, "JPObject", 0);
class_addMethod(cls, selector, implement, typedesc);
objc_registerClassPair(cls);
對(duì)于 Objective-C 對(duì)象模型和動(dòng)態(tài)消息發(fā)送的原理已有很多文章闡述得很詳細(xì),這里就不詳細(xì)闡述了郎任。理論上你可以在運(yùn)行時(shí)通過類名/方法名調(diào)用到任何 OC 方法秧耗,替換任何類的實(shí)現(xiàn)以及新增任意類。所以 JSPatch 的基本原理就是:JS 傳遞字符串給 OC舶治,OC 通過 Runtime 接口調(diào)用和替換 OC 方法分井。這是最基礎(chǔ)的原理,實(shí)際實(shí)現(xiàn)過程還有很多怪要打霉猛,接下來看看具體是怎樣實(shí)現(xiàn)的尺锚。
方法調(diào)用
require('UIView')
var view = UIView.alloc().init()
view.setBackgroundColor(require('UIColor').grayColor())
view.setAlpha(0.5)
引入 JSPatch 后,可以通過以上 JS 代碼創(chuàng)建了一個(gè) UIView 實(shí)例惜浅,并設(shè)置背景顏色和透明度瘫辩,涵蓋了 require 引入類,JS 調(diào)用接口坛悉,消息傳遞伐厌,對(duì)象持有和轉(zhuǎn)換,參數(shù)轉(zhuǎn)換這五個(gè)方面裸影,接下來逐一看看具體實(shí)現(xiàn)弧械。
1.require
調(diào)用 require('UIView') 后,就可以直接使用 UIView 這個(gè)變量去調(diào)用相應(yīng)的類方法了空民,require 做的事很簡(jiǎn)單刃唐,就是在JS全局作用域上創(chuàng)建一個(gè)同名變量,變量指向一個(gè)對(duì)象界轩,對(duì)象屬性 __clsName 保存類名画饥,同時(shí)表明這個(gè)對(duì)象是一個(gè) OC Class。
var _require = function(clsName) {
if (!global[clsName]) {
global[clsName] = {
__clsName: clsName
}
}
return global[clsName]
}
所以調(diào)用 require('UIView') 后浊猾,就在全局作用域生成了 UIView 這個(gè)變量抖甘,指向一個(gè)這樣一個(gè)對(duì)象:
{
__clsName: "UIView"
}
2.JS****接口
接下來看看 UIView.alloc() 是怎樣調(diào)用的。
i.****封裝 JS 對(duì)象
對(duì)于這個(gè)調(diào)用的實(shí)現(xiàn)葫慎,一開始我的想法是衔彻,根據(jù)JS特性薇宠,若要讓 UIView.alloc() 這句調(diào)用不出錯(cuò),唯一的方法就是給 UIView 這個(gè)對(duì)象添加 alloc 方法艰额,不然是不可能調(diào)用成功的澄港,JS 對(duì)于調(diào)用沒定義的屬性/變量,只會(huì)馬上拋出異常柄沮,而不像 OC/Lua/ruby 那樣會(huì)有轉(zhuǎn)發(fā)機(jī)制回梧。所以做了一個(gè)復(fù)雜的事,就是在require生成類對(duì)象時(shí)祖搓,把類名傳入OC狱意,OC 通過runtime方法找出這個(gè)類所有的方法返回給 JS,JS 類對(duì)象為每個(gè)方法名都生成一個(gè)函數(shù)拯欧,函數(shù)內(nèi)容就是拿著方法名去 OC 調(diào)用相應(yīng)方法详囤。生成的 UIView 對(duì)象大致是這樣的:
{
__clsName: "UIView",
alloc: function() {…},
beginAnimations_context: function() {…},
setAnimationsEnabled: function(){…},
...
}
實(shí)際上不僅要遍歷當(dāng)前類的所有方法,還要循環(huán)找父類的方法直到頂層镐作,整個(gè)繼承鏈上的所有方法都要加到JS對(duì)象上藏姐,一個(gè)類就有幾百個(gè)方法,這樣把方法全部加到 JS 對(duì)象上滑肉,碰到了挺嚴(yán)重的問題包各,引入幾個(gè)類就內(nèi)存暴漲摘仅,無法使用靶庙。后來為了優(yōu)化內(nèi)存問題還在 JS 搞了繼承關(guān)系,不把繼承鏈上所有方法都添加到一個(gè)JS對(duì)象娃属,避免像基類 NSObject 的幾百個(gè)方法反復(fù)添加在每個(gè) JS 對(duì)象上六荒,每個(gè)方法只存在一份,JS 對(duì)象復(fù)制了 OC 對(duì)象的繼承關(guān)系矾端,找方法時(shí)沿著繼承鏈往上找掏击,結(jié)果內(nèi)存消耗是小了一些,但還是大到難以接受秩铆。
ii.****__c()****元函數(shù)
當(dāng)時(shí)繼續(xù)苦苦尋找解決方案砚亭,若按 JS 語法,這是唯一的方法殴玛,但若不按 JS 語法呢捅膘?突然腦洞開了下,CoffieScript/JSX 都可以用 JS 實(shí)現(xiàn)一個(gè)解釋器實(shí)現(xiàn)自己的語法滚粟,我也可以通過類似的方式做到寻仗,再進(jìn)一步想到其實(shí)我想要的效果很簡(jiǎn)單,就是調(diào)用一個(gè)不存在方法時(shí)凡壤,能轉(zhuǎn)發(fā)到一個(gè)指定函數(shù)去執(zhí)行署尤,就能解決一切問題了耙替,這其實(shí)可以用簡(jiǎn)單的字符串替換,把 JS 腳本里的方法調(diào)用都替換掉曹体。最后的解決方案是俗扇,在 OC 執(zhí)行 JS 腳本前,通過正則把所有方法調(diào)用都改成調(diào)用 __c() 函數(shù)混坞,再執(zhí)行這個(gè) JS 腳本狐援,做到了類似 OC/Lua/Ruby 等的消息轉(zhuǎn)發(fā)機(jī)制:
UIView.alloc().init()
->
UIView.__c('alloc')().__c('init')()
給 JS 對(duì)象基類 Object 加上 __c 成員,這樣所有對(duì)象都可以調(diào)用到 __c究孕,根據(jù)當(dāng)前對(duì)象類型判斷進(jìn)行不同操作:
Object.defineProperty(Object.prototype, '__c', {value: function(methodName) {
if (!this.__obj && !this.__clsName) return this[methodName].bind(this);
var self = this
return function(){
var args = Array.prototype.slice.call(arguments)
return _methodFunc(self.__obj, self.__clsName, methodName, args, self.__isSuper)
}
}})
_methodFunc() 就是把相關(guān)信息傳給OC啥酱,OC用 Runtime 接口調(diào)用相應(yīng)方法,返回結(jié)果值厨诸,這個(gè)調(diào)用就結(jié)束了镶殷。
這樣做不用去 OC 遍歷對(duì)象方法,不用在 JS 對(duì)象保存這些方法微酬,內(nèi)存消耗直降 99%绘趋,這一步是做這個(gè)項(xiàng)目最爽的時(shí)候,用一個(gè)非常簡(jiǎn)單的方法解決了嚴(yán)重的問題颗管,替換之前又復(fù)雜效果又差的實(shí)現(xiàn)陷遮。
3.****消息傳遞
解決了 JS 接口問題,接下來看看 JS 和 OC 是怎樣互傳消息的垦江。這里用到了 JavaScriptCore 的接口帽馋,OC 端在啟動(dòng) JSPatch 引擎時(shí)會(huì)創(chuàng)建一個(gè) JSContext 實(shí)例,JSContext 是 JS 代碼的執(zhí)行環(huán)境比吭,可以給 JSContext 添加方法绽族,JS 就可以直接調(diào)用這個(gè)方法:
JSContext *context = [[JSContext alloc] init];
context[@"hello"] = ^(NSString *msg) {
NSLog(@"hello %@", msg);
};
[_context evaluateScript:@"hello('word')"]; //output hello word
JS 通過調(diào)用 JSContext 定義的方法把數(shù)據(jù)傳給 OC,OC 通過返回值傳會(huì)給 JS衩藤。調(diào)用這種方法吧慢,它的參數(shù)/返回值 JavaScriptCore 都會(huì)自動(dòng)轉(zhuǎn)換,OC 里的 NSArray, NSDictionary, NSString, NSNumber, NSBlock 會(huì)分別轉(zhuǎn)為JS端的數(shù)組/對(duì)象/字符串/數(shù)字/函數(shù)類型赏表。上述 _methodFunc 方法就是這樣把要調(diào)用的類名和方法名傳遞給 OC 的检诗。
4.****對(duì)象持有****/****轉(zhuǎn)換
結(jié)合上述幾點(diǎn),可以知道 UIView.alloc() 這個(gè)類方法調(diào)用語句是怎樣執(zhí)行的:
a.require('UIView') 這句話在 JS 全局作用域生成了 UIView 這個(gè)對(duì)象瓢剿,它有個(gè)屬性叫 __isCls砾赔,表示這代表一個(gè) OC 類犹撒。 b.調(diào)用 UIView 這個(gè)對(duì)象的 alloc() 方法碱茁,會(huì)去到 __c() 函數(shù)官脓,在這個(gè)函數(shù)里判斷到調(diào)用者 __isCls 屬性,知道它是代表 OC 類,把方法名和類名傳遞給 OC 完成調(diào)用坠韩。
調(diào)用類方法過程是這樣距潘,那實(shí)例方法呢?UIView.alloc() 會(huì)返回一個(gè) UIView 實(shí)例對(duì)象給 JS只搁,這個(gè) OC 實(shí)例對(duì)象在 JS 是怎樣表示的音比?怎樣可以在 JS 拿到這個(gè)實(shí)例對(duì)象后可以直接調(diào)用它的實(shí)例方法 UIView.alloc().init()?
對(duì)于一個(gè)自定義id對(duì)象氢惋,JavaScriptCore 會(huì)把這個(gè)自定義對(duì)象的指針傳給 JS洞翩,這個(gè)對(duì)象在 JS 無法使用,但在回傳給 OC 時(shí) OC 可以找到這個(gè)對(duì)象焰望。對(duì)于這個(gè)對(duì)象生命周期的管理骚亿,按我的理解如果JS有變量引用時(shí),這個(gè) OC 對(duì)象引用計(jì)數(shù)就加1 熊赖,JS 變量的引用釋放了就減1来屠,如果 OC 上沒別的持有者,這個(gè)OC對(duì)象的生命周期就跟著 JS 走了震鹉,會(huì)在 JS 進(jìn)行垃圾回收時(shí)釋放俱笛。
傳回給 JS 的變量是這個(gè) OC 對(duì)象的指針,這個(gè)指針也可以重新傳回 OC传趾,要在 JS 調(diào)用這個(gè)對(duì)象的某個(gè)實(shí)例方法迎膜,根據(jù)第2點(diǎn) JS 接口的描述,只需在 __c() 函數(shù)里把這個(gè)對(duì)象指針以及它要調(diào)用的方法名傳回給 OC 就行了浆兰,現(xiàn)在問題只剩下:怎樣在 __c() 函數(shù)里判斷調(diào)用者是一個(gè) OC 對(duì)象指針磕仅?
目前沒找到方法判斷一個(gè) JS 對(duì)象是否表示 OC 指針,這里的解決方法是在 OC 把對(duì)象返回給 JS 之前镊讼,先把它包裝成一個(gè) NSDictionary:
static NSDictionary *_wrapObj(id obj) {
return @{@"__obj": obj};
}
讓 OC 對(duì)象作為這個(gè) NSDictionary 的一個(gè)值宽涌,這樣在 JS 里這個(gè)對(duì)象就變成:
{__obj: [OC Object 對(duì)象指針]}
這樣就可以通過判斷對(duì)象是否有 __obj 屬性得知這個(gè)對(duì)象是否表示 OC 對(duì)象指針平夜,在 __c 函數(shù)里若判斷到調(diào)用者有 __obj 屬性蝶棋,取出這個(gè)屬性,跟調(diào)用的實(shí)例方法一起傳回給 OC忽妒,就完成了實(shí)例方法的調(diào)用玩裙。
5.****類型轉(zhuǎn)換
JS 把要調(diào)用的類名/方法名/對(duì)象傳給 OC 后,OC 調(diào)用類/對(duì)象相應(yīng)的方法是通過 NSInvocation 實(shí)現(xiàn)段直,要能順利調(diào)用到方法并取得返回值吃溅,要做兩件事:
a.取得要調(diào)用的 OC 方法各參數(shù)類型,把 JS 傳來的對(duì)象轉(zhuǎn)為要求的類型進(jìn)行調(diào)用鸯檬。 b.根據(jù)返回值類型取出返回值决侈,包裝為對(duì)象傳回給 JS。
例如開頭例子的 view.setAlpha(0.5)喧务, JS傳遞給OC的是一個(gè) NSNumber赖歌,OC 需要通過要調(diào)用 OC 方法的 NSMethodSignature 得知這里參數(shù)要的是一個(gè) float 類型值枉圃,于是把 NSNumber 轉(zhuǎn)為 float 值再作為參數(shù)進(jìn)行 OC 方法調(diào)用。這里主要處理了 int/float/bool 等數(shù)值類型庐冯,并對(duì) CGRect/CGRange 等類型進(jìn)行了特殊轉(zhuǎn)換處理孽亲,剩下的就是實(shí)現(xiàn)細(xì)節(jié)了。
方法替換
JSPatch 可以用 defineClass 接口任意替換一個(gè)類的方法展父,方法替換的實(shí)現(xiàn)過程也是頗為曲折返劲,一開始是用 va_list 的方式獲取參數(shù),結(jié)果發(fā)現(xiàn) arm64 下不可用栖茉,只能轉(zhuǎn)而用另一種 hack 方式繞道實(shí)現(xiàn)篮绿。另外在給類新增方法、實(shí)現(xiàn)property吕漂、支持self/super關(guān)鍵字上也費(fèi)了些功夫搔耕,下面逐個(gè)說明。
1.****基礎(chǔ)原理
OC上痰娱,每個(gè)類都是這樣一個(gè)結(jié)構(gòu)體:
struct objc_class {
struct objc_class * isa;
const char *name;
….
struct objc_method_list methodLists; /方法鏈表/
};
其中 methodList 方法鏈表里存儲(chǔ)的是 Method 類型:
typedef struct objc_method *Method;
typedef struct objc_ method {
SEL method_name;
char *method_types;
IMP method_imp;
};
Method 保存了一個(gè)方法的全部信息弃榨,包括 SEL 方法名,type 各參數(shù)和返回值類型梨睁,IMP 該方法具體實(shí)現(xiàn)的函數(shù)指針鲸睛。
通過 Selector 調(diào)用方法時(shí),會(huì)從 methodList 鏈表里找到對(duì)應(yīng)Method進(jìn)行調(diào)用坡贺,這個(gè) methodList 上的的元素是可以動(dòng)態(tài)替換的官辈,可以把某個(gè) Selector 對(duì)應(yīng)的函數(shù)指針I(yè)MP替換成新的,也可以拿到已有的某個(gè) Selector 對(duì)應(yīng)的函數(shù)指針I(yè)MP遍坟,讓另一個(gè) Selector 跟它對(duì)應(yīng)拳亿,Runtime 提供了一些接口做這些事,以替換 UIViewController 的 -viewDidLoad: 方法為例:
static void viewDidLoadIMP (id slf, SEL sel) {
JSValue *jsFunction = …;
[jsFunction callWithArguments:nil];
}
Class cls = NSClassFromString(@"UIViewController");
SEL selector = @selector(viewDidLoad);
Method method = class_getInstanceMethod(cls, selector);
//獲得viewDidLoad方法的函數(shù)指針
IMP imp = method_getImplementation(method)
//獲得viewDidLoad方法的參數(shù)類型
char *typeDescription = (char *)method_getTypeEncoding(method);
//新增一個(gè)ORIGViewDidLoad方法愿伴,指向原來的viewDidLoad實(shí)現(xiàn)
class_addMethod(cls, @selector(ORIGViewDidLoad), imp, typeDescription);
//把viewDidLoad IMP指向自定義新的實(shí)現(xiàn)
class_replaceMethod(cls, selector, viewDidLoadIMP, typeDescription);
這樣就把 UIViewController 的 -viewDidLoad 方法給替換成我們自定義的方法肺魁,APP里調(diào)用 UIViewController 的 viewDidLoad 方法都會(huì)去到上述 viewDidLoadIMP 函數(shù)里,在這個(gè)新的IMP函數(shù)里調(diào)用 JS 傳進(jìn)來的方法隔节,就實(shí)現(xiàn)了替換 viewDidLoad 方法為JS代碼里的實(shí)現(xiàn)鹅经,同時(shí)為 UIViewController 新增了個(gè)方法 -ORIGViewDidLoad 指向原來 viewDidLoad 的 IMP,JS 可以通過這個(gè)方法調(diào)用到原來的實(shí)現(xiàn)怎诫。
方法替換就這樣很簡(jiǎn)單的實(shí)現(xiàn)了瘾晃,但這么簡(jiǎn)單的前提是,這個(gè)方法沒有參數(shù)幻妓。如果這個(gè)方法有參數(shù)蹦误,怎樣把參數(shù)值傳給我們新的 IMP 函數(shù)呢?例如 UIViewController 的 -viewDidAppear: 方法,調(diào)用者會(huì)傳一個(gè) Bool 值强胰,我們需要在自己實(shí)現(xiàn)的IMP(上述的 viewDidLoadIMP)上拿到這個(gè)值尚镰,怎樣能拿到?如果只是針對(duì)一個(gè)方法寫 IMP哪廓,是可以直接拿到這個(gè)參數(shù)值的:
static void viewDidAppear (id slf, SEL sel, BOOL animated) {
[function callWithArguments:@(animated)];
}
但我們要的是實(shí)現(xiàn)一個(gè)通用的IMP狗唉,任意方法任意參數(shù)都可以通過這個(gè)IMP中轉(zhuǎn),拿到方法的所有參數(shù)回調(diào)JS的實(shí)現(xiàn)涡真。
2.va_list****實(shí)現(xiàn)****(32****位****)
最初我是用可變參數(shù) va_list 實(shí)現(xiàn):
static void commonIMP(id slf, ...)
va_list args;
va_start(args, slf);
NSMutableArray *list = [[NSMutableArray alloc] init];
NSMethodSignature *methodSignature = [cls instanceMethodSignatureForSelector:selector];
NSUInteger numberOfArguments = methodSignature.numberOfArguments;
id obj;
for (NSUInteger i = 2; i < numberOfArguments; i++) {
const char *argumentType = [methodSignature getArgumentTypeAtIndex:i];
switch(argumentType[0]) {
case 'I':
obj = @(va_arg(args, int));
break;
case 'B':
obj = @(va_arg(args, BOOL));
break;
case 'f':
case 'd':
obj = @(va_arg(args, double));
break;
…… //其他數(shù)值類型
default: {
obj = va_arg(args, id);
break;
}
}
[list addObject:obj];
}
va_end(args);
[function callWithArguments:list];
}
這樣無論方法參數(shù)是什么分俯,有多少個(gè),都可以通過 va_list的一組方法一個(gè)個(gè)取出來哆料,組成 NSArray 在調(diào)用 JS 方法時(shí)傳回缸剪。很完美地解決了參數(shù)的問題,一直運(yùn)行正常东亦,直到我跑在 arm64 的機(jī)子上測(cè)試杏节,一調(diào)用就 crash。查了資料典阵,才發(fā)現(xiàn) arm64 下 va_list 的結(jié)構(gòu)改變了奋渔,導(dǎo)致無法上述這樣取參數(shù)。詳見這篇文章壮啊。
3.ForwardInvocation****實(shí)現(xiàn)****(64****位****)
繼續(xù)尋找解決方案嫉鲸,最后找到另一種非常 hack 的方法解決參數(shù)獲取的問題,利用了 OC 消息轉(zhuǎn)發(fā)機(jī)制歹啼。
當(dāng)調(diào)用一個(gè) NSObject 對(duì)象不存在的方法時(shí)玄渗,并不會(huì)馬上拋出異常,而是會(huì)經(jīng)過多層轉(zhuǎn)發(fā)狸眼,層層調(diào)用對(duì)象的 -resolveInstanceMethod:, -forwardingTargetForSelector:, -methodSignatureForSelector:, -forwardInvocation: 等方法藤树,其中最后 -forwardInvocation: 是會(huì)有一個(gè) NSInvocation 對(duì)象,這個(gè) NSInvocation 對(duì)象保存了這個(gè)方法調(diào)用的所有信息拓萌,包括 Selector 名岁钓,參數(shù)和返回值類型,最重要的是有所有參數(shù)值司志,可以從這個(gè) NSInvocation 對(duì)象里拿到調(diào)用的所有參數(shù)值甜紫。我們可以想辦法讓每個(gè)需要被 JS 替換的方法調(diào)用最后都調(diào)到 -forwardInvocation:降宅,就可以解決無法拿到參數(shù)值的問題了骂远。
具體實(shí)現(xiàn),以替換 UIViewController 的 -viewWillAppear: 方法為例:
- 把UIViewController的 -viewWillAppear: 方法通過 class_replaceMethod() 接口指向 _objc_msgForward腰根,這是一個(gè)全局 IMP瘸恼,OC 調(diào)用方法不存在時(shí)都會(huì)轉(zhuǎn)發(fā)到這個(gè) IMP 上靠闭,這里直接把方法替換成這個(gè) IMP檩淋,這樣調(diào)用這個(gè)方法時(shí)就會(huì)走到 -forwardInvocation:。
- 為UIViewController添加 -ORIGviewWillAppear: 和 -_JPviewWillAppear: 兩個(gè)方法,前者指向原來的IMP實(shí)現(xiàn)瑞侮,后者是新的實(shí)現(xiàn)季俩,稍后會(huì)在這個(gè)實(shí)現(xiàn)里回調(diào)JS函數(shù)消痛。
- 改寫UIViewController的 -forwardInvocation: 方法為自定義實(shí)現(xiàn)纱新。一旦OC里調(diào)用 UIViewController 的 -viewWillAppear: 方法遇汞,經(jīng)過上面的處理會(huì)把這個(gè)調(diào)用轉(zhuǎn)發(fā)到 -forwardInvocation: 簿废,這時(shí)已經(jīng)組裝好了一個(gè) NSInvocation捏鱼,包含了這個(gè)調(diào)用的參數(shù)执庐。在這里把參數(shù)從 NSInvocation 反解出來,帶著參數(shù)調(diào)用上述新增加的方法 -_JPviewWillAppear: 藏斩,在這個(gè)新方法里取到參數(shù)傳給JS,調(diào)用JS的實(shí)現(xiàn)函數(shù)兆览。整個(gè)調(diào)用過程就結(jié)束了屈溉,整個(gè)過程圖示如下:
[圖片上傳失敗...(image-21864a-1629207517253)]
最后一個(gè)問題,我們把 UIViewController 的 -forwardInvocation: 方法的實(shí)現(xiàn)給替換掉了抬探,如果程序里真有用到這個(gè)方法對(duì)消息進(jìn)行轉(zhuǎn)發(fā)子巾,原來的邏輯怎么辦?首先我們?cè)谔鎿Q -forwardInvocation: 方法前會(huì)新建一個(gè)方法 -ORIGforwardInvocation:小压,保存原來的實(shí)現(xiàn)IMP线梗,在新的 -forwardInvocation: 實(shí)現(xiàn)里做了個(gè)判斷,如果轉(zhuǎn)發(fā)的方法是我們想改寫的怠益,就走我們的邏輯仪搔,若不是,就調(diào) -ORIGforwardInvocation: 走原來的流程溉痢。
其他就是實(shí)現(xiàn)上的細(xì)節(jié)了僻造,例如需要根據(jù)不同的返回值類型生成不同的 IMP憋他,要在各處處理參數(shù)轉(zhuǎn)換等孩饼。
4.****新增方法
i.****方案
在 JSPatch 剛開源時(shí)髓削,是不支持為一個(gè)類新增方法的,因?yàn)橛X得能替換原生方法就夠了镀娶,新的方法純粹添加在 JS 對(duì)象上立膛,只在 JS 端跑就行了。另外 OC 為類新增方法需要知道各個(gè)參數(shù)和返回值的類型梯码,需要在 JS 定一種方式把這些類型傳給 OC 才能完成新增方法宝泵,比較麻煩。后來挺多人比較關(guān)注這個(gè)問題轩娶,不能新增方法導(dǎo)致 action-target 模式無法用儿奶,我也開始想有沒有更好的方法實(shí)現(xiàn)添加方法。后來的解決方案是所有類型都用 id 表示鳄抒,因?yàn)榉凑略龅姆椒ǘ际?JS 在用(Protocol定義的方法除外)闯捎,不如新增的方法返回值和參數(shù)全統(tǒng)一成 id 類型,這樣就不用傳類型了许溅。
現(xiàn)在 defineClass 定義的方法會(huì)經(jīng)過 JS 包裝蒋情,變成一個(gè)包含參數(shù)個(gè)數(shù)和方法實(shí)體的數(shù)組傳給OC你画,OC會(huì)判斷如果方法已存在,就執(zhí)行替換的操作,若不存在壁榕,就調(diào)用 class_addMethod() 新增一個(gè)方法,通過傳過來的參數(shù)個(gè)數(shù)和方法實(shí)體生成新的 Method卧檐,把 Method 的參數(shù)和返回值類型都設(shè)為id切蟋。這里 JS 調(diào)用新增方法走的流程還是 forwardInvocation 這一套。
ii.Protocol
對(duì)于新增的方法還有個(gè)問題滚停,若某個(gè)類實(shí)現(xiàn)了某 protocol盹憎,protocol方法里有可選的方法,它的參數(shù)不全是 id 類型铐刘,例如 UITableViewDataSource 的一個(gè)方法:
- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index;
若原類沒有實(shí)現(xiàn)這個(gè)方法陪每,在 JS 里實(shí)現(xiàn)了,會(huì)走到新增方法的邏輯镰吵,每個(gè)參數(shù)類型都變成 id檩禾,與這個(gè) protocol 方法不匹配,產(chǎn)生錯(cuò)誤疤祭。
這里就需要在 JS 定義類時(shí)給出實(shí)現(xiàn)的 protocol盼产,這樣在新增 Protocol 里已定義的方法時(shí),參數(shù)類型會(huì)按照 Protocol 里的定義去實(shí)現(xiàn)勺馆,Protocol 的定義方式跟 OC 上的寫法一致:
defineClass("JPViewController: UIViewController <UIAlertViewDelegate>", {
alertView_clickedButtonAtIndex: function(alertView, buttonIndex) {
console.log('clicked index ' + buttonIndex)
}
})
實(shí)現(xiàn)方式比較簡(jiǎn)單戏售,先把 Protocol 名解析出來侨核,當(dāng) JS 定義的方法在原有類上找不到時(shí),再通過 objc_getProtocol 和 protocol_copyMethodDescriptionList runtime 接口把 Protocol 對(duì)應(yīng)的方法取出來灌灾,若匹配上搓译,則按其方法的定義走方法替換的流程。
5.Property****實(shí)現(xiàn)
若要在 JS 操作 OC 對(duì)象上已定義好的 property锋喜,只需要像調(diào)用普通 OC 方法一樣些己,調(diào)用這個(gè)對(duì)象的 get/set 方法就行了:
//OC
@property (nonatomic) NSString *data;
@property (nonatomic) BOOL *succ;
//JS
self.setSucc(1);
var str = self.data();
若要?jiǎng)討B(tài)給 OC 對(duì)象新增 property,則要另辟蹊徑:
defineClass('JPTableViewController : UITableViewController', {
dataSource: function() {
var data = self.getProp('data')
if (data) return data;
data = [1,2,3]
self.setProp_forKey(data, 'data')
return data;
}
}
JSPatch可以通過 -getProp:嘿般, -setProp:forKey: 這兩個(gè)方法給對(duì)象動(dòng)態(tài)添加成員變量段标。實(shí)現(xiàn)上用了運(yùn)行時(shí)關(guān)聯(lián)接口 objc_getAssociatedObject() 和 objc_setAssociatedObject() 模擬,相當(dāng)于把一個(gè)對(duì)象跟當(dāng)前對(duì)象self關(guān)聯(lián)起來炉奴,以后可以通過當(dāng)前對(duì)象self找到這個(gè)對(duì)象逼庞,跟成員的效果一樣,只是一定得是id對(duì)象類型瞻赶。
本來OC有 class_addIvar() 可以為類添加成員赛糟,但必須在類注冊(cè)之前添加完,注冊(cè)完成后無法添加共耍,這意味著可以為在JS新增的類添加成員虑灰,但不能為OC上已存在的類添加,所以只能用上述方法模擬痹兜。
6.self****關(guān)鍵字
defineClass("JPViewController: UIViewController", {
viewDidLoad: function() {
var view = self.view()
...
},
}
JSPatch支持直接在defineClass里的實(shí)例方法里直接使用 self 關(guān)鍵字穆咐,跟OC一樣 self 是指當(dāng)前對(duì)象,這個(gè) self 關(guān)鍵字是怎樣實(shí)現(xiàn)的呢字旭?實(shí)際上這個(gè)self是個(gè)全局變量对湃,在 defineClass 里對(duì)實(shí)例方法進(jìn)行了包裝,在調(diào)用實(shí)例方法之前遗淳,會(huì)把全局變量 self 設(shè)為當(dāng)前對(duì)象拍柒,調(diào)用完后設(shè)回空,就可以在執(zhí)行實(shí)例方法的過程中使用 self 變量了屈暗。這是一個(gè)小小的trick拆讯。
7.super****關(guān)鍵字
defineClass("JPViewController: UIViewController", {
viewDidLoad: function() {
self.super().viewDidLoad()
},
}
OC 里 super 是一個(gè)關(guān)鍵字,無法通過動(dòng)態(tài)方法拿到 super养叛,那么 JSPatch 的 super 是怎么實(shí)現(xiàn)的种呐?實(shí)際上調(diào)用 super 的方法,OC 做的事是調(diào)用父類的某個(gè)方法弃甥,并把當(dāng)前對(duì)象當(dāng)成 self 傳入父類方法爽室,我們只要模擬它這個(gè)過程就行了。
首先 JS 端需要告訴OC想調(diào)用的是當(dāng)前對(duì)象的 super 方法淆攻,做法是調(diào)用 self.super()時(shí)阔墩,__c 函數(shù)會(huì)做特殊處理嘿架,返回一個(gè)新的對(duì)象,這個(gè)對(duì)象同樣保存了 OC 對(duì)象的引用啸箫,同時(shí)標(biāo)識(shí) __isSuper = 1耸彪。
...
if (methodName == 'super') {
return function() {
return {__obj: self.__obj, __clsName: self.__clsName, __isSuper: 1}
}
}
...
再用這個(gè)返回的對(duì)象去調(diào)用方法時(shí),__c 函數(shù)會(huì)把 __isSuper 這個(gè)標(biāo)識(shí)位傳給 OC筐高,告訴 OC 要調(diào) super 的方法搜囱。OC 做的事情是丑瞧,如果是調(diào)用 super 方法柑土,找到 superClass 這個(gè)方法的 IMP 實(shí)現(xiàn),為當(dāng)前類新增一個(gè)方法指向 super 的 IMP 實(shí)現(xiàn)绊汹,那么調(diào)用這個(gè)類的新方法就相當(dāng)于調(diào)用 super 方法稽屏。把要調(diào)用的方法替換成這個(gè)新方法,就完成 super 方法的調(diào)用了西乖。
static id callSelector(NSString *className, NSString *selectorName, NSArray *arguments, id instance, BOOL isSuper) {
...
if (isSuper) {
NSString *superSelectorName = [NSString stringWithFormat:@"SUPER_%@", selectorName];
SEL superSelector = NSSelectorFromString(superSelectorName);
Class superCls = [cls superclass];
Method superMethod = class_getInstanceMethod(superCls, selector);
IMP superIMP = method_getImplementation(superMethod);
class_addMethod(cls, superSelector, superIMP, method_getTypeEncoding(superMethod));
selector = superSelector;
}
...
}
擴(kuò)展
1.Struct 支持
struct 類型在 JS 與 OC 間傳遞需要做轉(zhuǎn)換處理狐榔,一開始 JSPatch 只處理了原生的 NSRange / CGRect / CGSize / CGPoint 這四個(gè),其他 struct 類型無法在 OC / JS 間傳遞获雕。對(duì)于其他類型的 struct 支持薄腻,我是采用擴(kuò)展的方式,讓寫擴(kuò)展的人手動(dòng)處理每個(gè)要支持的 struct 進(jìn)行類型轉(zhuǎn)換届案,這種做法沒問題庵楷,但需要在 OC 代碼寫好這些擴(kuò)展,無法動(dòng)態(tài)添加楣颠,轉(zhuǎn)換的實(shí)現(xiàn)也比較繁瑣尽纽。于是轉(zhuǎn)為另一種實(shí)現(xiàn):
/*
struct JPDemoStruct {
CGFloat a;
long b;
double c;
BOOL d;
}
*/
require('JPEngine').defineStruct({
"name": "JPDemoStruct",
"types": "FldB",
"keys": ["a", "b", "c", "d"]
})
可以在 JS 動(dòng)態(tài)定義一個(gè)新的 struct,只需提供 struct 名童漩,每個(gè)字段的類型以及每個(gè)字段對(duì)應(yīng)的在 JS 的鍵值弄贿,就可以支持這個(gè) struct 類型在 JS 和 OC 間傳遞了:
//OC
@implementation JPObject
(void)passStruct:(JPDemoStruct)s;
(JPDemoStruct)returnStruct;
@end
//JS
require('JPObject').passStruct({a:1, b:2, c:4.2, d:1})
var s = require('JPObject').returnStruct();
這里的實(shí)現(xiàn)原理是順序去取 struct 里每個(gè)字段的值,再根據(jù) key 重新包裝成 NSDictionary 傳給 JS矫膨,怎樣順序取 struct 每字段的值呢差凹?可以根據(jù)傳進(jìn)來的 struct 字段的變量類型,拿到類型對(duì)應(yīng)的長(zhǎng)度侧馅,順序拷貝出 struct 對(duì)應(yīng)位置和長(zhǎng)度的值危尿,具體實(shí)現(xiàn):
for (int i = 0; i < types.count; i ++) {
size_t size = sizeof(types[i]); //types[i] 是 float double int 等類型
void *val = malloc(size);
memcpy(val, structData + position, size);
position += size;
}
struct 從 JS 到 OC 的轉(zhuǎn)換同理,只是反過來施禾,先生成整個(gè) struct 大小的內(nèi)存地址(通過 struct 所有字段類型大小累加)脚线,再逐漸取出 JS 傳過來的值進(jìn)行類型轉(zhuǎn)換拷貝到這端內(nèi)存里。
這種做法效果很好弥搞,JS 端要用一個(gè)新的 struct 類型邮绿,不需要 OC 事先定義好渠旁,可以直接動(dòng)態(tài)添加新的 struct 類型支持,但這種方法依賴 struct 各個(gè)字段在內(nèi)存空間上的嚴(yán)格排列船逮,如果某些機(jī)器在底層實(shí)現(xiàn)上對(duì) struct 的字段進(jìn)行一些字節(jié)對(duì)齊之類的處理顾腊,這種方式?jīng)]法用了,不過目前在 iOS 上還沒碰到這樣的問題挖胃。
2.C 函數(shù)支持
C 函數(shù)沒法通過反射去調(diào)用杂靶,所以只能通過手動(dòng)轉(zhuǎn)接的方式讓 JS 調(diào) C 方法,具體就是通過 JavaScriptCore 的方法在 JS 當(dāng)前作用域上定義一個(gè) C 函數(shù)同名方法酱鸭,在這個(gè)方法實(shí)現(xiàn)里調(diào)用 C 函數(shù)吗垮,以支持 memcpy() 為例:
context[@"memcpy"] = ^(JSValue *des, JSValue *src, size_t n) {
memcpy(des, src, n);
};
這樣就可以在 JS 調(diào)用 memcpy() 函數(shù)了。實(shí)際上這里還有參數(shù) JS <-> OC 轉(zhuǎn)換問題凹髓,這里先忽略烁登。
這里有兩個(gè)問題:
a.如果這些 C 函數(shù)的支持都寫在 JSPatch 源文件里,源文件會(huì)非常龐大蔚舀。 b.如果一開始就給 JS 加這些函數(shù)定義饵沧,若要支持的 C 函數(shù)量大時(shí)會(huì)影響性能。
對(duì)此設(shè)計(jì)了一種擴(kuò)展的方式去解決這兩個(gè)問題赌躺,JSPatch 需要做的就是為外部提供 JS 運(yùn)行上下文 JSContext狼牺,以及參數(shù)轉(zhuǎn)換的方法,最終設(shè)計(jì)出來的擴(kuò)展接口是這樣:
@interface JPExtension : NSObject
(void)main:(JSContext *)context;
(void *)formatPointerJSToOC:(JSValue *)val;
(id)formatPointerOCToJS:(void *)pointer;
(id)formatJSToOC:(JSValue *)val;
(id)formatOCToJS:(id)obj;
@end
+main 方法暴露了 JSPatch 的運(yùn)行環(huán)境 JSContext 給外部礼患,可以自由在這個(gè) JSContext 上加函數(shù)是钥。另外四個(gè) formatXXX 方法都是參數(shù)轉(zhuǎn)換方法。上述的 memcpy() 完整的擴(kuò)展定義如下:
@implementation JPMemory
- (void)main:(JSContext *)context
{
context[@"memcpy"] = ^id(JSValue *des, JSValue *src, size_t n) {
void *ret = memcpy([self formatPointerJSToOC:des], [self formatPointerJSToOC:src], n);
return [self formatPointerOCToJS:ret];
};
}
@end
同時(shí) JSPatch 提供了 +addExtensions: 接口讶泰,讓 JS 端可以動(dòng)態(tài)加載某個(gè)擴(kuò)展咏瑟,在需要的時(shí)候再給 JS 上下文添加這些 C 函數(shù):
require('JPEngine').addExtensions(['JPMemory'])
實(shí)際上還有另一種方法添加 C 函數(shù)的支持,就是定義 OC 方法轉(zhuǎn)接:
@implementation JPCFunctions
- (void)memcpy:(void *)des src:(void *)src n:(size_t)n {
memcpy(des, src, n);
}
@end
然后直接在 JS 上這樣調(diào):
require('JPFunctions').memcpy_src_n(des, src, n);
這樣的做法不需要擴(kuò)展機(jī)制痪署,也不需要在實(shí)現(xiàn)時(shí)進(jìn)行參數(shù)轉(zhuǎn)換码泞,但因?yàn)樗叩氖?OC runtime 那一套,相比擴(kuò)展直接調(diào)用的方式狼犯,速度慢了一倍余寥,為了更好的性能,還是提供一套擴(kuò)展接口悯森。
細(xì)節(jié)
整個(gè) JSPatch 的基礎(chǔ)原理上面大致闡述完了宋舷,接下來在看看一些實(shí)現(xiàn)上碰到的坑和的細(xì)節(jié)問題。
1.Special Struct
上文提到會(huì)把要覆蓋的方法指向_objc_msgForward瓢姻,進(jìn)行轉(zhuǎn)發(fā)操作祝蝠,這里出現(xiàn)一個(gè)問題,如果替換方法的返回值是某些 struct,使用 _objc_msgForward(或者之前的 @selector(__JPNONImplementSelector))會(huì) crash绎狭。幾經(jīng)輾轉(zhuǎn)细溅,找到了解決方法:對(duì)于某些架構(gòu)某些 struct,必須使用 _objc_msgForward_stret 代替 _objc_msgForward儡嘶。為什么要用 _objc_msgForward_stret 呢喇聊,找到一篇說明 objc_msgSend_stret 和 objc_msgSend 區(qū)別的文章:說得比較清楚,原理是一樣的蹦狂,是C的一些底層機(jī)制的原因誓篱,簡(jiǎn)單復(fù)述一下:
大多數(shù) CPU 在執(zhí)行 C 函數(shù)時(shí)會(huì)把前幾個(gè)參數(shù)放進(jìn)寄存器里,對(duì) obj_msgSend 來說前兩個(gè)參數(shù)固定是 self / _cmd凯楔,它們會(huì)放在寄存器上窜骄,在最后執(zhí)行完后返回值也會(huì)保存在寄存器上,取這個(gè)寄存器的值就是返回值:
-(int) method:(id)arg;
r3 = self
r4 = _cmd, @selector(method:)
r5 = arg
(on exit) r3 = returned int
普通的返回值(int/pointer)很小啼辣,放在寄存器上沒問題啊研,但有些 struct 是很大的御滩,寄存器放不下鸥拧,所以要用另一種方式,在一開始申請(qǐng)一段內(nèi)存削解,把指針保存在寄存器上富弦,返回值往這個(gè)指針指向的內(nèi)存寫數(shù)據(jù),所以寄存器要騰出一個(gè)位置放這個(gè)指針氛驮,self / _cmd 在寄存器的位置就變了:
-(struct st) method:(id)arg;
r3 = &struct_var (in caller's stack frame)
r4 = self
r5 = _cmd, @selector(method:)
r6 = arg
(on exit) return value written into struct_var
objc_msgSend 不知道 self / _cmd 的位置變了腕柜,所以要用另一個(gè)方法 objc_msgSend_stret 代替。原理大概就是這樣矫废。
上面說某些架構(gòu)某些 struct 有問題盏缤,那具體是哪些呢?iOS 架構(gòu)中非 arm64 的都有這問題蓖扑,而怎樣的 struct 需要走上述流程用 xxx_stret 代替原方法則沒有明確的規(guī)則唉铜,OC 也沒有提供接口,只有在一個(gè)奇葩的接口上透露了這個(gè)天機(jī)律杠,于是有這樣一個(gè)神奇的判斷:
if ([methodSignature.debugDescription rangeOfString:@"is special struct return? YES"].location != NSNotFound)
在 NSMethodSignature 的 debugDescription 上打出了是否 special struct潭流,只能通過這字符串判斷。所以最終的處理是柜去,在非 arm64 下灰嫉,是 special struct 就走 _objc_msgForward_stret,否則走 _objc_msgForward嗓奢。
2.****內(nèi)存問題
i.Double Release
實(shí)現(xiàn)過程中碰到一些內(nèi)存問題讼撒,首先是 Double Release 問題。從 -forwardInvocation: 里的 NSInvocation 對(duì)象取參數(shù)值時(shí)睦裳,若參數(shù)值是id類型二庵,我們會(huì)這樣取:
id arg;
[invocation getArgument:&arg atIndex:i];
但這樣的寫法會(huì)導(dǎo)致 crash,這是因?yàn)? id arg 在ARC下相當(dāng)于 __strong id arg比藻,若這時(shí)在代碼顯式為 arg 賦值郑象,根據(jù) ARC 的機(jī)制贡这,會(huì)自動(dòng)插入一條 retain 語句,然后在退出作用域時(shí)插入 release 語句:
- (void)method {
id arg = [SomeClass getSomething];
// [arg retain]
...
// [arg release] 退出作用域前release
}
但我們這里不是顯式對(duì) arg 進(jìn)行賦值厂榛,而是傳入 -getArgument:atIndex: 方法盖矫,在這里面賦值后 ARC 沒有自動(dòng)給這個(gè)變量插入 retain 語句,但退出作用域時(shí)還是自動(dòng)插入了 release 語句击奶,導(dǎo)致這個(gè)變量多釋放了一次辈双,導(dǎo)致 crash。解決方法是把 arg 變量設(shè)成 __unsafe_unretained 或 __weak柜砾,讓 ARC 不在它退出作用域時(shí)插入 release 語句即可:
__unsafe_unretained id arg;
[invocation getReturnValue:&arg];
還可以通過 __bridge 轉(zhuǎn)換讓局部變量持有返回對(duì)象湃望,這樣做也是沒問題的:
id returnValue;
void *result;
[invocation getReturnValue:&result];
returnValue = (__bridge id)result;
ii.****內(nèi)存泄露
Double Release 的問題解決了,又碰到內(nèi)存泄露的坑痰驱。某天 github issue 上有人提對(duì)象生成后沒有釋放证芭,幾經(jīng)排查,定位到還是這里 NSInvocation getReturnValue 的問題担映,當(dāng) NSInvocation 調(diào)用的是 alloc 時(shí)废士,返回的對(duì)象并不會(huì)釋放,造成內(nèi)存泄露蝇完,只有把返回對(duì)象的內(nèi)存管理權(quán)移交出來官硝,讓外部對(duì)象幫它釋放才行:
id returnValue;
void *result;
[invocation getReturnValue:&result];
if ([selectorName isEqualToString:@"alloc"] || [selectorName isEqualToString:@"new"]) {
returnValue = (__bridge_transfer id)result;
} else {
returnValue = (__bridge id)result;
}
這是因?yàn)?ARC 對(duì)方法名有約定,當(dāng)方法名開頭是 alloc / new / copy / mutableCopy 時(shí)短蜕,返回的對(duì)象是 retainCount = 1 的氢架,除此之外,方法返回的對(duì)象都是 autorelease 的朋魔,按上一節(jié)的說法岖研,對(duì)于普通方法返回值,ARC 會(huì)在賦給 strong 變量時(shí)自動(dòng)插入 retain 語句铺厨,但對(duì)于 alloc 等這些方法缎玫,不會(huì)再自動(dòng)插入 retain 語句:
id obj = [SomeObject alloc];
//alloc 方法返回的對(duì)象 retainCount 已 +1,這里不需要retain
id obj2 = [SomeObj someMethod];
//方法返回的對(duì)象是 autorelease解滓,ARC 會(huì)再這里自動(dòng)插入 [obj2 retain] 語句
而 ARC 并沒有處理非顯式調(diào)用時(shí)的情況赃磨,這里動(dòng)態(tài)調(diào)用這些方法時(shí),ARC 都不會(huì)自動(dòng)插入 retain洼裤,這種情況下邻辉,alloc / new 等這類方法返回值的 retainCount 是會(huì)比其他方法返回值多1的,所以需要特殊處理這類方法。
3.‘_’****的處理
JSPatch 用下劃線’_’連接OC方法多個(gè)參數(shù)間的間隔:
- (void)setObject:(id)anObject forKey:(id)aKey;
<==>
setObject_forKey()
那如果OC方法名里含有’_’值骇,那就出現(xiàn)歧義了:
- (void)set_object:(id)anObject forKey:(id)aKey;
<==>
set_object_forKey()
沒法知道 set_object_forKey 對(duì)應(yīng)的 selector 是 set_object:forKey: 還是 set:object:forKey:莹菱。
對(duì)此需要定個(gè)規(guī)則,在 JS 用其他字符代替 OC 方法名里的 _吱瘩。JS 命名規(guī)則除了字母和數(shù)字道伟,就只有 代替了使碾,但效果很丑:
(void)set_object:(id)anObject forKey:(id)aKey;
(void)_privateMethod();
<==>
set$object_forKey()
$privateMethod()
于是嘗試另一種方法蜜徽,用兩個(gè)下劃線 __ 代替:
set__object_forKey()
__privateMethod()
但用兩個(gè)下劃線代替有個(gè)問題,OC 方法名參數(shù)后面加下劃線會(huì)匹配不到
- (void)setObject_:(id)anObject forKey:(id)aKey;
<==>
setObject___forKey()
實(shí)際上 setObject___forKey() 匹配到對(duì)應(yīng)的 selector 是 setObject:_forKey:票摇。雖然有這個(gè)坑拘鞋,但因?yàn)楹苌僖姷竭@種奇葩的命名方式,感覺問題不大矢门,使用 字符的盆色,最終為了代碼顏值,使用了雙下劃線 __ 表示祟剔。
4.JPBoxing
在使用 JSPatch 過程中發(fā)現(xiàn)JS無法調(diào)用 NSMutableArray / NSMutableDictionary / NSMutableString 的方法去修改這些對(duì)象的數(shù)據(jù)隔躲,因?yàn)檫@三者都在從 OC 返回到 JS 時(shí) JavaScriptCore 把它們轉(zhuǎn)成了 JS 的 Array / Object / String,在返回的時(shí)候就脫離了跟原對(duì)象的聯(lián)系峡扩,這個(gè)轉(zhuǎn)換在 JavaScriptCore 里是強(qiáng)制進(jìn)行的蹭越,無法選擇。
若想要在對(duì)象返回 JS 后教届,回到 OC 還能調(diào)用這個(gè)對(duì)象的方法,就要阻止 JavaScriptCore 的轉(zhuǎn)換驾霜,唯一的方法就是不直接返回這個(gè)對(duì)象案训,而是對(duì)這個(gè)對(duì)象進(jìn)行封裝,JPBoxing 就是做這個(gè)事情的:
@interface JPBoxing : NSObject
@property (nonatomic) id obj;
@end
@implementation JPBoxing
- (instancetype)boxObj:(id)obj
{
JPBoxing *boxing = [[JPBoxing alloc] init];
boxing.obj = obj;
return boxing;
}
把 NSMutableArray / NSMutableDictionary / NSMutableString 對(duì)象作為 JPBoxing 的成員保存在 JPBoxing 實(shí)例對(duì)象上返回給 JS粪糙,JS 拿到的是 JPBoxing 對(duì)象的指針强霎,再傳回給 OC 時(shí)就可以通過對(duì)象成員取到原來的 NSMutableArray / NSMutableDictionary / NSMutableString 對(duì)象,類似于裝箱/拆箱操作蓉冈,這樣就避免了這些對(duì)象被 JavaScriptCore 轉(zhuǎn)換城舞。
實(shí)際上只有可變的 NSMutableArray / NSMutableDictionary / NSMutableString 這三個(gè)類有必要調(diào)用它的方法去修改對(duì)象里的數(shù)據(jù),不可變的 NSArray / NSDictionary / NSString 是沒必要這樣做的寞酿,直接轉(zhuǎn)為 JS 對(duì)應(yīng)的類型使用起來會(huì)更方便家夺,但為了規(guī)則簡(jiǎn)單,JSPatch 讓 NSArray / NSDictionary / NSString 也同樣以封裝的方式返回伐弹,避免在調(diào)用 OC 方法返回對(duì)象時(shí)還需要關(guān)心它返回的是可變還是不可變對(duì)象拉馋。最后整個(gè)規(guī)則還是挺清晰:NSArray / NSDictionary / NSString 及其子類與其他 NSObject 對(duì)象的行為一樣,在 JS 上拿到的都只是其對(duì)象指針,可以調(diào)用它們的 OC 方法煌茴,若要把這三種對(duì)象轉(zhuǎn)為對(duì)應(yīng)的 JS 類型随闺,使用額外的 .toJS() 的接口去轉(zhuǎn)換。
對(duì)于參數(shù)和返回值是C指針和 Class 類型的支持同樣是用 JPBoxing 封裝的方式蔓腐,把指針和 Class 作為成員保存在 JPBoxing 對(duì)象上返回給 JS矩乐,傳回 OC 時(shí)再解出來拿到原來的指針和 Class,這樣 JSPatch 就支持所有數(shù)據(jù)類型 OC<->JS 的互傳了回论。
5.nil****的處理
i.****區(qū)分****NSNull/nil
對(duì)于"空"的表示绰精,JS 有 null / undefined,OC 有 nil / NSNull透葛,JavaScriptCore 對(duì)這些參數(shù)傳遞處理是這樣的:
- 從 JS 到 OC萨蚕,直接傳遞 null / undefined 到 OC 都會(huì)轉(zhuǎn)為 nil,若傳遞包含 null / undefined 的 Array 給 OC认轨,會(huì)轉(zhuǎn)為 NSNull霍掺。
- 從 OC 到 JS举娩,nil 會(huì)轉(zhuǎn)為 null纹烹,NSNull 與普通 NSObject 一樣返回指針音念。
JSPatch 的流程上都是通過數(shù)組的方式把參數(shù)從 JS 傳入 OC,這樣所有的 null / undefined 到 OC 就都變成了 NSNull芋齿,而真正的 NSNull 對(duì)象傳進(jìn)來也是 NSNull赢赊,無法分辨從 JS 過來實(shí)際傳的是什么,需要有種方式區(qū)分這兩者级历。
考慮過在 JS 用一個(gè)特殊的對(duì)象代表 nil释移,null / undefined 只用來表示 NSNull,后來覺得 NSNull 是很少手動(dòng)傳遞的變量寥殖,而 null / undefined 以及 OC 的 nil 卻很常見玩讳,這樣做會(huì)給日常開發(fā)帶來很大不便。于是反過來扛禽,在 JS 用一個(gè)特殊變量 nsnull 表示 NSNull锋边,其他 null / undefined 表示 nil,這樣傳入 OC 就可以分辨出 nil 和 NSNull编曼,具體使用方式:
@implementation JPObject
- (void)testNil:(id)obj
{
NSLog(@"%@", obj);
}
@end
require("JPObject").testNil(null) //output: nil
require("JPObject").testNil(nsnull) //output: NSNull
這樣做有個(gè)小坑豆巨,就是顯式使用 NSNull.null() 作為參數(shù)調(diào)用時(shí),到 OC 后會(huì)變成 nil:
require("JPObject").testNil(require("NSNull").null()) //output: nil
這個(gè)只需注意下用 nsnull 代替就行掐场,從 OC 返回的 NSNull 再回傳回去還是可以識(shí)別到 NSNull往扔。
ii.****鏈?zhǔn)秸{(diào)用
第二個(gè)問題,nil 在 JS 里用 null / undefined 表示熊户,造成的后果是無法用 nil 調(diào)用方法萍膛,也就無法保證鏈?zhǔn)秸{(diào)用的安全:
@implementation JPObject
- (void)returnNil
{
return nil;
}
@end
[[JPObject returnNil] hash] //it’s OK
require("JPObject").returnNil().hash() //crash
原因是在 JS 里 null / undefined 不是對(duì)象,無法調(diào)用任何方法嚷堡,包括我們給所有對(duì)象加的 __c() 方法蝗罗。解決方式一度覺得只有回到上面說的,用一個(gè)特殊的對(duì)象表示 nil蝌戒,才能解決這個(gè)問題了串塑。但使用特殊的對(duì)象表示 nil,后果就是在 js 判斷是否為 nil 時(shí)就要很啰嗦:
//假設(shè)用一個(gè)_nil對(duì)象變量表示OC返回的nil
var obj = require("JPObject").returnNil()
obj.hash() //經(jīng)過特殊處理沒問題
if (!obj || obj == _nil) {
//判斷對(duì)象是否為nil就得附加判斷是否等于_nil
}
這樣的使用方式難以接受北苟,繼續(xù)尋找解決方案桩匪,發(fā)現(xiàn) true / false 在 JS 是個(gè)對(duì)象,是可以調(diào)用方法的友鼻,如果用 false 表示 nil傻昙,即可以做到調(diào)用方法闺骚,又可以直接通過 if (!obj) 判斷是否為 nil,于是沿著這個(gè)方向妆档,解決了用 false 表示 nil 帶來的各種坑僻爽,幾乎完美地解決了這個(gè)問題。實(shí)現(xiàn)上的細(xì)節(jié)就不多說了过吻,說"幾乎完美"进泼,是因?yàn)檫€有一個(gè)小坑,傳遞 false 給 OC 上參數(shù)類型是 NSNumber* 的方法纤虽,OC 會(huì)得到 nil 而不是 NSNumber 對(duì)象:
@implementation JPObject
- (void)passNSNumber:(NSNumber *)num {
NSLog(@"%@", num);
}
@end
require("JPObject").passNSNumber(false) //output: nil
如果 OC 方法的參數(shù)類型是 BOOL乳绕,或者傳入的是 true / 0,都是沒問題的逼纸,這小坑無傷大雅洋措。
題外話,神奇的 JS 里 false 的 this 竟然不再是原來的 false杰刽,而是另一個(gè) Boolean 對(duì)象菠发,太特殊了:
Object.prototype.c = function(){console.log(this === false)};
false.c() //output false
有人做出了解釋 #351
總結(jié)
JSPatch 的原理以及一些實(shí)現(xiàn)細(xì)節(jié)就闡述到這里,希望這篇文章對(duì)大家了解和使用 JSPatch 有幫助贺嫂。
**Pages 42 **
- Home
- Adding new extensions
- Base usage
- Basic Usage of JSPatch
- C 函數(shù)調(diào)用
- Debugging JavaScript
- defineClass****使用文檔
- defineProtocol 使用文檔
- GCD 擴(kuò)展使用文檔
- How JSPatch works
- How JSPatch works2 Detail
- JPBlock 擴(kuò)展使用文檔
- JPCleaner 即時(shí)撤回腳本
- JPMemory 使用文檔
- JPNumber****使用文檔
- JS 斷點(diǎn)調(diào)試
- JS 斷點(diǎn)調(diào)試滓鸠,
- JSPatch
- JSPatch %E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95
- JSPatch %E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95#property
- JSPatch Deployment Security Strategy
- JSPatch Loader 使用文檔
- JSPatch New Features Review
- JSPatch 基礎(chǔ)用法
- JSPatch 實(shí)現(xiàn)原理詳解
- JSPatch 常見問題
- JSPatch****常見問題解答
- performSelectorInOC 使用文檔
- Quick start
- Usage of defineClass
- Usage of JPMemory
- Use JSPatch development of functional modules
- 使用 JSPatch 開發(fā)功能模塊
- 創(chuàng)建擴(kuò)展
- 基礎(chǔ)用法
- 如何排查問題
- 快速開始
- 接入擴(kuò)展
- 支持 JSPatch
- 支持自定義 struct 類型
- 添加 struct 類型支持
- 添加對(duì)****CLLocationCoordinate2D 等結(jié)構(gòu),以及成員變量為結(jié)構(gòu)的結(jié)構(gòu)支持第喳。
- Show 27 more pages…
基礎(chǔ)
JSPatch 基礎(chǔ)用法 使用 JSPatch 開發(fā)功能模塊
常見問題
如何排查問題
進(jìn)階
performSelectorInOC 使用文檔 defineProtocol 使用文檔
JS 斷點(diǎn)調(diào)試
JSPatch Loader 使用文檔
JPCleaner 即時(shí)撤回腳本
C 函數(shù)調(diào)用
擴(kuò)展
接入擴(kuò)展 創(chuàng)建擴(kuò)展
JPMemory 使用文檔
JPNumber 使用文檔
GCD 擴(kuò)展使用文檔
添加 struct 類型支持
JPBlock 使用文檔