即使所有的努力都付诸东流那又怎样
或许你遇到过这样的情况——新版本的app刚提交应用市场没多久,突然发现有一个很严重的bug。这时候该怎么办呐,传统的方式就是修复bug重新提交应用市场,且不说审核需要时间,对用户也是极大的伤害。热修复恰好能解决这样的问题,其实大厂已经开源了很多热修复框架。bugly推出热修复框架已经有一段时间了,腾讯出品,必属于精品,不用担心会出大的问题。下面我来讲一下集成的过程。
https://bugly.qq.com/docs/user-guide/instruction-manual-android-hotfix/?v=20161206145314 这个是官方的文档,不过你若是按照官方文档一步步做下去,不会发现根本不行。
-1- 工程根目录下“build.gradle”文件中添加
<pre>
// tinker gradle插件
classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.7.5')
// tinkersupport插件
classpath "com.tencent.bugly:tinker-support:latest.release"
task wrapper(type: Wrapper){
gradleVersion = "2.14.1"
}
</pre>
-2-自定义ApplicationLike,把之前Application中的初始化方法全部都搬到这个类中,Appid替换成bugly上自己项目的Appid
<pre>
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, Resources[] resources,
ClassLoader[] classLoader, AssetManager[] assetManager) {
super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime,
applicationStartMillisTime, tinkerResultIntent, resources, classLoader,
assetManager);
}
@Override
public void onCreate() {
super.onCreate();
Bugly.init(getApplication(), "Appid", true);
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
@Override
public void onBaseContextAttached(Context base) {
super.onBaseContextAttached(base);
MultiDex.install(base);
Beta.installTinker(this);
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public void registerActivityLifecycleCallback(Application.ActivityLifecycleCallbacks callbacks) {
getApplication().registerActivityLifecycleCallbacks(callbacks);
}
}
</pre>
-3-自定义Application
<pre>
public class App extends TinkerApplication{
public App() {
super(ShareConstants.TINKER_ENABLE_ALL, "SampleApplicationLike带包名地址",
"com.tencent.tinker.loader.TinkerLoader", false);
}
}
</pre>
-4-AndroidManifest.xml配置
-
1. 权限配置
<pre>
<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" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
</pre> -
2.配置FileProvider(Android N之后配置,若是N之前这步不用配置也可以
<pre>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.tencent.bugly.hotfix.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"/>
</provider>
</pre>
provider_paths文件内容如下
<pre>
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="beta_external_path" path="Download/"/>
<external-path name="beta_external_files_path" path="Android/data/"/>
</paths>
</pre>
-5-在到app目录下新建
keep_in_main_dex.txt对应的内容如下:
<pre>
you can copy the tinker keep rule at
build/intermediates/tinker_intermediates/tinker_multidexkeep.pro
-keep class com.tencent.tinker.loader.** {
*;
}
-keep class com.tencent.bugly.hotfix.SampleApplication {
*;
}
-keep public class * implements com.tencent.tinker.loader.app.ApplicationLifeCycle {
*;
}
-keep public class * extends com.tencent.tinker.loader.TinkerLoader {
*;
}
-keep public class * extends com.tencent.tinker.loader.app.TinkerApplication {
*;
}
here, it is your own keep rules.
you must be careful that the class name you write won't be proguard
but the tinker class above is OK, we have already keep for you!
</pre>
-6-app目录下的build.gradle文件配置
<pre>
apply plugin: 'com.android.application'
android {
compileSdkVersion 25
buildToolsVersion "25.0.1"
// 编译选项
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
dexOptions {
jumboMode = true
}
// 签名配置
signingConfigs {
release {
try {
storeFile file("./keystore/release.keystore")
storePassword "testres"
keyAlias "testres"
keyPassword "testres"
} catch (ex) {
throw new InvalidUserDataException(ex.toString())
}
}
debug {
storeFile file("./keystore/debug.keystore")
}
}
defaultConfig {
applicationId "com.llf.update"
minSdkVersion 11
targetSdkVersion 25
versionCode 1
versionName "1.0"
// 开启multidex
multiDexEnabled true
// 以Proguard的方式手动加入要放到Main.dex中的类
multiDexKeepProguard file("keep_in_main_dex.txt")
}
buildTypes {
release {
minifyEnabled true
signingConfig signingConfigs.release
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
debuggable true
minifyEnabled false
signingConfig signingConfigs.debug
}
}
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
repositories {
flatDir {
dirs 'libs'
}
}
lintOptions {
checkReleaseBuilds false
abortOnError false
}
}
def gitSha() {
try {
String gitRev = 'git rev-parse --short HEAD'.execute(null, project.rootDir).text.trim()
if (gitRev == null) {
throw new GradleException("can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'")
}
return gitRev
} catch (Exception e) {
throw new GradleException("can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'")
}
}
def bakPath = file("${buildDir}/bakApk/")
ext {
// for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
tinkerEnabled = true
// for normal build
// old apk file to build patch apk
tinkerOldApkPath = "${bakPath}\\app-release-1209-11-19-41.apk"
// proguard mapping file to build patch apk
tinkerApplyMappingPath = "${bakPath}\\app-release-1209-11-19-41-mapping.txt"
// resource R.txt to build patch apk, must input if there is resource changed
tinkerApplyResourcePath = "${bakPath}\\app-release-1209-11-19-41-R.txt"
}
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
}
/**
-
更多Tinker插件详细的配置,参考:https://github.com/Tencent/tinker/wiki
*/
if (buildWithTinker()) {
apply plugin: 'com.tencent.bugly.tinker-support'
// 依赖tinker插件
apply plugin: 'com.tencent.tinker.patch'tinkerSupport {
}// 全局信息相关配置项
tinkerPatch {
oldApk = getOldApkPath() //必选, 基准包路径ignoreWarning = false // 可选,默认false useSign = true // 可选,默认true, 验证基准apk和patch签名是否一致 // 编译相关配置项 buildConfig { applyMapping = getApplyMappingPath() // 可选,设置mapping文件,建议保持旧apk的proguard混淆方式 applyResourceMapping = getApplyResourceMappingPath() // 可选,设置R.txt文件,通过旧apk文件保持ResId的分配 tinkerId = "addd91ae18b1f9c685c2d51f97391723" // 必选,默认为null } // dex相关配置项 dex { dexMode = "jar" // 可选,默认为jar usePreGeneratedPatchDex = true // 可选,默认为false pattern = ["classes*.dex", "assets/secondary-dex-?.jar"] // 必选 loader = ["com.tencent.tinker.loader.*", "com.llf.update.App", ] } // lib相关的配置项 lib { pattern = ["lib/armeabi.so"] } // res相关的配置项 res { pattern = ["res", "assets", "resources.arsc", "AndroidManifest.xml"] ignoreChange = ["assets/sample_meta.txt"] largeModSize = 100 } // 用于生成补丁包中的'package_meta.txt'文件 packageConfig { configField("patchMessage", "tinker is sample to use") configField("platform", "all") configField("patchVersion", "1.0") } // 7zip路径配置项,执行前提是useSign为true sevenZip { zipArtifact = "com.tencent.mm:SevenZip:1.1.10" // optional }
}
}
List<String> flavors = new ArrayList<>();
project.android.productFlavors.each { flavor ->
flavors.add(flavor.name)
}
boolean hasFlavors = flavors.size() > 0
/**
-
bak apk and mapping
/
android.applicationVariants.all { variant ->
/*- task type, you want to bak
*/
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" } } }
}
} - task type, you want to bak
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:25.0.1'
testCompile 'junit:junit:4.12'
compile "com.android.support:multidex:1.0.1"
compile "com.tencent.bugly:crashreport_upgrade:1.2.0"
}
</pre>
特别注意下面的三块地方:
release.keystore和debug.keystore换成你自己签名,当然对应的storePassword,keyAlias和keyPassword也要做对应的修改
这两块在热更新时要用app-release-1209-11-41.apk对应是要修复的apk,
修复版和线上版本tinkerId要一致。
-7-最后别忘了加上混淆
<pre>
第三方的bugly
-dontwarn com.tencent.bugly.**
-keep public class com.tencent.bugly.*{;}
</pre>
-8-接下来就可以生成测试包了,你可以造一个bug来测试,然后运行生成测试包,不过这里的运行跟以往不同,按下面点击右侧的Gradle,若出现Nothing to show,那就点一下我红圈标注的部分
然后运行assembleRelease生成测试包
测试包在下图所示的位置
然后把测试包上传,可以上传到测试分发,然后将app下载下来,看看是否存在你故意设置的bug,如果本身就没bug就搞笑了。
-9-然后生成修复包
将你故意设置的bug修复,注意我上面说的地方,修改ext下oldApp的路径。然后运行tinkerPatchRelease生成修复包。
修复包生成的位置在patch目录下
然后将发布新补丁,补丁可能需要1到2分钟生效。重新运行你之前下载的app,注意这里需要2次启动才能生效。看看之前的bug是否已在不知不觉中修复。