Tinker基本介绍
Tinker 是微信官方的 Andriod 热补丁解决方案,它支持动态下发代码,so库以及资源,让应用在不需要安装的情况下实现更新,当然,你也可以使用 Thinker 来更新你的插件。
它主要包含以下几部分:
1,gradle 编译插件 tinker-patch-gradle-plugin:主要用于在 as 中直接完成 patch 文件的生成
2,核心 sdk 库 tinker-android-lib :核心库,为应用层提供的 api
3,非 gradle 编译用户的命令行版本,tinker-path-cli.jar :为 eclipse 做的一个工具
为什么使用 Tinker
当前市面上 热修复的解决方案很多,但是他们都有一些无法解决的问题,但是 Tinker 的功能是比较全面的。
Tinker 执行原理及流程
基于 android 原生的 ClassLoader ,开发了自己的 ClassLoader,通过自定义的 ClassLoader去加载 patch 文件中的字节码
基于 android 原生的 aapt,开发了自己的 aapt ,加载 资源文件
基于 Dex 文件的格式,研发了 DexDiff 算法,通过 DexDiff 算法比较两个 apk 文件中的差异
简单的使用 Tinker
1,在项目的gradle.properties 中添加
# tinker版本号 ,控制版本,以下版本已经兼容 9.0 TINKER_VERSION=1.9.14 TINKERPATCH_VERSION=1.2.14
2,在项目的 gradle中添加:
classpath("com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}")
3,在 app 中的 gradle 中添加:
compileOnly("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true } annotationProcessor("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true } // tinker 的核心 sdk 库 implementation("com.tinkerpatch.sdk:tinkerpatch-android-sdk:${TINKERPATCH_VERSION}") { changing = true }
4,接着进行初始化,新建一个类用于管理 tinker 的初始化
/** * 对 TinkerManager api 做一层封装 */ public class TinkerManager { /** * 是否初始化 */ private static boolean isInstalled = false; private static ApplicationLike mApplike; /** * 完成 Tinker初始化 * * @param applicationLike */ public static void installTinker(ApplicationLike applicationLike) { mApplike = applicationLike; if (isInstalled) { return; } //完成 tinker 初始化 TinkerInstaller.install(mApplike); isInstalled = true; } public static void loadPatch(String path) { if (Tinker.isTinkerInstalled()) { TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), path); } } /** * 通过 ApplicationLike 获取 Context */ private static Context getApplicationContext() { if (mApplike != null) { return mApplike.getApplication().getApplicationContext(); } return null; } }
5,自定义 application 继承自 ApplicationLike
//通过 DefaultLifeCycle 注解来生成我们程序中需要用到的 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.installTinker(this); } }
为什么需要继承 ApplicationLike,而不直接在 Application 中初始化呢?
因为 ApplicationLike 需要对 Application 的生命周期进行监听, 所以他通过 ApplicationLike 进行代理。通过这个代理可以完成对 Application 的生命周期监听,然后在不同的生命周期做一些别的工作,因为Tinker 初始化非常复杂,所用用 ApplicationLike 进行了代理,这样使用就非常简单了!
注意上面的注解:通过这个注解可以生成 需要在程序中进行添加的 application,
public class MyTinkerApplication extends TinkerApplication { public MyTinkerApplication() { super(15, "com.testdemo.www.tinker.CustomTinkerLike", "com.tencent.tinker.loader.TinkerLoader", false); } }
上面这个就是通过注解生成的。我们需要将他添加的 AndroidManifest 中。
6,配置 tinker
//buildDir 代表的是 app 目录下 build 文件夹, // 如果创建成果,他会在 build 文件夹中创建 bakApk文件夹,存放 old.apk def bakPath = file("${buildDir}/bakApk") //指定基准文件存放位置 ext { tinkerEnable = true tinkerID = "1.0" tinkerOldApkPath = "${bakPath}/" tinkerApplyMappingPath = "${bakPath}/" tinkerApplyResourcePath = "${bakPath}/" } //是否启用 tinker def buildWithTinker() { return ext.tinkerEnable } // old 路径 def getOldApkPath() { return ext.tinkerOldApkPath } // 混淆文件路径 def getApplyMappingPath() { return ext.tinkerApplyMappingPath } // 资源文件路径 def getApplyResourceMappingPath() { return ext.tinkerApplyResourcePath } // id def getTinkerIdValue() { return ext.tinkerID } if (buildWithTinker()) { //启用 tinker apply plugin: 'com.tencent.tinker.patch' //所有 tinker 相关的参数配置 tinkerPatch() { oldApk = getOldApkPath() // old.apk 路径 ignoreWarning = false //不忽略警告,如果警告取消生成patch useSign = true // 强制 patch 文件使用签名 tinkerEnable = buildWithTinker() //指示是否启用 tinker buildConfig() { applyMapping = getApplyMappingPath() // 指定old.apk 打包时所使用的的混淆文件 applyResourceMapping = getApplyResourceMappingPath() // 指定 old.apk 资源文件 tinkerId = getTinkerIdValue() //指定 TinkerId keepDexApply = false //一般置为 false,true:生成patch 的时候会根据 dex 文件的分包去动态的编译 patch 文件 } dex() { dexMode = "jar" //jar 是配到14以下,会将dex压缩为jar文件,然后进行处理,体积小row只能在14以上使用,直接对 dex 文件处理 pattern = ["classes*.dex", "assets/secondary-dex-?.jar"]//指定 dex 文件位于哪些牡蛎 loader = ["com.testdemo.www.tinker.MyTinkerApplication"] //指定加载patch文件时所用到的类 } //工程中的 jar 和 so lib { pattern = ["libs/*/*.so"] } res { //指定 tinker 可以修改的资源文件路径 pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"] ignoreChange = ["assets/sampple_meta.txt"] //制定不受影响的资源路径,即时修改了这个资源文件,也不会被打入 patch largeModSize = 100 //资源修改大小默认值 } //-----------------必须项配置完成------------------------------------- //patch的介绍 packageConfig { //补丁加载完成后通过 key 可以拿到这些 value configField("patchMessage", "fix 1.0 version's bugs") configField("patchVersion", "1.0") } //使用压缩 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() > 0 /** * 复制基准包和其它必须文件到指定目录 */ 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[0].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") } } } } } } }
注意 ext 中:
tinkerEnable = true //是否启用 tinker tinkerID = “1.0” // id ,线上的版本 id 和 补丁包的 tinkerID 必须相等
详细的说明
7,进行测试,打包
public class MainActivity extends AppCompatActivity implements View.OnClickListener { private static final String FILE_END = ".apk"; private String mPatchDir; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //创建路径 mPatchDir = getExternalCacheDir().getAbsolutePath() + "/tpatch/"; File file = new File(mPatchDir); file.mkdir(); findViewById(R.id.btn).setOnClickListener(this); } @Override public void onClick(View v) { //加载补丁包 TinkerManager.loadPatch(getPatchName()); } //拼装一个路径 private String getPatchName() { return mPatchDir.concat("tinker").concat(FILE_END); } }
看一下布局
<androidx.appcompat.widget.LinearLayoutCompat 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=".MainActivity"> <androidx.appcompat.widget.AppCompatButton android:id="@+id/btn" android:layout_width="match_parent" android:layout_height="50dp" android:layout_marginTop="20dp" android:text="加载 PATCH 包" android:textSize="20sp" tools:ignore="HardcodedText" /> </androidx.appcompat.widget.LinearLayoutCompat>
接着我们就可以进行打包了,注意不能是 debug 。是需要签名的。打包完成后会在 build 文件下生成 bakApk 文件夹,里面就是打包的 apk。
然后把这个apk安装到手机上即可。