Android 热修复

2017-11-18  本文已影响0人  feibix33

一、关于热修复定义

1.1、定义

动态的修复或者 更新app 的行为,也叫热更新、动态修复技术

1.2几种容易混淆概念对比

二、主流热修复文案对比

目前市面上主要的热修复方案比较多,比较出名的有 阿里的Andfix(升级版本Hotfix ,QZone 超级补丁方案、美团的Robust、微信Tinker

文案对比 Tinker QZone Robust Andfix Sophix
类替换 支持 支持 不支持 不支持 支持
方法替换 支持 支持 支持 支持部分 支持
so替换 支持 不支持 不支持 不支持 支持
资源替换 支持 支持 不支持 不支持 支持
四大组件 不支持 不支持 不支持 不支持 不支持
Dex修复 冷启动 冷启动 冷启动 热启动 冷热启动
性能损耗 一般 较大 较小 较小 较小
补丁大小 较小 较大 较小 较小 较小
接入成本 复杂 一般 一般 一般 较小
性能损耗 一般 较高 一般 一般 一般
成功率 较高 较高 较高 一般 较高
服务端支持 支持(收费 ) 不支持 不支持 不支持 支持(收费)

三、热修复原理

热修复主要解决类的替换、资源的替换、so的替换。在Android中有四个类加载器,分别为PathClassLoader、DexClassLoader、BaseDexClassLoader 和BootClassLoader。PathClassLoader 只能加载已经安装到手机上的 dex文件,而DexClassLoader可以加载 未安装的apk 、jar、dex文件,它们有共同的父类BaseDexClassLoader ,核心代码都是在BaseDexClassLoader中实现的; BootClassLoader加载的是framework 层的类库、资源
热修复技术用到的主要 PathClassLoader 和DexClassLoader 两个类加载器

/*
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}
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 的目的,实时生效

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.

4.2,流程及原理

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库以及资源,让应用能够在不需要重新安装的情况下实现更新。需要重新启动后生效

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"
                    }

                }
            }
        }
    }

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 ,遇到的问题及容易出错的点

六、分支管理及发布

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,当我们加入了热修复后,我们应该如何进行版本的管理呢?
请参照如下连接

参考:

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/

上一篇 下一篇

猜你喜欢

热点阅读