1.热修复:
热修复从原理上说应该是属于插件化的一类,我们可以用热修复来处理线上紧急的bug,而不需要提示用户重新发版
这里对比下常见的热修复优缺点:
tinker对比图2.插件化:
插件化中通过DexClassLoader来加载类,将各个子bundle载入到宿主apk中,这个过程在第一次启动的时候会耗费一定时间,相关博客请参考http://www.jianshu.com/p/43a8a9b932de,这里不做详细描述.
3.增量更新:
增量更新的原理就是通过比较新apk和旧的apk,通过工具可以生成拆分包patch,然后客户端需要处理的就是将old.apk与patch合并成新的apk,最后进行安装.但是这里合并完成后会进行对比,两个apk的md5或者sha1是否一致.参见鸿洋的博客
下面我们就来具体实现下腾讯tinker的集成与使用(也可以参考官方wiki):
步骤1:
在工程外部添加对tinker的支持:
dependencies {
classpath'com.android.tools.build:gradle:2.3.3'
classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.8.0')
}
在工程里面添加需要的jar:
//apply tinker插件
applyplugin:'com.tencent.tinker.patch'
//可选,用于生成application类
provided('com.tencent.tinker:tinker-android-anno:1.8.0')
//tinker的核心库
compile('com.tencent.tinker:tinker-android-lib:1.8.0')
然后配置gradle中新增tinker组在工具栏选项,配置编译输出路径和基本参数:
defaultConfig中添加
/**
* 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","TINKER_ID","\"${getTinkerIdValue()}\""
buildConfigField"String","PLATFORM","\"all\""
其他需要添加的直接贴出来:
defgitSha() {
try{
String gitRev ='git rev-parse --short HEAD'.execute(null, project.rootDir).text.trim()
if(gitRev ==null) {
throw newGradleException("can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'")
}
returngitRev
}catch(Exception e) {
throw newGradleException("can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'")
}
}
defbakPath = 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
//old apk file to build patch apk
tinkerOldApkPath ="${bakPath}/Codes-old.apk"
//proguard mapping file to build patch apk
tinkerApplyMappingPath ="${bakPath}/Codes-debug-mapping.txt"
//resource R.txt to build patch apk, must input if there is resource changed
tinkerApplyResourcePath ="${bakPath}/Codes-debug-R.txt"
//only use for build all flavor, if not, just ignore this field
tinkerBuildFlavorDirectory ="${bakPath}/Codes-debug"
}
defgetOldApkPath() {
returnhasProperty("OLD_APK") ? OLD_APK : ext.tinkerOldApkPath
}
defgetApplyMappingPath() {
returnhasProperty("APPLY_MAPPING") ? APPLY_MAPPING : ext.tinkerApplyMappingPath
}
defgetApplyResourceMappingPath() {
returnhasProperty("APPLY_RESOURCE") ? APPLY_RESOURCE : ext.tinkerApplyResourcePath
}
defgetTinkerIdValue() {
returnhasProperty("TINKER_ID") ? TINKER_ID : gitSha()
}
defbuildWithTinker() {
returnhasProperty("TINKER_ENABLE") ? TINKER_ENABLE : ext.tinkerEnabled
}
defgetTinkerBuildFlavorDirectory() {
returnext.tinkerBuildFlavorDirectory
}
if(buildWithTinker()) {
applyplugin:'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
/**
* optional,default 'true'
* whether use tinker to build
*/
tinkerEnable = buildWithTinker()
/**
* 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()
tinkerId ="tinkerId"
/**
* if keepDexApply is true, class in which dex refer to the old apk.
* open this can reduce the dex diff file size.
*/
keepDexApply =false
/**
* optional, default 'false'
* Whether tinker should treat the base apk as the one being protected by app
* protection tools.
* If this attribute is true, the generated patch package will contain a
* dex including all changed classes instead of any dexdiff patch-info files.
*/
isProtectedApp =false
}
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"
/**
* 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 {@codetinker.sample.android.SampleApplication}
* own tinkerLoader, and the classes you use in them
*
*/
loader = [
//use sample, let BaseBuildInfo unchangeable with tinker
"com.zte.rs.RSApplication"
]
}
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/*/*.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 flavors =newArrayList<>();
project.android.productFlavors.each { flavor ->
flavors.add(flavor.name)
}
booleanhasFlavors = flavors.size() >0
defdate =newDate().format("MMdd-HH-mm-ss")
/**
* bak apk and mapping
*/
android.applicationVariants.all { variant ->
/**
* task type, you want to bak
*/
deftaskName = variant.name
tasks.all {
if("assemble${taskName.capitalize()}".equalsIgnoreCase(it.name)) {
it.doLast {
copy {
deffileNamePrefix ="${project.name}-${variant.baseName}"
defnewFileNamePrefix = hasFlavors ?"${fileNamePrefix}":"${fileNamePrefix}-${date}"
defdestPath = 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'
deforiginOldPath = getTinkerBuildFlavorDirectory()
for(String flavor : flavors) {
deftinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Release")
dependsOn tinkerTask
defpreAssembleTask = 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'
deforiginOldPath = getTinkerBuildFlavorDirectory()
for(String flavor : flavors) {
deftinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Debug")
dependsOn tinkerTask
defpreAssembleTask = 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:分析生成的各个文件
以debug为例,首先我们先运行assembleDebug,可以看到build下生成了bakApk文件夹,这里将outputs中apk的赋值到bakApk下取名Codes-old.apk,跟gradle中配置的文件名保存一致,然后我们修改bug后点击TinkerPatchDebug,可以看到会生成timerPatch文件夹,这里面详细的记录了编译中所对比的文件和加载中混淆的说明等等,这里我们只需要选着patch_signed_7zip.apk就可以,为了模拟运行,我们将拆分apk复制到手机存储,然后在程序的入口出载入patch,载入成功后程序会默认退出,然后下次进入看看bug是否被修护.
对应每个文件的说明3.自定义application
目的:热修复中为了能够让application更新
首先我们可以新建一个ApplicationLike类继承DefaultApplicationLike,
@DefaultLifeCycle(
application ="com.xx.xxx.Application",//你自己的包名路径
flags = ShareConstants.TINKER_ENABLE_ALL)
public class ApplicationLike extends DefaultApplicationLike
{
public ApplicationLike (Application application,inttinkerFlags,booleantinkerLoadVerifyFlag,longapplicationStartElapsedTime,longapplicationStartMillisTime, Intent tinkerResultIntent)
{
super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public voidregisterActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback)
{
getApplication().registerActivityLifecycleCallbacks(callback);
}
@Override
public voidonBaseContextAttached(Context base)
{
MultiDex.install(base);
TinkerInstaller.install(this);
super.onBaseContextAttached(base);
}
然后把原有的application中的代码复制到自定义的application中,这样我们就可以在更新时候改动自定义的application了,原来的application
原有的application放在com.tentcent.tinker.loader.app.TinkerApplication包下的是被保护不能被修改的.在添加完成后需要手动删除自己原有的application类.
我们在来模拟测试下代码:
/*
* Tencent is pleased to support the open source community by making Tinker available.
*
* Copyright (C) 2016 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
packagecom.zte.rs.test;
importandroid.app.AlertDialog;
importandroid.content.Context;
importandroid.graphics.Typeface;
importandroid.os.Bundle;
importandroid.os.Environment;
importandroid.support.v7.app.AppCompatActivity;
importandroid.util.Log;
importandroid.util.TypedValue;
importandroid.view.Gravity;
importandroid.view.View;
importandroid.view.ViewGroup;
importandroid.widget.Button;
importandroid.widget.TextView;
importcom.tencent.tinker.lib.library.TinkerLoadLibrary;
importcom.tencent.tinker.lib.tinker.Tinker;
importcom.tencent.tinker.lib.tinker.TinkerInstaller;
importcom.tencent.tinker.loader.shareutil.ShareConstants;
importcom.tencent.tinker.loader.shareutil.ShareTinkerInternals;
importcom.zte.rs.R;
importcom.zte.rs.util.ToastUtils;
public classTestActivityextendsAppCompatActivity
{
private static finalStringTAG="Tinker.TestActivity";
@Override
protected voidonCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.test_main);
Log.e(TAG,"i am on onCreate classloader:"+ TestActivity.class.getClassLoader().toString());
//test resource change
Log.e(TAG,"i am on onCreate string:"+"I am in the base apk");
Log.e(TAG,"i am on patch onCreate");
Button loadPatchButton = (Button) findViewById(R.id.loadPatch);
loadPatchButton.setOnClickListener(newView.OnClickListener()
{
@Override
public voidonClick(View v)
{
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), Environment.getExternalStorageDirectory().getAbsolutePath() +"/patch_signed_7zip.apk");
}
});
Button error = (Button) findViewById(R.id.btn_error);
error.setOnClickListener(newView.OnClickListener()
{
@Override
public voidonClick(View v)
{
// String a = null;
// if(a.length() > 1)
// {
// ToastUtils.show(TestActivity.this, "永远不会弹出");
// }
String a ="222";
if(a.length() >1)
{
ToastUtils.show(TestActivity.this,"bug修改了!这是第三次");
}
}
});
Button loadLibraryButton = (Button) findViewById(R.id.loadLibrary);
loadLibraryButton.setOnClickListener(newView.OnClickListener()
{
@Override
public voidonClick(View v)
{
// #method 1, hack classloader library path
TinkerLoadLibrary.installNavitveLibraryABI(getApplicationContext(),"armeabi");
System.loadLibrary("stlport_shared");
// #method 2, for lib/armeabi, just use TinkerInstaller.loadLibrary
// TinkerLoadLibrary.loadArmLibrary(getApplicationContext(), "stlport_shared");
// #method 3, load tinker patch library directly
// TinkerInstaller.loadLibraryFromTinker(getApplicationContext(), "assets/x86", "stlport_shared");
}
});
Button cleanPatchButton = (Button) findViewById(R.id.cleanPatch);
cleanPatchButton.setOnClickListener(newView.OnClickListener()
{
@Override
public voidonClick(View v)
{
Tinker.with(getApplicationContext()).cleanPatch();
}
});
Button killSelfButton = (Button) findViewById(R.id.killSelf);
killSelfButton.setOnClickListener(newView.OnClickListener()
{
@Override
public voidonClick(View v)
{
ShareTinkerInternals.killAllOtherProcess(getApplicationContext());
android.os.Process.killProcess(android.os.Process.myPid());
}
});
Button buildInfoButton = (Button) findViewById(R.id.showInfo);
buildInfoButton.setOnClickListener(newView.OnClickListener()
{
@Override
public voidonClick(View v)
{
showInfo(TestActivity.this);
}
});
}
public booleanshowInfo(Context context)
{
// add more Build Info
finalStringBuilder sb =newStringBuilder();
Tinker tinker = Tinker.with(getApplicationContext());
if(tinker.isTinkerLoaded())
{
sb.append(String.format("[patch is loaded]\n"));
sb.append(String.format("[TINKER_ID] %s\n", tinker.getTinkerLoadResultIfPresent().getPackageConfigByName(ShareConstants.TINKER_ID)));
sb.append(String.format("[packageConfig patchMessage] %s\n", tinker.getTinkerLoadResultIfPresent().getPackageConfigByName("patchMessage")));
sb.append(String.format("[TINKER_ID Rom Space] %d k\n", tinker.getTinkerRomSpace()));
}
else
{
sb.append(String.format("[patch is not loaded]\n"));
sb.append(String.format("[TINKER_ID] %s\n", ShareTinkerInternals.getManifestTinkerID(getApplicationContext())));
}
finalTextView v =newTextView(context);
v.setText(sb);
v.setGravity(Gravity.LEFT| Gravity.CENTER_VERTICAL);
v.setTextSize(TypedValue.COMPLEX_UNIT_DIP,10);
v.setLayoutParams(newViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
v.setTextColor(0xFF000000);
v.setTypeface(Typeface.MONOSPACE);
final intpadding =16;
v.setPadding(padding, padding, padding, padding);
finalAlertDialog.Builder builder =newAlertDialog.Builder(context);
builder.setCancelable(true);
builder.setView(v);
finalAlertDialog alert = builder.create();
alert.show();
return true;
}
@Override
protected voidonResume()
{
Log.e(TAG,"i am on onResume");
// Log.e(TAG, "i am on patch onResume");
super.onResume();
Utils.setBackground(false);
}
@Override
protected voidonPause()
{
super.onPause();
Utils.setBackground(true);
}
}
加载patch:
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), Environment.getExternalStorageDirectory().getAbsolutePath() +"/patch_signed_7zip.apk");
判断是否加载完毕:
tinker.isTinkerLoaded()
运行效果如下:
首先在程序中模拟一个异常:
异常这里点击异常会奔溃,然后我们修改代码,点击loadpatch,载入成功后再进入程序,点击异常,如图:
修改后到这里就全部结束了,这里需要注意几点:
基准版本不改变
改变基准版本时候改变tinkerid的值
自定义的application可以更新,但是manifest.xml是不能通过热修复更新的
debug的差分apk大
release的查分apk小
运行时候需要取消runtimeinstall选项, settings-->Instant Run ---Enalbe instant Runto hot取消打勾
先build再tinker!
网友评论