前言:
最近工作不是很忙尋思著學習學習一些新技術or熱門技術,熱修復,聽著好高大上啊,安卓高階程式設計師必會技能(高逼格技能~~)。熱修復這一兩年確實很火,技術文章滿天飛,BAT等大公司都有一套自己的熱修復框架,Tinker、Dexposed、AndFix、HotFix、Nuwa等等,熱修復有兩大流派:
Native,代表有阿里的Dexposed、AndFix與騰訊的內部方案KKFix;
Java,代表有Qzone的超級補丁、大眾點評的nuwa、百度金融的rocooFix, 餓了麼的amigo以及美團的robust。
Native流派與Java流派都有著自己的優缺點,它們具體差異可以參考這裡~~,這些框架GitHub上都有實現原理和踩坑方法~~大家可以根據需求選擇最適合自己的熱修復框架。好了我們直奔主題!!
一、為什麼使用Tinker?
出至騰訊是微信官方的Android熱補丁解決方案,我們每使用一個開源框架需要考慮的是效能、相容性成功率、後期維護等。首先效能和相容性大家看這裡,作為一個擁有9億使用者的超級app,Tinker能夠作為微信熱修復支撐,極致的效能和相容性是必須的,也是Tinker開發的初衷,經過了一系列的版本迭代到最新的1.9.2已經越發穩定成熟,然後再是維護,從16年9月第一次釋出版本到目前為止更新了17個版本,2178次commit。。。可見微信團隊一直在致力維護這個專案,而AndFix已經兩年沒更新了,issue也是一大堆問題沒人解決。。看來阿里爸爸是放棄這個框架了~~~,dexposed也是一個樣。。。貼個圖各個熱修復框架優勢對比:
二、開始採坑之旅
1、安卓Tinker整合
新建專案配置project的gradle:ps因為Tinker支援gradle配置,配置屬性比較多最好直接拷貝過來再把你原有的gradle屬性配置上去(要看懂這些配置屬性是什麼意思,需要有一定的gradle知識~~~)
buildscript {
repositories {
mavenLocal()
google()
jcenter()
}
dependencies {
if (project.hasProperty('GRADLE_3') && GRADLE_3.equalsIgnoreCase('TRUE')) {
classpath 'com.android.tools.build:gradle:3.0.0'
} else {
classpath 'com.android.tools.build:gradle:2.3.3'
}
classpath "com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}"
}
}
allprojects {
repositories {
mavenLocal()
google()
jcenter()
}
}
def is_gradle_3() {
return hasProperty('GRADLE_3') && GRADLE_3.equalsIgnoreCase('TRUE')
}複製程式碼
這個會報錯:
別急,開啟gradle.properties在最後面加入:
TINKER_VERSION=1.9.2
GRADLE_3=true複製程式碼
然後配置app的gradle(我把整個放放出來):
apply plugin: 'com.android.application'
dependencies {
if (is_gradle_3()) {
implementation fileTree(dir: 'libs', include: ['*.jar'])
testImplementation 'junit:junit:4.12'
implementation "com.android.support:appcompat-v7:23.1.1"
implementation("com.tencent.tinker:tinker-android-lib:${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"
//use to test multiDex
// implementation group: 'com.google.guava', name: 'guava', version: '19.0'
// implementation "org.scala-lang:scala-library:2.11.7"
//use for local maven test
// implementation("com.tencent.tinker:tinker-android-loader:${TINKER_VERSION}") { changing = true }
// implementation("com.tencent.tinker:aosp-dexutils:${TINKER_VERSION}") { changing = true }
// implementation("com.tencent.tinker:bsdiff-util:${TINKER_VERSION}") { changing = true }
// implementation("com.tencent.tinker:tinker-ziputils:${TINKER_VERSION}") { changing = true }
// implementation("com.tencent.tinker:tinker-commons:${TINKER_VERSION}") { changing = true }
} else {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile "com.android.support:appcompat-v7:23.1.1"
compile("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
provided("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
compile "com.android.support:multidex:1.0.1"
//use to test multiDex
// compile group: 'com.google.guava', name: 'guava', version: '19.0'
// compile "org.scala-lang:scala-library:2.11.7"
//use for local maven test
// compile("com.tencent.tinker:tinker-android-loader:${TINKER_VERSION}") { changing = true }
// compile("com.tencent.tinker:aosp-dexutils:${TINKER_VERSION}") { changing = true }
// compile("com.tencent.tinker:bsdiff-util:${TINKER_VERSION}") { changing = true }
// compile("com.tencent.tinker:tinker-ziputils:${TINKER_VERSION}") { changing = true }
// compile("com.tencent.tinker:tinker-commons:${TINKER_VERSION}") { changing = true }
}
compile 'com.dx168.patchsdk:patchsdk:1.2.7'
}
def javaVersion = JavaVersion.VERSION_1_7
android {
compileSdkVersion 26
buildToolsVersion '26.0.2'
compileOptions {
sourceCompatibility javaVersion
targetCompatibility javaVersion
}
//recommend
dexOptions {
jumboMode = true
}
// signingConfigs {
// release {
// try {
// storeFile file("./keystore/release.keystore")
// storePassword "testres"
// keyAlias "testres"
// keyPassword "testres"
// } catch (ex) {
// throw new InvalidUserDataException(ex.toString())
// }
// }
//
// debug {
// storeFile file("./keystore/debug.keystore")
// }
// }
defaultConfig {
applicationId "com.oking.mytinker"
minSdkVersion 14
targetSdkVersion 22
versionCode 1
versionName "1.0.0"
/**
* 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\""
}
// aaptOptions{
// cruncherEnabled false
// }
// //use to test flavors support
// productFlavors {
// flavor1 {
// applicationId 'tinker.sample.android.flavor1'
// }
//
// flavor2 {
// applicationId 'tinker.sample.android.flavor2'
// }
// }
buildTypes {
release {
minifyEnabled true
// signingConfig signingConfigs.release
proguardFiles getDefaultProguardFile('proguard-android.txt'), project.file('proguard-rules.pro')
}
debug {
debuggable true
minifyEnabled false
// signingConfig signingConfigs.debug
}
}
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
}
def bakPath = file("${buildDir}/bakApk/")
/**
* you can use assembleRelease to build you base apk
* use tinkerPatchRelease -POLD_APK= -PAPPLY_MAPPING= -PAPPLY_RESOURCE= to build patch
* add apk from the build/bakApk
*/
ext {
// 是否使用Tinker(當你的專案處於開發除錯階段時,可以改為false)
tinkerEnabled = true
// 基礎包檔案路徑(名字這裡寫死為old-app.apk。用於比較新舊app以生成補丁包,不管是debug還是release編譯)
tinkerOldApkPath = "${bakPath}/old-app.apk"
// 基礎包的mapping.txt檔案路徑(用於輔助混淆補丁包的生成,一般在生成release版app時會使用到混淆,所以這個mapping.txt檔案一般只是用於release安裝包補丁的生成)
tinkerApplyMappingPath = "${bakPath}/old-app-mapping.txt"
// 基礎包的R.txt檔案路徑(如果你的安裝包中資原始檔有改動,則需要使用該R.txt檔案來輔助生成補丁包)
tinkerApplyResourcePath = "${bakPath}/old-app-R.txt"
//only use for build all flavor, if not, just ignore this field
tinkerBuildFlavorDirectory = "${bakPath}/flavor"
}
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 buildWithTinker() {
return hasProperty("TINKER_ENABLE") ? TINKER_ENABLE : ext.tinkerEnabled
}
def getTinkerBuildFlavorDirectory() {
return ext.tinkerBuildFlavorDirectory
}
if (buildWithTinker()) {
apply plugin: 'com.tencent.tinker.patch'
tinkerPatch {
/**
* necessary,default 'null'
* the old apk path, use to diff with the new apk to build
* add apk from the build/bakApk
*/
oldApk = getOldApkPath()
/**
* optional,default 'false'
* there are some cases we may get some warnings
* if ignoreWarning is true, we would just assert the patch process
* case 1: minSdkVersion is below 14, but you are using dexMode with raw.
* it must be crash when load.
* case 2: newly added Android Component in AndroidManifest.xml,
* it must be crash when load.
* case 3: loader classes in dex.loader{} are not keep in the main dex,
* it must be let tinker not work.
* case 4: loader classes in dex.loader{} changes,
* loader classes is ues to load patch dex. it is useless to change them.
* it won't crash, but these changes can't effect. you may ignore it
* case 5: resources.arsc has changed, but we don't use applyResourceMapping to build
*/
ignoreWarning = true // 是否忽略有風險的補丁包。這裡選擇忽略,當補丁包風險時不會中斷編譯。
/**
* optional,default 'true'
* whether sign the patch file
* if not, you must do yourself. otherwise it can't check success during the patch loading
* we will use the sign config with your build type
*/
useSign = true
/**
* optional,default 'true'
* whether use tinker to build
*/
tinkerEnable = buildWithTinker()
/**
* Warning, applyMapping will affect the normal android build!
*/
buildConfig {
/**
* optional,default 'null'
* if we use tinkerPatch to build the patch apk, you'd better to apply the old
* apk mapping file if minifyEnabled is enable!
* Warning:
* you must be careful that it will affect the normal assemble build!
*/
applyMapping = getApplyMappingPath()
/**
* optional,default 'null'
* It is nice to keep the resource id from R.txt file to reduce java changes
*/
applyResourceMapping = getApplyResourceMappingPath()
/**
* necessary,default 'null'
* because we don't want to check the base apk with md5 in the runtime(it is slow)
* tinkerId is use to identify the unique base apk when the patch is tried to apply.
* we can use git rev, svn rev or simply versionCode.
* we will gen the tinkerId in your manifest automatic
*/
tinkerId = getTinkerIdValue()
/**
* if keepDexApply is true, class in which dex refer to the old apk.
* open this can reduce the dex diff file size.
*/
keepDexApply = false
/**
* optional, default 'false'
* Whether tinker should treat the base apk as the one being protected by app
* protection tools.
* If this attribute is true, the generated patch package will contain a
* dex including all changed classes instead of any dexdiff patch-info files.
*/
isProtectedApp = false
/**
* optional, default 'false'
* Whether tinker should support component hotplug (add new component dynamically).
* If this attribute is true, the component added in new apk will be available after
* patch is successfully loaded. Otherwise an error would be announced when generating patch
* on compile-time.
*
* <b>Notice that currently this feature is incubating and only support NON-EXPORTED Activity</b>
*/
supportHotplugComponent = false
}
dex {
/**
* optional,default 'jar'
* 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 = "jar"
/**
* necessary,default '[]'
* what dexes in apk are expected to deal with tinkerPatch
* it support * or ? pattern.
*/
pattern = ["classes*.dex",
"assets/secondary-dex-?.jar"]
/**
* necessary,default '[]'
* 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, and the classes you use in them
*
*/
loader = [
//use sample, let BaseBuildInfo unchangeable with tinker
"tinker.sample.android.app.BaseBuildInfo"
]
}
lib {
/**
* optional,default '[]'
* what library in apk are expected to deal with tinkerPatch
* it support * or ? pattern.
* for library in assets, we would just recover them in the patch directory
* you can get them in TinkerLoadResult with Tinker
*/
pattern = ["lib/*/*.so"]
}
res {
/**
* optional,default '[]'
* what resource in apk are expected to deal with tinkerPatch
* it support * or ? pattern.
* you must include all your resources in apk here,
* otherwise, they won't repack in the new apk resources.
*/
pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
/**
* optional,default '[]'
* the resource file exclude patterns, ignore add, delete or modify resource change
* it support * or ? pattern.
* Warning, we can only use for files no relative with resources.arsc
*/
ignoreChange = ["assets/sample_meta.txt"]
/**
* default 100kb
* for modify resource, if it is larger than 'largeModSize'
* we would like to use bsdiff algorithm to reduce patch file size
*/
largeModSize = 100
}
packageConfig {
/**
* optional,default 'TINKER_ID, TINKER_ID_VALUE' 'NEW_TINKER_ID, NEW_TINKER_ID_VALUE'
* package meta file gen. path is assets/package_meta.txt in patch file
* you can use securityCheck.getPackageProperties() in your ownPackageCheck method
* or TinkerLoadResult.getPackageConfigByName
* we will get the TINKER_ID from the old apk manifest for you automatic,
* other config files (such as patchMessage below)is not necessary
*/
configField("patchMessage", "tinker is sample to use")
/**
* just a sample case, you can use such as sdkVersion, brand, channel...
* you can parse it in the SamplePatchListener.
* Then you can use patch conditional!
*/
configField("platform", "all")
/**
* patch version via packageConfig
*/
configField("patchVersion", "1.0")
}
//or you can add config filed outside, or get meta value from old apk
//project.tinkerPatch.packageConfig.configField("test1", project.tinkerPatch.packageConfig.getMetaDataFromOldApk("Test"))
//project.tinkerPatch.packageConfig.configField("test2", "sample")
/**
* if you don't use zipArtifact or path, we just use 7za to try
*/
sevenZip {
/**
* optional,default '7za'
* the 7zip artifact path, it will use the right 7za with your platform
*/
zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
/**
* optional,default '7za'
* you can specify the 7za path yourself, it will overwrite the zipArtifact value
*/
// path = "/usr/local/bin/7za"
}
}
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")
/**
* bak apk and mapping
*/
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
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"
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"
}
}
}
}
}
}
複製程式碼
在這裡特別需要注意的幾點:
1)、如果出現這個錯誤
請檢查這個是否配置正確
def getTinkerIdValue() {
return hasProperty("TINKER_ID") ? TINKER_ID : android.defaultConfig.versionName
}複製程式碼
ps:我拷貝官方demo例子的gradle配置就報這個錯,後來把它改成上面一樣就行了。
2)、把這個改為true:ignoreWarning = true // 是否忽略有風險的補丁包。這裡選擇忽略,當補丁包風險時不會中斷編譯。不然在生成補丁包會出錯。
3)、然後就是這個:
tinkerOldApkPath:基礎包所在路徑複製程式碼
之所以改成old-app.apk,主要是為了區分新的apk和基礎包,因為每次build一次都會生成一個apk
雖然每次都不同,你不改下名字根據編號很容易搞錯。官方例子是用的apk名稱ps:old-app.apk和old-app-mapping.txt很重要!!!你後面製作補丁都需要根據這個基礎包來製作,最好儲存好並備份!!,如果不小心弄丟了,那就只能推送更新app吧~~~
基礎包:指的是釋出出去的安裝包(使用者正在使用的安裝包)。
4)、注意這個BuildInfo引用的BuildConfig類:
一開始會報錯,找不到這個BuildConfig類,你build一下就出來了。ps:我能說我直接把這個BuildInfo給的PLATFORM改成空了麼,在後面打補丁死活打不上~~~~~,主要還是gradle配置不能出錯~~~~
這裡的欄位都是gradle配置的時候生成的。
配置好gradle後build下,看下是否成功,然後切換project檢視app>build目錄下是否生成bakApk目錄,上圖所示。
開啟gradle操作介面雙擊TinkerPatchDebug,生成補丁包
切換project檢視app>build>outputs>tinkerPatch目錄下看是否生成補丁:
解釋:
如果失敗或者沒有生成這些個資料夾,請仔細檢查你的gradle配置是否正確~~。
拷貝java檔案(官方demo的這些檔案~~):
這些只是對Tinker功能的擴充和封裝,都是可選的,你也可以自己封裝。
TinkerApplication,這個類並不是繼承Application,如果自己有自定義的application可以把初始化操作放在onCreat()方法裡面:
@SuppressWarnings("unused")
@DefaultLifeCycle(application = "com.oking.mytinker.OriginalApplication",// application類名。只能用字串,這個MyApplication檔案是不存在的,但可以在AndroidManifest.xml的application標籤上使用(name)
flags = ShareConstants.TINKER_ENABLE_ALL,// tinkerFlags
loaderClass = "com.tencent.tinker.loader.TinkerLoader",//loaderClassName, 我們這裡使用預設即可!(可不寫)
loadVerifyFlag = false)//tinkerLoadVerifyFlag
public class TinkerApplication extends DefaultApplicationLike {
private Application mApplication;
private Context mContext;
private Tinker mTinker;
// 固定寫法
public TinkerApplication(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
}
// 固定寫法
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {
getApplication().registerActivityLifecycleCallbacks(callback);
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
@Override
public void onBaseContextAttached(Context base) {
super.onBaseContextAttached(base);
mApplication = getApplication();
mContext = getApplication();
initTinker(base);
// 可以將之前自定義的Application中onCreate()方法所執行的操作搬到這裡...
}
private void initTinker(Context base) {
// tinker需要你開啟MultiDex
MultiDex.install(base);
TinkerManager.setTinkerApplicationLike(this);
// 設定全域性異常捕獲
TinkerManager.initFastCrashProtect();
//開啟升級重試功能(在安裝Tinker之前設定)
TinkerManager.setUpgradeRetryEnable(true);
//設定Tinker日誌輸出類
TinkerInstaller.setLogIml(new MyLogImp());
//安裝Tinker(在載入完multiDex之後,否則你需要將com.tencent.tinker.**手動放到main dex中)
TinkerManager.installTinker(this);
mTinker = Tinker.with(getApplication());
}
@Override
public void onCreate() {
super.onCreate();
// 將之前自定義的Application中onCreate()方法所執行的操作搬到這裡...
String appId = "20171213203556412-8689";
String appSecret = "cd34d15329cb4caeac3bbd4dc335707d";
PatchManager.getInstance().init(getApplication(), "http://192.168.0.105:8080/hotfix-apis", appId, appSecret, new IPatchManager() {
@Override
public void cleanPatch(Context context) {
// TinkerInstaller.cleanPatch(context);
}
@Override
public void patch(Context context, String patchPath) {
// TinkerInstaller.onReceiveUpgradePatch(context, patchPath);
Contact.patchPath = patchPath;
System.out.println("patch:"+patchPath);
}
});
PatchManager.getInstance().register(new Listener() {
@Override
public void onQuerySuccess(String response) {
Log.d("TinkerApplication","獲取補丁成功"+response);
}
@Override
public void onQueryFailure(Throwable e) {
Log.d("TinkerApplication","獲取補丁失敗"+e.getMessage());
}
@Override
public void onDownloadSuccess(String path) {
Log.d("TinkerApplication","下載補丁成功"+path);
}
@Override
public void onDownloadFailure(Throwable e) {
}
@Override
public void onPatchSuccess() {
}
@Override
public void onPatchFailure(String error) {
}
@Override
public void onLoadSuccess() {
}
@Override
public void onLoadFailure(String error) {
}
});
PatchManager.getInstance().setTag("");
PatchManager.getInstance().setChannel("");
PatchManager.getInstance().queryAndPatch();
}
}
複製程式碼
清單檔案:
許可權
<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=".OriginalApplication"複製程式碼
Service
<service
android:name="com.oking.mytinker.tinker.SampleResultService"
android:exported="false"/>複製程式碼
三、伺服器搭建
1、下載部署所需要的檔案(war包、配置檔案、建庫sql檔案) war包下載.
2、在mysql(需要5.x版本)裡面建資料庫,建表sql在patchserver-manager/import.sql中
3、把hotfix-apis.properties和hotfix-console.properties兩個配置檔案放到/opt/config(*如果是windows部署,放置在tomcat對應的碟符下,假如tomcat在d://tomcat 目錄配置檔案就放在d://opt/config目錄下,並且修改裡面對應的配置(資料來源配置、訪問路徑配置、補丁存放目錄)
4、把hotfix-apis.war hotfix-console.war放到tomcat下面的webapps目錄下
等服務啟動完畢就可以在瀏覽器上訪問http://localhost:8080/hotfix-console
會配置Tomcat或者會一點Javaee方面的知識的部署起來會很容易。
下載Tomcat配置好環境變數
安裝資料庫Mysql新建資料庫patch_manager,ps:注意名稱要和配置檔案資料庫名稱一致
修改hotfix-apis.properties配置檔案
修改hotfix-console.properties檔案
注意上面加粗說明,檔案別放錯了!!給個部署參照
拷貝war包到tomcat的webapp目錄下
執行Tomcat伺服器
訪問地址,註冊登入。
到目前為止我們已經把Tinker整合好了,伺服器也部署成功了,下面我們來擼程式碼~~~
四、擼程式碼、實操
app的Gradle加入一行
compile 'com.dx168.patchsdk:patchsdk:1.2.7'複製程式碼
上面整合Tinker的時候有加上就不用加了
TinkerApplication的onCreat方法裡面加入:
String appId = "20171214154046922-6495";
String appSecret = "9f820d28ac854e9a82e755fefd69ea63";
PatchManager.getInstance().init(getApplication(), "http://192.168.0.105:8080/hotfix-apis", appId, appSecret, new IPatchManager() {
@Override
public void cleanPatch(Context context) {
// TinkerInstaller.cleanPatch(context);
}
@Override
public void patch(Context context, String patchPath) {
// TinkerInstaller.onReceiveUpgradePatch(context, patchPath);
Contact.patchPath = patchPath;
System.out.println("patch:"+patchPath);
}
});
PatchManager.getInstance().register(new Listener() {
@Override
public void onQuerySuccess(String response) {
Log.d("TinkerApplication","獲取補丁成功"+response);
}
@Override
public void onQueryFailure(Throwable e) {
Log.d("TinkerApplication","獲取補丁失敗"+e.getMessage());
}
@Override
public void onDownloadSuccess(String path) {
Log.d("TinkerApplication","下載補丁成功"+path);
}
@Override
public void onDownloadFailure(Throwable e) {
}
@Override
public void onPatchSuccess() {
}
@Override
public void onPatchFailure(String error) {
}
@Override
public void onLoadSuccess() {
}
@Override
public void onLoadFailure(String error) {
}
});
PatchManager.getInstance().setTag("");
PatchManager.getInstance().setChannel("");
PatchManager.getInstance().queryAndPatch();複製程式碼
我們登入補丁管理後臺,建立應用後面會得到appId和appSecret,儲存好:
用過第三方平臺的都明白是什麼意思,這個也是一樣的。
建立一個版本:
在AS中build project
然後把安裝包併傳送到手機上進行安裝(ps我這直接改了名字了~~):
安卓介面我們長這樣~~~ps注意中間文字和Toast~:
假設我們專案上線了突然出現緊急bug(如果用檢測更新推送新安裝包,這個使用者體驗就很不好了~~安裝包不要流量?一個小小的bug就讓我下載安裝包更新?差評啊,解除安裝~~~)
我們修復好的程式碼:
然後,我們找到上個版本安裝包和txt檔案把它放在bakApk目錄下改名為old-app,切換到gradle檢視雙擊tinkerPatchDebug:
找到patch_signed_7zip.apk補丁包,拷貝到桌面
進入補丁管理後臺上傳補丁:
釋出補丁:
重新開啟app點選“修復”按鈕:
檢視控制檯列印日誌
app介面有修復成功提示
然後點退出,重新開啟應用我們上面修改的程式碼都同步到app上來了,使用者不用重新下載安裝包且無感知修復bug:
整個熱修復過程就這樣~~~說實話有點複雜~~
ps:修復成功後再點選修復按鈕是沒用的,一個補丁成功完成一次修復使命也就結束了,同時補丁檔案也被刪除了(不用擔心補丁塞滿sd卡啥的~~),即使你再次用同一個補丁也是打不了補丁的,這就是補丁的作用,Tinker為我們做了處理,不過你可以解除安裝某個版本的補丁或者全部補丁~~~,具體請看文件。。。。
五、總結
Tinker的熱修復遠不止我講的這些像資原始檔、so、library修復等,更深層次原理性的東西還需要進一步去學習~~~官方WIKI文件。
關於那個補丁管理平臺,開源的後臺,下發補丁,管理補丁、補丁統計、黑名單機制等等。還有原始碼,有興趣的大佬可以去研究研究改造改造~~,這樣我們就不用出錢給第三方補丁平臺了。。。如果單純的就下發補丁,可以不用這個平臺,後臺寫個類似版本更新的介面就可以,下載檔案>修復。平臺的好處就是更加方便、智慧化管理、統計資料。