Tinker是什麼
Tinker是微信官方的Android熱更新檔解決方案,它支援動态下發代碼、So庫以及資源,讓應用能夠在不需要重新安裝的情況下實作更新。當然,你也可以使用Tinker來更新你的插件。
它主要包括以下幾個部分:
- gradle編譯插件:
tinker-patch-gradle-plugin
- 核心sdk庫:
tinker-android-lib
- 非gradle編譯使用者的指令行版本:
tinker-patch-cli.jar
為什麼使用Tinker
目前市面的熱更新檔方案有很多,其中比較出名的有阿裡的AndFix、美團的Robust以及QZone的超級更新檔方案。但它們都存在無法解決的問題,這也是正是我們推出Tinker的原因。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsICM38FdsYkRGZkRG9lcvx2bjxiNx8VZ6l2cswGbtFmdGdVY0YVbkZHetNGbk1mYwBnMMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnLwcTNyEDOykDM4AzNwAjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
Tinker核心原理
- 基于android原生的ClassLoader,開發了自己的ClassLoader
- 基于android原生的aapt,開發了自己的aapt
- 微信團隊自己基于Dex檔案的格式,研發了DexDiff算法
使用Tinker完成bug修複
在app的gradle檔案app/build.gradle,我們需要添加tinker的庫依賴
如果Gradle版本是3.0 (com.android.tools.build:gradle:3.0.0' )或者以上是的話配置
//tinker的核心庫
api("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
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"
gradle版本是3.0以下的配置
dependencies {
//tinker的核心庫
implementation("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
//可選,用于生成application類
provided("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
}
implementation 的作用是編譯的時候使用,把庫打包到apk中
provided 的作用是隻參與編譯,不參與把庫打包到apk中,減小apk包體積
gradle.properties配置公共參數
android.enableJetifier=true
TINKER_VERSION=1.9.14.5
android.enableR8 = false
public class TinkerManager {
private static boolean isInstalled = false;
private static ApplicationLike mApplike;//委托類
/**
* Tinker的初始化
* @param applicationLike
*/
public static void installTinker(CustomTinkerLike applicationLike) {
mApplike = applicationLike;
if (isInstalled) {
return;
} else {
TinkerInstaller.install(mApplike);//完成Tinker的初始化
isInstalled = true;
}
}
//完成Patch檔案的加載
public static void loadPatch(String path) {
if (Tinker.isTinkerInstalled()) {
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),path);
}
}
//通過ApplicationContext擷取Context
private static Context getApplicationContext() {
if (mApplike != null) {
return mApplike.getApplication().getApplicationContext();
}
return null;
}
}
@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);
//使應用支援分包
MultiDex.install(base);
TinkerManager.installTinker(this);
}
}
Tinker需要監聽Application對象的周期,通過ApplicationLike 進行委托,通過這個委托,可以在ApplicationLike完成對Application對象的周期的監聽,在不同的生命周期階段,完成不同的操作。
build一下就會生成MyTinkerApplication,在清單檔案裡面使用下。
<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">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
TInker的內建和初始化就完成了
public class MainActivity extends AppCompatActivity {
private static final String FILE_END = ".apk";
private String mPatchDir;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.loadPatch).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
loadPatch();
}
});
mPatchDir = getExternalCacheDir().getAbsolutePath() + "/tpatch/";
//是為了建立我們的檔案夾
File file = new File(mPatchDir);
if (file == null || !file.exists()) {
file.mkdir();
}
}
public void loadPatch() {
TinkerManager.loadPatch(getPatchName());
}
private String getPatchName() {
return mPatchDir.concat("patch_signed").concat(FILE_END);
}
}
布局檔案
patch生成方式
- 使用 指令行的方式完成Patch包的生成(不介紹了)
- 使用Gradle插件的方式完成Patch包的生成
內建插件
在項目的build.gradle中,添加
tinker-patch-gradle-plugin
的依賴
buildscript {
dependencies {
classpath("com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}")
}
}
在Gradle中配置生成patch檔案
- 在Gradle中正确配置tinker參數
- 在Android Studio中直接生成patch檔案
在Gradle中配置tinker參數
apply plugin: 'com.android.application'
def javaVersion = JavaVersion.VERSION_1_7
android {
signingConfigs {
release {
storeFile file('D:\\MyDownload\\TinkerDemo\\app\\newSign.jks')
storePassword '888888'
keyAlias = 'tinker'
keyPassword '888888'
}
}
compileSdkVersion 29
buildToolsVersion "29.0.3"
compileOptions {
sourceCompatibility javaVersion
targetCompatibility javaVersion
}
dexOptions {
jumboMode = true
}
defaultConfig {
applicationId "com.example.tinkerdemo"
minSdkVersion 21
targetSdkVersion 29
versionCode 1
versionName "1.0"
multiDexEnabled true
multiDexKeepProguard file("tinker_multidexkeep.pro")
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
javaCompileOptions { annotationProcessorOptions { includeCompileClasspath = true } }
}
buildTypes {
release {
minifyEnabled = true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
//tinker的核心庫
api("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
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"
}
def bakPath = file("${buildDir}/bakApk/")
ext {
tinkerEnanle = true
tinkerOldApkPath = "${bakPath}/app-release-0714-14-29-57.apk"
tinkerId = "1.0"
tinkerApplyMappingPath = "${bakPath}/app-release-0714-14-29-57-mapping.text"
tinkerApplyResourcePath = "${bakPath}/app-release-0714-14-29-57-R.text"
}
def buildWithTinker() {
return ext.tinkerEnanle
}
def getOldApkPath() {
return ext.tinkerOldApkPath
}
def getApplyMappingPath() {
return ext.tinkerApplyMappingPath
}
def getApplyResourceMappingPath() {
return ext.tinkerApplyResourcePath
}
def getTinkerIdValue() {
return ext.tinkerId
}
if (buildWithTinker()) {
//啟用tinker
apply plugin: 'com.tencent.tinker.patch'
//所有tinker相關的參數配置
tinkerPatch {
oldApk = getOldApkPath() //指定old apk檔案
ignoreWarning = false //不忽略tinker的警告,有則中止patch檔案的生成
useSign = true//強制patch檔案也使用簽名
tinkerEnanle = buildWithTinker()//指定是否啟用Tinker
buildConfig {
applyMapping = getApplyMappingPath()//指定old apk 打包時所使用的混淆檔案
applyResourceMapping = getApplyResourceMappingPath()//指定old apk所使用的資源檔案
tinkerId = getTinkerIdValue()//指定TinkerID,每個patch檔案的唯一辨別符
keepDexApply = false
isProtectedApp = false
supportHotplugComponent = false
}
dex {
dexMode = "jar" //jar、raw
pattern = ["classes*.dex", "assets/secondary-dex-?.jar"] //指定dex檔案目錄
loader = ["com.example.tinkerdemo.MyTinkerApplication"]//加載patch包所用的類
}
lib {
pattern = ["lib/*/*.so"]
}
res {
pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
//指定tinker可以修改的所有資源路徑
ignoreChange = ["assets/sample_meta.txt"]//修改了也不想patch包裡生效
largeModSize = 100 //資源修改大小的預設值
}
packageConfig {
configField("patchMessage", "fix the 1.0 version's bugs")
configField("patchVersion", "1.0")
}
}
}
注意事項1:打開混淆 minifyEnabled true,要不然不能生産mapping檔案
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}
注意事項2:tinker相關的參數配置的時候要把自動生成的MyTinkerApplication填進去
loader = ["com.example.tinkerdemo.MyTinkerApplication"]//加載patch包所用的類
添加混淆
# tinker混淆規則
-keepattributes SourceFile,LineNumberTable
-dontwarn com.google.**
-dontwarn com.android.**
接下來,ext裡面的檔案路徑還沒有指定。可以引入腳本,自動将檔案儲存到bakPath中
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")
/**
* 複制基準包和其他必須檔案到指定目錄
*/
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
if (variant.metaClass.hasProperty(variant, 'packageApplicationProvider')) {
def packageAndroidArtifact = variant.packageApplicationProvider.get()
if (packageAndroidArtifact != null) {
try {
from new File(packageAndroidArtifact.outputDirectory.getAsFile().get(), variant.outputs.first().apkData.outputFileName)
} catch (Exception e) {
from new File(packageAndroidArtifact.outputDirectory, variant.outputs.first().apkData.outputFileName)
}
} else {
from variant.outputs.first().mainOutputFile.outputFile
}
} else {
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"
from "${buildDir}/intermediates/symbol_list/${variant.dirName}/R.txt"
from "${buildDir}/intermediates/runtime_symbol_list/${variant.dirName}/R.txt"
into destPath
rename { String fileName ->
fileName.replace("R.txt", "${newFileNamePrefix}-R.txt")
}
}
}
}
}
}
這裡是可以從官方文檔複制
準備階段
- build一個old apk 并安裝到手機
- 修改一些功能後,build一個new apk
打包apk,安裝到手機上,保留基礎包
修改布局檔案,打包新的apk,
新的apk生成以後,我們要将ext裡面的檔案路徑補充完整,這個是剛剛我們基礎包的檔案路徑,不要弄錯。
ext {
tinkerEnanle = true
tinkerOldApkPath = "${bakPath}/app-release-0709-15-46-10.apk"
tinkerId ="1.0"
tinkerApplyMappingPath = "${bakPath}/app-release-0709-15-46-10-mapping.txt"
tinkerApplyResourcePath ="${bakPath}/app-release-0709-15-46-10-R.text"
}
點選tinkerPatchRelease就可以生成patch
在studio的terminal面闆中輸入指令adb push 将更新檔檔案push到手機
在手機的檔案夾中生成了更新檔檔案
點選加載更新檔包,發現新添加的内容顯示出來了,完美!!!
tinker自定義行為
主要是自定義patch安裝以後的行為,tinker預設實作是殺掉目前應用的程序,app閃退,第二次啟動修複了bug,我們進行優化,不要讓app閃退
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) {
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!");
}
}
}
TinkerServiceInternals.killTinkerPatchServiceProcess(getApplicationContext());
patch加載完以後,會删除手機裡的patch包,節省手機的空間
android.os.Process.killProcess(android.os.Process.myPid());
這個是殺掉目前的線程,導緻app閃退,app的體驗非常不好
我們需要重寫這個方法
public class CustomResultSerice extends DefaultTinkerResultService {
private static final String TAG ="Tinker.SampleResultService";
//傳回patch檔案的安裝結果
@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) {
deleteRawPatchFile(new File(result.rawPatchFilePath));
}
}
}
這樣自定義以後,app不會閃退,加載完patch包以後,第二次啟動的時候生效。
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;
} else {
LoadReporter loadReporter = new DefaultLoadReporter(applicationLike.getApplication());//日志上報
PatchReporter patchReporter = new DefaultPatchReporter(applicationLike.getApplication());//日志上報
DefaultPatchListener mPatchListener = new DefaultPatchListener(applicationLike.getApplication());
AbstractPatch upgradePatchProcessor = new UpgradePatch();
TinkerInstaller.install(applicationLike, loadReporter, patchReporter, mPatchListener, CustomResultSerice.class, upgradePatchProcessor);
isInstalled = true;
}
}
//完成Patch檔案的加載
public static void loadPatch(String path) {
if (Tinker.isTinkerInstalled()) {
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),path);
}
}
//通過ApplicationContext擷取Context
private static Context getApplicationContext() {
if (mApplike != null) {
return mApplike.getApplication().getApplicationContext();
}
return null;
}
}
注意再manifest注冊這個service
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.tinkerdemo">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<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">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name=".CustomResultSerice"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false"/>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.example.tinkerdemo.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"/>
</provider>
</application>
</manifest>
在這個過程中如果發現onPatchResult的result.isSuccess一直傳回false,那麼在清單檔案裡面加上provider的代碼
provider_paths.xml内容如下
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!-- /storage/emulated/0/Download/${applicationId}/.beta/apk-->
<external-path name="beta_external_path" path="Download/"/>
<!--/storage/emulated/0/Android/data/${applicationId}/files/apk/-->
<external-path name="beta_external_files_path" path="Android/data/"/>
</paths>
總結,tinker不僅适用于bug修複,也适用于小功能的添加。
demo下載下傳位址