Tinker热修复之 —— Gradle接入
Posted 郭霖
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Tinker热修复之 —— Gradle接入相关的知识,希望对你有一定的参考价值。
10月17日,据路透社报道,中国智能手机制造商华为公司发布了一款新的旗舰智能手机Mate 10系列,其改进功能甚至可与苹果或三星的高端手机相媲美,而性价比更高。华为的目标是通过技术进步来凸显自己,这将使其在高端市场竞争中处于有利位置,进而提高盈利能力。
本篇来自 BigSweetee 的投稿,讲解了如何用 gradle接入热修复tinker,希望对大家有所帮助!
http://blog.csdn.net/qq_15527709/
今天研究了一天的热修复,热修复,简单的来讲就是在不需要发包的情况下,修改你线上应用的bug,接入使用后对于我这种小白来说还是很神奇的,同时也考虑了一下,要不要接入我们的项目中,这样就不用因为一个小BUG而去再次发包了,不过,就算要接入项目中,也还有很多坑需要踩,tinker有俩种接入方式,一种命令行接入,一种是gradle接入,本篇只讲 gradle接入,下篇我在补充命令行,主要用于自己做个记录,把踩得坑和感想写下来.
将这个 C/C++编译链接生成二进制文件的这个过程是谁做的?
首先,基本的配置
在 project 的 build中配置如下
dependencies {
classpath 'com.android.tools.build:gradle:2.2.2'
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
}
TINKER_VERSION 需要在 gradle.properties 中进行配置
TINKER_VERSION=1.7.7
https://github.com/Tencent/tinker/blob/master/tinker-sample-android/app/build.gradle
于是我全部 copy 了过来,不过里面的几个地方还是需要修改的 。
这里因为源码太多,有兴趣的同学可以点击下方阅读全文,去原文中进行查看学习。
主要修改了下面几个地方
buildConfigField "String", "TINKER_ID", "\"${getTinkerIdValue()}\""
官方文档默认是将git提交的记录作为think_id记录下来,这里我不需要,所以我改成下图
buildConfigField "String", "TINKER_ID", "\"1.0\""
我直接写死,需要git提交记录作为tinker_id的也可以按照官方文档推荐的。同时,这些地方也可以相应的替换掉
//废弃 /*def getTinkerIdValue() { return hasProperty("TINKER_ID") ? TINKER_ID : gitSha() }*/ tinkerId = "1.0"/*getTinkerIdValue()*/
接下来修改自己的签名文件,我这里配置的文件路径是自己电脑上面的,这个位置大家需要修改为自己的签名文件路径,也可以按照我的去生成一个签名文件,我相信这个还是很简单的。
//配置自己的签名文件,签名文件的生成和导入可以去百度,本篇不讲解 signingConfigs {
release {
try {
keyAlias 'china'
keyPassword '123456'
storeFile file('D:/work/release.jks')
storePassword '123456'
} catch (ex) {
throw new InvalidUserDataException(ex.toString())
}
}
debug {
storeFile file('D:/work/debug.jks')
keyAlias 'china'
keyPassword '123456'
storePassword '123456'
}
}
buildTypes {
release {
minifyEnabled true
signingConfig signingConfigs.release
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
debuggable true
minifyEnabled false
signingConfig signingConfigs.debug
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
官网文档没有设置 debug 的 proguardFiles getDefaultProguardFile(‘proguard-android.txt’), ‘proguard-rules.pro’ 也就是说没有应用混淆文件,这样是不会生成 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")
}
}
}
}
}
这下面主要是设置生成的文件所在的目录是什么。配置完上面之后,build就配置好了,接下来配置 application, 先上代码:
package com.anlaiye.swt.gradletest;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.support.multidex.MultiDex;
import com.tencent.tinker.anno.DefaultLifeCycle;
import com.tencent.tinker.lib.tinker.TinkerInstaller;
import com.tencent.tinker.loader.app.ApplicationLike;
import com.tencent.tinker.loader.shareutil.ShareConstants;
@DefaultLifeCycle(application = ".SimpleTinkerInApplication",
flags = ShareConstants.TINKER_ENABLE_ALL,
loadVerifyFlag =true)
public class SimpleTinkerInApplicationLike extends ApplicationLike {
public SimpleTinkerInApplicationLike(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);
MultiDex.install(base);
TinkerInstaller.install(this);
}
@Override
public void onCreate() {
super.onCreate();
}
}
这个 application 官网有提供的,也可以copy我的,ApplicationLike 并不是一个application,真正的 application 是 @DefaultLifeCycle(application = “.SimpleTinkerInApplication” 这个,所以在 androidmanifest 中配置 applica的 name:
<application android:name=".SimpleTinkerInApplication" 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>
</application>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
同时配置读取sd卡的权限,接下来测试:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="com.anlaiye.swt.gradletest.MainActivity">
<TextView android:id="@+id/tv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="111"/>
<Button android:layout_below="@+id/tv" android:onClick="loadPath" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="更新"/>
</RelativeLayout>
第一次我设置 textview 的值是111,然后设置按钮调用 onclick:
package com.anlaiye.swt.gradletest;
import android.os.Bundle;
import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Toast;
import com.tencent.tinker.lib.tinker.TinkerInstaller;
import java.io.File;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
//加载补丁
public void loadPath(View view) {
String path = Environment.getExternalStorageDirectory().getAbsolutePath()+"/patch_signed_7zip.apk";
File file = new File(path);
if (file.exists()){
Toast.makeText(this, "补丁已经存在", Toast.LENGTH_SHORT).show();
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), path);
}else {
Toast.makeText(this, "补丁已经不存在", Toast.LENGTH_SHORT).show();
}
}
}
然后我运行APP,会生成如下的目录结构:
同时界面效果
接下来做如下修改:
//老版本的文件所在的位置,大家也可以动态配置,不用每次都在这里修改 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-0313-16-49-55.apk"
//proguard mapping file to build patch apk
tinkerApplyMappingPath = "${bakPath}/app-release-0313-16-49-55-mapping.txt"
//resource R.txt to build patch apk, must input if there is resource changed
tinkerApplyResourcePath = "${bakPath}/app-release-0313-16-49-55-R.txt"
//only use for build all flavor, if not, just ignore this field
tinkerBuildFlavorDirectory = "${bakPath}/app-1018-17-32-47" }
注意这里是 release 版本,不能 debug 版本,release 版本混着来,比如 oldapk 是 debug 版本,新的 apk 是 release版本,这样是不行的,统一用 release 版本。
这里我将对应路径改为 bakapk 中第一次运行生成的apk文件名字,比如第一次生成的 apk文件名为 app-release-0313-16-49-55.apk,所以我把 tinkerOldApkPath 的路径后面的名字修改为对应的 app-release-0313-16-49-55.apk,mapping 文件和 R 文件路径也要对应修改,最后一项 tinkerBuildFlavorDirectory 可以忽略,我一直觉得这里太死板,应该像设置tinker_id 那样对这里进行动态的配置,不用每次都来修改。
接下来我把 textview 改成2222,用来和第一次作区别。然后点击:
生成目录如下
注意箭头所指的文件 ,我们在 onclick 中执行的文件名就是这个,这个文件就是我们需要的最终的文件,然后将这个文件导入SD卡的最外层目录,点击更新按钮,点击后如下 :
因为模拟器不好导入patch的apk所以直接截图了,成功。
本篇只是简单的演示了整个流程,整理了一下容易踩得坑,可以优化的地方还有很多,大家可以在这个基础上自行发挥。
需要准备的工作:
总结一下自己碰到的坑,
首先没有在gradle.properties中配置tinker_id,会提示错误tinker_id not set!!!
debug 没有配置 proguardFiles getDefaultProguardFile(‘proguard-android.txt’), ‘proguard-rules.pro’
导入生成的apk一直没有mapping文件,找了半天才发现。
最后说下热修复的大概流程
最终我们需要的是一个patch.apk文件,这个文件是通过老apk和新apk对比生成的, 具体怎么对比和生成,是tinker控制的,所以我们需要导入他的依赖包,新APK和老APK的mapping 必须是一致的,所以我们需要把老APK的mapping保存起来,方便新APK与他对比。 这样就能得到最终的patch,然后调用 sdk 的TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), path),这样就实现了热修复的目的。
http://download.csdn.net/download/qq_15527709/10017530
https://github.com/BigSweet/TinkerGradleDemo
欢迎长按下图 -> 识别图中二维码
以上是关于Tinker热修复之 —— Gradle接入的主要内容,如果未能解决你的问题,请参考以下文章