文章开始之前先说明一下现在的Tinker主要适用于修复Bug,而不是app版本的更新升级。
随着现在热补丁越来越热,市面上的热补丁方案有很多,下面是几个比较热门框架的对比
Tinker | QZone | AndFix | Robust | |
---|---|---|---|---|
类替换 | yes | yes | no | no |
So替换 | yes | no | no | no |
资源替换 | yes | yes | no | no |
全平台支持 | yes | yes | yes | yes |
即时生效 | no | no | yes | yes |
性能损耗 | 较小 | 较大 | 较小 | 较小 |
补丁包大小 | 较小 | 较大 | 一般 | 一般 |
开发透明 | yes | yes | no | no |
复杂度 | 较低 | 较低 | 复杂 | 复杂 |
gradle支持 | yes | no | no | no |
Rom体积 | 较大 | 较小 | 较小 | 较小 |
成功率 | 较高 | 较高 | 一般 | 最高 |
Tinker的已知问题
由于原理与系统限制,Tinker有以下已知问题:
- Tinker不支持修改AndroidManifest.xml,Tinker不支持新增四大组件;
- 由于Google Play的开发者条款限制,不建议在GP渠道动态更新代码;
- 在Android N上,补丁对应用启动时间有轻微的影响;
- 不支持部分三星android-21机型,加载补丁时会主动抛出
"TinkerRuntimeException:checkDexInstall failed"
; - 由于各个厂商的加固实现并不一致,在1.7.6以及之后的版本,tinker不再支持加固的动态更新;
- 对于资源替换,不支持修改remoteView。例如transition动画,notification icon以及桌面图标。
截止到我写这篇文章的时候Tinker还是存在以上的问题还没有解决,如你所见如果你新增了功能页面(Activity),而Tinker不支持AndroidManifest.xml的修改和新增四大组件,如果只是修改Bug和一些功能上的修改还是可以的。
https://github.com/Tencent/tinker/wiki
上面这个链接的文档写的很好,关于接入和常见问题等等都有涉及,可是对于小白来说不熟悉Android文件结构还有Gradle文件和命令还是会有点麻烦的。废话少说下面我们开始集成。
一、导入Tinker 文件
1、在Project的gradle文件中的dependencies中加入引用classpath "com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}":
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.2'
classpath "com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
2.在gradle.properties中加入Tinler的版本变量现在1.7.7
TINKER_VERSION=1.7.7
3.在Moudlue:app的gradle文件中添加Demo中的的方法插件,这里要加的东西比较多,我是把自己这个文件整个先拷出来然后把对应位置的东西改为自己的。
主要是项目的依赖、defaultConfig、还有签名配置改为自己:
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile "com.android.support:appcompat-v7:23.1.1"
//tinker的核心库
compile("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
//可选,用于生成application类
provided("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
compile "com.android.support:multidex:1.0.1"
}
defaultConfig {
applicationId "com.example.myapplication"
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
signingConfigs {
release {
/* try {*/
storeFile file("./keystore/release.keystore")
storePassword "testres"
keyAlias "testres"
keyPassword "testres"
/* } catch (ex) {
throw new InvalidUserDataException(ex.toString())
}*/
}
debug {
storeFile file("./keystore/debug.keystore")
}
}
4.接着把整个包的类和相应的资源文件拷到自己项目中
有红线报错,是因为对应import的R文件和一些资源文件缺少,把它一个个改过来就好。
5.配置清单文件AndroidManifest.xml
6.修改Tinker中的SampleApplicationLike类里面生成Application的位置
@SuppressWarnings("unused")
@DefaultLifeCycle(application = "com.example.myapplication.SampleApplication",
flags = ShareConstants.TINKER_ENABLE_ALL,
loadVerifyFlag = false)
https://github.com/Tencent/tinker/wiki/Tinker-%E8%87%AA%E5%AE%9A%E4%B9%89%E6%89%A9%E5%B1%95
7.在MainActivity中设置好对应的点击事件:
package com.example.myapplication;
import android.content.Context;
import android.graphics.Typeface;
import android.os.Bundle;
import android.os.Environment;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import com.tencent.tinker.lib.tinker.Tinker;
import com.tencent.tinker.lib.tinker.TinkerInstaller;
import com.tencent.tinker.loader.shareutil.ShareConstants;
import com.tencent.tinker.loader.shareutil.ShareTinkerInternals;
import tinker.sample.android.app.BaseBuildInfo;
import tinker.sample.android.app.BuildInfo;
import tinker.sample.android.util.Utils;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button loadPatchButton = (Button) findViewById(R.id.loadPatch);
loadPatchButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed_7zip.apk");
}
});
/*Button loadLibraryButton = (Button) findViewById(R.id.loadLibrary);
loadLibraryButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(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(new View.OnClickListener() {
@Override
public void onClick(View v) {
Tinker.with(getApplicationContext()).cleanPatch();
}
});
Button killSelfButton = (Button) findViewById(R.id.killSelf);
killSelfButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ShareTinkerInternals.killAllOtherProcess(getApplicationContext());
android.os.Process.killProcess(android.os.Process.myPid());
}
});
Button buildInfoButton = (Button) findViewById(R.id.showInfo);
buildInfoButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showInfo(MainActivity.this);
}
});
}
public boolean showInfo(Context context) {
// add more Build Info
final StringBuilder sb = new StringBuilder();
Tinker tinker = Tinker.with(getApplicationContext());
if (tinker.isTinkerLoaded()) {
sb.append(String.format("[patch is loaded] \n"));
sb.append(String.format("[buildConfig TINKER_ID] %s \n", BuildInfo.TINKER_ID));
sb.append(String.format("[buildConfig BASE_TINKER_ID] %s \n", BaseBuildInfo.BASE_TINKER_ID));
sb.append(String.format("[buildConfig MESSSAGE] %s \n", BuildInfo.MESSAGE));
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("[buildConfig TINKER_ID] %s \n", BuildInfo.TINKER_ID));
sb.append(String.format("[buildConfig BASE_TINKER_ID] %s \n", BaseBuildInfo.BASE_TINKER_ID));
sb.append(String.format("[buildConfig MESSSAGE] %s \n", BuildInfo.MESSAGE));
sb.append(String.format("[TINKER_ID] %s \n", ShareTinkerInternals.getManifestTinkerID(getApplicationContext())));
}
sb.append(String.format("[BaseBuildInfo Message] %s \n", BaseBuildInfo.TEST_MESSAGE));
final TextView v = new TextView(context);
v.setText(sb);
v.setGravity(Gravity.LEFT | Gravity.CENTER_VERTICAL);
v.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 10);
v.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
v.setTextColor(0xFF000000);
v.setTypeface(Typeface.MONOSPACE);
final int padding = 16;
v.setPadding(padding, padding, padding, padding);
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setCancelable(true);
builder.setView(v);
final AlertDialog alert = builder.create();
alert.show();
return true;
}
@Override
protected void onResume() {
Log.e(TAG, "i am on onResume");
// Log.e(TAG, "i am on patch onResume");
super.onResume();
Utils.setBackground(false);
}
@Override
protected void onPause() {
super.onPause();
Utils.setBackground(true);
}
}
二、测试运行
点击底部的Terminal,执行命令gradlew.bat assembleDebug,接着一段时间会下载一堆文件,jar包等。可是最后却失败了
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':app:tinkerProcessDebugManifest'.
> tinkerId is not set!!!
没有设置tinkerId!!!这是很多人第一次集成的时候都会遇到的问题,一般在配置app:moudle文件的时候就会遇到,我们来看看gradle文件中他是通过gitSha()来获取
def gitSha() {
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'")
}
}
String gitRev = 'git rev-parse --short HEAD'.execute(null, project.rootDir).text.trim() 这行代码只是获取当前git中head的版本号,一个字符串,因为我公司用的的SVN,你想这个方法有用就必须下载Git,在Android Studio配置好git.exe,还有写好你在github中的账号,密码等等....最后成功的时候会出现这个
好的既然知道它只是获取一个版本号的字符串,那么我就直接写死的
def gitSha() {
String gitRev = "1.7.7"
return gitRev
}
每次打补丁的时候记得改一下就好了。那么再执行一次 gradlew.bat assembleDebug 命令
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':app:validateDebugSigning'.
> Keystore file C:\Users\admin\Desktop\MyApplication2\app\keystore\debug.keystore not found for signing config 'debug'.
意思就是签名没有用不了,好吧自己配置一下签名文件,不会的可以参考一下
http://www.cnblogs.com/gao-chun/p/4891275.html
成功后生成对应的文件
在Module:app的gradle文件中修改成我们刚刚生成的apk名字
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}/app-debug-0121-18-11-33.apk"
//proguard mapping file to build patch apk
tinkerApplyMappingPath = "${bakPath}/app-debug-0121-18-11-33-mapping.txt"
//resource R.txt to build patch apk, must input if there is resource changed
tinkerApplyResourcePath = "${bakPath}/app-debug-0121-18-11-33.txt"
//only use for build all flavor, if not, just ignore this field
tinkerBuildFlavorDirectory = "${bakPath}/app-debug-0121-18-11-33"
}
然后执行 gradlew.bat tinkerPatchDebug 命令或者MainActivity右边的Gradle里面直接双击tinkerPathDebug
这个patch_signed_7zip.apk就是我们的差分包。因为Demo中差分包是放在根目录的,我们手动把这个包复制到手机根目录
Button loadPatchButton = (Button) findViewById(R.id.loadPatch);
loadPatchButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed_7zip.apk");
}
});
安装成功后这个差分包会自动删除。
运行效果:
如果你觉得复杂可以使用TinkerPatch平台,可实现一键接入:
平台地址:http://www.tinkerpatch.com