Android 热更新 tinker 在Android 简单的使用 (替换class)
Hello! 大家好.
前段时间比较忙,今天终于闲下来了.于是研究研究比较高大上的东西. 热更新!
网上一搜热更新,好家伙一点一大堆,各种框架让人应接不暇.最后综合来看选择了 微信的Tinker 热修复框架.
至于缺点就不多说了 ,网上一大堆,这里我贴一个官方的对比图
废话不多,直接进入正题.
一: 新建一个空的项目. TrustHotFix
里面就是两个Button 和一个TextView.
贴一下MainActivity代码 很干净
运行.
ok 一个空的项目好了 .接下来进行相关配置
二: Tinker配置
1:在项目build.gradle 下面添加一下代码
//热修复 classpath "com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}"
2:在目录gradle.properties 中 添加 下面代码
TINKER_VERSION=1.7.11
注意 TIMKER_VERSIOM 最好用最新版的,我用的时候是直接按git上的Demo上的版本设置的.
3:在app build.gradle中添加相应的依赖
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"
4:接着配置其他参数
还是在app build.gradle中添加相应
def gitSha() { try { String gitRev = 'git rev-parse --short HEAD'.execute(null, project.rootDir).text.trim() 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 javaVersion = JavaVersion.VERSION_1_7
在 android {} 添加下面代码
//热修复 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") }上面这个签名文件是 官方Demo里面的
在 defaultConfig{}添加下面代码
//热更新----------- applicationId "tinker.sample.android" minSdkVersion 10 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\"" //---------------------------------------------
然后添加代码
//热更新------ 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 { //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-0616-15-27-53.apk" //proguard mapping file to build patch apk tinkerApplyMappingPath = "${bakPath}/app-release-0616-15-27-53-mapping.txt" //resource R.txt to build patch apk, must input if there is resource changed tinkerApplyResourcePath = "${bakPath}/app-release-0616-15-27-53-R.txt" //only use for build all flavor, if not, just ignore this field tinkerBuildFlavorDirectory = "${bakPath}/app-1018-17-32-47" } 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 { /** * 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 = false /** * 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 } 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.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" } } } } } }
以上就是tinker所需要的配置,如果不知道这些配置对应的位置可以下载官方Demo查看 或者在文末连接下载Demo查看
Clean 我们的项目,这个时候会发现一个错误
不好意思的是这个错,我忘了截图了.抱歉啊各位
错误的指向是你的build.gradle文件里面的
看到这里懂英文的大腿们,应该知道是什么原因了.
首先tinker是需要一个TINKER_ID的 这TINKER_ID可以是手写的(我没试过手写),也可以通过和git关联起来自动更新的.这个对以后的升级是很重要的.TINKER_ID必须是唯一的否则热更新的时候会加载之前的旧版的补丁
需要做的就是把本地的这项目,和git上的进行关联就行了.如果不会的话请看一下git的基本操作谢谢.
关联成功后 接下来我们要配置application文件了
首先创建一个class文件 名字随便起,这里我用的是和官方Demo一样的名字 SampleApplicationLike
extends DefaultApplicationLike
在上面添加下面代码
@SuppressWarnings("unused") @DefaultLifeCycle(application = "com.trust.trusthotfix.SampleApplication", flags = ShareConstants.TINKER_ENABLE_ALL, loadVerifyFlag = false)
注意:application 里面的包名不要随便乱起,前面的是你 SampleApplicationLike这个class文件的包名 后面的这个SampleApplication 是要在AndroidManifest.xml 文件里面使用的名字
这里没什么好说的都是直接复制的官方Demo 直接上整个类的代码
SampleApplicationLike类
@SuppressWarnings("unused") @DefaultLifeCycle(application = "com.trust.trusthotfix.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); SampleApplicationContext.application = getApplication(); SampleApplicationContext.context = getApplication(); TinkerManager.setTinkerApplicationLike(this); TinkerManager.initFastCrashProtect(); //should set before tinker is installed TinkerManager.setUpgradeRetryEnable(true); //optional set logIml, or you can use default debug log TinkerInstaller.setLogIml(new MyLogImp()); //installTinker after load multiDex //or you can put com.tencent.tinker.** to main dex TinkerManager.installTinker(this); Tinker tinker = Tinker.with(getApplication()); } @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) { getApplication().registerActivityLifecycleCallbacks(callback); } }
下面还需要几个相关类
BaseBuildInfo 类
public class BaseBuildInfo { public static String TEST_MESSAGE = "I won't change with tinker patch!"; public static String BASE_TINKER_ID = BuildConfig.TINKER_ID; }
BuildInfo类
public class BuildInfo { /** * they are not final, so they won't change with the BuildConfig values! */ public static boolean DEBUG = BuildConfig.DEBUG; public static String VERSION_NAME = BuildConfig.VERSION_NAME; public static int VERSION_CODE = BuildConfig.VERSION_CODE; public static String MESSAGE = BuildConfig.MESSAGE; public static String TINKER_ID = BuildConfig.TINKER_ID; public static String PLATFORM = BuildConfig.PLATFORM; }
SampleApplicationContext类
public class SampleApplicationContext { public static Application application = null; public static Context context = null; }.TinkerManager类
public class TinkerManager { private static final String TAG = "Tinker.TinkerManager"; private static ApplicationLike applicationLike; private static SampleUncaughtExceptionHandler uncaughtExceptionHandler; private static boolean isInstalled = false; public static void setTinkerApplicationLike(ApplicationLike appLike) { applicationLike = appLike; } public static ApplicationLike getTinkerApplicationLike() { return applicationLike; } public static void initFastCrashProtect() { if (uncaughtExceptionHandler == null) { uncaughtExceptionHandler = new SampleUncaughtExceptionHandler(); Thread.setDefaultUncaughtExceptionHandler(uncaughtExceptionHandler); } } public static void setUpgradeRetryEnable(boolean enable) { UpgradePatchRetry.getInstance(applicationLike.getApplication()).setRetryEnable(enable); } /** * all use default class, simply Tinker install method */ public static void sampleInstallTinker(ApplicationLike appLike) { if (isInstalled) { TinkerLog.w(TAG, "install tinker, but has installed, ignore"); return; } TinkerInstaller.install(appLike); isInstalled = true; } /** * you can specify all class you want. * sometimes, you can only install tinker in some process you want! * * @param appLike */ public static void installTinker(ApplicationLike appLike) { if (isInstalled) { TinkerLog.w(TAG, "install tinker, but has installed, ignore"); return; } //or you can just use DefaultLoadReporter LoadReporter loadReporter = new SampleLoadReporter(appLike.getApplication()); //or you can just use DefaultPatchReporter PatchReporter patchReporter = new SamplePatchReporter(appLike.getApplication()); //or you can just use DefaultPatchListener PatchListener patchListener = new SamplePatchListener(appLike.getApplication()); //you can set your own upgrade patch if you need AbstractPatch upgradePatchProcessor = new UpgradePatch(); TinkerInstaller.install(appLike, loadReporter, patchReporter, patchListener, SampleResultService.class, upgradePatchProcessor); isInstalled = true; } }
Utils类
public class Utils { private static final String TAG = "Tinker.Utils"; /** * the error code define by myself * should after {@code ShareConstants.ERROR_PATCH_INSERVICE */ public static final int ERROR_PATCH_GOOGLEPLAY_CHANNEL = -6; public static final int ERROR_PATCH_ROM_SPACE = -7; public static final int ERROR_PATCH_MEMORY_LIMIT = -8; public static final int ERROR_PATCH_CRASH_LIMIT = -9; public static final int ERROR_PATCH_CONDITION_NOT_SATISFIED = -10; public static final int ERROR_PATCH_ALREADY_APPLY = -11; public static final int ERROR_PATCH_RETRY_COUNT_LIMIT = -12; 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; } } public static class ScreenState { public interface IOnScreenOff { void onScreenOff(); } public ScreenState(final 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)) { if (onScreenOffInterface != null) { onScreenOffInterface.onScreenOff(); } } context.unregisterReceiver(this); } }, filter); } } }
SampleResultService类
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) { deleteRawPatchFile(new File(result.rawPatchFilePath)); //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 Utils.ScreenState(getApplicationContext(), new Utils.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()); } }
SampleLoadReporter类
public class SampleLoadReporter extends DefaultLoadReporter { private final static String TAG = "Tinker.SampleLoadReporter"; public SampleLoadReporter(Context context) { super(context); } @Override public void onLoadPatchListenerReceiveFail(final File patchFile, int errorCode) { super.onLoadPatchListenerReceiveFail(patchFile, errorCode); SampleTinkerReport.onTryApplyFail(errorCode); } @Override public void onLoadResult(File patchDirectory, int loadCode, long cost) { super.onLoadResult(patchDirectory, loadCode, cost); switch (loadCode) { case ShareConstants.ERROR_LOAD_OK: SampleTinkerReport.onLoaded(cost); break; } Looper.getMainLooper().myQueue().addIdleHandler(new MessageQueue.IdleHandler() { @Override public boolean queueIdle() { if (UpgradePatchRetry.getInstance(context).onPatchRetryLoad()) { SampleTinkerReport.onReportRetryPatch(); } return false; } }); } @Override public void onLoadException(Throwable e, int errorCode) { super.onLoadException(e, errorCode); SampleTinkerReport.onLoadException(e, errorCode); } @Override public void onLoadFileMd5Mismatch(File file, int fileType) { super.onLoadFileMd5Mismatch(file, fileType); SampleTinkerReport.onLoadFileMisMatch(fileType); } /** * try to recover patch oat file * * @param file * @param fileType * @param isDirectory */ @Override public void onLoadFileNotFound(File file, int fileType, boolean isDirectory) { super.onLoadFileNotFound(file, fileType, isDirectory); SampleTinkerReport.onLoadFileNotFound(fileType); } @Override public void onLoadPackageCheckFail(File patchFile, int errorCode) { super.onLoadPackageCheckFail(patchFile, errorCode); SampleTinkerReport.onLoadPackageCheckFail(errorCode); } @Override public void onLoadPatchInfoCorrupted(String oldVersion, String newVersion, File patchInfoFile) { super.onLoadPatchInfoCorrupted(oldVersion, newVersion, patchInfoFile); SampleTinkerReport.onLoadInfoCorrupted(); } @Override public void onLoadInterpret(int type, Throwable e) { super.onLoadInterpret(type, e); SampleTinkerReport.onLoadInterpretReport(type, e); } @Override public void onLoadPatchVersionChanged(String oldVersion, String newVersion, File patchDirectoryFile, String currentPatchName) { super.onLoadPatchVersionChanged(oldVersion, newVersion, patchDirectoryFile, currentPatchName); } }
SamplePatchListener类
public class SamplePatchListener extends DefaultPatchListener { private static final String TAG = "Tinker.SamplePatchListener"; protected static final long NEW_PATCH_RESTRICTION_SPACE_SIZE_MIN = 60 * 1024 * 1024; private final int maxMemory; public SamplePatchListener(Context context) { super(context); maxMemory = ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass(); TinkerLog.i(TAG, "application maxMemory:" + maxMemory); } /** * because we use the defaultCheckPatchReceived method * the error code define by myself should after {@code ShareConstants.ERROR_RECOVER_INSERVICE * * @param path * @param newPatch * @return */ @Override public int patchCheck(String path) { File patchFile = new File(path); TinkerLog.i(TAG, "receive a patch file: %s, file size:%d", path, SharePatchFileUtil.getFileOrDirectorySize(patchFile)); int returnCode = super.patchCheck(path); if (returnCode == ShareConstants.ERROR_PATCH_OK) { returnCode = Utils.checkForPatchRecover(NEW_PATCH_RESTRICTION_SPACE_SIZE_MIN, maxMemory); } if (returnCode == ShareConstants.ERROR_PATCH_OK) { String patchMd5 = SharePatchFileUtil.getMD5(patchFile); SharedPreferences sp = context.getSharedPreferences(ShareConstants.TINKER_SHARE_PREFERENCE_CONFIG, Context.MODE_MULTI_PROCESS); //optional, only disable this patch file with md5 int fastCrashCount = sp.getInt(patchMd5, 0); if (fastCrashCount >= SampleUncaughtExceptionHandler.MAX_CRASH_COUNT) { returnCode = Utils.ERROR_PATCH_CRASH_LIMIT; } else { //for upgrade patch, version must be not the same //for repair patch, we won't has the tinker load flag Tinker tinker = Tinker.with(context); if (tinker.isTinkerLoaded()) { TinkerLoadResult tinkerLoadResult = tinker.getTinkerLoadResultIfPresent(); if (tinkerLoadResult != null && !tinkerLoadResult.useInterpretMode) { String currentVersion = tinkerLoadResult.currentVersion; if (patchMd5.equals(currentVersion)) { returnCode = Utils.ERROR_PATCH_ALREADY_APPLY; } } } } //check whether retry so many times if (returnCode == ShareConstants.ERROR_PATCH_OK) { returnCode = UpgradePatchRetry.getInstance(context).onPatchListenerCheck(patchMd5) ? ShareConstants.ERROR_PATCH_OK : Utils.ERROR_PATCH_RETRY_COUNT_LIMIT; } } // Warning, it is just a sample case, you don't need to copy all of these // Interception some of the request if (returnCode == ShareConstants.ERROR_PATCH_OK) { Properties properties = ShareTinkerInternals.fastGetPatchPackageMeta(patchFile); if (properties == null) { returnCode = Utils.ERROR_PATCH_CONDITION_NOT_SATISFIED; } else { String platform = properties.getProperty(Utils.PLATFORM); TinkerLog.i(TAG, "get platform:" + platform); // check patch platform require if (platform == null || !platform.equals(BuildInfo.PLATFORM)) { returnCode = Utils.ERROR_PATCH_CONDITION_NOT_SATISFIED; } } } SampleTinkerReport.onTryApply(returnCode == ShareConstants.ERROR_PATCH_OK); return returnCode; } }
SamplePatchReporter类
public class SamplePatchReporter extends DefaultPatchReporter { private final static String TAG = "Tinker.SamplePatchReporter"; public SamplePatchReporter(Context context) { super(context); } @Override public void onPatchServiceStart(Intent intent) { super.onPatchServiceStart(intent); SampleTinkerReport.onApplyPatchServiceStart(); } @Override public void onPatchDexOptFail(File patchFile, List<File> dexFiles, Throwable t) { super.onPatchDexOptFail(patchFile, dexFiles, t); SampleTinkerReport.onApplyDexOptFail(t); } @Override public void onPatchException(File patchFile, Throwable e) { super.onPatchException(patchFile, e); SampleTinkerReport.onApplyCrash(e); } @Override public void onPatchInfoCorrupted(File patchFile, String oldVersion, String newVersion) { super.onPatchInfoCorrupted(patchFile, oldVersion, newVersion); SampleTinkerReport.onApplyInfoCorrupted(); } @Override public void onPatchPackageCheckFail(File patchFile, int errorCode) { super.onPatchPackageCheckFail(patchFile, errorCode); SampleTinkerReport.onApplyPackageCheckFail(errorCode); } @Override public void onPatchResult(File patchFile, boolean success, long cost) { super.onPatchResult(patchFile, success, cost); SampleTinkerReport.onApplied(cost, success); } @Override public void onPatchTypeExtractFail(File patchFile, File extractTo, String filename, int fileType) { super.onPatchTypeExtractFail(patchFile, extractTo, filename, fileType); SampleTinkerReport.onApplyExtractFail(fileType); } @Override public void onPatchVersionCheckFail(File patchFile, SharePatchInfo oldPatchInfo, String patchFileVersion) { super.onPatchVersionCheckFail(patchFile, oldPatchInfo, patchFileVersion); SampleTinkerReport.onApplyVersionCheckFail(); } }
SampleTinkerReport类
public class SampleTinkerReport { private static final String TAG = "Tinker.SampleTinkerReport"; // KEY - PV public static final int KEY_REQUEST = 0; public static final int KEY_DOWNLOAD = 1; public static final int KEY_TRY_APPLY = 2; public static final int KEY_TRY_APPLY_SUCCESS = 3; public static final int KEY_APPLIED_START = 4; public static final int KEY_APPLIED = 5; public static final int KEY_LOADED = 6; public static final int KEY_CRASH_FAST_PROTECT = 7; public static final int KEY_CRASH_CAUSE_XPOSED_DALVIK = 8; public static final int KEY_CRASH_CAUSE_XPOSED_ART = 9; public static final int KEY_APPLY_WITH_RETRY = 10; //Key -- try apply detail public static final int KEY_TRY_APPLY_UPGRADE = 70; public static final int KEY_TRY_APPLY_DISABLE = 71; public static final int KEY_TRY_APPLY_RUNNING = 72; public static final int KEY_TRY_APPLY_INSERVICE = 73; public static final int KEY_TRY_APPLY_NOT_EXIST = 74; public static final int KEY_TRY_APPLY_GOOGLEPLAY = 75; public static final int KEY_TRY_APPLY_ROM_SPACE = 76; public static final int KEY_TRY_APPLY_ALREADY_APPLY = 77; public static final int KEY_TRY_APPLY_MEMORY_LIMIT = 78; public static final int KEY_TRY_APPLY_CRASH_LIMIT = 79; public static final int KEY_TRY_APPLY_CONDITION_NOT_SATISFIED = 80; public static final int KEY_TRY_APPLY_JIT = 81; //Key -- apply detail public static final int KEY_APPLIED_UPGRADE = 100; public static final int KEY_APPLIED_UPGRADE_FAIL = 101; public static final int KEY_APPLIED_EXCEPTION = 120; public static final int KEY_APPLIED_DEXOPT_OTHER = 121; public static final int KEY_APPLIED_DEXOPT_EXIST = 122; public static final int KEY_APPLIED_DEXOPT_FORMAT = 123; public static final int KEY_APPLIED_INFO_CORRUPTED = 124; //package check public static final int KEY_APPLIED_PACKAGE_CHECK_SIGNATURE = 150; public static final int KEY_APPLIED_PACKAGE_CHECK_DEX_META = 151; public static final int KEY_APPLIED_PACKAGE_CHECK_LIB_META = 152; public static final int KEY_APPLIED_PACKAGE_CHECK_APK_TINKER_ID_NOT_FOUND = 153; public static final int KEY_APPLIED_PACKAGE_CHECK_PATCH_TINKER_ID_NOT_FOUND = 154; public static final int KEY_APPLIED_PACKAGE_CHECK_META_NOT_FOUND = 155; public static final int KEY_APPLIED_PACKAGE_CHECK_TINKER_ID_NOT_EQUAL = 156; public static final int KEY_APPLIED_PACKAGE_CHECK_RES_META = 157; public static final int KEY_APPLIED_PACKAGE_CHECK_TINKERFLAG_NOT_SUPPORT = 158; //version check public static final int KEY_APPLIED_VERSION_CHECK = 180; //extract error public static final int KEY_APPLIED_PATCH_FILE_EXTRACT = 181; public static final int KEY_APPLIED_DEX_EXTRACT = 182; public static final int KEY_APPLIED_LIB_EXTRACT = 183; public static final int KEY_APPLIED_RESOURCE_EXTRACT = 184; //cost time public static final int KEY_APPLIED_SUCC_COST_5S_LESS = 200; public static final int KEY_APPLIED_SUCC_COST_10S_LESS = 201; public static final int KEY_APPLIED_SUCC_COST_30S_LESS = 202; public static final int KEY_APPLIED_SUCC_COST_60S_LESS = 203; public static final int KEY_APPLIED_SUCC_COST_OTHER = 204; public static final int KEY_APPLIED_FAIL_COST_5S_LESS = 205; public static final int KEY_APPLIED_FAIL_COST_10S_LESS = 206; public static final int KEY_APPLIED_FAIL_COST_30S_LESS = 207; public static final int KEY_APPLIED_FAIL_COST_60S_LESS = 208; public static final int KEY_APPLIED_FAIL_COST_OTHER = 209; // KEY -- load detail public static final int KEY_LOADED_UNKNOWN_EXCEPTION = 250; public static final int KEY_LOADED_UNCAUGHT_EXCEPTION = 251; public static final int KEY_LOADED_EXCEPTION_DEX = 252; public static final int KEY_LOADED_EXCEPTION_DEX_CHECK = 253; public static final int KEY_LOADED_EXCEPTION_RESOURCE = 254; public static final int KEY_LOADED_EXCEPTION_RESOURCE_CHECK = 255; public static final int KEY_LOADED_MISMATCH_DEX = 300; public static final int KEY_LOADED_MISMATCH_LIB = 301; public static final int KEY_LOADED_MISMATCH_RESOURCE = 302; public static final int KEY_LOADED_MISSING_DEX = 303; public static final int KEY_LOADED_MISSING_LIB = 304; public static final int KEY_LOADED_MISSING_PATCH_FILE = 305; public static final int KEY_LOADED_MISSING_PATCH_INFO = 306; public static final int KEY_LOADED_MISSING_DEX_OPT = 307; public static final int KEY_LOADED_MISSING_RES = 308; public static final int KEY_LOADED_INFO_CORRUPTED = 309; //load package check public static final int KEY_LOADED_PACKAGE_CHECK_SIGNATURE = 350; public static final int KEY_LOADED_PACKAGE_CHECK_DEX_META = 351; public static final int KEY_LOADED_PACKAGE_CHECK_LIB_META = 352; public static final int KEY_LOADED_PACKAGE_CHECK_APK_TINKER_ID_NOT_FOUND = 353; public static final int KEY_LOADED_PACKAGE_CHECK_PATCH_TINKER_ID_NOT_FOUND = 354; public static final int KEY_LOADED_PACKAGE_CHECK_TINKER_ID_NOT_EQUAL = 355; public static final int KEY_LOADED_PACKAGE_CHECK_PACKAGE_META_NOT_FOUND = 356; public static final int KEY_LOADED_PACKAGE_CHECK_RES_META = 357; public static final int KEY_LOADED_PACKAGE_CHECK_TINKERFLAG_NOT_SUPPORT = 358; public static final int KEY_LOADED_SUCC_COST_500_LESS = 400; public static final int KEY_LOADED_SUCC_COST_1000_LESS = 401; public static final int KEY_LOADED_SUCC_COST_3000_LESS = 402; public static final int KEY_LOADED_SUCC_COST_5000_LESS = 403; public static final int KEY_LOADED_SUCC_COST_OTHER = 404; public static final int KEY_LOADED_INTERPRET_GET_INSTRUCTION_SET_ERROR = 450; public static final int KEY_LOADED_INTERPRET_INTERPRET_COMMAND_ERROR = 451; public static final int KEY_LOADED_INTERPRET_TYPE_INTERPRET_OK = 452; interface Reporter { void onReport(int key); void onReport(String message); } private static Reporter reporter = null; public void setReporter(Reporter reporter) { this.reporter = reporter; } public static void onTryApply(boolean success) { if (reporter == null) { return; } reporter.onReport(KEY_TRY_APPLY); reporter.onReport(KEY_TRY_APPLY_UPGRADE); if (success) { reporter.onReport(KEY_TRY_APPLY_SUCCESS); } } public static void onTryApplyFail(int errorCode) { if (reporter == null) { return; } switch (errorCode) { case ShareConstants.ERROR_PATCH_NOTEXIST: reporter.onReport(KEY_TRY_APPLY_NOT_EXIST); break; case ShareConstants.ERROR_PATCH_DISABLE: reporter.onReport(KEY_TRY_APPLY_DISABLE); break; case ShareConstants.ERROR_PATCH_INSERVICE: reporter.onReport(KEY_TRY_APPLY_INSERVICE); break; case ShareConstants.ERROR_PATCH_RUNNING: reporter.onReport(KEY_TRY_APPLY_RUNNING); break; case ShareConstants.ERROR_PATCH_JIT: reporter.onReport(KEY_TRY_APPLY_JIT); break; case Utils.ERROR_PATCH_ROM_SPACE: reporter.onReport(KEY_TRY_APPLY_ROM_SPACE); break; case Utils.ERROR_PATCH_GOOGLEPLAY_CHANNEL: reporter.onReport(KEY_TRY_APPLY_GOOGLEPLAY); break; case Utils.ERROR_PATCH_ALREADY_APPLY: reporter.onReport(KEY_TRY_APPLY_ALREADY_APPLY); break; case Utils.ERROR_PATCH_CRASH_LIMIT: reporter.onReport(KEY_TRY_APPLY_CRASH_LIMIT); break; case Utils.ERROR_PATCH_MEMORY_LIMIT: reporter.onReport(KEY_TRY_APPLY_MEMORY_LIMIT); break; case Utils.ERROR_PATCH_CONDITION_NOT_SATISFIED: reporter.onReport(KEY_TRY_APPLY_CONDITION_NOT_SATISFIED); break; } } public static void onLoadPackageCheckFail(int errorCode) { if (reporter == null) { return; } switch (errorCode) { case ShareConstants.ERROR_PACKAGE_CHECK_SIGNATURE_FAIL: reporter.onReport(KEY_LOADED_PACKAGE_CHECK_SIGNATURE); break; case ShareConstants.ERROR_PACKAGE_CHECK_DEX_META_CORRUPTED: reporter.onReport(KEY_LOADED_PACKAGE_CHECK_DEX_META); break; case ShareConstants.ERROR_PACKAGE_CHECK_LIB_META_CORRUPTED: reporter.onReport(KEY_LOADED_PACKAGE_CHECK_LIB_META); break; case ShareConstants.ERROR_PACKAGE_CHECK_PATCH_TINKER_ID_NOT_FOUND: reporter.onReport(KEY_LOADED_PACKAGE_CHECK_PATCH_TINKER_ID_NOT_FOUND); break; case ShareConstants.ERROR_PACKAGE_CHECK_APK_TINKER_ID_NOT_FOUND: reporter.onReport(KEY_LOADED_PACKAGE_CHECK_APK_TINKER_ID_NOT_FOUND); break; case ShareConstants.ERROR_PACKAGE_CHECK_TINKER_ID_NOT_EQUAL: reporter.onReport(KEY_LOADED_PACKAGE_CHECK_TINKER_ID_NOT_EQUAL); break; case ShareConstants.ERROR_PACKAGE_CHECK_PACKAGE_META_NOT_FOUND: reporter.onReport(KEY_LOADED_PACKAGE_CHECK_PACKAGE_META_NOT_FOUND); break; case ShareConstants.ERROR_PACKAGE_CHECK_RESOURCE_META_CORRUPTED: reporter.onReport(KEY_LOADED_PACKAGE_CHECK_RES_META); break; case ShareConstants.ERROR_PACKAGE_CHECK_TINKERFLAG_NOT_SUPPORT: reporter.onReport(KEY_LOADED_PACKAGE_CHECK_TINKERFLAG_NOT_SUPPORT); break; } } public static void onLoaded(long cost) { if (reporter == null) { return; } reporter.onReport(KEY_LOADED); if (cost < 0L) { TinkerLog.e(TAG, "hp_report report load cost failed, invalid cost"); return; } if (cost <= 500) { reporter.onReport(KEY_LOADED_SUCC_COST_500_LESS); } else if (cost <= 1000) { reporter.onReport(KEY_LOADED_SUCC_COST_1000_LESS); } else if (cost <= 3000) { reporter.onReport(KEY_LOADED_SUCC_COST_3000_LESS); } else if (cost <= 5000) { reporter.onReport(KEY_LOADED_SUCC_COST_5000_LESS); } else { reporter.onReport(KEY_LOADED_SUCC_COST_OTHER); } } public static void onLoadInfoCorrupted() { if (reporter == null) { return; } reporter.onReport(KEY_LOADED_INFO_CORRUPTED); } public static void onLoadFileNotFound(int fileType) { if (reporter == null) { return; } switch (fileType) { case ShareConstants.TYPE_DEX_OPT: reporter.onReport(KEY_LOADED_MISSING_DEX_OPT); break; case ShareConstants.TYPE_DEX: reporter.onReport(KEY_LOADED_MISSING_DEX); break; case ShareConstants.TYPE_LIBRARY: reporter.onReport(KEY_LOADED_MISSING_LIB); break; case ShareConstants.TYPE_PATCH_FILE: reporter.onReport(KEY_LOADED_MISSING_PATCH_FILE); break; case ShareConstants.TYPE_PATCH_INFO: reporter.onReport(KEY_LOADED_MISSING_PATCH_INFO); break; case ShareConstants.TYPE_RESOURCE: reporter.onReport(KEY_LOADED_MISSING_RES); break; } } public static void onLoadInterpretReport(int type, Throwable e) { if (reporter == null) { return; } switch (type) { case ShareConstants.TYPE_INTERPRET_GET_INSTRUCTION_SET_ERROR: reporter.onReport(KEY_LOADED_INTERPRET_GET_INSTRUCTION_SET_ERROR); reporter.onReport("Tinker Exception:interpret occur exception " + Utils.getExceptionCauseString(e)); break; case ShareConstants.TYPE_INTERPRET_COMMAND_ERROR: reporter.onReport(KEY_LOADED_INTERPRET_INTERPRET_COMMAND_ERROR); reporter.onReport("Tinker Exception:interpret occur exception " + Utils.getExceptionCauseString(e)); break; case ShareConstants.TYPE_INTERPRET_OK: reporter.onReport(KEY_LOADED_INTERPRET_TYPE_INTERPRET_OK); break; } } public static void onLoadFileMisMatch(int fileType) { if (reporter == null) { return; } switch (fileType) { case ShareConstants.TYPE_DEX: reporter.onReport(KEY_LOADED_MISMATCH_DEX); break; case ShareConstants.TYPE_LIBRARY: reporter.onReport(KEY_LOADED_MISMATCH_LIB); break; case ShareConstants.TYPE_RESOURCE: reporter.onReport(KEY_LOADED_MISMATCH_RESOURCE); break; } } public static void onLoadException(Throwable throwable, int errorCode) { if (reporter == null) { return; } boolean isCheckFail = false; switch (errorCode) { case ShareConstants.ERROR_LOAD_EXCEPTION_DEX: if (throwable.getMessage().contains(ShareConstants.CHECK_DEX_INSTALL_FAIL)) { reporter.onReport(KEY_LOADED_EXCEPTION_DEX_CHECK); isCheckFail = true; TinkerLog.e(TAG, "tinker dex check fail:" + throwable.getMessage()); } else { reporter.onReport(KEY_LOADED_EXCEPTION_DEX); TinkerLog.e(TAG, "tinker dex reflect fail:" + throwable.getMessage()); } break; case ShareConstants.ERROR_LOAD_EXCEPTION_RESOURCE: if (throwable.getMessage().contains(ShareConstants.CHECK_RES_INSTALL_FAIL)) { reporter.onReport(KEY_LOADED_EXCEPTION_RESOURCE_CHECK); isCheckFail = true; TinkerLog.e(TAG, "tinker res check fail:" + throwable.getMessage()); } else { reporter.onReport(KEY_LOADED_EXCEPTION_RESOURCE); TinkerLog.e(TAG, "tinker res reflect fail:" + throwable.getMessage()); } break; case ShareConstants.ERROR_LOAD_EXCEPTION_UNCAUGHT: reporter.onReport(KEY_LOADED_UNCAUGHT_EXCEPTION); break; case ShareConstants.ERROR_LOAD_EXCEPTION_UNKNOWN: reporter.onReport(KEY_LOADED_UNKNOWN_EXCEPTION); break; } //reporter exception, for dex check fail, we don't need to report stacktrace if (!isCheckFail) { reporter.onReport("Tinker Exception:load tinker occur exception " + Utils.getExceptionCauseString(throwable)); } } public static void onApplyPatchServiceStart() { if (reporter == null) { return; } reporter.onReport(KEY_APPLIED_START); } public static void onApplyDexOptFail(Throwable throwable) { if (reporter == null) { return; } if (throwable.getMessage().contains(ShareConstants.CHECK_DEX_OAT_EXIST_FAIL)) { reporter.onReport(KEY_APPLIED_DEXOPT_EXIST); } else if (throwable.getMessage().contains(ShareConstants.CHECK_DEX_OAT_FORMAT_FAIL)) { reporter.onReport(KEY_APPLIED_DEXOPT_FORMAT); } else { reporter.onReport(KEY_APPLIED_DEXOPT_OTHER); reporter.onReport("Tinker Exception:apply tinker occur exception " + Utils.getExceptionCauseString(throwable)); } } public static void onApplyInfoCorrupted() { if (reporter == null) { return; } reporter.onReport(KEY_APPLIED_INFO_CORRUPTED); } public static void onApplyVersionCheckFail() { if (reporter == null) { return; } reporter.onReport(KEY_APPLIED_VERSION_CHECK); } public static void onApplyExtractFail(int fileType) { if (reporter == null) { return; } switch (fileType) { case ShareConstants.TYPE_DEX: reporter.onReport(KEY_APPLIED_DEX_EXTRACT); break; case ShareConstants.TYPE_LIBRARY: reporter.onReport(KEY_APPLIED_LIB_EXTRACT); break; case ShareConstants.TYPE_PATCH_FILE: reporter.onReport(KEY_APPLIED_PATCH_FILE_EXTRACT); break; case ShareConstants.TYPE_RESOURCE: reporter.onReport(KEY_APPLIED_RESOURCE_EXTRACT); break; } } public static void onApplied(long cost, boolean success) { if (reporter == null) { return; } if (success) { reporter.onReport(KEY_APPLIED); } if (success) { reporter.onReport(KEY_APPLIED_UPGRADE); } else { reporter.onReport(KEY_APPLIED_UPGRADE_FAIL); } TinkerLog.i(TAG, "hp_report report apply cost = %d", cost); if (cost < 0L) { TinkerLog.e(TAG, "hp_report report apply cost failed, invalid cost"); return; } if (cost <= 5000) { if (success) { reporter.onReport(KEY_APPLIED_SUCC_COST_5S_LESS); } else { reporter.onReport(KEY_APPLIED_FAIL_COST_5S_LESS); } } else if (cost <= 10 * 1000) { if (success) { reporter.onReport(KEY_APPLIED_SUCC_COST_10S_LESS); } else { reporter.onReport(KEY_APPLIED_FAIL_COST_10S_LESS); } } else if (cost <= 30 * 1000) { if (success) { reporter.onReport(KEY_APPLIED_SUCC_COST_30S_LESS); } else { reporter.onReport(KEY_APPLIED_FAIL_COST_30S_LESS); } } else if (cost <= 60 * 1000) { if (success) { reporter.onReport(KEY_APPLIED_SUCC_COST_60S_LESS); } else { reporter.onReport(KEY_APPLIED_FAIL_COST_60S_LESS); } } else { if (success) { reporter.onReport(KEY_APPLIED_SUCC_COST_OTHER); } else { reporter.onReport(KEY_APPLIED_FAIL_COST_OTHER); } } } public static void onApplyPackageCheckFail(int errorCode) { if (reporter == null) { return; } TinkerLog.i(TAG, "hp_report package check failed, error = %d", errorCode); switch (errorCode) { case ShareConstants.ERROR_PACKAGE_CHECK_SIGNATURE_FAIL: reporter.onReport(KEY_APPLIED_PACKAGE_CHECK_SIGNATURE); break; case ShareConstants.ERROR_PACKAGE_CHECK_DEX_META_CORRUPTED: reporter.onReport(KEY_APPLIED_PACKAGE_CHECK_DEX_META); break; case ShareConstants.ERROR_PACKAGE_CHECK_LIB_META_CORRUPTED: reporter.onReport(KEY_APPLIED_PACKAGE_CHECK_LIB_META); break; case ShareConstants.ERROR_PACKAGE_CHECK_PATCH_TINKER_ID_NOT_FOUND: reporter.onReport(KEY_APPLIED_PACKAGE_CHECK_PATCH_TINKER_ID_NOT_FOUND); break; case ShareConstants.ERROR_PACKAGE_CHECK_APK_TINKER_ID_NOT_FOUND: reporter.onReport(KEY_APPLIED_PACKAGE_CHECK_APK_TINKER_ID_NOT_FOUND); break; case ShareConstants.ERROR_PACKAGE_CHECK_TINKER_ID_NOT_EQUAL: reporter.onReport(KEY_APPLIED_PACKAGE_CHECK_TINKER_ID_NOT_EQUAL); break; case ShareConstants.ERROR_PACKAGE_CHECK_PACKAGE_META_NOT_FOUND: reporter.onReport(KEY_APPLIED_PACKAGE_CHECK_META_NOT_FOUND); break; case ShareConstants.ERROR_PACKAGE_CHECK_RESOURCE_META_CORRUPTED: reporter.onReport(KEY_APPLIED_PACKAGE_CHECK_RES_META); break; case ShareConstants.ERROR_PACKAGE_CHECK_TINKERFLAG_NOT_SUPPORT: reporter.onReport(KEY_APPLIED_PACKAGE_CHECK_TINKERFLAG_NOT_SUPPORT); break; } } public static void onApplyCrash(Throwable throwable) { if (reporter == null) { return; } reporter.onReport(KEY_APPLIED_EXCEPTION); reporter.onReport("Tinker Exception:apply tinker occur exception " + Utils.getExceptionCauseString(throwable)); } public static void onFastCrashProtect() { if (reporter == null) { return; } reporter.onReport(KEY_CRASH_FAST_PROTECT); } public static void onXposedCrash() { if (reporter == null) { return; } if (ShareTinkerInternals.isVmArt()) { reporter.onReport(KEY_CRASH_CAUSE_XPOSED_ART); } else { reporter.onReport(KEY_CRASH_CAUSE_XPOSED_DALVIK); } } public static void onReportRetryPatch() { if (reporter == null) { return; } reporter.onReport(KEY_APPLY_WITH_RETRY); } }
MyLogImp类
public class MyLogImp implements TinkerLog.TinkerLogImp { private static final String TAG = "Tinker.MyLogImp"; public static final int LEVEL_VERBOSE = 0; public static final int LEVEL_DEBUG = 1; public static final int LEVEL_INFO = 2; public static final int LEVEL_WARNING = 3; public static final int LEVEL_ERROR = 4; public static final int LEVEL_NONE = 5; private static int level = LEVEL_VERBOSE; public static int getLogLevel() { return level; } public static void setLevel(final int level) { MyLogImp.level = level; android.util.Log.w(TAG, "new log level: " + level); } @Override public void v(String s, String s1, Object... objects) { if (level <= LEVEL_VERBOSE) { final String log = objects == null ? s1 : String.format(s1, objects); android.util.Log.v(s, log); } } @Override public void i(String s, String s1, Object... objects) { if (level <= LEVEL_INFO) { final String log = objects == null ? s1 : String.format(s1, objects); android.util.Log.i(s, log); } } @Override public void w(String s, String s1, Object... objects) { if (level <= LEVEL_WARNING) { final String log = objects == null ? s1 : String.format(s1, objects); android.util.Log.w(s, log); } } @Override public void d(String s, String s1, Object... objects) { if (level <= LEVEL_DEBUG) { final String log = objects == null ? s1 : String.format(s1, objects); android.util.Log.d(s, log); } } @Override public void e(String s, String s1, Object... objects) { if (level <= LEVEL_ERROR) { final String log = objects == null ? s1 : String.format(s1, objects); android.util.Log.e(s, log); } } @Override public void printErrStackTrace(String s, Throwable throwable, String s1, Object... objects) { String log = objects == null ? s1 : String.format(s1, objects); if (log == null) { log = ""; } log = log + " " + Log.getStackTraceString(throwable); android.util.Log.e(s, log); } }
SampleUncaughtExceptionHandler类
public class SampleUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler { private static final String TAG = "Tinker.SampleUncaughtExHandler"; private final Thread.UncaughtExceptionHandler ueh; private static final long QUICK_CRASH_ELAPSE = 10 * 1000; public static final int MAX_CRASH_COUNT = 3; private static final String DALVIK_XPOSED_CRASH = "Class ref in pre-verified class resolved to unexpected implementation"; public SampleUncaughtExceptionHandler() { ueh = Thread.getDefaultUncaughtExceptionHandler(); } @Override public void uncaughtException(Thread thread, Throwable ex) { TinkerLog.e(TAG, "uncaughtException:" + ex.getMessage()); tinkerFastCrashProtect(); tinkerPreVerifiedCrashHandler(ex); ueh.uncaughtException(thread, ex); } /** * Such as Xposed, if it try to load some class before we load from patch files. * With dalvik, it will crash with "Class ref in pre-verified class resolved to unexpected implementation". * With art, it may crash at some times. But we can't know the actual crash type. * If it use Xposed, we can just clean patch or mention user to uninstall it. */ private void tinkerPreVerifiedCrashHandler(Throwable ex) { ApplicationLike applicationLike = TinkerManager.getTinkerApplicationLike(); if (applicationLike == null || applicationLike.getApplication() == null) { TinkerLog.w(TAG, "applicationlike is null"); return; } if (!TinkerApplicationHelper.isTinkerLoadSuccess(applicationLike)) { TinkerLog.w(TAG, "tinker is not loaded"); return; } Throwable throwable = ex; boolean isXposed = false; while (throwable != null) { if (!isXposed) { isXposed = Utils.isXposedExists(throwable); } // xposed? if (isXposed) { boolean isCausedByXposed = false; //for art, we can't know the actually crash type //just ignore art if (throwable instanceof IllegalAccessError && throwable.getMessage().contains(DALVIK_XPOSED_CRASH)) { //for dalvik, we know the actual crash type isCausedByXposed = true; } if (isCausedByXposed) { SampleTinkerReport.onXposedCrash(); TinkerLog.e(TAG, "have xposed: just clean tinker"); //kill all other process to ensure that all process's code is the same. ShareTinkerInternals.killAllOtherProcess(applicationLike.getApplication()); TinkerApplicationHelper.cleanPatch(applicationLike); ShareTinkerInternals.setTinkerDisableWithSharedPreferences(applicationLike.getApplication()); return; } } throwable = throwable.getCause(); } } /** * if tinker is load, and it crash more than MAX_CRASH_COUNT, then we just clean patch. */ private boolean tinkerFastCrashProtect() { ApplicationLike applicationLike = TinkerManager.getTinkerApplicationLike(); if (applicationLike == null || applicationLike.getApplication() == null) { return false; } if (!TinkerApplicationHelper.isTinkerLoadSuccess(applicationLike)) { return false; } final long elapsedTime = SystemClock.elapsedRealtime() - applicationLike.getApplicationStartElapsedTime(); //this process may not install tinker, so we use TinkerApplicationHelper api if (elapsedTime < QUICK_CRASH_ELAPSE) { String currentVersion = TinkerApplicationHelper.getCurrentVersion(applicationLike); if (ShareTinkerInternals.isNullOrNil(currentVersion)) { return false; } SharedPreferences sp = applicationLike.getApplication().getSharedPreferences(ShareConstants.TINKER_SHARE_PREFERENCE_CONFIG, Context.MODE_MULTI_PROCESS); int fastCrashCount = sp.getInt(currentVersion, 0) + 1; if (fastCrashCount >= MAX_CRASH_COUNT) { SampleTinkerReport.onFastCrashProtect(); TinkerApplicationHelper.cleanPatch(applicationLike); TinkerLog.e(TAG, "tinker has fast crash more than %d, we just clean patch!", fastCrashCount); return true; } else { sp.edit().putInt(currentVersion, fastCrashCount).commit(); TinkerLog.e(TAG, "tinker has fast crash %d times", fastCrashCount); } } return false; } }
到这里需要的相关类已经完了,复制的好累.
接着是我们调用tinker的代码了
在MainActivity中两个按钮点击里面添加调用代码
加载需要更改的补丁
public void loadPath(View v){ TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed_7zip.apk"); }
删除补丁包已经杀死进程
public void kill (View v){ ShareTinkerInternals.killAllOtherProcess(getApplicationContext()); android.os.Process.killProcess(android.os.Process.myPid()); }
在as上点击Gradle
在build文件里面点击 assembleRelease 用assembleReleaseDebug也是可以的
点击以后tinker就会进行编译,等待编译成功后在app目录build里面找到bakApk文件打开里面会有apk和R.txt文件
这个apk就可以直接使用经过签名的.把apk文件安装到手机上.正常运行这个时候点击加载Path按钮是不会有任何提示的.点击杀死自己 就会把自己杀了但是重新进来的时候还是一样的布局点击Path还是没反应.
接下来
在APP build.gradle 里面找到下面代码
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-0616-15-27-53.apk" //proguard mapping file to build patch apk tinkerApplyMappingPath = "${bakPath}/app-release-0616-15-27-53-mapping.txt" //resource R.txt to build patch apk, must input if there is resource changed tinkerApplyResourcePath = "${bakPath}/app-release-0616-15-27-53-R.txt" //only use for build all flavor, if not, just ignore this field tinkerBuildFlavorDirectory = "${bakPath}/app-1018-17-32-47" }
tinkerOldApkPath 是你apk的路径
tinkerApplyMappingPath 如果你没混淆的话这个可以仿照我写的那样 ,你混淆以后就会在bakApk 会生成mapping文件
tinkerApplyResourcePath 这是生成R.txt文件路径
配置好了以后
对MainActivity里面的代码以及布局做一些修改
显示布局文件
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context="com.trust.trusthotfix.MainActivity"> <ImageView android:src="@mipmap/ic_launcher" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <TextView android:text="BUG 已经修复!!!!!1" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <EditText android:id="@+id/ed" android:layout_width="match_parent" android:layout_height="48dp" /> <Button android:onClick="click" android:text="确定" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <Button android:visibility="gone" android:onClick="loadPath" android:text="加载path" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <Button android:visibility="gone" android:onClick="kill" android:text="杀死自己" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout>
添加一个系统图片 添加一个 输入框 把之前的两个按钮隐藏 添加一个确定按钮
逻辑代码
public class MainActivity extends AppCompatActivity { EditText ed; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Log.d("lhh", "onCreate: BUG is nothing"); ed = (EditText) findViewById(R.id.ed); } public void loadPath(View v){ TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed_7zip.apk"); } public void kill (View v){ ShareTinkerInternals.killAllOtherProcess(getApplicationContext()); android.os.Process.killProcess(android.os.Process.myPid()); } public void click(View v){ String msg = ed.getText().toString().trim(); if(msg != null && !msg.equals("")){ Toast.makeText(this,"你输入的是:"+msg,Toast.LENGTH_LONG).show(); }else{ Toast.makeText(this,"不能为空!"+msg,Toast.LENGTH_LONG).show(); } }
只是简单的用toast 显示输入的文字
ok 继续 Gradle ---- >tinker---->tinkerPathRelease 双击
等待编译成功后,在build文件 ------>outputs------>tinkerPath----->patch_signed_7zip.apk
patch_signed_7zip.apk这个文件就是我们需要的更新补丁了.把这个补丁直接放入手机里面的根目录中,打开手机上的app.
点击 加载Path 成的话 会有一个toast 提示 success ,然后点击杀死自己,重新进入APP就会发现布局已经改变了
ok到这里 tinker 热更新 替换class 文件已经写完了.接下来时 更新lib文件介绍.
END