您的位置:首页 > 移动开发 > Android开发

Android热补丁动态修复技术(二):实践

2016-09-17 00:50 543 查看
一、前言

上一篇博客中,我们简单的介绍了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的手机上却可以正常运行,我也没找到具体原因,
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐