iOS 熱更新的3個(gè)可行方案(原理)

下面主要說兩個(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ù)的核心原理:

  1. 攔截目標(biāo)方法調(diào)用,讓其調(diào)用轉(zhuǎn)發(fā)到預(yù)先埋好的特定方法中
  2. 獲取目標(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è)類

  1. 首先創(chuàng)建新類:objc_allocateClassPair
  2. 然后注冊(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)用方式

  1. 常規(guī)調(diào)用
  2. 反射調(diào)用
  3. objc_msgSend
  4. C 函數(shù)調(diào)用
  5. 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í)很容易待侵。具體步驟如下:

  1. 首先新增一個(gè)方法實(shí)現(xiàn)跟目標(biāo)方法一致的別名方法丢早,用來調(diào)用原目標(biāo)方法。
  2. 其次將目標(biāo)方法的函數(shù)實(shí)現(xiàn)(IMP)替換成 _objc_msgForward秧倾,目的是讓目標(biāo)方法進(jìn)行強(qiáng)制轉(zhuǎn)發(fā)
  3. 最后將目標(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即可修改方法返回值熊榛。例如將 MyClassCclassName 方法的返回值改為 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ù):AspectsAspects庫(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 流程

  1. 檢查 selector 是否可以替換,里面涉及一些黑名單等判斷
  2. 獲取 AspectsContainer车柠,如果為空則創(chuàng)建并綁定目標(biāo)類
  3. 創(chuàng)建 AspectIdentifier剔氏,用來保存回調(diào)blockAspectOptions 等信息
  4. 將目標(biāo)類 forwardInvocation: 方法替換為自定義方法(__ASPECTS_ARE_BEING_CALLED__)
  5. 目標(biāo)類新增一個(gè)帶有aspects_前綴的方法,新方法(aliasSelector)實(shí)現(xiàn)跟目標(biāo)方法相同
  6. 將目標(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 流程

  1. 調(diào)用目標(biāo)方法進(jìn)入消息轉(zhuǎn)發(fā)流程
  2. 調(diào)用自定義 __ASPECTS_ARE_BEING_CALLED__ 方法
  3. 獲取對(duì)應(yīng) invocation,將 invocation.selector 設(shè)置為 aliasSelector
  4. 通過 aliasSelector 獲取對(duì)應(yīng) AspectsContainer
  5. 根據(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    

原因分析:

  1. NSInvocation 不會(huì)引用參數(shù)捻悯,詳情可以看官方文檔(This class does not retain the arguments for the contained invocation by default)
  2. 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 方法
  3. 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) MyClassAMyClassB 都被標(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,而使用NSInvocationperformSelector:動(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)

解決辦法:

  1. 使用__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;
}

  1. 采用常規(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_msgForwardforwardInvocation:特占。所以不存在風(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: 方法為例:

  1. 把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:。
  2. 為UIViewController添加 -ORIGviewWillAppear: 和 -_JPviewWillAppear: 兩個(gè)方法,前者指向原來的IMP實(shí)現(xiàn)瑞侮,后者是新的實(shí)現(xiàn)季俩,稍后會(huì)在這個(gè)實(shí)現(xiàn)里回調(diào)JS函數(shù)消痛。
  3. 改寫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)楹苌僖姷竭@種奇葩的命名方式,感覺問題不大矢门,使用也會(huì)導(dǎo)致替換不了 OC 方法名包含 字符的盆色,最終為了代碼顏值,使用了雙下劃線 __ 表示祟剔。

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 有幫助贺嫂。

Add a custom footer

**Pages 42 **

基礎(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 使用文檔

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末糜俗,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子曲饱,更是在濱河造成了極大的恐慌悠抹,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,591評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件扩淀,死亡現(xiàn)場(chǎng)離奇詭異楔敌,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)驻谆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門卵凑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人胜臊,你說我怎么就攤上這事氛谜。” “怎么了区端?”我有些...
    開封第一講書人閱讀 162,823評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)澳腹。 經(jīng)常有香客問我织盼,道長(zhǎng)杨何,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,204評(píng)論 1 292
  • 正文 為了忘掉前任沥邻,我火速辦了婚禮危虱,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘唐全。我一直安慰自己埃跷,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評(píng)論 6 388
  • 文/花漫 我一把揭開白布邮利。 她就那樣靜靜地躺著弥雹,像睡著了一般。 火紅的嫁衣襯著肌膚如雪延届。 梳的紋絲不亂的頭發(fā)上剪勿,一...
    開封第一講書人閱讀 51,190評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音方庭,去河邊找鬼厕吉。 笑死,一個(gè)胖子當(dāng)著我的面吹牛械念,可吹牛的內(nèi)容都是我干的头朱。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼龄减,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼项钮!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起欺殿,我...
    開封第一講書人閱讀 38,923評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤坯沪,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后隘竭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體滤祖,經(jīng)...
    沈念sama閱讀 45,334評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評(píng)論 2 333
  • 正文 我和宋清朗相戀三年棍潘,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了恃鞋。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,727評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡亦歉,死狀恐怖恤浪,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情肴楷,我是刑警寧澤水由,帶...
    沈念sama閱讀 35,428評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站赛蔫,受9級(jí)特大地震影響砂客,放射性物質(zhì)發(fā)生泄漏泥张。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評(píng)論 3 326
  • 文/蒙蒙 一鞠值、第九天 我趴在偏房一處隱蔽的房頂上張望媚创。 院中可真熱鬧,春花似錦彤恶、人聲如沸钞钙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽芒炼。三九已至,卻和暖如春抵恋,著一層夾襖步出監(jiān)牢的瞬間焕议,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工弧关, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留盅安,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,734評(píng)論 2 368
  • 正文 我出身青樓世囊,卻偏偏與公主長(zhǎng)得像别瞭,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子株憾,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評(píng)論 2 354

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