Bugly热修复之通过自建服务器管理补丁方案_MarkSDGD的博客-程序员秘密

技术标签: 笔记  java  android  android studio  

Bugly热修复之通过自建服务器管理补丁方案

Bugly虽然提供补丁管理后台,但有时项目可能希望自行管理补丁,今天就简单说一下这个方案。



背景

最近项目想集成Bugly热修复,基本测试已经通了。突然想自行管理补丁,不通过Bugly管理平台下发。于是就抽空看了一下Bugly 资料和Tinker部分源码,初步确定了一下方案步骤,在此进行一下记录。


步骤流程

主要步骤流程如下图所示:

完整流程图

其中版本标志可以自行定义,只要能区分出不同的版本,保证唯一性即可。最简单的就比如 VersionName ,VersionCode,PackageName组合等,也可以考虑上传DEVICEID用来精确定位。

  1. 移动端打出的补丁包交由后台进行上传管理。移动端每次启动的时候进行主动进行补丁检测,也可以通过后台push进行检测。
  2. 后台根据移动端传递的版本标志,进行检测有无此版本对应的补丁。后台接口返回的字段包含needFix,needRollback, RollbackStrategy,apkURL ,apkMD5, apkSize以及其他一些信息字段。
  3. 如果没有补丁,正常启动。如果有补丁信息,根据needRollback判断是否需要回滚补丁。
  4. 如果需要回滚,根据RollbackStrategy回滚策略进行回滚。如果不需要回滚, 判断是否需要更新,检测的方式这里是通过本地记录的补丁MD5值与接口返回的apkMD5进行比对。
  5. 如果 MD5一致,说明当前已经打了这个补丁,不需要重复下载。如果不一致,下载补丁到指定目录,然后进行补丁合并,补丁成功后会删除指定目录下的补丁文件,成功后本地记录更新补丁MD5值。
  6. 等待下次重启修复。

注意: Tinker 源码中就是采用的记录补丁MD5值信息作为标记的,我们可以采用Tinker中的已有的API即可。具体请阅读以下源码:

  @Override
    public boolean tryPatch(Context context, String tempPatchPath, PatchResult patchResult) {
    
         //省略部分代码
         ......
        String patchMd5 = SharePatchFileUtil.getMD5(patchFile);
        if (patchMd5 == null) {
    
            ShareTinkerLog.e(TAG, "UpgradePatch tryPatch:patch md5 is null, just return");
            return false;
        }
        //use md5 as version
        patchResult.patchVersion = patchMd5;

        ShareTinkerLog.i(TAG, "UpgradePatch tryPatch:patchMd5:%s", patchMd5);

        //check ok, we can real recover a new patch
        final String patchDirectory = manager.getPatchDirectory().getAbsolutePath();

        File patchInfoLockFile = SharePatchFileUtil.getPatchInfoLockFile(patchDirectory);
        File patchInfoFile = SharePatchFileUtil.getPatchInfoFile(patchDirectory);

        final Map<String, String> pkgProps = signatureCheck.getPackagePropertiesIfPresent();
        if (pkgProps == null) {
    
            ShareTinkerLog.e(TAG, "UpgradePatch packageProperties is null, do we process a valid patch apk ?");
            return false;
        }

        final String isProtectedAppStr = pkgProps.get(ShareConstants.PKGMETA_KEY_IS_PROTECTED_APP);
        final boolean isProtectedApp = (isProtectedAppStr != null && !isProtectedAppStr.isEmpty() && !"0".equals(isProtectedAppStr));

        SharePatchInfo oldInfo = SharePatchInfo.readAndCheckPropertyWithLock(patchInfoFile, patchInfoLockFile);

        //it is a new patch, so we should not find a exist
        SharePatchInfo newInfo;

        //already have patch
        if (oldInfo != null) {
    
            if (oldInfo.oldVersion == null || oldInfo.newVersion == null || oldInfo.oatDir == null) {
    
                ShareTinkerLog.e(TAG, "UpgradePatch tryPatch:onPatchInfoCorrupted");
                manager.getPatchReporter().onPatchInfoCorrupted(patchFile, oldInfo.oldVersion, oldInfo.newVersion);
                return false;
            }

            if (!SharePatchFileUtil.checkIfMd5Valid(patchMd5)) {
    
                ShareTinkerLog.e(TAG, "UpgradePatch tryPatch:onPatchVersionCheckFail md5 %s is valid", patchMd5);
                manager.getPatchReporter().onPatchVersionCheckFail(patchFile, oldInfo, patchMd5);
                return false;
            }

            final boolean usingInterpret = oldInfo.oatDir.equals(ShareConstants.INTERPRET_DEX_OPTIMIZE_PATH);

            if (!usingInterpret && !ShareTinkerInternals.isNullOrNil(oldInfo.newVersion) && oldInfo.newVersion.equals(patchMd5) && !oldInfo.isRemoveNewVersion) {
    
                ShareTinkerLog.e(TAG, "patch already applied, md5: %s", patchMd5);

                // Reset patch apply retry count to let us be able to reapply without triggering
                // patch apply disable when we apply it successfully previously.
                UpgradePatchRetry.getInstance(context).onPatchResetMaxCheck(patchMd5);

                return true;
            }
            // if it is interpret now, use changing flag to wait main process
            final String finalOatDir = usingInterpret ? ShareConstants.CHANING_DEX_OPTIMIZE_PATH : oldInfo.oatDir;
            newInfo = new SharePatchInfo(oldInfo.oldVersion, patchMd5, isProtectedApp, false, Build.FINGERPRINT, finalOatDir, false);
        } else {
    
            newInfo = new SharePatchInfo("", patchMd5, isProtectedApp, false, Build.FINGERPRINT, ShareConstants.DEFAULT_DEX_OPTIMIZE_PATH, false);
        }

        // it is a new patch, we first delete if there is any files
        // don't delete dir for faster retry
        // SharePatchFileUtil.deleteDir(patchVersionDirectory);
        final String patchName = SharePatchFileUtil.getPatchVersionDirectory(patchMd5);

        final String patchVersionDirectory = patchDirectory + "/" + patchName;

        ShareTinkerLog.i(TAG, "UpgradePatch tryPatch:patchVersionDirectory:%s", patchVersionDirectory);
       //省略部分代码
         ......
 
    }

经过源码分析,我们可以采用如下方式获取已经应用的补丁信息SharePatchInfo,就与Tinker的管理方式保持一致,不会出现问题。

 private void checkPatchVersion() {
    
        
        final String patchDirectory = Tinker.with(MainActivity.this).getPatchDirectory().getAbsolutePath();
        File patchInfoLockFile = SharePatchFileUtil.getPatchInfoLockFile(patchDirectory);
        File patchInfoFile = SharePatchFileUtil.getPatchInfoFile(patchDirectory);
        SharePatchInfo patchInfo = SharePatchInfo.readAndCheckPropertyWithLock(patchInfoFile, patchInfoLockFile);
        if (patchInfo != null) {
    
            Toast.makeText(this, "patchInfo.oldVersion==" + patchInfo.oldVersion + "\n  patchInfo.newVersion==" + patchInfo.newVersion , Toast.LENGTH_LONG).show();
            Log.i("MARK", "patchInfo.oldVersion==" + patchInfo.oldVersion + "  patchInfo.newVersion==" + patchInfo.newVersion );
        } else {
    
            Toast.makeText(this, "no patch !", Toast.LENGTH_LONG).show();
            Log.i("MARK", "no patch !");
        }

    }

多补丁情况

.阅读Tinker源码可知,Tinker 在多补丁情况下,会以最后一个补丁为准,新补丁会覆盖旧补丁。因此,如果有新的修改,我们仍然要以最原始的Base apk 作为基准包来生成补丁。
最终生成的补丁上传到自己的服务器后台,覆盖掉旧的补丁即可。补丁默认文件名是 patch_signed_7zip.apk
假设不小心把补丁弄混了,也不用担心,移动端合成补丁的时候会检测TinkerID 对应关系,A基准包生成的补丁,是无法合成进B基准包的。


        int returnCode = ShareTinkerInternals.checkTinkerPackage(context, manager.getTinkerFlags(), patchFile, signatureCheck);
        if (returnCode != ShareConstants.ERROR_PACKAGE_CHECK_OK) {
    
            ShareTinkerLog.e(TAG, "UpgradePatch tryPatch:onPatchPackageCheckFail");
            manager.getPatchReporter().onPatchPackageCheckFail(patchFile, returnCode);
            return false;
        }

补丁回滚

上面流程提到了回滚策略, Tinker 支持的回滚策略有两种,一种是去除补丁,立即杀掉进;,一种是锁屏之后去除补丁,杀掉进程;回滚补丁源码如下:

 public void onPatchRollback(boolean var1) {
    
        if (!Tinker.with(getApplication()).isTinkerLoaded()) {
    
            Object[] var2 = new Object[0];
            TinkerLog.w("Tinker.PatchRequestCallback", "TinkerPatchRequestCallback: onPatchRollback, tinker is not loaded, just return", var2);
        } else {
    
            Object[] var3;
            if (var1) {
    
                var3 = new Object[0];
                TinkerLog.i("Tinker.TinkerManager", "delete patch now", var3);
                TinkerUtils.rollbackPatch(getApplication());
            } else {
    
                var3 = new Object[0];
                TinkerLog.i("Tinker.TinkerManager", "tinker wait screen to restart process", var3);
                new ScreenState(getApplication(), new IOnScreenOff() {
    
                    public void onScreenOff() {
    
                        TinkerUtils.rollbackPatch(TinkerManager.getApplication());
                    }
                });
            }

            (new Handler(Looper.getMainLooper())).post(new Runnable() {
    
                public void run() {
    
                    if (TinkerManager.this.tinkerListener != null) {
    
                        TinkerManager.this.tinkerListener.onPatchRollback();
                    }

                }
            });
        }

    }

由此可见,第一种方式会闪退,第二种方式要求APP在后台活着并且锁屏 才会回滚,如果用户习惯直接杀掉进程的话,就无法完成补丁回滚。 两种方案都无法做到完美。因此,不到万不得已,实际应用中不建议开启回滚机制,变相采用多补丁覆盖的方式修复即可。因此简化的流程图如下所示:

简化流程图


总结

本次对Bugly热修复通过自建服务器管理补丁方案进行了描述,除了Tinker 声明的已知问题等局限性外,目前热修复已经覆盖android Q ,大家有需要的可以按照本方案进行改造尝试。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/nhce12931/article/details/114582106

智能推荐

【笨办法,勿用】Ubuntu 20.04 LTS下安装OpenCV_gpubgsegm_LateLinux的博客-程序员秘密

1. 下载opencv源码sudo git clone https://gitee.com/mirrors/opencv.git2. 添加Ubuntu的官方源(某些库需要,如libjasper-dev)sudo add-apt-repository "deb http://security.ubuntu.com/ubuntu xenial-security main"sudo apt update3. 安装依赖sudo apt-get install build-essentia

[] taskService.completeTask(task.getId());的执行过程分析_iteye_3791的博客-程序员秘密

taskservice.completetask(task.getid());单步跟踪下去的顺序1、执行函数体内,得到dbid,感觉是注入到这个id去的。在taskimpl中有setdbid的方法2、commandservice.execute(new completetaskcmd(taskid));3、在completetaskcmd中,public void execute(...

【软考系统架构设计师】2021年下系统架构师论文写作历年真题_进击的横打的博客-程序员秘密

【软考系统架构设计师】2021年下系统架构师论文写作历年真题

亿级流量电商详情页系统的大型高并发与高可用缓存架构实战 目录_zhuiqiuuuu的博客-程序员秘密

对于高并发的场景来说,比如电商类,o2o,门户,等等互联网类的项目,缓存技术是Java项目中最常见的一种应用技术。然而,行业里很多朋友对缓存技术的了解与掌握,仅仅停留在掌握redis/memcached等缓存技术的基础使用,最多了解一些集群相关的知识,大部分人都可以对缓存技术掌握到这个程度。然而,仅仅对缓存相关的技术掌握到这种程度,无论是对于开发复杂的高并发系统,或者是在往Java高级工程师、Ja...

通过抓包深入分析HTTPS_搜狐技术产品小编2023的博客-程序员秘密

本文字数:6189字预计阅读时间:16分钟Https介绍https其实是在http上加了一层(SSL/TSL)加密协议,根据维基百科的解释:❝超文本传输安全协议(英语:HyperText Transfer Protocol Secure,缩写:HTTPS;常称为HTTP over TLS、HTTP over SSL或HTTP Secure)是一种通过计算机网络进行安全通信的传输协议。❝HTTP...

怎么将matlab仿真电路生成为图片导出,求助前辈,用Cadence仿真的图形,怎么导出用Matlab重新画..._斯里兰卡七七的博客-程序员秘密

Try a Matlab code to access the Spectre result directly=====% There are two methods to access the Spectre simulation dataset% method 1: select all the plotted trace in Wavescan and save to *.csv% meth...

随便推点

Bootstrap多级下拉菜单_bootstrap 多级下拉菜单_小宋想站起来的博客-程序员秘密

首先需要将数据组装成树状数据,就这个样子的数据组装数据的函数如下: //组装数据 pId为父节点的id function rec(data,id){ var arr = []; for (var i = 0; i &lt; data.length; i++) { //如果是当前节点子节点 if(data[i].pId == id){ //深度遍历 ...

JSP内置对象session和request中setAttribute方法_leftforward的博客-程序员秘密

setAttribute这个方法,在JSP内置对象session和request都有这个方法,这个方法作用就是保存数据,然后还可以用getAttribute方法来取出。比如现在又个User对象,User curruser = new User("zhangsan", 20, "男");1,request.setAttribute(“curruser”, curruser)这个方法是将curru

ESP32驱动DHT11_esp32 dht11_bird1999625的博客-程序员秘密

这篇文章讲一下ESP32如何驱动DHT11这一款常见的传感器,由于我也是吃的现成的,找的官方的驱动库,但是在这里做一个分享,简单教大家用法直接贴代码:这是DHT11.h用法:DHT dht(Pin_DHT11, DHT11); 这是创建温湿度传感器的控制类程序开始之后调用这个类的dht.begin()函数初始化,就能用了,读取信息代码如下。 float h = dht.readHumidity(); // 定义一个变量读取dht11的湿度值 float t

opensuse leap 42.3用YaST安装NVIDIA驱动_qq_43068895的博客-程序员秘密

大家可按图片鼠标位置点击进入下一张图片,我就不作文字说明了。1、2、从NVIDIA软件源安装,点击“第三方软件源”,点击后如下图3、点击上图鼠标处的链接,点击后如下图4复制地址框内的地址https://download.nvidia.com/opensuse/leap/42.3/如下图5到此NVIDIA驱动的URL就复制到剪贴板里了,下面把URL复制到YaST的软件源里。6、打开Yast,如下图点击“软件源”如上图,点击后如下图7

Linux下查看版本信息_篮球风云的博客-程序员秘密

1、uname -a(Linux查看版本当前操作系统内核信息)Linux localhost.localdomain 3.10.0-514.el7.x86_64 #1 SMP Tue Nov 22 16:42:41 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux2、cat /proc/version (Linux查看当前操作系统版本信息)Linux versi...

UnityObjectToWorldDir和UnityObjectToWorldNormal的区别_zengjunjie59的博客-程序员秘密

UnityObjectToWorldDir用于把模型空间下的矢量转换到世界空间UnityObjectToWorldNormal用于把模型空间下的法线向量转换到世界空间。因为必须保证法线垂直于模型的表面,所以缩放的时候与普通矢量不一样。如果法线用UnityObjectToWorldDir,则会出现以下错误,而用UnityObjectToWorldNormal,则可得到正确的结果...

推荐文章

热门文章

相关标签