[热修复]--源码级分析以及项目实践

杂谈

最近在研究gradle ,插件化~自己碰到的坑很多.今天先总结一下

以下这三个都研究过,原理都是一样的,区别就在于用哪个更方便.
在这里我会讲述一下,这里面的原理和自己爬的坑,以便大家理解,还有少爬坑~~

原理是需要懂得~
不然,你遇到错误不会解决,并且你始终会是初级工程师~

首先,按照顺序,介绍下目前三种热修复的方式:

1.Nuwa (基于gradle写的脚本,操作起来比较麻烦,需要拷贝和运行命令行~)
https://github.com/jasonross/Nuwa

2.andfix (看过~)
https://github.com/alibaba/AndFix

3.基于Nuwa和andfix升级的RocooFix(上线项目正在用)
有两种模式 静态修复某种情况下需要重启应用。2动态修复,无需重启应用即可生效。
https://github.com/dodola/RocooFix

4.如果觉得上述的几个demo,没有实战的结合,那么请用我的项目玩一玩.自己的git成熟的架构项目,里面集成了热修复RocooFix,当然也可以学习其中的架构,大家可以下载,欢迎大家批评~
https://github.com/ccj659/clean-project-architecture


一.热修复原理

Android 插件化技术

这里写图片描述

插件化其实就是class动态加载技术

其中,热修复就是讲dex,进行分包,然后让虚拟机先加载第一个dex,的过程.
当然,说永远比做简单,对,这就是我现在还不能像这些大师一样,动手写点为人民服务的东西,自己会努力的~~ ps(鸡汤):程序员靠手吃饭,而不是靠嘴~~

原理第一步:理解CassLoader,

Android 中有三个 ClassLoader, 分别为URLClassLoader、PathClassLoader、DexClassLoader。其中

URLClassLoader 只能用于加载jar文件,但是由于 dalvik 不能直接识别jar,所以在 Android 中无法使用这个加载器。
PathClassLoader 它只能加载已经安装的apk。
DexClassLoader 在虚拟机上加载 dex上面的类

DexClassLoader 包含有一个dex数组Element[] dexElements,其中每个dex文件是一个Element,当需要加载类的时候会遍历 dexElements,如果找到类则加载,如果找不到从下一个 dex 文件继续查找。

BaseDexClassLoader源码(大家可以自行研究):
http://androidxref.com/4.0.4/xref/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java

原理第二步:防止被打上CLASS_ISPREVERIFIED.

当dalvik在加载android程序时候,其实不是读的class文件,
而是首先对dex进行优化,生成Odex(Optimized dex )文件(为了适配不同硬件平台),
虚拟机在启动优化的时候,会有一个选项就是 verify 选项,当 verify 选项被打开的时候,就会执行一次校验,校验的目的是为了判断,这个类是否有引用其他 dex 中的类,如果没有,那么这个类会被打上一个 CLASS_ISPREVERIFIED 的标志。一旦被打上这个标志,就无法再从其他 dex 中替换这个类了。而这个选项开启,则是由虚拟机控制的。(引自张涛-开源实验室)


二.代码实现

RocooFix代码实现之app如何动态加载更新:合并dex:

实现原理和Nuwa一样~,只不过它对sdk进行了适配.
都是分别获取主应用和补丁的dex中的PathList.dexElements,然后将两者进行拼接(补丁包在最前面即, newEles[0] = element;),形成新的array,然后返回给classLoader.

private static final class V24 {

    private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
                                File optimizedDirectory)
            throws IllegalArgumentException, IllegalAccessException,
            NoSuchFieldException, InvocationTargetException, NoSuchMethodException, InstantiationException, ClassNotFoundException {
        //寻找pathList,并且将私有变为可访问
        Field pathListField = findField(loader, "pathList");
        Object dexPathList = pathListField.get(loader);

        //寻找寻找pathList的dexElements,并且将私有变为可访问
        Field dexElement = findField(dexPathList, "dexElements");
        Class<?> elementType = dexElement.getType().getComponentType();
        Method loadDex = findMethod(dexPathList, "loadDexFile", File.class, File.class, ClassLoader.class, dexElement.getType());

        loadDex.setAccessible(true);
        //利用反射调用类中的方法
        Object dex = loadDex.invoke(null, additionalClassPathEntries.get(0), optimizedDirectory, loader, dexElement.get(dexPathList));
        Constructor<?> constructor = elementType.getConstructor(File.class, boolean.class, File.class, DexFile.class);
        constructor.setAccessible(true);
        Object element = constructor.newInstance(new File(""), false, additionalClassPathEntries.get(0), dex);
        //将将两者进行拼接(补丁包在最前面即, newEles[0] = element;)
        Object[] newEles = new Object[1];
        newEles[0] = element;
        expandFieldArray(dexPathList, "dexElements", newEles);
    }

}

RocooFix代码实现之如何编译生成补丁包,强大的grooxy脚本构建项目:

如何逃过 虚拟机打上CLASS_ISPREVERIFIED ?

在RocooFixPlugin中作者采用了nuwa的字节码的方法  

HotFixProcessors.processClass() 来避免这个问题.有兴趣的大家可 以研究一下

如何判定那个文件进行了改变?

在 project.task(rocooClassBeforeDex) 中

NuwaProcessor.processJar(hashFile, inputFile, patchDir, h   ashMap, includePackage, excludeClass)

循环遍历工程中的全部类对每个类计算hash值,并写入到hashFile文件中.通过比较hashFile文件与原工程的hashFile(即这里的hashMap参数) ,得到所有修改过的类生成这些类的class文件,以及所有修改过的class文件的集合jar文件。

  def path = inputFile.absolutePath
                                //借用nvwa的代码,就是找出哪些类是发生了改变,应该生成对应的补丁。
                                //如果不是support包或者引入的依赖库,则开始生成代码修改部分的hotfix包
                                if (NuwaProcessor.shouldProcessPreDexJar(path)) {
                                    NuwaProcessor.processJar(hashFile, inputFile, patchDir, hashMap, includePackage, excludeClass)
                                }
 /*循环遍历循环遍历工程中的全部类对每个类计算hash值,并写入到hashFile文件中.通过比较hashFile文件与原工程的hashFile(即这里的classHashMap参数) ,得到所有修改过的类生成这些类的class文件,以及所有修改过的class文件的集合jar文件。
*/                                NuwaProcessor.processClasses(inputFile, includePackage, excludeClass, dirName, hashMap, patchDir)
                                def path = inputFile.absolutePath
                                if (path.endsWith(".class") && !path.contains("/R\$") && !path.endsWith("/R.class") && !path.endsWith("/BuildConfig.class")) {
                                    if (NuwaSetUtils.isIncluded(path, includePackage)) {
                                        if (!NuwaSetUtils.isExcluded(path, excludeClass)) {
                                            //写入字节吗,防止被打上class_prevertified
                                            def bytes = NuwaProcessor.processClass(inputFile)
                                            path = path.split("${dirName}/")[1]
                                            def hash = DigestUtils.shaHex(bytes)
                                            hashFile.append(RocooUtils.format(path, hash))

                                            if (RocooUtils.notSame(hashMap, path, hash)) {
                                                def file = new File("${patchDir}/${path}")
                                                file.getParentFile().mkdirs()
                                                if (!file.exists()) {
                                                    file.createNewFile()
                                                }
                                                FileUtils.writeByteArrayToFile(file, bytes)
                                            }
                                        }
                                    }
                                }

三.项目实践(具体项目请看我的github)

配置roccofix简单配置 请到https://github.com/dodola/RocooFix,
详细的配置说明,请查看我的git项目我的git项目

Use

public class RocooApplication extends Application {
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        //打补丁
        RocooFix.init(this);
    }
}


//方案1:静态启用,一般在Application里加载补丁
/**
  * 从Assets里取出补丁,一般用于测试
  *
  * @param context
  * @param assetName
  */
RocooFix.initPathFromAssets(Context context, String assetName);
 /**
   * 从指定目录加载补丁
   * @param context
   * @param dexPath
   */
RocooFix.applyPatch(Context context, String dexPath);


//方案2:动态打补丁,立即生效,有性能问题,适用于补丁方法数较少的情况,建议在ART虚拟机里启用该模式
/**
 * 从Asset里加载补丁,一般用于本地测试
 * @param context
 * @param assetName
 */
RocooFix.initPathFromAssetsRuntime(Context context, String assetName) ;

/**
 * 从指定目录加载补丁
 * @param context
 * @param dexPath
 */ 
RocooFix.applyPatchRuntime(Context context, String dexPath)  ;

Configuration

  1. 在root的build.gradle增加如下内容:
 repositories {
        jcenter()
        maven {
            url "http://dl.bintray.com/dodola/maven"
        }
    }
 dependencies {
        classpath 'com.dodola:rocoofix:1.1’
    }
  1. 在你项目的build.gradle文件里添加如下配置
apply plugin: 'com.dodola.rocoofix'

repositories {
    maven {
        url  "http://dl.bintray.com/dodola/maven"
    }
}

rocoo_fix {
    includePackage = ['com/dodola/rocoofix']//限制需要制作补丁的package
    excludeClass = ['BaseApplication.class']//将不需要加到patch里的类写在这里

    preVersionPath = '1'//注意:此项属性只在需要制作补丁的时候才需开启!!如果不需要制作补丁则需要去掉此项

    enable = true//注意:关掉此项会无法生成Hash.txt文件

    scanref=true//默认为 false,开启这个选项会将与补丁 class 相引用的 class 都打入包中来解决 ART 虚拟机崩溃问题,功能 Beta 中
}

dependencies {

    compile 'com.dodola:rocoo:1.0'
}

这里主要介绍一下preVersionPath这个属性的作用。

rocoo_fix将制作补丁的步骤透明化,用户无需手动备份hash.txt文件,插件会自动根据当前的versionCode生成hash.txtmapping.txt文件到指定目录,比如:

上一个版本发布的时候版本号是1,那么生成的文件会放在app源码目录/rocooFix/version1/[debug]|[release]的目录下,如果需要制作补丁那么在配置里指定preVersionPath 属性,它的值是上一个版本的版本号,这里的值是1

然后将build.gradleversionCode的号码修改,这里修改成2,只要和之前的版本不同就可以,没有具体值的要求

Proguard

-keep class com.dodola.rocoofix.** {*;}
-keep class com.lody.legend.** {*;}

Build Patch

下面演示一下使用项目demo生成补丁的制作过程

  1. 假如我们需要打补丁的文件是
package com.dodola.rocoosample;

public class HelloHack {

    public String showHello() {
        return "hello world";
    }
}

此时build.gradle里的VersionCode1

这里写图片描述

  1. 运行一次应用,这时会在app的目录下生成如下文件:

这里写图片描述

这里可以看做是我们已经发布版本的hash.txt

  1. 假设我们需要修复步骤1 里的showHello方法,修改如下:
package com.dodola.rocoosample;

public class HelloHack {

    public String showHello() {
        return "hello Hack";//此处修复,补丁加载后该方法返回hello hack
    }
}
  1. 修改build.gradle 文件里rocoo_fix项,让其执行patch 的task,配置如下
rocoo_fix {

    preVersionPath = '1'//注意:这里指定的是需要打补丁的VersionCode
    enable = true
}
  1. 修改当前项目的versionCode2,说明这个是一个升级fix版本。

enter description here

  1. 正常发布应用,此时会在下图所示的路径中生成补丁文件:
    这里写图片描述

  2. 我们可以反编译一下来确认补丁是否正常
    这里写图片描述



四.注意

这边说几点注意事项:

1.注意代码混淆配置,如果项目第三方比较多,就比较蛋疼了…,可仿照

https://github.com/ccj659/clean-project-architecture的混淆规则进行编写.
对于不会进行 proguard-rules的同学
1.Warning: can’t find referenced field/method ‘…’ in library class (or: can’t find referenced classes..)
2.Warning: library class … depends on program class …
3.Warning: class file … unexpectedly contains class …
4. Warning:Ignoring InnerClasses attribute for an anonymous inner class

报错情况1,3:

大概意思是程序里面用到了库里的类,但库里没有这些类。所以警告了,同时,可以用命令来忽略这些警告。
只是忽略警告的话,实际上还是没有解决问题,后来查找了一些资料,发现是proguard在混淆的时候,把库里的jar包也混淆了,导致程序找不到这些类。于是解决方法就简单了,只需要在proguard.conf配置中用keep命令保留报警的类就行了。但是通常报警的类有成百上千个,不可能一个个保留,有一个简单的方法是只要类的路径前面有相同的根,就可以保留一个总的达到保留下面所有类的目的。比如:
解决办法:
1.-dontwarn org.springframework.** //报错的包路径
2.-keep class org.springframework.* { ;} //报错的包路径

报错情况2: 第2个问题与第一个类似,也是保留相应的类就可以了。 大概意思是类文件的目录名跟类的包名不一致导致了警报,需要确保目录和包名一样才行。特别是WEB-INF/classes目录下的文件需要打成jar包放到WEB-INF/lib 目录下。

 我的这个问题正是出在WEB-INF/classes/....目录这儿,然后我把classes下的类打包放到了lib下,结果然并卵。后来又试了试,发现因为proguard会同时读取classes下的类和lib下的jar包,jar包像上面说的一样是没有问题的,但是classes下的类目录必然要带着classes,然而类名却不是以WEB-INF.classes开头,所以这个警报不可避免会出现。最后消除警报是在-injars里面添加过滤把classes下的类都过滤掉,还有别忘了把jar包添加到lib里。

解决办法:
-injars D:/OrbitService/target/OrbitService.war(!WEB-INF/classes/**)

报错情况4 : 出现这个问题的愿意很简单。

   就是android sdk tools版本过低,升级一下就可以了

2.生成patch.jar的步骤

生成patch流程

3.混淆一定要开.至于没什么,因为涉及到hash和mapping,还有gralde中

4.在实际项目中, 进入app 请求服务器,看是否有补丁,以及补丁的版本,然后根据版本进行判断, 下载 调用 RocooFix.applyPatch(context, dexPath); , 同时 记录版本信息, 保存到 sharepreference, 然后悄悄的进行,下次启动再请求网络,得到数据,根据数据判断补丁版本是否对应, 然后进行上述操作~,

5.android热修复,原理是一样的,有些错误不认识,注意查看 message

这里写图片描述

关于具体的操作,解释.我这边都放在了自己的代码中,大家可以查看,理解.



最后,附上自己的请查看我的git项目我的git项目,我会不定期的更新,整合~欢迎star~谢谢~:

本页内容版权归属为原作者,如有侵犯您的权益,请通知我们删除。
题记——  难过了,悄悄走一走;         伤心了,默默睡一觉;         优雅不是训练出来的,而是一种阅历;         淡然不是伪装出来的,而是一种沉淀;         时间飞逝,老去的只是我们的容颜;         时间仿佛一颗灵魂,越来越动人; 1、简述:     在多线程的世界中,是那么的神奇 与 高效以及合理; 2、创建线程池实例     官方推荐使用Executors类工厂方法来创建线程池管理,Executors类是官方提供的一个工厂类,里面封装了好多功能不一样的线程池,
书接上回,我们已经了解了一些关于适配的一些相关概念,接下来我们会了解一下,在设置布局时我们应该注意的地方。 尽量不去设定具体的尺寸值。 为了确保布局适应各种尺寸的屏幕,在保证功能实现的前提下,最好不要写死一些尺寸,这样的硬编码,我们最好使用“match_parent”,”wrap_content”,”weight”这些不用指定具体的尺寸值的参数,这样视图就会根据自身需要的空间去充填。这样就可以让布局去适应各种屏幕的尺寸,当屏幕有旋转时也不会受到影响。 这里我们重点说一下“weight”,用过 LiearL

艺术般的波浪点击反馈效果 - 2016-07-24 17:07:27

Material Design之Rippledrawable 使用与简单封装(向下兼容至selector) 前言 Android 5.0问世以来,谷歌所推崇的Material Design得到业界的一致好评,其良好的UI规范与交互确实让界面交互友好和漂亮了不少,Rippledrawable便是其中之一,本博客今天着重讲如何将它运用到我们自己的项目中,并且封装得简单易用。 我们都知道,我们在之前做按钮或者布局的反馈效果,一般都用selector来实现,分别指定按下或正常状态的两种颜色即可,我们点击的效果也本
ubuntu环境 首先确定是否安装了Git管理工具 sudo apt-get install git 我选择SSH方式,比较安全方便,只需一次配置 1- 使用ssh命令连接github.com的SSH服务,登录名为git@github.com(所有GitHub用户共享此SSH用户名)。 wangxiong @Dell :~/Public/GitHubRepository/PaPaPlayer $ ssh - T git @github .com The authenticity of host 'gith
         每次看到iOS的远程消息推送,总是感觉很头大,即便后来项目都做完了,还是觉得摸不着远程推送的脉门,网上介绍的资料虽多,但不是写的太简单了,就是写的太详细了,不能一下抓住要点,今天终于能够抽出点时间,来扒一扒这其中究竟有怎样的奥秘。     根据苹果掌控一切的习惯,消息推送也当然不能例外,不论你在哪里推送,也不论你用什么方式推送,都必须首先把消息发给苹果的消息推送服务器APNs(Apple Push Notification Service),然后再由APNs发给指定的设备,也就是说消息推
Day02 Html、Css实战和WebView实现手机显示网页 1.html与css实战 1.1 程序猿小网页 先来看一下效果图 编程用图如下 实现代码如下 !DOCTYPE htmlhtml head meta charset="utf-8" title/title style #pic{ position: relative; float: left; } #text{ width: 400; height: 200; position: relative; float: left; font-si
前言 或许你知道了jni的简单调用,其实不算什么百度谷歌一大把,虽然这些jni绝大多数情况下都不会让我们安卓工程师来弄,毕竟还是有点难,但是我们还是得打破砂锅知道为什么这样干吧,至少也让我们知道调用流程和数据类型以及处理方法,或许你会有不一样的发现。 其实总的来说从java的角度来看.h文件就是java中的interface(插座),然后.c/.cpp文件呢就是实现类罢了,然后数据类型和java还是有点出入我们还是得了解下(妈蛋,天气真热不适合生存了)。 今天也给出一个JNI动态注册native方法的例子

Android渐变标题栏的实现 - 2016-07-24 14:07:56

Android4.4以上推出了Toolbar,改变程序的style属性就可以给手机的标题栏填充颜色,可以是你设置好的系统的主题色,也可以是自己填充的颜色,其实这个效果在iOS早就有了,但在Android中还是很少见的。在iOS中,最常见的Navigationbar的效果就是一个转场动画(多出现于两个界面切换的时候),一个就是随着手势滑动背景渐变(多出现于详情页)。今天我们就来实现下大多出现于详情页的这个渐变效果的标题栏。 具体效果见: 点击打开链接 接下来我们就来实现这个效果。 首先,我们要先把手机上面的
这一篇,承接地八话。使用高效的方式备份短信——xml序列化器。 存储短信,要以对象的方式存储。首先创建javabean: package com.itydl.createxml.domain;public class Message {private String body;private String date;private String address;private String type;public String getBody() {return body;}public void setB

android独特的天气预报 - 2016-07-24 14:07:50

android独特的天气预报 package com.dchan.myweather;import java.io.UnsupportedEncodingException;import java.net.URLEncoder;import java.security.PublicKey;import java.util.ArrayList;import java.util.Calendar;import java.util.Collection;import java.util.HashMap;impo