Android熱更新:微信Tinker框架的接入與測試
轉載於:http://www.jianshu.com/p/aadcf2ea69a6
Android熱修復框架的對比(最終選擇微信Tinker)
Android熱修復框架的對比
- AndFix作為native解決方案,首先面臨的是穩定性與相容性問題,更重要的是它無法實現類替換,它是需要大量額外的開發成本的;
- Robust相容性與成功率較高,但是它與AndFix一樣,無法新增變數與類只能用做的bugFix方案;
- Qzone方案可以做到釋出產品功能,但是它主要問題是插樁帶來Dalvik的效能問題,以及為了解決Art下記憶體地址問題而導致補丁包急速增大的。
特別是在Android N之後,由於混合編譯的inline策略修改,對於市面上的各種方案都不太容易解決。而Tinker熱補丁方案不僅支援類、So以及資源的替換,它還是2.X-7.X的全平臺支援。利用Tinker我們不僅可以用做bugfix,甚至可以替代功能的釋出。Tinker已執行在微信的數億Android裝置上,那麼為什麼你不使用Tinker呢?
Tinker的已知問題
- Tinker不支援修改AndroidManifest.xml,Tinker不支援新增四大元件;
- 由於Google Play的開發者條款限制,不建議在GP渠道動態更新程式碼;
- 在Android N上,補丁對應用啟動時間有輕微的影響;
- 不支援部分三星android-21機型,載入補丁時會主動丟擲"TinkerRuntimeException:checkDexInstall failed";
- 由於各個廠商的加固實現並不一致,在1.7.6以及之後的版本,tinker不再支援加固的動態更新;
- 對於資源替換,不支援修改remoteView。例如transition動畫,notification icon以及桌面圖示。
一、接入Tinker(文末有 Demo 的 github 連結)
步驟一:專案的build.gradle檔案
// Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:2.2.0' classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.7.7')//加入tinker // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } allprojects { repositories { jcenter() } } task clean(type: Delete) { delete rootProject.buildDir }
步驟二:app的build.gradle檔案
以下這些只是基本測試通過的屬性,Tinker官方github上面還有更多可選可設定的屬性,如果還需要設定更多,請移步至 Tinker 官方github接入指南 檢視。(如果覺得官方文件看起來有點迷惑的同學,直接按照我下面的來做就好了)
apply plugin: 'com.android.application'
def javaVersion = JavaVersion.VERSION_1_7
android {
compileSdkVersion 23
buildToolsVersion "23.0.2"
compileOptions {
sourceCompatibility javaVersion
targetCompatibility javaVersion
}
//recommend
dexOptions {
jumboMode = true
}
defaultConfig {
applicationId "com.tinker.deeson.mytinkerdemo"
minSdkVersion 15
targetSdkVersion 22
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
buildConfigField "String", "MESSAGE", "\"I am the base apk\""
buildConfigField "String", "TINKER_ID", "\"${getTinkerIdValue()}\""
buildConfigField "String", "PLATFORM", "\"all\""
}
signingConfigs {
release {
try {
storeFile file("./keystore/TinkerDemo.keystore")
storePassword "TinkerDemo"
keyAlias "TinkerDemo"
keyPassword "TinkerDemo"
} catch (ex) {
throw new InvalidUserDataException(ex.toString())
}
}
debug {
storeFile file("./keystore/TinkerDemo.keystore")
storePassword "TinkerDemo"
keyAlias "TinkerDemo"
keyPassword "TinkerDemo"
}
}
buildTypes {
release {
minifyEnabled true
signingConfig signingConfigs.release
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
debuggable true
minifyEnabled false
signingConfig signingConfigs.debug
}
}
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile "com.android.support:appcompat-v7:23.1.1"
testCompile 'junit:junit:4.12'
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"
}
def gitSha() {
try {
// String gitRev = 'git rev-parse --short HEAD'.execute(null, project.rootDir).text.trim()
String gitRev = "1008611"
if (gitRev == null) {
throw new GradleException("can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'")
}
return gitRev
} catch (Exception e) {
throw new GradleException("can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'")
}
}
def bakPath = file("${buildDir}/bakApk/")
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-0421-12-34-45.apk"
//proguard mapping file to build patch apk
tinkerApplyMappingPath = "${bakPath}/app-release-0421-12-34-45-mapping.txt"
//resource R.txt to build patch apk, must input if there is resource changed
tinkerApplyResourcePath = "${bakPath}/app-release-0421-12-34-45-R.txt"
//only use for build all flavor, if not, just ignore this field
tinkerBuildFlavorDirectory = "${bakPath}/app-0421-12-34-45"
}
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.tinker.patch'
tinkerPatch {
/**
* 預設為null
* 將舊的apk和新的apk建立關聯
* 從build / bakApk新增apk
*/
oldApk = getOldApkPath()
/**
* 可選,預設'false'
*有些情況下我們可能會收到一些警告
*如果ignoreWarning為true,我們只是斷言補丁過程
* case 1:minSdkVersion低於14,但是你使用dexMode與raw。
* case 2:在AndroidManifest.xml中新新增Android元件,
* case 3:裝載器類在dex.loader {}不保留在主要的dex,
* 它必須讓tinker不工作。
* case 4:在dex.loader {}中的loader類改變,
* 載入器類是載入補丁dex。改變它們是沒有用的。
* 它不會崩潰,但這些更改不會影響。你可以忽略它
* case 5:resources.arsc已經改變,但是我們不使用applyResourceMapping來構建
*/
ignoreWarning = false
/**
*可選,預設為“true”
* 是否簽名補丁檔案
* 如果沒有,你必須自己做。否則在補丁載入過程中無法檢查成功
* 我們將使用sign配置與您的構建型別
*/
useSign = true
/**
可選,預設為“true”
是否使用tinker構建
*/
tinkerEnable = buildWithTinker()
/**
* 警告,applyMapping會影響正常的android build!
*/
buildConfig {
/**
*可選,預設為'null'
* 如果我們使用tinkerPatch構建補丁apk,你最好應用舊的
* apk對映檔案如果minifyEnabled是啟用!
* 警告:你必須小心,它會影響正常的組裝構建!
*/
applyMapping = getApplyMappingPath()
/**
*可選,預設為'null'
* 很高興保持資源ID從R.txt檔案,以減少java更改
*/
applyResourceMapping = getApplyResourceMappingPath()
/**
*必需,預設'null'
* 因為我們不想檢查基地apk與md5在執行時(它是慢)
* tinkerId用於在試圖應用補丁時標識唯一的基本apk。
* 我們可以使用git rev,svn rev或者簡單的versionCode。
* 我們將在您的清單中自動生成tinkerId
*/
tinkerId = getTinkerIdValue()
/**
*如果keepDexApply為true,則表示dex指向舊apk的類。
* 開啟這可以減少dex diff檔案大小。
*/
keepDexApply = false
}
dex {
/**
*可選,預設'jar'
* 只能是'raw'或'jar'。對於原始,我們將保持其原始格式
* 對於jar,我們將使用zip格式重新包裝dexes。
* 如果你想支援下面14,你必須使用jar
* 或者你想儲存rom或檢查更快,你也可以使用原始模式
*/
dexMode = "jar"
/**
*必需,預設'[]'
* apk中的dexes應該處理tinkerPatch
* 它支援*或?模式。
*/
pattern = ["classes*.dex",
"assets/secondary-dex-?.jar"]
/**
*必需,預設'[]'
* 警告,這是非常非常重要的,載入類不能隨補丁改變。
* 因此,它們將從補丁程式中刪除。
* 你必須把下面的類放到主要的dex。
* 簡單地說,你應該新增自己的應用程式{@code tinker.sample.android.SampleApplication}
* 自己的tinkerLoader,和你使用的類
*
*/
loader = [
//use sample, let BaseBuildInfo unchangeable with tinker
"tinker.sample.android.app.BaseBuildInfo"
]
}
lib {
/**
可選,預設'[]'
apk中的圖書館應該處理tinkerPatch
它支援*或?模式。
對於資源庫,我們只是在補丁目錄中恢復它們
你可以得到他們在TinkerLoadResult與Tinker
*/
pattern = ["lib/armeabi/*.so"]
}
res {
/**
*可選,預設'[]'
* apk中的什麼資源應該處理tinkerPatch
* 它支援*或?模式。
* 你必須包括你在這裡的所有資源,
* 否則,他們不會重新包裝在新的apk資源。
*/
pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
/**
*可選,預設'[]'
*資原始檔排除模式,忽略新增,刪除或修改資源更改
* *它支援*或?模式。
* *警告,我們只能使用檔案沒有relative與resources.arsc
*/
ignoreChange = ["assets/sample_meta.txt"]
/**
*預設100kb
* *對於修改資源,如果它大於'largeModSize'
* *我們想使用bsdiff演算法來減少補丁檔案的大小
*/
largeModSize = 100
}
packageConfig {
/**
*可選,預設'TINKER_ID,TINKER_ID_VALUE','NEW_TINKER_ID,NEW_TINKER_ID_VALUE'
* 包元檔案gen。路徑是修補程式檔案中的assets / package_meta.txt
* 你可以在您自己的PackageCheck方法中使用securityCheck.getPackageProperties()
* 或TinkerLoadResult.getPackageConfigByName
* 我們將從舊的apk清單為您自動獲取TINKER_ID,
* 其他配置檔案(如下面的patchMessage)不是必需的
*/
configField("patchMessage", "tinker is sample to use")
/**
*只是一個例子,你可以使用如sdkVersion,品牌,渠道...
* 你可以在SamplePatchListener中解析它。
* 然後你可以使用補丁條件!
*/
configField("platform", "all")
/**
* 補丁版本通過packageConfig
*/
configField("patchVersion", "1.0")
}
//或者您可以新增外部的配置檔案,或從舊apk獲取元值
//project.tinkerPatch.packageConfig.configField("test1", project.tinkerPatch.packageConfig.getMetaDataFromOldApk("Test"))
//project.tinkerPatch.packageConfig.configField("test2", "sample")
/**
* 如果你不使用zipArtifact或者path,我們只是使用7za來試試
*/
sevenZip {
/**
* 可選,預設'7za'
* 7zip工件路徑,它將使用正確的7za與您的平臺
*/
zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
/**
* 可選,預設'7za'
* 你可以自己指定7za路徑,它將覆蓋zipArtifact值
*/
// path = "/usr/local/bin/7za"
}
}
List<String> flavors = new ArrayList<>();
project.android.productFlavors.each {flavor ->
flavors.add(flavor.name)
}
boolean hasFlavors = flavors.size() > 0
/**
* bak apk and mapping
*/
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.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"
}
}
}
}
}
}
步驟三:gradle.properties檔案
將下面這行 Tinker 的版本號新增到 gradle.properties 檔案中(Tinker的最新版本,請留意Tinker github)TINKER_VERSION=1.7.7
強烈建議同學們使用最新的版本,因為tinker 的wiki上面提到最新版本支援應用加固,見下圖
tinker 加固相關
步驟四:自己的application檔案
新建一個類,名字(SampleApplicationLike )隨意起,當然最好是意義明顯的,並繼承自DefaultApplicationLike ,注意,這裡並不是繼承 Application,這個是 Tinker 的推薦寫法。其他的註解和重寫的方法,照著寫就好了。最後自己的 Application 邏輯就寫在 onCreate() 方法裡面。
package com.tinker.deeson.mytinkerdemo;
import android.annotation.TargetApi;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.support.multidex.MultiDex;
import android.widget.Toast;
import com.tencent.tinker.anno.DefaultLifeCycle;
import com.tencent.tinker.lib.listener.DefaultPatchListener;
import com.tencent.tinker.lib.patch.UpgradePatch;
import com.tencent.tinker.lib.reporter.DefaultLoadReporter;
import com.tencent.tinker.lib.reporter.DefaultPatchReporter;
import com.tencent.tinker.lib.tinker.Tinker;
import com.tencent.tinker.lib.tinker.TinkerInstaller;
import com.tencent.tinker.loader.app.DefaultApplicationLike;
import com.tencent.tinker.loader.shareutil.ShareConstants;
@SuppressWarnings("unused")
@DefaultLifeCycle(application = "com.tinker.deeson.mytinkerdemo.SampleApplication",
flags = ShareConstants.TINKER_ENABLE_ALL,
loadVerifyFlag = false)
public class SampleApplicationLike extends DefaultApplicationLike {
private static final String TAG = "Tinker.SampleApplicationLike";
public SampleApplicationLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag,
long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
}
/**
* install multiDex before install tinker
* so we don't need to put the tinker lib classes in the main dex
*
* @param base
*/
@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);
TinkerInstaller.install(this,new DefaultLoadReporter(getApplication())
,new DefaultPatchReporter(getApplication()),new DefaultPatchListener(getApplication()),SampleResultService.class,new UpgradePatch());
Tinker tinker = Tinker.with(getApplication());
Toast.makeText(getApplication(),"載入完成", Toast.LENGTH_SHORT).show();
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {
getApplication().registerActivityLifecycleCallbacks(callback);
}
@Override
public void onCreate() {
super.onCreate();
//此處寫自己的Application邏輯
}
}
步驟五:註冊一個處理載入補丁結果的service(SampleResultService)
在步驟四中的application類裡,我們看到重寫的 onBaseContextAttached() 方法裡出現了一個繼承自 DefaultTinkerResultService 的 SampleResultService 類,而這個 SampleResultService 類就是我們在載入補丁後供 Tinker 回撥的一個類。Demo的service中所做的操作是在你載入成功熱更新外掛後,會提示你更新成功,並且這裡做了鎖屏操作就會載入熱更新外掛。然而,這個service裡的具體邏輯是可以根據自己專案的需求,具體設計。如下所示:
package com.tinker.deeson.mytinkerdemo;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Handler;
import android.os.Looper;
import android.widget.Toast;
import com.tencent.tinker.lib.service.DefaultTinkerResultService;
import com.tencent.tinker.lib.service.PatchResult;
import com.tencent.tinker.lib.util.TinkerLog;
import com.tencent.tinker.lib.util.TinkerServiceInternals;
import com.tencent.tinker.loader.shareutil.SharePatchFileUtil;
import java.io.File;
/**
* optional, you can just use DefaultTinkerResultService
* we can restart process when we are at background or screen off
*/
public class SampleResultService extends DefaultTinkerResultService {
private static final String TAG = "Tinker.SampleResultService";
@Override
public void onPatchResult(final PatchResult result) {
if (result == null) {
TinkerLog.e(TAG, "SampleResultService received null result!!!!");
return;
}
TinkerLog.i(TAG, "SampleResultService receive result: %s", result.toString());
//first, we want to kill the recover process
TinkerServiceInternals.killTinkerPatchServiceProcess(getApplicationContext());
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
if (result.isSuccess) {
Toast.makeText(getApplicationContext(), "patch success, please restart process", Toast.LENGTH_LONG).show();
} else {
Toast.makeText(getApplicationContext(), "patch fail, please check reason", Toast.LENGTH_LONG).show();
}
}
});
// is success and newPatch, it is nice to delete the raw file, and restart at once
// for old patch, you can't delete the patch file
if (result.isSuccess) {
File rawFile = new File(result.rawPatchFilePath);
if (rawFile.exists()) {
TinkerLog.i(TAG, "save delete raw patch file");
SharePatchFileUtil.safeDeleteFile(rawFile);
}
//not like TinkerResultService, I want to restart just when I am at background!
//if you have not install tinker this moment, you can use TinkerApplicationHelper api
if (checkIfNeedKill(result)) {
if (Utils.isBackground()) {
TinkerLog.i(TAG, "it is in background, just restart process");
restartProcess();
} else {
//we can wait process at background, such as onAppBackground
//or we can restart when the screen off
TinkerLog.i(TAG, "tinker wait screen to restart process");
new ScreenState(getApplicationContext(), new ScreenState.IOnScreenOff() {
@Override
public void onScreenOff() {
restartProcess();
}
});
}
} else {
TinkerLog.i(TAG, "I have already install the newly patch version!");
}
}
}
/**
* you can restart your process through service or broadcast
*/
private void restartProcess() {
TinkerLog.i(TAG, "app is background now, i can kill quietly");
//you can send service or broadcast intent to restart your process
android.os.Process.killProcess(android.os.Process.myPid());
}
static class ScreenState {
interface IOnScreenOff {
void onScreenOff();
}
ScreenState(Context context, final IOnScreenOff onScreenOffInterface) {
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_SCREEN_OFF);
context.registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent in) {
String action = in == null ? "" : in.getAction();
TinkerLog.i(TAG, "ScreenReceiver action [%s] ", action);
if (Intent.ACTION_SCREEN_OFF.equals(action)) {
context.unregisterReceiver(this);
if (onScreenOffInterface != null) {
onScreenOffInterface.onScreenOff();
}
}
}
}, filter);
}
}
}
步驟六:Utils工具類
package com.tinker.deeson.mytinkerdemo;
import android.os.Environment;
import android.os.StatFs;
import com.tencent.tinker.loader.shareutil.ShareConstants;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
public class Utils {
/**
* the error code define by myself
* should after {@code ShareConstants.ERROR_PATCH_INSERVICE
*/
public static final int ERROR_PATCH_GOOGLEPLAY_CHANNEL = -5;
public static final int ERROR_PATCH_ROM_SPACE = -6;
public static final int ERROR_PATCH_MEMORY_LIMIT = -7;
public static final int ERROR_PATCH_ALREADY_APPLY = -8;
public static final int ERROR_PATCH_CRASH_LIMIT = -9;
public static final int ERROR_PATCH_RETRY_COUNT_LIMIT = -10;
public static final int ERROR_PATCH_CONDITION_NOT_SATISFIED = -11;
public static final String PLATFORM = "platform";
public static final int MIN_MEMORY_HEAP_SIZE = 45;
private static boolean background = false;
public static boolean isGooglePlay() {
return false;
}
public static boolean isBackground() {
return background;
}
public static void setBackground(boolean back) {
background = back;
}
public static int checkForPatchRecover(long roomSize, int maxMemory) {
if (Utils.isGooglePlay()) {
return Utils.ERROR_PATCH_GOOGLEPLAY_CHANNEL;
}
if (maxMemory < MIN_MEMORY_HEAP_SIZE) {
return Utils.ERROR_PATCH_MEMORY_LIMIT;
}
//or you can mention user to clean their rom space!
if (!checkRomSpaceEnough(roomSize)) {
return Utils.ERROR_PATCH_ROM_SPACE;
}
return ShareConstants.ERROR_PATCH_OK;
}
public static boolean isXposedExists(Throwable thr) {
StackTraceElement[] stackTraces = thr.getStackTrace();
for (StackTraceElement stackTrace : stackTraces) {
final String clazzName = stackTrace.getClassName();
if (clazzName != null && clazzName.contains("de.robv.android.xposed.XposedBridge")) {
return true;
}
}
return false;
}
@Deprecated
public static boolean checkRomSpaceEnough(long limitSize) {
long allSize;
long availableSize = 0;
try {
File data = Environment.getDataDirectory();
StatFs sf = new StatFs(data.getPath());
availableSize = (long) sf.getAvailableBlocks() * (long) sf.getBlockSize();
allSize = (long) sf.getBlockCount() * (long) sf.getBlockSize();
} catch (Exception e) {
allSize = 0;
}
if (allSize != 0 && availableSize > limitSize) {
return true;
}
return false;
}
public static String getExceptionCauseString(final Throwable ex) {
final ByteArrayOutputStream bos = new ByteArrayOutputStream();
final PrintStream ps = new PrintStream(bos);
try {
// print directly
Throwable t = ex;
while (t.getCause() != null) {
t = t.getCause();
}
t.printStackTrace(ps);
return toVisualString(bos.toString());
} finally {
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private static String toVisualString(String src) {
boolean cutFlg = false;
if (null == src) {
return null;
}
char[] chr = src.toCharArray();
if (null == chr) {
return null;
}
int i = 0;
for (; i < chr.length; i++) {
if (chr[i] > 127) {
chr[i] = 0;
cutFlg = true;
break;
}
}
if (cutFlg) {
return new String(chr, 0, i);
} else {
return src;
}
}
}
步驟七:AndroidManifest.xml檔案
- 在application標籤里加入步驟四中新建的Application類
android:name=".SampleApplication"
,此處的名字需要與步驟四的SampleApplicationLike 類最頂部的@DefaultLifeCycle()註解保持一致。如果你新增不進去,或者是紅色的話,請先build一下。如下紅色圈中:
SampleApplicationLike - 註冊SampleResultService
- 加入訪問sdcard許可權,Android6.0以上的,請自行解決許可權問題,很簡單。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.tinker.deeson.mytinkerdemo">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application
android:name=".SampleApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
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=".SampleResultService"
android:exported="false" />
</application>
</manifest>
步驟八:MainActivity 類中對 Tinker API 的呼叫
只有兩個按鈕,一個是載入熱補丁外掛;一個是殺死應用載入補丁。
package com.tinker.deeson.mytinkerdemo;
import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import com.tencent.tinker.lib.tinker.TinkerInstaller;
import com.tencent.tinker.loader.shareutil.ShareTinkerInternals;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.btn_load).setOnClickListener(this);
findViewById(R.id.btn_kill).setOnClickListener(this);
}
@Override
protected void onResume() {
super.onResume();
Utils.setBackground(false);
}
@Override
protected void onPause() {
super.onPause();
Utils.setBackground(true);
}
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.btn_load:
loadPatch();
break;
case R.id.btn_kill:
killApp();
break;
}
}
/**
* 載入熱補丁外掛
*/
public void loadPatch() {
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),
Environment.getExternalStorageDirectory().getAbsolutePath() + "/myTinkerDemo/TinkerPatch");
}
/**
* 殺死應用載入補丁
*/
public void killApp() {
ShareTinkerInternals.killAllOtherProcess(getApplicationContext());
android.os.Process.killProcess(android.os.Process.myPid());
}
}
看看主介面的樣子,很樸素。
activity_main.xml
至此,我們對 Tinker 的接入已經完成了,剩下的就是對 Tinker 熱修復的測試。
二、測試Tinker
步驟一:打基礎包
點開 Android Studio的Gradle 介面,如下,雙擊 assembleDebug 或者 assembleRelease:
Gradle
注意看,專案目錄build資料夾裡面,在雙擊 assembleDebug 或者 assembleRelease 之前,是如下介面:
build資料夾
在雙擊 assembleDebug 或者 assembleRelease 之後,build資料夾下面會生成一個bakApk資料夾,裡面存放著我們的基礎包,裡面的apk檔案用於安裝到手機測試或者釋出到應用市場,(這裡生成的基礎安裝包和 R檔案以及release版本的mapping檔案一定要自己儲存好,因為通過後續的步驟你會清楚地看到,每次打補丁包都需要用到這些檔案作為基礎檔案,丟掉的話,後果就很滑稽臉了),如下:
- 如果打包失敗,請clean一下專案,再雙擊 assembleDebug 或者 assembleRelease;
- 如果clean之後再打包還失敗,那就需要看具體的報錯,慢慢除錯設定(首先很有可能是程式碼混淆的問題,文末有混淆相關的介紹文章,寫得很全面易懂)。
bakApk資料夾
安裝到手機後,發現,有bug,如下顯示,果斷接著步驟二,使用Tinker緊急熱修復這個bug:
有bug版本
步驟二:打補丁包
1.將步驟一生成的 bakApk 資料夾中的 apk 檔案和 R 檔案的名稱,填寫到app的 build.gradle 類的 ext 這裡,sync一下,如下:
build.gradle debug版
當然,如果在步驟一打的是release的基礎包的話,會多一個mapping檔案,同樣將它的名稱填寫到app的 build.gradle 類的 ext 這裡,介面如下:
build.gradle release版
2.接著,我們去修改主介面的bug,並增加一個圖片資原始檔(圖片自己找),如下:
activity_main.xml
3.接下來,真正地打補丁包,點開 Android Studio的Gradle 介面,如下,雙擊 tinkerPatchDebug 或者tinkerPatchRelease ,如下:
Gradle Tinker
4.緊接著,Tinker 在build 資料夾下的 outputs 資料夾裡面會生成我們需要的補丁檔案,patch_signed_7zip.apk 就是我們所要的補丁包,如下:
outputs tinkerPatch
當然,如果你想了解更多關於輸出檔案的情況,可以點開Tinker Wiki 的 輸出檔案詳解。
步驟三:將補丁包拷貝到手機sdcard中測試
將步驟二生成的 tinkerPatch 資料夾下面的 patch_signed_7zip.apk 檔案,拷貝出來,改成你的 MainActivity中載入的檔名字,demo這裡叫TinkerPatch,將其拷貝到手機的sdcard中的myTinkerDemo 資料夾下,沒有這個資料夾你就自己手動新建一個,下圖帶你回顧一下 MainActivity 的設定:
注意此處,測試和釋出版本的不同:釋出版本的補丁檔案一般是通過網路下載下來,存放到sdcard中,再載入。
MainActivity
步驟四:載入補丁
點選主頁的載入補丁按鈕,沒載入之前如下介面:
有bug
點選載入補丁之後,鎖屏或者殺死程序,再次進入demo,補丁已經加載出來,並且 sdcard中的補丁包也會被刪除掉,因為它和老apk合併了。如下:
tinker fixed
OK!大功告成!
問題記錄
- 如果有同學遇到熱修復過的app,無法正常進行版本升級的問題的話,可以參考這裡,每次版本升級都需要更新 build.gradle 檔案裡的 TINKER_ID。如下圖所示:
關於TinkerId的問題