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以及桌面图标。
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 { /** * 默认为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" } } } } } }
3.在清单文件中集成application和服务 ,name的application必须是.amsky,如果你添加不进去,或者是红色的话,请先build一下,如果你已经有了自己的application,后面我会说怎么来集成,service中做的操作是在你加载成功热更新插件后,会提示你更新成功,并且这里做了锁屏操作就会加载热更新插件,继续往下看。
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.tinker.demo.tinkerdemo"> <uses-permission android:name="android.permission.write_external_storage"/> <uses-permission android:name="android.permission.read_external_storage"/> <application android:allowbackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsrtl="true" android:name=".amsky" android:theme="@style/apptheme"> <service android:name=".service.sampleresultservice" android:exported="false"/> <activity android:name=".mainactivity"> <intent-filter> <action android:name="android.intent.action.main" /> <category android:name="android.intent.category.launcher" /> </intent-filter> </activity> </application> </manifest>
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); } /** * 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); //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); } /** * 加载热补丁插件 * @param v */ public void loadpatch(view v) { tinkerinstaller.onreceiveupgradepatch(getapplicationcontext(), "/sdcard/debug/patch.apk"); } /** * 杀死应用加载补丁 * @param v */ 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!"); } } } /** * 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); } } }
7.utils文件
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; } } }
到这里就已经集成完毕,下面来说下使用的方法
这是有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一键集成后台:
更多精彩内容请点击《android微信开发教程汇总》,《java微信开发教程汇总》欢迎大家学习阅读。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
上一篇: Android离线缓存的实例代码