Tinker热更新
来源:互联网 发布:php 不识别utf8 编辑:程序博客网 时间:2024/06/06 07:04
Android热更新:微信Tinker框架的接入与测试
Android热修复框架的对比(最终选择微信Tinker)
总结对比摘自Tinker官方Wiki
- AndFix作为native解决方案,首先面临的是稳定性与兼容性问题,更重要的是它无法实现类替换,它是需要大量额外的开发成本的;
- Robust兼容性与成功率较高,但是它与AndFix一样,无法新增变量与类只能用做的bugFix方案;
- Qzone方案可以做到发布产品功能,但是它主要问题是插桩带来Dalvik的性能问题,以及为了解决Art下内存地址问题而导致补丁包急速增大的。
特别是在Android N之后,由于混合编译的inline策略修改,对于市面上的各种方案都不太容易解决。而Tinker热补丁方案不仅支持类、So以及资源的替换,它还是2.X-7.X的全平台支持。利用Tinker我们不仅可以用做bugfix,甚至可以替代功能的发布。Tinker已运行在微信的数亿Android设备上,那么为什么你不使用Tinker呢?
Tinker的已知问题
截至文章发布当天,微信 Tinker 热修复框架 尚存问题如下所示:
- Tinker不支持修改AndroidManifest.xml,Tinker不支持新增四大组件;
- 由于Google Play的开发者条款限制,不建议在GP渠道动态更新代码;
- 在Android N上,补丁对应用启动时间有轻微的影响;
- 不支持部分三星android-21机型,加载补丁时会主动抛出"TinkerRuntimeException:checkDexInstall failed";
- 由于各个厂商的加固实现并不一致,在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_7android { 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上面提到最新版本支持应用加固,见下图
步骤四:自己的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一下。如下红色圈中: - 注册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()); }}
看看主界面的样子,很朴素。
至此,我们对 Tinker 的接入已经完成了,剩下的就是对 Tinker 热修复的测试。
二、测试Tinker
步骤一:打基础包
点开 Android Studio的Gradle 界面,如下,双击 assembleDebug 或者 assembleRelease:
注意看,项目目录build文件夹里面,在双击 assembleDebug 或者 assembleRelease 之前,是如下界面:
在双击 assembleDebug 或者 assembleRelease 之后,build文件夹下面会生成一个bakApk文件夹,里面存放着我们的基础包,里面的apk文件用于安装到手机测试或者发布到应用市场,(这里生成的基础安装包和 R文件以及release版本的mapping文件一定要自己保存好,因为通过后续的步骤你会清楚地看到,每次打补丁包都需要用到这些文件作为基础文件,丢掉的话,后果就很滑稽脸了),如下:
- 如果打包失败,请clean一下项目,再双击 assembleDebug 或者 assembleRelease;
- 如果clean之后再打包还失败,那就需要看具体的报错,慢慢调试设置(首先很有可能是代码混淆的问题,文末有混淆相关的介绍文章,写得很全面易懂)。
安装到手机后,发现,有bug,如下显示,果断接着步骤二,使用Tinker紧急热修复这个bug:
步骤二:打补丁包
1.将步骤一生成的 bakApk 文件夹中的 apk 文件和 R 文件的名称,填写到app的 build.gradle 类的 ext 这里,sync一下,如下:
当然,如果在步骤一打的是release的基础包的话,会多一个mapping文件,同样将它的名称填写到app的 build.gradle 类的 ext 这里,界面如下:
2.接着,我们去修改主界面的bug,并增加一个图片资源文件(图片自己找),如下:
3.接下来,真正地打补丁包,点开 Android Studio的Gradle 界面,如下,双击 tinkerPatchDebug 或者tinkerPatchRelease ,如下:
4.紧接着,Tinker 在build 文件夹下的 outputs 文件夹里面会生成我们需要的补丁文件,patch_signed_7zip.apk 就是我们所要的补丁包,如下:
当然,如果你想了解更多关于输出文件的情况,可以点开Tinker Wiki 的 输出文件详解。
步骤三:将补丁包拷贝到手机sdcard中测试
将步骤二生成的 tinkerPatch 文件夹下面的 patch_signed_7zip.apk 文件,拷贝出来,改成你的 MainActivity中加载的文件名字,demo这里叫TinkerPatch,将其拷贝到手机的sdcard中的myTinkerDemo 文件夹下,没有这个文件夹你就自己手动新建一个,下图带你回顾一下 MainActivity 的设置:
注意此处,测试和发布版本的不同:发布版本的补丁文件一般是通过网络下载下来,存放到sdcard中,再加载。
步骤四:加载补丁
点击主页的加载补丁按钮,没加载之前如下界面:
点击加载补丁之后,锁屏或者杀死进程,再次进入demo,补丁已经加载出来,并且 sdcard中的补丁包也会被删除掉,因为它和老apk合并了。如下:
OK!大功告成!
问题记录
- 如果有同学遇到热修复过的app,无法正常进行版本升级的问题的话,可以参考这里,每次版本升级都需要更新 build.gradle 文件里的 TINKER_ID。如下图所示:
后续
感谢微信团队!
Tinker github
关于release版本的混淆
可以参考这篇文章 5分钟搞定android混淆
关于release版本的多渠道打包
可以参考我的下一篇文章 (续)Android热更新:Tinker与Walle多渠道打包
关于某些平台需要加固的问题
可以参考Tinker官方wiki Tinker是否兼容加固
Demo 源码下载
哈哈哈哈,Demo 的 github 地址在这里
- 集成tinker热更新
- Tinker热更新
- Android 热更新之tinker
- 微信tinker热更新
- Android热更新(Tinker)
- Android热更新框架Tinker无法更新?
- 热更新Tinker研究(一):运行tinker-sample-android
- 微信Tinker热更新集成文档
- 热更新Tinker的初次集成使用
- 热更新Tinker的集成使用
- 微信Tinker热更新详细使用
- Android 热更新 Bugly(Tinker)接入
- 热更新Tinker研究(四):TinkerLoader
- 热更新Tinker研究(六):TinkerPatchPlugin
- 热更新Tinker 的研究与集成
- Android Tinker热更新Sample_demo接入
- Android 热更新Tinker简单使用
- Android热更新——Tinker 使用
- Airlines — 2
- 职场历程记 2017-09-01
- SonarQube与SonarRunner安装配置
- maven常见问题问答
- 数组中的逆序对
- Tinker热更新
- Android 源码解析 之 setContentView
- 欢迎使用CSDN-markdown编辑器
- php中的单项链表与双向链表
- Centos下数据写入MySQL数据库汉字是????
- MySql教程Link
- 第五章——视图控制器
- fasttext初步使用
- JavaScript的一些基本方法总结