微信Tinker热更新方案
一、微信Tinker热更新简介
1、github地址: https://github.com/Tencent/tinker
2、Tinker原理: http://mp.weixin.qq.com/s?__biz=MzAwNDY1ODY2OQ==&mid=2649286306&idx=1&sn=d6b2865e033a99de60b2d4314c6e0a25#rd
3、Tinker是微信官方的Android热补丁解决方案,它支持动态下发代码、So库以及资源,让应用能够在不需要重新安装的情况下实现更新。当然,你也可以使用Tinker来更新你的插件。
二、导入官方Sample
1、github下载下来sample,然后解压打开导入Android Studio,我们只需要把tinker-sample-android这个目录导入即可.
2、导入之后,构建一下,发现提示“tinkerId is not set!!!”,这时候我们在app/bulid.gradle中,设置tinkerId的值.
这里可以直接就把当前的版本号作为tinkerId
3、编辑运行原版apk
打开Android studio 右上角Gradle,双击运行assembleDebug命令
拿到下图中的app-debug-xxxxx.apk装在手机上运行
或者直接运行(不过要先关闭Instant Run) ->file->setting->Build.E….->Instant Run 第一个去掉就可以运行了
4、配置原版apk路径
ext {
//for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
tinkerEnabled = true
//for normal build
//旧版本apk路径配置
tinkerOldApkPath = "${bakPath}/app-debug-1111-14-05-21.apk"
//用于混淆,没有混淆可以不管
tinkerApplyMappingPath = "${bakPath}/app-debug-1111-14-05-21-mapping.txt"
//旧版本apk R文件
tinkerApplyResourcePath = "${bakPath}/app-debug-1111-14-05-21-R.txt"
//only use for build all flavor, if not, just ignore this field
tinkerBuildFlavorDirectory = "${bakPath}/app-debug-1111-14-05-21"
}
5、修改源码(修改R文件和Res文件),制作差分包.
修改完后,双击运行tinkerPatchDebug命令
运行完后(如果运行时老卡住,可重启Android studio再试),在app/build/outputs/tinkerPatch/debug/patch_signed_7zip.apk路径下找到这个差异包,也就是我们俗称的补丁.
6、运行应用,加载补丁
把patch_signed_7zip.apk放到手机SD卡跟目录中,或者使用adb命令
adb push ./app/build/outputs/tinkerPatch/debug/patch_signed_7zip.apk /storage/sdcard0/
再次运行apk,点击LoadPatch,这时候界面提示“patch success, please restart process”表示加载补丁成功,最后在后台App管理杀掉该App重新启动应用
补充:返回键退出后进入,并没有执行修复。杀进程后再进入 ,应该就可以修复成功了,如果不成功,把补丁包逆向一下,看看自己修复的部分有没有在里面。
三、集成到项目中(这里以河南快处快赔警用版为例)
1、添加Gradle依赖
项目的build.gradle中,添加tinker-patch-gradle-plugin的依赖
dependencies {
classpath 'com.android.tools.build:gradle:2.2.0'
classpath "com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}"
}
项目的gradle.properties文件中加入Tinker版本号配置
TINKER_VERSION=1.7.3
然后在app的gradle文件app/build.gradle,需要添加tinker的库依赖以及apply tinker的gradle插件.
//tinker 核心lib库
compile("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
//可选,用于生成application类
provided("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
添加生成补丁方法以及配置,这里可以把官网配置照搬过来,有些可以去掉
def bakPath = file("${buildDir}/bakApk/")
ext {
tinkerEnabled = true
tinkerOldApkPath = "${bakPath}/app-debug-1111-15-04-57.apk"
tinkerApplyMappingPath = "${bakPath}/app-debug-1018-17-32-47-mapping.txt"
tinkerApplyResourcePath = "${bakPath}/app-debug-1111-15-04-57-R.txt"
tinkerBuildFlavorDirectory = "${bakPath}/app-debug-1108-10-27-36"
}
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 = true
useSign = true
buildConfig {
applyMapping = getApplyMappingPath()
applyResourceMapping = getApplyResourceMappingPath()
tinkerId = getTinkerIdValue()
}
dex {
dexMode = "jar"
usePreGeneratedPatchDex = false
pattern = ["classes*.dex",
"assets/secondary-dex-?.jar"]
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 {
pattern = ["lib/armeabi/*.so"]
}
res {
pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
ignoreChange = ["assets/sample_meta.txt"]
largeModSize = 100
}
packageConfig {
configField("patchMessage", "tinker is sample to use")
configField("platform", "all")
configField("patchVersion", "1.0")
}
sevenZip {
zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
}
}
List<String> flavors = 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 {
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"
}
}
}
}
}
}
2、配置Application
这里需要将Application继承TinkerApplication交给Tinker管理,Tinker提供了一个ApplicationLike供使用,为了避免少的改动,我们将项目中的Application 继承TinkerApplication(此时onCreate方法会报错,因为父类用了final修饰,不能覆盖),去掉onCreate方法,将onCreate方法中的初始化操作单独抽一个init方法()出来,我们找个合适的时机进行调用,例如MainActivity,这样我们其他引用Application的地方都不需要改动。
public void init() {
// 获取登录信息
String loginInfoStr = PrefUtils.getString(getApplicationContext(), "loginInfo", null);
if (null != null && !"".equals(loginInfoStr))
{
EntityBean loginInfo = (EntityBean) com.longrise.LEAP.Base.IO.JSONSerializer.getInstance()
.DeSerialize(loginInfoStr, EntityBean.class);
if (null != loginInfo)
{
EntityBean userInfo = loginInfo.getBean("userinfo");
this.token = userInfo.getString("token", null);
}
}
kckpName = "110000002000";
kckpPass = EncryptService.getInstance().MD5Encrypt("888888a");
// 程序的入口
// 初始化化一些.常用属性.然后放到盒子里面来
// 上下文
mContext = getApplicationContext();
// 主线程
mMainThread = Thread.currentThread();
// 主线程Id
mMainTreadId = android.os.Process.myTid();
// tid thread
// uid user
// pid process
// 主线程Looper对象
mMainLooper = getMainLooper();
// 定义一个handler
mHandler = new Handler();
super.onCreate();
//初始化imageloader
ImageLoader.getInstance().init(ImageLoaderConfiguration.createDefault(getContext()));
}
在程序启动的第一个Activity初始化(例如向导页)
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//调用application里的初始化方法
((BaseApplication)getApplicationContext()).init();
this.requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_guid);
initUI();
initData();
initListener();
}
另外拷贝Sample中ApplicationLike,修改名称,去掉自动生成Application的注解。
四、本地测试是否能进行热更新,测试步骤与上面官方例子一样.
五、调试服务端接口,测试热更新
服务端接口
1、请求接口参数:APP名称(APPName)、APP版本号(versionCode)、JS文件的版本号、系统(Android或者IOS)
2、返回参数:(这个接口的返回值需要加密返回)
{
data:
{
isUpdate: true, //是否需要更新
JSVersion:1, //JS文件的版本
url: "http://" //JS文件的下载地址
}
}
接口请求
LoadDataManagerCar.getInstance(context).callService(null,
((BaseApplication) context.getApplicationContext()).getServerUrl(), URLConstant.GETAPPUPDATEVERSION,
bean, new LoadDataManagerFather.OnRequestCompleteListener() {
@Override
public void onSuccess(String key, String service, Object result) {
if (null != processDialog) {
processDialog.cancel();
}
if (null != result) {
EntityBean bean = (EntityBean) result;
EntityBean data = bean.getBean("data");
String restate = bean.getString("restate");
String redes = bean.getString("redes");
if ("1".equals(restate)) {
try {
EnCryptDeCrypt encryptdecrypt = EnCryptDeCrypt.getInstance();
//sp获取本地版本号,对比是否需要下载安装包,安装热更新文件,重置sp的值
String encryAppVersion = encryptdecrypt.deCrypt(data.getString("appversion"));
String url = data.getString("url");
_apkurl_hot = encryptdecrypt.deCrypt(url);//获取下载apk的url
Integer appversion =Integer.parseInt(encryAppVersion) ;//后台获取的热更新版本号
//对比版本号,存进sp
int appSpVersion = PrefUtils.getInt(context, "appSpVersion", 1);//本地sp版本号
if(appversion>appSpVersion){
//下载热更新包安装
Thread thread = new Thread(null, _doDownLoadApk2, "downloadapkBackground2");
thread.start();
PrefUtils.setInt(context,"appSpVersion",appversion);
}
} catch (Exception e) {
e.printStackTrace();
}
} else {
Toast.makeText(context, redes, Toast.LENGTH_SHORT).show();
}
}
}
文件下载(差分包放入到/sdcard/Android/app package/cache目录)
if (200 == conn.getResponseCode()) {
this.isbreak = false;
InputStream is = conn.getInputStream();
String locDir = "hotPatch";
String updateApk = new Date().getTime() + ".apk";
File file = new File(context.getExternalCacheDir(), locDir);
if (null != file) {
if (!file.exists()) {
file.mkdir();
}
}
file = new File(context.getExternalCacheDir(), locDir + "/" + updateApk);
if (null != file) {
FileOutputStream fos = new FileOutputStream(file);
BufferedInputStream bis = new BufferedInputStream(is);
byte[] buffer = new byte[1024];
int len;
int total = 0;
while ((len = bis.read(buffer)) != -1) {
if (isbreak) {
file = null;
break;
}
fos.write(buffer, 0, len);
total += len;
/*if (null != pd) {
pd.setProgress(total);
}*/
}
fos.close();
bis.close();
is.close();
}
文件下载成功后,加载差分包
if(null!=_apkfile_hot){
String absolutePath = _apkfile_hot.getAbsolutePath();
TinkerInstaller.onReceiveUpgradePatch(context.getApplicationContext(), absolutePath);
}
在TinkerResultService中可以检测到差分包是否加载成功,从而作相应的处理