先看一下效果图
Tinker已知问题
由于原理与系统限制,Tinker有以下已知问题:
- Tinker不支持修改AndroidManifest.xml,Tinker不支持新增四大组件;
- 由于Google Play的开发者条款限制,不建议在GP渠道动态更新代码;
- 在Android N上,补丁对应用启动时间有轻微的影响;
- 不支持部分三星android-21机型,加载补丁时会主动抛出”TinkerRuntimeException:checkDexInstall failed”;
- 由于各个厂商的加固实现并不一致,在1.7.6以及之后的版本,tinker不再支持加固的动态更新;
- 对于资源替换,不支持修改remoteView。例如transition动画,notification icon以及桌面图标。
1.首先在项目的build中,集成tinker插件 ,如下所示(目前最新版是1.7.6)
先看结构图,只有几个类而已:
项目中的build集成
buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:2.2.3' classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.7.6') // 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 }
1.再将app的build中的关联属性添加进去,这些属性都是经过测试过的,都有注释显示,如果自己需要其他属性,可以自己去github上查看并集成,文章末尾会送上地址,ps:官方的集成特别麻烦,有时候一整天都有可能搞不定,根据自己的需求和情况来添加,末尾会送上demo
apply plugin: 'com.android.application' def javaVersion = JavaVersion.VERSION_1_7 android { compileSdkVersion 23 buildToolsVersion "23.0.2" compileOptions { sourceCompatibility javaVersion targetCompatibility javaVersion } //recommend dexOptions { jumboMode = true } defaultConfig { applicationId "com.tinker.demo.tinkerdemo" 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/release.keystore") storePassword "testres" keyAlias "testres" keyPassword "testres" } catch (ex) { throw new InvalidUserDataException(ex.toString()) } } debug { storeFile file("./keystore/debug.keystore") } } 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-debug-0113-14-01-29.apk" //proguard mapping file to build patch apk tinkerApplyMappingPath = "${bakPath}/app-debug-1018-17-32-47-mapping.txt" //resource R.txt to build patch apk, must input if there is resource changed tinkerApplyResourcePath = "${bakPath}/app-debug-0113-14-01-29-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 { oldApk = getOldApkPath() ignoreWarning = false useSign = true tinkerEnable = buildWithTinker() buildConfig { applyMapping = getApplyMappingPath() applyResourceMapping = getApplyResourceMappingPath() tinkerId = getTinkerIdValue() keepDexApply = false } dex { dexMode = "jar" pattern = ["classes*.dex", "assets/secondary-dex-?.jar"] loader = [ //use sample, let baseBuildInfo unchangeable with tinker "tinker.sample.android.app.baseBuildInfo" ] } lib { pattern = ["lib/armeabi pattern = ["res ignoreChange = ["assets/sample_meta.txt"] largeModSize = 100 } packageConfig { configField("patchMessage", "tinker is sample to use") configField("platform", "all") configField("patchVersion", "1.0") } //或者您可以添加外部的配置文件,或从旧apk获取元值 //project.tinkerPatch.packageConfig.configField("test1", project.tinkerPatch.packageConfig.getmetaDataFromOldApk("Test")) //project.tinkerPatch.packageConfig.configField("test2", "sample") sevenZip { zipArtifact = "com.tencent.mm:SevenZip:1.1.10" // path = "/usr/local/bin/7za" } } Listflavors = new ArrayList<>(); project.android.productFlavors.each {flavor -> flavors.add(flavor.name) } boolean hasFlavors = flavors.size() > 0 android.applicationVariants.all { variant -> 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" } } } } } }
3.在清单文件中集成application和服务 ,name的application必须是.AMSKY,如果你添加不进去,或者是红色的话,请先build一下,如果你已经有了自己的application,后面我会说怎么来集成,Service中做的操作是在你加载成功热更新插件后,会提示你更新成功,并且这里做了锁屏操作就会加载热更新插件,继续往下看。
4.到这里就已经基本集成的差不多了,剩下的就是代码里面的集成,首先是application,这里主要说如果是自已已经存在的application的时候改怎么操作 ,这个applicaiton可以说就是自己的一个application,只不过写法,要这样去写,可以在onCreate中做自己的一些操作,只不过清单文件中,要写AMSKY
@SuppressWarnings("unused") @DefaultLifeCycle(application = "com.tinker.demo.tinkerdemo.AMSKY", 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,Resources[] resources, ClassLoader[] classLoader, AssetManager[] assetManager) { super(application,tinkerFlags,tinkerLoadVerifyFlag,applicationStartElapsedTime,applicationStartMillisTime, tinkerResultIntent, resources, classLoader, assetManager); } @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) @Override public void onbaseContextAttached(Context base) { super.onbaseContextAttached(base); //MultiDex必须在Tinker初始化之前 MultiDex.install(base); //这里就是初始化Tinker TinkerInstaller.install(this,new DefaultLoadReporter(getApplication()),new DefaultPatchReporter(getApplication()), new DefaultPatchListener(getApplication()),SampleResultService.class,new UpgradePatch()); Tinker tinker = Tinker.with(getApplication()); //这个只是一个Toast提示 Toast.makeText( getApplication(),"没鸟用,就是Toast提示而已", Toast.LENGTH_SHORT).show(); } @Override public void onCreate() { super.onCreate(); //这里可以做自己的操作 } @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) { getApplication().registerActivityLifecycleCallbacks(callback); } }
5.这里就是在MainActivity中来加载热更新文件,在点击加载的时候,就直接锁屏加载(不要删除service),当然退出app,下次进来也是可以加载的吗,这里加载补丁插件的话,路径可以自己设置,我是放在根目录的debug文件夹当中的,并且我的补丁插件名字叫patch,可以自行更改。
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } public void loadPatch(View v) { TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), "/sdcard/debug/patch.apk"); } public void killApp(View v) { ShareTinkerInternals.killAllOtherProcess(getApplicationContext()); android.os.Process.killProcess(android.os.Process.myPid()); } @Override protected void onResume() { super.onResume(); Utils.setBackground(false); } @Override protected void onPause() { super.onPause(); Utils.setBackground(true); } }
6.Service文件
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!"); } } } 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); } } }
7.Utils文件
public class Utils { 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; } } }
到这里就已经集成完毕,下面来说下使用的方法
这是有bug的版本,我们测试就使用assembleDebug来测试 ,注意没点击assembleDebug之前,build文件夹里面是没有bakApk文件夹的
2.点击assembleDebug之后会出现bakApk这个文件夹,里面就有apk文件,如果失败,记得clean一下,然后build一下
3.接下来在build文件夹里面,更改ext中的属性,将bakApk中生成的apk文件和R文件复制到ext这里,如果你打的release包有mapping的话同样复制到这里,我们这里是debug测试,所以没有mapping文件
4.下面就修改我们需要更新,或者更改的bug,我这里是添加一张图片,并且更改标题显示
这是有bug的版本,我还没添加图片,更改标题
这里我添加了一张aa的图片,并且更改了标题
5.接下来我们运行tinker下面的tinkerPatchDebug,来生成补丁包,这个补丁包在outputs下面
点击完成后,就会生成tinkerPatch文件夹
将tinkerPatch文件夹下面的patch_signed_7zip.apk文件,粘贴出来,改成你的MainActivity中加载的文件名字,我这里叫patch,然后点击加载没加载之前
加载之后,锁频,解锁 ,补丁已经加载出来了,并且文件夹中的补丁已经不在了,因为它和老apk合并了
注意
签名文件的话 在build的signingConfigs中设置,以及左侧的kestore文件夹中设置 ,如下图
项目github地址:TinkerDemo
Tinker原项目地址:https://github.com/Tencent/tinker
Tinker使用指南:https://github.com/Tencent/tinker/wiki
Tinker一键集成(这个简单,但是不能从自己服务器上下载补丁,不需配置Tinker自己的后台,有部分局限性,自行选择):https://github.com/TinkerPatch/tinkerpatch-sdk/blob/master/docs/tinkerpatch-android-sdk.md
Tinker一键集成后台:http://www.tinkerpatch.com/
更多精彩内容请点击《Android微信开发教程汇总》,《java微信开发教程汇总》欢迎大家学习阅读。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持考高分网。