Android热修复-微信Tinker

热修复现在是比较火的技术,它能帮助我们快速的升级,优点就是体积小,只有几十到几百k左右

本文环境

官方地址
SdkVersion 24
gradle:2.2.0
Tinker版本 1.7.5

集成Tinker

在项目根目录的build.gradle中添加,例:

 
     
     
dependencies {
···
classpath( 'com.tencent.tinker:tinker-patch-gradle-plugin:1.7.5')
}

切换到module的build.gradle中添加,例:

 
     
     
dependencies {
···
provided( 'com.tencent.tinker:tinker-android-anno:1.7.5')
compile( 'com.tencent.tinker:tinker-android-lib:1.7.5')
}

这里我把打包签名的jks文件配置到gradle中,例:
因为我这里测试用到的是项目的签名文件,所以不方便给出,需要的自行生成,毕竟这是很简单的事;

 
     
     
android {
···
signingConfigs {
release {
try {
storeFile file("./keystore/app.jks")
storePassword "password"
keyAlias "alias"
keyPassword "password"
} catch (ex) {
throw new InvalidUserDataException(ex.toString())
}
}
debug {
try {
storeFile file("./keystore/app.jks")
storePassword "password"
keyAlias "alias"
keyPassword "password"
} catch (ex) {
throw new InvalidUserDataException(ex.toString())
}
}
}
}

下面配置Tinker的gradle插件,官方demo配置还是比较麻烦的,看着就头晕,建议可以把先前的内容先复制出来,在拷贝官方demo的build.gradle进去一点点修改;

如果还是懒得改,直接把下面内容复制到build.gradle最下面:

 
     
     
/**-----------------------------------配置开始-----------------------------------*/
def bakPath = file( "${buildDir}/bakApk/")
def gitSha() {
try {
String gitRev = '11.2.3.5'
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'")
}
}
ext {
tinkerEnabled = true
//旧apk
tinkerOldApkPath = "${bakPath}/app-debug-0118-15-13-26.apk"
//旧包混淆文件
tinkerApplyMappingPath = "${bakPath}/app-debug-1018-17-32-47-mapping.txt"
//旧apk R文件
tinkerApplyResourcePath = "${bakPath}/app-debug-0118-15-13-26-R.txt"
//多渠道
tinkerBuildFlavorDirectory = "${bakPath}/app-1018-17-32-47"
}
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 {
/**
* 基准apk包路径,也就是旧包路径
* */
oldApk = getOldApkPath()
/**
* 如果出现以下的情况,并且ignoreWarning为false,我们将中断编译。因为这些情况可能会导致编译出来的patch包
* 带来风险:
* 1. minSdkVersion小于14,但是dexMode的值为"raw";
* 2. 新编译的安装包出现新增的四大组件(Activity, BroadcastReceiver...);
* 3. 定义在dex.loader用于加载补丁的类不在main dex中;
* 4. 定义在dex.loader用于加载补丁的类出现修改;
* 5. resources.arsc改变,但没有使用applyResourceMapping编译。
* */
ignoreWarning = false
/**
* 在运行过程中,我们需要验证基准apk包与补丁包的签名是否一致,是否需要为你签名
* */
useSign = true
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 {
/**
* 只能是'raw'或者'jar'。
* 对于'raw'模式,我们将会保持输入dex的格式。
* 对于'jar'模式,我们将会把输入dex重新压缩封装到jar。如果你的minSdkVersion小于14,你必须选择‘jar’模式
* ,而且它更省存储空间,但是验证md5时比'raw'模式耗时()
* */
dexMode = "jar"
/**
* 是否提前生成dex,而非合成的方式。这套方案即回退成Qzone的方案,对于需要使用加固或者多flavor打包(建
* 议使用其他方式生成渠道包)的用户可使用。但是这套方案需要插桩,会造成Dalvik下性能损耗以及Art补丁包可
* 能过大的问题,务必谨慎使用。另外一方面,这种方案在Android N之后可能会产生问题,建议过滤N之后的用户。
*/
usePreGeneratedPatchDex = false
/**
* 需要处理dex路径,支持*、?通配符,必须使用'/'分割。路径是相对安装包的,例如/assets/...
*/
pattern = [ "classes*.dex",
"assets/secondary-dex-?.jar"]
/**
* 这一项非常重要,它定义了哪些类在加载补丁包的时候会用到。这些类是通过Tinker无法修改的类,也是一定要放在main dex的类。
这里需要定义的类有:
1. 你自己定义的Application类;
2. Tinker库中用于加载补丁包的部分类,即com.tencent.tinker.loader.*;
3. 如果你自定义了TinkerLoader,需要将它以及它引用的所有类也加入loader中;
4. 其他一些你不希望被更改的类,例如Sample中的BaseBuildInfo类。这里需要注意的是,这些类的直接引用类也
需要加入到loader中。或者你需要将这个类变成非preverify。
*/
loader = [ "com.tencent.tinker.loader.*",
//warning, you must change it with your application
//TODO 换成自己的Application
"com.tinker.MyApplication",
]
}
lib {
/**
* 需要处理lib路径,支持*、?通配符,必须使用'/'分割。与dex.pattern一致, 路径是相对安装包的,例如/assets/...
*/
pattern = [ "lib/armeabi/*.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
}
packageConfig {
/**
* configField("key", "value"), 默认我们自动从基准安装包与新安装包的Manifest中读取tinkerId,并自动
* 写入configField。在这里,你可以定义其他的信息,在运行时可以通过TinkerLoadResult.getPackageConfigByName得到相应的数值。但是建议直接通过修改代码来实现,例如BuildConfig。
*/
configField( "patchMessage", "tinker is sample to use")
}
sevenZip {
/**
* 例如"com.tencent.mm:SevenZip:1.1.10",将自动根据机器属性获得对应的7za运行文件,推荐使用
*/
zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
}
/**
* 文件名 描述
* patch_unsigned.apk 没有签名的补丁包
* patch_signed.apk 签名后的补丁包
* patch_signed_7zip.apk 签名后并使用7zip压缩的补丁包,也是我们通常使用的补丁包。但正式发布的时候,最好不要以.apk结尾,防止被运营商挟持。
* log.txt 在编译补丁包过程的控制台日志
* dex_log.txt 在编译补丁包过程关于dex的日志
* so_log.txt 在编译补丁包过程关于lib的日志
* tinker_result 最终在补丁包的内容,包括diff的dex、lib以及assets下面的meta文件
* resources_out.zip 最终在手机上合成的全量资源apk,你可以在这里查看是否有文件遗漏
* resources_out_7z.zip 根据7zip最终在手机上合成的全量资源apk
* tempPatchedDexes 在Dalvik与Art平台,最终在手机上合成的完整Dex,我们可以在这里查看dex合成的产物。
*
*
* */
/**
* 获得所有渠道集合,并判断数量
*/
List<String> flavors = new ArrayList<>();
project.android.productFlavors.each { flavor ->
flavors.add(flavor.name)
}
boolean hasFlavors = flavors.size() > 0
/**
* bak apk and mapping
* 创建Task并执行文件操作
*/
android.applicationVariants.all { variant ->
/**
* task type, you want to bak
*/
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 {
//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"
}
}
}
}
}
}
}
/**-----------------------------------配置结束-----------------------------------*/

注意tinkerId目前暂时是写死的;
新建SampleApplicarion文件继承DefaultApplicationLike,例:

 
     
     
@DefaultLifeCycle(application = "com.tinker.MyApplication",flags = ShareConstants.TINKER_ENABLE_ALL)
public class SampleApplicarion extends DefaultApplicationLike {
public SampleApplicarion(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);
TinkerInstaller.install( this);
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {
getApplication().registerActivityLifecycleCallbacks(callback);
}
}

上面的application=”你的包名.MyApplication”并在上一步将dex中的loader修改为:

     
     
     
     
loader = [ "com.tencent.tinker.loader.*", "你的包名.MyApplication",]

记得在AndroidManifest.xml中添加:

 
     
     
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
<application
android:name= ".MyApplication"
android:allowBackup= "true"
android:icon= "@mipmap/ic_launcher"
android:label= "@string/app_name"
android:supportsRtl= "true"
android:theme= "@style/AppTheme">
···
···
</application>

测试修复BUG

在MainActivity中添加完初始代码后点击Build->Build APK生成APK;
描述


将outputs里面的apk拷贝出去安装在手机上,然后打开的build.radle修改ext,其它不管,例:

 
     
     
ext {
tinkerEnabled = true
//旧apk
tinkerOldApkPath = "${bakPath}/app-debug-0118-16-38-14.apk"
···
//旧apk R文件
tinkerApplyResourcePath = "${bakPath}/app-debug-0118-16-38-14-R.txt"
···
}

然后切换到MainActivity修复你的bug,这里我添加了一张资源图片(刚好同事在玩支付宝集福,就P了张有9张”敬业福”的图片骗他们,哈哈),修复好代码后点击右侧Gradle找到:app->Tasks->tinker,然后双击tinkerPatchDebug后会在outputs下面生成tinkerPatch文件夹,其中的patch_signed_7zip.apk就是你需要的补丁:
描述
将patch_signed_7zip.apk文件放到你设置的SD卡目录,

在MainActivity里面加入安装补丁的方法:


String patchPath  = Environment.getExternalStorageDirectory() + File.separator+"patch_signed_7zip.apk";
//                String patchPath  = path + "FixPath"+ File.separator+"patch_signed_7zip.apk";
                File file = new File(patchPath);
                if (file.exists()) {
                    Log.e(TAG,"补丁文件存在");
                    TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), patchPath);
                    Log.e(TAG,"安装完成");
                    Toast.makeText(getApplicationContext(),"安装完成",Toast.LENGTH_LONG).show();

                } else {
                    Log.e(TAG,"补丁文件不存在");
                    Toast.makeText(getApplicationContext(),"文件不存在",Toast.LENGTH_LONG).show();
                }


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值