需要创建以下类
- TinkerManager(对Tinker进行组件化再次封装,减少对系统的侵入)
- CustomApplicationLike(委托(代理)模式:Tinker需要对Application的生命周期进行监听,通过ApplicationLike进行委托)
- CustomPatchListener(自定义PatchListener监听patch receiver事件)
- CustomResultService(自定义TinkerResultService改变patch安装后的行为)
- TinkerService(功能:应用程序Tinker更新服务: 1.检查补丁更新 2.从服务器下载patch文件 3.使用TinkerManager完成patch文件加载 4.patch文件会在下次进程启动时生效)
项目展示
TinkerManager
/**
* 作者:wujie on 2018/12/1 14:49
* 邮箱:705030268@qq.com
* 功能:对Tinker进行组件化再次封装,减少对系统的侵入
*/
public class TinkerManager {
private static boolean isInstalled = false;
private static ApplicationLike mApplicationLike;
private static CustomPatchListener mPatchListener;
/**
* 对Tinker的初始化
* @param applicationLike
*/
public static void installTinker(ApplicationLike applicationLike) {
mApplicationLike = applicationLike;
if(isInstalled) {
return;
}
mPatchListener = new CustomPatchListener(getApplicationContext());
LoadReporter loadReporter = new DefaultLoadReporter(getApplicationContext());
PatchReporter patchReporter = new DefaultPatchReporter(getApplicationContext());
AbstractPatch upgradePatchProcessor = new UpgradePatch();
// TinkerInstaller.install(mApplicationLike); //对Tinke初始化
TinkerInstaller.install(applicationLike,
loadReporter,
patchReporter,
mPatchListener,
CustomResultService.class,
upgradePatchProcessor); //完成Tinker初始化
isInstalled = true;
}
//完成Patch文件的加载
public static void loadPatch(String localPath) {
if(Tinker.isTinkerInstalled()) {
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), localPath);
}
}
//加个md5校验规则,防止补丁文件被篡改
//服务端返回原始文件的md5值,客户端下载patch文件以后,获取patch文件的MD5值与服务端返回的md5值比较是否相等
public static void loadPatch(String path, String md5Value) {
if (Tinker.isTinkerInstalled()) {
mPatchListener.setCurrentMD5(md5Value);
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), path);
}
}
private static Context getApplicationContext() {
if(null != mApplicationLike) {
return mApplicationLike.getApplication().getApplicationContext();
}
return null;
}
}
CustomApplicationLike
/**
* 作者:wujie on 2018/12/1 15:07
* 邮箱:705030268@qq.com
* 功能:委托(代理)模式
* Tinker需要对Application的生命周期进行监听,通过ApplicationLike进行委托,在ApplicationLike中对Tinker进行监听
*/
@DefaultLifeCycle(application = ".MyTinkerApplication", //通过注解生成Application
flags = ShareConstants.TINKER_ENABLE_ALL,
loadVerifyFlag = false)
public class CustomApplicationLike extends ApplicationLike {
public CustomApplicationLike(Application application, int tinkerFlags,
boolean tinkerLoadVerifyFlag,
long applicationStartElapsedTime,
long applicationStartMillisTime,
Intent tinkerResultIntent) {
super(application, tinkerFlags, tinkerLoadVerifyFlag,
applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
}
@Override
public void onBaseContextAttached(Context base) {
super.onBaseContextAttached(base);
//使应用支持分包
MultiDex.install(base);
TinkerManager.installTinker(this);
}
}
自定义PatchListener
/**
* @function: 自定义PatchListener监听patch receiver事件
* 1.较验patch文件是否合法(自定义规则) 2.启动Service去安装patch文件
*/
public class CustomPatchListener extends DefaultPatchListener {
private String currentMD5;
public void setCurrentMD5(String md5Value) {
this.currentMD5 = md5Value;
}
public CustomPatchListener(Context context) {
super(context);
}
@Override
protected int patchCheck(String path, String patchMd5) {
//patch文件md5较验
if (!Utils.isFileMD5Matched(path, currentMD5)) {
return ShareConstants.ERROR_PATCH_DISABLE;
}
return super.patchCheck(path, patchMd5);
}
}
自定义TinkerResultService
/**
* @function 自定义TinkerResultService改变patch安装后的行为
* 本类的作用:决定在patch安装完以后的后续操作,默认实现是杀进程
*/
public class CustomResultService extends DefaultTinkerResultService {
private static final String TAG = "Tinker.SampleResultService";
//返回patch文件的最终安装结果
@Override
public void onPatchResult(PatchResult result) {
if (result == null) {
TinkerLog.e(TAG, "DefaultTinkerResultService received null result!!!!");
return;
}
TinkerLog.i(TAG, "DefaultTinkerResultService received a result:%s ", result.toString());
//first, we want to kill the recover process
TinkerServiceInternals.killTinkerPatchServiceProcess(getApplicationContext());
// if success and newPatch, it is nice to delete the raw file, and restart at once
// only main process can load an upgrade patch!
if (result.isSuccess) {
deleteRawPatchFile(new File(result.rawPatchFilePath));
//默认加载patch以后,app闪退
// if (checkIfNeedKill(result)) {
// android.os.Process.killProcess(android.os.Process.myPid());
// } else {
// TinkerLog.i(TAG, "I have already install the newly patch version!");
// }
}
}
}
TinkerService
/**
* 作者:wujie on 2018/11/23 23:36
* 邮箱:705030268@qq.com
* 功能:应用程序Tinker更新服务:
* 1.检查补丁更新
* 2.从服务器下载patch文件
* 3.使用TinkerManager完成patch文件加载
* 4.patch文件会在下次进程启动时生效
*/
public class TinkerService extends Service {
private String mPatchFileDir;
private String mPatchFile;
private AppVersion mAppVersion;
private final int CHECK_PATCH_UPDATE = 0x01;
private final int DOWNLOAD_PATH = 0x02;
private final String PATH_DOT = ".apk"; //补丁文件后缀
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case CHECK_PATCH_UPDATE:
checkPatchUpdate();
break;
case DOWNLOAD_PATH:
downPatch();
break;
default:
break;
}
}
};
//对外提供启动servcie方法
public static void runTinkerService(Context context) {
try {
Intent intent = new Intent(context, TinkerService.class);
context.startService(intent);
} catch (Exception e) {
e.printStackTrace();
}
}
//用来与被启动者通信的接口
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
init(); //初始化Patch文件目录
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
mHandler.sendEmptyMessage(CHECK_PATCH_UPDATE); //检查是否有补丁
return START_NOT_STICKY; //START_NOT_STICKY表示service如果被系统回收不重启
}
private void checkPatchUpdate() {
ApiService.getInstance()
.getLatestVersion()
.subscribe(appVersion -> {
//如果当前不是最新版本,则不去下载补丁包
//原则上补丁包只是应付当前版本的紧急bug
if(StringUtils.isEmpty(appVersion.patchurl)) {
stopSelf();
return;
}
mAppVersion = appVersion;
mHandler.sendEmptyMessage(DOWNLOAD_PATH);
}, new ErrorAction() {
@Override
public void accept(Throwable throwable) throws Exception {
super.accept(throwable);
stopSelf();
}
});
}
private void downPatch() {
//保存的补丁文件命名规则:xxx/1.0.0.1.apk 1.0.0是正常版本 .1是补丁版本号
if(null != mAppVersion) {
mPatchFile = mPatchFileDir.concat(mAppVersion.patch_versionname).concat(PATH_DOT);
//判断该补丁是否存在,如果已经存在,不需要重复下载
// File apatchFile = new File(mPatchFileDir);
// if(apatchFile == null || !apatchFile.exists()) {
//下载补丁
Logger.d("正在下载");
FileDownloadService.getInstance(new IDownloadListener() {
@Override
public void onDownloadSuccess() {
ToastUtils.showShortSafe("文件下载成功");
TinkerManager.loadPatch(mPatchFile);
// TinkerManager.loadPatch(mPatchFile, md5);
}
@Override
public void onDownloadFail(Exception exception) {
ToastUtils.showShortSafe("文件下载失败");
}
@Override
public void onProgress(int progress) {
Logger.d("文件下载%d", progress);
}
}).download(mAppVersion.patchurl, mPatchFile);
// }
}
}
public void init() {
if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
//sd卡已挂载
mPatchFileDir = getExternalCacheDir().getAbsolutePath() + "/patch/";
File patchDir = new File(mPatchFileDir);
try {
if(null == patchDir || !patchDir.exists()) {
patchDir.mkdir();
}
} catch (Exception e) {
e.printStackTrace();
stopSelf();
}
} else {
ToastUtils.showShortSafe("SD卡未挂载");
stopSelf();
}
}
}
gradle生成热修复脚本
apply plugin: 'com.android.application'
apply plugin: 'me.tatarka.retrolambda' //retrolambda表达式
apply plugin: 'com.jakewharton.butterknife' //butterknife View注解
def javaVersion = JavaVersion.VERSION_1_8
def bakPath = file("${buildDir}/bakApk/")
android {
compileSdkVersion 25
buildToolsVersion "25.0.3"
defaultConfig {
applicationId "com.wj.tinkerutils"
minSdkVersion 18
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
manifestPlaceholders = [
UMENG_APPKEY_VALUE: "xxxx",
UMENG_CHANNEL_VALUE: "xxxx"
]
}
signingConfigs {
release {
try {
storeFile file("test.jks")
keyAlias 'xxx'
keyPassword 'xxx'
storePassword 'xxx'
} catch (ex) {
throw new InvalidUserDataException(ex.toString())
}
}
}
buildTypes {
release {
minifyEnabled true
signingConfig signingConfigs.release
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
configurations.all {
resolutionStrategy.force 'com.google.code.findbugs:jsr305:1.3.9'
}
compileOptions {
sourceCompatibility javaVersion
targetCompatibility javaVersion
}
//多渠道脚本支持
productFlavors {
googleplayer {
manifestPlaceholders = [UMENG_CHANNEL_VALUE: "googleplayer"]
}
baidu {
manifestPlaceholders = [UMENG_CHANNEL_VALUE: "baidu"]
}
productFlavors.all { flavor -> flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name]
}
}
}
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.+'
compile 'com.android.support.constraint:constraint-layout:1.0.2'
testCompile 'junit:junit:4.12'
compile 'com.android.support:multidex:1.0.1'
//dex分包
//butterknife View注解
compile 'com.jakewharton:butterknife:8.8.1'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
//tinker
//optional, help to generate the final application
//provided参与编译,不参与打包,这样可以减少apk的大小
provided("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
//tinker's main Android lib
compile("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
compile 'com.umeng.analytics:analytics:latest.integration'
//添加友盟统计
compile 'com.blankj:utilcode:1.7.1'
//android工具类
compile project(':good_http_utils')
//防止RxJava内存泄漏
compile 'com.trello:rxlifecycle:0.8.0'
compile 'com.trello:rxlifecycle-components:0.8.0'
compile 'com.tbruyelle.rxpermissions:rxpermissions:0.9.1@aar' //android6+特殊权限申请
}
def appTinkerVersion = "app-20181201-21-44-37"
/**
* ext相关配置
*/
ext {
tinkerEnabled = true
// 基础版本apk
tinkerOldApkPath = "${bakPath}/" + appTinkerVersion
// 未开启混淆的话mapping可以忽略,如果开启混淆mapping要保持一致。
tinkerApplyMappingPath = "${bakPath}/" + appTinkerVersion
// 与基础版本一起生成的R.text文件
tinkerApplyResourcePath = "${bakPath}/" + appTinkerVersion
// 只用于构建所有的Build,如果不是,此处可忽略。
tinkerBuildFlavorDirectory = "${bakPath}/" + appTinkerVersion
}
def buildWithTinker() {
return hasProperty("TINKER_ENABLE") ? TINKER_ENABLE : ext.tinkerEnabled
}
//基础APK的位置
def getOldApkPath() {
return hasProperty("OLD_APK") ? OLD_APK : ext.tinkerOldApkPath
}
// Mapping的位置
def getApplyMappingPath() {
return hasProperty("APPLY_MAPPING") ? APPLY_MAPPING : ext.tinkerApplyMappingPath
}
// ResourceMapping的位置
def getApplyResourceMappingPath() {
return hasProperty("APPLY_RESOURCE") ? APPLY_RESOURCE : ext.tinkerApplyResourcePath
}
// 用来获取TinkerId(当前版本号就是TinkerId)
def getTinkerIdValue() {
return android.defaultConfig.versionName
}
//多渠道
def getTinkerBuildFlavorDirectory() {
return ext.tinkerBuildFlavorDirectory
}
if (buildWithTinker()) {
//如果启用,加载Tinker插件,插件封装了些tinker脚本
apply plugin: 'com.tencent.tinker.patch'
//所有tinker相关参数配置
tinkerPatch {
oldApk = getOldApkPath() //基准apk包的路径,必须输入,否则会报错
/**
* 如果出现以下的情况,并且ignoreWarning为false,我们将中断编译。
* 因为这些情况可能会导致编译出来的patch包带来风险:
* case 1: minSdkVersion小于14,但是dexMode的值为"raw";
* case 2: 新编译的安装包出现新增的四大组件(Activity, BroadcastReceiver...);
* case 3: 定义在dex.loader用于加载补丁的类不在main dex中;
* case 4: 定义在dex.loader用于加载补丁的类出现修改;
* case 5: resources.arsc改变,但没有使用applyResourceMapping编译。
*/
ignoreWarning = false //不忽略tinker的警告,有则中止patch文件的生成
useSign = true //强制patch文件也需要签名
tinkerEnable = buildWithTinker() //是否启用tinker
//编译相关的配置项
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,编译补丁时可能会由于类的移动导致变更增多。
* 若打开keepDexApply模式,补丁包将根据基准包的类分布来编译。
*/
keepDexApply = false
}
//dex相关的配置项
dex {
/**
* 只能是'raw'或者'jar'。
* 对于'raw'模式,我们将会保持输入dex的格式。
* 对于'jar'模式,我们将会把输入dex重新压缩封装到jar。
* 如果你的minSdkVersion小于14,你必须选择‘jar’模式,而且它更省存储空间,但是验证md5时比'raw'模式耗时。
* 默认我们并不会去校验md5,一般情况下选择jar模式即可。
*/
dexMode = "jar"
/**
* 需要处理dex路径,支持*、?通配符,必须使用'/'分割。路径是相对安装包的,例如assets/...
*/
pattern = ["classes*.dex", "assets/secondary-dex-?.jar"]
//todo
loader = ["com.wj.tinkerutils.tinker.MyTinkerApplication"] //指定加载patch文件用到的类
}
lib {
/**
* 需要处理lib路径,支持*、?通配符,必须使用'/'分割。
* 与dex.pattern一致, 路径是相对安装包的,例如assets/...
*/
pattern = ["lib/*/*.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
}
//用于生成补丁包中的'package_meta.txt'文件
packageConfig {
/**
* configField("key", "value"), 默认我们自动从基准安装包与新安装包的Manifest中读取tinkerId,并自动写入configField。
* 在这里,你可以定义其他的信息,在运行时可以通过TinkerLoadResult.getPackageConfigByName得到相应的数值。
* 但是建议直接通过修改代码来实现,例如BuildConfig。
*/
//todo
configField("patchMessage", "tinker is sample to use")
configField("platform", "all")
/**
* 配置patch补丁版本
*/
configField("patchVersion", "1.0.0_1")
}
//7zip路径配置项,执行前提是useSign为true
// sevenZip {
// /**
// * 例如"com.tencent.mm:SevenZip:1.1.10",将自动根据机器属性获得对应的7za运行文件,推荐使用。
// */
// zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
// /**
// * 系统中的7za路径,例如"/usr/local/bin/7za"。path设置会覆盖zipArtifact,若都不设置,将直接使用7za去尝试。
// */
// // path = "/usr/local/bin/7za"
// }
}
List<String> flavors = new ArrayList<>();
project.android.productFlavors.each { flavor ->
flavors.add(flavor.name)
}
boolean hasFlavors = flavors.size() > 0
/**
* bak apk and mapping
*/
android.applicationVariants.all { variant ->
/**
* task type, you want to bak
*/
def taskName = variant.name
def date = new Date().format("yyyyMMdd-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"
}
}
}
}
}
}