需求场景:
当我们的app发布以后,发现有bug,比如维护数据错误,应用逻辑错误,严重的可能引发应用崩溃。这时修改应用可能只需要修改几行代码,或者某个方法就可以搞定。以前为了解决这样的问题发只能发布新版本。而紧急发布新版本会造成很恶劣的影响,使用户使用的成本升高,并且影响产品在用户心中的形象(不靠谱啊~~~)。
技术背景:
在不断迭代我们的应用的时候,功能越多,不可避免的方法量也不断增加,当方法量不断增加,最终可能会遇到这样的问题:
1.生成的apk在2.3以前的机器无法安装,提示INSTALL_FAILED_DEXOPT
原因:
首先我们要知道打包过程中我们开发的java类的变化,首先java类被编译成class文件,接着class文件会被编译生成dex文件,我们打包完成后,一个App的所有代码都在一个dex文件中(class.dex,解压apk就可以看到)。当Android系统启动一个应用的时候,会使用DexOpt工具对Dex进行优化,DexOpt的执行过程是在第一次加载Dex文件的时候执行的。这个过程会生成一个ODEX文件。执行ODex的效率会比直接执行Dex文件的效率要高很多。但是在早期的Android系统中,DexOpt的LinearAlloc存在着限制: Android 2.2和2.3的缓冲区只有5MB,Android 4.x提高到了8MB或16MB。当方法数量过多导致超出缓冲区大小时,会造成dexopt崩溃,导致无法安装.
2. 方法数量过多,编译时出错,提示:
Conversion to Dalvik format failed:Unable to execute dex: method ID not in [0, 0xffff]: 65536
原因:这是由于dex的文件限制,dex文件中method的的索引的id类型被定义为short类型(0~65535),field和class的个数也有此限制。导致dex文的方法总数被限制为65536(包括自己开发以及所引用的Android Framework和第三方类库的代码)。
解决方案:
- facebook曾经遇到过这样的问题,详情查看:https://www.facebook.com/notes/facebook-engineering/under-the-hood-dalvik-patch-for-facebook-for-android/10151345597798920
- 网上还有插件式解决方案,DynamicLod:https://github.com/singwhatiwanna/dynamic-load-apk
- google官方目前在已经在API 21中提供了通用的解决方案,那就是android-support-multidex.jar. 这个jar包最低可以支持到API 4的版本(Android L及以上版本会默认支持mutidex).
public Class findClass(String name, List<Throwable> 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;
}
理论上,如果在不同的dex中有相同的类存在,那么会优先选择排在前面的dex文件的类,此处盗一张图:
也就是说,如果patch.jar的dex在app包dex的前面,修复过 Qzone.class会被加载,原来包里的Qzone.class被忽略。
- https://github.com/dodola/HotFix
- https://github.com/jasonross/Nuwa
- https://github.com/bunnyblue/DroidFix
- https://github.com/alibaba/dexposed
- https://github.com/alibaba/AndFix
public static void init(Context context) {
File dexDir = new File(context.getFilesDir(), DEX_DIR);
dexDir.mkdir();
String dexPath = null;
try {
dexPath = AssetUtils.copyAsset(context, HACK_DEX, dexDir);
} catch (IOException e) {
Log.e(TAG, "copy " + HACK_DEX + " failed");
e.printStackTrace();
}
loadPatch(context, dexPath);
}
init()函数做了两件事:1,把asset目录下的hack,apk拷贝到应用的私有目录下;2,加载hack.apk到ClassLoader中dexElement的最前面。
loadPatch方法也是之后进行热修复的关键方法,你的所有补丁文件都是通过这个方法动态加载进来
public static void loadPatch(Context context, String dexPath) {
if (context == null) {
Log.e(TAG, "context is null");
return;
}
if (!new File(dexPath).exists()) {
Log.e(TAG, dexPath + " is null");
return;
}
File dexOptDir = new File(context.getFilesDir(), DEX_OPT_DIR);
dexOptDir.mkdir();
try {
DexUtils.injectDexAtFirst(dexPath, dexOptDir.getAbsolutePath());
} catch (Exception e) {
Log.e(TAG, "inject " + dexPath + " failed");
e.printStackTrace();
}
}
其中调用injectDexAtFirst将dex放到ClassLoader中dexElements的最前面的方法:
public static void injectDexAtFirst(String dexPath, String defaultDexOptPath) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
DexClassLoader dexClassLoader = new DexClassLoader(dexPath, defaultDexOptPath, dexPath, getPathClassLoader());
Object baseDexElements = getDexElements(getPathList(getPathClassLoader()));
Object newDexElements = getDexElements(getPathList(dexClassLoader));
Object allDexElements = combineArray(newDexElements, baseDexElements);
Object pathList = getPathList(getPathClassLoader());
ReflectionUtils.setField(pathList, pathList.getClass(), "dexElements", allDexElements);
}
内部使用combineArray()方法将这两个对象进行结合,将我们传进来的dex插到该对象的最前面,之后调用ReflectionUtils.setField()方法,将dexElements进行替换。combineArray方法中做的就是扩展数组,将第二个数组插入到第一个数组的最前面
private static Object combineArray(Object firstArray, Object secondArray) {
Class<?> localClass = firstArray.getClass().getComponentType();
int firstArrayLength = Array.getLength(firstArray);
int allLength = firstArrayLength + Array.getLength(secondArray);
Object result = Array.newInstance(localClass, allLength);
for (int k = 0; k < allLength; ++k) {
if (k < firstArrayLength) {
Array.set(result, k, Array.get(firstArray, k));
} else {
Array.set(result, k, Array.get(secondArray, k - firstArrayLength));
}
}
return result;
}
这个hack.apk里面只有一个类,下面看一下这个Hack.java的源码:
public class Hack {
}
因为这里面还存在一个CLASS_ISPREVERIFIED
的问题,对于这个问题呢,详见:安卓App热补丁动态修复技术介绍
关于这个CLASS_ISPREVERIFIED
,简单来说就是:
在虚拟机启动的时候,当verify选项被打开的时候,如果static方法、private方法、构造函数等,其中的直接引用(第一层关系)到的类都在同一个dex文件中,那么该类就会被打上CLASS_ISPREVERIFIED
标志。
注意,是阻止引用者的类,在Nuwa的示例里面,MainActivity内部引用了Hello。发布过程中发现Hello
有编写错误,那么想要发布一个新的Hello
类,那么你就要阻止MainActivity
这个类打上CLASS_ISPREVERIFIED
的标志。也就是说,在生成apk之前,就需要阻止相关类打上CLASS_ISPREVERIFIED
的标志了。对于如何阻止,上面的文章说的很清楚,让MainActivity
在构造方法中,去引用别的dex文件,在本例中,就是hack.apk。
关于注入Hello方式,具体参见:http://blog.youkuaiyun.com/sbsujjbcy/article/details/50812674
其实这个问题Nuwa框架内部已经解决了,我们要做的就是给app打补丁包,下面我们来看看怎么给app打补丁。一开始我们的app运行的界面是这样的:
接下来我们把Hello.java代码修改掉:
public class Hello {
public String say() {
return "hello world~~~ After Fix";
}
}
然后需要把我们的Hello.java打成patch_dex.jar包:
下一步就是把我们的patch.jar加载进来,一行代码搞定:
class打成jar包(可指定class文件)
jar cvf patch.jar cn/ jiajixin/nuwasample/Hello/Hello.java
jar打成dex包(dx工具在sdk的build-tools目录下)
dx --dex --output patch_dex.jar patch.jar
Nuwa.loadPatch(this, Environment.getExternalStorageDirectory().getAbsolutePath().concat("/patch_dex.jar"));
关闭app.重新打开,MainActivity的界面变成这样了: