首先为什么要集成bugly热修复。市面上有其他的热修复框架,为什么就用bugly?这里给出2张图大家就明白了。
引用腾讯bugly官网的一段话:
无需关注Tinker是如何合成补丁的无需自己搭建补丁管理后台无需考虑后台下发补丁策略的任何事情无需考虑补丁下载合成的时机,处理后台下发的策略我们提供了更加方便集成Tinker的方式我们提供应用升级一站式解决方案 进入正题:接入流程主要是以下几个步骤:
打基准包安装并上报联网(注:填写唯一的tinkerId)对基准包的bug修复(可以是Java代码变更,资源的变更)修改基准包路径、填写补丁包tinkerId、mapping文件路径、resId文件路径执行tinkerPatchRelease打Release版本补丁包选择app/build/outputs/patch目录下的补丁包并上传(注:不要选择tinkerPatch目录下的补丁包,不然上传会有问题)编辑下发补丁规则,点击立即下发重启基准包,请求补丁策略(SDK会自动下载补丁并合成)再次重启基准包,检验补丁应用结果
1:新建基准包工程项目(人为制造有BUG的app版本)
[java] view plain copy btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { / String str = LoadBugClass.getBugString(); String str = BugClass.bug(); Toast.makeText(MainActivity.this,str,Toast.LENGTH_SHORT).show();; } }); [java] view plain copy public class BugClass { public static String bug(){ String str = null; int str_length = str.length(); return "this is bug class"; } } 这个可以看出点击一个按钮会报空指针异常。
2:接着就是配置相关属性和添加一个插件依赖了。
官方教程地址:点击打开链接
下面也给出我自己配置的过程。
首先在最外层的build.gradle文件中添加依赖,看下图:
其次新建sampleapplication和sampleapplicationLike两个Java类
[java] view plain copy package com.henry.testappbugly; import android.annotation.TargetApi; import android.app.Application; import android.content.Context; import android.content.Intent; import android.content.res.AssetManager; import android.content.res.Resources; import android.os.Build; import android.support.multidex.MultiDex; import com.tencent.bugly.Bugly; import com.tencent.bugly.beta.Beta; import com.tencent.tinker.loader.app.DefaultApplicationLike; /** * Created by W61 on 2016/11/29. */ 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(); // 这里实现SDK初始化,appId替换成你的在Bugly平台申请的appId Bugly.init(getApplication(), "", true); } @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); } } [java] view plain copy package com.henry.testappbugly; import com.tencent.tinker.loader.app.TinkerApplication; import com.tencent.tinker.loader.shareutil.ShareConstants; /** * Created by W61 on 2016/11/29. */ public class SampleApplication extends TinkerApplication { public SampleApplication() { super(ShareConstants.TINKER_ENABLE_ALL, "SampleApplicationLike所在的包名路径", "com.tencent.tinker.loader.TinkerLoader", false); } } 在在Androidmanifest.xml文件中配置权限及application类名 [java] view plain copy <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.henry.testappbugly"> <application android:name=".SampleApplication" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <!--API 24以上配置--> <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> </application> <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"/> </manifest> 在到res目录下:
[java] view plain copy <?xml version="1.0" encoding="utf-8"?> <paths xmlns:android="http://schemas.android.com/apk/res/android"> <!-- 这里配置的两个外部存储路径是升级SDK下载的文件可能存在的路径 --> <!-- /storage/emulated/0/Download/com.bugly.upgrade.demo/.beta/apk--> <external-path name="beta_external_path" path="Download/"/> <!--/storage/emulated/0/Android/data/com.bugly.upgrade.demo/files/apk/--> <external-path name="beta_external_files_path" path="Android/data/"/> </paths>
在到app目录下新建:
[java] view plain copy # 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! 然后在混淆文件.pro中添加这几句代码(bugly都有说明解释)
[java] view plain copy -dontwarn com.tencent.bugly.** -keep public class com.tencent.bugly.**{*;} 最后就是app目录下的build.gradle文件配置了:
[java] view plain copy apply plugin: 'com.android.application' dependencies { compile fileTree(include: ['*.jar'], dir: 'libs') compile 'com.android.support:appcompat-v7:24.1.1' // 多dex配置 compile "com.android.support:multidex:1.0.1" // 集成Bugly热更新aar(灰度时使用方式) // compile(name: 'bugly_crashreport_upgrade-1.2.0', ext: 'aar') compile "com.tencent.bugly:crashreport_upgrade:1.2.0" } android { compileSdkVersion 23 buildToolsVersion "23.0.2" // 编译选项 compileOptions { sourceCompatibility JavaVersion.VERSION_1_7 targetCompatibility JavaVersion.VERSION_1_7 } // recommend dexOptions { jumboMode = true } // 签名配置 signingConfigs { // 签名配置 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.henry.testappbugly" minSdkVersion 14 targetSdkVersion 23 versionCode 2 versionName "2.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/") /** * you can use assembleRelease to build you base apk * use tinkerPatchRelease -POLD_APK= -PAPPLY_MAPPING= -PAPPLY_RESOURCE= to build patch * add apk from the build/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-1201-09-46-25.apk" // proguard mapping file to build patch apk tinkerApplyMappingPath = "${bakPath}/app-release-1201-09-46-25-mapping.txt" // resource R.txt to build patch apk, must input if there is resource changed tinkerApplyResourcePath = "${bakPath}/app-release-1201-09-46-25-R.txt" // only use for build all flavor, if not, just ignore this field tinkerBuildFlavorDirectory = "${bakPath}/app-release-1201-09-46-25" } 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()) { // 依赖tinker插件 apply plugin: 'com.tencent.tinker.patch' apply plugin: 'com.tencent.bugly.tinker-support' 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 = "可以是签名版本号字符串等等比如:assdhfkdshfksdhfuksfhuk" // 必选,默认为null } // dex相关配置项 dex { dexMode = "jar" // 可选,默认为jar usePreGeneratedPatchDex = true // 可选,默认为false pattern = ["classes*.dex", "assets/secondary-dex-?.jar"] // 必选 loader = ["com.tencent.tinker.loader.*", "SampleApplication所在的全路径", ] } // 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 // path = "/usr/local/bin/7za" // 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 { //sample use for build all flavor for one time 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" } } } } } } 最后run as生成有bug的基准包app
在去腾讯bugly官网将这个基准包上传上去即可。
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
接下来,制作补丁包。
由于刚才点击按钮报空指针,下面将代码稍做改动如下:
[java] view plain copy protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btn = (Button) findViewById(R.id.btn); btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { String str = LoadBugClass.getBugString(); // String str = BugClass.bug(); Toast.makeText(MainActivity.this,str,Toast.LENGTH_SHORT).show();; } }); } [java] view plain copy public class LoadBugClass { /** * 获取bug字符串. * * @return 返回bug字符串 */ public static String getBugString() { // BugClass bugClass = new BugClass(); return "iS OK"; } } 这样点击按钮就会弹出is ok了不会报错。
修改配置文件:
这里注意点,补丁包是基于基准包所生成的patch文件并不是版本升级,所以此处补丁包不需要修改versioncode,versionname
然后双击下图中所指地方:
稍等片刻就会出现下图中类容:
其中的patch_signed_7zip.apk就是补丁包了。将这个补丁包上传到腾讯bugly即可。
注意:上传完补丁包点击了立即下发,就需要重新启动基准包策略。从有bug版本的app到修复有一个时间差的。估计1到2分钟左右才能看到效果。
附上集成过程中可能遇到的坑解决办法地址:点击打开链接
最后附上自己写的demo地址:点击打开链接