一、关于热修复定义
1.1、定义
动态的修复或者 更新app 的行为,也叫热更新、动态修复技术
1.2几种容易混淆概念对比
- 组件化是一种编程思想,它将一个app分成多个模块,每个模块对应一个module(lib),它们之前相互依赖,合成一个整体 apk ;
- 插件化可以认为是一种技术解决方案,将一个app 拆分成多个模块,每个模块可对应一个apk,可独立运行,也可将宿主apk 和插件apk联合打包。
- 热修复和插件化都是动态加载技术的应用
- 组件化是为了代码的高复用性而出现的,插件化是为了解决apk 包体积,热修复是为了解决线上bug或者小功能更新出现的
二、主流热修复文案对比
目前市面上主要的热修复方案比较多,比较出名的有 阿里的Andfix(升级版本Hotfix ,QZone 超级补丁方案、美团的Robust、微信Tinker
文案对比 | Tinker | QZone | Robust | Andfix | Sophix |
---|---|---|---|---|---|
类替换 | 支持 | 支持 | 不支持 | 不支持 | 支持 |
方法替换 | 支持 | 支持 | 支持 | 支持部分 | 支持 |
so替换 | 支持 | 不支持 | 不支持 | 不支持 | 支持 |
资源替换 | 支持 | 支持 | 不支持 | 不支持 | 支持 |
四大组件 | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 |
Dex修复 | 冷启动 | 冷启动 | 冷启动 | 热启动 | 冷热启动 |
性能损耗 | 一般 | 较大 | 较小 | 较小 | 较小 |
补丁大小 | 较小 | 较大 | 较小 | 较小 | 较小 |
接入成本 | 复杂 | 一般 | 一般 | 一般 | 较小 |
性能损耗 | 一般 | 较高 | 一般 | 一般 | 一般 |
成功率 | 较高 | 较高 | 较高 | 一般 | 较高 |
服务端支持 | 支持(收费 ) | 不支持 | 不支持 | 不支持 | 支持(收费) |
- AndFix作为native解决方案,首先面临的是稳定性与兼容性问题,它无法实现类和资源替换,部分方法无法替换
- Robust兼容性与成功率较高,但是它与AndFix一样,无法新增变量、类以及资源 ,只能用做的bugFix方案;
- Qzone方案可以做到发布产品功能,但是它主要问题是插桩带来Dalvik的性能问题,以及为了解决Art下内存地址问题而导致补丁包急速增大的。
- Tinker热补丁方案不仅支持类、So以及资源的替换,它还是2.X-8.X(1.9.0以上支持8.X)的全平台支持。利用Tinker我们不仅可以用做bugfix,甚至可以替代功能的发布,但是接入成本比较大,tinker提供了服务端的支持,需要收费;
- Sophix方案是对 阿里百川 Hotfix 1.x文案的升级衍进,它支持类、资源及SO 的替换,傻瓜式接入,安全方面加入了加密传输及签名校验,提供服务端控制等,不过此热修复方案需要收费
三、热修复原理
热修复主要解决类的替换、资源的替换、so的替换。在Android中有四个类加载器,分别为PathClassLoader、DexClassLoader、BaseDexClassLoader 和BootClassLoader。PathClassLoader 只能加载已经安装到手机上的 dex文件,而DexClassLoader可以加载 未安装的apk 、jar、dex文件,它们有共同的父类BaseDexClassLoader ,核心代码都是在BaseDexClassLoader中实现的; BootClassLoader加载的是framework 层的类库、资源
热修复技术用到的主要 PathClassLoader 和DexClassLoader 两个类加载器
- PathClassLoader ,通过Context getClassLoader() 获取,源码如下:
/*
2 * Copyright (C) 2007 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package dalvik.system;
18
19/**
20 * Provides a simple {@link ClassLoader} implementation that operates on a list
21 * of files and directories in the local file system, but does not attempt to
22 * load classes from the network. Android uses this class for its system class
23 * loader and for its application class loader(s).
24 */
25public class PathClassLoader extends BaseDexClassLoader {
26 /**
27 * Creates a {@code PathClassLoader} that operates on a given list of files
28 * and directories. This method is equivalent to calling
29 * {@link #PathClassLoader(String, String, ClassLoader)} with a
30 * {@code null} value for the second argument (see description there).
31 *
32 * @param dexPath the list of jar/apk files containing classes and
33 * resources, delimited by {@code File.pathSeparator}, which
34 * defaults to {@code ":"} on Android
35 * @param parent the parent class loader
36 */
37 public PathClassLoader(String dexPath, ClassLoader parent) {
38 super(dexPath, null, null, parent);
39 }
40
41 /**
42 * Creates a {@code PathClassLoader} that operates on two given
43 * lists of files and directories. The entries of the first list
44 * should be one of the following:
45 *
46 * <ul>
47 * <li>JAR/ZIP/APK files, possibly containing a "classes.dex" file as
48 * well as arbitrary resources.
49 * <li>Raw ".dex" files (not inside a zip file).
50 * </ul>
51 *
52 * The entries of the second list should be directories containing
53 * native library files.
54 *
55 * @param dexPath the list of jar/apk files containing classes and
56 * resources, delimited by {@code File.pathSeparator}, which
57 * defaults to {@code ":"} on Android
58 * @param librarySearchPath the list of directories containing native
59 * libraries, delimited by {@code File.pathSeparator}; may be
60 * {@code null}
61 * @param parent the parent class loader
62 */
63 public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
64 super(dexPath, null, librarySearchPath, parent);
65 }
66}
- DexClassLoader ,通过构造函数 new DexClassLoader()获取。
1/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package dalvik.system;
18
19import java.io.File;
20
21/**
22 * A class loader that loads classes from {@code .jar} and {@code .apk} files
23 * containing a {@code classes.dex} entry. This can be used to execute code not
24 * installed as part of an application.
25 *
26 * <p>This class loader requires an application-private, writable directory to
27 * cache optimized classes. Use {@code Context.getCodeCacheDir()} to create
28 * such a directory: <pre> {@code
29 * File dexOutputDir = context.getCodeCacheDir();
30 * }</pre>
31 *
32 * <p><strong>Do not cache optimized classes on external storage.</strong>
33 * External storage does not provide access controls necessary to protect your
34 * application from code injection attacks.
35 */
36public class DexClassLoader extends BaseDexClassLoader {
37 /**
38 * Creates a {@code DexClassLoader} that finds interpreted and native
39 * code. Interpreted classes are found in a set of DEX files contained
40 * in Jar or APK files.
41 *
42 * <p>The path lists are separated using the character specified by the
43 * {@code path.separator} system property, which defaults to {@code :}.
44 *
45 * @param dexPath the list of jar/apk files containing classes and
46 * resources, delimited by {@code File.pathSeparator}, which
47 * defaults to {@code ":"} on Android
48 * @param optimizedDirectory directory where optimized dex files
49 * should be written; must not be {@code null}
50 * @param librarySearchPath the list of directories containing native
51 * libraries, delimited by {@code File.pathSeparator}; may be
52 * {@code null}
53 * @param parent the parent class loader
54 */
55 public DexClassLoader(String dexPath, String optimizedDirectory,
56 String librarySearchPath, ClassLoader parent) {
57 super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
58 }
59}
60
四、阿里Andfix 方案
4.1,Andfix基本介绍
Andfix 是阿里最早的热修复方案,它作用于方法级别,直接替换类中的方法达到修复bug 的目的,实时生效
- 关于如何使用,请参照github andfix介绍
- patch 文件生成(只支持命令行生成方式)apkpatch,patch文件以“.apatch”结尾
apkpatch -f <new> -t <old> -o <output> -k <keystore> -p <> -a <alias> -e <>
-a,--alias <alias> keystore entry alias.
-e,--epassword <> keystore entry password.
-f,--from <loc> new Apk file path.
-k,--keystore <loc> keystore path.
-n,--name <name> patch name.
-o,--out <dir> output dir.
-p,--kpassword <> keystore password.
-t,--to <loc> old Apk file path.
- 支持程度:仅支持方法级别修改,系统版本支持2.3-7.0,架构支持arm/x86,支持Dalvik、ART运行模式
4.2,流程及原理
- 为什么andfix 可以实现即时生效呢?
app 运行到一半的时候,所有需要发生变更的类已经加载过了,android 中无法对一个分类进行卸载,而腾讯、微信都是让ClassLoader重新加载一个新类,如果不重启,原先的类还在虚拟机中,就无法加载新类了,因此只有在下次重启的时候抢先加载补丁中的新类,从而达到热修复的目的。
而andfix 中采用的方法是 在已经加载过的类中 直接在native 层进行方法替换,即在原先的类中进行替换 ,所以不需要重启就能生效 -
一般流程
-
apkpatch将两个apk做一次对比,然后找出不同的部分。可以看到生成的apatch了文件,后缀改成zip再解压开,里面有一个dex文件。通过ja-gui.exe查看一下源码,里面就是被修复的代码所在的类文件,这些更改过的类都加上了一个_CF的后缀,并且变动的方法都被加上了一个叫@MethodReplace的annotation,通过clazz和method指定了需要替换的方法。
然后客户端sdk得到补丁文件后就会根据annotation来寻找需要替换的方法。最后由JNI层完成方法的替换。
patch 补丁文件结构如下:
4.3核心代码
/**
* fix
*
* @param file
* patch file
* @param classLoader
* classloader of class that will be fixed
* @param classes
* classes will be fixed
*/
public synchronized void fix(File file, ClassLoader classLoader, List<String> classes) {
if (!mSupport) {
return;
}
if (!mSecurityChecker.verifyApk(file)) { // security check fail
return;
}
try {
File optfile = new File(mOptDir, file.getName());
boolean saveFingerprint = true;
if (optfile.exists()) {
// need to verify fingerprint when the optimize file exist,
// prevent someone attack on jailbreak device with
// Vulnerability-Parasyte.
// btw:exaggerated android Vulnerability-Parasyte
// http://secauo.com/Exaggerated-Android-Vulnerability-Parasyte.html
if (mSecurityChecker.verifyOpt(optfile)) {
saveFingerprint = false;
} else if (!optfile.delete()) {
return;
}
}
final DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),
optfile.getAbsolutePath(), Context.MODE_PRIVATE);
if (saveFingerprint) {
mSecurityChecker.saveOptSig(optfile);
}
ClassLoader patchClassLoader = new ClassLoader(classLoader) {
@Override
protected Class<?> findClass(String className)
throws ClassNotFoundException {
Class<?> clazz = dexFile.loadClass(className, this);
if (clazz == null
&& className.startsWith("com.alipay.euler.andfix")) {
return Class.forName(className);// annotation’s class
// not found
}
if (clazz == null) {
throw new ClassNotFoundException(className);
}
return clazz;
}
};
Enumeration<String> entrys = dexFile.entries();
Class<?> clazz = null;
while (entrys.hasMoreElements()) {
String entry = entrys.nextElement();
if (classes != null && !classes.contains(entry)) {
continue;// skip, not need fix
}
clazz = dexFile.loadClass(entry, patchClassLoader);
if (clazz != null) {
fixClass(clazz, classLoader);
}
}
} catch (IOException e) {
Log.e(TAG, "pacth", e);
}
}
/**
* replace method
*
* @param classLoader classloader
* @param clz class
* @param meth name of target method
* @param method source method
*/
private void replaceMethod(ClassLoader classLoader, String clz,
String meth, Method method) {
try {
String key = clz + "@" + classLoader.toString();
Class<?> clazz = mFixedClass.get(key);
if (clazz == null) {// class not load
Class<?> clzz = classLoader.loadClass(clz);
// initialize target class
clazz = AndFix.initTargetClass(clzz);
}
if (clazz != null) {// initialize class OK
mFixedClass.put(key, clazz);
Method src = clazz.getDeclaredMethod(meth,
method.getParameterTypes());
AndFix.addReplaceMethod(src, method);
}
} catch (Exception e) {
Log.e(TAG, "replaceMethod", e);
}
}
4.4遇到的问题:
a,测试时getApplicationContext 或者用父类的mContext时不起作用,换成当前类的引用就正常
b,混淆生成文件失败处理,发布的版本需要记住混淆文件配置
-printmapping mapping.txt
当发布的版本发生bug ,打包时需要用到上面记住的混淆配置文件,将上线版本生成的mappping.txt文件拷贝到主module app 目录下,proguard-rules.pro中删除printmapping 命令,同时添加如下命令
-applymapping mapping.txt
c,需要修复类结构发生变化,例如新增或者减少方法数,热修复失败
五、微信Tinker 方案
5.1,tinker 基本介绍
Tinker是微信官方的Android热补丁解决方案,它支持动态下发代码、So库以及资源,让应用能够在不需要重新安装的情况下实现更新。需要重新启动后生效
关于如何使用,请参照github tinker介绍
patch 文件,patch文件以“.apk”结尾,有两种集成方式
1.命令行工具生成patch文件,tinker-patch-cli.jar,此jar从官方提供的demo 中 生成,命令行参数如下:
java -jar tinker-patch-cli.jar -old old.apk -new new.apk -config tinker_config.xml -out output_path
此种方式与gradle不同的是,在编译时我们需要将TINKER_ID插入到AndroidManifest.xml中
<meta-data android:name="TINKER_ID" android:value="tinker_id_b168b32"/>
2,通过gradle生成,配置比较繁琐,需要仔细,否则导致生成patch文件失败或者生成的patch文件无法动态修复bug ,主要配置如下(多渠道):
def bakPath = file("${buildDir}/bakApk/")
ext {
//for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
tinkerEnabled = true
// 在运行过程中,我们需要验证基准apk包的tinkerId是否等于补丁包的tinkerId。
// 这个是决定补丁包能运行在哪些基准包上面,一般来说我们可以使用git版本号、versionName等等。
tinkerID = "1.0.0"
//for normal build
//old apk file to build patch apk
tinkerOldApkPath = "${bakPath}/app-1109-20-48-18"
//proguard mapping file to build patch apk
tinkerApplyMappingPath = "${bakPath}/app-1109-20-48-18"
//resource R.txt to build patch apk, must input if there is resource changed
tinkerApplyResourcePath = "${bakPath}/app-1109-20-48-18"
//only use for build all flavor, if not, just ignore this field
tinkerBuildFlavorDirectory = "${bakPath}/app-1109-20-48-18"
}
/**
* 是否使用tinker
* @return
*/
def buildWithTinker() {
return ext.tinkerEnabled
}
/**
* 得到需要 patch 的apk,用于生成patch 文件
* @return
*/
def getOldApkPath() {
return ext.tinkerOldApkPath
}
/**
* 得到需要 patch 的apk对应 mapping文件,用于生成patch 文件
* @return
*/
def getApplyMappingPath() {
return ext.tinkerApplyMappingPath
}
/**
* 得到需要 patch 的apk对应 资源文件(R),用于生成patch 文件
* @return
*/
def getApplyResourceMappingPath() {
return ext.tinkerApplyResourcePath;
}
/**
* 获取 tinker ID
* @return
*/
def getTinkerIdValue() {
return ext.tinkerID
}
/**
* 获取多渠道打包的目录
* @return
*/
def getTinkerBuildFlavorDirectory() {
return ext.tinkerBuildFlavorDirectory;
}
if (buildWithTinker()) {
//启动tinker
apply plugin: 'com.tencent.tinker.patch'
tinkerPatch {
/**
* necessary,default 'null'
* the old apk path, use to diff with the new apk to build
* add apk from the build/bakApk
*/
oldApk = getOldApkPath()
/**
* optional,default 'false'
* there are some cases we may get some warnings
* if ignoreWarning is true, we would just assert the patch process
* case 1: minSdkVersion is below 14, but you are using dexMode with raw.
* it must be crash when load.
* case 2: newly added Android Component in AndroidManifest.xml,
* it must be crash when load.
* case 3: loader classes in dex.loader{} are not keep in the main dex,
* it must be let tinker not work.
* case 4: loader classes in dex.loader{} changes,
* loader classes is ues to load patch dex. it is useless to change them.
* it won't crash, but these changes can't effect. you may ignore it
* case 5: resources.arsc has changed, but we don't use applyResourceMapping to build
*/
ignoreWarning = false
/**
* optional,default 'true'
* whether sign the patch file
* if not, you must do yourself. otherwise it can't check success during the patch loading
* we will use the sign config with your build type
*/
useSign = true
/**
* optional,default 'true'
* whether use tinker to build
*/
tinkerEnable = buildWithTinker()
/**
* Warning, applyMapping will affect the normal android build!
*/
buildConfig {
/**
* optional,default 'null'
* if we use tinkerPatch to build the patch apk, you'd better to apply the old
* apk mapping file if minifyEnabled is enable!
* Warning:
* you must be careful that it will affect the normal assemble build!
*/
applyMapping = getApplyMappingPath()
/**
* optional,default 'null'
* It is nice to keep the resource id from R.txt file to reduce java changes
*/
applyResourceMapping = getApplyResourceMappingPath()
/**
* necessary,default 'null'
* because we don't want to check the base apk with md5 in the runtime(it is slow)
* tinkerId is use to identify the unique base apk when the patch is tried to apply.
* we can use git rev, svn rev or simply versionCode.
* we will gen the tinkerId in your manifest automatic
*/
tinkerId = getTinkerIdValue()
/**
* if keepDexApply is true, class in which dex refer to the old apk.
* open this can reduce the dex diff file size.
*/
keepDexApply = false
/**
* optional, default 'false'
* Whether tinker should treat the base apk as the one being protected by app
* protection tools.
* If this attribute is true, the generated patch package will contain a
* dex including all changed classes instead of any dexdiff patch-info files.
*/
isProtectedApp = false
/**
* optional, default 'false'
* Whether tinker should support component hotplug (add new component dynamically).
* If this attribute is true, the component added in new apk will be available after
* patch is successfully loaded. Otherwise an error would be announced when generating patch
* on compile-time.
*
* <b>Notice that currently this feature is incubating and only support NON-EXPORTED Activity</b>
*/
supportHotplugComponent = false
}
/**
* dex 相关配置,对dex修改
*/
dex {
/**
* optional,default 'jar'
* only can be 'raw' or 'jar'. for raw, we would keep its original format
* for jar, we would repack dexes with zip format.
* if you want to support below 14, you must use jar
* or you want to save rom or check quicker, you can use raw mode also
*/
dexMode = "jar"
/**
* necessary,default '[]'
* what dexes in apk are expected to deal with tinkerPatch
* it support * or ? pattern.
*/
//指定需要处理的dex 文件目录
pattern = ["classes*.dex", "assets/secondary-dex-?.jar"]
/**
* necessary,default '[]'
* Warning, it is very very important, loader classes can't change with patch.
* thus, they will be removed from patch dexes.
* you must put the following class into main dex.
* Simply, you should add your own application {@code tinker.sample.android.SampleApplication}
* own tinkerLoader, and the classes you use in them
*
*/
loader = [
//use sample, let BaseBuildInfo unchangeable with tinker
//指定加载patch 文件时需要用到的类,就是我们通过注解生成的application
"com.example.corab.hotfixdemo.app.HotFixApplication"
]
}
/**
* 工程中lib 指定,可以对工程中的jar和so进行替换
*/
lib {
/**
* optional,default '[]'
* what library in apk are expected to deal with tinkerPatch
* it support * or ? pattern.
* for library in assets, we would just recover them in the patch directory
* you can get them in TinkerLoadResult with Tinker
*/
pattern = ["lib/*/*.so"]
}
/**
* 指定可以修改的资源文件
*/
res {
/**
* optional,default '[]'
* what resource in apk are expected to deal with tinkerPatch
* it support * or ? pattern.
* you must include all your resources in apk here,
* otherwise, they won't repack in the new apk resources.
*/
pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
/**
* optional,default '[]'
* the resource file exclude patterns, ignore add, delete or modify resource change
* it support * or ? pattern.
* Warning, we can only use for files no relative with resources.arsc
*/
//指定不受影响的资源路径
ignoreChange = ["assets/sample_meta.txt"]
/**
* default 100kb
* for modify resource, if it is larger than 'largeModSize'
* we would like to use bsdiff algorithm to reduce patch file size
*/
// 资源修改大小默认值,如果大于largeModSize,我们将使用bsdiff算法。
// 这可以降低补丁包的大小,但是会增加合成时的复杂度。默认大小为100kb
largeModSize = 100
}
packageConfig {
/**
* optional,default 'TINKER_ID, TINKER_ID_VALUE' 'NEW_TINKER_ID, NEW_TINKER_ID_VALUE'
* package meta file gen. path is assets/package_meta.txt in patch file
* you can use securityCheck.getPackageProperties() in your ownPackageCheck method
* or TinkerLoadResult.getPackageConfigByName
* we will get the TINKER_ID from the old apk manifest for you automatic,
* other config files (such as patchMessage below)is not necessary
*/
configField("patchMessage", "tinker is sample to use")
/**
* just a sample case, you can use such as sdkVersion, brand, channel...
* you can parse it in the SamplePatchListener.
* Then you can use patch conditional!
*/
configField("platform", "all")
/**
* patch version via packageConfig
*/
configField("patchVersion", "1.0")
}
//or you can add config filed outside, or get meta value from old apk
//project.tinkerPatch.packageConfig.configField("test1", project.tinkerPatch.packageConfig.getMetaDataFromOldApk("Test"))
//project.tinkerPatch.packageConfig.configField("test2", "sample")
// /**
// * if you don't use zipArtifact or path, we just use 7za to try
// */
// sevenZip {
// /**
// * optional,default '7za'
// * the 7zip artifact path, it will use the right 7za with your platform
// */
// zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
// /**
// * optional,default '7za'
// * you can specify the 7za path yourself, it will overwrite the zipArtifact value
// */
//// path = "/usr/local/bin/7za"
// }
}
List<String> flavors = new ArrayList<>();
project.android.productFlavors.each { flavor ->
flavors.add(flavor.name)
}
boolean hasFlavors = flavors.size() > 0
def date = new Date().format("MMdd-HH-mm-ss")
/**
* bak apk and mapping
*/
android.applicationVariants.all { variant ->
/**
* task type, you want to bak
*/
def taskName = variant.name
tasks.all {
if ("assemble${taskName.capitalize()}".equalsIgnoreCase(it.name)) {
it.doLast {
copy {
def fileNamePrefix = "${project.name}-${variant.baseName}"
def newFileNamePrefix = hasFlavors ? "${fileNamePrefix}" : "${fileNamePrefix}-${date}"
def destPath = hasFlavors ? file("${bakPath}/${project.name}-${date}/${variant.flavorName}") : bakPath
from variant.outputs.first().outputFile
into destPath
rename { String fileName ->
fileName.replace("${fileNamePrefix}.apk", "${newFileNamePrefix}.apk")
}
from "${buildDir}/outputs/mapping/${variant.dirName}/mapping.txt"
into destPath
rename { String fileName ->
fileName.replace("mapping.txt", "${newFileNamePrefix}-mapping.txt")
}
from "${buildDir}/intermediates/symbols/${variant.dirName}/R.txt"
into destPath
rename { String fileName ->
fileName.replace("R.txt", "${newFileNamePrefix}-R.txt")
}
}
}
}
}
}
project.afterEvaluate {
//sample use for build all flavor for one time
if (hasFlavors) {
task(tinkerPatchAllFlavorRelease) {
group = 'tinker'
def originOldPath = getTinkerBuildFlavorDirectory()
for (String flavor : flavors) {
def tinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Release")
dependsOn tinkerTask
def preAssembleTask = tasks.getByName("process${flavor.capitalize()}ReleaseManifest")
preAssembleTask.doFirst {
String flavorName = preAssembleTask.name.substring(7, 8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 15)
project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${flavorName}_hotfix.apk"
project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release-mapping.txt"
project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release-R.txt"
}
}
}
task(tinkerPatchAllFlavorDebug) {
group = 'tinker'
def originOldPath = getTinkerBuildFlavorDirectory()
for (String flavor : flavors) {
def tinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Debug")
dependsOn tinkerTask
def preAssembleTask = tasks.getByName("process${flavor.capitalize()}DebugManifest")
preAssembleTask.doFirst {
String flavorName = preAssembleTask.name.substring(7, 8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 13)
project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug.apk"
project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug-mapping.txt"
project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug-R.txt"
}
}
}
}
}
- 支持程度
1,不支持新增四大组件,不支持清单文件的修改
2,不支持部分三星android-21机型,会抛出异常
3,由于Google Play的开发者条款限制,不建议在GP渠道动态更新代码
4,资源的替换,不支持动画、桌面icon等
5.2,流程及原理
5.3,核心代码
//we use destPatchFile instead of patchFile, because patchFile may be deleted during the patch process
if (!DexDiffPatchInternal.tryRecoverDexFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch dex failed");
return false;
}
if (!BsDiffPatchInternal.tryRecoverLibraryFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch library failed");
return false;
}
if (!ResDiffPatchInternal.tryRecoverResourceFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch resource failed");
return false;
}
以上三个if分别完成dex、library、以及资源的处理
5.4 ,遇到的问题及容易出错的点
- gradle 进行patch 打包时 参数配置容易出错
- tinker自定义行为加载patch包时上下文对象为空,待解决
- 同一个版本渠道包 功能不同需要处理不同的情况
- 几个版本之间最好只发布一个热修复版本,多了patch 不好管理
六、分支管理及发布
6.1 svn 管理及版本发布
svn 是一种集中式版本控制系统,它的用法在这里就不做过多的介绍了,相信大家都已经很熟悉了。如果我的项目引入了热修复后,我们如何对代码进行管理呢?
引入前:主干开发、分支发布
引入后:主干开发、分支发布
可以看出基本没有什么变化,最新的需求会在主干上开发,如果当某一个版本出现重大bug后,比如说我们在1.0.0版本上有个bug 需要发布一个bug的补丁包,我们会1.0.0版本的分支代码的基础上打个1.0.1的分支进行bug 的修改,修改完成生成补丁包、并将修改的代码合到主干。唯一不同的是热修复更新通过 补丁包下发到用户手机与旧包合成进行更新的,而未引入热修复则是将1.0.1整份代码打包成apk,下发到用户手机替换安装
6.2 git 管理及版本发布
git 是一种分布式的版本控制系统,它的用处大家也不陌生了,对于git,当我们加入了热修复后,我们应该如何进行版本的管理呢?
请参照如下连接
- git分支管理
-
版本发布
1.根据上图可看出,两个主版本之前最好只有一个热修复的版本,热修复的版本太多不好管理;
2.热修复的版本和正常的版本的发版流程一致,都需要经历测试和上线的步骤
3.热修复hotfix 分支bug 修复完成后需要将代码合并到master分支进行发布,同时develop作为最新最全的代码分支,也需要将hotfix分支的代码合并到develop分支上
参考:
https://github.com/alibaba/AndFix
https://github.com/Tencent/tinker
https://help.aliyun.com/document_detail/53287.html?spm=5176.2020520107.0.0.74b461e6eKdG4t
http://blog.csdn.net/qq_19711823/article/details/53199045
http://blog.jobbole.com/109466/