工作中偶尔会遇到的场景:当一个App发布之后,突然发现了一个严重bug需要进行紧急修复,这时候公司各方就会忙得焦头烂额:重新打包App、测试、向各个应用市场和渠道换包、提示用户升级、用户下载、覆盖安装。
重点是还会有原来的版本遗留,无论你怎么提示都有人放弃治疗,不愿意升级。
如果这是一个影响公司收入或者体验影响极其不好的Bug,那去求了,老板脾气不好的话扣钱是小事,严重的就可以回家种地了。
最后最致命的是:
有时候仅仅是因为不小心写错了一行代码,就让所有部门的加班成果都付之东流,屈不屈。
还有一种剧情是研发总监把锅甩给测试团队,测试不过关,测试摊摊手说我也不是神啊,总会有漏网之鱼。锅甩过来甩过去就造成了部门、同事之间的不和谐,以后测试会提bug提到你欲哭无泪。
那能不能神不知鬼不觉在没有产生较大影响前把bug快速修复了呢?
答案肯定是可以的。
1:热更新介绍
1.1:什么是热更新?
百度百科:热更新就是动态下发代码,它可以使开发者在不发布新版本的情况下,修复bug和发布功能,避免长时间的审核等待以及多次被拒造成的成本。
热修复提出于2014年,兴起于2016年,尤其是在Instant run 问世以后,各种热修复技术相继涌出。
是一种摆脱传统发版方案直接使用补丁来更新app内容,不需要重新下载安装apk等略过一系列繁琐过程的新兴技术,目前国内部分成熟App都拥有自己的热修复技术,如:手淘、QQ、微信、阿里、美团、饿了么等。
1.2:热更新的优势。
· 无需重新发版,简单高效
· 用户无感知,无需下载新应用,代价小
· 修复成功率高,挽回用户群体
1.3:有哪些比较火的热更新方案?
· 手机淘宝:Sophix(前身是HotFix)
· 微信:Tinker
· 饿了么:Amigo
· 美团:Robust
· 阿里:AndFix
· QQ:QZone
两个表格可以不看,没什么卵用,是在网上查阅的资料,第一张图是讲Sophix热更新的,所以他说Sophix是最好的。第二张图是讲bugly热更新的,自然说Tinker好(bugly是基于Tinker的)。但是还是能看出来手机淘宝的Sophix和微信的Tinker更加好一些,两个的用户都是数亿级别。
微信的用户更加活跃频繁一些,那我们就用Tinker,也就是bugly。
2:集成官方sdk
bugly官方网站:点击打开链接
第一步:添加插件依赖
工程目录下“build.gradle”文件中添加:
dependencies标签中添加一行:
classpath "com.tencent.bugly:tinker-support:latest.release"
添加完成后如下:
第二步:集成SDK
gradle配置
步骤1
在app module的“build.gradle”文件里面的defaultConfig标签中添加:
ndk {
//设置支持的SO库架构,模拟器需要‘x86’,‘armeabi-v7a’
abiFilters 'armeabi' //, 'x86', 'armeabi-v7a', 'x86_64', 'arm64-v8a'
}
步骤2
dependencies标签中添加:
compile "com.android.support:multidex:1.0.1" // 多dex配置
//注释掉原有bugly的仓库
//compile 'com.tencent.bugly:crashreport:latest.release'//其中latest.release指代最新版本号,也可以指定明确的版本号,例如2.3.2
compile 'com.tencent.bugly:crashreport_upgrade:latest.release'
compile 'com.tencent.bugly:nativecrashreport:latest.release' //其中latest.release指代最新版本号,也可以指定明确的版本号,例如2.2.0
步骤3
signingConfigs标签中添加:
release {
try {
storeFile file("E:/jksdemo/demo.jks")//根据自己项目的密钥具体地址更改
storePassword "123456"//根据自己项目的密钥密码更改
keyAlias "key0"
keyPassword "123456"//根据自己项目的密钥密码更改
} catch (ex) {
throw new InvalidUserDataException(ex.toString())
}
}
步骤4
buildTypes标签内release标签里面添加:
signingConfig signingConfigs.release
以上步骤操作完成后如下:
步骤5
把项目从Android视图样式切换到Project视图:
鼠标放在src文件夹上右键
New-->File
注意后缀是gradle,点击OK
就生成了名称为tinker-support.gradle的文件
然后打开它 将下面的代码全部复制进去:
apply plugin: 'com.tencent.bugly.tinker-support'
def bakPath = file("${buildDir}/bakApk/")
/**
* 此处填写每次构建生成的基准包目录,基准包就是你第一次打的正式包
*/def baseApkDir = " app-0208-15-10-00"
/**
* 对于插件各参数的详细解析请参考
*/
tinkerSupport {
// 开启tinker-support插件,默认值true
enable = true
// 指定归档目录,默认值当前module的子目录tinker
autoBackupApkDir = "${bakPath}"
// 是否启用覆盖tinkerPatch配置功能,默认值false
// 开启后tinkerPatch配置不生效,即无需添加tinkerPatch
overrideTinkerPatchConfiguration = true
// 编译补丁包时,必需指定基线版本的apk,默认值为空
// 如果为空,则表示不是进行补丁包的编译
// @{link tinkerPatch.oldApk }
baseApk = "${bakPath}/${baseApkDir}/app-release.apk"
// 对应tinker插件applyMapping
baseApkProguardMapping = "${bakPath}/${baseApkDir}/app-release-mapping.txt"
// 对应tinker插件applyResourceMapping
baseApkResourceMapping = "${bakPath}/${baseApkDir}/app-release-R.txt"
// 构建基准包和补丁包都要指定不同的tinkerId,并且必须保证唯一性
tinkerId = "base-1.0.1"
// 构建多渠道补丁时使用
// buildAllFlavorsDir = "${bakPath}/${baseApkDir}"
// 是否启用加固模式,默认为false.(tinker-spport 1.0.7起支持),使用加固必须打开
// isProtectedApp = true
// 是否开启反射Application模式
enableProxyApplication = false
}
/**
* 一般来说,我们无需对下面的参数做任何的修改
* 对于各参数的详细介绍请参考:
* https://github.com/Tencent/tinker/wiki/Tinker-%E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97
*/
tinkerPatch {
//oldApk ="${bakPath}/${appName}/app-release.apk"
ignoreWarning = false
useSign = true
dex {
dexMode = "jar"
pattern = ["classes*.dex"]
loader = []
}
lib {
pattern = ["lib/*/*.so"]
}
res {
pattern = ["res/*", "r/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
ignoreChange = []
largeModSize = 100
}
packageConfig {
}
sevenZip {
zipArtifact = "com.tencent.mm:SevenZip:1.1.10"// path = "/usr/local/bin/7za"
}
buildConfig {
keepDexApply = false
//tinkerId = "1.0.1-base"
//applyMapping = "${bakPath}/${appName}/app-release-mapping.txt" // 可选,设置mapping文件,建议保持旧apk的proguard混淆方式
//applyResourceMapping = "${bakPath}/${appName}/app-release-R.txt" // 可选,设置R.txt文件,通过旧apk文件保持ResId的分配
}
}
步骤6
切换到Android视图
打开build.gradle(Module:app)在里面添加依赖刚才建的插件脚本:
// 依赖插件脚本
apply from: 'tinker-support.gradle'
操作完成是这样的:
步骤7
上面的配置先告一段落,来到bugly官方网站进行登录账户
创建完成:
复制App ID
步骤8
来到项目新建一个类SampleApplicationLike如下:(别忘记更换为自己app的ID)
public class SampleApplicationLike extends DefaultApplicationLike {
public static final String TAG = "Tinker.SampleApplicationLike";
public SampleApplicationLike(Application application, int tinkerFlags,
boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime,
long applicationStartMillisTime, Intent tinkerResultIntent) {
super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
}
@Override
public void onCreate() {
super.onCreate();
// 这里实现SDK初始化,appId替换成你的在Bugly平台申请的appId
// 这第二个参数是Bugly平台申请的appId
// 调试时,将第三个参数改为true
Bugly.init(getApplication(), "5e61a00cb0", false);
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
@Override
public void onBaseContextAttached(Context base) {
super.onBaseContextAttached(base);
// you must install multiDex whatever tinker is installed!
MultiDex.install(base);
// 安装tinker
// TinkerManager.installTinker(this); 替换成下面Bugly提供的方法
Beta.installTinker(this);
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public void registerActivityLifecycleCallback(Application.ActivityLifecycleCallbacks callbacks) {
getApplication().registerActivityLifecycleCallbacks(callbacks);
}
}
然后再建个类SampleApplication代码如下:(别忘记更换为自己的SampleApplicationLike类的包名)
public class SampleApplication extends TinkerApplication {
public SampleApplication() {
super(ShareConstants.TINKER_ENABLE_ALL, "com.example.administrator.buglytestdemo.SampleApplicationLike",
"com.tencent.tinker.loader.TinkerLoader", false);
}
}
步骤9
然后打开AndroidManifest.xml
application标签中添加:
android:name="com.example.administrator.buglytestdemo.SampleApplication"
(别忘记改为自己的包名)
接下来添加权限:
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_LOGS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
然后activity配置:(必配,框架里自带的Activity,无需新建)
<activity
android:name="com.tencent.bugly.beta.ui.BetaActivity"
android:configChanges="keyboardHidden|orientation|screenSize|locale"
android:theme="@android:style/Theme.Translucent" />
然后配置FileProvider
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"/></provider>
配置完成如下:
如果你使用的第三方库也配置了同样的FileProvider, 可以通过继承FileProvider类来解决合并冲突的问题,示例如下:
<provider
android:name=".utils.BuglyFileProvider"
android:authorities="${applicationId}.fileProvider"
android:exported="false"
android:grantUriPermissions="true"
tools:replace="name,authorities,exported,grantUriPermissions">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"
tools:replace="name,resource"/>
</provider>
这里要注意一下,FileProvider类是在support-v4包中的,检查你的工程是否引入该类库。
在res目录新建xml文件夹,创建provider_paths.xml文件如下:
provider_paths.xml代码如下:
<?xml version="1.0" encoding="utf-8"?><paths xmlns:android="http://schemas.android.com/apk/res/android">
<!-- /storage/emulated/0/Download/${applicationId}/.beta/apk-->
<external-path name="beta_external_path" path="Download/"/>
<!--/storage/emulated/0/Android/data/${applicationId}/files/apk/-->
<external-path name="beta_external_files_path" path="Android/data/"/></paths>
这里配置的两个外部存储路径是升级SDK下载的文件可能存在的路径,一定要按照上面格式配置,不然可能会出现错误。
步骤10
为了避免混淆SDK,在Proguard混淆文件中增加以下配置:
-dontwarn com.tencent.bugly.**
-keep public class com.tencent.bugly.**{*;}
如果你使用了support-v4包,你还需要配置以下混淆规则:
-keep class android.support.**{*;}
配置完成如下:
经过上面的步骤到这里配置就基本完成了。
接下来是打基准包(就是apk,每个版本的基准包要和后面的补丁一一对应的话补丁才能用)。
基准包的版本就是在下图箭头地方指定和设置的
tinkerId最好是一个唯一标识,例如git版本号、versionName等等。 如果你要测试热更新,你需要对基线版本进行联网上报。
这里强调一下,基线版本配置一个唯一的tinkerId,而这个基线版本能够应用补丁的前提是集成过热更新SDK,并启动上报过联网,这样我们后台会将这个tinkerId对应到一个目标版本,例如tinkerId = "bugly_1.0.0" 对应了一个目标版本是1.0.0,基于这个版本打的补丁包就能匹配到目标版本。
备注:tinkerId的版本号其实是根据build里面的versionName+versionCode拼接而成的。也就是
所以升级包的时候要versionCode加1,versionName根据实际项目情况决定要不要增大。
tinkerId的版本号尽量每次改为versionName+versionCode的拼接一致,如1.0.1
接下来打基准包:
完成后会是这样的:
实际应用中,请注意保存线上发布版本的基准apk包、R.txt文件,如果线上版本有bug,就可以借助我们tinker-support插件进行补丁包的生成。
基准包打成功后来到项目的路径\app\build\bakApk\最后一个文件夹里面app-release.apk就是这个基准包。然后安装在手机或者模拟器上。
3:热修复、热更新使用
接下来使用补丁修复:
根据实际情况修改自己项目中的代码或者解决一个bug。
这个地方改为和这次要打补丁的基准包一样的名称
然后Gradle如下:
完成后是这样的
项目路径\app\build\outputs\patch
这个就是补丁包。
接下来来到官网
点击创建的这个项目
点击应用升级
点击热更新-->发布新补丁
选择刚才的补丁,选择为全量设备,立即下发。
然后手机上的该版本的app会自动神不知鬼不觉的把bug修复了。
接下来试下升级apk版本:
versionCode增加1
versionName根据实际情况改变,这里就先不变了
tinkerId最好改为和versionName+versionCode拼接一样的版本号。
按照前面步骤生成基准包。
来到官网
填写标题、更新说明。点击创建策略
然后搞定。
上个之前测试的效果图片: