很早之前就想深入的研究和学习一下热修复,由于时间的原因一直拖着,现在才执笔弄起来。


Android而更新系列:
Android热更新一:JAVA的类加载机制
Android热更新二:理解Java反射
Android热更新三:Android类加载机制
Android热更新四:热修复机制
Android热更新五:四大热修复方案分析
Android热更新六:Qzone热更新原理
Android热更新七:Tinker热更新原理
Android热更新八:AndFix热更新原理
Android热更新九:Robust热更新原理
Android热更新十:自己写一个Android热修复


经过之前分析了各大热修复的实现原理,参考原理,我们来写一个属于自己的Android热修复吧。

一. 热修复简述。

所谓热修复,就是已经上线APP发现了Bug,不需要花大精力发布新版本,即可通过在线下载补丁并且修复Bug。

热修复的基本原理:

Android框架中存在一个数组,它的作用是维护全部的dex文件(我们写的类的二进制表述方式,用来给安卓虚拟机加载),安卓虚拟机会根据需要从该数组按照自上而下的顺序加载对应的类文件,即使数组中存多个同一个类对应的dex文件,虚拟机一旦找到了对应的dex文件就会停止查找,并加载。根据这个规则,我们只需要把Bug修复涉及到的类文件插入到数组的最前面去,就可以达到修复的目的。

说白了,热修复是利用Android Application的加载dex的规则,从中干预,从而达到修复的目的。

二. 根据原理,我们先来写一个热修复的核心类,

有了上面的原理分析,这个类也肯定不会太复杂,主要用到的是Java的反射以及ClassLoader(DexClassLoader以及PathClassLoader)。

package com.yb.demo.olfix.fixdex;import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;import android.content.Context;import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;/*** 作者:created by yufenfen on 2019/3/21:12:13* 邮箱: ybyj1314@126.com*/
public final class HotFix {/*** 修复指定的类** @param context        上下文对象* @param fixDexFilePath   修复的dex文件路径*/public static void fixDexFile(Context context, String fixDexFilePath) {if (fixDexFilePath != null && new File(fixDexFilePath).exists()) {try {injectDexToClassLoader(context, fixDexFilePath);} catch (Exception e) {e.printStackTrace();}}}/*** @param context* @param fixDexFilePath 修复文件的路径* @throws ClassNotFoundException* @throws NoSuchFieldException* @throws IllegalAccessException*/private static void injectDexToClassLoader(Context context, String fixDexFilePath)throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {//读取 baseElementsPathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();Object basePathList = getPathList(pathClassLoader);Object baseElements = getDexElements(basePathList);//读取 fixElementsString baseDexAbsolutePath = context.getDir("dex", 0).getAbsolutePath();DexClassLoader fixDexClassLoader = new DexClassLoader(fixDexFilePath, baseDexAbsolutePath, fixDexFilePath, context.getClassLoader());Object fixPathList = getPathList(fixDexClassLoader);Object fixElements = getDexElements(fixPathList);//合并两份ElementsObject newElements = combineArray(baseElements, fixElements);//一定要重新获取,不要用basePathList,会报错Object basePathList2 = getPathList(pathClassLoader);//新的dexElements对象重新设置回去setField(basePathList2, basePathList2.getClass(), "dexElements", newElements);}/*** 通过反射先获取到pathList对象** @param obj* @return* @throws ClassNotFoundException* @throws NoSuchFieldException* @throws IllegalAccessException*/private static Object getPathList(Object obj) throws ClassNotFoundException, NoSuchFieldException,IllegalAccessException {return getField(obj, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");}/*** 从上面获取到的PathList对象中,进一步反射获得dexElements对象** @param obj* @return* @throws NoSuchFieldException* @throws IllegalAccessException*/private static Object getDexElements(Object obj) throws NoSuchFieldException, IllegalAccessException {return getField(obj, obj.getClass(), "dexElements");}private static Object getField(Object obj, Class cls, String str)throws NoSuchFieldException, IllegalAccessException {Field declaredField = cls.getDeclaredField(str);declaredField.setAccessible(true);//设置为可访问return declaredField.get(obj);}private static void setField(Object obj, Class cls, String str, Object obj2)throws NoSuchFieldException, IllegalAccessException {Field declaredField = cls.getDeclaredField(str);declaredField.setAccessible(true);//设置为可访问declaredField.set(obj, obj2);}/*** 合拼dexElements ,并确保 fixElements 在 baseElements 之前** @param baseElements* @param fixElements* @return*/private static Object combineArray(Object baseElements, Object fixElements) {Class componentType = fixElements.getClass().getComponentType();int length = Array.getLength(fixElements);int length2 = Array.getLength(baseElements) + length;Object newInstance = Array.newInstance(componentType, length2);for (int i = 0; i < length2; i++) {if (i < length) {Array.set(newInstance, i, Array.get(fixElements, i));} else {Array.set(newInstance, i, Array.get(baseElements, i - length));}}return newInstance;}
}

三. 写bug修bug

修复主体类写好了,那么,我们来写个baseAPP,,然后在baseAPP里写一个专门带有bug的类,既然要测试热修复,我们肯定要写一个带有bug的类。

package com.yb.demo.olfix;import android.app.Activity;
import android.content.Context;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;import java.net.URL;/*** 作者:created by yufenfen on 2019/3/27:08:26* 邮箱: ybyj1314@126.com*/
public class FixMe {private final String TAG = "FixMe";private ImageView mBelle;private TextView mNotGril;private Context mContext;private MyGlide myGlide;//false: bug, true: fixprivate boolean fix = false;public FixMe(Activity context) {mContext = context;mBelle = (ImageView) context.findViewById(R.id.gril);mNotGril = (TextView) context.findViewById(R.id.notgril);myGlide = MyGlide.getInstance(mContext);}public void showWhat() {if (fix) {fixBug();Log.d(TAG, "fix bug!");} else {mBelle.setVisibility(View.GONE);mNotGril.setVisibility(View.VISIBLE);Log.d(TAG, "this is a bug!");}}private void fixBug() {try {mBelle.setVisibility(View.VISIBLE);mNotGril.setVisibility(View.GONE);URL url = new URL("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1553252483041&di=3c51ed29d8b2efe3c98dac5168f19e6b&imgtype=0&src=http%3A%2F%2Fpic.feizl.com%2Fupload%2Fallimg%2F171016%2F522zpd0y2srfqa.jpg");myGlide.loadImageAndAddToTarget(mBelle, url);} catch (Exception e) {e.printStackTrace();}}
}

然后打包安装到我们的设备上。

接下来,我们把bug修正,也就是在出bug的对应类修复bug,其实就是上面的类的变量赋值为true

    //false: bug, true: fixprivate boolean fix = true;

四. 打补丁包打补丁

修复好bug后,先不要着急编译运行,我们要先在AndroidStudio里面关闭掉Instant_Run。
由于Android Studio的instan run的原理也是热修复,所以安装的时候不会安装完整的安装包,只会安装新改变的代码。

Jietu20190321-155822.jpg

重新编译,然后就可以打热修复补丁包了,我们这里了非常原始的打补丁包的方式,步骤如下:

1. 拷贝出新修改的类

点击Build->RebuildProject来重新构建,构建完成之后,可以在app/build/interintermediate/debug/包名/找到你刚刚修改的class文件,将他拷贝出来,要连同包名路径一起拷贝出来。

Jietu20190322-095337.jpg

2. 将class文件打包成dex文件

我们前面知道热修复的原理是Dalvik/ART加载dex文件,所以接下来我们要将class文件打包成dex文件,首先我们找到AndroidSDK的build-tools 目录下,在控制台下进入该目录下的任意一个版本,执行dx命令,关于dx命令的使用帮助可以使用dx -- help,下面们通过 dx --dex [指定输出路径]/classes.dex [刚才拷贝的修复bug的类及包名的目录]这样我们就得到了.dex文件。

dx --dex --output=/Users/yufenfen/Desktop/outputdex/classes2.dex /Users/yufenfen/Desktop/outputdex

3. 将补丁包放到目的地

由于实现在线下载补丁文件(classes2.dex)还是比较麻烦一点,我们这里采取简单粗暴的方式,就是手动的把补丁放到以下目录:

Environment.getExternalStorageDirectory()

四. 调用热更新

放好补丁文件后,就可以在页面点击按钮“修复”后进行检查热修复,方式如下

      private void checkFix(){try {String dexPath = Environment.getExternalStorageDirectory() + "/classes2.dex";HotFix.fixDexFile(this, dexPath);Toast.makeText(this, "修复成功", Toast.LENGTH_SHORT).show();} catch (Exception e) {Toast.makeText(this, "修复失败" + e.getMessage(), Toast.LENGTH_SHORT).show();e.printStackTrace();}}

OK,造补丁包打补丁就是这么简单粗暴的搞定了,接下来就可以验证是否成功了。
先验证APP是在bug状态的,然后重新启动有bug的APP,点击修复,我们就可以看到美女了。

存在的问题,是如果先点击“来个妞”,再点修复,貌似没有效果,一定要先点修复,再点击“来个妞”才好,原因待确定。
初步估计是相关的类已经被加载,因为根据类加载机制,尽管修复bug了,也就是对相关的类进行了提位操作,而该类已经被加载内存,不会再重新加载,故无效。
有时间找找准确的原因,找到后会更新。

作者:雨纷纷__
链接:https://www.jianshu.com/p/b65e5da3dff2
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Android热更新十:自己写一个Android热修复相关推荐

  1. android splash 公司,正确地写一个Android Splash页面

    正确地写一个Android Splash页面 打开APP立即进入应用的主页面并呈现出用户想要的内容,对于用户来讲是最好的体验.但是通常APP在启动时需要进行一系列的初始化.网络加载等耗时的操作,因此启 ...

  2. Android笔记(二十):写一个图片文字识别SDK给自己用

    背景 市面上文字识别大多需要开通会员才能使用,所以决定自己封装一个sdk出来,供后面开发扫描类app提供便捷工具 效果 SDK API 初始化 需进行初始化才能使用sdk EasyOcrSDK.get ...

  3. 写一个Android输入法01——最简步骤

    本文演示用Android Studio写一个最简单的输入法.界面和交互都很简陋,只为剔肉留骨,彰显写一个Android输入法的要点. 1.打开Android Studio创建项目,该项目和普通APP的 ...

  4. OCRunner 第零篇:从零教你写一个 iOS 热修复框架

    为什么要热修复 在软件开发过程中,很难避免 BUG 的存在,尤其是对于一些达到一定规模的 App 因为协作模式错综复杂,就很容易带着问题上线. 一旦问题上线之后,问题就麻烦了,不仅需要重新打包.测试, ...

  5. android sdk build-tools_从零开始仿写一个抖音App——视频编辑SDK开发(一)

    本文首发于微信公众号--世界上有意思的事,搬运转载请注明出处,否则将追究版权责任.交流qq群:859640274. 大家好久不见,又有一个多月没有发文章了.不知道还有哪些读者记得我的 从零开始仿写抖音 ...

  6. [深入剖析React Native]热更新之react-native-pushy使用指南(Android)

    本文使用RN版本:0.33.0 react-native-pushy是ReactNative中文网推出的代码热更新服务,github地址:https://github.com/reactnativec ...

  7. java写一个android程序_【Android开发笔记】3.编写第一个Android程序

    前言 上一节我们通过一个Demo熟悉了Eclipse的基本使用.如何在模拟器和手机中运行以及如何打包成APK,但没具体编写代码,相信很多同学已经按耐不住了吧,这一节我们会动手编写代码来熟悉Androi ...

  8. 【Android】【Java】写一个字符串到Android系统得文件里去存起来,保存字符串到文件

    manifest.xml加权限: <uses-permissionandroid:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS& ...

  9. 如何写一个Android inline hook框架

    Android_Inline_Hook https://github.com/GToad/Android_Inline_Hook_ARM64 有32和64的实现,但是是分离的,要用的话还要自己把两份代 ...

最新文章

  1. Git 高频命令、版本回退、分支操作、文件修改删除、撤销、标签、远程仓库推送、拉取
  2. (C++)CSP202006-2 稀疏向量 two pointers
  3. 清华学霸直博简历火了!CPU、操作系统、编译器全自主写,刘知远点赞
  4. 349套HTML5+CSS3各行各业网站模板免费下载
  5. BZOJ 2655 calc (组合计数、DP、多项式、拉格朗日插值)
  6. 文献记录(part41)--Residual multi-task learning for facial landmark localization and expression ...
  7. 自学python系列14:映像,集合类型-集合类型
  8. 我想说:mysql 的 join 真的很弱|文末福利
  9. 深入理解java虚拟机---JDK8-废弃永久代(PermGen)迎来元空间(Metaspace)(十二)
  10. c# 盖尔-沙普利算法的改进
  11. Alex 的 Hadoop 菜鸟教程: 第21课 不只是在HBase中用SQL:Phoenix
  12. 微星GS安装Ubuntu系统
  13. 对计算机课程的意见和建议对老师,对计算机应用基础课程的探讨
  14. 【ArcGIS微课1000例】0052:创建地理数据库注记(标准注记、要素关联注记、尺寸注记)
  15. FFplay文档解读-49-多媒体过滤器三
  16. 数值优化理论的数学基础
  17. go-gorilla的ping pong
  18. RH358管理DHCP和IP地址分配--配置分配IPv6地址
  19. echo -e 命令详解
  20. 电商商品分类EXCEL(仅供参考)

热门文章

  1. Springboot添加白名单Ip
  2. 类的设计---麻球与油果
  3. adobe 奥多比bridge extention等产品 “安装程序无法初始化。请下载Adobe Support Advisor检测该问题”
  4. 看雪软件安全精选:二进制各种漏洞原理实战分析总结
  5. 深圳中学因招聘上热搜:名校博士挤破头想进,教学成绩也确实不服不行!
  6. 3步释放工作和生活压力
  7. 【糗事】ADSL 连接时出现815错误
  8. 雷军:《我十年的程序员生涯》系列之二(我赚的第一桶金)
  9. 戴尔新品移动工作站precision3541拆机图
  10. python协程gevent案例 爬取斗鱼图片过程解析 - python