JSPatch 源码分析(一)

作者: Arnold134777 | 来源:发表于2016-04-10 22:11 被阅读971次

    一统天下 —— pandoc

    1.前言

    前一段时间在公司做了一个iOS热补丁的模块,就用到了JSPatch框架,期间有了解过一些关于框架的源码分析的博客:

    1.JSPatch学习:JSPatch核心和实现原理分析

    2.JSPatch defineProtocol部分实现详解

    思考再三还是决定自己调试了解一下整体的实现机制。

    2.准备工具

    3.项目文件

    3.1项目文件图

    3.2具体文件分析

    • JPEngine.m 核心的Native端实现。
    • JSpatch.js 核心的js端实现。
    • Extensions 扩展的方法提供给js端调用,内部是OC实现。
    • Loader 一套热补丁动态更新补丁脚本的客户端实现,需要配合服务端才能实现整个更新框架。(本文不对此作多赘述)

    4.调试分析

    4.1调用代码

    [JPEngine startEngine];
    NSString *sourcePath = [[NSBundle mainBundle] pathForResource:@"demo" ofType:@"js"];
    NSString *script = [NSString stringWithContentsOfFile:sourcePath encoding:NSUTF8StringEncoding error:nil];
    [JPEngine evaluateScript:script];
    

    4.2 JPEngine初始化

    [JPEngine startEngine];
    

    我们来看看具体的代码实现,

    • 代码片段一:
    + (void)startEngine
    {
        if (![JSContext class] || _context) {
            return;
        }
        
        JSContext *context = [[JSContext alloc] init];
        
        context[@"_OC_defineClass"] = ^(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods) {
            return defineClass(classDeclaration, instanceMethods, classMethods);
        };
    
        context[@"_OC_defineProtocol"] = ^(NSString *protocolDeclaration, JSValue *instProtocol, JSValue *clsProtocol) {
            return defineProtocol(protocolDeclaration, instProtocol,clsProtocol);
        };
        
        /*.... 分块分析,暂时省略以下代码*/
    }
    

    _content 为JSContect的实例,根据苹果官方文档 :

    我们知道JSContext为js的执行环境。因此例如:
    context[@"_OC_defineProtocol"]=block实现这样的调用就很好的理解为为js的上下文注入了全局的_OC_defineProtocol方法,而具体的实现对应着Native端的block的实现。

    • 代码片段二:
    NSString *path = [[NSBundle bundleForClass:[self class]] pathForResource:@"JSPatch" ofType:@"js"];
    NSAssert(path, @"can't find JSPatch.js");
    NSString *jsCore = [[NSString alloc] initWithData:[[NSFileManager defaultManager] contentsAtPath:path] encoding:NSUTF8StringEncoding];
        
     if ([_context respondsToSelector:@selector(evaluateScript:withSourceURL:)]) {
            [_context evaluateScript:jsCore withSourceURL:[NSURL URLWithString:@"JSPatch.js"]];
     } else {
            [_context evaluateScript:jsCore];
     }
    

    加载核心的JSPatch.js代码完成初始化,具体js代码后续具体调用再作分析。

    4.3具体修复代码

    我们来看看Demo的修复代码:

    defineClass('JPViewController', {
      handleBtn: function(sender) {
        var tableViewCtrl = JPTableViewController.alloc().init()
        self.navigationController().pushViewController_animated(tableViewCtrl, YES)
      }
    })
    

    调试看看执行步骤:

    4.3.1 首先执行global.defineClass (js端)

    global.defineClass = function(declaration, properties, instMethods, clsMethods)
    

    我们断点到var ret = _OC_defineClass(declaration, newInstMethods, newClsMethods)处,查看局部变量表

    会发现newInstMethods newClsMethods均被赋值,且其中每个实例或类方法的js对象被修改添加参数的个数的说明,只是好奇为什么需要加入参数个数的说明呢???

    4.3.2 执行_OC_defineClass (js端 -> Native端)

     context[@"_OC_defineClass"] = ^(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods) {
            return defineClass(classDeclaration, instanceMethods, classMethods);
        };
    
    

    实际执行的是Native的 static NSDictionary *defineClass(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods) 方法,以下分代码片段解析。

    • 代码片段一:
    NSScanner *scanner = [NSScanner scannerWithString:classDeclaration];
        
        NSString *className;
        NSString *superClassName;
        NSString *protocolNames;
        [scanner scanUpToString:@":" intoString:&className];
        if (!scanner.isAtEnd) {
            scanner.scanLocation = scanner.scanLocation + 1;
            [scanner scanUpToString:@"<" intoString:&superClassName];
            if (!scanner.isAtEnd) {
                scanner.scanLocation = scanner.scanLocation + 1;
                [scanner scanUpToString:@">" intoString:&protocolNames];
            }
        }
        
        if (!superClassName) superClassName = @"NSObject";
        className = trim(className);
        superClassName = trim(superClassName);
        
        NSArray *protocols = [protocolNames length] ? [protocolNames componentsSeparatedByString:@","] : nil;
        
        Class cls = NSClassFromString(className);
        if (!cls) {
            Class superCls = NSClassFromString(superClassName);
            if (!superCls) {
                NSCAssert(NO, @"can't find the super class %@", superClassName);
                return @{@"cls": className};
            }
            cls = objc_allocateClassPair(superCls, className.UTF8String, 0);
            objc_registerClassPair(cls);
        }
    
        if (protocols.count > 0) {
            for (NSString* protocolName in protocols) {
                Protocol *protocol = objc_getProtocol([trim(protocolName) cStringUsingEncoding:NSUTF8StringEncoding]);
                class_addProtocol (cls, protocol);
            }
        }
    

    通过传递的classDeclaration解析相应的className,superClassName,protocols,

    //1.
    cls = objc_allocateClassPair(superCls, className.UTF8String, 0);
    objc_registerClassPair(cls);
     
     //2.
     if (protocols.count > 0) {
            for (NSString* protocolName in protocols) {
                Protocol *protocol = objc_getProtocol([trim(protocolName) cStringUsingEncoding:NSUTF8StringEncoding]);
                class_addProtocol (cls, protocol);
            }
        }
    

    1.运行期间创建一个新类,并完成注册. 2.遍历协议名,依此初始化并完成对类的协议的添加。

    • 代码片段二:
    for (int i = 0; i < 2; i ++) {
            BOOL isInstance = i == 0;
            JSValue *jsMethods = isInstance ? instanceMethods: classMethods;
            
            Class currCls = isInstance ? cls: objc_getMetaClass(className.UTF8String);
            NSDictionary *methodDict = [jsMethods toDictionary];
            for (NSString *jsMethodName in methodDict.allKeys) {
                JSValue *jsMethodArr = [jsMethods valueForProperty:jsMethodName];
                int numberOfArg = [jsMethodArr[0] toInt32];
                NSString *selectorName = convertJPSelectorString(jsMethodName);
                
                if ([selectorName componentsSeparatedByString:@":"].count - 1 < numberOfArg) {
                    selectorName = [selectorName stringByAppendingString:@":"];
                }
                
                JSValue *jsMethod = jsMethodArr[1];
                if (class_respondsToSelector(currCls, NSSelectorFromString(selectorName))) {
                    overrideMethod(currCls, selectorName, jsMethod, !isInstance, NULL);
                } else {
                    BOOL overrided = NO;
                    for (NSString *protocolName in protocols) {
                        char *types = methodTypesInProtocol(protocolName, selectorName, isInstance, YES);
                        if (!types) types = methodTypesInProtocol(protocolName, selectorName, isInstance, NO);
                        if (types) {
                            overrideMethod(currCls, selectorName, jsMethod, !isInstance, types);
                            free(types);
                            overrided = YES;
                            break;
                        }
                    }
                    if (!overrided) {
                        if (![[jsMethodName substringToIndex:1] isEqualToString:@"_"]) {
                            NSMutableString *typeDescStr = [@"@@:" mutableCopy];
                            for (int i = 0; i < numberOfArg; i ++) {
                                [typeDescStr appendString:@"@"];
                            }
                            overrideMethod(currCls, selectorName, jsMethod, !isInstance, [typeDescStr cStringUsingEncoding:NSUTF8StringEncoding]);
                        }
                    }
                }
            }
        }
    
    
    • 遍历传递过过来的实例方法,类方法的js实例,然后依次遍历方法字典,完成方法名js命名到native命名的转换。

    • 通过转换后的方法名,用class_respondsToSelector判断是否该类的方法列表中是否已经存在该方法的实现,存在即调用overrideMethod(currCls, selectorName, jsMethod, !isInstance, NULL);覆盖方法实现。

    • 如果类的方法列表中不存在该方法的实现,则通过实现的协议的列表查找,依次判断方法是否在协议的实现方法中,如果以上都不是说明是新添加的方法.

    • 代码片段三:

    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Wundeclared-selector"
        class_addMethod(cls, @selector(getProp:), (IMP)getPropIMP, "@@:@");
        class_addMethod(cls, @selector(setProp:forKey:), (IMP)setPropIMP, "v@:@@");
    #pragma clang diagnostic pop
    

    添加类的getProp:, setProp:forKey:的方法及实现。

    • 代码片段四:
    static void overrideMethod(Class cls, NSString *selectorName, JSValue *function, BOOL isClassMethod, const char *typeDescription)
    {
        SEL selector = NSSelectorFromString(selectorName);
        
        if (!typeDescription) {
            Method method = class_getInstanceMethod(cls, selector);
            typeDescription = (char *)method_getTypeEncoding(method);
        }
        
        IMP originalImp = class_respondsToSelector(cls, selector) ? class_getMethodImplementation(cls, selector) : NULL;
        
        IMP msgForwardIMP = _objc_msgForward;
        #if !defined(__arm64__)
            if (typeDescription[0] == '{') {
                //In some cases that returns struct, we should use the '_stret' API:
                //http://sealiesoftware.com/blog/archive/2008/10/30/objc_explain_objc_msgSend_stret.html
                //NSMethodSignature knows the detail but has no API to return, we can only get the info from debugDescription.
                NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:typeDescription];
                if ([methodSignature.debugDescription rangeOfString:@"is special struct return? YES"].location != NSNotFound) {
                    msgForwardIMP = (IMP)_objc_msgForward_stret;
                }
            }
        #endif
    
        class_replaceMethod(cls, selector, msgForwardIMP, typeDescription);
    
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Wundeclared-selector"
        if (class_getMethodImplementation(cls, @selector(forwardInvocation:)) != (IMP)JPForwardInvocation) {
            IMP originalForwardImp = class_replaceMethod(cls, @selector(forwardInvocation:), (IMP)JPForwardInvocation, "v@:@");
            class_addMethod(cls, @selector(ORIGforwardInvocation:), originalForwardImp, "v@:@");
        }
    #pragma clang diagnostic pop
    
        if (class_respondsToSelector(cls, selector)) {
            NSString *originalSelectorName = [NSString stringWithFormat:@"ORIG%@", selectorName];
            SEL originalSelector = NSSelectorFromString(originalSelectorName);
            if(!class_respondsToSelector(cls, originalSelector)) {
                class_addMethod(cls, originalSelector, originalImp, typeDescription);
            }
        }
        
        NSString *JPSelectorName = [NSString stringWithFormat:@"_JP%@", selectorName];
        SEL JPSelector = NSSelectorFromString(JPSelectorName);
    
        _initJPOverideMethods(cls);
        _JSOverideMethods[cls][JPSelectorName] = function;
        
        class_addMethod(cls, JPSelector, msgForwardIMP, typeDescription);
    }
    

    核心的替换添加方法实现的方法:

    • 把原始的方法的实现替换为_objc_msgForward,即该方法的调用会走消息转发的路径.
    • 类的forwardInvocation:方法的实现被替换为JPForwardInvocation的实现
    • 添加的方法ORIGforwardInvocation指向原始的实现IMP.
    • 添加的方法ORIG+selector指向原始的实现的IMP.
    • 添加_JP+selector指向新的函数的实现.
      </br>

    上述代码只是讲解了替换添加方法的实现,而新的实现方法是一个个js的对象,如何关联到Native端的调用?后续会继续更新,分析 调用核心方法JPForwardInvocation,JPExtension等其他部分的实现,对于上述的分析有问题处欢迎及时指出,谢谢!

    补充:4.3.1 中为什么需要加入参数个数的说明呢,与方法的签名有关,具体下次更新作分析!!!

    本人还在不断的学习积累中,有问题欢迎及时指出,谢谢!

    相关文章

      网友评论

        本文标题:JSPatch 源码分析(一)

        本文链接:https://www.haomeiwen.com/subject/pszblttx.html