Android热更新——Tinker 使用
来源:互联网 发布:mac录音精灵破解版 编辑:程序博客网 时间:2024/06/05 09:14
现在热修复的技术基本上有阿里的AndFix、QZone的方案、美团提出的思想方案以及腾讯的Tinker等。
Tinker是微信官方的Android热补丁解决方案,它支持动态下发代码。.so库以及资源库,让应用能够在不需要重复安装的情况下实现更新,当然也可以使用Tinker来更新你的插件。
原理:tinker将old.apk和new.apk做了diff,拿到patch.dex,然后将patch.dex与本机中apk的classes.dex做了合并,生成新的classes.dex,运行时通过反射将合并后的dex文件放置在加载的dexElements数组的前面
使用 这里只是一个demo:
在项目的build.gradle中
buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:2.3.3' // TinkerPatch 插件 classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.9.1') }}
然后在app的gradle文件app/build.gradle中
//optional, help to generate the final application provided('com.tencent.tinker:tinker-android-anno:1.9.1') //tinker's main Android lib compile('com.tencent.tinker:tinker-android-lib:1.9.1')
tinker 在app的gradle文件app/build.gradle中一些配置需要更改代码部分(————————文字) 用时候可以直接复制完整代码中开始配置下的完整代码。
def bakPath = file("${buildDir}/bakApk/")def gitSha() { try { // appid 和版本好对应 不容易混(也可以随便写) String gitRev = '1.0.0' //_______________________________更改 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'") }}ext { // 在debug这里改为fase tinkerEnabled = true //旧apk tinkerOldApkPath = "${bakPath}/app-debug-1122-11-59-56.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 旧包R文件 tinkerApplyResourcePath = "${bakPath}/app-debug-1122-11-59-56-R.txt" // //_______________________________更改 //only use for build all flavor, if not, just ignore this field 多渠道 tinkerBuildFlavorDirectory = "${bakPath}/app-1018-17-32-47" //______________________________多渠道后更改 否则这不用}
app的gradle文件app/build.gradle中完整代码 ,使用时候直接复制———配置开始————下代码做修改 。如何修改后边会说
apply plugin: 'com.android.application'android { signingConfigs { release { try { storeFile file('/Users/hexiang/Downloads/AndroidDemo/detu/TinkerTestProject/keystore.jks') keyAlias 'key0' keyPassword '123456' storePassword '123456' } catch (ex) { throw new InvalidUserDataException(ex.toString()) } } debug { try { storeFile file('/Users/hexiang/Downloads/AndroidDemo/detu/TinkerTestProject/keystore.jks') keyAlias 'key0' keyPassword '123456' storePassword '123456' } catch (ex) { throw new InvalidUserDataException(ex.toString()) } } } compileSdkVersion 25 buildToolsVersion "25.0.3" defaultConfig { applicationId "com.guohao.tinkertest" minSdkVersion 15 targetSdkVersion 25 versionCode 1 versionName "1.0.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } }}dependencies { compile fileTree(include: ['*.jar'], dir: 'libs') 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:25.3.1' compile 'com.android.support.constraint:constraint-layout:1.0.2' testCompile 'junit:junit:4.12' //可选,用于生成application类 provided('com.tencent.tinker:tinker-android-anno:1.7.5') //tinker的核心库 compile('com.tencent.tinker:tinker-android-lib:1.7.5')}//-----------------------------------配置开始-----------------------------------def bakPath = file("${buildDir}/bakApk/")def gitSha() { try { // appik 一版和版本好对应 不容易混 String gitRev = '1.0.0' //_______________________________更改 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'") }}ext { // 在debug这里改为fase tinkerEnabled = true //旧apk tinkerOldApkPath = "${bakPath}/app-debug-1122-11-59-56.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 旧包R文件 tinkerApplyResourcePath = "${bakPath}/app-debug-1122-11-59-56-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 { /** * 基准apk包路径,也就是旧包路径 * */ oldApk = getOldApkPath() /** * 如果出现以下的情况,并且ignoreWarning为false,我们将中断编译。因为这些情况可能会导致编译出来的patch包 * 带来风险: * 1. minSdkVersion小于14,但是dexMode的值为"raw"; * 2. 新编译的安装包出现新增的四大组件(Activity, BroadcastReceiver...); * 3. 定义在dex.loader用于加载补丁的类不在main dex中; * 4. 定义在dex.loader用于加载补丁的类出现修改; * 5. resources.arsc改变,但没有使用applyResourceMapping编译。 * */ ignoreWarning = false /** * 在运行过程中,我们需要验证基准apk包与补丁包的签名是否一致,我们是否需要为你签名 * */ useSign = true buildConfig { /** * 可选参数;在编译新的apk时候,我们希望通过保持旧apk的proguard混淆方式,从而减少补丁包的大小。这个只 * 是推荐的,但设置applyMapping会影响任何的assemble编译。 * */ applyMapping = getApplyMappingPath() /** * 可选参数;在编译新的apk时候,我们希望通过旧apk的R.txt文件保持ResId的分配,这样不仅可以减少补丁包的 * 大小,同时也避免由于ResId改变导致remote view异常。 * */ applyResourceMapping = getApplyResourceMappingPath() /** * 在运行过程中,我们需要验证基准apk包的tinkerId是否等于补丁包的tinkerId。这个是决定补丁包能运行在哪些 * 基准包上面,一般来说我们可以使用git版本号、versionName等等。 * */ tinkerId = getTinkerIdValue() } dex { /** * 只能是'raw'或者'jar'。 * 对于'raw'模式,我们将会保持输入dex的格式。 * 对于'jar'模式,我们将会把输入dex重新压缩封装到jar。如果你的minSdkVersion小于14,你必须选择‘jar’模式 * ,而且它更省存储空间,但是验证md5时比'raw'模式耗时() * */ dexMode = "jar" /** * 是否提前生成dex,而非合成的方式。这套方案即回退成Qzone的方案,对于需要使用加固或者多flavor打包(建 * 议使用其他方式生成渠道包)的用户可使用。但是这套方案需要插桩,会造成Dalvik下性能损耗以及Art补丁包可 * 能过大的问题,务必谨慎使用。另外一方面,这种方案在Android N之后可能会产生问题,建议过滤N之后的用户。 */ usePreGeneratedPatchDex = false /** * 需要处理dex路径,支持*、?通配符,必须使用'/'分割。路径是相对安装包的,例如/assets/... */ pattern = ["classes*.dex", "assets/secondary-dex-?.jar"] /** * 这一项非常重要,它定义了哪些类在加载补丁包的时候会用到。这些类是通过Tinker无法修改的类,也是一定要放在main dex的类。 这里需要定义的类有: 1. 你自己定义的Application类; 2. Tinker库中用于加载补丁包的部分类,即com.tencent.tinker.loader.*; 3. 如果你自定义了TinkerLoader,需要将它以及它引用的所有类也加入loader中; 4. 其他一些你不希望被更改的类,例如Sample中的BaseBuildInfo类。这里需要注意的是,这些类的直接引用类也 需要加入到loader中。或者你需要将这个类变成非preverify。 */ loader = ["com.tencent.tinker.loader.*", //warning, you must change it with your application //TODO 换成自己的Application "com.tinker.MyApplication", ] } lib { /** * 需要处理lib路径,支持*、?通配符,必须使用'/'分割。与dex.pattern一致, 路径是相对安装包的,例如/assets/... */ pattern = ["lib/armeabi/*.so"] } res { /** * 需要处理res路径,支持*、?通配符,必须使用'/'分割。与dex.pattern一致, 路径是相对安装包的,例如/assets/..., * 务必注意的是,只有满足pattern的资源才会放到合成后的资源包。 */ pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"] /** * 支持*、?通配符,必须使用'/'分割。若满足ignoreChange的pattern,在编译时会忽略该文件的新增、删除与修改。 * 最极端的情况,ignoreChange与上面的pattern一致,即会完全忽略所有资源的修改。 */ ignoreChange = ["assets/sample_meta.txt"] /** * 对于修改的资源,如果大于largeModSize,我们将使用bsdiff算法。这可以降低补丁包的大小,但是会增加合成 * 时的复杂度。默认大小为100kb */ largeModSize = 100 } packageConfig { /** * configField("key", "value"), 默认我们自动从基准安装包与新安装包的Manifest中读取tinkerId,并自动 * 写入configField。在这里,你可以定义其他的信息,在运行时可以通过TinkerLoadResult.getPackageConfigByName得到相应的数值。但是建议直接通过修改代码来实现,例如BuildConfig。 */ configField("patchMessage", "tinker is sample to use") } sevenZip { /** * 例如"com.tencent.mm:SevenZip:1.1.10",将自动根据机器属性获得对应的7za运行文件,推荐使用 */ zipArtifact = "com.tencent.mm:SevenZip:1.1.10" } /** * 文件名 描述 * patch_unsigned.apk 没有签名的补丁包 * patch_signed.apk 签名后的补丁包 * patch_signed_7zip.apk 签名后并使用7zip压缩的补丁包,也是我们通常使用的补丁包。但正式发布的时候,最好不要以.apk结尾,防止被运营商挟持。 * log.txt 在编译补丁包过程的控制台日志 * dex_log.txt 在编译补丁包过程关于dex的日志 * so_log.txt 在编译补丁包过程关于lib的日志 * tinker_result 最终在补丁包的内容,包括diff的dex、lib以及assets下面的meta文件 * resources_out.zip 最终在手机上合成的全量资源apk,你可以在这里查看是否有文件遗漏 * resources_out_7z.zip 根据7zip最终在手机上合成的全量资源apk * tempPatchedDexes 在Dalvik与Art平台,最终在手机上合成的完整Dex,我们可以在这里查看dex合成的产物。 * * * */ /** * 获得所有渠道集合,并判断数量 */ List<String> flavors = new ArrayList<>(); project.android.productFlavors.each { flavor -> flavors.add(flavor.name) } boolean hasFlavors = flavors.size() > 0 /** * bak apk and mapping * 创建Task并执行文件操作 */ 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" } } } } } }}
初始化 app Applicarion 用官方推荐的写法,
import android.annotation.TargetApi;import android.app.Application;import android.content.Context;import android.content.Intent;import android.content.res.AssetManager;import android.content.res.Resources;import android.os.Build;import android.os.Environment;import android.util.Log;import com.tencent.tinker.anno.DefaultLifeCycle;import com.tencent.tinker.lib.tinker.TinkerInstaller;import com.tencent.tinker.loader.app.DefaultApplicationLike;import com.tencent.tinker.loader.shareutil.ShareConstants;@DefaultLifeCycle( application = "com.tinker.MyApplication", flags = ShareConstants.TINKER_ENABLE_ALL)public class SampleApplicarion extends DefaultApplicationLike { private static final String TAG= "SampleApplicarion_path"; public SampleApplicarion(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); } @Override public void onBaseContextAttached(Context base) { super.onBaseContextAttached(base); TinkerInstaller.install(this); } @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) { getApplication().registerActivityLifecycleCallbacks(callback); } @Override public void onCreate() { super.onCreate(); // 这里我们把新包放置在 DCIM 文件夹下制定文件 String patchPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/DCIM/patch_signed_7zip.apk"; Log.d(TAG,"需要放置的新包patch_" + patchPath); TinkerInstaller.onReceiveUpgradePatch(getApplication().getApplicationContext(), patchPath); }}
更改清单文件 使用我们自己的app Applicarion 并加入权限读取sd卡 以及 上网的权限
注意 :这里的name 属性 是我们注解里写的
<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.guohao.tinkertest"> <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <application android:name="com.tinker.MyApplication" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" 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> </application></manifest>
配置结束就需要试验 下面是详细步骤:
1.我们在activity 里什么也不做。
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); }}
2.点击编译器右边的 gradle 如下图操作打包 gradle —> build——>assembleDebug (正式选 assembleRelease)
3 打包好后 到app ——>build——>bakApk 可以看到打出来包我们把它安装到手机上做为基本包(即 有问题的apk 接下来我们会试验更新 )。我们记住这个包名.apk 和R.txt 文件名称(以时间生成的) 并把这个apk 安装到手机。打开app 只有我们开始创建项目的hello word 。
4 .下边我们我们进行更新。在activity 里边添加一个Toast
package com.guohao.tinkertest;import android.support.v7.app.AppCompatActivity;import android.os.Bundle;import android.widget.Toast;public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 更新后 Toast Toast.makeText(this, "这是个补丁", Toast.LENGTH_SHORT).show(); }}
6 更改我们的app的gradle文件 上边提到的部分 tinkerOldApkPath 和 tinkerApplyResourcePath 改为我们打包出来的 两个文件名字 改好后编译。
ext { // 在debug这里改为fase tinkerEnabled = true //旧apk tinkerOldApkPath = "${bakPath}/app-debug-1122-11-59-56.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 旧包R文件 tinkerApplyResourcePath = "${bakPath}/app-debug-1122-11-59-56-R.txt" // //_______________________________更改 //only use for build all flavor, if not, just ignore this field 多渠道 tinkerBuildFlavorDirectory = "${bakPath}/app-1018-17-32-47" //______________________________多渠道后更改 否则这不用}
7 .点击编译器右边的 gradle 如下图操作打包 gradle —> tinker——>tinkrPatchDebug (正式选 inkrPatchRelease)
8 .打包好后 到app ——>build——>outpouts——>tinkerPatch 可以看到打出来包我们只关注我们需要的补丁包patch_signed_7zip.apk。
9 .我们把这个 补丁包放到手机里的我们指定文件夹 (/DCIM/patch_signed_7zip.apk。 需要注意文件名字要和代码写的一样)(做项目一般放置在服务器,自己去下载)。我是用电脑下载 android 文件传输 直接放进去的。
@Override public void onCreate() { super.onCreate(); // 这里我们把新包放置在 DCIM 文件夹下 String patchPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/DCIM/patch_signed_7zip.apk"; Log.d(TAG,"需要放置的新包patch_" + patchPath); TinkerInstaller.onReceiveUpgradePatch(getApplication().getApplicationContext(), patchPath); }}
10 .有以上步骤 就结束了 ,终于可以看看我们 的成果。我们关闭前的app , 一定要清理后台杀死进程。然后在进入 ,发现会散退下。接着在打开,发现Toast弹出来的,这样我们就更新成功了。
这里只是一个集成使用的小demo .有很多东西使用时候 更具使用做调整。最后附上
官网 tinker github https://github.com/Tencent/tinker
集成文档:http://www.tinkerpatch.com/Docs/SDK
- Android热更新——Tinker 使用
- Android 热更新Tinker简单使用
- Android 热更新之tinker
- Android热更新(Tinker)
- 使用Tinker与极光推送实现Android热更新
- Android热更新框架Tinker无法更新?
- Android热修复——Tinker
- 热更新Tinker研究(一):运行tinker-sample-android
- 站在巨人的肩上——Android热更新框架Tinker探索之旅
- Android 热更新 Bugly(Tinker)接入
- Android Tinker热更新Sample_demo接入
- 热更新Tinker的初次集成使用
- 热更新Tinker的集成使用
- 微信Tinker热更新详细使用
- Android热更新(3)-Bugly&Tinker 热更新实战!
- 集成tinker热更新
- Tinker热更新
- Android 热更新 tinker 在Android 简单的使用 (替换class)
- Hadoop二次排序
- mac系统 安装 JDK 并配置环境
- 单机MySQL多实例安装
- Sql Server 配置IP地址连接
- Eclipse更新SVN版本
- Android热更新——Tinker 使用
- 文章标题
- 只是觉得有点儿意思 值得琢磨下
- python 中文乱码
- C# DirectInput游戏手柄和键盘开发心得2(SharpDX版)
- 简单网络爬虫实现<爬取网页邮箱>
- mysql命令行查询中文乱码问题
- Linux ftp传送问题 WARNING! 258831 bare linefeeds received in ASCII mode
- Overlay网络与物理网络的关系