热修复之我看

lxf2023-05-23 01:09:59

背景

移动端热修复是指在移动应用程序中对已发布的应用进行动态修复和更新,而无需用户重新下载安装新版本的应用。以下是一些研究移动端热修复的原因:

  1. 修复应用程序漏洞和Bug:移动应用程序可能存在各种漏洞和Bug,这些问题可能会导致应用程序崩溃、功能失效或数据泄露等问题。通过研究移动端热修复技术,开发人员可以快速响应并修复这些问题,而无需用户等待和下载完整的应用程序更新。

  2. 提供快速更新和功能迭代:移动应用程序的开发和发布通常需要经过一定的时间和审核过程,这使得快速修复和更新应用程序变得困难。通过研究移动端热修复技术,开发人员可以更快地推出新功能和修复问题,以满足用户的需求并提供更好的用户体验。

  3. 减少用户流失和提高用户满意度:当用户遇到应用程序中的问题或缺陷时,他们可能会感到不满并选择卸载应用程序。通过使用移动端热修复技术,开发人员可以在最短时间内修复问题,并避免用户因问题而离开应用程序,从而提高用户满意度和用户留存率。

  4. 降低应用程序维护成本:传统的应用程序更新需要发布新版本,并要求用户下载和安装更新,这需要耗费开发人员和用户的时间和资源。通过使用移动端热修复技术,可以减少应用程序的发布次数和用户的下载次数,从而降低应用程序的维护成本。

总而言之,研究移动端热修复可以提供一种快速、高效和经济的方式来修复移动应用程序中的问题,提供更好的用户体验,并降低开发和维护成本。

痛点

移动端热修复的痛点主要有以下几个:

1.上架问题: 如果被商店查到使用此功能可能会面临被拒风险。

2.安全风险:热修复可能会导致应用程序的安全风险,因为攻击者可能会篡改热修复包,导致应用程序崩溃或泄露敏感信息。

3.兼容性问题:热修复可能会引起应用程序的兼容性问题,因为新的代码可能与旧的代码不兼容,导致应用程序崩溃或异常。

4.调试困难:热修复的代码通常比较难以调试,因为它是动态加载的,不容易在开发环境中调试。

5。性能问题:热修复可能会导致应用程序的性能问题,因为它需要动态加载代码,这可能会导致应用程序响应速度变慢或者增加内存占用。

6.维护存在一定成本:热修复需要额外的维护成本,因为需要开发额外的代码和测试热修复的效果。

因此,在使用移动端热修复技术时,需要仔细权衡各种利弊,并采取相应的安全措施来确保应用程序的安全性。同时,开发人员需要充分考虑兼容性问题和性能问题,并进行充分的测试和调试工作。

业界方案

jspatch 目前不能上架

JSPatch 是一个用于 iOS 应用的热修复框架,它允许开发者在不重新发布应用的情况下修复线上的 Bug 或者添加新功能。JSPatch 的原理可以简单概括如下:

  1. JSPatch 的运行环境是 JavaScriptCore,它是苹果提供的 JavaScript 引擎,可以在 iOS 应用中执行 JavaScript 代码。

  2. 在应用启动时,JSPatch 将下载的 JavaScript 脚本注入到 JavaScriptCore 中,并创建一个全局的 JavaScript 上下文。

  3. JSPatch 使用 JavaScriptCore 提供的 Objective-C 与 JavaScript 交互的 API。开发者可以通过这些 API 将 Objective-C 对象方法暴露给 JavaScript,从而实现在 JavaScript 中调用原生的 Objective-C 方法。

  4. JavaScript 脚本中的代码可以直接访问应用中的 Objective-C 对象和方法,以及修改和执行这些对象和方法。

  5. 当开发者需要修复 Bug 或者添加新功能时,他们可以在 JavaScript 脚本中编写相应的逻辑。然后将这个脚本发布到服务器,并通知应用更新。

  6. 应用在后台通过网络请求下载最新的 JavaScript 脚本,并将其注入到 JavaScriptCore 中。

  7. 下次应用启动时,注入的新 JavaScript 脚本将生效,并可以执行修复 Bug 或者添加新功能的逻辑。

总的来说,JSPatch 的原理是通过将 JavaScript 脚本注入到 iOS 应用中的 JavaScriptCore 中,实现在运行时修复 Bug 或者添加新功能。开发者可以通过 JavaScript 代码访问和修改应用中的 Objective-C 对象和方法,从而实现动态修复和扩展应用的能力。

Ocrunner 

是基于 mangofix做了优化,支持直接使用oc语法做脚本,原理和mangofix 一样

mangofix

热修复之我看

Mango源码分析 目录结构

主要有 Compiler , Execute,libffi,symdl 这几个核心模块组成

Compiler

主要  yacc 词法,语法解析器,内置对象映射表,AST 解析后的内置对象相关的model类组成。

Execute

主要来承接yacc 处理后的后续操作,把对脚本解析后转换成相应的内置对象。 

包含 MFContext执行的上下文 它用来执行整个热修复的 脚本初始化,加密,解密执行,以及解析入口等。

 MangoFix脚本中很多功能都是通过预先创建内置对象的方式支持的,比如常用结构体的声明、变量、宏、C函数和GCD相关的操作等,如果想详细了解MangoFix中有哪些内置对象,当然MangoFix也开放了相关接口,你也可以向MangoFix执行上下文中注入你需要的对象。

libffi

主要用来动态的执行某些函数方法。

 libffi(The Foreign Function Interface library)是一个开源的软件库,它提供了一种通用的编程接口,用于调用不同编程语言之间的函数。它允许在运行时动态地调用和执行函数,而无需在编译时提前知道函数的签名或参数类型。

libffi的设计目标是为不同的编程语言提供一个统一的接口,使它们能够相互调用。它为许多常见的编程语言,如C、C++、Python、Ruby等提供了支持,并可以方便地扩展到其他语言。

libffi的核心功能是动态地生成机器码来调用函数,这使得在运行时可以根据需要解析函数的签名、分配内存、处理参数传递和返回值等操作。它提供了一个高度灵活和可移植的接口,使得开发者可以轻松地在不同的编程环境中进行函数调用。

libffi的主要用途之一是在解释型语言中实现对外部库的调用。通过使用libffi,解释型语言的开发者可以方便地扩展语言的功能,调用本地代码库或其他语言编写的函数。它还可以用于实现动态链接,通过在运行时加载和执行共享库中的函数。

总的来说,libffi是一个非常有用的库,它提供了一种通用的方式来调用不同编程语言之间的函数,为开发者提供了更大的灵活性和互操作性。无论你是开发解释型语言、扩展其他编程语言功能,还是需要实现动态链接,libffi都是一个值得考虑的选择。

symdl是一个简单的小工具,它的功能与dlsym非常相似,使用symdl,可以传入动态链接的C函数名字符串,获取函数指针,从而实现对C函数的动态调用。

下面来看源码执行流程

首先来看下mango 脚本格式

热修复之我看

关于脚本语法可以去相应地址查看

github.com/YPLiang19/M…

注意事项

一般来说在脚本是需要加密解密的,因为如果被别人动态的修改,可能会对app造成一定的影响。

1.对脚本进行ASE加密,此步骤一般最后会上传到服务器,通过服务器下发。


    NSError *outErr = nil;

    BOOL writeResult = NO;

    NSURL *scriptUrl = [[NSBundle mainBundle] URLForResource:@"demo" withExtension:@"mg"];

    NSString *plainScriptString = [NSString stringWithContentsOfURL:scriptUrl encoding:NSUTF8StringEncoding error:&outErr];

    if (outErr) goto err;
    {

        NSData *scriptData = [plainScriptString dataUsingEncoding:NSUTF8StringEncoding];

        NSData *encryptedScriptData = [scriptData AES128ParmEncryptWithKey:aes128Key iv:aes128Iv];

        NSString * encryptedPath= [(NSString *)[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:@"demo_encrypted.mg"];

        NSFileManager *fileManager = [NSFileManager defaultManager];

        if (![fileManager fileExistsAtPath:encryptedPath]) {

            [fileManager createFileAtPath:encryptedPath contents:nil attributes:nil];

        }

        writeResult = [encryptedScriptData writeToFile:encryptedPath options:NSDataWritingAtomic error:&outErr];

    }

err:

    if (outErr) NSLog(@"%@",outErr);

    return writeResult;

}

2.context 初始化解密脚本,执行整个流程

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    BOOL writeResult = [self encryptPlainScirptToDocument];

    if (!writeResult) {

        return NO;

    }

    MFContext *context = [[MFContext alloc] initWithAES128Key:aes128Key iv:aes128Iv];

    NSString * encryptedPath= [(NSString *)[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:@"demo_encrypted.mg"];

    [NSURL URLWithString:@""];

    NSURL *scriptUrl = [NSURL fileURLWithPath:encryptedPath];

    [context evalMangoScriptWithURL:scriptUrl];

return YES;

}

evalMangoScriptWithAES128Data是解密并执行的核心方法。

- (void)evalMangoScriptWithURL:(NSURL *)url{

    @autoreleasepool {

        NSError *error;

        NSData *encryptedData = [NSData dataWithContentsOfURL:url];

        if (error) {

            NSLog(@"[MangoFix] [ERROR] : %@",error);

            return;

        }

        [self evalMangoScriptWithAES128Data:encryptedData];

    }
}


- (void)evalMangoScriptWithAES128Data:(NSData *)scriptData {

    @autoreleasepool {

        NSData *mangoFixData = [scriptData AES128ParmDecryptWithKey:_key iv:_iv];

        NSString *mangoFixString = [[NSString alloc] initWithData:mangoFixData encoding:NSUTF8StringEncoding];

        if (!mangoFixString.length) {

            NSLog(@"[MangoFix] [ERROR] : AES128(ECBMode) decrypt error!");

            return;

        }

        mf_set_current_compile_util(self.interpreter);

        mf_add_built_in(self.interpreter);

        [self.interpreter compileSourceWithString:mangoFixString];

        mf_set_current_compile_util(nil);

        mf_interpret(self.interpreter);

    }

}
几个核心步骤
  • mf_set_current_compile_util(self.interpreter);
  • mf_add_built_in(self.interpreter);
  • [self.interpreter compileSourceWithString:mangoFixString];
  • mf_set_current_compile_util(nil);
  • mf_interpret(self.interpreter);
1.mf_set_current_compile_util

会创建一个解析对象,通过 mf_set_current_compile_util 设置在对于线程里,推测是为了后续获取访问。

2.mf_add_built_in

这段代码是为了向MFInterpreter对象添加内置功能,包括 结构体,方法,变量,gcd相关等。通过使用dispatch_once函数,这些内置操作只会在第一次调用mf_add_built_in函数时执行一次,确保添加内置功能的唯一性和线程安全性。

void mf_add_built_in(MFInterpreter *inter){

    static dispatch_once_t onceToken;

    dispatch_once(&onceToken, ^{

    add_built_in_struct_declare();

    add_build_in_function(inter);

    add_build_in_var(inter);

    add_gcd_build_in(inter);

    });
}
3.compileSourceWithString 接下来开始进行相关解析工作
- (void)compileSourceWithString:(NSString *)source{

    extern void mf_set_source_string(char const *source);

    mf_set_source_string([source UTF8String]);

    extern void yyrestart  (FILE * input_file );

    extern int yyparse(void);

        yyrestart(NULL); /* 每次解析前,重置yylex */

    if (yyparse()) {

    return;

    }
}
4.mf_set_source_string

是yacc + lex 的解析入口

热修复之我看

热修复之我看

5.经过了lex和 yacc 的处理后然后把内容放到了一个容器对象中,拿mf_add_class_definition来分析

热修复之我看

6.最终保存到了  topList 数组中

void mf_add_class_definition(MFClassDefinition *classDefinition){

MFInterpreter *interpreter = mf_get_current_compile_util();

interpreter.classDefinitionDic[classDefinition.name] = classDefinition;

[interpreter.topList addObject:classDefinition];

}
7.mf_interpret

最终通过 mf_interpret 遍历 topList 中的对象,通过runtime执行相应的操作,如动态添加方法 属性,交换方法等。最终通过 fix_class函数执行完整个流程,后续在程序动态执行的时候 会调用事先交换处理好的方法等。

void mf_interpret(MFInterpreter *interpreter){

    for (__kindof NSObject *top in interpreter.topList) {

        if ([top isKindOfClass:[MFStatement class]]) {

            execute_statement(interpreter, interpreter.topScope, top);

        }else if ([top isKindOfClass:[MFStructDeclare class]]){

            add_struct_declare(interpreter,top);

        }else if ([top isKindOfClass:[MFClassDefinition class]]){

            define_class(interpreter, top);

        fix_class(interpreter,top);

        }

    }

}

看几个关键 代码 

热修复之我看

热修复之我看

关于性能:

关于性能,我没有详细分析过,但是之前看到过一篇文章,对三种方案做过性能对比如下

性能:

设备: iPhone SE 2,iOS 14.3

在求斐波那契数列第25项的测试中:

  • JSPatch: 执行时间,平均为 0.169 s。内存占用一直稳定在 12 MB 左右。
  • OCRunner: 执行时间,平均为 1.05 s。内存占用,峰值为 60 MB 左右,其他稳定在 12 MB 左右。
  • Mango: 执行时间,平均时间为 2.38 s。内存占用,持续走高,最高的时候大约为 350 MB。

总结

我这里初衷也是对mangofix 使用流程和源码做一下简单的分析,对于最后的选择当然还要根据自身需求场景 和业务来决定是否使用哪一种,当然无论那种方案都是有一定风险的,希望能对大家有一定帮助。

本网站是一个以CSS、JavaScript、Vue、HTML为核心的前端开发技术网站。我们致力于为广大前端开发者提供专业、全面、实用的前端开发知识和技术支持。 在本网站中,您可以学习到最新的前端开发技术,了解前端开发的最新趋势和最佳实践。我们提供丰富的教程和案例,让您可以快速掌握前端开发的核心技术和流程。 本网站还提供一系列实用的工具和插件,帮助您更加高效地进行前端开发工作。我们提供的工具和插件都经过精心设计和优化,可以帮助您节省时间和精力,提升开发效率。 除此之外,本网站还拥有一个活跃的社区,您可以在社区中与其他前端开发者交流技术、分享经验、解决问题。我们相信,社区的力量可以帮助您更好地成长和进步。 在本网站中,您可以找到您需要的一切前端开发资源,让您成为一名更加优秀的前端开发者。欢迎您加入我们的大家庭,一起探索前端开发的无限可能!