Android学习——手把手教你实现Android热修复
前言
最近一段时间看了一些关于Android热修复的知识,比如Andfix,Tinker,Sophix等,看了这些框架的原理,就想着自己能不能手撸一个简单的demo。下面我们就来自己动手实现Android热修复吧。
热修复实现原理
所谓热修复就是,在我们应用上线后出现小bug需要及时修复时,不用再发新的安装包,只需要发布补丁包,在客户不知不觉之间修复掉bug,JAVA虚拟机JVM在运行时,加载的是.classes的字节码文件。而Android也有自己的虚拟机Dalvik/ART虚拟机,不过他们加载的是dex文件,但是他们的工作原理都一样,都是经过ClassLoader类加载器。Android在ClassLoader的基础上又定义类PathClassLoader和DexClassLoader,两者都继承自BaseDexClassLoader,下面我们看下他们间的区别:
* BaseDexClassLoader
源代码位于libcore\dalvik\src\main\java\dalvik\system\BaseDexClassLoader.java
。
* PathClassLoader
源代码位于libcore\dalvik\src\main\Java\dalvik\system\PathClassLoader.java
。他主要用来加载系统类和应用类。
* DexClassLoader
源代码位于libcore\dalvik\src\main\java\dalvik\system\DexClassLoader.java
。用来加载jar、apk、dex文件.加载jar、apk也是最终抽取里面的Dex文件进行加载.
手写Android热修复框架
下面我们一步一步来实现Android热修复。
写一个专门带有bug的类
既然要测试热修复,我们肯定要写一个带有bug的类。
package com.example.bthvi.mycloassloaderapplication.xxx; import android.content.Context; import android.view.View; import android.widget.Toast; /** * bug测试类 */ public class BugClass { public BugClass(Context context){ Toast.makeText(context,"这是一个优美的bug!",Toast.LENGTH_SHORT).show(); } }
下面我们要写一个热修复的核心工具类。
热修复核心类
package com.example.bthvi.mycloassloaderapplication; import android.content.Context; import android.os.Environment; import android.support.annotation.NonNull; import android.widget.Toast; import java.io.File; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.util.HashSet; import dalvik.system.DexClassLoader; import dalvik.system.PathClassLoader; public class FixDexUtil { private static final String DEX_SUFFIX = ".dex"; private static final String APK_SUFFIX = ".apk"; private static final String JAR_SUFFIX = ".jar"; private static final String ZIP_SUFFIX = ".zip"; public static final String DEX_DIR = "odex"; private static final String OPTIMIZE_DEX_DIR = "optimize_dex"; private static HashSet<File> loadedDex = new HashSet<>(); static { loadedDex.clear(); } /** * 加载补丁,使用默认目录:data/data/包名/files/odex * * @param context */ public static void loadFixedDex(Context context) { loadFixedDex(context, null); } /** * 加载补丁 * * @param context 上下文 * @param patchFilesDir 补丁所在目录 */ public static void loadFixedDex(Context context, File patchFilesDir) { // dex合并之前的dex doDexInject(context, loadedDex); } /** *@author bthvi *@time 2018/6/25 0025 15:51 *@desc 验证是否需要热修复 */ public static boolean isGoingToFix(@NonNull Context context) { boolean canFix = false; File externalStorageDirectory = Environment.getExternalStorageDirectory(); // 遍历所有的修复dex , 因为可能是多个dex修复包 File fileDir = externalStorageDirectory != null ? new File(externalStorageDirectory,"007"): new File(context.getFilesDir(), DEX_DIR);// data/data/包名/files/odex(这个可以任意位置) File[] listFiles = fileDir.listFiles(); if (listFiles != null){ for (File file : listFiles) { if (file.getName().startsWith("classes") && (file.getName().endsWith(DEX_SUFFIX) || file.getName().endsWith(APK_SUFFIX) || file.getName().endsWith(JAR_SUFFIX) || file.getName().endsWith(ZIP_SUFFIX))) { loadedDex.add(file);// 存入集合 //有目标dex文件, 需要修复 canFix = true; } } } return canFix; } private static void doDexInject(Context appContext, HashSet<File> loadedDex) { String optimizeDir = appContext.getFilesDir().getAbsolutePath() + File.separator + OPTIMIZE_DEX_DIR; // data/data/包名/files/optimize_dex(这个必须是自己程序下的目录) File fopt = new File(optimizeDir); if (!fopt.exists()) { fopt.mkdirs(); } try { // 1.加载应用程序dex的Loader PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader(); for (File dex : loadedDex) { // 2.加载指定的修复的dex文件的Loader DexClassLoader dexLoader = new DexClassLoader( dex.getAbsolutePath(),// 修复好的dex(补丁)所在目录 fopt.getAbsolutePath(),// 存放dex的解压目录(用于jar、zip、apk格式的补丁) null,// 加载dex时需要的库 pathLoader// 父类加载器 ); // 3.开始合并 // 合并的目标是Element[],重新赋值它的值即可 /** * BaseDexClassLoader中有 变量: DexPathList pathList * DexPathList中有 变量 Element[] dexElements * 依次反射即可 */ //3.1 准备好pathList的引用 Object dexPathList = getPathList(dexLoader); Object pathPathList = getPathList(pathLoader); //3.2 从pathList中反射出element集合 Object leftDexElements = getDexElements(dexPathList); Object rightDexElements = getDexElements(pathPathList); //3.3 合并两个dex数组 Object dexElements = combineArray(leftDexElements, rightDexElements); // 重写给PathList里面的Element[] dexElements;赋值 Object pathList = getPathList(pathLoader);// 一定要重新获取,不要用pathPathList,会报错 setField(pathList, pathList.getClass(), "dexElements", dexElements); } Toast.makeText(appContext, "修复完成", Toast.LENGTH_SHORT).show(); } catch (Exception e) { e.printStackTrace(); } } /** * 反射给对象中的属性重新赋值 */ private static void setField(Object obj, Class<?> cl, String field, Object value) throws NoSuchFieldException, IllegalAccessException { Field declaredField = cl.getDeclaredField(field); declaredField.setAccessible(true); declaredField.set(obj, value); } /** * 反射得到对象中的属性值 */ private static Object getField(Object obj, Class<?> cl, String field) throws NoSuchFieldException, IllegalAccessException { Field localField = cl.getDeclaredField(field); localField.setAccessible(true); return localField.get(obj); } /** * 反射得到类加载器中的pathList对象 */ private static Object getPathList(Object baseDexClassLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList"); } /** * 反射得到pathList中的dexElements */ private static Object getDexElements(Object pathList) throws NoSuchFieldException, IllegalAccessException { return getField(pathList, pathList.getClass(), "dexElements"); } /** * 数组合并 */ private static Object combineArray(Object arrayLhs, Object arrayRhs) { Class<?> clazz = arrayLhs.getClass().getComponentType(); int i = Array.getLength(arrayLhs);// 得到左数组长度(补丁数组) int j = Array.getLength(arrayRhs);// 得到原dex数组长度 int k = i + j;// 得到总数组长度(补丁数组+原dex数组) Object result = Array.newInstance(clazz, k);// 创建一个类型为clazz,长度为k的新数组 System.arraycopy(arrayLhs, 0, result, 0, i); System.arraycopy(arrayRhs, 0, result, i, j); return result; } }
我们这里暂且指定热修复目录007
,下面我们看一下如何调用。
Splash页面调用检查热修复
private void init() { File externalStorageDirectory = Environment.getExternalStorageDirectory(); // 遍历所有的修复dex , 因为可能是多个dex修复包 File fileDir = externalStorageDirectory != null ? new File(externalStorageDirectory,"007"): new File(getFilesDir(), FixDexUtil.DEX_DIR);// data/user/0/包名/files/odex(这个可以任意位置) if (!fileDir.exists()){ fileDir.mkdirs(); } if (FixDexUtil.isGoingToFix(this)) { FixDexUtil.loadFixedDex(this, Environment.getExternalStorageDirectory()); textView.setText("正在修复。。。。"); } new Handler().postDelayed(new Runnable() { @Override public void run() { startActivity(new Intent(SplashActivity.this,MainActivity.class)); finish(); } },3000); }
下面我们先来看下有bug时的APP。
在出bug的对应类修复bug
package com.example.bthvi.mycloassloaderapplication.xxx; import android.content.Context; import android.view.View; import android.widget.Toast; /** * bug测试类 */ public class BugClass { public BugClass(Context context){ Toast.makeText(context,"你很优秀!bug已修复��",Toast.LENGTH_SHORT).show(); } }
修改好bug之后我们需要打出补丁包。
打出热修复补丁包
在AndroidStudio里面关闭掉Instant_Run
由于Android Studio的instan run的原理也是热修复,所以安装的时候不会安装完整的安装包,只会安装新改变的代码。
重新编译并拷贝出新修改的类
首先点击Build->RebuildProject
来重新构建,构建完成之后,可以在app/build/interintermediate/debug/包名/
找到你刚刚修改的class文件,将他拷贝出来,要连同包名路径一起拷贝出来。
将class文件打包成dex文件
我们前面知道热修复的原理是Dalvik/ART加载dex文件,所以接下来我们要将class文件打包成dex文件,首先我们找到AndroidSDK的build-tools 目录下,在控制台下进入该目录下的任意一个版本,执行dx命令,关于dx命令的使用帮助可以使用dx -- help
,下面们通过 dx --dex [指定输出路径]/classes.dex [刚才拷贝的修复bug的类及包名的目录]
这样我们就得到了.dex文件。
将打出来的dex文件放至我们指定的目录下
我们将打出来的dex文件放在我们指定的目录007
下,当然这个目录也可以是包名。
重新启动有bug的APP
我们启动就会后发现bug已经修复了
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
WPF获取相对位置、坐标的方法
原文: WPF获取相对位置、坐标的方法 1.获取鼠标在控件中的坐标: 1 private void item_MouseDown(object sender, MouseButtonEventArgs e) 2 { 3 Point point = e.GetPosition(lbl); 4 5 } 6 7 //或者直接使用Mouse类的静态方法GetPosition(), 8 //需要注意的是参数为IInputElement类型,也就是说要是能输入的控件 9 Point point2 = Mouse.GetPosition(lbl2); 10 lbl2.Content = "(" + point2.X + ", " + point2.Y + ")"; View Code 完整例子代码: XAML代码 1 <Window x:Class="WpfGetPointDemo.MainWindow" 2 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 3 xmlns:x="http://schemas....
- 下一篇
python利用web3.py开发以太坊应用dapp的实战教程
以太坊作为最流行区块链平台,最大的特点是运行各种智能合约。我们已经出过node.js、java、php的以太坊开发实战教程,这一次是python。Python官方提供了一系列的Python包用于支持在Python应用中访问以太坊,其中最流行的就是web3.py —— 对JSON RPC接口的Python封装包。在本课程中,我们将主要使用web3.py,同时结合一些其他的包,来开发支持以太坊的Python应用。 本教程的目的是帮助Python工程师快速掌握开发以太坊应用的技能,同时穿插 讲解以太坊的一些核心概念,例如:账户、交易和智能合约等,大概的目录结构是: Hi,以太坊 将通过一个简单的Python应用的开发来讲解使用 Python进行以太坊应用开发的最简流程,通过这一部分的学习,你就可以在自己 的Python应用中引入基本的以太坊支持了。 账户管理 将详细介绍以太坊的账户管理接口。如果你对开发中心化钱包应用感兴趣,或者需要在自己的网站中动态创建账户(例如,你 希望为网站增加对以太坊支付的支持),那么这部分内容会有很大的帮助。 理解状态与交易 讲解以太坊的交易操作接口,同时也介绍一些...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Hadoop3单机部署,实现最简伪集群
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- Linux系统CentOS6、CentOS7手动修改IP地址
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- CentOS8安装Docker,最新的服务器搭配容器使用
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- Windows10,CentOS7,CentOS8安装Nodejs环境
- SpringBoot2全家桶,快速入门学习开发网站教程
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- CentOS7安装Docker,走上虚拟化容器引擎之路