Tinker时微信推出的热修复框架,优点就是很稳定,可以gradle打包,缺点是这个修复不是实时的,需要重启。这是由于其实现的原理决定的,简单的说他是通过把生成的不定apk,加载进来,通过与基线apk的整合生成新的dex集合,最后生成oat文件。这只是整合成的文件而已,并没有影响ClassLoader的加载既存在的dex类。而重启之后Tinker会通过反射获取BaseDexClassloder对象,然后在获取里面的PathDexList对象中的Excument[]数据,这里面放的都是dex,这时候我们会把整合好的补丁dex插入到数据的前端,当ClassLoder进行加载使用的时候就会取到。也就是说重启是为了让整合的dex补丁插入进去。
下面来简单说下步骤:
1.加依赖
buildscript {
repositories {
google()
jcenter()
maven {
url 'https://maven.google.com/'
name 'Google'
}
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.2'
classpath("com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}") {
changing = TINKER_VERSION?.endsWith("-SNAPSHOT")
exclude group: 'com.android.tools.build', module: 'gradle'
}
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
maven {
url 'https://maven.google.com/'
name 'Google'
}
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
def is_gradle_3() {
return hasProperty('GRADLE_3') && GRADLE_3.equalsIgnoreCase('TRUE')
}
2.依赖
apply plugin: 'com.android.application'
def javaVersion = JavaVersion.VERSION_1_7
android {
compileSdkVersion 28
defaultConfig {
applicationId "com.example.liukang.mytinkerproject"
minSdkVersion 19
targetSdkVersion 28
versionCode 1
versionName "2.0"
/**
* you can use multiDex and install it in your ApplicationLifeCycle implement
*/
// multiDexEnabled true
}
compileOptions {
sourceCompatibility javaVersion
targetCompatibility javaVersion
}
//recommend
dexOptions {
jumboMode = true
}
signingConfigs {
release {
storeFile file("sign/release.keystore")
storePassword "123456"
keyAlias "123456"
keyPassword "123456"
}
debug {
storeFile file("sign/release.keystore")
storePassword "123456"
keyAlias "123456"
keyPassword "123456"
}
}
buildTypes {
release {
minifyEnabled true
signingConfig signingConfigs.release
proguardFiles getDefaultProguardFile('proguard-android.txt'), project.file('proguard-rules.pro')
}
debug {
debuggable true
minifyEnabled true
signingConfig signingConfigs.debug
proguardFiles getDefaultProguardFile('proguard-android.txt'), project.file('proguard-rules.pro')
}
}
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
}
def bakPath = file("${buildDir}/bakApk/")
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
testImplementation 'junit:junit:4.12'
implementation "com.android.support:appcompat-v7:28.0.0+"
api("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
// Maven local cannot handle transist dependencies.
implementation("com.tencent.tinker:tinker-android-loader:${TINKER_VERSION}") { changing = true }
annotationProcessor("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") {
changing = true
}
compileOnly("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
implementation "com.android.support:multidex:1.0.1"
implementation 'com.orhanobut:hawk:2.0.1'
}
/**
* 使用Tinker的一些常量配置
*/
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-0317-18-54-47.apk"
//proguard mapping file to build patch apk混淆文件
tinkerApplyMappingPath = "${bakPath}/app-release-0317-18-54-47-mapping.txt"
//resource R.txt to build patch apk, must input if there is resource changed资源路径
tinkerApplyResourcePath = "${bakPath}/app-release-0317-18-54-47-R.txt"
//only use for build all flavor, if not, just ignore this field
tinkerBuildFlavorDirectory = "${bakPath}/"
}
/**
* Tinker的全剧配置
*/
if (buildWithTinker()) {
apply plugin: 'com.tencent.tinker.patch'
tinkerPatch {
/**
* 第一次生成的基线包
*/
oldApk = getOldApkPath()
/**
* default 'false'
* 如果出现以下的情况,并且ignoreWarning为false,我们将中断编译。因为这些情况可能会导致编译出来的patch包带来风险:
1. minSdkVersion小于14,但是dexMode的值为"raw";
2. 新编译的安装包出现新增的四大组件(Activity, BroadcastReceiver...);
3. 定义在dex.loader用于加载补丁的类不在main dex中;
4. 定义在dex.loader用于加载补丁的类出现修改;
5. resources.arsc改变,但没有使用applyResourceMapping编译。
*/
ignoreWarning = false
/**
* 在运行过程中,我们需要验证基准apk包与补丁包的签名是否一致,我们是否需要为你签名。
*/
useSign = true
/**
* optional,default 'true'
* 是否打开Tinker功能
*/
tinkerEnable = buildWithTinker()
/**
* 编译相关的配置项
*/
buildConfig {
/**
* optional,default 'null'
* 在编译新的apk时候,我们希望通过保持旧apk的proguard混淆方式,
* 从而减少补丁包的大小。这个只是推荐设置,
* 不设置applyMapping也不会影响任何的assemble编译。
*/
// applyMapping = getApplyMappingPath()
/**
* optional,default 'null'
* 可选参数;在编译新的apk时候,我们希望通过旧apk的R.txt文件保持ResId的分配
* ,这样不仅可以减少补丁包的大小,同时也避免由于ResId改变导致remote view异常
*/
applyResourceMapping = getApplyResourceMappingPath()
/**
* necessary,default 'null'
* 在运行过程中,我们需要验证基准apk包的tinkerId是否等于补丁包的tinkerId。
* 这个是决定补丁包能运行在哪些基准包上面,一般来说我们可以使用git版本号
* 、versionName等等。
*/
tinkerId = getTinkerIdValue()
/**
* 如果我们有多个dex,编译补丁时可能会由于类的移动导致变更增多。
* 若打开keepDexApply模式,补丁包将根据基准包的类分布来编译
*/
keepDexApply = true
/**
* optional, default 'false'
* 是否使用加固模式,仅仅将变更的类合成补丁。注意,这种模式仅仅可以用于加固应用中。
*/
isProtectedApp = false
/**
* optional, default 'false'
* 是否支持新增非export的Activity
* <b>Notice that currently this feature is incubating and only support NON-EXPORTED Activity</b>
*/
supportHotplugComponent = false
}
dex {
/**
* optional,default 'jar'
* 只能是'raw'或者'jar'。
对于'raw'模式,我们将会保持输入dex的格式。
对于'jar'模式,我们将会把输入dex重新压缩封装到jar。
如果你的minSdkVersion小于14,你必须选择‘jar’模式,而且它更省存储空间,
但是验证md5时比'raw'模式耗时。默认我们并不会去校验md5,一般情况下选择jar模式即可。
*/
dexMode = "jar"
/**
* necessary,default '[]'
* what dexes in apk are expected to deal with tinkerPatch
* it support * or ? pattern.
*/
pattern = ["classes*.dex",
"assets/secondary-dex-?.jar"]
/**
* necessary,default '[]'
* Warning, it is very very important, loader classes can't change with patch.
* thus, they will be removed from patch dexes.
* you must put the following class into main dex.
* Simply, you should add your own application {@code tinker.sample.android.SampleApplication}
* own tinkerLoader, and the classes you use in them
*
*/
loader = [
//use sample, let BaseBuildInfo unchangeable with tinker
"tinker.sample.android.app.BaseBuildInfo"
]
}
lib {
/**
* optional,default '[]'
* 需要处理lib路径,
* 支持*、?通配符,必须使用'/'分割。与dex.pattern一致,
* 路径是相对安装包的,例如assets/...
*/
pattern = ["lib/*/*.so"]
}
res {
/**
* optional,default '[]'
* 需要处理res路径,支持*、?通配符,必须使用'/'分割。
* 与dex.pattern一致, 路径是相对安装包的,例如assets/...,务必注意的是,
* 只有满足pattern的资源才会放到合成后的资源包
*/
pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
/**
* optional,default '[]'
* 支持*、?通配符,必须使用'/'分割。若满足ignoreChange的pattern,
* 在编译时会忽略该文件的新增、删除与修改。 最极端的情况,
* ignoreChange与上面的pattern一致,即会完全忽略所有资源的修改。
ignoreChange = ["assets/sample_meta.txt"]
/**
* default 100kb
* 如果大于largeModSize,我们将使用bsdiff算法。
* 这可以降低补丁包的大小,但是会增加合成时的复杂度。默认大小为100kb
*/
largeModSize = 100
}
packageConfig {
/**
* optional,default 'TINKER_ID, TINKER_ID_VALUE' 'NEW_TINKER_ID, NEW_TINKER_ID_VALUE'
* package meta file gen. path is assets/package_meta.txt in patch file
* you can use securityCheck.getPackageProperties() in your ownPackageCheck method
* or TinkerLoadResult.getPackageConfigByName
* we will get the TINKER_ID from the old apk manifest for you automatic,
* other config files (such as patchMessage below)is not necessary
*/
configField("patchMessage", "tinker is sample to use")
/**
* just a sample case, you can use such as sdkVersion, brand, channel...
* you can parse it in the SamplePatchListener.
* Then you can use patch conditional!
*/
configField("platform", "all")
/**
* patch version via packageConfig
*/
configField("patchVersion", "1.0")
}
//or you can add config filed outside, or get meta value from old apk
//project.tinkerPatch.packageConfig.configField("test1", project.tinkerPatch.packageConfig.getMetaDataFromOldApk("Test"))
//project.tinkerPatch.packageConfig.configField("test2", "sample")
/**
* if you don't use zipArtifact or path, we just use 7za to try
*/
sevenZip {
/**
* optional,default '7za'
* the 7zip artifact path, it will use the right 7za with your platform
*/
zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
/**
* optional,default '7za'
* you can specify the 7za path yourself, it will overwrite the zipArtifact value
*/
// path = "/usr/local/bin/7za"
}
}
List<String> flavors = new ArrayList<>();
project.android.productFlavors.each { flavor ->
flavors.add(flavor.name)
}
boolean hasFlavors = flavors.size() > 0
def date = new Date().format("MMdd-HH-mm-ss")
/**
* bak apk and mapping
*/
android.applicationVariants.all { variant ->
/**
* task type, you want to bak
*/
def taskName = variant.name
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.first().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")
}
}
}
}
}
}
}
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 : android.defaultConfig.versionName
}
def buildWithTinker() {
return hasProperty("TINKER_ENABLE") ? Boolean.parseBoolean(TINKER_ENABLE) : ext.tinkerEnabled
}
def getTinkerBuildFlavorDirectory() {
return ext.tinkerBuildFlavorDirectory
}
3.创建代理的MyTinkerApplication
package com.example.liukang.mytinkerproject;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.support.multidex.MultiDex;
import com.orhanobut.hawk.Hawk;
import com.tencent.tinker.anno.DefaultLifeCycle;
import com.tencent.tinker.entry.ApplicationLike;
import com.tencent.tinker.entry.DefaultApplicationLike;
import com.tencent.tinker.loader.shareutil.ShareConstants;
@SuppressWarnings("unused")
@DefaultLifeCycle(application = ".MyTinkerApplication" ,
flags = ShareConstants.TINKER_ENABLE_ALL,
loadVerifyFlag = false)//都是官方要求这么写的
public class SampleApplicationLike extends ApplicationLike {
public SampleApplicationLike(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);
}
@Override
public void onCreate() {
super.onCreate();
Hawk.init(getApplication()).build();
}
}
4.TinkerManager对Tinker进行管理,这里面可以将重写的如DefaultResultService等一些自定义处理的东西,在这里面通过Tinker 进行初始化
package com.example.liukang.mytinkerproject;
import android.content.Context;
import com.tencent.tinker.entry.ApplicationLike;
import com.tencent.tinker.lib.patch.UpgradePatch;
import com.tencent.tinker.lib.service.DefaultTinkerResultService;
import com.tencent.tinker.lib.tinker.Tinker;
import com.tencent.tinker.lib.tinker.TinkerInstaller;
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;
}
Tinker tinker = new Tinker.Builder(applicationLike.getApplication()).build();
Tinker.create(tinker);
tinker.install(applicationLike.getTinkerResultIntent(),MyTinkerService.class, new UpgradePatch());
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;
}
}
5.清单配置 需要修改application 的name
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.liukang.mytinkerproject">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:name=".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">
<service android:name=".MyTinkerService"
android:exported="false"/>
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity></application>
</manifest>
6.生成基线
包
--》
--》对应替换
7.修改一下代码之后,在生成补丁包
===》这样便得到了
8.使用实例:
public class MainActivity extends AppCompatActivity {
private static final String TAG = "Tinker.MainActivity";
private static final String FILE_END = ".apk";//文件后缀
private String FILEDIR;//文件路径
Activity activity;
TextView showInfo;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
activity=this;
createDir();
Button loadPatchButton = findViewById(R.id.loadPatch);
loadPatchButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new PermissionHelper(activity).requestPermissions("请同意读取权限"
, new PermissionHelper.PermissionListener() {
@Override
public void doAfterGrand(String... permission) {
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), getPatchName());
}
@Override
public void doAfterDenied(String... permission) {
Toast.makeText(activity, "没有权限", Toast.LENGTH_LONG).show();
}
}, Manifest.permission.READ_EXTERNAL_STORAGE
, Manifest.permission.WRITE_EXTERNAL_STORAGE);
}
});
Button cleanPatchButton = (Button) findViewById(R.id.cleanPatch);
cleanPatchButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Tinker.with(getApplicationContext()).cleanPatch();
}
});
Button killSelfButton = (Button) findViewById(R.id.killSelf);
killSelfButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ShareTinkerInternals.killAllOtherProcess(getApplicationContext());
android.os.Process.killProcess(android.os.Process.myPid());
}
});
showInfo=findViewById(R.id.showInfo);
showInfo.setText("哈哈哈哈哈!");
}
@Override
protected void onResume() {
Log.e(TAG, "i am on onResume");
// Log.e(TAG, "i am on patch onResume");
super.onResume();
Utils.setBackground(false);
}
@Override
protected void onPause() {
super.onPause();
Utils.setBackground(true);
}
public void createDir() {
FILEDIR = Environment.getExternalStorageDirectory().getPath() + "/tpatch/";
//创建路径对应的文件夹
File file = new File(FILEDIR);
if (!file.exists())
file.mkdir();
}
public String getPatchName() {
String s = FILEDIR.concat("tinker").concat(FILE_END);
Log.e("TAG", s);
return s;
}
}