首页 > 系统 > Android > 正文

Android应用热修复

2019-11-07 22:59:33
字体:
来源:转载
供稿:网友

一、修复的工具

当前主要有两个主流的热修复工具: 1.阿里系:使用了DeXposed(修改了国外的),一年没有维护了,现在又搞了一个andfix,是一种黑客技术。自己去实现了底层的zyqote。从底层C的二进制来入手的。

2.腾讯系:tinker java类加载机制来入手的。

这里我们使用tinker。

二、热修复的原理(Java类加载机制)

什么是热修复?

一般的Bug修复,都是等下一个版本解决,然后发布新的apk 热修复:可以直接在客户已经安装的程序当中修复bug。

bug一般会出现在某个类的某个方法地方。如果我们能够动态的将客户手机里面的apk里面的某个类给替换成我们已经修复好的类。

AndroidStudio的Instant run,也是一种热修复或者增量更新的方式。如果只是改了一个类,那么它只会把修改的东西打进新的包里,放到手机上运行。 所以,在用AndroidStudio做热修复的时候记得把Instant run功能关闭。不然会影响热修复实现。 机制:dex分包。mutildex。 这里写图片描述

如何实现呢?实现的原理? 从Java的类加载机制来入手的:ClassLoader

这里写图片描述

Android 是如何加载class.dex文件,启动程序。 这里提供了两个类: 1.PathClassLoader :这个类用来加载应用程序的dex

public class PathClassLoader extends BaseDexClassLoader {}

2.DexClassLoader :这个类可以加载指定的某个dex文件。(限制:必须要在应用程序的目录下面)

public class DexClassLoader extends BaseDexClassLoader {}

修复方案:

1.搞多个dex 第一个版本:classes.dex。 修复后的补丁包:classes2.dex(包涵了我们修复xxx.class) 这种实现方式也可以用于插件开发。

2.把两个dex合并 将修复的class替换原来出bug的class. 通过BaseDexClassLoader调用findClass(className):

Class<?> findClass(String name)

实际上替换的是修复了的dex文件,这里面集成了修改了的class文件。Element[] dexElements;存储的是dex的集合。

这里写图片描述

在findClass方法中是通过循环去找dexElements中的类,如果找到了,就会返回这个Class,停止执行寻找。

这里写图片描述

所以可以采取以下方式: 将修复好的dex插入到dexElements的集合,位置:出现bug的xxx.class所在的dex的前面。 最本质的实现原理:类加载器去加载某个类的时候,是去dexElements里面从头往下查找的。 fixed.dex,classes1.dex,classes2.dex,classes3.dex

三、如何实现

上面已经介绍了其中的原理,接下来在开发中如何具体实现。

步骤

1.先安装一个带有bug的版本apk。

2.修复bug,重新打包成dex文件,放在后台服务器。

3.通过主动方式或者推送将修复的dex文件,放到手机中,进行dex文件的合并。

用AndroidStudio打包multidex(官方待验证)

准备工作

1.配置gradle文件:

1)

dependencies { compile 'com.android.support:multidex:1.0.1'}

2)

defaultConfig { multiDexEnabled true }

3)

buildTypes {release { multiDexKeepFile file('dex.keep') def myFile = file('dex.keep') PRintln("isFileExists:"+myFile.exists()) println "dex keep" minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'}}

2.在application中的attachBaseContext方法加入MultiDex.install(base);

public class MyApplication extends Application{ @Override public void onCreate() { // TODO Auto-generated method stub super.onCreate(); } @Override protected void attachBaseContext(Context base) { // TODO Auto-generated method stub MultiDex.install(base); FixDexUtils.loadFixedDex(base); super.attachBaseContext(base); }}

这样配置之后运行打包出来的apk中有两个dex文件:

代码处理 1.修复的dex文件必须要在应用的目录下面,所以第一步要将修复的文件移动在/data/data/目录下面

这里写图片描述

代码处理

1.修复的dex文件必须要在应用的目录下面,所以第一步要将修复的文件移动在/data/data/目录下面

public static final String DEX_DIR = "odex";private void fixBug() { //目录:/data/data/packageName/odex File fileDir = getDir(MyConstants.DEX_DIR,Context.MODE_PRIVATE); //往该目录下面放置我们修复好的dex文件。 String name = "classes2.dex"; String filePath = fileDir.getAbsolutePath()+File.separator+name; File file= new File(filePath); if(file.exists()){ file.delete(); } //搬家:把下载好的在SD卡里面的修复了的classes2.dex搬到应用目录filePath InputStream is = null; FileOutputStream os = null; try { is = new FileInputStream(Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator+name); os = new FileOutputStream(filePath); int len = 0; byte[] buffer = new byte[1024]; while ((len=is.read(buffer))!=-1){ os.write(buffer,0,len); } File f = new File(filePath); if(f.exists()){ Toast.makeText(this ,"dex 重写成功", Toast.LENGTH_SHORT).show(); } //热修复 FixDexUtils.loadFixedDex(this); } catch (Exception e) { e.printStackTrace(); } }

2.扫描dex文件目录下的所有dex,并用HashSet存储。 记得每次初始化工具类的时候清空HashSet的数据

private static HashSet<File> loadedDex = new HashSet<File>();static{ loadedDex.clear(); }

扫描:

public static void loadFixedDex(Context context){ if(context == null){ return ; } //遍历所有的修复的dex File fileDir = context.getDir(MyConstants.DEX_DIR,Context.MODE_PRIVATE); File[] listFiles = fileDir.listFiles(); for(File file:listFiles){ if(file.getName().startsWith("classes")&&file.getName().endsWith(".dex")){ loadedDex.add(file);//存入集合 } } //dex合并之前的dex doDexInject(context,fileDir,loadedDex); }

3.获取需要修复的dex文件,并通过反射拿到对应的dex数组,然后一一合并,再通过反射的方式设置给应用的dex数组中。这样就实现了热修复。

private static void doDexInject(final Context appContext, File filesDir,HashSet<File> loadedDex) { String optimizeDir = filesDir.getAbsolutePath()+File.separator+"opt_dex"; File fopt = new File(optimizeDir); if(!fopt.exists()){ fopt.mkdirs(); } //1.加载应用程序的dex try { PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader(); //原来的dex文件加载路径 for (File dex : loadedDex) { //2.加载指定的修复的dex文件。 DexClassLoader classLoader = new DexClassLoader( dex.getAbsolutePath(),//String dexPath, fopt.getAbsolutePath(),//String optimizedDirectory, null,//String libraryPath, pathLoader//ClassLoader parent ); //3.合并 Object dexObj = getPathList(classLoader); Object pathObj = getPathList(pathLoader); Object mDexElementsList = getDexElements(dexObj); //需要修复的dex文件数组 Object pathDexElementsList = getDexElements(pathObj); //原来的dex文件数组 //合并完成 Object dexElements = combineArray(mDexElementsList,pathDexElementsList); //重写给PathList里面的lement[] dexElements;赋值 Object pathList = getPathList(pathLoader); setField(pathList,pathList.getClass(),"dexElements",dexElements); } } catch (Exception e) { e.printStackTrace(); } }

通过反射设置和获取值:

//通过反射获取baseDexClassLoader的pathList private static Object getPathList(Object baseDexClassLoader) throws Exception { return getField(baseDexClassLoader,Class.forName("dalvik.system.BaseDexClassLoader"),"pathList"); } //通过反射获取dexElements private static Object getDexElements(Object obj) throws Exception { return getField(obj,obj.getClass(),"dexElements"); } /** * 获取某个对象中的属性 * obj:某个对象 * cl:对象的类 * field:属性值 **/ private static Object getField(Object obj, Class<?> cl, String field) throws NoSuchFieldException, IllegalArgumentException, IllegalaccessException { Field localField = cl.getDeclaredField(field); localField.setAccessible(true); return localField.get(obj); } /** * 给某个对象中的添加属性值 * obj:某个对象 * cl:对象的类 * field:属性值 * value:具体值 **/ private static void setField(Object obj,Class<?> cl, String field, Object value) throws Exception { Field localField = cl.getDeclaredField(field); localField.setAccessible(true); localField.set(obj,value); }

合并数组:

/** * 两个数组合并 * @param arrayLhs * @param arrayRhs * @return */ private static Object combineArray(Object arrayLhs, Object arrayRhs) { Class<?> localClass = arrayLhs.getClass().getComponentType(); int i = Array.getLength(arrayLhs); int j = i + Array.getLength(arrayRhs); Object result = Array.newInstance(localClass, j); for (int k = 0; k < j; ++k) { if (k < i) { Array.set(result, k, Array.get(arrayLhs, k)); } else { Array.set(result, k, Array.get(arrayRhs, k - i)); } } return result; }// [12345] [9876]// [9876 12345]

备注:

BaseDexClassLoader类中的

DexPathList pathList;

DexPathList类中的

Element[] dexElements;

源码链接: http://androidxref.com/4.4.2_r1/xref/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java#pathList http://androidxref.com/4.4.2_r1/xref/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java 提供参考源码解析文章阅读: http://blog.csdn.net/ch15851302205/article/details/44671687

Element[] dexElements;原来的dex文件集合 Element[] dexElements2;合并以后的文件集合

四、测试

通过上面代码的处理,应用就可以实现热修复,那么该怎么生成修复的dex文件呢?

1.找到MyTestClass.class fixdix_test/app/build/intermediates/bin/TestClass.class 2.配置dx.bat的环境变量 Android/sdk/build-tools/23.0.3/dx.bat 3.命令

dx --dex --output=D:/Users/song/Desktop/dex/classes2.dex D:/Users/ricky/Desktop/dex

命令解释: –output=D:/Users/song/Desktop/dex/classes2.dex 指定输出路径 D:/Users/song/Desktop/dex 最后指定去打包哪个目录下面的class字节文件(注意要包括全路径的文件夹,也可以有多个class)


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表