首页 >> 大全

类加载机制实现Android热修复

2023-07-22 大全 29 作者:考证青年

本文通过类加载机制实现热修复,Demo实现的功能:检测服务器是否存在补丁,存在即下载补丁,安装补丁,重启APP生效。支持多个补丁包修复:如果已经下载了多个补丁包,重启app对补丁包进行排序,并依次修复。本文比较贴近实际应用。

效果图

这里写图片描述

如果感觉不能一步步自己实现,可以看看本文将的热修复原理,然后直接下载完整代码,多敲几遍,就行了。

什么是热修复技术?

PS:本文通过 “类加载机制” 实现代码热修复,资源和so库类修复不在本文介绍范围内,是在想使用的话,请参考下文介绍的当下流行的热修复技术框架。

对于这个弱智的问题,相信不需要过多的解释,就是:在不重新安装apk的情况下,通过补丁,修复bug。盗用阿里的2张图(阿里)

这里写图片描述

这里写图片描述

目前主流的热修复技术框架 代码热修复实现原理和优缺点

Ps:上述4大系列框架都很全面,包括了代码修复、资源修复、so库修复等。本文我们只谈如何自己动手实现代码热修复(我觉得日常开发足够了)

代码热修复2种方案

通过类加载机制实现代码热修复(来点干货) 类加载机制有什么是我们可利用的呢? 认识、和 用到xx类的时候,虚拟机是怎么找到它的?

简单描述下:用到xx类的时候,虚拟机会利用去遍历加载过的所有dex文件,从中查找到xx类,一旦找到就。

查找类的源码

@Overrideprotected Class findClass(String name) throws ClassNotFoundException {List suppressedExceptions = new ArrayList();Class c = pathList.findClass(name, suppressedExceptions);if (c == null) {ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);for (Throwable t : suppressedExceptions) {cnfe.addSuppressed(t);}throw cnfe;}return c;}

通过源码可以看到,通过.查找类的,这里出现一个 大Boss “”

:中保存类所有dex文件和信息,看一下它是怎么查找类的

源码:

public Class findClass(String name, List suppressed) {for (Element element : dexElements) {DexFile dex = element.dexFile;if (dex != null) {Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);if (clazz != null) {return clazz;}}}if (dexElementsSuppressedExceptions != null) {suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));}return null;}

前方高能:

看到没有,从中查找类,如果clazz != null直接 class,这就是我们可以利用的地方,从源码看,应该是个数组或者集合,设想:我们是不是可以把我们修复bug后的xx类,打包成dex,插入到的最前面,这样,系统通过,查找bug类的时候,就会下找到我们的修复bug的xx类,然后直接返回,不去管后面有bug的那个xx类,达到热修复的功能。

Ps:不放心,看看中到底是什么?

贴一部分能说明问题的代码:

private Element[] dexElements;static class Element {private final File dir;private final boolean isDirectory;private final File zip;private final DexFile dexFile;private ClassPathURLStreamHandler urlHandler;private boolean initialized;public Element(File dir, boolean isDirectory, File zip, DexFile dexFile) {this.dir = dir;this.isDirectory = isDirectory;this.zip = zip;this.dexFile = dexFile;}

这段你代码是从.java中复制的,是类型的数组,而是的内部类,其中保存了和路径等信息。证实我们的热修复方案是可行的。

推荐一个可以看在线看源码的网站

上面贴的源码是“”级别的,在中是看不到的,看到的是一堆抛异常的代码,我看了中的源码还傻傻的去别人博客留言说我的版本源码变了,和你的不一样,还能不能实现热修复,现在想想挺搞笑。

* 在线查看各版本源码 *

理一下我们热修复的方案 代码实现热修复(代码开撸) 需求

现有一个app,中有2个按钮,“跳转”和“查看信息”,从服务器下载补丁,把“查看信息”按钮改为“悄悄修改了代码”,将原来展示的信息,改为“我又来热修复了…”

实现步骤

编写改变前的app

编写热修复需要重写生成的类

通过2步骤的新类生成dex补丁包””,并放到本地的服务器,编写配置文件

编写补丁检测和下载代码

编写修复补丁代码

进行加载_加载装置是什么_

Ps:为了更清晰,一步步来,大牛请直接跳到第5部,哈哈~~~

编写原app

一共3个类:、、类,部分代码如下

.java


public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);}// 跳转Activity2按钮点击回调方法public void jumpToA2(View v){Intent intent = new Intent(this, Activity2.class) ;startActivity(intent) ;}// 展示People信息public void showPeopleInfo(View v){Toast.makeText(this, new People(20, "小明").toString(), Toast.LENGTH_LONG).show() ;}
}

.java

public class Activity2 extends Activity {private TextView view ;@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity2);view = (TextView) findViewById(R.id.tv_info) ;view.setText("Activit2") ;}
}

.java

public class People {private int age ;private String name ;public People(int age, String name) {this.age = age;this.name = name;}@Overridepublic String toString() {return "People{" +"age=" + age +", name=" + name +'}';}
}

以上3个类特别简单,可以运行一下试试

修改 、类和类

1. MainActivity类修改如下:
//将展示People信息按钮文字,改为"悄悄修改了代码"
showPeopleInfoButton.setText("悄悄修改了代码") ;2. Activity2类修改如下:
将view.setText("Activit2") ;
改为:view.setText("我又来热修复啦...") ;3. People类修改如下:
将 return "People{" +"age=" + age +", name=" + name +'}';
改为:return "People{" +"age=" + age +", name=" + name +'}'+"史上最牛逼人物!!!";

编译生成class文件,修改也非常简单,我们就将这两个类做成””补丁

用class文件生成””补丁

在sdk/build-tools/文件件下提供了”dx”命令工具,帮助我们将class文件生成dex文件

生成方式如下:

dx –dex –=

例如:

dx –dex –=001.dex … ….class ….class

将”001.dex”放入服务器并编写配置文件

将放到服务器这一步,我们不用写代码,只要能通过局域网访问到这个文件就行,”.0\--7.0.64\\\”新建””文件夹,在文件夹中创建“”文件夹和“patch”文件夹,将001.dex放入”patch”文件夹

Ps:我的是7.0,”.0\--7.0.64\”是我的安装目录(说白了,就是解压目录,我们都知道只需解压就能用,不用安装,当然也有安装的),根据自己的实际情况来。

编写配置文件

所谓的配置文件,就是app用来检测是否存在补丁的一个文件,可以是.txt文件、xml文件、json文件等等。这里我们用json格式的文件。

在“.0\--7.0.64\\\\”目录创建”.json”,内容如下:

{"patchCode":"001.dex","patchUrl":"http://192.168.1.106:8080/example/hotfix/patch/001.dex"}

配置文件中有2个字段:一个是补丁代码,一个是补丁下载地址。是我的ip地址,只需将ip换成你自己的即可

* 开启tomct服务器,测试是否成功*

* 在浏览器输入.json的地址”:8080////.json”

这里写图片描述

检测补丁

/*** [检测服务器是否存在补丁和本地是否一下载过该补丁]* @type {Request}*/
public void checkPatch(){//检测是否存在补丁包Request request = new Request.Builder().get().url(CHECK_URL).build() ;Call call = mClient.newCall(request) ;call.enqueue(new Callback() {@Overridepublic void onFailure(Call call, IOException e) {Log.d(TAG, "check patch failed....") ;}@Overridepublic void onResponse(Call call, Response response) throws IOException {JSONObject jsonObject = JSON.parseObject(response.body().string());patchCode = jsonObject.getString("patchCode") ;Log.d(TAG, "pathCode:"+patchCode) ;//判断是否存在补丁if(!"-1".equals(patchCode)){//判断当前补丁是否已经下载过if(isDownLoad()){Log.d(TAG, "this version pathCode is fixed...") ;fixPatch();}else{//获取补丁链接patchUrl = jsonObject.getString("patchUrl") ;//开启补丁下载downLoadPatch(patchUrl) ;}}}});}

下载补丁

Ps:这里需要注意,补丁文件要下载到我们的安装目录,只有我们自己可以访问,如果下载到存储卡,很容易被别人替换,影响app的安全。先贴一段初始化下载目录的代码:

//这句代码,的意思是:在我们app的安装目录新建一个叫“patch”的文件夹,
//如果不存在,则创建,路径为: /data/data/app的包名/app_patch,
//在安装目录创建的文件夹,均会被加上"app_"
File fPatchPath = context.getDir("patch", Context.MODE_PRIVATE) ;
//为了保险起见,我们判断一下此路径是否存在,不存在则创建
if(fPatchPath.exists()){fPatchPath.mkdirs() ;
}

    /*** [下载补丁]* @type {Request}*/private void downLoadPatch(String downUrl) {Request request = new Request.Builder().get().url(downUrl).build() ;Call call = mClient.newCall(request) ;call.enqueue(new Callback() {@Overridepublic void onFailure(Call call, IOException e) {Log.d(TAG, "the patch download failed...") ;Log.d(TAG, e.getMessage()) ;}@Overridepublic void onResponse(Call call, Response response) throws IOException {//请求成功,获取补丁输入流,下面就是文件的读取和保存了//相信都是平时大家写烂的东西了,就不备注了byte[] buffer = new byte[2048] ;int len = 0  ;OutputStream os = new FileOutputStream(patchPath+File.separator+patchCode+".dex") ;InputStream is = response.body().byteStream() ;Log.d(TAG, "start download patch...") ;while((len=is.read(buffer,0,buffer.length))!=-1){os.write(buffer,0, len);}os.close();is.close();Log.d(TAG, "download patch completion...") ;//保存当前补丁编码SharedPreferences sp = context.getSharedPreferences(HOTFIX_SP, Context.MODE_PRIVATE) ;sp.edit().putString(HOTFIX_CODE, hotfixConfig.getPatchCode()).commit() ;}});}

安装补丁包的2种方案(其实也没多大的意义,可以不看,直接看代码)

对于安装补丁包,我考虑了2种方案

小结:当然,如果采用第二种方式,我们搭建的简易服务器肯定是不行的,因为,如果有的用户没有下载安装上一个补丁,而直接安装了最新的,意味着上一个补丁修复的bug,他将永远带着(除非更新app)。

当然,也不能说第一种方案是完美的,对于用户来讲,dex补丁包越下载越大;对开发人员来说,每次都要保留上次修复bug的class,还要对比,这次有没有对该class做修改,也是比较麻烦的,很容易出错。

这里,虽然我们的服务器暂不完美,暂且使用第二种方式吧,可以多学到一点东西:比如:补丁打包的排序;遍历/目录安装每一个补丁等。

Ps:既然多个补丁都会安装了,那么,第一种方案的只安装一个补丁,应该是手到擒来的吧~

编写核心代码:安装dex补丁包

在回顾一下我们热修复的原理,核心就一句话:将我们的dex补丁插入到系统加载的dex数组之前,让系统查找类的收,先找到我们补丁中的类,而不再去加载后面的有bug的类。

具体实现步骤

通过反射机制拿到””中的””对象通过反射机制拿到””对象中的””数组通过”” 加载我们的xxx.dex补丁包通过反射机制拿到””中的””对象通过反射机制拿到””对象中的””数组将””的””插入””的的前面

Ps:上面也说过,和均继承自,重要的方法都在中,包括””,所有我们重要反射就可以了。

安装补丁,就是通过反射机制实现,如果不熟悉反射机制,下面的代码可能会让你像坐过山车一样晕头转向。像了解反射机制的朋友可以看下我的上一篇文章 java/中的反射机制 。

代码开撸


/*** [修复aap_patch目录下的所有补丁]* @type {File}*/
private void fixPatch() {//获取patch文件夹下所有的补丁文件File[] files = new File(patchPath).listFiles() ;if(files.length>0){//补丁按下载日期排序(最新补丁放前面)patchSort(files);for (File file : files) {//判断file是否为补丁if(file.isFile() && file.getAbsolutePath().endsWith(".dex")){System.out.println("---:"+file.getName());//开始加载补丁并修复loadPatch(file);}}Log.d(TAG, "fiexd success....") ;}}

上面一段代码是遍历补丁文件加下所有的补丁,并对补丁排序。为什么要排序?因为,如果上次的补丁001.dex修复了”类A”的一个bug,而这次的002.dex补丁又对”类A”做了其他修复,那么,如果不排序,遍历补丁文件夹的时候,如果把001.dex补丁打在了002.dex补丁的前面,那么系统会先找到001.dex中的”类A”,002.dex补丁将永远不会生效。

按日期排序

/*** [按最后修改日期排序],排序方式有很多:冒泡排序、快速排序等等,* 这个随便,只要排序后,保证最新的dex补丁在最前面就行* @type {[type]}*/
private void patchSort(File[] files){Arrays.sort(files, new Comparator() {@Overridepublic int compare(File file, File t1) {System.out.println(file.getName()+":"+file.lastModified());System.out.println(t1.getName()+":"+t1.lastModified());long d = t1.lastModified() - file.lastModified() ;//从大到小排序if(d>0){return -1 ;}else if(d<0){return 1 ;}else{return 0 ;}}@Overridepublic boolean equals(Object obj) {return true ;}});}

加载并安装dex补丁

/*** 加载并安装补丁* @type {[type]}*/
private void loadPatch(File file){Log.d(TAG, file.getAbsolutePath()) ;if(file.exists()){Log.d(TAG,"文件存在...") ;}else{Log.d(TAG, "文件不存在...") ;}//获取系统PathClassLoaderPathClassLoader pLoader = (PathClassLoader) context.getClassLoader();//获取PathClassLoader中的PathListObject pPathList = getPathList(pLoader) ;if(pPathList == null){Log.d(TAG, "get PathClassLoader pathlist failed...") ;return ;}//加载补丁DexClassLoader dLoader = new DexClassLoader(file.getAbsolutePath(),optPath, null, pLoader) ;//获取DexClassLoader的pathLit,即BaseDexClassLoader中的pathListObject dPathList = getPathList(dLoader) ;if(dPathList == null){Log.d(TAG, "get DexClassLoader pathList failed...") ;return ;}//获取PathList和DexClassLoader的DexElementsObject pElements = getElements(pPathList) ;Object dElements = getElements(dPathList) ;//将补丁dElements[]插入系统pElements[]的最前面Object newElements = insertElements(pElements, dElements) ;if(newElements == null){Log.d(TAG, "patch insert failed...") ;return ;}//用插入补丁后的新Elements[]替换系统Elements[]try {Field fElements = pPathList.getClass().getDeclaredField("dexElements") ;fElements.setAccessible(true);fElements.set(pPathList, newElements);} catch (Exception e) {e.printStackTrace();Log.d(TAG, "fixed failed....") ;return ;}}/*** 将补丁插入系统DexElements[]最前端,生成一个新的DexElements[]* @param pElements* @param dElements* @return*/private Object insertElements(Object pElements, Object dElements){//判断是否为数组if(pElements.getClass().isArray() && dElements.getClass().isArray()){//获取数组长度int pLen = Array.getLength(pElements) ;int dLen = Array.getLength(dElements) ;//创建新数组Object newElements = Array.newInstance(pElements.getClass().getComponentType(), pLen+dLen) ;//循环插入for(int i=0; iif(ielse{Array.set(newElements, i, Array.get(pElements, i-dLen)) ;}}return newElements ;}return null ;}/***  获取DexElements* @param object* @return*/private Object getElements(Object object){try {Class c = object.getClass() ;Field fElements = c.getDeclaredField("dexElements") ;fElements.setAccessible(true);Object obj = fElements.get(object) ;return obj ;} catch (Exception e) {e.printStackTrace();}return null ;}/*** 通过反射机制获取PathList* @param loader* @return*/private Object getPathList(BaseDexClassLoader loader){try {Class c = Class.forName("dalvik.system.BaseDexClassLoader") ;//获取成员变量pathListField fPathList = c.getDeclaredField("pathList") ;//抑制jvm检测访问权限fPathList.setAccessible(true);//获取成员变量pathList的值Object obj = fPathList.get(loader) ;return obj ;} catch (Exception e) {e.printStackTrace();}return null ;}

上面的代码有点长,但是注释也很详细。如果你熟悉了热修复原理和反射机制,我觉得上面的代码对你来说,完全就是力气活。不熟悉就多敲几遍,熟能生巧。

Ps:记住检测和下载dex补丁,可以在程序的任何位置进行,但是安装dex补丁,一定要在代码逻辑未执行之前,即:在中的方法中安装补丁。

到这一步,热修复基本完成。经测试,可正常修复,但是修复其他类,会报错,重复加载xxx类,就是传说中”问题标志”问题。

解决问题

经过测试,如果不修复问,也是可以修复和等类的。

是怎么产生的呢?

dilvk虚拟机通过加载类的时候,如果A类中用到了类B中的方法,而且A类和B类又在同一个Dex包中,那么B将被会被虚拟机打上标志,再查找到我们的B类,则会报错。

解决方案,大致有2种

QQ的提出的,创建一个””类,并打包成hack.dex,让项目中的每个类都引用hack.dex中的””类,就不会被虚拟机打上标志了。有个牛逼的名字叫:代码入侵。

利用的dex分包方案,将app中的某个X类打包带另一个dex中,app中的每个类都引用X类。

想深入了解的可自行差资料,这里我们采用QQ的方案,使用QQ的方案,晚上有很多介绍,都是使用插件,我也不是很了解,也是看的别人的,其实也没那么难。我就不误人子弟了,这里推荐2篇文章,本文解决问题,就是参考的这篇文章。我仅仅会用,可能教不好,还是劳烦各位朋友移步,或者自己百度解决方案。

两篇很有技术含量的文章。

总结

其实本文实现的热修复仅仅只是皮毛,仅仅做到了代码修复。当下有太多成熟的热修复框架了,能实现资源、代码、so库等的热修复。我们自己实现热修复是为在使用这些框架的时候,而不至于一脸茫然,不知其所以然。对于这些框架,我个人比较细化阿里系的“”,这里有一本阿里的热修复的书,推荐给大家: 深入探索热修复技术原理6.29b-final.pdf

贴一张对比图:

这里写图片描述

对于热修复技术框架的选型,大家心里应该清楚自己更需要哪个体系,各有有缺,为大家推荐一篇不错的文章:热修复技术选型——三大流派解析,希望对大家有帮助。

Demo源码下载

Demo源码下载

关于我们

最火推荐

小编推荐

联系我们


版权声明:本站内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 88@qq.com 举报,一经查实,本站将立刻删除。备案号:桂ICP备2021009421号
Powered By Z-BlogPHP.
复制成功
微信号:
我知道了