Tinker 热修复

介绍

 

Tinker是微信官方的Android热补丁解决方案,它支持动态修改代码(class文件)、So库以及资源,让应用能够在不需要重新安装的情况下实现更新。当然,你也可以使用Tinker来更新你的插件,实现动态部署。

它主要包括以下几个部分:

1gradle编译插件: tinker-patch-gradle-plugin (用于生成差分补丁的gradle插件)

 

2)核心sdk: tinker-android-lib (核心sdk,主要的类位于com.tencent.tinker.lib.tinker.*, 比如TinkerTinker补丁清除,判断是否加载补丁)、TinkerInstallerTinker补丁安装)、TinkerApplication(负责重写ClassLoader的加载规则,先加载补丁dex中的类,从而达到覆盖旧class的目的))

 

Github

https://github.com/Tencent/tinker/wiki

https://github.com/Tencent/tinker

 

 

原理

腾讯系

  1. QZone

QQ空间超级补丁技术,基于虚拟机class loader动态加载,大致的过程就是:把BUG方法修复以后,放到一个单独的DEX里,应用启动后,将补丁dex插入到class loaderdexElements数组的最前面,让虚拟机优先去加载修复完后的方法。

 

 

 2.Tinker

微信针对QQ空间超级补丁技术的不足提出了一个提供DEX差量包,整体替换DEX的方案。主要的原理是与QQ空间超级补丁技术基本相同,区别在于不再将patch.dex增加到elements数组中,而是差量的方式给出patch.dex,然后将patch.dex与应用的classes.dex合并,然后整体替换掉旧的DEX,达到修复的目的。

 

阿里系 

HotFix

HotFIxAndfix技术+后台管理(补丁安全,版本管理和发布)) 是阿里百川推出的热修复服务,相对于QQ空间超级补丁技术和微信Tinker来说,定位于紧急bug修复的场景下,能够最及时的修复bug,下拉补丁立即生效无需等待。

AndFix作为native解决方案,运行时在Native修改Filed指针的方式,实现方法的替换(hook),达到即时生效无需重启

 

 

 

Sophix

结合了AndfixTinker这两个开源框架的优势,支即时生效的热修复,同时也支持资源、类替换等需要APK重启的冷修复。

基于两大热修复技术的同时,也对接入方式做了改良,提供快速接入的方式,很傻瓜。接入方式请参考#Sophix接入

而且APK打包方式是无侵入式的(Tinker会使用gradle插件,根据注解等信息生成一些中间类)。依托于阿里云,可以轻松实现版本管理、补丁推送、灰度发布等功能。缺点是,功能逐步完善后,已经商业化了,需要money。不过东航2018应该会引入阿里的EMAC,届时热修复也是其中的一项功能。

优势

 

类替换

Sophix

Tinker

Andfix

So替换

支持

支持

不支持

类替换

支持

支持

不支持(只支持方法替换)

资源替换

支持

支持

不支持

全平台支持

支持

支持

部分支持(由于厂商的自定义ROM,对少数机型暂不支持)

及时生效

部分支持

不支持

支持

接入复杂度

傻瓜式

较复杂

比较简单

补丁包大小

较小

较小

较小

侵入式打包

依赖侵入式

后台发布支持

支持(阿里云)

支持(tinkerpatch,今天挂掉了)

支持(HotFix

价格

收费(有免费调用次数限制)

免费开源(支持本地打补丁)

免费开源


http://www.tinkerpatch.com/Docs/SDK

 可见Tinker与阿里的Sophix相比,基本上没有任何优势。最大的优势是开源和免费。

 Tinker的已知问题

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

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

Tinker接入

 Demo

https://github.com/Tencent/tinker/tree/master/tinker-sample-android

https://github.com/Tencent/tinker/wiki/Tinker-入指南 

Gradle配置

 

gradle.properties

TINKER_VERSION=1.9.2

build.gradle

dependencies {

    classpath "com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}"

}

 
dependencies {

    //optional, help to generate the final application

    provided('com.tencent.tinker:tinker-android-anno:1.9.1')

    //tinker's main Android lib

    compile('com.tencent.tinker:tinker-android-lib:1.9.1')

}

...

...

apply plugin: 'com.tencent.tinker.patch'

++ 补丁生成的gradle脚本

重点关注下面的信息:

buildConfigField "String", "MESSAGE", "\"I am the base apk\""
buildConfigField "String", "TINKER_ID", "\"${getTinkerIdValue()}\""
buildConfigField "String", "PLATFORM", "\"all\""

 

buildTypes {
    release {
        minifyEnabled false
        signingConfig signingConfigs.release
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        /*proguardFiles getDefaultProguardFile('proguard-android.txt'), project.file('proguard-rules.pro')*/
    }
    debug {
        debuggable true
        minifyEnabled false
        signingConfig signingConfigs.debug
    }
}

 

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.apk"
    //proguard mapping file to build patch apk
    tinkerApplyMappingPath = "${bakPath}/app-mapping.txt"
    //resource R.txt to build patch apk, must input if there is resource changed
    tinkerApplyResourcePath = "${bakPath}/app-R.txt"

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

上面的R.txt很重要,主要用于资源文件。因为Android apk在打包的时候,res下的资源文件会对应生成一个到Resource的java文件中,存放着id和内存地址的映射。而我们上层通过getString(R.id.xxx), getDrawable(R.drawable.xxx)获取这些对象。

 

热更新之所以支持资源替换,实际上是修改了映射资源的java文件,并把其中的映射关系修改为指向,patch包中需要替换的资源。

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

混淆配置

proguard-rules.pro

-dontwarn cn.jpush.android.**

代码接入

 修改并重构Application,使用TinkerApplication

 

这里可以配置,我们支持的热更新方式可以选择,包括

TINKER_DEX_MASK

TINKER_ENABLE_ALL

TINKER_DEX_AND_LIBRARY

 

public void onBaseContextAttached(Context base) {
    super.onBaseContextAttached(base);
    //you must install multiDex whatever tinker is installed!
    MultiDex.install(base);

    SampleApplicationContext.application = getApplication();
    SampleApplicationContext.context = getApplication();
    TinkerManager.setTinkerApplicationLike(this);

    TinkerManager.initFastCrashProtect();
    //should set before tinker is installed
    TinkerManager.setUpgradeRetryEnable(true);

    //optional set logIml, or you can use default debug log
    TinkerInstaller.setLogIml(new MyLogImp());

    //installTinker after load multiDex
    //or you can put com.tencent.tinker.** to main dex
    TinkerManager.installTinker(this);
    Tinker tinker = Tinker.with(getApplication());
}

推荐使用tinker-android-anno,通过注解自动生成中间类。我们只需要对MyApp的生命周期处理的交给TinkerApplication

getApplication().registerActivityLifecycleCallbacks(callback);

加载补丁

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

清除补丁

Tinker.with(getApplicationContext()).cleanPatch();

补丁校验

SamplePatchListener

public int patchCheck(String path, String patchMd5) {

}

可以做一些MD5校验等补丁应用前的操作

 补丁监听

SampleResultService

public void onPatchResult(final PatchResult result) {

}

监听补丁的应用情况,并做出响应动作。比如补丁应用成功后,提示用户重启。或者监控应用一旦切换到后台,自动的杀掉进程。

应用到海外APP

参考工程文件:UpdateManager.java

启动应用检测更新(checkUpdateOnUi->  有大版本更新优先进行APK更新 -> 通过主应用版本号从服务器获取补丁版本(临时方案,简化原定为一对一的原则) -> 判断是否需要删除已应用的补丁(旧的补丁可能会有不良影响,比如APK升级后,旧的补丁也会被加载) -> 否有补丁更新(本地应用的补丁,与服务器的补丁比大小) -> 检测补丁是否已经下载 -> 进度条弹窗下载补丁 -> 下载完成后,关闭弹窗,静默应用补丁 -> 补丁合成成功则提示重启进程(失败不作任何提示)

 

补丁会临时的下载到SD卡上,一旦应用成功原始的patch文件会被删除,它们会与原始apkdex被合成为fix_dex文件,放到apk的文件目录中。每次启动应用都会去加载。

 

 

补充:Sophix接入

简单概括,大体上是

 

1. 登录移动热修复控制台:https://hotfix.console.aliyun.com/

 

创建应用,并在阿里云平台上上传并管理补丁

 

 

 

2. 本地应用程序清单,配置好API KEY

application节点下加入如下配置

<meta-data

android:name="com.taobao.android.hotfix.IDSECRET"

android:value="24725702-1" />

<meta-data

android:name="com.taobao.android.hotfix.APPSECRET"

android:value="3bc454cddb160ddb44b960201461109b" />

<meta-data

android:name="com.taobao.android.hotfix.RSASECRET"

android:value="MIIE...I48gzIJ3u/dgQBHDtjA==" />


 

3. 本地代码加载Sophix框架

public class MyApplication extends MultiDexApplication {

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        MultiDex.install(this);

        String appVersion = "0.0.0";
        try {
            appVersion = this.getPackageManager()
                    .getPackageInfo(this.getPackageName(), 0)
                    .versionName;
        } catch (Exception e) {
        }

        // appVersion如果为空,后面initialize会抛异常
        LogMan.logDebug("appVersion: " + appVersion);

        // initialize最好放在attachBaseContext最前面,初始化直接在Application类里面,切勿封装到其他类
        SophixManager.getInstance().setContext(this)
                .setAppVersion(appVersion)
                .setAesKey(null)
                .setEnableDebug(true)
                .setPatchLoadStatusStub(new PatchLoadStatusListener() {
                    @Override
                    public void onLoad(final int mode, final int code, final String info, final int handlePatchVersion) {
                        LogMan.logDebug("PatchLoadStatusListener code: " + code);
                        // 补丁加载回调通知
                        if (code == PatchStatus.CODE_LOAD_SUCCESS) {
                            // 表明补丁加载成功
                            LogMan.logDebug("patch loading success");
                        } else if (code == PatchStatus.CODE_LOAD_RELAUNCH) {
                            // 表明新补丁生效需要重启. 开发者可提示用户或者强制重启;
                            // 建议: 用户可以监听进入后台事件, 然后调用killProcessSafely自杀,以此加快应用补丁,详见1.3.2.3
                            LogMan.logDebug("code patch loading success");
                            SophixManager.getInstance().killProcessSafely();
                        } else {
                            LogMan.logDebug("code patch loading failed");
                            // 其它错误信息, 查看PatchStatus类说明
                        }
                    }
                }).initialize();
    }

抓取补丁

btn_load.setOnClickListener(new View.OnClickListener() {

@Override

public void onClick(View v) {

LogMan.logDebug("start to load patch from AliYun");

SophixManager.getInstance().queryAndLoadNewPatch();

}

});



 

 

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值