本文主要是介绍Android热修复(3):Tinker的使用,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
github官网
Tinker的基本介绍
Tinker是微信官方的Android热补丁解决方案,它支持动态下发代码、So库以及资源,让应用能够在不需要重新安装的情况下实现更新。当然,你也可以使用Tinker来更新你的插件。
它主要包括以下几个部分:
1.gradle编译插件: tinker-patch-gradle-plugin
2.核心sdk库: tinker-android-lib
3.非gradle编译用户的命令行版本: tinker-patch-cli.jar
为什么使用Tinker
当前市面的热补丁方案有很多,其中比较出名的有阿里的AndFix、美团的Robust以及QZone的超级补丁方案。但它们都存在无法解决的问题,这也是正是我们推出Tinker的原因。
总的来说:
1.AndFix作为native解决方案,首先面临的是稳定性与兼容性问题,更重要的是它无法实现类替换,它是需要大量额外的开发成本的;
2.Robust兼容性与成功率较高,但是它与AndFix一样,无法新增变量与类只能用做的bugFix方案;
3.Qzone方案可以做到发布产品功能,但是它主要问题是插桩带来Dalvik的性能问题,以及为了解决Art下内存地址问题而导致补丁包急速增大的。
特别是在Android N之后,由于混合编译的inline策略修改,对于市面上的各种方案都不太容易解决。而Tinker热补丁方案不仅支持类、So以及资源的替换,它还是2.X-8.X(1.9.0以上支持8.X)的全平台支持。利用Tinker我们不仅可以用做bugfix,甚至可以替代功能的发布。Tinker已运行在微信的数亿Android设备上,那么为什么你不使用Tinker呢?
Tinker的已知问题
由于原理与系统限制,Tinker有以下已知问题:
1.Tinker不支持修改AndroidManifest.xml,Tinker不支持新增四大组件(1.9.0支持新增非export的Activity);
2.由于Google Play的开发者条款限制,不建议在GP渠道动态更新代码;
3.在Android N上,补丁对应用启动时间有轻微的影响;
4.不支持部分三星android-21机型,加载补丁时会主动抛出"TinkerRuntimeException:checkDexInstall failed";
5.对于资源替换,不支持修改remoteView。例如transition动画,notification icon以及桌面图标。
尽管Tinker有着这些“小缺点”,但也丝毫不影响Tinker在国内众多热修复方案中的地位,一方面Tinker是开源的(这意味着Tinker本身免费),另一方面则是Tinker已运行在微信的数亿Android设备上(说明该方案相当稳定)。下面开始进行对Tinker的集成与使用。
Tinker 接入指南
Tinker提供了命令行接入和gradle接入2种方式,gradle是推荐的接入方式。
官方文档接入指南
接入方式1:命令行接入
添加gradle依赖:Gradle版本大于2.3
//tinker的核心库implementation('com.tencent.tinker:tinker-android-lib:1.9.1') { changing = true }//可选,用于生成application类annotationProcessor("com.tencent.tinker:tinker-android-anno:1.9.1") { changing = true }compileOnly('com.tencent.tinker:tinker-android-anno:1.9.1') { changing = true }//Tinker需要使用到MulitDeximplementation 'com.android.support:multidex:1.0.1'
对Tinker进行封装
/*** Created by xiaoyehai on 2018/11/27 0027.* 对Tinker进行封装*/public class TinkerManager {//是否初始化Tinkerprivate static boolean isInstalled = false;private static ApplicationLike mApplicationLike;/*** 初始化Tinker** @param applicationLike*/public static void inatallTinker(ApplicationLike applicationLike) {mApplicationLike = applicationLike;if (isInstalled) {return;}TinkerInstaller.install(mApplicationLike); //Tinker初始化isInstalled = true;}/*** 加载补丁文件** @param path*/public static void loadPatach(String path) {if (Tinker.isTinkerInstalled()) {TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), path);}}/*** 通过ApplicationLike获取Context** @return*/private static Context getApplicationContext() {if (mApplicationLike != null) {return mApplicationLike.getApplication().getApplicationContext();}return null;}
}
编写Application的代理类
程序启动时会加载默认的Application类,这导致补丁包无法对它做修改,Application无法动态修复,所以需要改代理类。
Tinker表示,Application无法动态修复,所以有两种选择:
1.使用「继承TinkerApplication + DefaultApplicationLike」。
2.使用「DefaultLifeCycle注解 + DefaultApplicationLike」。
第1种方式感觉比较鸡肋,这里使用第2种(Tinker官方推荐的方式):「DefaultLifeCycle注解 + DefaultApplicationLike」,DefaultLifeCycle注解生成Application,下面就用第2种方式来编写Application的代理类:
/*** 使用DefaultLifeCycle注解生成Application(这种方式是Tinker官方推荐的)* <p>* Application的代理类:Tinker表示,Application无法动态修复,所以需要改代理类。* <p>* 程序启动时会加载默认的Application类,这导致补丁包无法对它做修改。所以Tinker官方说不建议自己去实现Application,而是由Tinker自动生成。* 即需要创建一个TinkerApplicationLike类继承ApplicationLike,然后将我们自己的MyApplication中所有逻辑放在TinkerApplicationLike中的* onCreate()中或onBaseContextAttached()方法中。* Created by xiaoyehai on 2018/11/27 0027.*/@DefaultLifeCycle(application = "com.xiaoyehai.tinker_demo.MyApplication", // application类名。只能用字符串,这个MyApplication文件是不存在的,但可以在AndroidManifest.xml的application标签上使用(name)flags = ShareConstants.TINKER_ENABLE_ALL, // tinkerFlagsloaderClass = "com.tencent.tinker.loader.TinkerLoader",//loaderClassName, 我们这里使用默认即可!(可不写)loadVerifyFlag = false) //tinkerLoadVerifyFlag
public class TinkerApplicationLike extends DefaultApplicationLike {public TinkerApplicationLike(Application application, int tinkerFlags,boolean tinkerLoadVerifyFlag,long applicationStartElapsedTime,long applicationStartMillisTime,Intent tinkerResultIntent) {super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);}@Overridepublic void onCreate() {super.onCreate();//把项目中在自定义Application的操作移到TinkerApplicationLike的onCreate()或onBaseContextAttached()方法中。}@Overridepublic void onBaseContextAttached(Context base) {super.onBaseContextAttached(base);initTinker(base);// 可以将之前自定义的Application中onCreate()方法所执行的操作搬到这里...}private void initTinker(Context base) {//使应用支持分包, tinker需要你开启MultiDexMultiDex.install(base);//初始化TinkerTinkerManager.inatallTinker(this);}
}
重新编译后自动生成的MyApplication
package com.xiaoyehai.tinker_demo;import com.tencent.tinker.loader.app.TinkerApplication;/*** Generated application for tinker life cycle*/
public class MyApplication extends TinkerApplication {public MyApplication() {super(7,"com.xiaoyehai.tinker_demo.tinker.TinkerApplicationLike","com.tencent.tinker.loader.TinkerLoader", false);}}
然后,把项目中在自定义Application的操作移到TinkerApplicationLike的onCreate()或onBaseContextAttached()方法中。
清单文件中注册:
<applicationandroid:name="com.lqr.tinker.MyApplication"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">...</application>
注意:
此时name属性会报红,因为项目源码中根本不存在MyApplication.java文件,但不必担心,因为它是动态生成的,Build一下项目就好了,不管它也无所谓。
在编译时我们需要将TINKER_ID插入到AndroidManifest.xml中。例如
<meta-data android:name="TINKER_ID" android:value="tinker_id_b168b32"/>
上面步骤都准备好了之后,来写案例
1.先准备一个release版本的apk:old.apk
public class MainActivity extends AppCompatActivity {//补丁文件后缀名private static final String FILE_END = ".apk";//apatch文件路径private String mPatchDir;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);mPatchDir = getExternalCacheDir().getAbsolutePath() + "/tpatch/";//创建文件夹File file = new File(mPatchDir);if (file == null || !file.exists()) {file.mkdir();}}/*** 加载补丁文件** @param view*/public void addPatach(View view) {TinkerManager.loadPatach(getPatachPath());}private String getPatachPath() {return mPatchDir.concat("Thinker").concat(FILE_END);}
}
布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"tools:context="com.xiaoyehai.tinker_demo.MainActivity"><Buttonandroid:layout_width="match_parent"android:layout_height="wrap_content"android:onClick="addPatach"android:text="加载补丁" />
</LinearLayout>
2.准备一个修改后的apk:new.apk
修改后效果:布局新增一个按钮
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"tools:context="com.xiaoyehai.tinker_demo.MainActivity"><Buttonandroid:layout_width="match_parent"android:layout_height="wrap_content"android:onClick="addPatach"android:text="加载补丁" /><Buttonandroid:layout_width="match_parent"android:layout_height="wrap_content"android:text="加载补丁后显示的按钮" /></LinearLayout>
3.使用命令行来生成patch文件
命令行工具tinker-patch-cli.jar提供了基准包与新安装包做差异,生成补丁包的功能。
可以在该地址中提取工具tinker-patch-cli:https://github.com/Tencent/tinker/tree/master/tinker-build/tinker-patch-cli
大致的文件结构如下:
tinker_config.xml文件:里面包含tinker的配置,例如签名文件等。
需要修改的地方
1.文件最末尾的sing块,修改为自己的签名证书相关信息.
<!--sign, if you want to sign the apk, and if you want to use 7zip, you must fill in the following data--><issue id="sign"><!--the signature file path, in window use \, in linux use /, and the default path is the running location--><path value="lantu.jks"/><!--storepass--><storepass value="123456"/><!--keypass--><keypass value="123456"/><!--alias--><alias value="lantu"/></issue>
2.修改为自己的application:
<loader value="com.xiaoyehai.tinker_demo.MyApplication"/>
<issue id="dex"><!--only can be 'raw' or 'jar'. for raw, we would keep its original format--><!--for jar, we would repack dexes with zip format.--><!--if you want to support below 14, you must use jar--><!--or you want to save rom or check quicker, you can use raw mode also--><dexMode value="jar"/><!--what dexes in apk are expected to deal with tinkerPatch--><!--it support * or ? pattern.--><pattern value="classes*.dex"/><pattern value="assets/secondary-dex-?.jar"/><!--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 {@code SampleTinkerLoader}, and the classes you use in them--><loader value="com.tencent.tinker.loader.*"/><loader value="com.xiaoyehai.tinker_demo.MyApplication"/></issue>
生成patach文件命令:
java -jar tinker-patch-cli-1.7.7.jar -old old.apk -new new.apk -config tinker_config.xml -out output
生成的文件目录:
patch_signed.apk就是我们需要的补丁文件,改名后拷贝到我们的内存卡。
点击加载补丁文件,如果成功,应用默认会重启重启即可达到修复效果。
接入方式2:gradle接入
gradle是推荐的接入方式,也是实际开发中真正用到的方式,在gradle插件tinker-patch-gradle-plugin中我们帮你完成proguard、multiDex以及Manifest处理等工作。
在gradle中正确配置Tinker参数,在android studio中直接生成patach文件。
一、配置gradle
1,在项目的gradle.properties文件中添加Tinker的版本号:
TINKER_VERSION=1.9.1
2.在项目的build.gradle中,添加tinker-patch-gradle-plugin的依赖
dependencies {classpath 'com.android.tools.build:gradle:3.0.0'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}
3.在app的gradle文件app/build.gradle,我们需要添加tinker的库依赖以及apply tinker的gradle插件.
Gradle版本小于2.3的这么写:
dependencies {//可选,用于生成application类 provided("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}")//tinker的核心库compile("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") //Tinker需要使用到MulitDexcompile'com.android.support:multidex:1.0.1'
}
Gradle版本大于2.3的这么写:
//tinker的核心库implementation("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }//可选,用于生成application类annotationProcessor("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") {changing = true}compileOnly("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }//使应用支持分包, tinker需要你开启MultiDeximplementation 'com.android.support:multidex:1.0.1'
应用tinker的gradle插件:
//apply tinker插件
apply plugin: 'com.tencent.tinker.patch'
4.在app的gradle文件app/build.gradle,添加tinker的相关配置
我们将原apk包称为基准apk包,tinkerPatch直接使用基准apk包与新编译出来的apk包做差异,得到最终的补丁包。
gradle配置的参数详细解释:
https://github.com/Tencent/tinker/wiki/Tinker-接入指南
具体的参数设置事例可参考sample中的app/build.gradle:
https://github.com/Tencent/tinker/blob/master/tinker-sample-android/app/build.gradle
在app的gradle文件app/build.gradle,添加tinker的相关配置
apply plugin: 'com.android.application'android {signingConfigs {release {keyAlias 'lantu'keyPassword '123456'storeFile file('D:/as3.0workspace/HotRepair/lantu.jks')storePassword '123456'}}compileSdkVersion 26dexOptions {// 支持大工程模式jumboMode = true}defaultConfig {applicationId "com.xiaoyehai.tinker_demo2"minSdkVersion 15targetSdkVersion 26versionCode 1versionName "1.0"testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"/*** you can use multiDex and install it in your ApplicationLifeCycle implement*/multiDexEnabled true/*** buildConfig can change during patch!* we can use the newly value when patch*/buildConfigField "String", "MESSAGE", "\"I am the base apk\""
// buildConfigField "String", "MESSAGE", "\"I am the patch apk\""/*** client version would update with patch* so we can get the newly git version easily!*/buildConfigField "String", "TINKER_ID", "\"${getTinkerIdValue()}\""buildConfigField "String", "PLATFORM", "\"all\""}buildTypes {release {minifyEnabled true //打开混淆才会生成mapping文件signingConfig signingConfigs.releaseproguardFiles getDefaultProguardFile('proguard-android.txt'), project.file('proguard-rules.pro')}debug {debuggable trueminifyEnabled falsesigningConfig signingConfigs.debug}}
}dependencies {implementation fileTree(include: ['*.jar'], dir: 'libs')implementation 'com.android.support:appcompat-v7:26.1.0'implementation 'com.android.support.constraint:constraint-layout:1.1.3'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'//Gradle版本小于2.3的这么写//可选,用于生成application类//provided('com.tencent.tinker:tinker-android-anno:1.9.1')//tinker的核心库//compile('com.tencent.tinker:tinker-android-lib:1.9.1')//Gradle版本大于2.3的这么写//tinker的核心库implementation("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }//可选,用于生成application类annotationProcessor("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") {changing = true}compileOnly("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }//使应用支持分包, tinker需要你开启MultiDeximplementation 'com.android.support:multidex:1.0.1'
}def bakPath = file("${buildDir}/bakApk/")ext {// 是否使用Tinker(当你的项目处于开发调试阶段时,可以改为false)tinkerEnabled = true// 基础包路径tinkerOldApkPath = "${bakPath}/tinker_demo2-release-1128-16-24-40.apk"// 基础包的mapping.txt文件路径(用于辅助混淆补丁包的生成,一般在生成release版app时会使用到混淆,// 所以这个mapping.txt文件一般也是用于release安装包补丁的生成)tinkerApplyMappingPath = "${bakPath}/tinker_demo2-release-1128-16-24-40-mapping.txt"// 基础包的R.txt文件路径(如果你的安装包中资源文件有改动,则需要使用该R.txt文件来辅助生成补丁包)tinkerApplyResourcePath = "${bakPath}/tinker_demo2-release-1128-16-24-40-R.txt"//只用于构建所有flavor,如果没有,就忽略这个字段(多渠道打包路径)tinkerBuildFlavorDirectory = "${bakPath}/"
}//是否要使用Tinker
def buildWithTinker() {return hasProperty("TINKER_ENABLE") ? TINKER_ENABLE : ext.tinkerEnabled
}//获取基准apk包的路径
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 getTinkerBuildFlavorDirectory() {return ext.tinkerBuildFlavorDirectory
}// 启用Tinker
if (buildWithTinker()) {//apply tinker插件apply plugin: 'com.tencent.tinker.patch'// 所有Tinker相关的参数配置项tinkerPatch {oldApk = getOldApkPath() // 基准apk包的路径,必须输入,否则会报错。ignoreWarning = false // 是否忽略有风险的补丁包。这里选择不忽略,当补丁包风险时会中断编译。useSign = true // 在运行过程中,我们需要验证基准apk包与补丁包的签名是否一致,我们是否需要为你签名。tinkerEnable = buildWithTinker()// 是否打开tinker的功能。// 编译相关的配置项buildConfig {// 可选参数;在编译新的apk时候,我们希望通过保持旧apk的proguard混淆方式,从而减少补丁包的大小。// 这个只是推荐设置,不设置applyMapping也不会影响任何的assemble编译。applyMapping = getApplyMappingPath()// 可选参数;在编译新的apk时候,我们希望通过旧apk的R.txt文件保持ResId的分配,这样不仅可以减少补丁包的大小,// 同时也避免由于ResId改变导致remote view异常。applyResourceMapping = getApplyResourceMappingPath()// 在运行过程中,我们需要验证基准apk包的tinkerId是否等于补丁包的tinkerId。这个是决定补丁包能运行在哪些基准包上面,// 一般来说我们可以使用git版本号、versionName等等。tinkerId = getTinkerIdValue()// 如果我们有多个dex,编译补丁时可能会由于类的移动导致变更增多。若打开keepDexApply模式,补丁包将根据基准包的类分布来编译。keepDexApply = falseisProtectedApp = false // 是否使用加固模式,仅仅将变更的类合成补丁。注意,这种模式仅仅可以用于加固应用中。supportHotplugComponent = false // 是否支持新增非export的Activity(1.9.0版本开始才有的新功能)}// dex相关的配置项dex {// 只能是'raw'或者'jar'。 对于'raw'模式,我们将会保持输入dex的格式。对于'jar'模式,我们将会把输入dex重新压缩封装到jar。// 如果你的minSdkVersion小于14,你必须选择‘jar’模式,而且它更省存储空间,但是验证md5时比'raw'模式耗时。默认我们并不会// 去校验md5,一般情况下选择jar模式即可。dexMode = "jar"// 需要处理dex路径,支持*、?通配符,必须使用'/'分割。路径是相对安装包的,例如assets/...pattern = ["classes*.dex","assets/secondary-dex-?.jar"]loader = [//加载patch需要用到的类"com.xiaoyehai.tinker_demo2.MyApplication"// 定义哪些类在加载补丁包的时候会用到。这些类是通过Tinker无法修改的类,也是一定要放在main dex的类。// 如果你自定义了TinkerLoader,需要将它以及它引用的所有类也加入loader中;// 其他一些你不希望被更改的类,例如Sample中的BaseBuildInfo类。这里需要注意的是,这些类的直接引用类也需要加入到loader中。// 或者你需要将这个类变成非preverify。]}//lib相关的配置项:用于.so替换lib {// 需要处理lib路径,支持*、?通配符,必须使用'/'分割。与dex.pattern一致, 路径是相对安装包的,例如assets/...pattern = ["lib/*/*.so", "src/main/jniLibs/*/*.so"]}// res相关的配置项:用于资源替换res {// 需要处理res路径,支持*、?通配符,必须使用'/'分割。与dex.pattern一致, 路径是相对安装包的,例如assets/...,务必注意的是,// 只有满足pattern的资源才会放到合成后的资源包。pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]//不替换的文件ignoreChange = [// 支持*、?通配符,必须使用'/'分割。若满足ignoreChange的pattern,在编译时会忽略该文件的新增、删除与修改。// 最极端的情况,ignoreChange与上面的pattern一致,即会完全忽略所有资源的修改。"assets/sample_meta.txt"]// 对于修改的资源,如果大于largeModSize,我们将使用bsdiff算法。这可以降低补丁包的大小,但是会增加合成时的复杂度。默认大小为100kblargeModSize = 100}// 用于生成补丁包中的'package_meta.txt'文件,表明patach文件的一些信息,不是必须,但实际开发中通常会用到packageConfig {// configField("key", "value"), 默认我们自动从基准安装包与新安装包的Manifest中读取tinkerId,并自动写入configField。// 在这里,你可以定义其他的信息,在运行时可以通过TinkerLoadResult.getPackageConfigByName得到相应的数值。// 但是建议直接通过修改代码来实现,例如BuildConfig。configField("platform", "all")configField("patchVersion", "1.0") //patach文件的版本号configField("patchMessage", "tinker is sample to use")}// 7zip路径配置项,执行前提是useSign为true,实际开发中通常不配置sevenZip {zipArtifact = "com.tencent.mm:SevenZip:1.1.10"}}List<String> flavors = new ArrayList<>();project.android.productFlavors.each { flavor ->flavors.add(flavor.name)}boolean hasFlavors = flavors.size() > 0def 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.nametasks.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}") : bakPathfrom variant.outputs.first().outputFileinto destPathrename { String fileName ->fileName.replace("${fileNamePrefix}.apk", "${newFileNamePrefix}.apk")}from "${buildDir}/outputs/mapping/${variant.dirName}/mapping.txt"into destPathrename { String fileName ->fileName.replace("mapping.txt", "${newFileNamePrefix}-mapping.txt")}from "${buildDir}/intermediates/symbols/${variant.dirName}/R.txt"into destPathrename { String fileName ->fileName.replace("R.txt", "${newFileNamePrefix}-R.txt")}}}}}}//多渠道包脚本project.afterEvaluate {//sample use for build all flavor for one timeif (hasFlavors) {task(tinkerPatchAllFlavorRelease) {group = 'tinker'def originOldPath = getTinkerBuildFlavorDirectory()for (String flavor : flavors) {def tinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Release")dependsOn tinkerTaskdef 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 tinkerTaskdef 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"}}}}}
}task sortPublicTxt() {doLast {File originalFile = project.file("public.txt")File sortedFile = project.file("public_sort.txt")List<String> sortedLines = new ArrayList<>()originalFile.eachLine {sortedLines.add(it)}Collections.sort(sortedLines)sortedFile.delete()sortedLines.each {sortedFile.append("${it}\n")}}
}
二、自定义Application类
程序启动时会加载默认的Application类,这导致补丁包无法对它做修改。所以Tinker官方说不建议自己去实现Application,而是由Tinker自动生成。即需要创建一个SampleApplication类,继承DefaultApplicationLike,然后将我们自己的MyApplication中所有逻辑放在SampleApplication中的onCreate中。最后需要将我们项目中之前的MyApplication类删除。
这与上面第一种方式命令行接入方式完全一样,不再啰嗦。
测试Tinker热修复
这里只讲release版本。
一、按正常流程打包出带签名的APK,并装到手机上
二、将上面的三个文件路径复制到app.build中对应的位置,如图
ext {// 是否使用Tinker(当你的项目处于开发调试阶段时,可以改为false)tinkerEnabled = true// 基础包路径tinkerOldApkPath = "${bakPath}/tinker_demo2-release-1128-15-53-40.apk"// 基础包的mapping.txt文件路径(用于辅助混淆补丁包的生成,一般在生成release版app时会使用到混淆,// 所以这个mapping.txt文件一般也是用于release安装包补丁的生成)tinkerApplyMappingPath = "${bakPath}/tinker_demo2-release-1128-15-53-40-mapping.txt"// 基础包的R.txt文件路径(如果你的安装包中资源文件有改动,则需要使用该R.txt文件来辅助生成补丁包)tinkerApplyResourcePath = "${bakPath}/tinker_demo2-release-1128-15-53-40-R.txt"//只用于构建所有flavor,如果没有,就忽略这个字段(多渠道打包路径)tinkerBuildFlavorDirectory = "${bakPath}/"
}
三、修复bug(测试的时候随便改动一点代码)
四、运行补丁命令获取补丁包
运行补丁命令,单击AS右侧顶部gradle–>双击tinkerPatchRelease,如图:
运行完成会在build->outputs->apk->tinkerPatch->release文件夹中生成一个名为patch_signed_7zip.apk的补丁包,如图:
五、将该补丁包重命名后(patch_signed_7zip.apk)复制到之前加载补丁包中对应的SD卡路径。
六、运行项目发现bug并没有修复,因为tinker是不支持即时修复的,关掉APP重启。恭喜你!bug已修复!
把Tinker封装到服务中来实现
/*** 1.检查服务端是否有新的patch文件* 2.有:下载patch文件* 3.加载下载好的patch文件,修复bug或更新功能* 4.patach文件会在应用重启时生效* Created by xiaoyehai on 2018/11/27 0027.*/public class TinkerService extends Service {public static final String TAG = TinkerService.class.getSimpleName();private static final int DOWNLOAD_APATCH = 0x01;private static final int UPDATE_APATCH = 0x02;public static final String UPDATE_PATCH_URL = "";public static final String DOWNLOAD_PATCH_URL = "";//存放apatch文件的目录private String mPatchFileDir;private Handler mHandler = new Handler() {@Overridepublic void handleMessage(Message msg) {super.handleMessage(msg);switch (msg.what) {case DOWNLOAD_APATCH: //下载patch文件downloadPatch();break;case UPDATE_APATCH: //检查服务端是否有新的patch文件checkApatchUpdate();break;}}};@Nullable@Overridepublic IBinder onBind(Intent intent) {return null;}@Overridepublic void onCreate() {super.onCreate();init();}@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {mHandler.sendEmptyMessage(UPDATE_APATCH);return START_NOT_STICKY; //服务被系统回收之后不会自动重启}private void init() {mPatchFileDir = getExternalCacheDir().getAbsolutePath() + "/apatch/";File patchDir = new File(mPatchFileDir);try {if (patchDir == null || !patchDir.exists()) {patchDir.mkdirs();}} catch (Exception e) {e.printStackTrace();stopSelf(); //停止服务}}/*** 检查服务端是否有新的apatch文件*/private void checkApatchUpdate() {//获取服务器信息,判断是否有新的apatch文件OkHttpManager.getInstance().asyncJsonStringByURL(UPDATE_PATCH_URL, new OkHttpManager.StringCallback() {@Overridepublic void onResponse(String result) {//有新的apatch文件,下载文件mHandler.sendEmptyMessage(DOWNLOAD_APATCH);//如果没有新文件//stopSelf();}@Overridepublic void onFailure(IOException e) {stopSelf();}});}/*** 下载apatch文件*/private void downloadPatch() {DownloadManager.getInstance().downloadFile(DOWNLOAD_PATCH_URL, mPatchFileDir, new DownloadManager.FileCallback() {@Overridepublic void onSuccess(File file) {//文件下载成功,加载apatc文件,修复bugTinkerManager.loadPatach(file.getAbsolutePath());}@Overridepublic void onProgress(int progress, long total) {Log.e(TAG, "onProgress: " + progress);}@Overridepublic void onError(Call call, Exception e) {stopSelf();}});}
}
Tinker常用的一些高级功能
一. Tinker如何支持多渠道打包
命令行接入方式只能一个渠道一个渠道的打patch文件,所以强烈不建议使用这种方式。
gradle接入方式只需要简单的修改一下gradle脚本即可。
我们知道多渠道打包是采用productFlavors实现的。但是这种多渠道打包会造成20个渠道包的热更新就需要20个补丁,这样肯定是不合理的。那怎样才能实现20个渠道包只需要一个补丁包呢?Tinker官方也说了,推荐我们多渠道打包使用Walle,这样就能实现多个渠道包只使用一个补丁包了!
Walle的github地址
按照Walle的文档去集成,既可以实现多渠道打包。修改bug后和上面打patach文件的步骤一样,但只会生成一个补丁文件,这里的一个补丁包就适用于各个渠道包。
因我本人对Walle不熟悉,所以我就使用友盟的多渠道打包来讲解,但是友盟多渠道打包每个渠道包都会有一个补丁文件,熟悉Walle的最好使用Walle。
1.按照umeng的要求,manifest文件中需要有:
<!--友盟统计相关meta-data--><meta-dataandroid:name="UMENG_APPKEY"android:value="你的appkey" /><meta-dataandroid:name="UMENG_CHANNEL"android:value="${UMENG_CHANNEL_VALUE}" />
2,在module(一般也就是app)的build.gradle的android{}中添加如下内容:
//多渠道脚本支持productFlavors {googleplayer {manifestPlaceholders = [UMENG_CHANNEL_VALUE: "googleplayer"]}xiaomi {manifestPlaceholders = [UMENG_CHANNEL_VALUE: "xiaomi"]}baidu {manifestPlaceholders = [UMENG_CHANNEL_VALUE: "baidu"]}productFlavors.all { flavor ->flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name]}}
3.AS3.0要加上defaultConfig:
defaultConfig {applicationId "com.xiaoyehai.tinker_demo2"minSdkVersion 15targetSdkVersion 26versionCode 1versionName "1.0"// AS3.0之后:原因就是使用了productFlavors分包,解决方法就是在build.gradle中的defaultConfig中// 添加一个flavorDimensions "1"就可以了,后面的1一般是跟你的versionCode相同flavorDimensions "1"testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"}
配置完毕,开始打包。
4.按正常流程打包出带签名的APK,并装到手机上:
5.将上面的文件夹路径复制到app.build中对应的位置:
ext {// 是否使用Tinker(当你的项目处于开发调试阶段时,可以改为false)tinkerEnabled = true// 基础包路径tinkerOldApkPath = "${bakPath}/tinker_demo2-1129-11-53-12"// 基础包的mapping.txt文件路径(用于辅助混淆补丁包的生成,一般在生成release版app时会使用到混淆,// 所以这个mapping.txt文件一般也是用于release安装包补丁的生成)tinkerApplyMappingPath = "${bakPath}/tinker_demo2-1129-11-53-12"// 基础包的R.txt文件路径(如果你的安装包中资源文件有改动,则需要使用该R.txt文件来辅助生成补丁包)tinkerApplyResourcePath = "${bakPath}/tinker_demo2-1129-11-53-12"//只用于构建所有flavor,如果没有,就忽略这个字段(多渠道打包路径)tinkerBuildFlavorDirectory = "${bakPath}/tinker_demo2-1129-11-53-12"
}
6.修复bug(测试的时候随便改动一点代码)
7.运行补丁命令获取补丁包
运行补丁命令,单击AS右侧顶部gradle–>双击tinkerPatchAllFlavorRelease,如图:
运行完成会在会为每个渠道生成一个补丁文件:
8.将该补丁包重修修改名字后发给后台。
二. 如何自定义Tinker行为
1.自定义PatachListener监听patach receive事件
/*** 自定义行为:自定义PatachListener箭头patach receive事件* Created by xiaoyehai on 2018/11/29 0029.*/public class CustomPatachListener extends DefaultPatchListener {public CustomPatachListener(Context context) {super(context);}@Overrideprotected int patchCheck(String path, String patchMd5) {//可以在这个方法中做一些自定义行为,比如文件的合法性等//然后再加载patch文件的时候调用该方法return super.patchCheck(path, patchMd5);}
}
看看DefaultPatchListener中已经定义了很多行为,我们可以重写patchCheck()方法自定义一些行为:
protected int patchCheck(String path, String patchMd5) {Tinker manager = Tinker.with(context);//check SharePreferences alsoif (!manager.isTinkerEnabled() || !ShareTinkerInternals.isTinkerEnableWithSharedPreferences(context)) {return ShareConstants.ERROR_PATCH_DISABLE;}File file = new File(path);if (!SharePatchFileUtil.isLegalFile(file)) {return ShareConstants.ERROR_PATCH_NOTEXIST;}//patch service can not send requestif (manager.isPatchProcess()) {return ShareConstants.ERROR_PATCH_INSERVICE;}//if the patch service is running, pendingif (TinkerServiceInternals.isTinkerPatchServiceRunning(context)) {return ShareConstants.ERROR_PATCH_RUNNING;}if (ShareTinkerInternals.isVmJit()) {return ShareConstants.ERROR_PATCH_JIT;}Tinker tinker = Tinker.with(context);if (tinker.isTinkerLoaded()) {TinkerLoadResult tinkerLoadResult = tinker.getTinkerLoadResultIfPresent();if (tinkerLoadResult != null && !tinkerLoadResult.useInterpretMode) {String currentVersion = tinkerLoadResult.currentVersion;if (patchMd5.equals(currentVersion)) {return ShareConstants.ERROR_PATCH_ALREADY_APPLY;}}}if (!UpgradePatchRetry.getInstance(context).onPatchListenerCheck(patchMd5)) {return ShareConstants.ERROR_PATCH_RETRY_COUNT_LIMIT;}return ShareConstants.ERROR_PATCH_OK;}
2.自定义TinkeReceiveService改变patcah安装成功后行为
比如:实现成功加载patach文件后不让进程自动被杀死。
/*** 自定义行为:自定义TinkeReceiveService改变patcah安装成功后行为* 就是决定patach安装成功以后的后续操作,默认实现杀死进程* Created by xiaoyehai on 2018/11/29 0029.*/public class CustomReceiveService extends DefaultTinkerResultService {public static final String TAG = "CustomReceiveService";/*** 重写该方法,实现成功加载patach文件后不让进程自动被杀死** @param result*/@Overridepublic 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 processTinkerServiceInternals.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) {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!");}*/}}
}
CustomReceiveService是个服务,需要注册:
<service android:name=".tinker.CustomReceiveService" />
在TinkerManager中加入2个自定义行为:
/*** Created by xiaoyehai on 2018/11/27 0027.* 对Tinker进行封装*/public class TinkerManager {//是否初始化Tinkerprivate static boolean isInstalled = false;private static ApplicationLike mApplicationLike;private static CustomPatachListener customPatachListener;/*** 初始化Tinker** @param applicationLike*/public static void installTinker(ApplicationLike applicationLike) {mApplicationLike = applicationLike;if (isInstalled) {return;}customPatachListener = new CustomPatachListener(getApplicationContext());//TinkerInstaller.install(mApplicationLike); //Tinker初始化DefaultLoadReporter loadReporter = new DefaultLoadReporter(getApplicationContext());DefaultPatchReporter patchReporter = new DefaultPatchReporter(getApplicationContext());AbstractPatch abstractPatch = new UpgradePatch();TinkerInstaller.install(mApplicationLike,loadReporter,patchReporter,customPatachListener,CustomReceiveService.class,abstractPatch);isInstalled = true;}/*** 加载补丁文件** @param path*/public static void loadPatach(String path, String patchMd5) {customPatachListener.patchCheck(path, patchMd5);if (Tinker.isTinkerInstalled()) {TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), path);}}/*** 通过ApplicationLike获取Context** @return*/private static Context getApplicationContext() {if (mApplicationLike != null) {return mApplicationLike.getApplication().getApplicationContext();}return null;}
}
使用过程中遇到的问题
1.tinker和android studio的Instant Run 不兼容。 所以啦!当你项目接入tinker热修复时,一定要把要把 instant run 给关掉。
否则就会报这个错误
Tinker does not support instant run mode, please trigger build by assembleDebug or disable instant run in ‘File->Settings…’.
在 设置里面找到Instant Run 取消箭头所指向的选中项。点击apply 就行了。
2.项目打包后有bak目录下有生成apk和R.txt文件,就是没有mapping文件,官网说打开混淆才会生成:
release {minifyEnabled truesigningConfig signingConfigs.releaseproguardFiles getDefaultProguardFile('proguard-android.txt'), project.file('proguard-rules.pro')}
Android热修复(1):热修复的介绍和原理解析
Android热修复(2):AndFix热修复框架的使用
这篇关于Android热修复(3):Tinker的使用的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!