Android热修复使用详解

概述

热修复是一种动态修复程序解决问题的思想,其特点如下:

  • 无需发版,实现高效Bug修复,用户无感知
  • 无需下载新应用,只需下载补丁包,代价小
  • 修复成功率高,降低紧急Bug的损失

原理介绍

大致有三种方案:底层Native替换方案、Java层类加载方案

Java层类加载方案

类加载方案基于Dex分包方案,Dex分包方案则是因为65536限制和LinearAlloc限制。

  • 65536限制就是DVM指令集的方法调用指令invoke-kind索引为16bits,最多只能引用65535个方法,超过则会编译失败。
  • LinearAlloc限制就是LinearAlloc是DVM中一个固定的缓存区,方法数超过混存取大小会在安装时提示INSTALL_FAILED_DEXOPT错误。
    Dex分包方案就是打包时将应用代码分成多个Dex文件,启动必须用到的类放到主Dex中,其他代码放到次Dex中,应用启动先加载主Dex把应用启动起来,再动听加载次Dex,从而解决65536限制和LinearAlloc限制的问题。
    说回类加载方案,当应用在加载一个类的时候 他会利用ClassLoader机制去寻找这个类,关键代码在DexPathList.findClass()函数中
 public Class<?> findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {//1
            Class<?> clazz = element.findClass(name, definingContext, suppressed);//2
            if (clazz != null) {
                return clazz;
            }
        }
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

Element代表了一个dex文件,其内部封装了DexFile用于加载dex文件,由于上面的分包方案一个应用有了多个dex,而dexElements则有序的存储了所有dex。这边findClass则会遍历dexElements在各个dex中按顺序查找所需类,找到后就返回并终止查找。类加载方案则是将要修改的.class文件打包成包含dex的patch.jar,App下载拿到后然后将这个dex放到dexElements列表的首位或者利用其它方法让patch.jar中的class排在前面位置,则要修改的.class则会先被找到,存在bug的旧.class根据ClassLoader的双亲委派模式就不会被加载

PS:由于类加载后不会卸载,class被findClass一次后不需要findClass第二次所以需要重新启动应用,重新findClass才会生效。

底层Native替换方案

利用Native反射替换要修复的类的方法的信息(执行入口、访问权限、所属类、代码执行地址等)
PS:即时生效,但由于基于Native层直接替换原有类,限制多,无法增减原有类的方法和字段

Android 热修复原理篇及几大方案比较

方案对比

特性AndFix(阿里)Hotfix(阿里)Sophix(阿里)超级补丁(QQ空间)Tinker(微信) Amigo(饿了么)Robust(美团) Aceso(美丽说蘑菇街)
即时生效同时支持即时生效和冷启动修复
方法替换
类替换
资源替换
so替换
支持ART

Tinker使用介绍

  1. http://www.tinkerpatch.com 平台注册一个app并获取appKey
  2. 根据 http://www.tinkerpatch.com/Docs/SDK 文档做配置
  3. build.gradle.中
// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {

    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.1.2'
        // TinkerPatch 插件
        classpath "com.tinkerpatch.sdk:tinkerpatch-gradle-plugin:1.2.6"
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}
  1. app/build.gradle中,主要是签名和tinker的包
apply plugin: 'com.android.application'
apply from: 'tinkerpatch.gradle'
android {
    signingConfigs {
        release {
            keyAlias 'key0'
            keyPassword 'tinker'
            storeFile file('../tinker.jks')
            storePassword 'tinker'
        }
    }
    compileSdkVersion 26
    defaultConfig {
        applicationId "com.android.commonlib.tinkertest"
        minSdkVersion 19
        targetSdkVersion 26
        versionCode 1
        versionName "1.0.1"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        javaCompileOptions { annotationProcessorOptions { includeCompileClasspath = true } }
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.release
        }
        debug {
            signingConfig signingConfigs.release
        }
    }
}

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation 'com.android.support:appcompat-v7:26.1.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

    implementation "com.android.support:multidex:1.0.2"
    //若使用annotation需要单独引用,对于tinker的其他库都无需再引用
    annotationProcessor("com.tinkerpatch.tinker:tinker-android-anno:1.9.6") { changing = true }
    compileOnly("com.tinkerpatch.tinker:tinker-android-anno:1.9.6") { changing = true }
    implementation("com.tinkerpatch.sdk:tinkerpatch-android-sdk:1.2.6") { changing = true }
}
  1. Application
public class SampleApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        initTinkerPatch();
    }

    private void initTinkerPatch() {
        // 我们可以从这里获得Tinker加载过程的信息
        ApplicationLike tinkerApplicationLike = TinkerPatchApplicationLike.getTinkerPatchApplicationLike();
        // 初始化TinkerPatch SDK
        TinkerPatch.init(
                tinkerApplicationLike
//                new TinkerPatch.Builder(tinkerApplicationLike)
//                    .requestLoader(new OkHttp3Loader())
//                    .build()
        )
                .reflectPatchLibrary()
                .setPatchRollbackOnScreenOff(true)
                .setPatchRestartOnSrceenOff(true)
                .setFetchPatchIntervalByHours(3)
        ;
        // 获取当前的补丁版本

        // fetchPatchUpdateAndPollWithInterval 与 fetchPatchUpdate(false)
        // 不同的是,会通过handler的方式去轮询
        TinkerPatch.with().fetchPatchUpdateAndPollWithInterval();
    }

    @Override
    public void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        //you must install multiDex whatever tinker is installed!
        MultiDex.install(base);
    }
}
  1. tinkerpatch.gradle
apply plugin: 'tinkerpatch-support'

/**
 * TODO: 请按自己的需求修改为适应自己工程的参数
 */
def bakPath = file("${buildDir}/bakApk/")
def baseInfo = "app-1.0.1-0525-15-59-41"
def variantName = "release"

/**
 * 对于插件各参数的详细解析请参考
 * http://tinkerpatch.com/Docs/SDK
 */
tinkerpatchSupport {
    /** 可以在debug的时候关闭 tinkerPatch **/
    /** 当disable tinker的时候需要添加multiDexKeepProguard和proguardFiles,
     这些配置文件本身由tinkerPatch的插件自动添加,当你disable后需要手动添加
     你可以copy本示例中的proguardRules.pro和tinkerMultidexKeep.pro,
     需要你手动修改'tinker.sample.android.app'本示例的包名为你自己的包名, com.xxx前缀的包名不用修改
     **/
    tinkerEnable = true
    reflectApplication = true
    /**
     * 是否开启加固模式,只能在APK将要进行加固时使用,否则会patch失败。
     * 如果只在某个渠道使用了加固,可使用多flavors配置
     **/
    protectedApp = false
    /**
     * 实验功能
     * 补丁是否支持新增 Activity (新增Activity的exported属性必须为false)
     **/
    supportComponent = true

    autoBackupApkPath = "${bakPath}"

    appKey = "a2fc5f63bf186415"

    /** 注意: 若发布新的全量包, appVersion一定要更新 **/
    appVersion = "1.0.1"

    def pathPrefix = "${bakPath}/${baseInfo}/${variantName}/"
    def name = "${project.name}-${variantName}"

    baseApkFile = "${pathPrefix}/${name}.apk"
    baseProguardMappingFile = "${pathPrefix}/${name}-mapping.txt"
    baseResourceRFile = "${pathPrefix}/${name}-R.txt"

    /**
     *  若有编译多flavors需求, 可以参照: https://github.com/TinkerPatch/tinkerpatch-flavors-sample
     *  注意: 除非你不同的flavor代码是不一样的,不然建议采用zip comment或者文件方式生成渠道信息(相关工具:walle 或者 packer-ng)
     **/
}

/**
 * 用于用户在代码中判断tinkerPatch是否被使能
 */
android {
    defaultConfig {
        buildConfigField "boolean", "TINKER_ENABLE", "${tinkerpatchSupport.tinkerEnable}"
    }
}

/**
 1. 一般来说,我们无需对下面的参数做任何的修改
 2. 对于各参数的详细介绍请参考:
 3. https://github.com/Tencent/tinker/wiki/Tinker-%E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97
 */
tinkerPatch {
    ignoreWarning = false
    useSign = true
    dex {
        dexMode = "jar"
        pattern = ["classes*.dex"]
        loader = []
    }
    lib {
        pattern = ["lib/*/*.so"]
    }

    res {
        pattern = ["res/*", "r/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
        ignoreChange = []
        largeModSize = 100
    }

    packageConfig {
    }
    sevenZip {
        zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
//        path = "/usr/local/bin/7za"
    }
    buildConfig {
        keepDexApply = false
    }
}

这里写图片描述
配置完成,测试下,先运行assembleRelease模拟打正式包,生成文件如下(即app-release.apk)安装在手机上。
这里写图片描述
每次编译都会生成一个基准包,这边此次生成的基准包放在app-1.0.1-0525-15-59-41目录中,修改tinkerpatch.gradle中baseInfo的值为【app-1.0.1-0525-15-59-41】这个目录。
然后随意修改下我们的代码,表示我们要打的补丁。之后再运行tinkerPatchRelease就会基于这个基准包生成补丁包了。即tinker_result/patch_signed_7zip.apk

  1. 发布到tinkerpatch平台,我们刚刚的appVersion是1.0.1,在tinkerpatch中建一个1.0.1的版本,然后下发补丁即可。
  2. 建议本地先测试下,可以将补丁包发送到本地目录下,然后调用以下代码即可自动打补丁,重启应用即可生效。如果有错误也可以从log中看出
 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView textView = findViewById(R.id.textview);
        textView.setText("...Hello World...");

        textView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed_7zip.apk");
            }
        });
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值