自2016年底Android Studio3.0版本退出以来,Android提出了InstantRun热修复方案,基于这种机制,各种热修复框架竞相涌现,国内的软件大厂纷纷开发了自己的热修复框架。对于热修复的更多介绍大家可以通过下面的文章来了解:全面了解Android热修复技术。
这些框架主要支持的功能如下:
这张图漏掉了阿里的Spofix,该框架可以及时更新,由于目前大多数的热修复框架,缺点是收费,可以通过下面文章来详细了解:阿里第三代非侵入式热修复Sophix
本篇要讲的是如何接入微信的热修复框架Tinker,官网接入资料:Tinker接入指南
Tinker接入
目前,Tinker提供了两种接入方式,一种是基于命令行的方式,类似于AndFix的接入方式;一种就是gradle的方式。官方推荐使用gradle方式接入。
添加gradle依赖
在项目的build.gradle中,添加tinker-patch-gradle-plugin的依赖。
buildscript {
dependencies {
classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.9.1')
}
}
然后,在app的gradle文件app/build.gradle,添加tinker的库依赖以及apply tinker的gradle插件。
dependencies {
//可选,用于生成application类
provided('com.tencent.tinker:tinker-android-anno:1.9.1')
//tinker的核心库
compile('com.tencent.tinker:tinker-android-lib:1.9.1')
}
...
...
//apply tinker插件
apply plugin: 'com.tencent.tinker.patch'
完善gradle配置
添加完Tinker依赖以后,还需要在gradle文件中做以下配置。
- 开启Multidex;
- 配置签名文件,方便打包调试;
- 引入另一个gradle文件专门来对Tinker生成拆分包的配置。
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'com.tencent.tinker.patch'
android {
compileSdkVersion 27
defaultConfig {
applicationId "com.xzh.demo"
minSdkVersion 21
targetSdkVersion 27
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
dexOptions {
jumboMode = true
}
signingConfigs {
debug {
keyAlias 'alias'
keyPassword '123456'
storeFile file("../tinker.keystore")
storePassword '123456'
}
release {
keyAlias 'alias'
keyPassword '123456'
storeFile file("../tinker.keystore")
storePassword '123456'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:27.1.1'
implementation 'com.android.support.constraint:constraint-layout:1.1.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
//可选,用于生成application类
provided('com.tencent.tinker:tinker-android-anno:1.9.1')
//Tinker的核心库
compile('com.tencent.tinker:tinker-android-lib:1.9.1')
}
// 加入Tinker生成补丁包的gradle
apply from: 'tinker.gradle'
其中,buildTinker.gradle是专门为Tinker配置和生成拆分包而写的,具体可以参考官方gradle配置。
由于生成拆分包的时候会涉及到文件的读写权限,所以需要在Manifest中添加如下权限。
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
gradle参数详解
我们将原apk包称为基准apk包,tinkerPatch直接使用基准apk包与新编译出来的apk包做差异,得到最终的补丁包。gradle文件配置涉及到的常见参数包含。
- tinkerPatch:全局信息相关的配置项;
- tinkerEnable:是否打开tinker的功能;
- oldApk:基准apk包的路径,必须输入,否则会报错;
- newApk:选填,用于编译补丁apk路径。如果路径合法,即不再编译新的安装包,使用oldApk与newApk直接编译;
- outputFolder null:选填参数,设置编译输出路径。默认输出路径为build/outputs/tinkerPatch中;
- ignoreWarning:如果出现以下的情况,并且ignoreWarning为false,我们将中断编译。因为这些情况可能会导致编译出来的patch包带来风险:
- minSdkVersion小于14,但是dexMode的值为"raw";
- 新编译的安装包出现新增的四大组件(Activity, BroadcastReceiver...);
- 定义在dex.loader用于加载补丁的类不在main dex中;
- 定义在dex.loader用于加载补丁的类出现修改;
- resources.arsc改变,但没有使用applyResourceMapping编译。
- applyMapping:可选参数,在编译新的apk时候,我们希望通过保持旧apk的proguard混淆方式,从而减少补丁包的大小。这个只是推荐设置,不设置applyMapping也不会影响任何的assemble编译。
- applyResourceMapping:可选参数,在编译新的apk时候,我们希望通过旧apk的R.txt文件保持ResId的分配,这样不仅可以减少补丁包的大小,同时也避免由于ResId改变导致remote view异常。
- tinkerId:在运行过程中,我们需要验证基准apk包的tinkerId是否等于补丁包的tinkerId。这个是决定补丁包能运行在哪些基准包上面,一般来说我们可以使用git版本号、versionName等等。
- keepDexApply:如果我们有多个dex,编译补丁时可能会由于类的移动导致变更增多。若打开keepDexApply模式,补丁包将根据基准包的类分布来编译。
- isProtectedApp:是否使用加固模式,仅仅将变更的类合成补丁。注意,这种模式仅仅可以用于加固应用中。
当然,还有很多其他的参数属性,可以通过build.gradle配置属性来获取详情。
Tinker使用
自定义Tinker封装
为了方便操作了管理,我们还需要自定义对Tinker进行一些简单的封装。该类涉及的代码如下:
public class TinkerManager {
private static boolean isInstalled = false;
//ApplicationLike可以理解为Application的载体,可以当成Application去使用
private static ApplicationLike mAppLike;
private static SimplePatchListener mPatchListener;
/**
* 初始化Tinker
* @param applicationLike
*/
public static void installTinker(ApplicationLike applicationLike) {
mAppLike = applicationLike;
if (isInstalled) {
return;
}
TinkerInstaller.install(mAppLike);
isInstalled = true;
}
/**
* 初始化Tinker,带有拓展模块
* @param applicationLike
* @param md5Value 服务器下发的md5
*/
public static void installTinker(ApplicationLike applicationLike, String md5Value) {
mAppLike = applicationLike;
if (isInstalled) {
return;
}
mPatchListener = new SimplePatchListener(getApplicationContext());
mPatchListener.setCurrentMD5(md5Value);
// Load补丁包时候的监听
LoadReporter loadReporter = new DefaultLoadReporter(getApplicationContext());
// 补丁包加载时候的监听
PatchReporter patchReporter = new DefaultPatchReporter(getApplicationContext());
AbstractPatch upgradePatchProcessor = new UpgradePatch();
TinkerInstaller.install(applicationLike,
loadReporter,
patchReporter,
mPatchListener,
SimpleResultService.class,
upgradePatchProcessor);
isInstalled = true;
}
/**
* 添加补丁包路径
* @param path
*/
public static void addPatch(String path) {
if (Tinker.isTinkerInstalled()) {
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), path);
}
}
private static Context getApplicationContext() {
if (mAppLike != null) {
return mAppLike.getApplication().getApplicationContext();
}
return null;
}
}
由于Tinker默认Patch检查是没有对文件做Md5校验,所以如果需要可以重写其中进行检验相关的逻辑。
public class SimplePatchListener extends DefaultPatchListener {
private String currentMD5;
public void setCurrentMD5(String md5Value) {
this.currentMD5 = md5Value;
}
public SimplePatchListener(Context context) {
super(context);
}
@Override
protected int patchCheck(String path, String patchMd5) {
//增加patch文件的md5较验
if (!MD5Utils.isFileMD5Matched(path, currentMD5)) {
return ShareConstants.ERROR_PATCH_DISABLE;
}
return super.patchCheck(path, patchMd5);
}
}
当补丁包完成替换安装之后,删除补丁包,然后杀掉进程,我们可以根据实际情况修改修复结果操作。
public class CustomResultService extends DefaultTinkerResultService {
private static final String TAG = "Tinker.SampleResultService";
/**
* patch文件的最终安装结果,默认是安装完成后杀掉自己进程
* 此段代码主要是复制DefaultTinkerResultService的代码逻辑
*/
@Override
public void onPatchResult(PatchResult result) {
if (result == null) {
TinkerLog.e(TAG, "DefaultTinkerResultService received null result!!!!");
return;
}
TinkerLog.i(TAG, "DefaultTinkerResultService received a result:%s ", result.toString());
//first, we want to kill the recover process
TinkerServiceInternals.killTinkerPatchServiceProcess(getApplicationContext());
// if success and newPatch, it is nice to delete the raw file, and restart at once
// only main process can load an upgrade patch!
if (result.isSuccess) {
//删除patch包
deleteRawPatchFile(new File(result.rawPatchFilePath));
//杀掉自己进程,如果不需要则可以注释
if (checkIfNeedKill(result)) {
android.os.Process.killProcess(android.os.Process.myPid());
} else {
TinkerLog.i(TAG, "I have already install the newly patch version!");
}
}
}
}
不过上面的东西不做定制开发,用不到,只需要按照下面的步骤即可。
Tinker接入
正常情况下,我们会考虑在Application的onCreate函数中去初始化Tinker相关的内容,不过Tinker更推荐下面的写法。
@DefaultLifeCycle(application = ".SimpleTinkerApplication", flags = ShareConstants.TINKER_ENABLE_ALL, loadVerifyFlag = false)
public class SimpleTinkerLike extends ApplicationLike {
public SimpleTinkerLike(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);
TinkerManager.installTinker(this);
}
}
ApplicationLike,通过名字你可能会猜到,该类并非Application的子类,而是一个类似Application的类。Tinker建议编写一个ApplicationLike子类,可以理解为Application的载体,可以当成Application去使用。
注意顶部的注解:@DefaultLifeCycle。其application属性,会在编译期生成一个SimpleTinkerInApplication类。该类需要在Manifest中替换我们的默认的Application。
<application
android:name=".SimpleTinkerInApplication"
.../>
如果报错,请重新编译一下。关于这方面的内容可以查看下面的文章链接:
Android 如何编写基于编译时注解的项目
为了方便,我们在主页面按钮的点击事件,来加载放在缓存目录下的补丁包,代码如下:
public class MainActivity extends AppCompatActivity {
private String mPath;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mPath = getExternalCacheDir().getAbsolutePath() + File.separatorChar;
}
/**
* 加载Tinker补丁
*
* @param view
*/
public void Fix(View view) {
File patchFile = new File(mPath, "patch_signed.apk");
if (patchFile.exists()) {
TinkerManager.addPatch(patchFile.getAbsolutePath());
Toast.makeText(this, "File Exists,Please wait a moment ", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "File No Exists", Toast.LENGTH_SHORT).show();
}
}
}
测试
为了验证热修复的效果,我们在MainActivity中新增一个按钮,并增加一个ImageView图像。
然后,找到gradle工具栏(Android Studio的右上角),点击生成Release包,作为1.0版本的程序。
在项目的build文件夹下bakAPK(该文件夹是在tink.gradle文件中设置的)文件夹下回看到编译成功的apk文件。
将apk安装到手机上,该apk可以认为是old.apk。启动apk看到的效果如下:
2,然后在主界面添加加载图片的按钮,同时添加一个drawable文件。
public class MainActivity extends AppCompatActivity {
private String mPath;
private ImageView iv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//新增
iv = (ImageView) findViewById(R.id.iv);
mPath = getExternalCacheDir().getAbsolutePath() + File.separatorChar;
}
/**
* 加载Tinker补丁
* @param view
*/
public void Fix(View view) {
File patchFile = new File(mPath, "patch_signed.apk");
if (patchFile.exists()) {
TinkerManager.addPatch(patchFile.getAbsolutePath());
Toast.makeText(this, "File Exists,Please wait a moment ", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "File No Exists", Toast.LENGTH_SHORT).show();
}
}
/**
* 新增的按钮点击事件
* @param view
*/
public void Load(View view) {
iv.setImageResource(R.drawable.bg_content_header);
}
}
同时记得修改buildTinker.gradle的old安装包的路径,Tinker需要比对前后安装包然后生成补丁包。例如:
ext {
//开启Tinker
tinkerEnable = true
//旧的apk位置,需要我们手动指定
tinkerOldApkPath = "${bakPath}/app-2018-05-04-17-00-19"
//旧的混淆映射位置,如果开启了混淆,则需要我们手动指定
tinkerApplyMappingPath = "${bakPath}/app-2018-05-04-17-00-19"
//旧的resource位置,需要我们手动指定
tinkerApplyResourcePath = "${bakPath}/app-2018-05-04-17-00-19"
//旧的多渠道位置,需要我们手动指定
tinkerBuildFlavorDirectory = "${bakPath}/app-2018-05-04-17-00-19"
appKey = "0481b2ba9d770294"
tinkerID = "1.0"
}
找到gradle工具栏,点击Tinker生成Release补丁包,作为1.0版本的补丁。
然后将生成的Release补丁包Push到手机的缓存目录上,运行程序点击修复补丁包,稍等数秒程序会被杀掉,重启点击加载图片按钮。使用Tinker的一个缺点是修复的程序必须重启才能执行。生成的补丁包的位置如下:
生成的补丁patch_signed.apk放到手机的包名的cache文件夹下(可以使用应用宝等工具)。例如:
然后重启应用,就发现应用加载了差分包的内。
当然,本文讲解的只是本地的热修复功能,更多的时候我们会需要将差分包放到服务端,然后由服务器控制热更新。后台管理界面如下:
更多内容,请查看Android 热更新服务平台相关的介绍。
除此之外,Tinker还支持Tinker多渠道打包功能。
附:[源码](https://download.csdn.net/download/xiangzhihong8/10392663)