前言
前面我们讲解了AndFix的使用,这篇我们来讲解下微信的Tinker热修复,相比AndFix,Tinker的功能更加全面,更主要的是他支持gradle。他不仅做到了热修复更实现了“热更新”。既然他这么强大,下面我们就来了解他是如何使用的。
命令行生成补丁文件
在学习AndFix时由于它不自持Gradle,所以我们在生成补丁文件时是需要命令行去生成的。然而Tinker不仅支持Gradle同时也支持命令行生成补丁文件。不过在实际开发中,我们往往是使用Gradle去生成补丁文件,同时去配置一些需要的参数与属性。不过既然我们想详细了解它那么我们还是讲解下命令行生成补丁文件。
建议:无论学习什么技术,以官方文档为主,教程文章为辅。这样会好一些。
- 引入依赖
·
//注解库 用于生成application类 provided编译不打包
provided('com.tencent.tinker:tinker-android-anno:1.7.7'){ changing = true }
//是否将依赖关系标记为正在改变
//tinker的核心库 compile编译并打包
compile('com.tencent.tinker:tinker-android-lib:1.7.7'){ changing = true }
命令行相对简单。首先我们要引入两个依赖。
-
创建ApplicationLike代理类
这里我们创建TinkerManager来实现对Tinker的管理。
TinkerManager:
/**
* 功能 :Tinker管理类
*/
public class TinkerManager {
private static boolean isInstalled = false;//是否已经初始化标志位
private static ApplicationLike mApplicationLike;
/**
* 完成Tinker初始化
*
* @param applicationLike
*/
public static void installedTinker(ApplicationLike applicationLike) {
mApplicationLike = applicationLike;
if (isInstalled) {
return;
}
TinkerInstaller.install(mApplicationLike);
isInstalled = true;
}
/**
* 完成patch文件的加载
*
* @param path 补丁文件路径
*/
public static void loadPatch(String path) {
if (Tinker.isTinkerInstalled()) {//是否已经安装过
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), path);
}
}
/**
* 利用Tinker代理Application 获取应用全局的上下文
* @return 全局的上下文
*/
private static Context getApplicationContext() {
if (mApplicationLike != null)
return mApplicationLike.getApplication().getApplicationContext();
return null;
}
}
这里我们不是自己创建Application。而是使用Tinker为我们提供的ApplicationLike(也可以继承DefaultApplicationLike),作用已经有注释了。同时必须实现它的构造方法。并且我们重写了onBaseContextAttached这个方法并在里面初始化Tinker。完成这个后我们需要同步,然后就会生成MyTinkerApplication这个类(同步后没有出现这个类可以rebuild项目)。代码如下:
`
/**
* 功能 :ApplicationLike为Tinker生成Context对象 官方建议 而不是继承我们自己的Application
* 作用 :使用这个ApplicationLike这个类作为Application的委托代理是因为,Tinker需要监听Application
* 的生命周期并针对不同的生命周期来做相应的初始化与处理,这样就减少使用者需要自己处理。
*/
@DefaultLifeCycle(application = ".MyTinkerApplication" ,
flags = ShareConstants.TINKER_ENABLE_ALL,
loadVerifyFlag = false)//都是官方要求这么写的
public class CustomTinkerLike extends ApplicationLike {
public CustomTinkerLike(Application application,
int tinkerFlags,
boolean tinkerLoadVerifyFlag,
long applicationStartElapsedTime,
long applicationStartMillisTime,
Intent tinkerResultIntent) {
super(application,
tinkerFlags,
tinkerLoadVerifyFlag,
applicationStartElapsedTime,
applicationStartMillisTime,
tinkerResultIntent);
}
@Override
public void onBaseContextAttached(Context base) {
super.onBaseContextAttached(base);
TinkerManager.installedTinker(this);
}
}
3.在Manifest.xml中配置
·
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.ggxiaozhi.tinker">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:name=".tinker.MyTinkerApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
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>
<!-- 这个标签开判断我们生成的patch的.apk文件中的tinker_id_XXX
与我们的版本号tinker_id_XXX比较。相同合法,不同则不会进行更新 -->
<meta-data
android:name="TINKER_ID"
android:value="tinker_id_19940208"/>
</application>
</manifest>
首先我们加上必要的权限。然设置我们的MyTinkerApplication。同时我们还需要配置TINKER_ID这个属性,value值的数字部分一般为我们的versionCode。
-
生成差异apk文件
在完成配置后我们需要生成一个old.apk(也就是需要修复的apk)。代码如下:
MainActivity.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.example.ggxiaozhi.tinker.MainActivity">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="loadPatch"
android:text="修复BUG"/>
</LinearLayout>
MainActivity:
public class MainActivity extends AppCompatActivity {
private static final String FILE_END = ".apk";//文件后缀
private String FILEDIR;//文件路径
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// /storage/emulated/0/Android/data/com.example.ggxiaozhi.tinker/cache/tpatch/
FILEDIR = getExternalCacheDir().getAbsolutePath() + "/tpatch/";
//创建路径对应的文件夹
File file = new File(FILEDIR);
if (!file.exists())
file.mkdir();
}
public void loadPatch(View view) {
TinkerManager.loadPatch(getPatchName());
}
public String getPatchName() {
return FILEDIR.concat("tinker").concat(FILE_END);
}
}
这是old.apk中的代码。布局与代码也非常简单就是创建补丁文件的路径,在点击按钮时加载补丁文件。然后我就开始打包带签名文件的old.apk。这里我就不带大家打包了。打包完成后,我们修改下布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.example.ggxiaozhi.tinker.MainActivity">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="loadPatch"
android:text="修复BUG"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="测试按钮"/>
</LinearLayout>
这里我们其他代码不动,只是增加了一个按钮。同时我们在打包一个新的new.apk文件出来。并将两个文件和签名文件。同时copy到命令行工具中。
-
命令行生成补丁文件
首先我们利用Tinker官方为我们提供的命令行工具目录如下:
将上面我们生成的两个apk文件重命名并将签名文件copy到该目录下。(注意.keystore是eclipse的签名文件.jks是AndroidStudio的签名文件,可以直接修改后缀名,并不影响)然后我们输入一下命令:
java -jar tinker-patch-cli.jar -old old.apk -new new.apk -config tinker_config.xml -out output_path
生成补丁文件。具体如下:
output为我们补丁文件的输出文件夹,不存在会自动创建。输入完命令后output文件夹如下:
patch_signed.apk文件就是我们的补丁文件。然后我们安装old.apk并将这个补丁文件通过命令或是拷贝我们之前创建的指定文件下并重命名成我们代码中写的tinker.apk。这样点击按钮就会完成修复。
注意,在点击后会杀到当前进程,需要重新进入后才能看到效果。官方建议我们去监听手机的广播,比如锁屏的广播,点击HOME键等。来去重新启动,这个问题后面我们再去优化
gradle生成补丁文件
文章开始我们就说过在实际中,我们是通常是以gradle生成补丁文件较多。当然网上也有很多配置教程,基本上大同小异。下面我们来在上面代码的基础上修改,来完成gradle生成补丁文件。首先我们先修改下Manifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.ggxiaozhi.tinker">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
...
<!-- 这个标签开判断我们生成的patch的.apk文件中的tinker_id_
与我们的版本号tinker_id_比较。相同合法,不同则不会进行更新 -->
<!--<meta-data
android:name="TINKER_ID"
android:value="tinker_id_19940208"/>-->
</manifest>
这里我们将tinker_id注释掉,因为我们会在gradle中去配置。然后在最外面的gradle中添加插件。
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.3'
//加入Tinker的插件 里面包含gradle脚本
classpath "com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
其他不同修改,只需加上插件即可。然后我们就可以配置gradle了。关于gradle配置网上有很多,基本上都懂小异。我把我的gradler配置粘贴出来,供大家参考:
apply plugin: 'com.android.application'
/*================================常量块中的引用常量====================================*/
def javaVersion=JavaVersion.VERSION_1_7
//这个目录是基于项目的目录:Tinker/app/build/bakApk目录下存放oldApk
//buildDir : Tinker/app/build/
def bakPath = file("${buildDir}/bakApk/")//指定基准文件(oldApk)存放位置
android {
compileSdkVersion 25
buildToolsVersion "25.0.3"
defaultConfig {
applicationId "com.example.ggxiaozhi.tinker"
minSdkVersion 19
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
multiDexEnabled true
}
//排除目录下不需要编译的包
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
//java版本
compileOptions {
sourceCompatibility javaVersion
targetCompatibility javaVersion
}
//建议 recommend Tinker相关配置
dexOptions {
//启动矩形模式
jumboMode = true
}
signingConfigs {
release {
try {
storeFile file("release.jks")//目录位置app/release.jks
storePassword "gg199402"
keyAlias "gg199402"
keyPassword "gg199402"
} catch (ex) {
throw new InvalidUserDataException(ex.toString())
}
}
}
buildTypes {
release {
//是否进行混淆
minifyEnabled true
// 混淆文件的位置
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}
<!-- //真正的多渠道脚本支持
productFlavors {
googleplayer {
manifestPlaceholders = [UMENG_CHANNEL_VALUE: "googleplayer"]
}
baidu {
manifestPlaceholders = [UMENG_CHANNEL_VALUE: "baidu"]
}
productFlavors.all {
flavor -> flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name]
}
}-->
}
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'
})
//可选,用于生成application类 provided编译不打包
provided("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
//是否将依赖关系标记为正在改变
//tinker的核心库 compile编译并打包
compile("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
compile "com.android.support:multidex:1.0.1"//分包
compile 'com.android.support:appcompat-v7:25.3.1'
compile 'com.android.support.constraint:constraint-layout:1.0.2'
testCompile 'junit:junit:4.12'
}
ext {
tinkerEnabled = true //是否启用Tinker的标志位
tinkerOldApkPath = "${bakPath}/"//oldApk 文件路径
tinkerID = "1.0"//与版本号一致
tinkerApplyMappingPath = "${bakPath}/" //混淆文件路径
tinkerApplyResourcePath = "${bakPath}/" //资源路径
<!-- tinkerBuildFlavorDirectory = "${bakPath}/" //多渠道路径-->
}
/*================================方法实现模块====================================*/
def getOldApkPath() {
return ext.tinkerOldApkPath
}
def getApplyMappingPath() {
return ext.tinkerApplyMappingPath
}
def getApplyResourceMappingPath() {
return ext.tinkerApplyResourcePath
}
def getTinkerIdValue() {
return ext.tinkerID
}
def buildWithTinker() {
return ext.tinkerEnabled
}
<!--def getTinkerBuildFlavorDirectory(){
return ext.tinkerBuildFlavorDirectory
}
-->
if (buildWithTinker()) {
//启用Tinker 引入相关Gradle方法
apply plugin: 'com.tencent.tinker.patch'
//所有Tinker相关参数的配置
tinkerPatch {
/*================================基本配置====================================*/
//指定old apk(即上一个版本的Apk) 的文件路径
oldApk = getOldApkPath()
//是否忽略Tinker在产生patch文件时的错误警告并中断编译 false 不忽略 这样可以在生成patch文件时查看错误 具体哪些错误类型查考文档
ignoreWarning = false
//patch是否需要签名 true为需要 防止恶意串改
useSign = true
//是否启用tinker
tinkerEnable = buildWithTinker()
/*================================build配置====================================*/
buildConfig {
//指定old apk打包时所使用的混淆文件 (因为patch文件也是需要混淆的 所以必须要与Apk的打包混淆文件一致)
applyMapping = getApplyMappingPath()
//指定old apk的资源文件 希望new apk与其保持一致(R.txt 文件保持ResId的分配)
applyResourceMapping = getApplyResourceMappingPath()
//指定TinkerID patch文件的唯一标识符 要与新旧Apk一致
tinkerId = getTinkerIdValue()
//通常为false true会根据dex分包动态编译patch文件
keepDexApply = false
}
/*================================dex相关配置====================================*/
dex {
//Tinker提供两种模式jar、raw
//jar 适配到了api=14以下 而raw只能再14以上
//jar模式下 Tinker会对dex文件压缩成jar文件 在对jar进行处理
//raw模式下 Tinker直接对dex进行处理
//使用jar文件体积相对会小一些 在实际开发中用jar模式较多
dexMode = "jar"
//指定dex目录 "assets/secondary-dex-?.jar"为Tinker官方Demo中建议参数
//在没有分包的情况下 "classes*.dex" 会匹配到应用中的所有dex文件 分包会是classes1,classes2....
pattern = ["classes*.dex", "assets/secondary-dex-?.jar"]
//制定patch文件用到类
loader = ["com.example.ggxiaozhi.tinker.tinker.MyTinkerApplication"]
}
/*================================Tinker关于jar与.so文件的替换相关配置====================================*/
lib {
pattern = ["libs/*/*.so"]
}
/*================================Tinker关于资源文件替换相关配置====================================*/
res {
//指定Tinker可以修改的资源文件路径
// resources.arcs :AndroidReSourCe也就是与Android资源相关的一种文件格式。
// 具体角色是提供资源ID到资源文件路径的映射关系,
// 具体来说就是R.layout.activity_main(0x7f030000)到res/layout/activity_main.xml的映射关系
// 其中R.layout.activity_main就是应用开发过程中所使用的资源ID
pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
//在编译时会忽略该文件的新增、删除与修改 即使修改了文件 也不会patch文件生效
ignoreChange = ["assets/sample_meta.txt"]
//对于修改的资源,如果大于largeModSize,我们将使用bsdiff算法。
// 默认大小为100kb
largeModSize = 100
}
/*=============附加说明字段 配置 说明本次Patch文件的相关信息 非必须 packageConfig(官方:用于生成补丁包中的'package_meta.txt'文件)=================*/
packageConfig {
/*configField("key","value") 键值对 用于说明 当客户端使用patch文件修复成功 可以通过代码获取下面patch相关信息*/
configField("patchMessage", "fix the version's bugs")
configField("patchVersion", "1.0")
}
//sevenZip ......
sevenZip {
/**
* 例如"com.tencent.mm:SevenZip:1.1.10",将自动根据机器属性获得对应的7za运行文件,推荐使用
*/
zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
}
}
/*================================备份脚本 用来将生成的APK的制定文件备份到制定目录====================================*/
//多渠道相关遍历
List<String> flavors = new ArrayList<>();
project.android.productFlavors.each { flavor ->
flavors.add(flavor.name)
}
//如果是多渠道 则size()>0 为true
boolean hasFlavors = flavors.size() > 0
/**
* bak apk and mapping 备份pak与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}"
//destPath为备份的目录 没有没有多渠道打包那么hasFlavors为false destPath=bakPath bakPath即最上面定义的基础目录
def destPath = hasFlavors ? file("${bakPath}/${project.name}-${date}/${variant.flavorName}") : bakPath
from variant.outputs.outputFile
into destPath
//备份.apk文件
rename { String fileName ->
fileName.replace("${fileNamePrefix}.apk", "${newFileNamePrefix}.apk")
}
from "${buildDir}/outputs/mapping/${variant.dirName}/mapping.txt"
into destPath
//备份mapping.txt文件
rename { String fileName ->
fileName.replace("mapping.txt", "${newFileNamePrefix}-mapping.txt")
}
from "${buildDir}/intermediates/symbols/${variant.dirName}/R.txt"
into destPath
//备份R.txt文件 即用于映射的资源ID
rename { String fileName ->
fileName.replace("R.txt", "${newFileNamePrefix}-R.txt")
}
}
}
}
}
}
/* Tinker多渠道打包文件拼凑脚本*/
<!-- 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"
}
}
}
//Debug签名多渠道打包基本
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"
}
}
}
}
}-->
}
这里有关多渠道打包的部分我先注释掉,我们先从简单看起。里面注释应该是比较详细的了,在使用中这些配置也基本满足需求。关于参数与配置也可以参考官方文档。sample中的app/build.gradle以及gradle参数详解。
做完这些Tinker的gradle接入就完成了。还是之前的代码我们先打一个包含一个button的带签名的正式包。
首先我们点击1.生成基准(oldApk)签名包。2.是用来生成补丁文件的。然后我们修改代码,在加入一个Button,也可以同时给加上点击事件Toast。生成apk文件后目录如下:
首先我们在app/build/outputs/apk/app-release.apk生成签名文件apk,并备份到在app/build/bakApk/下,并以时间重命名文件。这三个文件分别是基准包(oldApk)、混淆文件、资源文件。然后我们分别将这个文件名写入到我们的gradle中,如下:
ext {
tinkerEnabled = true //是否启用Tinker的标志位
tinkerOldApkPath = "${bakPath}/app-release-0201-16-15-06.apk"//oldApk 文件路径
tinkerID = "1.0"//与版本号一致
tinkerApplyMappingPath = "${bakPath}/app-release-0201-16-15-06-mapping.txt" //混淆文件路径
tinkerApplyResourcePath = "${bakPath}/app-release-0201-16-15-06-R.txt" //资源路径
}
只需要修改ext部分其他不变。然后我们点击2.部分生成补丁文件。补丁文件目录文件如下:
目录中的参数作用,可以参考下表:
然后我们就将基准包安装到手机中,并将补丁文件copy到我们代码中指定的文件夹下并重命名。就可以完成动态更新。我亲测有效。所以就不发动图了。
结语
现在Tinker的版本已经更新到了1.9.2。相对与本文的1.7.7最主要的改动就是支持加固同时也进行了一些优化,比如支持Android8.0 等。由于最新的版本我要使用所以就没有去以最新版本去分析。大家有需要的可以在学习本系列后具体了解下。下篇我们来讲解Gradle生成补丁文件的扩展和优化以及从源码查看流程分析。