Android 热修复

一、关于热修复定义

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 补丁文件结构如下:


    patch文件结构.png

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分支管理
  • 版本发布


    版本发布.png

    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/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容