微信Tinker热更新集成文档

本文详细介绍了微信Tinker热更新的实现过程,从导入官方Sample,设置tinkerId,构建并运行原版apk,制作差分包,到集成到项目中,包括Gradle依赖配置,Application的调整,再到本地测试和服务端接口调试,为热更新的实施提供了清晰的步骤。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

微信Tinker热更新方案


一、微信Tinker热更新简介

1、github地址: https://github.com/Tencent/tinker
2、Tinker原理: http://mp.weixin.qq.com/s?__biz=MzAwNDY1ODY2OQ==&mid=2649286306&idx=1&sn=d6b2865e033a99de60b2d4314c6e0a25#rd
3、Tinker是微信官方的Android热补丁解决方案,它支持动态下发代码、So库以及资源,让应用能够在不需要重新安装的情况下实现更新。当然,你也可以使用Tinker来更新你的插件。

二、导入官方Sample

1、github下载下来sample,然后解压打开导入Android Studio,我们只需要把tinker-sample-android这个目录导入即可.
2、导入之后,构建一下,发现提示“tinkerId is not set!!!”,这时候我们在app/bulid.gradle中,设置tinkerId的值.

这里可以直接就把当前的版本号作为tinkerId

3、编辑运行原版apk
打开Android studio 右上角Gradle,双击运行assembleDebug命令

拿到下图中的app-debug-xxxxx.apk装在手机上运行

或者直接运行(不过要先关闭Instant Run) ->file->setting->Build.E….->Instant Run 第一个去掉就可以运行了

4、配置原版apk路径
ext {
    //for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
    tinkerEnabled = true

    //for normal build
    //旧版本apk路径配置
    tinkerOldApkPath = "${bakPath}/app-debug-1111-14-05-21.apk"
    //用于混淆,没有混淆可以不管
    tinkerApplyMappingPath = "${bakPath}/app-debug-1111-14-05-21-mapping.txt"
    //旧版本apk R文件
    tinkerApplyResourcePath = "${bakPath}/app-debug-1111-14-05-21-R.txt"

    //only use for build all flavor, if not, just ignore this field
    tinkerBuildFlavorDirectory = "${bakPath}/app-debug-1111-14-05-21"
}
5、修改源码(修改R文件和Res文件),制作差分包.
修改完后,双击运行tinkerPatchDebug命令

运行完后(如果运行时老卡住,可重启Android studio再试),在app/build/outputs/tinkerPatch/debug/patch_signed_7zip.apk路径下找到这个差异包,也就是我们俗称的补丁.

6、运行应用,加载补丁
把patch_signed_7zip.apk放到手机SD卡跟目录中,或者使用adb命令
adb push ./app/build/outputs/tinkerPatch/debug/patch_signed_7zip.apk /storage/sdcard0/ 
再次运行apk,点击LoadPatch,这时候界面提示“patch success, please restart process”表示加载补丁成功,最后在后台App管理杀掉该App重新启动应用
补充:返回键退出后进入,并没有执行修复。杀进程后再进入 ,应该就可以修复成功了,如果不成功,把补丁包逆向一下,看看自己修复的部分有没有在里面。

三、集成到项目中(这里以河南快处快赔警用版为例)

1、添加Gradle依赖
项目的build.gradle中,添加tinker-patch-gradle-plugin的依赖
dependencies {
    classpath 'com.android.tools.build:gradle:2.2.0'
    classpath "com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}"
}
项目的gradle.properties文件中加入Tinker版本号配置
TINKER_VERSION=1.7.3
然后在app的gradle文件app/build.gradle,需要添加tinker的库依赖以及apply tinker的gradle插件.
//tinker 核心lib库
compile("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
//可选,用于生成application类 
provided("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
添加生成补丁方法以及配置,这里可以把官网配置照搬过来,有些可以去掉
def bakPath = file("${buildDir}/bakApk/")
ext {
    tinkerEnabled = true
    tinkerOldApkPath = "${bakPath}/app-debug-1111-15-04-57.apk"
    tinkerApplyMappingPath = "${bakPath}/app-debug-1018-17-32-47-mapping.txt"
    tinkerApplyResourcePath = "${bakPath}/app-debug-1111-15-04-57-R.txt"
    tinkerBuildFlavorDirectory = "${bakPath}/app-debug-1108-10-27-36"
}

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 = true
        useSign = true
        buildConfig {
            applyMapping = getApplyMappingPath()
            applyResourceMapping = getApplyResourceMappingPath()
            tinkerId = getTinkerIdValue()
        }

        dex {

            dexMode = "jar"
            usePreGeneratedPatchDex = false
            pattern = ["classes*.dex",
                       "assets/secondary-dex-?.jar"]
            loader = ["com.tencent.tinker.loader.*",
                      //warning, you must change it with your application
                      "tinker.sample.android.app.SampleApplication",
                      //use sample, let BaseBuildInfo unchangeable with tinker
                      "tinker.sample.android.app.BaseBuildInfo"
            ]
        }

        lib {
            pattern = ["lib/armeabi/*.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
    android.applicationVariants.all { variant ->
        def taskName = variant.name
        def date = new Date().format("MMdd-HH-mm-ss")

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

                }
            }
        }
    }
}
2、配置Application
这里需要将Application继承TinkerApplication交给Tinker管理,Tinker提供了一个ApplicationLike供使用,为了避免少的改动,我们将项目中的Application 继承TinkerApplication(此时onCreate方法会报错,因为父类用了final修饰,不能覆盖),去掉onCreate方法,将onCreate方法中的初始化操作单独抽一个init方法()出来,我们找个合适的时机进行调用,例如MainActivity,这样我们其他引用Application的地方都不需要改动。
public void init() {
    // 获取登录信息
    String loginInfoStr = PrefUtils.getString(getApplicationContext(), "loginInfo", null);
    if (null != null && !"".equals(loginInfoStr))
    {
        EntityBean loginInfo = (EntityBean) com.longrise.LEAP.Base.IO.JSONSerializer.getInstance()
                .DeSerialize(loginInfoStr, EntityBean.class);
        if (null != loginInfo)
        {
            EntityBean userInfo = loginInfo.getBean("userinfo");
            this.token = userInfo.getString("token", null);
        }
    }

    kckpName = "110000002000";
    kckpPass = EncryptService.getInstance().MD5Encrypt("888888a");

    // 程序的入口
    // 初始化化一些.常用属性.然后放到盒子里面来
    // 上下文
    mContext = getApplicationContext();

    // 主线程
    mMainThread = Thread.currentThread();

    // 主线程Id
    mMainTreadId = android.os.Process.myTid();

    // tid thread
    // uid user
    // pid process
    // 主线程Looper对象
    mMainLooper = getMainLooper();

    // 定义一个handler

    mHandler = new Handler();
    super.onCreate();
    //初始化imageloader
    ImageLoader.getInstance().init(ImageLoaderConfiguration.createDefault(getContext()));
}
在程序启动的第一个Activity初始化(例如向导页)
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    //调用application里的初始化方法
    ((BaseApplication)getApplicationContext()).init();

    this.requestWindowFeature(Window.FEATURE_NO_TITLE);

    setContentView(R.layout.activity_guid);

    initUI();
    initData();
    initListener();

}
另外拷贝Sample中ApplicationLike,修改名称,去掉自动生成Application的注解。

四、本地测试是否能进行热更新,测试步骤与上面官方例子一样.

五、调试服务端接口,测试热更新

服务端接口
1、请求接口参数:APP名称(APPName)、APP版本号(versionCode)、JS文件的版本号、系统(Android或者IOS)
2、返回参数:(这个接口的返回值需要加密返回)

{
  data: 
  {
     isUpdate: true, //是否需要更新
     JSVersion:1,    //JS文件的版本
     url: "http://"  //JS文件的下载地址
  }
}
接口请求
LoadDataManagerCar.getInstance(context).callService(null,
            ((BaseApplication) context.getApplicationContext()).getServerUrl(), URLConstant.GETAPPUPDATEVERSION,
            bean, new LoadDataManagerFather.OnRequestCompleteListener() {
                @Override
                public void onSuccess(String key, String service, Object result) {
                    if (null != processDialog) {
                        processDialog.cancel();
                    }
                    if (null != result) {
                        EntityBean bean = (EntityBean) result;
                        EntityBean data = bean.getBean("data");
                        String restate = bean.getString("restate");
                        String redes = bean.getString("redes");
                        if ("1".equals(restate)) {
                            try {
                                EnCryptDeCrypt encryptdecrypt = EnCryptDeCrypt.getInstance();
                                //sp获取本地版本号,对比是否需要下载安装包,安装热更新文件,重置sp的值

                                String encryAppVersion = encryptdecrypt.deCrypt(data.getString("appversion"));

                                String url = data.getString("url");

                                _apkurl_hot = encryptdecrypt.deCrypt(url);//获取下载apk的url
                                Integer appversion =Integer.parseInt(encryAppVersion) ;//后台获取的热更新版本号
                                //对比版本号,存进sp
                                int appSpVersion = PrefUtils.getInt(context, "appSpVersion", 1);//本地sp版本号
                                if(appversion>appSpVersion){
                                    //下载热更新包安装
                                    Thread thread = new Thread(null, _doDownLoadApk2, "downloadapkBackground2");
                                    thread.start();
                                    PrefUtils.setInt(context,"appSpVersion",appversion);
                                }
                            } catch (Exception e) {
                                e.printStackTrace();
                            }

                        } else {
                            Toast.makeText(context, redes, Toast.LENGTH_SHORT).show();
                        }
                    }
                }
文件下载(差分包放入到/sdcard/Android/app package/cache目录)
                    if (200 == conn.getResponseCode()) {
                        this.isbreak = false;
                        InputStream is = conn.getInputStream();
                        String locDir = "hotPatch";
                        String updateApk = new Date().getTime() + ".apk";
                        File file = new File(context.getExternalCacheDir(), locDir);
                        if (null != file) {
                            if (!file.exists()) {
                                file.mkdir();
                            }
                        }
                        file = new File(context.getExternalCacheDir(), locDir + "/" + updateApk);
                        if (null != file) {

                            FileOutputStream fos = new FileOutputStream(file);
                            BufferedInputStream bis = new BufferedInputStream(is);
                            byte[] buffer = new byte[1024];
                            int len;
                            int total = 0;
                            while ((len = bis.read(buffer)) != -1) {
                                if (isbreak) {
                                    file = null;
                                    break;
                                }
                                fos.write(buffer, 0, len);
                                total += len;
                                /*if (null != pd) {
                                    pd.setProgress(total);
                                }*/
                            }
                            fos.close();
                            bis.close();
                            is.close();
                        }
文件下载成功后,加载差分包
            if(null!=_apkfile_hot){
                String absolutePath = _apkfile_hot.getAbsolutePath();
                TinkerInstaller.onReceiveUpgradePatch(context.getApplicationContext(), absolutePath);
            }
在TinkerResultService中可以检测到差分包是否加载成功,从而作相应的处理
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值