关于使用腾讯 Bugly 平台 Tinker开源热修复框架的 项目集成

时间:2021-09-20 08:53:42

Tinker是什么

Tinker是微信官方的Android热补丁解决方案,它支持动态下发代码、So库以及资源,让应用能够在不需要重新安装的情况下实现更新。当然,你也可以使用Tinker来更新你的插件。

它主要包括以下几个部分:

  1. gradle编译插件: tinker-patch-gradle-plugin
  2. 核心sdk库: tinker-android-lib
  3. 非gradle编译用户的命令行版本: tinker-patch-cli.jar



为什么使用Tinker
关于使用腾讯 Bugly 平台 Tinker开源热修复框架的 项目集成关于使用腾讯 Bugly 平台 Tinker开源热修复框架的 项目集成关于使用腾讯 Bugly 平台 Tinker开源热修复框架的 项目集成

总的来说:

  1. AndFix作为native解决方案,首先面临的是稳定性与兼容性问题,更重要的是它无法实现类替换,它是需要大量额外的开发成本的;
  2. Robust兼容性与成功率较高,但是它与AndFix一样,无法新增变量与类只能用做的bugFix方案;
  3. Qzone方案可以做到发布产品功能,但是它主要问题是插桩带来Dalvik的性能问题,以及为了解决Art下内存地址问题而导致补丁包急速增大的。
特别是在Android N之后,由于混合编译的inline策略修改,对于市面上的各种方案都不太容易解决。而Tinker热补丁方案不仅支持类、So以及资源的替换,它还是2.X-7.X的全平台支持。利用Tinker我们不仅可以用做bugfix,甚至可以替代功能的发布。Tinker已运行在微信的数亿Android设备上,那么为什么你不使用Tinker呢?

Tinker的已知问题

由于原理与系统限制,Tinker有以下已知问题:

  1. Tinker不支持修改AndroidManifest.xml,Tinker不支持新增四大组件;
  2. 由于Google Play的开发者条款限制,不建议在GP渠道动态更新代码;
  3. 在Android N上,补丁对应用启动时间有轻微的影响;
  4. 不支持部分三星android-21机型,加载补丁时会主动抛出"TinkerRuntimeException:checkDexInstall failed"
  5. tinker的一般模式并不支持加固,需要使用usePreGeneratedPatchDex模式,即提前生成补丁模式。某些加固工具可能会将非exported的四大组件类名替换,这些类将无法修改。对于Android N之后的设备,本模式可能会因为内联而出现问题,建议过滤N之后的设备;
  6. 对于资源替换,不支持修改remoteView。例如transition动画,notification icon以及桌面图标。

如何使用Tinker
接下来带大家 在项目中一步步使用 Tinker,我们选一个平台 我选的是bugly ,腾讯的,当然你也可以选择其他的平台, TinkerPatch ,这个也可以 ,但以后肯定要收费的,  TinkerPatch平台文档


首先第一步  添加插件依赖
工程根目录下“build.gradle”文件中添加:
关于使用腾讯 Bugly 平台 Tinker开源热修复框架的 项目集成关于使用腾讯 Bugly 平台 Tinker开源热修复框架的 项目集成关于使用腾讯 Bugly 平台 Tinker开源热修复框架的 项目集成
第二步:在app module的“build.gradle”文件中添加(我的配置):
apply  plugin: 'com.android.application'
def releaseTime() {
return new Date().format( "yyyy-MM-dd", TimeZone. getTimeZone( "UTC"))
}
dependencies {
compile fileTree( dir: 'libs', include: [ '*.jar'])
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:latest.release' // 其中 latest.release 指代最新版本号,也可以指定明确的版本号,例如 1.2.0
compile 'com.tencent.bugly:nativecrashreport:latest.release' // 其中 latest.release 指代最新版本号,也可以指定明确的版本号,例如 2.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 {
debug {
storeFile file( "../KeyDianda.jks")
storePassword "111111"
keyAlias "key_dianda"
keyPassword "111111"
}

release {
storeFile file( "../****.jks")//修改成自己的秘钥
storePassword "***"// 改成自己的密码
keyAlias "***"
keyPassword "***"
}
}

// 默认配置
defaultConfig {
applicationId "com.tinker.ddinfo"
minSdkVersion 14
targetSdkVersion 23
versionCode 200
versionName "2.0.0"

// 开启 multidex
multiDexEnabled true
// Proguard 的方式手动加入要放到 Main.dex 中的类
multiDexKeepProguard file( "keep_in_main_dex.txt")
}

// 构建类型
buildTypes {
// release {
// // 不显示 Log
// buildConfigField "boolean", "LOG_DEBUG", "false"
// // 是否进行混淆
// minifyEnabled false
// proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
// signingConfig signingConfigs.release
// applicationVariants.all { variant ->
// variant.outputs.each { output ->
// def outputFile = output.outputFile
// if (outputFile != null && outputFile.name.endsWith('.apk')) {
// // 输出 apk 名称为 ddmall1.0.1_6.apk boohee_v1.0_2015-01-15_wandoujia.apk
// def fileName = "tinker${defaultConfig.versionName}_${releaseTime()}_${variant.productFlavors[0].name}.apk"
// output.outputFile = new File(outputFile.parent, fileName)
// }
// }
// }
// }
//
// debug {
// debuggable true
// minifyEnabled false
// signingConfig signingConfigs.debug
// }
release {
// 是否进行混淆
minifyEnabled false
signingConfig signingConfigs.release
proguardFiles getDefaultProguardFile( 'proguard-android.txt'), 'proguard-rules.pro'
}
debug {
debuggable true
minifyEnabled false
signingConfig signingConfigs.debug
}
}

sourceSets {
main {
jniLibs. srcDirs = [ 'libs']
java. srcDirs = [ 'src/main/java']
}
}

repositories {
flatDir {
dirs 'libs'
}
maven { url "https://jitpack.io" }
}

// // 多渠道打包
// productFlavors {
// "Dianda" {}
//// "Tencent" {}
// }
//
// productFlavors.all { flavor ->
// flavor.manifestPlaceholders = [CHANNEL_VALUE: name]
// }


lintOptions {
checkReleaseBuilds false
abortOnError false
}

splits {
abi {
enable true
reset()
include 'armeabi' //,'x86'//, 'x86_64', 'arm64-v8a', 'armeabi-v7a'
universalApk false
}
}
sourceSets. main {
jniLibs.srcDirs = [ 'libs']
}
}

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
* 打一个补丁包需要改哪些东西?
修复 bug 的类、修改资源
修改 oldApk 配置
修改 tinkerId
*/
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-1223-13-43-58.apk"
// proguard mapping file to build patch apk
tinkerApplyMappingPath = "${bakPath} /app-release-1223-13-43-58-mapping.txt"
// resource R.txt to build patch apk, must input if there is resource changed
tinkerApplyResourcePath = "${bakPath} /app-release-1223-13-43-58-R.txt"

// Todo 需要修改
tinkerBuildFlavorDirectory = "${bakPath} /app-release-1223-13-43-58"
}

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 签名是否一致

// 编译相关配置项
// Todo 需要修改 tinkerId 每次打补丁时 tinkerId 跟打基准包时得 tinkerId 不能一样
buildConfig {
applyMapping = getApplyMappingPath() // 可选,设置 mapping 文件,建议保持旧 apk proguard 混淆方式
applyResourceMapping = getApplyResourceMappingPath() // 可选,设置 R.txt 文件,通过旧 apk 文件保持 ResId 的分配
tinkerId = "bugbugbug_v2.0.0gggggg"
}

// dex 相关配置项
dex {
dexMode = "jar" // 可选,默认为 jar
usePreGeneratedPatchDex = true // 可选,默认为 false
pattern = [ "classes*.dex",
"assets/secondary-dex-?.jar"]
// Todo 需要修改 自己的包名
loader = [ "com.tencent.tinker.loader.*",
"com.tinker.ddinfo.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"
}

}
}
}
}
}


*这里要注意 需要修改tinkerId每次打补丁时tinkerId跟打基准包时得tinkerId不能一样

关于使用腾讯 Bugly 平台 Tinker开源热修复框架的 项目集成关于使用腾讯 Bugly 平台 Tinker开源热修复框架的 项目集成关于使用腾讯 Bugly 平台 Tinker开源热修复框架的 项目集成

第三步:
package com.tinker.ddinfo ;

import com.tencent.tinker.loader.app.TinkerApplication ;
import com.tencent.tinker.loader.shareutil.ShareConstants ;

/**
 *  自定义 Application.
 *
 *  注意:这个类集成 TinkerApplication 类,这里面不做任何操作,所有 Application 的代码都会放到 ApplicationLike 继承类当中 <br/>
  <pre>
  参数解析:
  参数 1 int tinkerFlags  表示 Tinker 支持的类型  dex only library only or all suuport default: TINKER_ENABLE_ALL
 *  参数 2 String delegateClassName Application 代理类 这里填写你自定义的 ApplicationLike
 *  参数 3 String loaderClassName  Tinker 的加载器,使用默认即可
  参数 4 boolean tinkerLoadVerifyFlag  加载 dex 或者 lib 是否验证 md5 ,默认为 false
 *  </pre>
  @author  wenjiewu
 *  @since  2016/11/15
 */
public class SampleApplication  extends TinkerApplication {
    public SampleApplication() {
        super(ShareConstants. TINKER_ENABLE_ALL "com.tinker.ddinfo.SampleApplicationLike" ,//修改成自己的包名
                "com.tencent.tinker.loader.TinkerLoader" , false) ;
    }
}

第四步: 自定义ApplicationLike
package com.tinker.ddinfo;

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;
import com.tinker.ddinfo.utils.ExampleConfig;

/**
 * 自定义ApplicationLike.
 *
 * 注意:这个类是Application的代理类,以前所有在Application的实现必须要全部拷贝到这里<br/>
 *
 * @author wenjiewu
 * @since 2016/11/7
 */
public class SampleApplicationLike extends DefaultApplicationLike {

    public static final String TAG "Tinker.SampleApplicationLike";

    public SampleApplicationLike(Application application, int tinkerFlags,
            boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime,
            long applicationStartMillisTimeIntent tinkerResultIntentResources[] resources,
            ClassLoader[] classLoaderAssetManager[] assetManager) {
        super(applicationtinkerFlagstinkerLoadVerifyFlagapplicationStartElapsedTime,
                applicationStartMillisTimetinkerResultIntentresourcesclassLoader,
                assetManager);
    }


    @Override
    public void onCreate() {
        super.onCreate();
        // TODO: 这里进行Bugly初始化
       // 设置开发设备
        Bugly.setIsDevelopmentDevice(getApplication(), true);
        // 这里实现SDK初始化,appId替换成你的在Bugly平台申请的appId
        Bugly.init(getApplication()ExampleConfig.BUGLY_APP_ID, 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);

        // TODO: 安装tinker
        Beta.installTinker(this);
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    public void registerActivityLifecycleCallback(Application.ActivityLifecycleCallbacks callbacks) {
        getApplication().registerActivityLifecycleCallbacks(callbacks);
    }

}
第五步:Androidmanifest 文件
关于使用腾讯 Bugly 平台 Tinker开源热修复框架的 项目集成关于使用腾讯 Bugly 平台 Tinker开源热修复框架的 项目集成关于使用腾讯 Bugly 平台 Tinker开源热修复框架的 项目集成
第六步:MainActivity 这个类是为了配合显示 修改bug 
关于使用腾讯 Bugly 平台 Tinker开源热修复框架的 项目集成关于使用腾讯 Bugly 平台 Tinker开源热修复框架的 项目集成
关于使用腾讯 Bugly 平台 Tinker开源热修复框架的 项目集成
第七步:根据命令打基准包 (就是apk包) 在bakAPK 目录下
关于使用腾讯 Bugly 平台 Tinker开源热修复框架的 项目集成关于使用腾讯 Bugly 平台 Tinker开源热修复框架的 项目集成关于使用腾讯 Bugly 平台 Tinker开源热修复框架的 项目集成
第八步:这时要注意打完基准包后 ,需要修改bug 我在mainActivity 里面修改了一行Toast代码
关于使用腾讯 Bugly 平台 Tinker开源热修复框架的 项目集成关于使用腾讯 Bugly 平台 Tinker开源热修复框架的 项目集成关于使用腾讯 Bugly 平台 Tinker开源热修复框架的 项目集成
然后修改 app 下面的build.gradle  文件
关于使用腾讯 Bugly 平台 Tinker开源热修复框架的 项目集成关于使用腾讯 Bugly 平台 Tinker开源热修复框架的 项目集成关于使用腾讯 Bugly 平台 Tinker开源热修复框架的 项目集成
记得需要修改 tinkerId 
关于使用腾讯 Bugly 平台 Tinker开源热修复框架的 项目集成关于使用腾讯 Bugly 平台 Tinker开源热修复框架的 项目集成关于使用腾讯 Bugly 平台 Tinker开源热修复框架的 项目集成
第九步:执行tinkerPatchRelease 命令 打补丁包
关于使用腾讯 Bugly 平台 Tinker开源热修复框架的 项目集成关于使用腾讯 Bugly 平台 Tinker开源热修复框架的 项目集成关于使用腾讯 Bugly 平台 Tinker开源热修复框架的 项目集成
第十步 上传 到腾讯Bugly 平台。
关于使用腾讯 Bugly 平台 Tinker开源热修复框架的 项目集成关于使用腾讯 Bugly 平台 Tinker开源热修复框架的 项目集成关于使用腾讯 Bugly 平台 Tinker开源热修复框架的 项目集成

关于使用腾讯 Bugly 平台 Tinker开源热修复框架的 项目集成
关于使用腾讯 Bugly 平台 Tinker开源热修复框架的 项目集成
需要注意的是 在手机应用上 需要 冷启动 ,杀掉进程 重新 进入应用才会成功。 有时候需要等一两分钟


最后附上 demo 下载链接

关于使用腾讯 Bugly 平台 Tinker开源热修复框架的 项目集成