技术标签: 笔记 java android android studio
Bugly虽然提供补丁管理后台,但有时项目可能希望自行管理补丁,今天就简单说一下这个方案。
最近项目想集成Bugly热修复,基本测试已经通了。突然想自行管理补丁,不通过Bugly管理平台下发。于是就抽空看了一下Bugly 资料和Tinker部分源码,初步确定了一下方案步骤,在此进行一下记录。
主要步骤流程如下图所示:
其中版本标志可以自行定义,只要能区分出不同的版本,保证唯一性即可。最简单的就比如 VersionName ,VersionCode,PackageName组合等,也可以考虑上传DEVICEID用来精确定位。
注意: 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 ,大家有需要的可以按照本方案进行改造尝试。
方式一:生成字典形式使用DictReader逐行读取csv文件 返回的每一个单元格都放在一个字典的值内,而这个字典的键则是这个单元格的列标题# 逐行读取csv文件with open(filename,'r',encoding="utf-8") as csvfile: reader = csv.DictReader(csvfile) for row in reader...
1、set(Kkey,Vvalue) 新增一个字符串类型的值,key是键,value是值。 redisTemplate.opsForValue().set("stringValue","bbb");2、get(Objectkey) 获取key键对应的值StringstringValue=redisTemplate.opsForValue(...
展开全部强烈2113建议刚刚涉及ABAQUS二次开发的5261同学读读,反正我读书的过4102程中觉得思路清晰,而且获1653益匪浅《Python语言在Abaqus中的应用》讲述了:Python语言在Abaqus中的应用ABAQUSCAE工程师系列丛书Python语言在Abaqus中的应用曹金凤王旭春孔亮编著机械工业出版社《Python语言在Abaqus中的应用》详细介绍了Python语言基础知识...
前沿之前学习的时候就是不是很懂,学着学着就会更加麻烦,而且生活中,一些麻烦事情你不解决,往往这些麻烦事情就会接二连三的找你,就像我们平时复习的时候有些题目不会,结果考试的时候,偏偏就会考这些不会的,这些小麻烦影响心情的同时更加影响效率。科三练车也是第一次课程回来有些地方我不明白,第二次我就赶紧去就问这个,不要拍出错,更不要怕出丑,该问就问,不懂就问,最终曲折大家会问你过没过,谁都不会在乎过程的
主从复制进行配置的时候出现如下错误: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;gt; show slave status...
MDVRServerMessageProcessor mockProcessor = PowerMockito.spy(new MDVRServerMessageProcessor()); String hexStrOfCmd = "9700030000003100000000000C0000000001DC732F073BBC7D021909261105000001"; ...
1.继承保存// C# & 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中的“多态”方法是什么func (recv receiver_type) methodName(parameter_list) (return_value_list) { … }在 Go 语言中,结构体就像是类的一种简化形式,那么面向对象程序员可能会问:类的方法在哪里呢?在 Go 中有一个概念,它和方法有着同样的名字,并且大体上意思相同:Go 方法是作用在接收者(receiver)上的一个函数,接收者是某种类型的变量。因此方法是一种特殊类型的函数
8种机械键盘轴体对比本人程序员,要买一个写代码的键盘,请问红轴和茶轴怎么选?导语:在学习Linux文件系统的硬链接和符号连接时,我也产生了这个疑问。搜索找到几篇中文资料,总感觉像是隔靴搔痒,似懂非懂。改用英文搜索,找到了这篇文章,短短几句话,一针见血,深入浅出。于是翻译出来与大家分享。提问:教科书中说,Unix/Linux不允许硬链接指向目录,但软链接却可以。这是因为目录的硬链接会在文件系统中产...
给定一个仅包含大小写字母和空格' '的字符串,返回其最后一个单词的长度。如果不存在最后一个单词,请返回 0。说明:一个单词是指由字母组成,但不包含任何空格的字符串。示例:输入: "Hello World"输出: 5来源:力扣(LeetCode)链接:https://leetcode-cn.com/problems/length-of-last-word著作权归领扣网...
建筑师在对一栋建筑物进行施工之前,首先会根据建筑图纸上的平面图、立体图、剖面图和构造详图等对建筑物进行整体布局后再从局部施工(当然不排除有先装修完厕所再砌卧室围墙的奇葩)。在一个网页页面的搭建过程中,对于前端工程师来说也是如此。在拿到UI设计图的时候,我们需要先对页面的整体布局进行分析,再从布局开始搭建整个页面。页面布局是一个前端工程师最最基础的基本功,前端布局的方式非常多,本文将根据网站...
例子代码就在我的博客中,包括六个UDP和TCP发送接受的cpp文件,一个基于MFC的局域网聊天小工具工程,和此小工具的所有运行时库、资源和执行程序。代码的压缩包位置是http://www.blogjava.net/Files/wxb_nudt/socket_src.rar。1 前言在一些常用的编程技术中,Socket网络编程可以说是最简单的一种。而且Socket编程