Android热更新:微信Tinker框架的接入与测试

2023-11-05 11:38

本文主要是介绍Android热更新:微信Tinker框架的接入与测试,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

转载于:http://www.jianshu.com/p/aadcf2ea69a6
Android热修复框架的对比(最终选择微信Tinker)

Android热修复框架的对比

总结对比摘自Tinker官方Wiki

  1. AndFix作为native解决方案,首先面临的是稳定性与兼容性问题,更重要的是它无法实现类替换,它是需要大量额外的开发成本的;
  2. Robust兼容性与成功率较高,但是它与AndFix一样,无法新增变量与类只能用做的bugFix方案;
  3. Qzone方案可以做到发布产品功能,但是它主要问题是插桩带来Dalvik的性能问题,以及为了解决Art下内存地址问题而导致补丁包急速增大的。

    特别是在Android N之后,由于混合编译的inline策略修改,对于市面上的各种方案都不太容易解决。而Tinker热补丁方案不仅支持类、So以及资源的替换,它还是2.X-7.X的全平台支持。利用Tinker我们不仅可以用做bugfix,甚至可以替代功能的发布。Tinker已运行在微信的数亿Android设备上,那么为什么你不使用Tinker呢?

Tinker的已知问题

截至文章发布当天,微信 Tinker 热修复框架 尚存问题如下所示:

  1. Tinker不支持修改AndroidManifest.xml,Tinker不支持新增四大组件;
  2. 由于Google Play的开发者条款限制,不建议在GP渠道动态更新代码;
  3. 在Android N上,补丁对应用启动时间有轻微的影响;
  4. 不支持部分三星android-21机型,加载补丁时会主动抛出"TinkerRuntimeException:checkDexInstall failed";
  5. 由于各个厂商的加固实现并不一致,在1.7.6以及之后的版本,tinker不再支持加固的动态更新;
  6. 对于资源替换,不支持修改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 23buildToolsVersion "23.0.2"compileOptions {sourceCompatibility javaVersiontargetCompatibility javaVersion}//recommenddexOptions {jumboMode = true}defaultConfig {applicationId "com.tinker.deeson.mytinkerdemo"minSdkVersion 15targetSdkVersion 22versionCode 1versionName "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 truesigningConfig signingConfigs.releaseproguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'}debug {debuggable trueminifyEnabled falsesigningConfig 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 apktinkerOldApkPath = "${bakPath}/app-release-0421-12-34-45.apk"//proguard mapping file to build patch apktinkerApplyMappingPath = "${bakPath}/app-release-0421-12-34-45-mapping.txt"//resource R.txt to build patch apk, must input if there is resource changedtinkerApplyResourcePath = "${bakPath}/app-release-0421-12-34-45-R.txt"//only use for build all flavor, if not, just ignore this fieldtinkerBuildFlavorDirectory = "${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.namedef 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}") : bakPathfrom variant.outputs.outputFileinto destPathrename { String fileName ->fileName.replace("${fileNamePrefix}.apk", "${newFileNamePrefix}.apk")}from "${buildDir}/outputs/mapping/${variant.dirName}/mapping.txt"into destPathrename { String fileName ->fileName.replace("mapping.txt", "${newFileNamePrefix}-mapping.txt")}from "${buildDir}/intermediates/symbols/${variant.dirName}/R.txt"into destPathrename { String fileName ->fileName.replace("R.txt", "${newFileNamePrefix}-R.txt")}}}}}}project.afterEvaluate {//sample use for build all flavor for one timeif (hasFlavors) {task(tinkerPatchAllFlavorRelease) {group = 'tinker'def originOldPath = getTinkerBuildFlavorDirectory()for (String flavor : flavors) {def tinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Release")dependsOn tinkerTaskdef 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 tinkerTaskdef 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)@Overridepublic 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);}@Overridepublic 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";@Overridepublic 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 processTinkerServiceInternals.killTinkerPatchServiceProcess(getApplicationContext());Handler handler = new Handler(Looper.getMainLooper());handler.post(new Runnable() {@Overridepublic 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 fileif (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 apiif (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 offTinkerLog.i(TAG, "tinker wait screen to restart process");new ScreenState(getApplicationContext(), new ScreenState.IOnScreenOff() {@Overridepublic 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 processandroid.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() {@Overridepublic 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;}@Deprecatedpublic 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 directlyThrowable 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文件
  1. 在application标签里加入步骤四中新建的Application类 android:name=".SampleApplication"此处的名字需要与步骤四的SampleApplicationLike 类最顶部的@DefaultLifeCycle()注解保持一致。如果你添加不进去,或者是红色的话,请先build一下。如下红色圈中:

    SampleApplicationLike
  2. 注册SampleResultService
  3. 加入访问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" /><applicationandroid: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><serviceandroid: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 {@Overrideprotected 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);}@Overrideprotected void onResume() {super.onResume();Utils.setBackground(false);}@Overrideprotected void onPause() {super.onPause();Utils.setBackground(true);}@Overridepublic 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!大功告成!

问题记录

  1. 如果有同学遇到热修复过的app,无法正常进行版本升级的问题的话,可以参考这里,每次版本升级都需要更新 build.gradle 文件里的 TINKER_ID。如下图所示:

关于TinkerId的问题

后续

感谢微信团队!
Tinker github

关于release版本的混淆

可以参考这篇文章 5分钟搞定android混淆

关于release版本的多渠道打包

可以参考我的下一篇文章 (续)Android热更新:Tinker与Walle多渠道打包

关于某些平台需要加固的问题

可以参考Tinker官方wiki Tinker是否兼容加固

Demo 源码下载

哈哈哈哈,Demo 的 github 地址在这里



作者:涤生_Woo
链接:http://www.jianshu.com/p/aadcf2ea69a6
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

这篇关于Android热更新:微信Tinker框架的接入与测试的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/349599

相关文章

W外链微信推广短连接怎么做?

制作微信推广链接的难点分析 一、内容创作难度 制作微信推广链接时,首先需要创作有吸引力的内容。这不仅要求内容本身有趣、有价值,还要能够激起人们的分享欲望。对于许多企业和个人来说,尤其是那些缺乏创意和写作能力的人来说,这是制作微信推广链接的一大难点。 二、精准定位难度 微信用户群体庞大,不同用户的需求和兴趣各异。因此,制作推广链接时需要精准定位目标受众,以便更有效地吸引他们点击并分享链接

性能测试介绍

性能测试是一种测试方法,旨在评估系统、应用程序或组件在现实场景中的性能表现和可靠性。它通常用于衡量系统在不同负载条件下的响应时间、吞吐量、资源利用率、稳定性和可扩展性等关键指标。 为什么要进行性能测试 通过性能测试,可以确定系统是否能够满足预期的性能要求,找出性能瓶颈和潜在的问题,并进行优化和调整。 发现性能瓶颈:性能测试可以帮助发现系统的性能瓶颈,即系统在高负载或高并发情况下可能出现的问题

字节面试 | 如何测试RocketMQ、RocketMQ?

字节面试:RocketMQ是怎么测试的呢? 答: 首先保证消息的消费正确、设计逆向用例,在验证消息内容为空等情况时的消费正确性; 推送大批量MQ,通过Admin控制台查看MQ消费的情况,是否出现消费假死、TPS是否正常等等问题。(上述都是临场发挥,但是RocketMQ真正的测试点,还真的需要探讨) 01 先了解RocketMQ 作为测试也是要简单了解RocketMQ。简单来说,就是一个分

poj3468(线段树成段更新模板题)

题意:包括两个操作:1、将[a.b]上的数字加上v;2、查询区间[a,b]上的和 下面的介绍是下解题思路: 首先介绍  lazy-tag思想:用一个变量记录每一个线段树节点的变化值,当这部分线段的一致性被破坏我们就将这个变化值传递给子区间,大大增加了线段树的效率。 比如现在需要对[a,b]区间值进行加c操作,那么就从根节点[1,n]开始调用update函数进行操作,如果刚好执行到一个子节点,

hdu1394(线段树点更新的应用)

题意:求一个序列经过一定的操作得到的序列的最小逆序数 这题会用到逆序数的一个性质,在0到n-1这些数字组成的乱序排列,将第一个数字A移到最后一位,得到的逆序数为res-a+(n-a-1) 知道上面的知识点后,可以用暴力来解 代码如下: #include<iostream>#include<algorithm>#include<cstring>#include<stack>#in

hdu1689(线段树成段更新)

两种操作:1、set区间[a,b]上数字为v;2、查询[ 1 , n ]上的sum 代码如下: #include<iostream>#include<algorithm>#include<cstring>#include<stack>#include<queue>#include<set>#include<map>#include<stdio.h>#include<stdl

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

Android平台播放RTSP流的几种方案探究(VLC VS ExoPlayer VS SmartPlayer)

技术背景 好多开发者需要遴选Android平台RTSP直播播放器的时候,不知道如何选的好,本文针对常用的方案,做个大概的说明: 1. 使用VLC for Android VLC Media Player(VLC多媒体播放器),最初命名为VideoLAN客户端,是VideoLAN品牌产品,是VideoLAN计划的多媒体播放器。它支持众多音频与视频解码器及文件格式,并支持DVD影音光盘,VCD影

【测试】输入正确用户名和密码,点击登录没有响应的可能性原因

目录 一、前端问题 1. 界面交互问题 2. 输入数据校验问题 二、网络问题 1. 网络连接中断 2. 代理设置问题 三、后端问题 1. 服务器故障 2. 数据库问题 3. 权限问题: 四、其他问题 1. 缓存问题 2. 第三方服务问题 3. 配置问题 一、前端问题 1. 界面交互问题 登录按钮的点击事件未正确绑定,导致点击后无法触发登录操作。 页面可能存在

hdu 1754 I Hate It(线段树,单点更新,区间最值)

题意是求一个线段中的最大数。 线段树的模板题,试用了一下交大的模板。效率有点略低。 代码: #include <stdio.h>#include <string.h>#define TREE_SIZE (1 << (20))//const int TREE_SIZE = 200000 + 10;int max(int a, int b){return a > b ? a :