博客原地址:http://blog.csdn.net/allan_bst/article/details/72904721

一、什么是热修复

热修复说白了就是”打补丁”,比如你们公司上线一个app,用户反应有重大bug,需要紧急修复。如果按照通
常做法,那就是程序猿加班搞定bug,然后测试,重新打包并发布。这样带来的问题就是成本高,效率低。于是,热
修复就应运而生.一般通过事先设定的接口从网上下载无Bug的代码来替换有Bug的代码。这样就省事多了,用
户体验也好。(如下图所示:Android 插件化技术的三个技术点以及它们的应用场景)

二、热修复原理

ClassLoader

在 Java 中,要加载一个类需要用到 ClassLoader 。

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

  • URLClassLoader 只能用于加载jar文件,但是由于 dalvik 不能直接识别jar,所以在 Android 中无法使用这个加载器。
  • PathClassLoader 它只能加载已经安装的apk。因为 PathClassLoader 只会去读取 /data/dalvik-cache 目录下的 dex 文件。例如我们安装一个包名为 com.allan.xxx 的 apk,那么当 apk 安装过程中,就会在 /data/dalvik-cache 目录下生产一个名为 data@app@com.allan.xxx-1.apk@classes.dex 的 ODEX 文件。在使用 PathClassLoader 加载 apk 时,它就会去这个文件夹中找相应的 ODEX 文件,如果 apk 没有安装,自然会报 ClassNotFoundException 。
  • DexClassLoader 是最理想的加载器。它的构造函数包含四个参数:
[html] view plain copy print ?
  1. 1.dexPath,指目标类所在的APK或jar文件的路径.类装载器将从该路径中寻找指定的目标类,
  2. 该类必须是APK或jar的全路径.如果要包含多个路径,路径之间必须使用特定的分割符分隔,
  3. 特定的分割符可以使用System.getProperty(“path.separtor”)获得.
  4. 2.dexOutputDir,由于dex文件被包含在APK或者Jar文件中,因此在装载目标类之前需要先从APK或Jar文件
  5. 中解压出dex文件,该参数就是制定解压出的dex 文件存放的路径.在Android系统中,
  6. 一个应用程序一般对应一个Linux用户id,应用程序仅对属于自己的数据目录路径有写的权限,
  7. 因此,该参数可以使用该程序的数据路径.
  8. 3.libPath,指目标类中所使用的C/C++库存放的路径
  9. 4.classload,是指该装载器的父装载器,一般为当前执行类的装载器
    1.dexPath,指目标类所在的APK或jar文件的路径.类装载器将从该路径中寻找指定的目标类,该类必须是APK或jar的全路径.如果要包含多个路径,路径之间必须使用特定的分割符分隔,特定的分割符可以使用System.getProperty(“path.separtor”)获得.2.dexOutputDir,由于dex文件被包含在APK或者Jar文件中,因此在装载目标类之前需要先从APK或Jar文件中解压出dex文件,该参数就是制定解压出的dex 文件存放的路径.在Android系统中,一个应用程序一般对应一个Linux用户id,应用程序仅对属于自己的数据目录路径有写的权限,因此,该参数可以使用该程序的数据路径.3.libPath,指目标类中所使用的C/C++库存放的路径4.classload,是指该装载器的父装载器,一般为当前执行类的装载器

从 framework源码 中的 dalvik.system 包下,找到 DexClassLoader 源码,实际内容是在它的父类 BaseDexClassLoader 中,顺带一提,这个类最低在API14开始有用。包含了两个变量:

[java] view plain copy print ?
  1. private final String originalPath;
  2. private final DexPathList pathList;
  3. //pathList就是多dex的结构列表,查看 其源码:
  4. /** class definition context */
  5. private final ClassLoader definingContext;
  6. /** list of dex/resource (class path) elements */
  7. private final Element[] dexElements;
  8. /** list of native library directory elements */
  9. private final File[] nativeLibraryDirectories;
private final String originalPath;
private final DexPathList pathList;
//pathList就是多dex的结构列表,查看 其源码:
/** class definition context */
private final ClassLoader definingContext;/** list of dex/resource (class path) elements */
private final Element[] dexElements;/** list of native library directory elements */
private final File[] nativeLibraryDirectories;
dexElements 就是一个dex列表,那么我们就可以把每个 Element 当成是一个 dex。 

看下PathClassLoader代码

public class PathClassLoader extends BaseDexClassLoader {public PathClassLoader(String dexPath, ClassLoader parent) {super(dexPath, null, null, parent);}public PathClassLoader(String dexPath, String libraryPath,ClassLoader parent) {super(dexPath, null, libraryPath, parent);}
} 

DexClassLoader代码

public class DexClassLoader extends BaseDexClassLoader {public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {super(dexPath, new File(optimizedDirectory), libraryPath, parent);}
}

两个ClassLoader就两三行代码,只是调用了父类的构造函数.

public class BaseDexClassLoader extends ClassLoader {private final DexPathList pathList;public BaseDexClassLoader(String dexPath, File optimizedDirectory,String libraryPath, ClassLoader parent) {super(parent);this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {List<Throwable> suppressedExceptions = new ArrayList<Throwable>();Class c = pathList.findClass(name, suppressedExceptions);if (c == null) {ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);for (Throwable t : suppressedExceptions) {cnfe.addSuppressed(t);}throw cnfe;}return c;}

在BaseDexClassLoader 构造函数中创建一个DexPathList类的实例,这个DexPathList的构造函数会创建一个dexElements 数组

public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) {... this.definingContext = definingContext;ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();//创建一个数组this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions);... }

然后BaseDexClassLoader 重写了findClass方法,调用了pathList.findClass,跳到DexPathList类中.

/* package */final class DexPathList {...public Class findClass(String name, List<Throwable> suppressed) {//遍历该数组for (Element element : dexElements) {//初始化DexFileDexFile dex = element.dexFile;if (dex != null) {//调用DexFile类的loadClassBinaryName方法返回Class实例Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);if (clazz != null) {return clazz;}}}       return null;}...
} 

会遍历这个数组,然后初始化DexFile,如果DexFile不为空那么调用DexFile类的loadClassBinaryName方法返回Class实例.

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

那么我们的实现就是把这个插件 dex 插入到 Elements 的最前面,这么做的好处是不仅可以动态的加载一个类,并且由于 DexClassLoader 会优先加载靠前的类,所以我们同时实现了宿主 apk 的热修复功能。

三、热修复之HotFix2.0体验

先来看一下他的通俗易懂的原理图

接下来说一下如何实现hotfix,首先打开阿里百川网址http://baichuan.taobao.com,下载最新官方的SDK,其中包括打包工具,调试工具

1.首先在阿里百川的我的应用中添加一个个人应用,记下来应用的APP ID,APPKey,和RSA密钥

2.创建好应用后,打开AndroidStudio新建一个Project,然后集成hotfix

gradle远程仓库依赖, 打开项目找到app的build.gradle文件,添加如下配置:

添加maven仓库地址:

[html] view plain copy print ?
  1. repositories {
  2. maven {
  3. url "http://repo.baichuan-android.taobao.com/content/groups/BaichuanRepositories"
  4. }
  5. }
repositories {maven {url "http://repo.baichuan-android.taobao.com/content/groups/BaichuanRepositories"}
}
[html] view plain copy print ?
  1. 添加gradle坐标版本依赖:
  2. dependencies {
  3. compile 'com.taobao.android:alisdk-hotfix:2.0.9'
  4. }
添加gradle坐标版本依赖:dependencies {compile 'com.taobao.android:alisdk-hotfix:2.0.9'
}

如果编译期间报utdid重复, 所以此时进行如下处理即可, 关闭传递性依赖:

[html] view plain copy print ?
  1. compile ('com.taobao.android:alisdk-hotfix:2.0.9') {
  2. exclude(module:'utdid4all')
  3. }
compile ('com.taobao.android:alisdk-hotfix:2.0.9') {exclude(module:'utdid4all')
}

添加权限

[html] view plain copy print ?
  1. <! -- 网络权限 -->
  2. <uses-permission android:name="android.permission.INTERNET" />
  3. <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
  4. <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
  5. <! -- 外部存储读权限 -->
  6. <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<! -- 网络权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<! -- 外部存储读权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

如果API 23以上别忘了添加运行时权限,因为读写SD卡为危险权限

配置AndroidManifest文件,这时用到我们前面所说的那三个id,key,密钥了

AndroidManifest.xml中间的application节点下添加如下配置,吧value的数值改成那三个即可:

[html] view plain copy print ?
  1. <meta-data
  2. android:name="com.taobao.android.hotfix.IDSECRET"
  3. android:value="App ID" />
  4. <meta-data
  5. android:name="com.taobao.android.hotfix.APPSECRET"
  6. android:value="App Secret" />
  7. <meta-data
  8. android:name="com.taobao.android.hotfix.RSASECRET"
  9. android:value="RSA密钥" />
<meta-data
android:name="com.taobao.android.hotfix.IDSECRET"
android:value="App ID" />
<meta-data
android:name="com.taobao.android.hotfix.APPSECRET"
android:value="App Secret" />
<meta-data
android:name="com.taobao.android.hotfix.RSASECRET"
android:value="RSA密钥" />

3.接下来进行初始化工做,官方文档建议在App全局的Application的onCreate()方法进行初始化

[java] view plain copy print ?
  1. SophixManager.getInstance().setContext(this)
  2. .setAppVersion(appVersion)
  3. .setAesKey(null)
  4. .setEnableDebug(true)
  5. .setPatchLoadStatusStub(new PatchLoadStatusListener() {
  6. @Override
  7. public void onLoad(final int mode, final int code, final String info, final int handlePatchVersion) {
  8. // 补丁加载回调通知
  9. if (code == PatchStatus.CODE_LOAD_SUCCESS) {
  10. // 表明补丁加载成功
  11. } else if (code == PatchStatus.CODE_LOAD_RELAUNCH) {
  12. // 表明新补丁生效需要重启. 开发者可提示用户或者强制重启;
  13. // 建议: 用户可以监听进入后台事件, 然后应用自杀
  14. } else if (code == PatchStatus.CODE_LOAD_FAIL) {
  15. // 内部引擎异常, 推荐此时清空本地补丁, 防止失败补丁重复加载
  16. // SophixManager.getInstance().cleanPatches();
  17. } else {
  18. // 其它错误信息, 查看PatchStatus类说明
  19. }
  20. }
  21. }).initialize();
  22. SophixManager.getInstance().queryAndLoadNewPatch();
SophixManager.getInstance().setContext(this).setAppVersion(appVersion).setAesKey(null).setEnableDebug(true).setPatchLoadStatusStub(new PatchLoadStatusListener() {@Overridepublic void onLoad(final int mode, final int code, final String info, final int handlePatchVersion) {// 补丁加载回调通知if (code == PatchStatus.CODE_LOAD_SUCCESS) {// 表明补丁加载成功} else if (code == PatchStatus.CODE_LOAD_RELAUNCH) {// 表明新补丁生效需要重启. 开发者可提示用户或者强制重启;// 建议: 用户可以监听进入后台事件, 然后应用自杀} else if (code == PatchStatus.CODE_LOAD_FAIL) {// 内部引擎异常, 推荐此时清空本地补丁, 防止失败补丁重复加载// SophixManager.getInstance().cleanPatches();} else {// 其它错误信息, 查看PatchStatus类说明}}}).initialize();
SophixManager.getInstance().queryAndLoadNewPatch();

》》》》》》》》》》》》下面内容摘取自官方文档《《《《《《《《《《《《《

[html] view plain copy print ?
  1. 1.3.2 接口说明
  2. 1.3.2.1 initialize方法
  3. initialize(): <必选>
  4. 该方法主要做些必要的初始化工作以及如果本地有补丁的话会加载补丁, 但不会自动请求补丁。因此需要自行调用queryAndLoadNewPatch方法拉取补丁。这个方法调用需要尽可能的早, 推荐在Application的onCreate方法中调用, initialize()方法调用之前你需要先调用如下几个方法, 方法调用说明如下:
  5. setContext(this): <必选> Application上下文context
  6. setAppVersion(appVersion): <必选> 应用的版本号
  7. setAesKey(aesKey): <可选> 用户自定义aes秘钥, 会对补丁包采用对称加密。这个参数值必须是16位数字或字母的组合,是和补丁工具设置里面AES Key保持完全一致, 补丁才能正确被解密进而加载。此时平台无感知这个秘钥, 所以不用担心百川平台会利用你们的补丁做一些非法的事情。
  8. setEnableDebug(true/false): <可选> 默认为false, 是否调试模式, 调试模式下会输出日志以及不进行补丁签名校验. 线下调试此参数可以设置为true, 查看日志过滤TAG:Sophix, 同时强制不对补丁进行签名校验, 所有就算补丁未签名或者签名失败也发现可以加载成功. 但是正式发布该参数必须为false, false会对补丁做签名校验, 否则就可能存在安全漏洞风险
  9. setPatchLoadStatusStub(new PatchLoadStatusListener()): <可选> 设置patch加载状态监听器, 该方法参数需要实现PatchLoadStatusListener接口, 接口说明见1.3.2.2说明
  10. setUnsupportedModel(modelName, sdkVersionInt):<可选> 把不支持的设备加入黑名单,加入后不会进行热修复。modelName为该机型上Build.MODEL的值,这个值也可以通过adb shell getprop | grep ro.product.model取得。sdkVersionInt就是该机型的Android版本,也就是Build.VERSION.SDK_INT,若设为0,则对应该机型所有安卓版本。
  11. 1.3.2.2 queryAndLoadNewPatch方法
  12. 该方法主要用于查询服务器是否有新的可用补丁. SDK内部限制连续两次queryAndLoadNewPatch()方法调用不能短于3s, 否则的话就会报code:19的错误码. 如果查询到可用的话, 首先下载补丁到本地, 然后
  13. 应用原本没有补丁, 那么如果当前应用的补丁是热补丁, 那么会立刻加载(不管是冷补丁还是热补丁). 如果当前应用的补丁是冷补丁, 那么需要重启生效.
  14. 应用已经存在一个补丁, 首先会把之前的补丁文件删除, 然后不立刻加载, 而是等待下次应用重启再加载该补丁
  15. 补丁在后台发布之后, 并不会主动下行推送到客户端, 需要手动调用queryAndLoadNewPatch方法查询后台补丁是否可用.
  16. 只会下载补丁版本号比当前应用存在的补丁版本号高的补丁, 比如当前应用已经下载了补丁版本号为5的补丁, 那么只有后台发布的补丁版本号>5才会重新下载.
  17. 同时1.4.0以上版本服务后台上线了“一键清除”补丁的功能, 所以如果后台点击了“一键清除”那么这个方法将会返回code:18的状态码. 此时本地补丁将会被强制清除, 同时不清除本地补丁版本号
  18. 1.3.2.3 cleanPatches()方法
  19. 清空本地补丁
  20. 1.3.2.4 PatchLoadStatusListener接口
  21. 该接口需要自行实现并传入initialize方法中, 补丁加载状态会回调给该接口, 参数说明如下:
  22. mode: 补丁模式, 0:正常请求模式 1:扫码模式 2:本地补丁模式
  23. code: 补丁加载状态码, 详情查看PatchStatusCode类说明
  24. info: 补丁加载详细说明, 详情查看PatchStatusCode类说明
  25. handlePatchVersion: 当前处理的补丁版本号, 0:无 -1:本地补丁 其它:后台补丁
  26. 这里列举几个常见的code码说明, 详情查看SDK中PatchStatus类的代码,其中有具体说明
  27. code: 1 补丁加载成功
  28. code: 6 服务端没有最新可用的补丁
  29. code: 11 RSASECRET错误,官网中的密钥是否正确请检查
  30. code: 12 当前应用已经存在一个旧补丁, 应用重启尝试加载新补丁
  31. code: 13 补丁加载失败, 导致的原因很多种, 比如UnsatisfiedLinkError等异常, 此时应该严格检查logcat异常日志
  32. code: 16 APPSECRET错误,官网中的密钥是否正确请检查
  33. code: 18 一键清除补丁
  34. code: 19 连续两次queryAndLoadNewPatch()方法调用不能短于3s
1.3.2 接口说明
1.3.2.1 initialize方法initialize(): <必选>该方法主要做些必要的初始化工作以及如果本地有补丁的话会加载补丁, 但不会自动请求补丁。因此需要自行调用queryAndLoadNewPatch方法拉取补丁。这个方法调用需要尽可能的早, 推荐在Application的onCreate方法中调用, initialize()方法调用之前你需要先调用如下几个方法, 方法调用说明如下:setContext(this): <必选> Application上下文contextsetAppVersion(appVersion): <必选> 应用的版本号setAesKey(aesKey): <可选> 用户自定义aes秘钥, 会对补丁包采用对称加密。这个参数值必须是16位数字或字母的组合,是和补丁工具设置里面AES Key保持完全一致, 补丁才能正确被解密进而加载。此时平台无感知这个秘钥, 所以不用担心百川平台会利用你们的补丁做一些非法的事情。setEnableDebug(true/false): <可选> 默认为false, 是否调试模式, 调试模式下会输出日志以及不进行补丁签名校验. 线下调试此参数可以设置为true, 查看日志过滤TAG:Sophix, 同时强制不对补丁进行签名校验, 所有就算补丁未签名或者签名失败也发现可以加载成功. 但是正式发布该参数必须为false, false会对补丁做签名校验, 否则就可能存在安全漏洞风险setPatchLoadStatusStub(new PatchLoadStatusListener()): <可选> 设置patch加载状态监听器, 该方法参数需要实现PatchLoadStatusListener接口, 接口说明见1.3.2.2说明setUnsupportedModel(modelName, sdkVersionInt):<可选> 把不支持的设备加入黑名单,加入后不会进行热修复。modelName为该机型上Build.MODEL的值,这个值也可以通过adb shell getprop | grep ro.product.model取得。sdkVersionInt就是该机型的Android版本,也就是Build.VERSION.SDK_INT,若设为0,则对应该机型所有安卓版本。1.3.2.2 queryAndLoadNewPatch方法该方法主要用于查询服务器是否有新的可用补丁. SDK内部限制连续两次queryAndLoadNewPatch()方法调用不能短于3s, 否则的话就会报code:19的错误码. 如果查询到可用的话, 首先下载补丁到本地, 然后应用原本没有补丁, 那么如果当前应用的补丁是热补丁, 那么会立刻加载(不管是冷补丁还是热补丁). 如果当前应用的补丁是冷补丁, 那么需要重启生效.应用已经存在一个补丁, 首先会把之前的补丁文件删除, 然后不立刻加载, 而是等待下次应用重启再加载该补丁补丁在后台发布之后, 并不会主动下行推送到客户端, 需要手动调用queryAndLoadNewPatch方法查询后台补丁是否可用.只会下载补丁版本号比当前应用存在的补丁版本号高的补丁, 比如当前应用已经下载了补丁版本号为5的补丁, 那么只有后台发布的补丁版本号>5才会重新下载.同时1.4.0以上版本服务后台上线了“一键清除”补丁的功能, 所以如果后台点击了“一键清除”那么这个方法将会返回code:18的状态码. 此时本地补丁将会被强制清除, 同时不清除本地补丁版本号
1.3.2.3 cleanPatches()方法清空本地补丁
1.3.2.4 PatchLoadStatusListener接口该接口需要自行实现并传入initialize方法中, 补丁加载状态会回调给该接口, 参数说明如下:mode: 补丁模式, 0:正常请求模式 1:扫码模式 2:本地补丁模式code: 补丁加载状态码, 详情查看PatchStatusCode类说明info: 补丁加载详细说明, 详情查看PatchStatusCode类说明handlePatchVersion: 当前处理的补丁版本号, 0:无 -1:本地补丁 其它:后台补丁这里列举几个常见的code码说明, 详情查看SDK中PatchStatus类的代码,其中有具体说明code: 1 补丁加载成功code: 6 服务端没有最新可用的补丁code: 11 RSASECRET错误,官网中的密钥是否正确请检查code: 12 当前应用已经存在一个旧补丁, 应用重启尝试加载新补丁code: 13 补丁加载失败, 导致的原因很多种, 比如UnsatisfiedLinkError等异常, 此时应该严格检查logcat异常日志code: 16 APPSECRET错误,官网中的密钥是否正确请检查code: 18 一键清除补丁code: 19 连续两次queryAndLoadNewPatch()方法调用不能短于3s

4. 生成补丁
patch补丁包生成需要使用到打补丁工具SophixPatchTool, 如还未下载打包工具,请前往文档SDK下载&版本更新记录下载Android打包工具。
打开这个工具

  • 旧包:<必填> 选择基线包路径(有问题的APK)
  • 新包:<必填> 选择新包路径(修复过该问题APK)
  • 日志:打开日志输出窗口。
  • 高级:展开高级选项,见2.2.1。
  • 设置:配置其他信息。
  • GO!:开始生成补丁!

按照说明先选择旧版本(有Bug)的apk包,和新版本的apk包,接下来我来演示一个Demo
【下面这两个Demo的app我就把有bug的叫做bapp 】
1>首先看bapp的源码【篇幅有限,只看有bug的部分】

[java] view plain copy print ?
  1. /**
  2. * 监听返回键
  3. */
  4. //    @Override
  5. //    public void onBackPressed() {
  6. //        if(drawerLayout.isDrawerOpen(nv)){
  7. //            drawerLayout.closeDrawers();
  8. //            return;
  9. //        }
  10. //        if (System.currentTimeMillis() - newTime > 2000) {
  11. //            newTime = System.currentTimeMillis();
  12. //            Snackbar snackbar = Snackbar.make(pager, "再按一次返回键退出程序", Snackbar.LENGTH_SHORT);
  13. //            snackbar.getView().setBackgroundColor(getResources().getColor(R.color.colorPrimary));
  14. //            snackbar.show();
  15. //        } else {
  16. //            finish();
  17. //        }
  18. //    }
    /*** 监听返回键*/
//    @Override
//    public void onBackPressed() {
//        if(drawerLayout.isDrawerOpen(nv)){
//            drawerLayout.closeDrawers();
//            return;
//        }
//        if (System.currentTimeMillis() - newTime > 2000) {
//            newTime = System.currentTimeMillis();
//            Snackbar snackbar = Snackbar.make(pager, "再按一次返回键退出程序", Snackbar.LENGTH_SHORT);
//            snackbar.getView().setBackgroundColor(getResources().getColor(R.color.colorPrimary));
//            snackbar.show();
//        } else {
//            finish();
//        }
//    }

很简单,在MainActivity中,将返回键退出程序的监听给注释掉,也就是,bapp点击返回键会没有任何效果
2>然后app的源码,就不赘述了,很简单,就是取消bapp的注释,让程序重新可以监听返回键退出
下面我们来编译baap和app
【我们的目的是通过SophixPatchTool将这两个app对比,生成一个补丁,在不推送升级app的情况下修复bapp的不能退出程序的bug】

使用SophixPatchTool,按照提示选择bapp和app,点击Go,生成一个baichuan-hotfix-patch.jar,然后将这个jar文件
移动到手机的根目录,然后打开我们上面下载的Sophix调试工具(一个安卓端的apk),接下来请看高端操作
上gif:::
【讲解下这个图片,首先这个Demo是bapp,不具备点击返回键退出功能,在Sophix调试工具上输入bapp包名,输入jar补丁绝对路径,点击应用补丁,重启bapp后,bapp就修复了不能点击返回键退出的bug,理论上不用重启】

差不多也就这些了,有什么问题可以留言交流

阿里百川HotFix2.0热修复初体验相关推荐

  1. 阿里百川4.0授权后,渠道页面授权免帐密实现方法

    写这个内容,只是自己查了很多资料仍然无法解决.后续自己看到阿里百川的解释以后.才明白 故分享一下自己的经验. 百川登录就不说了. 更多的说明一下.调用code方式进行免帐密登录的方式.以下链接通过we ...

  2. Tomcat8.0之源代码初体验

    Tomcat8.0本身是一个软件.是用Java编写的必须依赖java虚拟机才能跑.今天对它的源代码稍微体验了一把.以下是源代码的入口.有兴趣的朋友可以仔细研究! 转载于:https://www.cnb ...

  3. 阿里出品的ETL工具dataX初体验

    我的毕设选择了大数据方向的题目.大数据的第一步就是要拿到足够的数据源.现实情况中我们需要的数据源分布在不同的业务系统中,而这些系统往往是异构的,而且我们的分析过程不能影响原有业务系统的运行.为了把不同 ...

  4. 阿里云ECS和轻量云服务器区别比较,阿里云轻量云服务器初体验

    一.阿里云云服务器ECS和阿里云轻量云服务器有哪些区别 先介绍下阿里云云服务器ECS和阿里云轻量云服务器有哪些区别? 阿里云云服务器官方文档:https://help.aliyun.com/docum ...

  5. php版本最低要求:5.4_Zabbix 5.0.0beta1版本初体验

    Zabbix 5.0.0 beta1 升级要求 PHP版本 PHP版本已从最低的5.4.0 升级到 7.2.0 数据库版本 MySQL 5.5.62 MariaDB 10.0.37 PostgreSQ ...

  6. 阿里百川4.0接口源码记录

    SDK下载地址:https://baichuan.taobao.com/docs/doc.htm?spm=a3c0d.7629140.0.0.12a5be48y3x2sa&treeId=129 ...

  7. DNN3.0 beta 本地化初体验

    几天没上今天早上突然收到一个DNN站点的mail,说beta版可以下载了. 马上到博客园看到师域都已经开始汉化差不多完成了呵呵. 下下来,装上,马上看本地化的功能,呵呵,最关心的当然是这个了,唉唉我的 ...

  8. 字符设备驱动基础篇0——驱动开发初体验

    以下内容源于朱有鹏嵌入式课程的学习,如有侵权,请告知删除. 参考资料:http://www.cnblogs.com/biaohc/p/6575074.html 1.驱动开发的准备工作 (1)内核源码树 ...

  9. 【云原生】Docker 进阶 -- 阿里云服务器安装Docker Compose与初体验

最新文章

  1. Java获取异常堆栈信息
  2. 应用计算机测定线性电阻伏安特性实验器材,线性电阻与非线性电阻伏安特性实验的Origin处理...
  3. Scanner类、Random类、ArrayList类
  4. RAC+单实例DATAGUARD 配置
  5. matlab基础与实例教程,MATLAB基础与实例教程
  6. 剑指offer:26-30记录
  7. Ampere 收购 OnSpecta,加速对云原生应用程序的 AI 推理
  8. 互联网晚报 | 3月22日 星期二 |​ ​工作人员标注mu5735残骸并展开调查;万门大学疑似解散VIP群跑路...
  9. 通过yum下载rpm包
  10. linux固定dns怎么设置,Linux之如何进行固定IP、DNS等设置
  11. linux检查哪些进程消耗io,Linux 不同方法查看进程消耗CPU IO 等
  12. 绝对值编码器:从调研到开发
  13. 《Web应用基础》课程结业报告
  14. 微信小程序怎么做【零基础教程附源码】
  15. 多视图几何三维重建实战系列之COLMAP
  16. 仿百度文库解决方案(三)- 利用JODConverter把文档转换成pdf格式
  17. ValueError: Duplicate plugins for name projector
  18. 数字化转型:信息系统的生命周期(一)
  19. 4. 自动封IP和解IP
  20. 注销使用苹果登录的账号

热门文章

  1. ld 和 ld.so命令
  2. network status:no carrier
  3. 学习制作平衡小车:(四)PID学习、位置PID参数整定以及匿名上位机显示
  4. 英伟达收购,ARM也要变美国公司,国产芯出路几何?
  5. GPS/GPRS定位
  6. 最新30个优秀的旅行网站设计作品欣赏
  7. 得实Dascom AR-430K 打印机驱动
  8. Windows XP 启动过程详解
  9. php中函数trim,PHP trim()函数
  10. c语言局域网oicq程序,局域网聊天的程序(C++版)