前言
最近公司新上一个项目,天使用户试用阶段每天各种各样的需求要让改。改一次发一个版本改一次发一个版本简直苦不堪言,大多数时候只是界面上的调整改动,交易结果的排版这些问题。毕竟太高频次又没有多大实质变化的更新让人很反感,因此得想一些改进办法。于是决定接入热更新技术,小问题采用发补丁包的方式。定期发布版本对改动进行整体迭代。Android热更新技术已经有很多框架可以直接使用了,至于怎么选择见仁见智,选择自己项目适合的方式。本文主要记录一下我自己项目接入微信的Tinker热修复框架的过程。
使用之前一定仔细阅读Wiki文档和sample中的代码!!!
Tinker Github地址
快速接入过程
一、引入依赖
参考Tinker-接入指南
项目的build.gradle中添加
repositories {
//mavenLocal要特别注意别漏了,否则更改资源文件会出空指针异常
mavenLocal()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.1'
//集成Tinker,热修复
classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.7.0')
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
allprojects {
repositories {
mavenLocal()
jcenter()
}
}
APP的build.gradle中添加
dependencies {
//可选,用于生成application类,(版本号换成最新的发布版)
compile('com.tencent.tinker:tinker-android-anno:1.7.0')
//tinker的核心库
compile('com.tencent.tinker:tinker-android-lib:1.7.0')
}
...
...
//apply tinker插件
apply plugin: 'com.tencent.tinker.patch'
}
二、其他配置
比较快速的接入方式是直接对比自己app的build.gradle 文件和sample项目的build.gradle文件将自己的配置和tinker需要的配置合并。修改defaultConfig中的applicationId为自己的包名,注意signConfig中的签名文件路径(Gradle打包签名用的)。修改tinkerPatch中loader数组中的Application为自己的Application。
build.gradle中各个配置的解释wiki文档中都有说明
这里我说一说我在接入时遇到的几个问题(文档里面也都有说明,但不仔细看文档又一定会遇到的问题)
- 1.tinkerId的问题
如果项目没有初始化git并commit,官方sample如果不是采用git clone的方式获取也会有这个问题。因为sample配置中
tinkerId = getTinkerIdValue()
...
...
def getTinkerIdValue() {
return hasProperty("TINKER_ID") ? TINKER_ID : gitSha()
}
是通过git的版本sha来设置tinkerId的。
解决办法:①直接设置一个任意的字符串②初始化git并提交一次。为了避免补丁混乱,每个基础包发布版本(不是每一次补丁包发布)都对应一个唯一的tinkerId,打补丁时补丁和基础包根据tinkerId关联。可采用发布版的git版本号或者versionName来作为tinkerId。我自己是试用的版本号来作为tinkerId的这样一个版本对应的是一个id
//修改tinkerid的值
def getTinkerIdValue() {
return android.defaultConfig.versionName
}
- 2.keep_in_main_dex.txt 找不到的问题
/**
* 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")
sample中copy过来的配置中有上面这么一条,在sample中的对应目也有这个文件。里面是一些keep规则,复制txt文件到自己项目对应目录即可
- 3.需要注意的是tinker插件需要读写文件的权限,Android6.0之下mianfest中配置即可,6.0之后还需Java代码中申请权限。
- 4.bapApk目录下不能生成mapping文件的问题
a.
/**
* task type, you want to bak
* taskName与编译选择的方式要一致
*/
def taskName = "release"
b.直接使用AndroidStudio菜单栏build打包也会出现这种问题,这里使用gradle命令打包的方式。
点击AndroidStudio的右边栏打开Gradle命令菜单展开build接点如图,根据配置的taskName选择debug方式还是release方式打包,release方式即为要发布的打包版本。如需打包签名的apk则需要配置app的gradle.build中的签名文件信息
/**
* Gradle打包签名配置
*/
signingConfigs {
release {
storeFile file('E:/PandaQ/tinkerDemo/test.jks')
keyAlias 'tinker测试'
keyPassword 'tinkertest'
storePassword 'tinkertest'
}
debug {
storeFile file('C:/Users/PandaQ/.android/debug.keystore')
}
}
配置后运行gradle打包命令就会按上面的签名文件对应用进行打包,但直接将签名文件密码放在配置中不安全,可以设置在打包过程中输入。具体方式可以看这里
- 5.clean或者rebuild工程后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
//you should bak the following files
//old apk file to build patch apk
//app-debug-1017-11-29-32.apk
tinkerOldApkPath = "${bakPath}/app-release-1024-16-46-49.apk"
//proguard mapping file to build patch apk
tinkerApplyMappingPath = "${bakPath}/app-release-1024-16-46-49-mapping.txt"
//resource R.txt to build patch apk, must input if there is resource changed
tinkerApplyResourcePath = "${bakPath}/app-release-1024-16-46-49-R.txt"
}
默认的输出目录为bakApk目录,但是每次clean或者rebuild工程时这个文件夹都会被清理,因此需要手动将发布出去的基础包和对应的mapping.txt及R.txt备份
。或者修改Gradle配置中的输出目录。
三、Application类
参考Tinker-自定义扩展
Tinker的接入文档中已经对Application类做了说明,为了使真正的Application能被修改将Application中的所有逻辑都移动到ApplicationLike代理类中来。Application继承TinkerApplication并只做一个super的空实现,或者通过在ApplicationLike代理中添加注解直接动态生成Application类,防止不小心在里面写了其他代码。
@DefaultLifeCycle(
application = "com.seuic.seuickp.KPApplication", //application name to generate
flags = ShareConstants.TINKER_ENABLE_ALL)
public class KPApplicationLike extends DefaultApplicationLike {
public static Context mContext;
public static ImageLoader mImageLoader;
public static DisplayImageOptions sOptions;
public KPApplicationLike(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);
MultiDex.install(base);
TinkerManager.setTinkerApplicationLike(this);
TinkerManager.installTinker(this);
}
public static ImageLoader getmImageLoader() {
return mImageLoader;
}
public static DisplayImageOptions getOptions() {
return sOptions;
}
@Override
public void onCreate() {
super.onCreate();
mContext = getApplication().getApplicationContext();
DBTools.init(mContext);
mImageLoader = ImageLoader.getInstance();
ImageLoaderConfiguration config = new ImageLoaderConfiguration
.Builder(mContext)
.threadPoolSize(4)
.imageDownloader(new AuthImageDownloader(getApplication().getApplicationContext()))
.build();
mImageLoader.init(config);
sOptions = new DisplayImageOptions
.Builder()
.showImageForEmptyUri(R.drawable.loading_pic)
.showImageOnFail(R.drawable.loading_pic)
.showImageOnLoading(R.drawable.loading_pic)
.cacheInMemory(true)
.cacheOnDisk(true)
// .displayer(new RoundedBitmapDisplayer(10))
.build();
//初始化腾讯Bugly
CrashReport.initCrashReport(getApplication().getApplicationContext(), "900053571", false);
}
public static Context getContext() {
return mContext;
}
public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {
getApplication().registerActivityLifecycleCallbacks(callback);
}
}
上面是我接入tinker的一个小项目的Application,将Application的oncreate()
方法直接移入ApplicationLike中,在onBaseContextAttached()
方法中对tinker进行初始化操作。然后将所有关于Application的引用换成ApplicationLike.getApplication()
即可
完成上述操作,配置部分就已经基本完成,接下来就可以测试一下打包更新了。
打包测试
根据配置文件在Gradle的build中选择合适的打包方式(上面图中有标注),打包后未修改默认输出路径的情况下会在工程app->build目录下生产bakAPK文件夹和outputs文件夹。
- a 备份bakAPK中的三个文件,很重要!生成补丁包的时候需要的
- b outputs-->abp-->app-release.apk为需要发布的版本
- c 打包发布版补丁:将bakApk中的文件配置到build.gradle中
/**
* 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
//you should bak the following files
//old apk file to build patch apk
//app-debug-1017-11-29-32.apk
tinkerOldApkPath = "${bakPath}/app-debug-1024-20-00-21.apk"
//proguard mapping file to build patch apk
tinkerApplyMappingPath = "${bakPath}/app-release-1024-20-02-47-mapping.txt"
//resource R.txt to build patch apk, must input if there is resource changed
tinkerApplyResourcePath = "${bakPath}/app-debug-1024-20-00-21-R.txt"
}
只修改了方法的情况下只需配置OldApkPath即可,修改了资源文件时还需要配置mapping.txt和R.txt
- d 选择Gradle-->tinker-->tinkerPatchRelease/Debug打包生成补丁,打包后会在outputs中生成tinkerPatch文件夹,将生成的patch_signed.apk或者patch_signed_7zip.apk(选择小的)推送到手机的文件系统中(文件名可以不以apk结尾)。
- e 在服务中或者通过事件调用加载补丁
loadPatchButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), Environment.getExternalStorageDirectory().getAbsolutePath() + "/seuic/kp/patch_signed_7zip.apk");
}
});
默认情况下初始化tinker有两种方式查看TinkerInstaller
类的install()
方法可以发现未指定ResultService时使用DefaultResultService,加载成功后会自动结束进程应用直接关闭。显然不太友好,因此我们可以继承DefaultResultService然后重写他的onPatchResult()
方法,来达到在加载完成补丁后进行的操作(弹个对话框让用户重启,因为补丁加载后需进程重启才有效果)。
- f 一个基础包可以打多次补丁,打不定时配置中的oldApk不变始终为基础包,不能使用前面的补丁包作为后面补丁的oldApk。
上面就是接入tinker热更新的全部步骤。需要更多的场景或者接入按上面方式不适用可以去研究一下tinker的源码和仔细看一看github上的wiki文档跟常见问题一般都能解决
Tinker仓库地址