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

智能推荐

python按行或列读取csv文件的方式_python 按列读取csv_Cuzur的博客-程序员秘密

方式一:生成字典形式使用DictReader逐行读取csv文件 返回的每一个单元格都放在一个字典的值内,而这个字典的键则是这个单元格的列标题# 逐行读取csv文件with open(filename,'r',encoding=&quot;utf-8&quot;) as csvfile: reader = csv.DictReader(csvfile) for row in reader...

RedisTemplate.opsForValue 常用方法_redistemplele.ops_云豆豆的大爷的博客-程序员秘密

1、set(Kkey,Vvalue) 新增一个字符串类型的值,key是键,value是值。  redisTemplate.opsForValue().set("stringValue","bbb");2、get(Objectkey) 获取key键对应的值StringstringValue=redisTemplate.opsForValue(...

python在abaqus中的应用_Python语言在Abaqus中的应用怎么样_weixin_39551366的博客-程序员秘密

展开全部强烈2113建议刚刚涉及ABAQUS二次开发的5261同学读读,反正我读书的过4102程中觉得思路清晰,而且获1653益匪浅《Python语言在Abaqus中的应用》讲述了:Python语言在Abaqus中的应用ABAQUSCAE工程师系列丛书Python语言在Abaqus中的应用曹金凤王旭春孔亮编著机械工业出版社《Python语言在Abaqus中的应用》详细介绍了Python语言基础知识...

OpenCV---坐标体系完全解析_opencv图像坐标原点_逐夸父的博客-程序员秘密

前沿之前学习的时候就是不是很懂,学着学着就会更加麻烦,而且生活中,一些麻烦事情你不解决,往往这些麻烦事情就会接二连三的找你,就像我们平时复习的时候有些题目不会,结果考试的时候,偏偏就会考这些不会的,这些小麻烦影响心情的同时更加影响效率。科三练车也是第一次课程回来有些地方我不明白,第二次我就赶紧去就问这个,不要拍出错,更不要怕出丑,该问就问,不懂就问,最终曲折大家会问你过没过,谁都不会在乎过程的

Last_IO_Error: Got fatal error 1236 from master when reading data from binary log: 'Could not find_mameng1998的博客-程序员秘密

主从复制进行配置的时候出现如下错误:Last_IO_Error: Got fatal error 1236 from master when reading data from binary log: 'Could not find first log file name in binary log index file'错误详情如下:mysql&amp;amp;gt; show slave status...

Android单元测试Mock_面向对象World的博客-程序员秘密

MDVRServerMessageProcessor mockProcessor = PowerMockito.spy(new MDVRServerMessageProcessor()); String hexStrOfCmd = "9700030000003100000000000C0000000001DC732F073BBC7D021909261105000001"; ...

随便推点

DB4O学习(七)--继承关联_weixin_30614587的博客-程序员秘密

1.继承保存// C# &amp; JAVAEmployee e1 = new Employee("Michael", "1234", "michael", 101, "10/5/1975");Manager m1 = new Manager("Sue", "9876", "sue", 102, "3/8/1982");CasualEmployee c1 = new CasualEmp...

关于Go语言中的函数方法_小生凡一的博客-程序员秘密

关于Go语言中的函数方法方法是什么函数和方法的区别Go中的“多态”方法是什么func (recv receiver_type) methodName(parameter_list) (return_value_list) { … }在 Go 语言中,结构体就像是类的一种简化形式,那么面向对象程序员可能会问:类的方法在哪里呢?在 Go 中有一个概念,它和方法有着同样的名字,并且大体上意思相同:Go 方法是作用在接收者(receiver)上的一个函数,接收者是某种类型的变量。因此方法是一种特殊类型的函数

linux不允许将硬链接指向目录,为什么 UNIX/Linux 不允许目录硬链 【翻译】_有见Finsight的博客-程序员秘密

8种机械键盘轴体对比本人程序员,要买一个写代码的键盘,请问红轴和茶轴怎么选?导语:在学习Linux文件系统的硬链接和符号连接时,我也产生了这个疑问。搜索找到几篇中文资料,总感觉像是隔靴搔痒,似懂非懂。改用英文搜索,找到了这篇文章,短短几句话,一针见血,深入浅出。于是翻译出来与大家分享。提问:教科书中说,Unix/Linux不允许硬链接指向目录,但软链接却可以。这是因为目录的硬链接会在文件系统中产...

Leetcode刷题记录 58. 最后一个单词的长度_Mr_dogyang的博客-程序员秘密

给定一个仅包含大小写字母和空格' '的字符串,返回其最后一个单词的长度。如果不存在最后一个单词,请返回 0。说明:一个单词是指由字母组成,但不包含任何空格的字符串。示例:输入: "Hello World"输出: 5来源:力扣(LeetCode)链接:https://leetcode-cn.com/problems/length-of-last-word著作权归领扣网...

前端必经之路:CSS页面布局(深入理解浮动布局、定位布局、圣杯布局和双飞翼布局等重要布局方案)_magin铺满一行_方宏伟的博客-程序员秘密

建筑师在对一栋建筑物进行施工之前,首先会根据建筑图纸上的平面图、立体图、剖面图和构造详图等对建筑物进行整体布局后再从局部施工(当然不排除有先装修完厕所再砌卧室围墙的奇葩)。在一个网页页面的搭建过程中,对于前端工程师来说也是如此。在拿到UI设计图的时候,我们需要先对页面的整体布局进行分析,再从布局开始搭建整个页面。页面布局是一个前端工程师最最基础的基本功,前端布局的方式非常多,本文将根据网站...

windows socket编程_err=wsastarup(wversionrequested,&wsadata)_大疯疯的博客-程序员秘密

例子代码就在我的博客中,包括六个UDP和TCP发送接受的cpp文件,一个基于MFC的局域网聊天小工具工程,和此小工具的所有运行时库、资源和执行程序。代码的压缩包位置是http://www.blogjava.net/Files/wxb_nudt/socket_src.rar。1         前言在一些常用的编程技术中,Socket网络编程可以说是最简单的一种。而且Socket编程

推荐文章

热门文章

相关标签