Android热修复方案_第1页
Android热修复方案_第2页
Android热修复方案_第3页
Android热修复方案_第4页
已阅读5页,还剩5页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

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 ,把热更做成一个商业化的功能,就不

3、清楚Andfix以后在 github 上的维护情况了 ,但是同时也证明了 Andfix的价值 .而 Dexposed 一直没有兼容 ART, 这里就先不详细分析了.实现原理AndfixAndfix 实现热更的核心方法是在而替换的方法则是由源apkJNI 中动态文件和修改过的hook 替换目标方法 ,来达到即时修复bug 的目的 .apk 文件的dex 做 diff, 反编译补丁包工具apkpatch 可以看到两个dex遍历做diff的过程.public DiffInfo diff(File newFile, File oldFile) throws IOException DexBackedD

4、exFile newDexFile = DexFileFactory.loadDexFile(newFile, 19, true);DexBackedDexFile oldDexFile = DexFileFactory.loadDexFile(oldFile, 19, true);DiffInfo info = DiffInfo.getInstance();boolean contains = false;for(Iterator iterator = newDexFile.getClasses().iterator(); iterator.hasNext();)DexBackedClass

5、Def newClazz = (DexBackedClassDef)iterator.next();Set oldclasses = oldDexFile.getClasses();for(Iterator iterator1 = oldclasses.iterator(); iterator1.hasNext();)DexBackedClassDef oldClazz = (DexBackedClassDef)iterator1.next(); if(newClazz.equals(oldClazz)compareField(newClazz, oldClazz, info);compare

6、Method(newClazz, oldClazz, info);contains = true;break;if(!contains)info.addAddedClasses(newClazz);return info;遍历出修改过的方法加上一个MethodReplace 的注解 (包含要替换的目标类和目标方法),生成一个 diffdex,再签上名更名为.apatch 的补丁包通过更新的方式分发的各个终端处.通过反编译中间 diff dex 可以看到补丁文件中对fix method 的描述 .public static String getBuildId() return "6f3

7、d1afc-d890-47c2-8ebe-76dc6c53050c"终端在效验过补丁包的合法性后据注解中的目标方法配置,将method.,则把补丁包中带有MethodReplaceold method 利用 classloader 加载进内存注解的方法遍历出来,然后交给 JNI 去替换,根oldprivate void fixClass(Class<?> clazz, ClassLoader classLoader) Method methods = clazz.getDeclaredMethods(); MethodReplace methodReplace;Strin

8、g clz;String meth;for (Method method : methods) methodReplace = 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

9、 replaceMethod(ClassLoader classLoader, String clz, String meth, 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 =

10、 AndFix.initTargetClass(clzz);if (clazz != null) / initialize class OKmFixedClass.put(key, clazz);Method src = clazz.getDeclaredMethod(meth,method.getParameterTypes();AndFix.addReplaceMethod(src, method); catch (Exception e) Log.e(TAG , "replaceMethod", e);在 Andfix.app 中可以看到 JNI 中 replaceM

11、ethod 方法 ,由于从 Lolipop 开始 Android 放弃使用dalvik 转向 android runtime, 所以 Andfix 也要区分不同的平台进行替换 .像 Dexposed 到目前为止都没有做 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 _

12、attribute_ (visibility ("hidden") dalvik_replaceMethod( JNIEnv* env, jobject src, jobject dest) jobject clazz = env->CallObjectMethod(dest, jClassMethod); ClassObject* clz = (ClassObject*) dvmDecodeIndirectRef_fnPtr(dvmThreadSelf_fnPtr(), clazz);clz->status = CLASS_INITIALIZED;Method

13、* meth = (Method*) env->FromReflectedMethod(src); Method* target = (Method*) env->FromReflectedMethod(dest);LOGD("dalvikMethod: %s", meth->name);meth->accessFlags |= ACC_PUBLIC;meth->methodIndex = target->methodIndex;meth->jniArgInfo = target->jniArgInfo;meth->re

14、gistersSize = target->registersSize;meth->outsSize = target->outsSize;meth->insSize = target->insSize;meth->prototype = target->prototype;meth->insns = target->insns;meth->nativeFunc = target->nativeFunc;由于兼容问题在ART 的 replaceMethod 方法中对每一个不同的系统版本进行区分extern void _attri

15、bute_ (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 > 21) replace_5_1(env, src, dest); else if (apilevel > 19) replace_5_0(

16、env, src, dest);elsereplace_4_4(env, src, dest);,分别实现 .因为 Andfix的方案是在native 替换方法 ,所以稳定性和兼容性就是差一些.就 Andfix 开源项目来说在实际接入的过程中发现对multidex 支持不友好,还需要修改补丁包生成工具apkpatch,并且 apkpatch 开源得也不友好,修复静态方法有问题.Nuwa由于 Qzone 只是分享了实现原理,并没有开源出来.而 Nuwa 是参考 Qzone 的实现方式开源的一套方案 ,这里就主要分析Nuwa 了 .Nuwa 的修复流程并不复杂,不像Andfix 需要在 JNI 中

17、进行方法替换 .在 Application中的 attachBaseContext 方法中对Nuwa 进行初始化 ,先将 asset路径下的hack.apk 复制到指定位置,然后以加载补丁的方式加载hack.apk 至于这个hack.apk的作用下面会讲.public static void init(Context context) File dexDir = new File(context.getFilesDir(), DEX_DIR);dexDir.mkdir();String dexPath = null;try dexPath = AssetUtils.copyAsset(cont

18、ext, HACK_DEX, dexDir); catch (IOException e) Log.e(TAG , "copy " + HACK_DEX + " failed");e.printStackTrace();loadPatch(context, dexPath);加载补丁的方法主要的作用是把补丁dex 通过反射加载到dexElements数组的最前端。因为Classloader 在 findClass 的时候是按顺序遍历dexElements(dex 数组 ),只要 dexElement 中有该class 就加载并停止遍历.所以利用Class

19、loader 的这种特性把补丁包插入dexElements 的首位系统在 findClass 的时候就优先拿到补丁包中的class,达到修复bug 的目的 .,public static void loadPatch(Context context, String dexPath) if (context = null) Log.e(TAG , "context is null");return;if (!new File(dexPath).exists() Log.e(TAG , dexPath + " is null");return;File de

20、xOptDir = 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 dexPath, St

21、ring defaultDexOptPath) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException Object baseDexElements = getDexElements(getPathList(getPathClassLoader();Object newDexElements = getDexElements(getPathList(dexClassLoader);Object allDexElements = combineArray(newDexElements, baseDex

22、Elements);Object pathList = getPathList(getPathClassLoader();ReflectionUtils.setField(pathList, pathList.getClass(), "dexElements", allDexElements);如 果 只 是 把 补 丁 包 插 入dexElements的 首 位 然 后 运 行 就 会 有 一 个 异 常造成这个异常的原因是因为补丁包中的类和与其有关联的类不在同一个dex 文件中 .跟踪这个异常 ,定位到 Android 源码中的Resolve.cpp 中的 dvmRes

23、olveClass 方法,可以看到只要满足最外层(!fromUnverifiedConstant&&IS_CLASS_FLAG_SET(referrer,CLASS_ISPREVERIFIED)CLASS_ISPREVERIFIED的 条 件 就 会 抛 出pre-verified的 异 常 .Qzone就 是 从标记入手 , 想办法让 Class 不打上 CLASS_ISPREVERIFIED标签 .ClassObject* dvmResolveClass(const ClassObject* referrer, u4 classIdx, bool fromUnverifie

24、dConstant).if (!fromUnverifiedConstant &&IS_CLASS_FLAG_SET(referrer, CLASS_ISPREVERIFIED)ClassObject* resClassCheck = resClass;if (dvmIsArrayClass(resClassCheck)resClassCheck = resClassCheck->elementClass;if (referrer->pDvmDex != resClassCheck->pDvmDex &&resClassCheck->cl

25、assLoader != NULL)ALOGW("Class resolved by unexpected DEX:"" %s(%p):%p ref %s %s(%p):%p", referrer->descriptor, referrer->classLoader, referrer->pDvmDex,resClass->descriptor, resClassCheck->descriptor, resClassCheck->classLoader, resClassCheck->pDvmDex);ALOGW

26、("(%s had used a different %s during pre-verification)",referrer->descriptor, resClass->descriptor);dvmThrowIllegalAccessError("Class ref in pre-verified class resolved to unexpected ""implementation");return NULL;.return resClass;Qzone 根据 dexopt 的过程中 (DexPrepare.

27、cpp -> verifyAndOptimizeClass) 如果 dvmVerifyClass 返回 true 了 ,就会给 class 标记上 CLASS_ISPREVERIFIED. 所以我们要确保 dvmVerifyClass 返回 false, 只要不被打上 CLASS_ISPREVERIFIED 标记 ,就不会触发上述的异常 ./* Verify and/or optimize a specific class.*/static void verifyAndOptimizeClass(DexFile* pDexFile, ClassObject* clazz, const D

28、exClassDef* pClassDef, bool doV erify, bool doOpt)./* First, try to verify it.*/if (doVerify) if (dvmVerifyClass(clazz) /* Set the "is preverified" flag in the DexClassDef.* do it here, rather than in the ClassObject structure,* because the DexClassDef is part of the odex file.We*/assert(c

29、lazz->accessFlags & JAVA_FLAGS_MASK) =pClassDef->accessFlags);(DexClassDef*)pClassDef)->accessFlags |= CLASS_ISPREVERIFIED;verified = true; else / TODO: log when in verbose modeALOGV("DexOpt: '%s' failed verification", classDescriptor);.为了能让 dvmVerifyClass 返回 false,我们继

30、续跟踪这个方法(DexVerify.app -> dvmVerifyClass).首先是过滤重复验证,由于补丁包加载之前是没有做过验证的,所以这个条件可以直接忽略.接下来是遍历clazz 的 directMethods( 包含构造 ,静态 ,私有方法 )和 virtualMethods, 只要这两个数组中的方法存在有关联的对象跨dex 文件的情况就可以让dvmVerifyClass 返回 false./* Verify a class.* By the time we get here, the value of gDvm.classVerifyMode should already*

31、have been factored in. If you want to call into the verifier even* though verification is disabled, that's your business.* Returns "true" on success.*/bool dvmVerifyClass(ClassObject* clazz)int i;if (dvmIsClassVerified(clazz) ALOGD("Ignoring duplicate verify attempt on %s", c

32、lazz->descriptor); return true;for (i = 0; i < clazz->directMethodCount; i+) if (!verifyMethod(&clazz->directMethodsi) LOG_VFY("Verifier rejected class %s", clazz->descriptor);return false;for (i = 0; i < clazz->virtualMethodCount; i+) if (!verifyMethod(&clazz->virtualMethodsi) LOG_VFY("Verifier rejected class %s", clazz->descriptor);return false;return true;Qzone 给出的方案是在gradle 插件中对除了Application子类之外的所有类(包含 Jar 包中的 )的构造方法里面通过ASM动态注入一个独立dex 中Class 的引用,这样这些类就不会被打上CLASS_ISPREVERIFIED,就可以对其进行热更.把 App

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论