Android打补丁 热修复(HotFix)小结

来源:互联网 发布:windows设置ntp服务器 编辑:程序博客网 时间:2024/05/16 07:03

需求场景:

   当我们的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
  • 网上还有插件式解决方案,DynamicLodhttps://github.com/singwhatiwanna/dynamic-load-apk
  • google官方目前在已经在API 21中提供了通用的解决方案,那就是android-support-multidex.jar. 这个jar包最低可以支持到API 4的版本(Android L及以上版本会默认支持mutidex).
说了这么多,到底跟我们的热修复有什么关系呢?
以上三种解决方案都是基于dex分包:
    dex分包的解决方案。简单来说,其原理是将编译好的class文件拆分打包成两个dex,绕过dex方法数量的限制以及安装时的检查,在运行时再动态加载第二个dex文件中。
这里需要注意的是在具体的实施过程中,可以都不同的形式,比如DynamicLod使用的是apk文件,可以包含资源文件。不涉及资源时,可以使用简单的编译过的jar文件。简单来说,只要能让ClassLoader加载到dex文件的归档文件都是可以实现的(甚至可以是zip)。
    OK~
    这就是补丁的基础,让app加载多个dex文件,假如我的发布包里有一个Qzone.class,发布之后发现这个类有bug,然后修改了一行代码(一定是大意导致的~~~),然后把这个类打成dex包(具体操作后文详述)。客户端通过某种途径下载到客户端(一般通过是下载),启动应用时让app加载这个patch.jar包。注意原本我们的项目里有一个Qzone.class,此处我们的patch.jar里面也有一个Qzone.class,那么ClassLoader在加载dex的时候是怎么加载的呢?我们看看BaseClassLoader的源码:

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;  }  
一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element,多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找。
理论上,如果在不同的dex中有相同的类存在,那么会优先选择排在前面的dex文件的类,此处盗一张图:



也就是说,如果patch.jar的dex在app包dex的前面,修复过Qzone.class会被加载,原来包里的Qzone.class被忽略。

说到这儿,相信看懂的同学已经笑了,原来补丁的原理这么简单~~~

OK~
那我们怎么去实现这个Android的补丁方案呢,网上有几种解决方案:
  • 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
其中后两个解决方案是基于C的实现,
前三个解决方案是java的解决方案,思路和实现方式基本一致。因为Nuwa的内部实现了很多自动化的处理,本文以Nuwa为例

Nuwa框架实现
使用Nuwa的第一步是初始化,源码如下:
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.csdn.net/sbsujjbcy/article/details/50812674

其实这个问题Nuwa框架内部已经解决了,我们要做的就是给app打补丁包,下面我们来看看怎么给app打补丁。

OK~
一开始我们的app运行的界面是这样的:

接下来我们把Hello.java代码修改掉:
public class Hello {    public String say() {        return "hello world~~~ After Fix";    }}
然后需要把我们的Hello.java打成patch_dex.jar包:

下一步就是把我们的patch.jar加载进来,一行代码搞定:

classjar(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
最后要在Application里面加载我们的补丁包:

Nuwa.loadPatch(this, Environment.getExternalStorageDirectory().getAbsolutePath().concat("/patch_dex.jar"));

同样是调用loadPatch()和injectDexAtFirst()方法将dex插入到dexElements最前面。
关闭app.重新打开,MainActivity的界面变成这样了:

到此为止,Nuwa框架的实现和流程就分析完了,希望对大家有一些帮助~~~




1 0
原创粉丝点击