版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
1、Android热修复方案分析绝大部分的APP项目其实都需要一个动态化方案,来应对线上紧急bug修复发新版本的高成本.之前有利用加壳,分拆两个dex结合DexClassLoader实现了一套全量更新的热更方案.实现原理在这篇博客中有分解因为这套方案是在Java端实现,并且是全量更新所以兼容性较好,成功率较高但是在线上跑了几个月之后就碰到了瓶颈,因为随着业务的增长分拆过之后的dex文件方法数也超过 65535个,更换拆包方案的话维护成本太高同时由于没有做差异diff,就带来了 patch包过大,冗余多等缺点.正好微信的动态化方案 Tinker也开源了,就趁这个机 会先把市面上主流的热更方案汇总分析
2、下,再选一个方向深入研究一个尽量兼并兼容性扩展性及时性的方案.Github相关数据分析先统计下github上几个star比较多的开源热更方案,数据为2016年11月3号采集的,仅供参 考.从非技术的角度来分析下表的数据,根据开源时间到最近 commit时间、commit数量、issues的关闭率和Release版本数都可以看出这几个项目目前的维护情况.还有Wiki相关文档的支持.怎么看Tinker现在都是一副很生猛的架势.而阿里百川的商业化 Hotfix现在还在公测,方式用的是Andfix,把热更做成一个商业化的功能,就不清楚Andfix以后在github上的维护情况了,但是同时也证明了 An
3、dfix的价值.而Dexposed 一直没有兼容 ART,这里就先不详细分析 了.2016/11/11Aa.df izDexpoisedSuvaTinker乘源支付宝淘宝锻信开源时间2015/9/52015/3/1S2015/11/32016/9/21st世数4560324524295515commit数7714TZ毘近提交时间2016/10/282015/10/212015/11/142016/11/1ieeuse (open/closed)171/10432/3761/316/142ReIexe版本数0108文档支持有无无有实现原理An dfixAndfix实现热更的核心方法是在JNI中动
4、态hook替换目标方法,来达到即时修复bug的目的.而替换的方法则是由源apk文件和修改过的apk文件的dex做diff,反编译补丁包工具apkpatch可以看到两个 dex遍历做diff的过程.public DiffInfo diff(File newFile,File oldFile) throws lOException DexBackedDexFile n ewDexFile = DexFileFactory .lo adDexFile (n ewFile,19, true); DexBackedDexFile oldDexFile = DexFileFactory.loadDexFi
5、le(oldFile, 19, true); DiffInfo info = Difflnfo.getlnstance();boolean contains = false;for(Iterator iterator = newDexFile.getClasses().iterator(); iterator.hasNext();)DexBackedClassDef newClazz = (DexBackedClassDef)iterator.next();Set oldclasses = oldDexFile.getClasses(); for(Iterator iterator1 = ol
6、dclasses.iterator(); iterator1.hasNext();) DexBackedClassDef oldClazz = (DexBackedClassDef)iterator1.next(); if(newClazz.equals(oldClazz) compareField(newClazz, oldClazz, info); compareMethod(newClazz, oldClazz, info); contains = true;break;if(!contains) info.addAddedClasses(newClazz);return info;遍历
7、出修改过的方法加上一个MethodReplace 的注解 ( 包含要替换的目标类和目标方法),生成一个diff dex,再签上名更名为.apatch的补丁包通过更新的方式分发的各个终端处通过反编译中间 diff dex 可以看到补丁文件中对 fix method 的描述 .MethodReplace(clazz=workbench.agent.impl.NBSAgent, method=getBuildId) public static String getBuildId() return 6f3d1afc-d890-47c2-8ebe-76dc6c53050c;终端在效验过补丁
8、包的合法性后 ,则把补丁包中带有 MethodReplace 注解的方法遍历出来 ,根 据注解中的目标方法配置,将old method利用classloader加载进内存,然后交给JNI去替换old method.private void fixClass(Class clazz, ClassLoader classLoader) Method methods = clazz.getDeclaredMethods();MethodReplace methodReplace;String clz;String meth;for (Method method : methods) methodRe
9、place = method.getAnnotation(MethodReplace.class);if (methodReplace = null) continue;clz = methodReplace.clazz();meth = methodReplace.method();if (!isEmpty(clz) & !isEmpty(meth) replaceMethod(classLoader, clz, meth, method);private void replaceMethod(ClassLoader classLoader, String clz,String meth,
10、Method method) try String key = clz + + classLoader.toString();Class clazz = mFixedClass.get(key); if (clazz = null) / class not loadClass clzz = classLoader.loadClass(clz);/ initialize target classclazz = AndFix.initTargetClass(clzz); if (clazz != null) / initialize class OK mFixedClass.put(key, cl
11、azz); Method src = clazz.getDeclaredMethod(meth, method.getParameterTypes();AndFix.addReplaceMethod(src, method); catch (Exception e) Log.e(TAG , replaceMethod, e);在 Andfix.app 中可以看到 JNI 中 replaceMethod 方法 , 由于从 Lolipop 开始 Android 放弃使用 dalvik 转向 android runtime, 所以 Andfix 也要区分不同的平台进行替换 .像 Dexposed 到
12、目前为 止都没有做 ART 的兼容 .static void replaceMethod(JNIEnv* env, jclass clazz, jobject src,jobject dest) if (isArt) art_replaceMethod(env, src, dest); else dalvik_replaceMethod(env, src, dest);extern void _attribute_ (visibility (hidden) dalvik_replaceMethod(JNIEnv* env, jobject src, jobject dest) jobject
13、clazz = env-CallObjectMethod(dest, jClassMethod); ClassObject* clz = (ClassObject*) dvmDecodeIndirectRef_fnPtr( dvmThreadSelf_fnPtr(), clazz);clz-status = CLASS_INITIALIZED;Method* meth = (Method*) env-FromReflectedMethod(src);Method* target = (Method*) env-FromReflectedMethod(dest);LOGD(dalvikMetho
14、d: %s, meth-name);meth-accessFlags |= ACC_PUBLIC; meth-methodIndex = target-methodIndex; meth-jniArgInfo = target-jniArgInfo; meth-registersSize = target-registersSize; meth-outsSize = target-outsSize; meth-insSize = target-insSize;meth-prototype = target-prototype; meth-insns = target-insns; meth-n
15、ativeFunc = target-nativeFunc;由于兼容问题在 ART 的 replaceMethod 方法中对每一个不同的系统版本进行区分,分别实现 .extern void _attribute_ (visibility (hidden) art_replaceMethod(JNIEnv* env, jobject src, jobject dest) if (apilevel 23) replace_7_0(env, src, dest); else if (apilevel 22) replace_6_0(env, src, dest); else if (apilevel
16、 21) replace_5_1(env, src, dest); else if (apilevel 19) replace_5_0(env, src, dest);else replace_4_4(env, src, dest); 因为 Andfix 的方案是在 native 替换方法 ,所以稳定性和兼容性就是差一些.就 Andfix 开源项目来说在实际接入的过程中发现对 multi dex 支持不友好 , 还需要修改补丁包生成工具 apkpatch,并且apkpatch开源得也不友好,修复静态方法有问题.Nuwa由于 Qzone 只是分享了实现原理 , 并没有开源出来 .而 Nuwa 是
17、参考 Qzone 的实现方式开源的 一套方案 ,这里就主要分析Nuwa 了 .Nuwa 的修复流程并不复杂,不像 Andfix 需要在 JNI 中进行方法替换 .在 Application 中的 attachBaseContext 方法中对 Nuwa 进行初始化 ,先将 asset 路径下的 hack.apk 复制到指定位置 ,然后以加载补丁的方式加载hack.apk 至于这个 hack.apk的作用下面会讲 .public static void init(Context context) File dexDir = new File(context.getFilesDir(), DEX_D
18、IR);dexDir.mkdir();String dexPath = null;try dexPath = AssetUtils.copyAsset(context, HACK_DEX, dexDir); catch (IOException e) Log.e(TAG , copy + HACK_DEX + failed);e.printStackTrace();loadPatch(context, dexPath);加载补丁的方法主要的作用是把补丁 dex 通过反射加载到 dexElements 数组的最前端。因为 Classloader 在 findClass 的时候是按顺序遍历 dex
19、Elements(dex 数组 ), 只要 dexElement 中有该 class 就加载并停止遍历 .所以利用 Classloader 的这种特性把补丁包插入 dexElements 的首位 , 系统在findClass的时候就优先拿到补丁包中的class达到修复bug的目的.public static void loadPatch(C on text con text, String dexPath) if (context = null) Log.e(TAG, context is null);return;if (!new File(dexPath).exists() Log.e(T
20、AG, dexPath + is null);return;File dexOptDir = new File(context.getFilesDir(), DEX_OPT_DIR);dexOptDir.mkdir();try DexUtils.injectDexAtFirst(dexPath, dexOptDir.getAbsolutePath(); catch (Exception e) Log.e(TAG, inject + dexPath + failed);e.printStackTrace();public static void injectDexAtFirst(String d
21、exPath, String defaultDexOptPath) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException DexClassLoader dexClassLoader = new DexClassLoader(dexPath, defaultDexOptPath, dexPath, getPathCassLoader();Object baseDexElements = getDexElements(getPathList(getPathClas
22、sLoader();Object newDexElements = getDexElements(getPathList(dexClassLoader); Object allDexElements = combineArray(newDexElements, baseDexElements);Object pathList = getPathList(getPathClassLoader();ReflectionUtils.setField(pathList, pathList.getClass(), dexElements, allDexElements);如果只 是 把补 丁包 插 入
23、dexElements 的首 位 然后 运行 就 会有 一个 异 常 java.lang.IllegaAccessError:Class ref in pre-verified class resoved to unexpected implementation 造成这个异常的原因是因为补丁包中的类和与其有关联的类不在同一个dex 文件中 .跟踪这个异常 ,定位到 Android 源码中的 Resolve.cpp 中的 dvmResolveClass 方法,可以看到只要满 足 最 外 层 (!fromUnverifiedConstant & IS_CLASS_FLAG_SET(referrer
24、, CLASS_ISPREVERIFIED) 的 条 件 就 会 抛 出 pre-verified 的 异 常 .Qzone 就 是 从 CLASS_ISPREVERIFIED 标记入手 , 想办法让 Class 不打上 CLASS_ISPREVERIFIED 标签.ClassObject* dvmResolveClass(const ClassObject* referrer, u4 classIdx,bool fromUnverifiedConstant)if (!fromUnverifiedConstant &IS_CLASS_FLAG_SET(referrer, CLASS_ISPRE
25、VERIFIED)ClassObject* resClassCheck = resClass;if (dvmIsArrayClass(resClassCheck)resClassCheck = resClassCheck-elementClass;if (referrer-pDvmDex != resClassCheck-pDvmDex & resClassCheck-classLoader != NULL)ALOGW(Class resolved by unexpected DEX: %s(%p):%p ref %s %s(%p):%p, referrer-descriptor, refer
26、rer-classLoader, referrer-pDvmDex, resClass-descriptor, resClassCheck-descriptor, resClassCheck-classLoader, resClassCheck-pDvmDex);ALOGW(%s had used a different %s during pre-verification),referrer-descriptor, resClass-descriptor);dvmThrowIllegalAccessError(Class ref in pre-verified class resolved
27、to unexpected implementation);return NULL;return resClass;Qzone 根据 dexopt 的过程中 (DexPrepare.cpp - verifyAndOptimizeClass) 如果 dvmVerifyClass 返回 true 了 ,就会给 class 标记上 CLASS_ISPREVERIFIED. 所以我们要确保 dvmVerifyClass 返回 false, 只要不被打上 CLASS_ISPREVERIFIED 标记 ,就不会触发上述的异常 ./* Verify and/or optimize a specific cl
28、ass.*/static void verifyAndOptimizeClass(DexFile* pDexFile, ClassObject* clazz,const DexClassDef* pClassDef, bool doV erify, bool doOpt)/* First, try to verify it.*/if (doVerify) if (dvmVerifyClass(clazz) /* Set the is preverified flag in the DexClassDef. We* do it here, rather than in the ClassObje
29、ct structure,* because the DexClassDef is part of the odex file. */assert(clazz-accessFlags & JAVA_FLAGS_MASK) = pClassDef-accessFlags);(DexClassDef*)pClassDef)-accessFlags |= CLASS_ISPREVERIFIED; verified = true; else / TODO: log when in verbose modeALOGV(DexOpt: %s failed verification, classDescri
30、ptor);为了能让 dvmVerifyClass 返回 false, 我们继续跟踪这个方法 (DexVerify.app - dvmVerifyClass). 首先是过滤重复验证 ,由于补丁包加载之前是没有做过验证的,所以这个条件可以直接忽略接下来是遍历 clazz 的 directMethods( 包含构造 ,静态 ,私有方法 )和 virtualMethods, 只要这两个 数组中的方法存在有关联的对象跨 dex 文件的情况就可以让 dvmVerifyClass 返回 false./* Verify a class.* By the time we get here, the value
31、 of gDvm.classVerifyMode should already* have been factored in. If you want to call into the verifier even* though verification is disabled, thats your business.* Returns true on success.*/bool dvmVerifyClass(ClassObject* clazz)int i;if (dvmIsClassVerified(clazz) ALOGD(Ignoring duplicate verify atte
32、mpt on %s, clazz-descriptor); return true;for (i = 0; i directMethodCount; i+) if (!verifyMethod(&clazz-directMethodsi) LOG_VFY(Verifier rejected class %s, clazz-descriptor);return false;for (i = 0; i virtualMethodCount; i+) if (!verifyMethod(&clazz-virtualMethodsi) LOG_VFY(Verifier rejected class %
33、s, clazz-descriptor);return false;return true;Qzone 给出的方案是在 gradle 插件中对除了 Application 子类之外的所有类 (包含 Jar 包中的 ) 的构造方法里面通过 ASM 动态注入一个独立 dex 中 Class 的引用 ,这样这些类就不会被打上 CLASS_ISPREVERIFIED, 就可以对其进行热更 .把 Application 排除之外是因为这套方案是在 Application 中加载 dex, Application 启动的时候是找不到这个 dex 中的 clazz 的 .同时gradle插件遍历目标 cla
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 二零二五年度智能停车系统车库交易合同2篇
- 二零二五年度家政服务与家庭旅游策划合同3篇
- 二零二五年度叉车维修保养与维修设备租赁合同2篇
- 2025年度综合体物业房使用及公共设施维护协议3篇
- 二零二五年度大棚养殖废弃物资源化利用合作协议2篇
- 二零二五年度文化旅游项目借款及旅游资源担保服务协议3篇
- 二零二五年度保障性住房置换合同范本
- 二零二五年度ISO9001质量管理体系认证咨询与实施合同3篇
- 二零二五年度加油站油品供应与信息化建设合同3篇
- 船舶管系课程设计
- 河南省郑州高新技术产业开发区2023-2024学年三年级上学期1月期末科学试题
- 女装行业退货率分析
- 领导沟通的艺术
- 纯视觉方案算法
- 道士述职报告
- 绿色贷款培训课件
- 2024年七年级语文上学期期末作文题目及范文汇编
- 云南省昆明市五华区2023-2024学年九年级上学期期末英语试卷+
- 2023年生产运营副总经理年度总结及下一年计划
- 2023年中考语文标点符号(顿号)练习(含答案)
- 施工图审查招标文件范文
评论
0/150
提交评论