Tinker接入
2016-12-15 16:42
357 查看
Tinker是什么
Tinker是微信官方的Android热补丁解决方案,它支持动态下发代码、So库以及资源,让应用能够在不需要重新安装的情况下实现更新。当然,你也可以使用Tinker来更新你的插件。
接入流程
在项目的build.gradle中,添加依赖
buildscript { dependencies { classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.7.5') } }
接下来,就是app下的build.gradle中的设置了,这里可以copy一下Tinker提供的sample中的设置,稍加改动就可以了。具体如下
apply plugin: 'com.android.application' 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:24.2.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() { return hasProperty("MY_TINKER_ID")?MY_TINKER_ID:"tango001"//原本官网中默认返回的是git版本号作为TinkerId,这里先默认写死一个,这里在gradle.properties中定义了一个MY_TINKER_ID // try { // String gitRev = 'git rev-parse --short HEAD'.execute(null, project.rootDir).text.trim() // if (gitRev == null) { // throw new GradleException("can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'") // } // return gitRev // } catch (Exception e) { // throw new GradleException("can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'") // } } def javaVersion = JavaVersion.VERSION_1_7 android { compileSdkVersion 24 buildToolsVersion "24.0.2" compileOptions { sourceCompatibility javaVersion targetCompatibility javaVersion } signingConfigs { //gradle assembleRelease /** * 可以定义多个签名配置项,如下面的myConfig */ myConfig { storeFile file("babytree.keystore") storePassword "babytree" //修改成自己的签名文件store密码 keyAlias "babytree" keyPassword "babytree" //修改成自己的签名文件key密码 v2SigningEnabled=false } } buildTypes { //debug运行时使用试用release相同的签名配置,不需要时注释即可 debug { signingConfig signingConfigs.myConfig minifyEnabled false zipAlignEnabled false shrinkResources false } release { signingConfig signingConfigs.myConfig minifyEnabled true zipAlignEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } dexOptions { jumboMode = true } defaultConfig { applicationId "com.tango.tinkerdemo" minSdkVersion 10 targetSdkVersion 24 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" /** * you can use multiDex and install it in your ApplicationLifeCycle implement */ multiDexEnabled true /** * not like proguard, multiDexKeepProguard is not a list, so we can't just * add for you in our task. you can copy tinker keep rules at * build/intermediates/tinker_intermediates/tinker_multidexkeep.pro */ multiDexKeepProguard file("keep_in_main_dex.txt") /** * buildConfig can change during patch! * we can use the newly value when patch */ buildConfigField "String", "MESSAGE", "\"I am the base apk\"" // buildConfigField "String", "MESSAGE", "\"I am the patch apk\"" /** * client version would update with patch * so we can get the newly git version easily! */ buildConfigField "String", "CLIENTVERSION", "\"${getTinkerIdValue()}\"" buildConfigField "String", "PLATFORM", "\"all\"" } } def bakPath = file("${buildDir}/bakApk/") /** * you can use assembleRelease to build you base apk * use tinkerPatchRelease -POLD_APK= -PAPPLY_MAPPING= -PAPPLY_RESOURCE= to build patch * add apk from the build/bakApk */ ext { //for some reason, you may want to ignore tinkerBuild, such as instant run debug build? tinkerEnabled = true //for normal build //基准包所在位置,当我们要生成补丁时,需要填写它 tinkerOldApkPath = "${bakPath}/app-release-1116-15-41-25.apk" //生成基准包时用到的mapping文件 tinkerApplyMappingPath = "${bakPath}/app-release-1116-15-41-25-mapping.txt" //生成的资源文件R.txt tinkerApplyResourcePath = "${bakPath}/app-release-1116-15-41-25-R.txt" // tinkerBuildFlavorDirectory = "${bakPath}/app-release-1115-16-10-50-R.txt" } def getOldApkPath() { return hasProperty("OLD_APK") ? OLD_APK : ext.tinkerOldApkPath } def getApplyMappingPath() { return hasProperty("APPLY_MAPPING") ? APPLY_MAPPING : ext.tinkerApplyMappingPath } def getApplyResourceMappingPath() { return hasProperty("APPLY_RESOURCE") ? APPLY_RESOURCE : ext.tinkerApplyResourcePath } def getTinkerIdValue() { return hasProperty("TINKER_ID") ? TINKER_ID : gitSha() } def buildWithTinker() { return hasProperty("TINKER_ENABLE") ? TINKER_ENABLE : ext.tinkerEnabled } def getTinkerBuildFlavorDirectory() { return ext.tinkerBuildFlavorDirectory } if (buildWithTinker()) { apply plugin: 'com.tencent.tinker.patch' tinkerPatch { /** * necessary,default 'null' * the old apk path, use to diff with the new apk to build * add apk from the build/bakApk */ oldApk = getOldApkPath() /** * optional,default 'false' * there are some cases we may get some warnings * if ignoreWarning is true, we would just assert the patch process * case 1: minSdkVersion is below 14, but you are using dexMode with raw. * it must be crash when load. * case 2: newly added Android Component in AndroidManifest.xml, * it must be crash when load. * case 3: loader classes in dex.loader{} are not keep in the main dex, * it must be let tinker not work. * case 4: loader classes in dex.loader{} changes, * loader classes is ues to load patch dex. it is useless to change them. * it won't crash, but these changes can't effect. you may ignore it * case 5: resources.arsc has changed, but we don't use applyResourceMapping to build */ ignoreWarning = false /** * optional,default 'true' * whether sign the patch file * if not, you must do yourself. otherwise it can't check success during the patch loading * we will use the sign config with your build type */ useSign = true /** * Warning, applyMapping will affect the normal android build! */ buildConfig { /** * optional,default 'null' * if we use tinkerPatch to build the patch apk, you'd better to apply the old * apk mapping file if minifyEnabled is enable! * Warning: * you must be careful that it will affect the normal assemble build! */ applyMapping = getApplyMappingPath() /** * optional,default 'null' * It is nice to keep the resource id from R.txt file to reduce java changes */ applyResourceMapping = getApplyResourceMappingPath() /** * necessary,default 'null' * because we don't want to check the base apk with md5 in the runtime(it is slow) * tinkerId is use to identify the unique base apk when the patch is tried to apply. * we can use git rev, svn rev or simply versionCode. * we will gen the tinkerId in your manifest automatic */ tinkerId = getTinkerIdValue() } dex { /** * optional,default 'jar' * only can be 'raw' or 'jar'. for raw, we would keep its original format * for jar, we would repack dexes with zip format. * if you want to support below 14, you must use jar * or you want to save rom or check quicker, you can use raw mode also */ dexMode = "jar" /** * optional,default 'false' * if usePreGeneratedPatchDex is true, tinker framework will generate auxiliary class * and insert auxiliary instruction when compiling base package using * assemble{Debug/Release} task to prevent class pre-verified issue in dvm. * Besides, a real dex file contains necessary class will be generated and packed into * patch package instead of any patch info files. * * Use this mode if you have to use any dex encryption solutions. * * Notice: If you change this value, please trigger clean task * and regenerate base package. */ usePreGeneratedPatchDex = false /** * necessary,default '[]' * what dexes in apk are expected to deal with tinkerPatch * it support * or ? pattern. */ pattern = ["classes*.dex", "assets/secondary-dex-?.jar"] /** * necessary,default '[]' * Warning, it is very very important, loader classes can't change with patch. * thus, they will be removed from patch dexes. * you must put the following class into main dex. * Simply, you should add your own application {@code tinker.sample.android.SampleApplication} * own tinkerLoader, and the classes you use in them * */ loader = ["com.tencent.tinker.loader.*", //warning, you must change it with your application "tinker.sample.android.app.SampleApplication", //use sample, let BaseBuildInfo unchangeable with tinker "tinker.sample.android.app.BaseBuildInfo" ] } lib { /** * optional,default '[]' * what library in apk are expected to deal with tinkerPatch * it support * or ? pattern. * for library in assets, we would just recover them in the patch directory * you can get them in TinkerLoadResult with Tinker */ pattern = ["lib/armeabi/*.so"] } res { /** * optional,default '[]' * what resource in apk are expected to deal with tinkerPatch * it support * or ? pattern. * you must include all your resources in apk here, * otherwise, they won't repack in the new apk resources. */ pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"] /** * optional,default '[]' * the resource file exclude patterns, ignore add, delete or modify resource change * it support * or ? pattern. * Warning, we can only use for files no relative with resources.arsc */ ignoreChange = ["assets/sample_meta.txt"] /** * default 100kb * for modify resource, if it is larger than 'largeModSize' * we would like to use bsdiff algorithm to reduce patch file size */ largeModSize = 100 } packageConfig { /** * optional,default 'TINKER_ID, TINKER_ID_VALUE' 'NEW_TINKER_ID, NEW_TINKER_ID_VALUE' * package meta file gen. path is assets/package_meta.txt in patch file * you can use securityCheck.getPackageProperties() in your ownPackageCheck method * or TinkerLoadResult.getPackageConfigByName * we will get the TINKER_ID from the old apk manifest for you automatic, * other config files (such as patchMessage below)is not necessary */ configField("patchMessage", "tinker is sample to use") /** * just a sample case, you can use such as sdkVersion, brand, channel... * you can parse it in the SamplePatchListener. * Then you can use patch conditional! */ configField("platform", "all") /** * patch version via packageConfig */ configField("patchVersion", "1.0") } //or you can add config filed outside, or get meta value from old apk //project.tinkerPatch.packageConfig.configField("test1", project.tinkerPatch.packageConfig.getMetaDataFromOldApk("Test")) //project.tinkerPatch.packageConfig.configField("test2", "sample") /** * if you don't use zipArtifact or path, we just use 7za to try */ sevenZip { /** * optional,default '7za' * the 7zip artifact path, it will use the right 7za with your platform */ zipArtifact = "com.tencent.mm:SevenZip:1.1.10" /** * optional,default '7za' * you can specify the 7za path yourself, it will overwrite the zipArtifact value */ // path = "/usr/local/bin/7za" } } List<String> flavors = new ArrayList<>(); project.android.productFlavors.each {flavor -> flavors.add(flavor.name) } boolean hasFlavors = flavors.size() > 0 /** * 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 { //这里一系列的方法就是生成apk/mapping.text/R.txt 时的重命名及copy到bakPath路径下 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' b8ae 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" } } } } } }
在打补丁包的时候,我们要注意替换下其中的属性
/** * you can use assembleRelease to build you base apk * use tinkerPatchRelease -POLD_APK= -PAPPLY_MAPPING= -PAPPLY_RESOURCE= to build patch * add apk from the build/bakApk */ ext { //for some reason, you may want to ignore tinkerBuild, such as instant run debug build? tinkerEnabled = true //for normal build //基准包所在位置,当我们要生成补丁时,需要填写它 tinkerOldApkPath = "${bakPath}/app-release-1116-15-41-25.apk" //生成基准包时用到的mapping文件 tinkerApplyMappingPath = "${bakPath}/app-release-1116-15-41-25-mapping.txt" //生成的资源文件R.txt tinkerApplyResourcePath = "${bakPath}/app-release-1116-15-41-25-R.txt" // tinkerBuildFlavorDirectory = "${bakPath}/app-release-1115-16-10-50-R.txt" }
由于Tinker用到了multiDex分包,细看之前上面给出的gradle,里面有一句
multiDexKeepProguard file("keep_in_main_dex.txt")
这个是在分包的时候,我们需要保持特定的类在主dex中,需要保持的定义写在了app下的keep_in_main_dex.txt中,内容如下:(需要copy到你的app目录下)
# you can copy the tinker keep rule at # build/intermediates/tinker_intermediates/tinker_multidexkeep.pro -keep class com.tencent.tinker.loader.** { *; } -keep class tinker.sample.android.app.SampleApplication { *; } -keep public class * implements com.tencent.tinker.loader.app.ApplicationLifeCycle { *; } -keep public class * extends com.tencent.tinker.loader.TinkerLoader { *; } -keep public class * extends com.tencent.tinker.loader.app.TinkerApplication { *; } # here, it is your own keep rules. # you must be careful that the class name you write won't be proguard # but the tinker class above is OK, we have already keep for you!
接下来是app下的gradle.properties文件中的内容:
# Project-wide Gradle settings. # IDE (e.g. Android Studio) users: # Gradle settings configured through the IDE *will override* # any settings specified in this file. # For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. org.gradle.jvmargs=-Xmx1536m # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true TINKER_VERSION=1.7.5 MY_TINKER_ID=tango001
gradle方面的配置基本上就差不多了,对gradle语法不熟的同学可以看看语法,其实我也不熟,但是里面的方法和java的基本区别不太大,还是比较容易理解的
接下来,应该算是比较复杂的地方了,也是我个人对于Tinker最大的槽点,那就是,Tinker里不允许出现我们自己的Application,而我们自己的application必须经过改造才可以,下面引用官网原话:
程序启动时会加载默认的Application类,这导致我们补丁包是无法对它做修改了。如何规避?在这里我们并没有使用类似InstantRun hook Application的方式,而是通过代码框架的方式来避免,这也是为了尽量少的去反射,提升框架的兼容性。这里我们要实现的是完全将原来的Application类隔离起来,即其他任何类都不能再引用我们自己的Application。
其实个人觉得是比较麻烦的,接下来先说一说改造的大概步骤吧
1.先定义一个MyApplicationLike类,继承DefaultApplicationLike
2.然后将我们的Application里面所有方法copy到MyApplicationLike中,可能有人会有疑问了,那么如果我们原application是继承自Library中的Application怎么办呢?一样,连带着把曾经继承的Application里面的方法也一起copy到MyApplicationLike中。
3.原Application中的attachBaseContext方法实现要单独移动到MyApplicationLike的onBaseContextAttached中
4.MyApplicationLike中需要用到application的地方一律改成getApplication()
5.其他地方引用到原Application的对象及方法的地方,改为引用MyApplicationLike的静态对象与方法
接下来,就是需要引用官网提供的sample代码了,其实直接拿来用就可以,也就是Application改造那里需要注意一下,代码风格很赞的,具体问题也可以查看他们的官方文档,写的很详细Tinker接入指南
使用的时候注意,基准包要用gradle的assembleRelease脚本去打包,而不是instantRun,在生成基准包的时候,要打补丁包时注意替换gradle中oldApk等配置,然后再利用Tinker自带的gradle脚本tinkerPatchRelease去打补丁包。
相关文章推荐
- 使用C++实现JNI接口需要注意的事项
- Android IPC进程间通讯机制
- Android Manifest 用法
- [转载]Activity中ConfigChanges属性的用法
- Android之获取手机上的图片和视频缩略图thumbnails
- Android之使用Http协议实现文件上传功能
- Android学习笔记(二九):嵌入浏览器
- android string.xml文件中的整型和string型代替
- i-jetty环境搭配与编译
- android之定时器AlarmManager
- android wifi 无线调试
- Android Native 绘图方法
- Android java 与 javascript互访(相互调用)的方法例子
- android 代码实现控件之间的间距
- android FragmentPagerAdapter的“标准”配置
- Android"解决"onTouch和onClick的冲突问题
- android:installLocation简析
- android searchView的关闭事件