Tinker是什麼
Tinker是微信官方的Android熱更新檔解決方案,它支援動态下發代碼、So庫以及資源,讓應用能夠在不需要重新安裝的情況下實作更新。當然,你也可以使用Tinker來更新你的插件。
它主要包括以下幾個部分:
- gradle編譯插件:
tinker-patch-gradle-plugin
- 核心sdk庫:
tinker-android-lib
- 非gradle編譯使用者的指令行版本:
tinker-patch-cli.jar
為什麼使用Tinker
總的來說:
- AndFix作為native解決方案,首先面臨的是穩定性與相容性問題,更重要的是它無法實作類替換,它是需要大量額外的開發成本的;
- Robust相容性與成功率較高,但是它與AndFix一樣,無法新增變量與類隻能用做的bugFix方案;
- Qzone方案可以做到釋出産品功能,但是它主要問題是插樁帶來Dalvik的性能問題,以及為了解決Art下記憶體位址問題而導緻更新檔包急速增大的。
特别是在Android N之後,由于混合編譯的inline政策修改,對于市面上的各種方案都不太容易解決。而Tinker熱更新檔方案不僅支援類、So以及資源的替換,它還是2.X-7.X的全平台支援。利用Tinker我們不僅可以用做bugfix,甚至可以替代功能的釋出。Tinker已運作在微信的數億Android裝置上,那麼為什麼你不使用Tinker呢?
Tinker的已知問題
由于原理與系統限制,Tinker有以下已知問題:
- Tinker不支援修改AndroidManifest.xml,Tinker不支援新增四大元件;
- 由于Google Play的開發者條款限制,不建議在GP管道動态更新代碼;
- 在Android N上,更新檔對應用啟動時間有輕微的影響;
- 不支援部分三星android-21機型,加載更新檔時會主動抛出
;"TinkerRuntimeException:checkDexInstall failed"
- tinker的一般模式并不支援加強,需要使用usePreGeneratedPatchDex模式,即提前生成更新檔模式。某些加強工具可能會将非exported的四大元件類名替換,這些類将無法修改。對于Android N之後的裝置,本模式可能會因為内聯而出現問題,建議過濾N之後的裝置;
- 對于資源替換,不支援修改remoteView。例如transition動畫,notification icon以及桌面圖示。
如何使用Tinker 接下來帶大家 在項目中一步步使用 Tinker,我們選一個平台 我選的是bugly ,騰訊的,當然你也可以選擇其他的平台, TinkerPatch ,這個也可以 ,但以後肯定要收費的, TinkerPatch平台文檔 。
首先第一步 添加插件依賴 工程根目錄下“build.gradle”檔案中添加:
第二步:在app module的“build.gradle”檔案中添加(我的配置): apply plugin: 'com.android.application'
def releaseTime() {
return new Date().format( "yyyy-MM-dd", TimeZone. getTimeZone( "UTC"))
}
dependencies {
compile fileTree( dir: 'libs', include: [ '*.jar'])
compile 'com.android.support:appcompat-v7:24.1.1'
// 多 dex 配置
compile "com.android.support:multidex:1.0.1"
// 內建 Bugly 熱更新 aar (灰階時使用方式)
// compile(name: 'bugly_crashreport_upgrade-1.2.0', ext: 'aar')
compile 'com.tencent.bugly:crashreport_upgrade:latest.release' // 其中 latest.release 指代最新版本号,也可以指定明确的版本号,例如 1.2.0
compile 'com.tencent.bugly:nativecrashreport:latest.release' // 其中 latest.release 指代最新版本号,也可以指定明确的版本号,例如 2.2.0
}
android {
compileSdkVersion 23
buildToolsVersion "23.0.2"
// 編譯選項
compileOptions {
sourceCompatibility JavaVersion. VERSION_1_7
targetCompatibility JavaVersion. VERSION_1_7
}
// recommend
dexOptions {
jumboMode = true
}
// 簽名配置
signingConfigs {
debug {
storeFile file( "../KeyDianda.jks")
storePassword "111111"
keyAlias "key_dianda"
keyPassword "111111"
}
release {
storeFile file( "..
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-1223-13-43-58.apk"
// proguard mapping file to build patch apk
tinkerApplyMappingPath = "${bakPath} /app-release-1223-13-43-58-mapping.txt"
// resource R.txt to build patch apk, must input if there is resource changed
tinkerApplyResourcePath = "${bakPath} /app-release-1223-13-43-58-R.txt"
// Todo 需要修改
tinkerBuildFlavorDirectory = "${bakPath} /app-release-1223-13-43-58"
}
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 : gitSha()
}
def buildWithTinker() {
return hasProperty( "TINKER_ENABLE") ? TINKER_ENABLE : ext.tinkerEnabled
}
def getTinkerBuildFlavorDirectory() {
return ext.tinkerBuildFlavorDirectory
}
if (buildWithTinker()) {
apply plugin: 'com.tencent.bugly.tinker-support'
// 依賴 tinker 插件
apply plugin: 'com.tencent.tinker.patch'
tinkerSupport {
}
// 全局資訊相關配置項
tinkerPatch {
oldApk = getOldApkPath() // 必選, 基準包路徑
ignoreWarning = false // 可選,預設 false
useSign = true // 可選,預設 true , 驗證基準 apk 和 patch 簽名是否一緻
// 編譯相關配置項
// Todo 需要修改 tinkerId 每次打更新檔時 tinkerId 跟打基準包時得 tinkerId 不能一樣
buildConfig {
applyMapping = getApplyMappingPath() // 可選,設定 mapping 檔案,建議保持舊 apk 的 proguard 混淆方式
applyResourceMapping = getApplyResourceMappingPath() // 可選,設定 R.txt 檔案,通過舊 apk 檔案保持 ResId 的配置設定
tinkerId = "bugbugbug_v2.0.0gggggg"
}
// dex 相關配置項
dex {
dexMode = "jar" // 可選,預設為 jar
usePreGeneratedPatchDex = true // 可選,預設為 false
pattern = [ "classes*.dex",
"assets/secondary-dex-?.jar"]
// Todo 需要修改 自己的包名
loader = [ "com.tencent.tinker.loader.*",
"com.tinker.ddinfo.SampleApplication",
]
}
// lib 相關的配置項
lib {
pattern = [ "lib/armeabi.so"]
}
// res 相關的配置項
res {
pattern = [ "res", "assets", "resources.arsc", "AndroidManifest.xml"]
ignoreChange = [ "assets/sample_meta.txt"]
largeModSize = 100
}
// 用于生成更新檔包中的 'package_meta.txt' 檔案
packageConfig {
configField( "patchMessage", "tinker is sample to use")
configField( "platform", "all")
configField( "patchVersion", "1.0")
}
// 7zip 路徑配置項,執行前提是 useSign 為 true
sevenZip {
zipArtifact = "com.tencent.mm:SevenZip:1.1.10" // optional
// path = "/usr/local/bin/7za" // optional
}
}
List<String> flavors = new ArrayList<>();
project. android.productFlavors.each { flavor ->
flavors.add(flavor.name)
}
boolean hasFlavors = flavors.size() > 0
android. applicationVariants.all { variant ->
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.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")
}
}
}
}
}
}
project.afterEvaluate {
//sample use for build all flavor for one time
if (hasFlavors) {
task( tinkerPatchAllFlavorRelease) {
group = 'tinker'
def originOldPath = getTinkerBuildFlavorDirectory()
for (String flavor : flavors) {
def tinkerTask = tasks.getByName( "tinkerPatch${flavor.capitalize()} Release")
dependsOn tinkerTask
def 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 tinkerTask
def 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"
}
}
}
}
}
}
*這裡要注意 需要修改tinkerId每次打更新檔時tinkerId跟打基準包時得tinkerId不能一樣
第三步: package com.tinker.ddinfo ;
import com.tencent.tinker.loader.app.TinkerApplication ;
import com.tencent.tinker.loader.shareutil.ShareConstants ;
public class SampleApplication extends TinkerApplication {
public SampleApplication() {
super(ShareConstants. TINKER_ENABLE_ALL , "com.tinker.ddinfo.SampleApplicationLike" ,//修改成自己的包名
"com.tencent.tinker.loader.TinkerLoader" , false) ;
}
}
第四步: 自定義ApplicationLike package com.tinker.ddinfo;
import android.annotation.TargetApi;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.os.Build;
import android.support.multidex.MultiDex;
import com.tencent.bugly.Bugly;
import com.tencent.bugly.beta.Beta;
import com.tencent.tinker.loader.app.DefaultApplicationLike;
import com.tinker.ddinfo.utils.ExampleConfig;
public class SampleApplicationLike extends DefaultApplicationLike {
public static final String TAG = "Tinker.SampleApplicationLike";
public SampleApplicationLike(Application application, int tinkerFlags,
boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime,
long applicationStartMillisTime, Intent tinkerResultIntent, Resources[] resources,
ClassLoader[] classLoader, AssetManager[] assetManager) {
super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime,
applicationStartMillisTime, tinkerResultIntent, resources, classLoader,
assetManager);
}
@Override
public void onCreate() {
super.onCreate();
// TODO: 這裡進行Bugly初始化
// 設定開發裝置
Bugly.setIsDevelopmentDevice(getApplication(), true);
// 這裡實作SDK初始化,appId替換成你的在Bugly平台申請的appId
Bugly.init(getApplication(), ExampleConfig.BUGLY_APP_ID, true);
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
@Override
public void onBaseContextAttached(Context base) {
super.onBaseContextAttached(base);
// you must install multiDex whatever tinker is installed!
MultiDex.install(base);
// TODO: 安裝tinker
Beta.installTinker(this);
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public void registerActivityLifecycleCallback(Application.ActivityLifecycleCallbacks callbacks) {
getApplication().registerActivityLifecycleCallbacks(callbacks);
}
} 第五步:Androidmanifest 檔案
第六步:MainActivity 這個類是為了配合顯示 修改bug
第七步:根據指令打基準包 (就是apk包) 在bakAPK 目錄下
第八步:這時要注意打完基準包後 ,需要修改bug 我在mainActivity 裡面修改了一行Toast代碼
然後修改 app 下面的build.gradle 檔案
記得需要修改 tinkerId
第九步:執行tinkerPatchRelease 指令 打更新檔包
第十步 上傳 到騰訊Bugly 平台。
需要注意的是 在手機應用上 需要 冷啟動 ,殺掉程序 重新 進入應用才會成功。 有時候需要等一兩分鐘
最後附上 demo 下載下傳連結
http://download.csdn.net/detail/weitf/9719535