Android开发经验谈Android开发

移动架构02-Tinker热修复

2018-05-31  本文已影响28人  最爱的火

移动架构02-Tinker热修复

Tinker是目前开源框架中兼容性最好的热修复框架。

Tinker的Github地址:https://github.com/Tencent/tinker

一、为什么使用Tinker

1.总体比较

当前市面的热补丁方案有很多,其中比较出名的有微信的Tinker、阿里的AndFix、美团的Robust以及QZone的超级补丁方案。那么为什么使用Tinker呢?我们先来做下简单对比。

Tinker QZone AndFix Robust
类替换 yes yes no no
So替换 yes no no no
资源替换 yes yes no no
全平台支持 yes yes yes yes
即时生效 no no yes yes
性能损耗 较小 较大 较小 较小
补丁包大小 较小 较大 一般 一般
开发透明 yes yes no no
复杂度 较低 较低 复杂 复杂
gradle支持 yes no no no
Rom体积 较大 较小 较小 较小
成功率 较高 较高 一般 最高

总的来说:

  1. AndFix是native层的修复方案,优点是即时生效,缺点是只能修复方法、并且成功率不高;
  2. Robust也native层的修复方案,优点是即时生效,并且成功率很高,缺点也是只能修复方法;
  3. Qzone(非开源)是Java层的修复方案,优点是可以修复类和资源,缺点是补丁包大、性能差、不能即时生效;
  4. Tinker也是Java层的修复方案,优点是可以修复类、so和资源,补丁包小,缺点是Rom体积大、不能即时生效;

2.实现对比

热修复是基于hook技术实现的,它可以动态修改内存中的代码,但是不能修改在SD卡中的dex文件。

AndFix

AndFix采用native hook的方式,这套方案直接使用dalvik_replaceMethod替换class中方法的实现。由于它并没有整体替换class, 而field在class中的相对地址在class加载时已确定,所以AndFix无法支持新增或者删除filed的情况(通过替换initclinit只可以修改field的数值)。

QZone

QZone方案并没有开源,但在github上的HotFix采用了相同的方式。这个方案使用classloader的方式,将修复好的类生成dex文件,将这个dex文件插入到系统的dex数组的前面,当系统加载这个类时,优先从dex数组的签名查找,找到后就加载到内存中,从而实现类的替换。

但是直接使用classloader的方式,被修复的类和它的引用类不处于同一个dex中,会带来unexpected DEX problem

因为,apk在安装时,虚拟机(dexopt)会把apk中的classes.dex优化成odex文件。优化时,如果启动参数verify为true,就会执行dvmVerifyClass进行类的校验,如果一个类的直接引用类和calzz都在同一个dex中的话,那么这个类就会被打上CLASS_ISPREVERIFIED。当加载这个类时,由于它有CLASS_ISPREVERIFIED标记,会先进行dex校验,验证它的引用类和它是否处于同一个dex中,如果不处于同一个dex就会unexpected DEX problem

为了解决unexpected DEX problem,QZone使用插桩的方式,在每一个类中加入如下代码,防止他们被打上CLASS_ISPREVERIFIED:

if (ClassVerifier.PREVENT_VERIFY) {
    System.out.println(AntilazyLoad.class);//AntilazyLoad处于一个独立的dex中
}

可是,Android系统做类校验是非常有意义的,在Dalvik与Art中使用插桩式都会产生一些问题。

Tinker

Tinker在编译时通过新旧两个Dex生成差异path.dex。在运行时,将差异patch.dex重新跟原始安装包的旧Dex还原为新的Dex。这个过程可能比较耗费时间与内存,所以我们是单独放在一个后台进程:patch中。为了补丁包尽量的小,微信自研了DexDiff算法,它深度利用Dex的格式来减少差异的大小。它的粒度是Dex格式的每一项,可以充分利用原本Dex的信息,而BsDiff的粒度是文件,AndFix/QZone的粒度为class。

二、Tinker怎么使用

1.添加gradle依赖

在gradle.properties中配置tinker的版本和ID

TINKER_VERSION = 1.9.1
TINKER_ID = 100

在项目的build.gradle中,添加tinker-patch-gradle-plugin的依赖

buildscript {
    dependencies {
         //添加tinker热修复插件
        classpath "com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}"
    }
}

增加tinker的gradle文件:tinker.gradle

/*----------------------------------------配置tinker插件----------------------------------------------*/
apply plugin: 'com.tencent.tinker.patch'
//获取提交git的版本号,作为tinkerid
def gitSha() {
    try {
        String gitRev = 'git rev-parse --short HEAD'.execute(null, project.rootDir).text.trim()
        if (gitRev == null) {
            throw new GradleException("can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'")
        }
        return gitRev
    } catch (Exception e) {
        throw new GradleException("can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'")
    }
}

def bakPath = file("${buildDir}/bakApk/")

/**
 * you can use assembleRelease to build you base apk
 * use tinkerPatchRelease -POLD_APK=  -PAPPLY_MAPPING=  -PAPPLY_RESOURCE= to build patch
 * add apk from the build/bakApk
 */
ext {
    //for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
    tinkerEnabled = true

    //for normal build
    //old apk file to build patch apk
    tinkerOldApkPath = "${bakPath}/app-debug-0430-23-10-22.apk"
    //proguard mapping file to build patch apk
    tinkerApplyMappingPath = "${bakPath}/app-debug-1018-17-32-47-mapping.txt"
    //resource R.txt to build patch apk, must input if there is resource changed
    tinkerApplyResourcePath = "${bakPath}/app-debug-0430-23-10-22-R.txt"

    //only use for build all flavor, if not, just ignore this field
    tinkerBuildFlavorDirectory = "${bakPath}/app-1018-17-32-47"
}


def getOldApkPath() {
    return hasProperty("OLD_APK") ? OLD_APK : ext.tinkerOldApkPath
}

def getApplyMappingPath() {
    return hasProperty("APPLY_MAPPING") ? APPLY_MAPPING : ext.tinkerApplyMappingPath
}

def getApplyResourceMappingPath() {
    return hasProperty("APPLY_RESOURCE") ? APPLY_RESOURCE : ext.tinkerApplyResourcePath
}

def getTinkerIdValue() {
    return hasProperty("TINKER_ID") ? TINKER_ID : gitSha()
}


def buildWithTinker() {
    return hasProperty("TINKER_ENABLE") ? TINKER_ENABLE : ext.tinkerEnabled
}

def getTinkerBuildFlavorDirectory() {
    return ext.tinkerBuildFlavorDirectory
}

if (buildWithTinker()) {
    apply plugin: 'com.tencent.tinker.patch'

    tinkerPatch {
        oldApk = getOldApkPath()
        ignoreWarning = false
        useSign = true
        tinkerEnable = buildWithTinker()
        buildConfig {
            applyMapping = getApplyMappingPath()
            applyResourceMapping = getApplyResourceMappingPath()
            tinkerId = getTinkerIdValue()
            keepDexApply = false
            isProtectedApp = false
            supportHotplugComponent = false
        }

        dex {
            dexMode = "jar"
            pattern = ["classes*.dex", "assets/secondary-dex-?.jar"]
            loader = [ "tinker.sample.android.app.BaseBuildInfo"]
        }

        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 {
            pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
            ignoreChange = ["assets/sample_meta.txt"]
            largeModSize = 100
        }

        packageConfig {
            configField("patchMessage", "tinker is sample to use")
            configField("platform", "all")
            configField("patchVersion", "1.0")
        }
 
        sevenZip {
            zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
        }
    }

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

    android.applicationVariants.all { variant ->

        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}/${project.name}-${flavorName}-release.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"
                    }

                }
            }
        }
    }
}

然后在app的build.gradle中,我们需要添加tinker的库依赖以及应用tinker的gradle文件.

apply from: 'tinker.gradle'
android {
    defaultConfig {
        ...
        multiDexEnabled true
    }
    
//以下是签名信息,测试环境可以省略
//    signingConfigs {
//        release {
//            try {
//                storeFile file("./keystore/release.keystore")
//                storePassword "testres"
//                keyAlias "testres"
//                keyPassword "testres"
//            } catch (ex) {
//                throw new InvalidUserDataException(ex.toString())
//            }
//        }
//
//        debug {
//            storeFile file("./keystore/debug.keystore")
//        }
//    }
//    buildTypes {
//        release {
//            minifyEnabled true
//            signingConfig signingConfigs.release
//            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
//        }
//        debug {
//            debuggable true
//            minifyEnabled false
//            signingConfig signingConfigs.debug
//        }
//    }

//支持jni的热修复
//    sourceSets {
//        main {
//            jniLibs.srcDirs = ['libs']
//        }
//    }
}

dependencies {
    ...
    //gradle 3.0.0的一定要使用如下的依赖
    implementation("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
    annotationProcessor("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
    compileOnly("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
    //Android5.0以下需要添加multidex库.如果其他的库包含了multidex库,就不需要额外添加了。
    implementation "com.android.support:multidex:1.0.1"
}

2.构建Application

使用Tinker热修复时,不能使用原来的Application,需要使用DefaultApplicationLike代替。通过DefaultLifeCycle注解声明需要生成的真实Application名,比如:gsw.demotinker.TinkerApp。然后tinker-android-anno框架会在编译时生成这个Application。

@SuppressWarnings("unused")
@DefaultLifeCycle(application = "gsw.demotinker.TinkerApp",
        flags = ShareConstants.TINKER_ENABLE_ALL,
        loadVerifyFlag = false)
public class TinkerAppLike extends DefaultApplicationLike {
    private static final String TAG = "Tinker.TinkerAppLike";

    public TinkerAppLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag,
                         long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
        super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
    }

    /**
     * install multiDex before install tinker
     * so we don't need to put the tinker lib classes in the main dex
     *
     * @param base
     */
    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    @Override
    public void onBaseContextAttached(Context base) {
        super.onBaseContextAttached(base);
        //you must install multiDex whatever tinker is installed!
        MultiDex.install(base);
        UpgradePatchRetry.getInstance(getApplication()).setRetryEnable(true);
        TinkerInstaller.install(this);
        Tinker tinker = Tinker.with(getApplication());
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {
        getApplication().registerActivityLifecycleCallbacks(callback);
    }

}

然后再清单文件中声明这个Application:

 <uses-permission android:name="android.permission.INTERNET" />
 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
  
  <application
        android:name="gsw.demotinker.TinkerApp"
   //注意,声明时会报错,使用assembleDebug命令编译一下就OK了

3.生成差异包

  1. 使用AS右上角的Gradle-app-Tasks-build-assembleDebug编译,会在app-build-bakApk中生成apk文件和R文件,将它安装到手机上;

  2. 将tinker.gradle中的tinkerOldApkPath和tinkerApplyResourcePath,改成在app-build-bakApk中新生成的apk文件和R文件路径,然后修改项目的代码或者资源;

  3. 使用AS右上角的Gradle-app-Tasks-tinker-tinkerPatchDebug编译,会在app-build-outputs-apk-tinkerPatch-debug下生成patch_signed_7zip.apk,即为补丁包。将patch_signed_7zip.apk通过adb命令放入SD的根目录下

  4. 调用下面的方法,并重启App,热修复就完成了。

    TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed_7zip.apk");
    

4.Release的使用方法

Tinker的使用方式如下,以gradle接入的release包为例:

  1. 每次编译或发包将安装包与mapping文件备份;
  2. 若有补丁包的需要,按自身需要修改你的代码、库文件等;
  3. 将备份的基准安装包与mapping文件输入到tinkerPatch的配置中;
  4. 运行tinkerPatchRelease,即可自动编译最新的安装包,并与输入基准包作差异,得到最终的补丁包。

三、Tinker的已知问题

由于原理与系统限制,Tinker有以下已知问题:

  1. Tinker不支持修改AndroidManifest.xml,Tinker不支持新增四大组件(1.9.0支持新增非export的Activity);
  2. 由于Google Play的开发者条款限制,不建议在GP渠道动态更新代码;
  3. 在Android N上,补丁对应用启动时间有轻微的影响;
  4. 不支持部分三星android-21机型,加载补丁时会主动抛出"TinkerRuntimeException:checkDexInstall failed"
  5. 对于资源替换,不支持修改remoteView。例如transition动画,notification icon以及桌面图标。

最后

代码地址:https://gitee.com/yanhuo2008/Common/tree/master/DemoTinker

移动架构专题:https://www.jianshu.com/nb/25128604

喜欢请点赞,谢谢!

上一篇下一篇

猜你喜欢

热点阅读