浅析“热更新”(热修复)解决方案

版权声明:本文为EnweiTech原创文章,未经博主允许不得转载。 https://blog.csdn.net/English0523/article/details/84643308

新闻事件背景:11月27日,苹果应用商店集中下架了拼多多、搜狗、科大讯飞、悦跑圈等多家公司的应用产品。科大讯飞和悦跑圈均表示,下架与“热更新”相关。然而,这并不是苹果应用商店第一次因为“热更新”而作出如此大规模的动作。不过,此次多款知名应用遭遇突然下架,也体现出苹果对其封闭生态系统的强力维护。数据显示,年初至今苹果商店中国区单日超万款APP下架的情况发生过8次以上。在今年5月份和6月份的两次大清理中,先后有1.4万款应用和2万多款应用被下架。

由此可见,此次苹果下架大量应用,主要针对的应该就是“热更新”问题。所谓“热更新”,是指在应用中动态下发代码,它可以让开发者在不发布新版本的状态下修复技术缺陷或增添功能,在用户打开应用时会自动提醒并下载升级,下载完成后软件会自动安装。“热更新”不需要通过苹果应用商店软件版本更新审核,因此有很多公司选择利用此方式修改技术缺陷,实现快速迭代。

早在2017年3月份,苹果就曾对开发者发送警告邮件,要求停止使用应用“热更新”功能,否则将会遭到下架处理。该条款于当年6月份生效时,很多知名软件都曾遭遇过短暂下架。

苹果为何会对“热更新”如此严防死守?一方面,利用“热更新”,确实有可能对用户利益造成侵害。“通过‘热更新’,可以绕开苹果的审核机制,部分开发者有可能会在应用中植入色情、赌博、暴力等违规内容。此外,有些‘热更新’开发框架存在不安全因素,若有黑客组织发现此类开发框架存在安全漏洞,可以利用后门窃取用户设备中的隐私信息。”APP开发从业者王守强说。

苹果严禁“热更新”,还有保护其自身商业利益的考虑。成都游戏开发者马觅说:“苹果应用商店的盈利主要来自应用内付费分成,但利用‘热更新’,开发者有可能绕过苹果支付体系,这在行业内被称为‘切支付’,其实就是开发者自己叠加一个支付通道,通过这样的方式获得的收入,就可以不与苹果分成。”

那么今天我们就来聊聊,专业名词——热更新。

热更新

热更新是一种各大手游等众多APP常用的更新方式。简单来说,就是在用户通过App Store下载App之后,打开App时遇到的即时更新。

2017年6月,AppStore审核团队针对AppStore中“热更新”的App开发者发送邮件,要求移除所有相关的代码、框架或SDK,并重新提交审核,否则就会在AppStore中下架该软件。

工作原理

热更新就是动态下发代码,它可以使开发者在不发布新版本的情况下,修复 BUG 和发布功能,让开发者得以绕开苹果的审核机制,避免长时间的审核等待以及多次被拒造成的成本。 

技术特点

    在iOS中有两种APP更新方式:一种是在APPStore内进行更新,更新时重新下载全部安装包;另一种就是热更新,用户只有在打开APP时才会发现热更新包,更新时只需下载安装更新部分的代码,再次打开时即可。热更新最大的优点就是快,它可以绕过苹果方面的审核,更新通常只需一个晚上即可上线,另一大优点就是更新包较小,一般都在1M左右,用户不连接WiFi也可随意下载。

安全隐患

    由于软件热更新绕过了苹果的审核,黑客开发者有可能会通过提交正常的版本之后,通过热更新的方式修改APP导致安全隐患,这违反了苹果的安全隐私政策。另外苹果此举既能改善部分使用混编语言的App的流畅性,也能重新掌握一些渠道的App审核权限。

APP热更新方案

APP热更新方案

为什么要做热更新

当一个App发布之后,突然发现了一个严重bug需要进行紧急修复,这时候公司各方就会忙得焦头烂额:重新打包App、测试、向各个应用市场和渠道换包、提示用户升级、用户下载、覆盖安装。

重点是还会有原来的版本遗留,无论你怎么提示都有人放弃治疗,不愿意升级,强制不能使用体验又足够糟糕到让人不能启齿。

如果这是一个影响公司收入或者体验影响极其不好的Bug,那完蛋了,可能公司老板会对整个技术团队的技术能力丧失信心,其对技术人员的伤害是致命的。

最后最致命的是:

有时候仅仅是因为不小心写错了一行代码,就让所有的加班都付之东流,苦不苦,冤不冤,想想都苦。

还有一种剧情是研发总监把锅甩给测试团队,测试不过关,测试摊摊手说我也不是神啊,总会有漏网之鱼.

那能不能神不知鬼不觉再没有产生较大影响前把bug快速修复了呢?

热更新的行业情况

先来说说Android

并不是因为Android更有料就先说他,而是它的用户量级比Iphone大,我们写文章也是讲究大数据分析的不是..

Andoid端在15年热补丁就比较火,先后出现了Dexposed、AndFix,Qzone超级补丁的类Nuwa方式,微信的Tinker, 大众点评的nuwa、百度金融的rocooFix, 饿了么的amigo以及美团的robust.

再来看看Iphone端

技术上要在 iOS 上做到原生动态化比 Android 更容易,iOS 开发语言 Objective-C 天生动态,运行时都能随意替换方法,运行时加载动态库又是项很老的技术,只要我把增量的代码和资源打包到一个 framework 里,动态下发运行时加载,修 bug,加功能都不在话下,性能完全无损,这件事就结束了。

但是呢。苹果把加载动态库的功能给封了,动态库必须跟随安装包一起签名才能被加载,无法通过别的途径签名后再下发。

于是有了 waxPatch 和 JSPatch 这样的方案,以及异军突起不局限于热修复Bug而能做主体功能发布的React Native 和 Weex,后面又有了吊口味的滴滴的DynamicCocoa方案和OCScript

热更新的技术原理

先来说JAVA

技术派系:

• Native,代表有阿里的Dexposed、AndFix与腾讯的内部方案KKFix;

• Java,代表有Qzone的超级补丁、大众点评的nuwa、百度金融的rocooFix, 饿了么的amigo以及美团的robust。

Native流派与Java流派都有着自己的优缺点,它们具体差异大家可参考上文。事实上从来都没有最好的方案,只有最适合自己的。

下面我们来一一简单看下各热更新的实现方案:

Dexposed

阿里开源项目,基于Xposed的AOP框架,方法级粒度,可以进行AOP编程、插桩、热补丁、SDK hook等功能。

不同的是,Xposed通过劫持 zygote(须root),而dexposed通过劫持 java method ( 而非楼上说的劫持class loader方法),将java method改变为native,并且将这个方法的实现链接到一个通用的Native Dispatch方法上.)用处,最大的自然是hotpatch,用这种东西来热替换某个导致崩溃的方法。手淘还有做的一件事,就是用它作性能监控。这主要得益于无侵入式的方法调用Befor和After事件,能够让我们很好的记录和分析一个方法的调用时间。开源项目promeG/XLog就是基于dexposed实现的方法调用logging

APP热更新方案

使用方法:

dexposed提供了3个使用方法:

beforeHookedMethod

afterHookedMethod

replaceHookedMethod

APP热更新方案

来看看使用方式,也极其简单.

APP热更新方案

APP热更新方案

优缺点:

来说说硬伤吧,不支持art,不支持art,不支持art。

不支持Dalvik 3.0.

所以注定它会逐步失声,再多的优点也是徒劳

Qzon的超级补丁方案

该方案基于的是android dex分包方案的,关于dex分包方案本身更多是为了解决Android的64K方法调用限制问题,具体的原因是:

• DexOpt 会把每一个类的方法 id 检索起来,存在一个链表结构里面,但是这个链表的长度是用一个 short 类型来保存的,导致了方法 id 的数目不能够超过65536个。当一个项目足够大的时候,显然这个方法数的上限是不够的。

•Dexopt 使用 LinearAlloc 来存储应用的方法信息。Dalvik LinearAlloc 是一个固定大小的缓冲区。在Android 版本的历史上,LinearAlloc 分别经历了4M/5M/8M/16M限制。Android 2.2和2.3的缓冲区只有5MB,Android 4.x提高到了8MB 或16MB。当方法数量过多导致超出缓冲区大小时,也会造成dexopt崩溃

尽管在新版本的 Android 系统中,DexOpt 修复了方法数65K的限制问题,并且扩大了 LinearAlloc 限制,但是这套技术机制保留了下来

分包的方案简单来说就是在打包时将应用的代码分成多个 dex,使得主 dex 的方法数和所需的 LinearAlloc 不超过系统限制。在应用启动或运行过程中,首先是主 dex 启动运行后,再加载从 dex,这样就绕开了这两个限制。

如何拆分和如何加载可以查看Google官方的方案MultiDex

http://developer.android.com/intl/zh-cn/tools/building/multidex.htm

Qzon的超级补丁方案玩的是什么招呢?

把BUG方法修复以后,放到一个单独的DEX里,插入到dexElements数组的最前面,让虚拟机去加载修复完后的方法。

APP热更新方案

Patch.dex中的A.class会有优先加载,后续的dex中的A.class就不会加载直接跳过,达到修复目的。

核心问题:

当两个调用关系的类不在同一个DEX时,就会产生异常报错。我们知道,在APK安装时,虚拟机需要将classes.dex优化成odex文件,然后才会执行。在这个过程中,会进行类的verify操作,如果调用关系的类都在同一个DEX中的话就会被打上CLASS_ISPREVERIFIED的标志,然后才会写入odex文件。具体如何解决这个问题可以参见QQ空间终端开发团队QQ空间终端开发团队发布的” 安卓App热补丁动态修复技术介绍”

优缺点:

1.没有合成整包(和微信Tinker比起来),产物比较小,比较灵活

2.可以实现类替换,兼容性高。(某些三星手机不起作用)

不足:

1.不支持即时生效,必须通过重启才能生效。

2.为了实现修复这个过程,必须在应用中加入两个dex!dalvikhack.dex中只有一个类,对性能影响不大,但是对于patch.dex来说,修复的类到了一定数量,就需要花不少的时间加载。对手淘这种航母级应用来说,启动耗时增加2s以上是不能够接受的事。

3.在ART模式下,如果类修改了结构,就会出现内存错乱的问题。为了解决这个问题,就必须把所有相关的调用类、父类子类等等全部加载到patch.dex中,导致补丁包异常的大,进一步增加应用启动加载的时候,耗时更加严重。

微信Tinker

这个项目之初最大难点在于如何突破Qzone方案的性能问题,通过研究Instant Run的冷插拔与buck的exopackage给了我们灵感。它们的思想都是全量替换新的Dex

APP热更新方案

因为使用全新的dex,所以自然绕开了Art地址可能错乱的问题,在Dalvik模式下也不需要插桩,加载全新的合成dex即可。

焦点问题是合并的过程会不会有问题,会不会耗时或者效率低? 为此腾讯在DEX方面也花了很多时间研究内部的格式以及如何做Merge和进行校验工作,详细了解可以查看” 大腾讯的第一个开源项目「Tinker」”这篇文章

优势:

1. 合成整包,不用在构造函数插入代码,防止verify,verify和opt在编译期间就已经完成,不会在运行期间进行

2. 性能提高。兼容性和稳定性比较高。

3. 开发者透明,不需要对包进行额外处理。

不足:

1. 与超级补丁技术一样,不支持即时生效,必须通过重启应用的方式才能生效。

2. 需要给应用开启新的进程才能进行合并,并且很容易因为内存消耗等原因合并失败。

3. 合并时占用额外磁盘空间,对于多DEX的应用来说,如果修改了多个DEX文件,就需要下发多个patch.dex与对应的classes.dex进行合并操作时这种情况会更严重,因此合并过程的失败率也会更高。

阿里Andfix方案

为何唯独Andfix能够做到即时生效呢?

原因是这样的,在app运行到一半的时候,所有需要发生变更的Class已经被加载过了,在Android上是无法对一个Class进行卸载的。而腾讯系的方案,都是让Classloader去加载新的类。如果不重启,原来的类还在虚拟机中,就无法加载新类。因此,只有在下次重启的时候,在还没走到业务逻辑之前抢先加载补丁中的新类,这样后续访问这个类时,就会Resolve为新的类。从而达到热修复的目的。

Andfix采用的方法是,在已经加载了的类中直接在native层替换掉原有方法,是在原来类的基础上进行修改的。

以Art为例,每一个Java方法在art中都对应着一个ArtMethod,ArtMethod记录了这个Java方法的所有信息,包括所属类、访问权限、代码执行地址等等。通过env->FromReflectedMethod,可以由Method对象得到这个方法对应的ArtMethod的真正起始地址。然后就可以把它强转为ArtMethod指针,从而对其所有成员进行修改。

这很C/C++ 研发的味道,实际上Andfix的核心代码replaceMethod就是用cpp写的。

APP热更新方案

面临的挑战:

因为安卓各ROM乱象的原因,ArtMethod的结构可能会不一样, ArtMethod类包含些什么其实都是在编译阶段,在运行阶段可能不是这么回事,例如sizeof(ArtMethod)可能实际在各平台就完全不一样,但是我们在编译的时候就确定了值,直接操作容易改乱内存数据导致奔溃。

有什么好的方法来解决这个问题呢?

APP热更新方案

APP热更新方案

APP热更新方案

由于f1和f2都是static方法,所以都属于direct ArtMethod Array。由于NativeStructsModel类中只存在这两个方法,因此它们肯定是相邻的。

那么我们就可以在JNI层取得它们地址的差值:

然后,就以这个methSize作为sizeof(ArtMethod),代入之前的代码。

问题就迎刃而解了。即使以后的Android版本不断修改ArtMethod的成员,只要保证ArtMethod数组仍是以线性结构排列就能完美兼容。

著:此方法最新方案并不在开源的方案中

最大的优势在于

1. BUG修复的即时性

2. 补丁包同样采用差量技术,生成的PATCH体积小

3. 对应用无侵入,几乎无性能损耗

不足:

1. 不支持新增字段,以及修改<init>方法,也不支持对资源的替换。

再来看看IOS的热更新技术:

苹果把加载动态库的功能给封了,动态库必须跟随安装包一起签名才能被加载,无法通过别的途径签名后再下发。

Wax

最早要从 Wax 这个项目开始说,大家都知道 Objective-C 有着非常强大的动态特性。比如说:

•运行时构造类和方法

•运行时替换方法的实现实际上这两个能力是非常恐怖的像脚本语言那样,文本即代码,无须编译。后来出现了一个叫做 Wax的项目(这个项目目前由阿里巴巴维护),这个项目打出的口号是用 Lua 来写 iOS 原生应用,当然现实中没有人会这样干,因为写起来实在是太痛苦了。但是鉴于 iOS 应用审核比写 Wax 还痛苦,所以 Wax 成为了做 HotFix 的最佳选择。

这个项目的做法是通过加载 Lua 脚本,动态的生成 Objective-C 的方法,通常用来替换掉出了问题的那个,Lua 脚本是可以动态下发的,所以也就实现了修复线上 bug 的使命。

当然,Wax 用起来是极为痛苦的,尤其是和 Objective-C 的类型转换。

JSPatch

iOS 7 的时候 Apple 推出了 JavaScriptCore,这是一个非常有趣的框架,他是 JS 与原生交互的桥梁,让你在原生和 JS 之间穿梭自如,现在 iOS 平台各种动态技术大多都是基于此。

JSCore 推出不久之后,一个更优秀的项目诞生了:由 bang 写的 JSPatch。这个项目无疑从各种角度碾压了 Wax,并且 JS 也比 Lua 更为人熟知,所以也就迅速替代 Wax 成为了热修复的主流选择。

JSPatch 的接入成本非常低,对项目的影响也非常小,不需要引入额外的脚本解释器(因为已经有 JSCore 了),并且 JS 写起来真的比 Lua 要爽很多。

3月8日,很多iOS开发者发了警告邮件,声称其App违规使用动态方法,责令限时整改,Jspatch一直就被打入冷宫了

这次警告事件无疑是对iOS平台Native动态化是一次严重打击,其影响甚至可能波及到Android平台,毕竟Google也是禁止加载远程代码的,并且执行更为严格,只是管不到中国的Android开发而已。

苹果是如何检测的呢,大概可以从给开发者的邮件看出来:

APP热更新方案

最后我们来看看苹果的灰度发布功能吧,对于一个花了将近5年时间做国内超大规模私有云的我来说,感受到了熟悉的味道(服务器端灰度发布也是一个套路)

  • 热修复和热更新1热更新和热修复:在线修复程序的BUG2JSPach的使用原理:OC是一门动态运行时的语言,方法的运行和对象的创建是在运行时中创建的.JSPatch正的用运行时,通过JavaScriptCore.framework作为JS引擎,从JS动态调用方法和对象到OC中,再作用NSInvocation动态调用对应的方法.例   Classclass=NSClassFromString(@"UIViewController"
  • 热修复和热更新

    1 热更新和热修复:在线修复程序的 BUG

    2 JSPach 的使用原理: OC 是一门动态运行时的语言,方法的运行和对象的创建是在运行时中创建的.JSPatch 正的用运行时,通过JavaScriptCore.framework作为 JS引擎,从 JS 动态调用方法和对象到OC 中,再作用NSInvocation动态调用对应的方法.例

        Class class = NSClassFromString(@"UIViewController");

        id controller = [class new];

        SEL selector = NSSelectorFromString(@"viewDidLoad");

        [controller performSelector:selector];

    3 使用步骤

                把JSPatch这个文件夹拖入到文件中然后将在 gitHub 下载的dome.js文件拖入到项目中,在 APPDelegate中:

    #import "AppDelegate.h"

    #import "JPEngine.h"

    #import "ViewController.h"

    @interface AppDelegate ()

    @end

    @implementation AppDelegate

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

        [JPEngine startEngine];

        NSString *jsPath = [[NSBundle mainBundle] pathForResource:@"demo.js" ofType:nil];

        [JPEngine evaluateScriptWithPath:jsPath];

        self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];

        ViewController *rootViewController = [[ViewController alloc] init];

        UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:rootViewController];

        self.window.rootViewController = navigationController;

        [self.window makeKeyAndVisible];

        return YES;

    }

    @end

    并在 ViewController.m 中实现

    - (void)viewDidLoad {

        [super viewDidLoad];

        UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(0, 100, [UIScreen mainScreen].bounds.size.width, 50)];

        [btn setTitle:@"Push JPTableViewController" forState:UIControlStateNormal];

        [btn addTarget:self action:@selector(handleBtn:) forControlEvents:UIControlEventTouchUpInside];

        [btn setBackgroundColor:[UIColor grayColor]];

        [self.view addSubview:btn];

    }

    - (void)handleBtn:(UIButton *)btn {

    }

    最后将 dome.js 中的 JSViewController 改为 ViewController 即可

    React Native

    扫盲:是一种可以同时操作前段,后台,移动端都能实时更新开发的技术

    注:通过 JavaSript运行时来创建JavaSript的代码

  • 【其他技术方案推荐】

  • 深入浅出 React Native:使用 JavaScript 构建原生应用 - 知乎 https://zhuanlan.zhihu.com/p/19996445

猜你喜欢

转载自blog.csdn.net/English0523/article/details/84643308