Android热补丁动态修复技术(二):实践
2016-09-17 00:50
543 查看
一、前言
上一篇博客中,我们简单的介绍了dex分包的原理,这节我们就通过代码实践的方式来阐述如何使用dex分包实现热修复的问题,要想实现我们需要面临解决两个问题。
1. 怎么将修复后的Bug类打包成dex
2. 怎么将外部的dex插入到ClassLoader中
(1)建立测试工程
(2)代码片段
布局很简单, 在activity_main中放了一个按钮,用来演示点击事件的触发,这里就不粘出来了.
(3)运行结果
假设这是我们公司的开发项目,刚刚上线就发现了严重bug,本来HelloHack应该返回”hello world fix by storm”,却返回了”hello world”
想修复bug,让用户再立刻更新一次显然很不友好,此时热补丁修复技术就派上用场了。
二.制作补丁
1. 首先我们将HelloHack类修复,然后重新编译项目。(Rebuild一下就行了)
2. 去保存项目的地方,将HelloHack.class文件拷贝出来,在这里
新建文件夹,要和HelloHack.class文件的包名一致(注意这里是根据包的层级结构建立对应的文件目录),然后将HelloHack.class复制到这里,如图
然后cd到makePatchJar目录,执行命令
jar cvf fixbug.jar com/*
这条命令就是把com下的所有文件打包到fixbug.jar文件中,最终会如图所示在当前目录生成fixbug.jar包.
4.接下需要把jar文件转换成dex文件: 在当前目录下执行
(前提是在环境变量中配置了dx命令,如何配置请自行百度,因为这是太基础的问题了,哈哈)。最后就会在当前目录下看的patch.jar文件.
tips:说下在执行过程中遇到的坑:
执行jar cvf fixbug.jar com/*
异常:bad class file magic (cafebabe) or version (0033.0000)
解决方法:在build.gradle中jdk的版本修改为1.6
导致这个问题的原因是由于Android项目属性中的Java Compiler设置为1.6版本的,而AndroidStudio中的被设置为1.7所致,也换成1.6就不会提示这个错误了.
大家看到了,上面的步骤有点繁琐,还需要手工操作很容易出现问题,其实我们完全可以使用gradle自动化生成dex文件,这需要对Groovy有一定功底的童鞋才可以做到 ,由于时间的原因后续我会把这部分加上.
三:加载补丁
3.1:思路:
我们知道dex保存在这个位置
BaseDexClassLoader–>pathList–>dexElements
apk的classes.dex可以从应用本身的DexClassLoader中获取。 path_dex的dex需要new一个DexClassLoader加载后再获取。 分别通过反射取出dex文件,重新合并成一个数组,然后赋值给盈通本身的ClassLoader的dexElements(建议去看源码,加深理解,重点是pathClassLoader,DexClassLoader以及BaseDexClassLoader和DexPathList这几个文件,它们存在于davilk.system包下).
3.2:代码实现
加载外部dex,我们可以在Application中操作。
首先新建一个HotPatchApplication,然后在清单文件中配置,顺便加上读取sdcard的权限,因为补丁就保存在那里。了解了原理实现就比较简单了,我将代码分享给大家:
五、CLASS_ISPREVERIFIED
(这里说明下, 运行下demo是会报错的,但是在我的乐Max2,系统6.0.1的手机上却可以正常运行,我也没找到具体原因,
上一篇博客中,我们简单的介绍了dex分包的原理,这节我们就通过代码实践的方式来阐述如何使用dex分包实现热修复的问题,要想实现我们需要面临解决两个问题。
1. 怎么将修复后的Bug类打包成dex
2. 怎么将外部的dex插入到ClassLoader中
(1)建立测试工程
(2)代码片段
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); this.findViewById(R.id.click).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { HelloHack hack = new HelloHack(); Toast.makeText(MainActivity.this, hack.showHello(), Toast.LENGTH_SHORT).show(); } }); } public class HelloHack { public String showHello() { return "hello world"; } }
布局很简单, 在activity_main中放了一个按钮,用来演示点击事件的触发,这里就不粘出来了.
(3)运行结果
假设这是我们公司的开发项目,刚刚上线就发现了严重bug,本来HelloHack应该返回”hello world fix by storm”,却返回了”hello world”
想修复bug,让用户再立刻更新一次显然很不友好,此时热补丁修复技术就派上用场了。
二.制作补丁
1. 首先我们将HelloHack类修复,然后重新编译项目。(Rebuild一下就行了)
2. 去保存项目的地方,将HelloHack.class文件拷贝出来,在这里
新建文件夹,要和HelloHack.class文件的包名一致(注意这里是根据包的层级结构建立对应的文件目录),然后将HelloHack.class复制到这里,如图
然后cd到makePatchJar目录,执行命令
jar cvf fixbug.jar com/*
这条命令就是把com下的所有文件打包到fixbug.jar文件中,最终会如图所示在当前目录生成fixbug.jar包.
4.接下需要把jar文件转换成dex文件: 在当前目录下执行
(前提是在环境变量中配置了dx命令,如何配置请自行百度,因为这是太基础的问题了,哈哈)。最后就会在当前目录下看的patch.jar文件.
tips:说下在执行过程中遇到的坑:
执行jar cvf fixbug.jar com/*
异常:bad class file magic (cafebabe) or version (0033.0000)
解决方法:在build.gradle中jdk的版本修改为1.6
compileOptions { sourceCompatibility JavaVersion.VERSION_1_ 4000 6 targetCompatibility JavaVersion.VERSION_1_6 }
导致这个问题的原因是由于Android项目属性中的Java Compiler设置为1.6版本的,而AndroidStudio中的被设置为1.7所致,也换成1.6就不会提示这个错误了.
大家看到了,上面的步骤有点繁琐,还需要手工操作很容易出现问题,其实我们完全可以使用gradle自动化生成dex文件,这需要对Groovy有一定功底的童鞋才可以做到 ,由于时间的原因后续我会把这部分加上.
三:加载补丁
3.1:思路:
我们知道dex保存在这个位置
BaseDexClassLoader–>pathList–>dexElements
apk的classes.dex可以从应用本身的DexClassLoader中获取。 path_dex的dex需要new一个DexClassLoader加载后再获取。 分别通过反射取出dex文件,重新合并成一个数组,然后赋值给盈通本身的ClassLoader的dexElements(建议去看源码,加深理解,重点是pathClassLoader,DexClassLoader以及BaseDexClassLoader和DexPathList这几个文件,它们存在于davilk.system包下).
3.2:代码实现
加载外部dex,我们可以在Application中操作。
首先新建一个HotPatchApplication,然后在清单文件中配置,顺便加上读取sdcard的权限,因为补丁就保存在那里。了解了原理实现就比较简单了,我将代码分享给大家:
public class HotPatchApplication extends Application { @Override public void onCreate() { super.onCreate(); initPathFromAssets(this, "patch.jar"); } /** * 从Assets里取出补丁,一般用于测试 * * @param context * @param assetName */ public static void initPathFromAssets(Context context, String assetName) { File dexDir = new File(context.getFilesDir(), PATCH_PATH); dexDir.mkdir(); clearPaths(dexDir); 建议在实际开发中版本号不同时删除本地补丁,否则加载本地本地补丁而不是每次从网络读取 String dexPath = null; try { dexPath = copyAsset(context, assetName, dexDir); } catch (IOException e) { } finally { if (dexPath != null && new File(dexPath).exists()) { inject(dexpath); } } } public static String copyAsset(Context context, String assetName, File dir) throws IOException { File outFile = new File(dir, assetName); if (!outFile.exists()) { AssetManager assetManager = context.getAssets(); InputStream in = assetManager.open(assetName); OutputStream out = new FileOutputStream(outFile); copyFile(in, out); in.close(); out.close(); } return outFile.getAbsolutePath(); } private static void copyFile(InputStream in, OutputStream out) throws IOException { byte[] buffer = new byte[1024]; int read; while ((read = in.read(buffer)) != -1) { out.write(buffer, 0, read); } } /** * 要注入的dex的路径 * * @param path */ private void inject(String path) { try { // 获取classes的dexElements Class cl = Class.forName("dalvik.system.BaseDexClassLoader"); Object pathList = getField(cl, "pathList", getClassLoader()); Object baseElements = getField(pathList.getClass(), "dexElements", pathList); // 获取patch_dex的dexElements(需要先加载dex) String dexopt = getDir("dexopt", 0).getAbsolutePath(); DexClassLoader dexClassLoader = new DexClassLoader(path, dexopt, dexopt, getClassLoader()); Object obj = getField(cl, "pathList", dexClassLoader); Object dexElements = getField(obj.getClass(), "dexElements", obj); // 合并两个Elements Object combineElements = combineArray(dexElements, baseElements); // 将合并后的Element数组重新赋值给app的classLoader setField(pathList.getClass(), "dexElements", pathList, combineElements); //======== 以下是测试是否成功注入 ================= Object object = getField(pathList.getClass(), "dexElements", pathList); int length = Array.getLength(object); Log.e("BugFixApplication", "length = " + length); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { e.printStackTrace(); } } /** * 通过反射获取对象的属性值 */ private Object getField(Class cl, String fieldName, Object object) throws NoSuchFieldException, IllegalAccessException { Field field = cl.getDeclaredField(fieldName); field.setAccessible(true); return field.get(object); } /** * 通过反射设置对象的属性值 */ private void setField(Class cl, String fieldName, Object object, Object value) throws NoSuchFieldException, IllegalAccessException { Field field = cl.getDeclaredField(fieldName); field.setAccessible(true); field.set(object, value); } /** * 通过反射合并两个数组 */ private Object combineArray(Object firstArr, Object secondArr) { int firstLength = Array.getLength(firstArr); int secondLength = Array.getLength(secondArr); int length = firstLength + secondLength; Class componentType = firstArr.getClass().getComponentType(); Object newArr = Array.newInstance(componentType, length); for (int i = 0; i < length; i++) { if (i < firstLength) { Array.set(newArr, i, Array.get(firstArr, i)); } else { Array.set(newArr, i, Array.get(secondArr, i - firstLength)); } } return newArr; } }
五、CLASS_ISPREVERIFIED
(这里说明下, 运行下demo是会报错的,但是在我的乐Max2,系统6.0.1的手机上却可以正常运行,我也没找到具体原因,
相关文章推荐
- Android热补丁动态修复技术(一):从Dex分包原理到热补丁
- Android热补丁动态修复技术(四):自动化生成补丁——解决混淆问题
- Android热补丁动态修复技术(一):从Dex分包原理到热补丁
- Android热补丁动态修复技术
- (4.2.32.1)android热修复之ClassLoader方式:安卓App热补丁动态修复技术介绍(QQ控件||Nuwa)
- Android热补丁动态修复技术(二):实战!CLASS_ISPREVERIFIED问题!
- 【Android开发高级技术】Android 热补丁动态修复框架分析与总结
- Android热补丁动态修复技术(三)—— 使用Javassist注入字节码,完成热补丁框架雏形(可使用)
- Android热补丁动态修复技术(二):实战!CLASS_ISPREVERIFIED问题!
- Android热补丁动态修复技术(三)—— 使用Javassist注入字节码,完成热补丁框架雏形(可使用)
- Android热补丁动态修复实践
- Android热补丁动态修复技术(完结篇):自动生成打包带签名的补丁,重构项目
- Android热补丁动态修复技术(一):从Dex分包原理到热补丁
- Android热补丁动态修复技术(一):从Dex分包原理到热补丁
- Android热补丁动态修复技术(二):实战!CLASS_ISPREVERIFIED问题!(热修复技术)
- Android热补丁动态修复技术(一)dex分包原理
- Android热补丁动态修复实践
- Android热补丁动态修复技术(二):实战!CLASS_ISPREVERIFIED问题!
- Android 热补丁动态修复框架小结
- Android 热补丁动态修复框架总结