Android热更新方案Robust

美团是中国最大的O2O交易平台,目前已拥有近6亿用户,合作各类商户达432万,订单峰值突破1150万单。美团App是平台主要的入口之一,O2O交易场景的复杂性决定了App稳定性要达到近乎苛刻的要求。用户到店消费买优惠券时死活下不了单,定外卖一个明显可用的红包怎么点也选不中,上了一个新活动用户一点就Crash……过去发生过的这些画面太美不敢想象。客户端相对Web版最大的短板就是有发版的概念,对线上事故很难有即时生效的解决方式,每次发版都如临深渊如履薄冰,毕竟就算再完善的开发测试流程也无法保证不会将Bug带到线上。

从去年开始,Android平台出现了一些优秀的热更新方案,主要可以分为两类:一类是基于multidex的热更新框架,包括Nuwa、Tinker等;另一类就是native hook方案,如阿里开源的Andfix和Dexposed。这样客户端也有了实时修复线上问题的可能。但经过调研之后,我们发现上述方案或多或少都有一些问题,基于native hook的方案:需要针对dalvik虚拟机和art虚拟机做适配,需要考虑指令集的兼容问题,需要native代码支持,兼容性上会有一定的影响;基于Multidex的方案,需要反射更改DexElements,改变Dex的加载顺序,这使得patch需要在下次启动时才能生效,实时性就受到了影响,同时这种方案在android N [speed-profile]编译模式下可能会有问题,可以参考Android N混合编译与对热补丁影响解析。考虑到美团Android用户机型分布的碎片化,很难有一个方案能覆盖所有机型。 去年底的Android Dev Summit上,Google高调发布了Android Studio 2.0,其中最重要的新特性Instant Run,实现了对代码修改的实时生效(热插拔)。我们在了解Instant Run原理之后,实现了一个兼容性更强的热更新方案,这就是产品化的hotpatch框架--Robust。

Robust插件对每个产品代码的每个函数都在编译打包阶段自动的插入了一段代码,插入过程对业务开发是完全透明。如State.java的getIndex函数:

public long getIndex() {return 100;}

被处理成如下的实现:

public static ChangeQuickRedirect changeQuickRedirect;public long getIndex() {if(changeQuickRedirect != null) {//PatchProxy中封装了获取当前className和methodName的逻辑,并在其内部最终调用了changeQuickRedirect的对应函数if(PatchProxy.isSupport(new Object[0], this, changeQuickRedirect, false)) {return ((Long)PatchProxy.accessDispatch(new Object[0], this, changeQuickRedirect, false)).longValue();}}return 100L;}

可以看到Robust为每个class增加了个类型为ChangeQuickRedirect的静态成员,而在每个方法前都插入了使用changeQuickRedirect相关的逻辑,当 changeQuickRedirect不为null时,可能会执行到accessDispatch从而替换掉之前老的逻辑,达到fix的目的。 如果需将getIndex函数的返回值改为return 106,那么对应生成的patch,主要包含两个class:PatchesInfoImpl.java和StatePatch.java。 PatchesInfoImpl.java:

public class PatchesInfoImpl implements PatchesInfo {public List<PatchedClassInfo> getPatchedClassesInfo() {List<PatchedClassInfo> patchedClassesInfos = new ArrayList<PatchedClassInfo>();PatchedClassInfo patchedClass = new PatchedClassInfo("com.meituan.sample.d", StatePatch.class.getCanonicalName());patchedClassesInfos.add(patchedClass);return patchedClassesInfos;}
}

StatePatch.java:

public class StatePatch implements ChangeQuickRedirect {@Overridepublic Object accessDispatch(String methodSignature, Object[] paramArrayOfObject) {String[] signature = methodSignature.split(":");if (TextUtils.equals(signature[1], "a")) {//long getIndex() -> areturn 106;}return null;}@Overridepublic boolean isSupport(String methodSignature, Object[] paramArrayOfObject) {String[] signature = methodSignature.split(":");if (TextUtils.equals(signature[1], "a")) {//long getIndex() -> areturn true;}return false;}
}

客户端拿到含有PatchesInfoImpl.java和StatePatch.java的patch.dex后,用DexClassLoader加载patch.dex,反射拿到PatchesInfoImpl.java这个class。拿到后,创建这个class的一个对象。然后通过这个对象的getPatchedClassesInfo函数,知道需要patch的class为com.meituan.sample.d(com.meituan.sample.State混淆后的名字),再反射得到当前运行环境中的com.meituan.sample.d class,将其中的changeQuickRedirect字段赋值为用patch.dex中的StatePatch.java这个class new出来的对象。这就是打patch的主要过程。通过原理分析,其实Robust只是在正常的使用DexClassLoader,所以可以说这套框架是没有兼容性问题的。

大体流程如下:

OK,到这里Robust原理就介绍完了。很简单是不是?而且sample这个例子中也验证成功了。难道一切这么顺利?其实现实并不是这样,我们将这套实现用到美团的主App时,问题出现了:

Conversion to Dalvik format failed:Unable to execute dex: method ID not in [0, 0xffff]: 65536

居然不能打出包来了!从原理上分析,除了引入的patch过程aar外,我们这套实现是不会增加别的方法的,而且引入的那个aar的方法才100个左右,怎么会造成美团的mainDex超过65536呢?进一步分析,我们一共处理7万多个函数,导致最后方法数总共增加7661个。这是为什么呢?

看下patch前后的dex对比:

针对com.meituan.android.order.adapter.OrderCenterListAdapter.java分析一下,发现进行hotpatch之后增加了如下6个方法:

public boolean isEditMode() {return isEditMode;}
private int incrementDelCount() {return delCount.incrementAndGet();}
private boolean isNeedDisplayRemainingTime(OrderData orderData) {return null != orderData.remindtime && getRemainingTimeMillis(orderData.remindtime) > 0;}
private boolean isNeedDisplayUnclickableButton(OrderData orderData) {return null != orderData.remindtime && getRemainingTimeMillis(orderData.remindtime) <= 0;}
private boolean isNeedDisplayExpiring(boolean expiring) {return expiring && isNeedDisplayExpiring;}
private View getViewByTemplate(int template, View convertView, ViewGroup parent) {View view = null;switch (template) {case TEMPLATE_DEFALUT:default:view = mInflater.inflate(R.layout.order_center_list_item, null);}return view;}

但是这些多出来的函数其实就在原来的产品代码中,为什么没有Robust的情况下不见了,而使用了插件后又出现在最终的class中了呢?只有一个可能,就是ProGuard的内联受到了影响。使用了Robust插件后,原来能被ProGuard内联的函数不能被内联了。看了下ProGuard的Optimizer.java的相关片段:

if (methodInliningUnique) {// Inline methods that are only invoked once.programClassPool.classesAccept(new AllMethodVisitor(new AllAttributeVisitor(new MethodInliner(configuration.microEdition,configuration.allowAccessModification,true,methodInliningUniqueCounter))));
}
if (methodInliningShort) {// Inline short methods.programClassPool.classesAccept(new AllMethodVisitor(new AllAttributeVisitor(new MethodInliner(configuration.microEdition,configuration.allowAccessModification,false,methodInliningShortCounter))));
}

通过注释可以看出,如果只被调用一次或者足够小的函数,都可能被内联。深入分析代码,我们发现确实如此,只被调用了一次的私有函数、只有一行函数体的函数(比如get、set函数等)都极可能内联。前面com.meituan.android.order.adapter.OrderCenterListAdapter.java多出的那6个函数也证明了这一点。知道原因了就能有解决问题的思路。 其实仔细思考下,那些可能被内联的只有一行函数体的函数,真的有被插件处理的必要吗?别说一行代码的函数出问题的可能性小,就算出问题了也可以通过patch内联它的那个函数来解决问题,或者patch这一行代码调用的那个函数。只调用了一次的函数其实是一样的。所以通过分析,这样的函数其实是可以不被插件处理的。那么有了这个认识,我们对插件做了处理函数的判断,跳过被ProGuard内联可能性比较大的函数。重新在团购试了一次,这次apk顺利的打包出来了。通过对打出来apk中的dex做分析,发现优化后的插件还是影响了内联效果,不过只导致方法数增加了不到1000个,所以算是临时简单的解决了这个问题。

原理上,Robust是为每个函数都插入了一段逻辑,为每个class插入了ChangeQuickRedirect的字段,所以最终肯定会增加apk的体积。以美团主App为例,平均一个函数会比原来增加17.47个字节,整个App中我们一共处理了6万多个函数,导致包大小由原来的19.71M增加到了20.73M。有些class没有必要添加ChangeQuickRedirect字段,以后可以通过将这些class过滤掉的方式来做优化。 Robust在每个方法前都加上了额外的逻辑,那对性能上有什么影响呢?

从图中可以看到,对一个只有内存运算的函数,处理前后分别执行10万次的时间增加了128ms。这是在华为4A上的测试结果。 对启动速度上的影响:

在同一个机器上的结果,处理前后的启动时间相差了5ms。

再来看看补丁本身。要制作出补丁,我们可能会面临如下两个问题: 1. 如何解决混淆问题? 2. 被补的函数中使用了super相关的调用怎么办?

其实混淆的问题比较好处理。先针对混淆前的代码生成patch.class,然后利用生成release包时对应的mapping文件中的class的映射关系,对patch.class做字符串上的处理,让它使用线上运行环境中混淆的class。 被补的函数中使用了super相关的调用怎么办?比如某个Activity的onCreate方法中需要调用super.onCreate,而现在这个bad.Class的badMethod就是这个Activity的onCreate方法,那么在patched.class的patchedMethod中如何通过这个Activity的对象,调用它父类的onCreate方法呢?通过分析Instant Run对这个问题的处理,发现它是在每个class中都添加了一个代理函数,专门来处理super的问题的。为每个class都增加一个函数无疑会增加总的方法数,这样做肯定会遇到65536这个问题。所以直接使用Instant Run的做法显然是不可取的。 在Java中super是个关键字,也无法通过别的对象来访问到。看来,想直接在patched.java代码中通过Activity的对象调用到它父类的onCreate方法有点不太可能了。不过通过对class文件做分析,发现普通的函数调用是使用JVM指令集的invokevirtual指令,而super.onCreate的调用使用的是invokesuper指令。那是不是将class文件中这个调用的指令改为invokesuper就好了?看如下的例子: 产品代码SuperClass.java:

public class SuperClass {String uuid;public void setUuid(String id) {uuid = id;}public void thisIsSuper() {Log.d("SuperClass", "thisIsSuper "+uuid);}
}

产品代码TestSuperClass.java:

public class TestSuperClass extends SuperClass{String subUuid;public void setSubUuid(String id) {subUuid = id;}@Overridepublic void thisIsSuper() {Log.d("TestSuperClass", "thisIsSuper no call");}
}

TestSuperPatch.java是DexClassLoader将要加载的代码:

public class TestSuperPatch {public static void testSuperCall() {TestSuperClass testSuperClass = new TestSuperClass();String t = UUID.randomUUID().toString();Log.d("TestSuperPatch", "UUID " + t);testSuperClass.setUuid(t);testSuperClass.thisIsSuper();}
}

对TestSuperPatch.class的testSuperClass.thisIsSuper()调用做invokesuper的替换,并且将invokesuper的调用作用在testSuperClass这个对象上,然后加载运行:

Caused by: java.lang.NoSuchMethodError: No super method thisIsSuper()V in class Lcom/meituan/sample/TestSuperClass; or its super classes (declaration of 'com.meituan.sample.TestSuperClass' appears in /data/app/com.meituan.robust.sample-3/base.apk)

报错信息说在TestSuperClass和TestSuperClass的父类中没有找到thisIsSuper()V函数!但是实际上TestSuperClass和父类中是存在thisIsSuper()V函数的,而且通过apk反编译看也确实存在的,那怎么就找不到呢?分析invokesuper指令的实现,发现系统会在执行指令所在的class的父类中去找需要调用的方法,所以要将TestSuperPatch跟TestSuperClass一样作为SuperClass的子类。修改如下:

public class TestSuperPatch extends SuperClass {...
}

然后再做一次尝试:

08-11 09:12:03.012 1787-1787/? D/TestSuperPatch: UUID c5216480-5c3a-4990-896d-58c3696170c5
08-11 09:12:03.012 1787-1787/? D/SuperClass: thisIsSuper c5216480-5c3a-4990-896d-58c3696170c5

看一下testSuperCall的实现,将UUID.randomUUID().toString()的结果,通过setUuid赋值给了testSuperClass这个对象的父类的uuid字段。从日志可以看出,对testSuperClass.thisIsSuper处理后,确实是调用到了testSuperClass这个对象的super的thisIsSuper函数。OK,super的问题看来解决了,而且这种方式不会增加方法数。

Robust 靠谱吗?

尝试修个线上的问题,我们是在07.14下午17:00多的时候上线的补丁,我们可以看到接下来的几天一直到07.17号将补丁下线,这个线上问题得到了明显的修复,补丁下线后看到07.18号这个问题又明显上升了。直到07.18号下班前又重新上线补丁。

补丁的兼容性和成功率如何?通过以上的理论分析,可以看到这套实现基本没有兼容性问题,实际上线的数据如下:

先简单解释下这几个指标: 补丁列表拉取成功率=拉取补丁列表成功的用户/尝试拉取补丁列表的用户 补丁下载成功率=下载补丁成功的用户/补丁列表拉取成功的用户 patch应用成功率=patch成功的用户/补丁下载成功的用户

通过这个表能够看出,我们的patch信息拉取的成功最低,平均97%多,这是因为实际的网络原因,而下载成功后的patch成功率是一直在99.8%以上。而且我们做的是无差别下发,服务端没有做任何针对机型版本的过滤,线上的结果再次证明了Robust的高兼容性。

目前业界已有的Android App热更新方案,包括Multidesk和native hook两类,都存在一些兼容性问题。为此我们借鉴Instant Run原理,实现了一个兼容性更强的热更新方案--Robust。Robust除了高兼容性之外,还有实时生效的优势。so和资源的替换目前暂时未做实现,但是从框架上来说未来是完全有能力支持的。当然,这套方案虽然对开发者是透明的,但毕竟在编译阶段有插件侵入了产品代码,对运行效率、方法数、包体积还是产生了一些副作用。这也是我们下一步努力的方向。

  • Instant Run, Android Tools Project Site, http://tools.android.com/tech-docs/instant-run.
  • Oracle, The Java Virtual Machine Instruction Set, https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html.
  • Oracle, ClassLoader, https://docs.oracle.com/javase/7/docs/api/java/lang/ClassLoader.html).
  • ltshddx, https://github.com/ltshddx/jaop).
  • w4lle, Android热补丁之AndFix原理解析.
  • shwenzhang, Android N混合编译与对热补丁影响解析.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/478154.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

会议研讨 | TF45: 知识图谱新技术、新场景、新应用

基于“新基建”面向新产业、新业态和新模式的背景下&#xff0c;知识图谱作为人工智能的重要基石正在火热发展中。新的知识图谱技术在新的场景和应用中使人工智能从感知智能逐渐向认知智能过渡。知识图谱与产业的结合愈加紧密&#xff0c;除了在搜索与推荐、知识问答等通用领域…

LeetCode 653. 两数之和 IV - 输入 BST(二叉搜索树迭代器双指针)

文章目录1. 题目2. 解题1. 题目 给定一个二叉搜索树和一个目标结果&#xff0c;如果 BST 中存在两个元素且它们的和等于给定的目标结果&#xff0c;则返回 true。 案例 1: 输入: 5/ \3 6/ \ \ 2 4 7Target 9 输出: True案例 2: 输入: 5/ \3 6/ \ \ 2 4 7Tar…

盘点来自工业界的GPU共享方案

文 | 阎姝含源 | 极市平台进年来工业界一直孜孜不倦地寻求提升GPU利用率的方案&#xff0c;能被更多用户理解和使用的GPU共享走进工程师的视野中。本文将总结目前有公开PR的、来自工业界的部分GPU容器计算共享方案&#xff0c;看看工业界对GPU共享的定位和需求。本文将依旧着眼…

百面机器学习|第二章模型评估知识点 蓝白绛

前言 如果你能找到这里&#xff0c;真是我的幸运~这里是蓝白绛的学习笔记&#xff0c;本集合主要针对《百面机器学习——算法工程师带你去面试》这本书。主要记录我认为重要的知识点&#xff0c;希望对大家有帮助。 第二章 模型评估 1、评估指标的局限性 准确率(Accuracy)&…

美团外卖订单中心的演进

美团外卖从2013年9月成交第一单以来&#xff0c;已走过了三个年头。期间&#xff0c;业务飞速发展&#xff0c;美团外卖由日均几单发展为日均500万单&#xff08;9月11日已突破600万&#xff09;的大型O2O互联网外卖服务平台。平台支持的品类也由最初外卖单品拓展为全品类。 随…

论文浅尝 | 基于知识图谱的智能调研方法(DI佳作)

转载公众号 | 数据智能英文刊题目&#xff1a;A Knowledge Graph Based Approach to Social Science Surveys引用&#xff1a;Z. Pan, Z.J., et al.: A Knowledge Graph Based Approach to Social Science Surveys. Data Intelligence 3(3). doi: 10.1162/dint_a_00107文章摘要…

NYU Google: 知识蒸馏无处不在,但它真的有用吗?

文 | 小伟编 | 小轶导师: 小伟&#xff0c;听说你对知识蒸馏比较了解&#xff0c;你来给我说说知识蒸馏有什么用&#xff1f;我: 知识蒸馏是一种很典型的模型压缩的方法&#xff0c;我们可以用它来有效地从大型教师模型学习小型学生模型&#xff0c;并且学生模型的性能也很不错…

pyscript+py-env实现python+html效果

参考链接&#xff1a;https://developer.aliyun.com/article/976083?spma2c6h.12873581.group.dArticle976083.3a8057c73DINVs 今天我们要介绍的东西&#xff0c;叫做PyScript&#xff0c;使用它&#xff0c;不需要安装任何软件。只要有一个记事本&#xff0c;就能写一段HTMLP…

美团的DBProxy实践

本文整理自美团技术沙龙第10期&#xff1a;数据库技术架构与实践。 美团技术沙龙由美团技术团队主办&#xff0c;每月一期&#xff0c;每期沙龙邀请美团及其它互联网公司的技术专家分享来自一线的实践经验&#xff0c;覆盖各主要技术领域。 本次沙龙主要围绕数据库相关的主题&a…

LeetCode 606. 根据二叉树创建字符串(递归)

文章目录1. 题目2. 递归解题1. 题目 你需要采用前序遍历的方式&#xff0c;将一个二叉树转换成一个由括号和整数组成的字符串。 空节点则用一对空括号 “()” 表示。而且你需要省略所有不影响字符串与原始二叉树之间的一对一映射关系的空括号对。 示例 1: 输入: 二叉树: [1,…

论文浅尝 | 面向开放域的无监督实体对齐

笔记整理 | 谭亦鸣&#xff0c;东南大学博士生来源&#xff1a;DASFAA’21链接&#xff1a;https://arxiv.org/pdf/2101.10535.pdf概述与动机知识图谱对齐的目的是建立两个不同知识图谱之间实体的对应关系&#xff0c;如图1&#xff0c;本文作者发现现有的实体对齐方法依赖于标…

聊聊推荐系统

这两天&#xff0c;有种把某宝卸载的冲动&#xff0c;它的“猜你喜欢”推荐简直是我肚子里的蛔虫&#xff0c;每次看都忍不住剁手&#xff0c;钱包就这么日渐消瘦……但从技术的角度想想&#xff0c;不得不说阿里的推荐系统的确做得不错。其实&#xff0c;除了电商平台&#xf…

LeetCode 695. 岛屿的最大面积(图的BFS/DFS)

文章目录1. 题目2. 解题2.1 BFS广度优先搜索2.2 DFS深度优先搜索1. 题目 给定一个包含了一些 0 和 1的非空二维数组 grid , 一个 岛屿 是由四个方向 (水平或垂直) 的 1 (代表土地) 构成的组合。你可以假设二维矩阵的四个边缘都被水包围着。 找到给定的二维数组中最大的岛屿面…

Neo4j:入门基础(一)之安装与使用

原文链接&#xff1a;https://blog.csdn.net/sinat_36226553/article/details/108541370 # 图数据库 链接&#xff1a;什么是原生(Native)图数据库 一般认为具有“无索引邻接”特性的图数据库才称为原生图数据库 链接&#xff1a;常用的图数据库 图存储可以分为属性图、三元组…

论文浅尝 | 问题多样性对于问答的帮助

笔记整理 | 毕胜 东南大学在读博士&#xff0c;研究方向&#xff1a;自然语言处理 知识图谱问题生成通过生成一些合成的问题作为训练语料有效提高了问答系统的效果&#xff0c;本文的研究点是&#xff1a;在QG中&#xff0c;生成问题的文本多样性是否对下游的QA有帮助&#xf…

RDS平台介绍

本文整理自美团点评技术沙龙第10期&#xff1a;数据库技术架构与实践。 美团点评技术沙龙由美团点评技术团队主办&#xff0c;每期沙龙邀请美团点评及其它互联网公司的技术专家分享来自一线的实践经验&#xff0c;覆盖各主要技术领域。 本次沙龙主要围绕数据库相关的主题&#…

你已经是一个成熟的地图了,该学会帮我...

文 | 小戏当我们打开百度地图&#xff0c;选定到达目的地&#xff0c;导航软件里的小姐姐马上就用不紧不慢的语调告诉我们“准备出发&#xff0c;全程12公里&#xff0c;预计需要30分钟……”但一看手表上显示的不风驰电掣铁定迟到的时间&#xff0c;肯定不允许我们和导航软件里…

LeetCode 427. 建立四叉树(递归)

1. 题目 我们想要使用一棵四叉树来储存一个 N x N 的布尔值网络。网络中每一格的值只会是真或假。树的根结点代表整个网络。对于每个结点, 它将被分等成四个孩子结点直到这个区域内的值都是相同的. 每个结点还有另外两个布尔变量: isLeaf 和 val。isLeaf 当这个节点是一个叶子…

基于 KIF 的 iOS UI 自动化测试和持续集成

客户端 UI 自动化测试是大多数测试团队的研究重点&#xff0c;本文介绍猫眼测试团队在猫眼 iOS 客户端实践的基于 KIF 的 UI 自动化测试和持续集成过程。 一、测试框架的选择 iOS UI 自动化测试框架有不少&#xff0c;其中 UI Automation 是 Apple 早期提供的 UI 自动化测试解决…

论文浅尝 | 利用冻结语言模型的多模态少样本学习

笔记整理 | 李磊&#xff0c;浙江大学硕士&#xff0c;研究方向为自然语言处理链接&#xff1a;https://arxiv.org/abs/2106.13884动机大规模的自回归语言模型&#xff08;如GPT&#xff09;在预训练阶段学习到了大量的知识&#xff0c;具有很好的学习新任务的能力&#xff0c…