最近两日,国内两大视频公司哔哩哔哩和爱奇艺先后登陆美国纳斯达克上市。但是受到近段时间国际市场动荡的影响,哔哩哔哩和爱奇艺上市当日都双双跌破首发价,其中爱奇艺更是在上市第一天就大跌13%。目前哔哩哔哩市值为30亿美元,爱奇艺的市值为110亿美元。
作者简介又到了开心的周五了,提前祝大家周末愉快!
本篇来自 g小志 的投稿,分享了他对 Tinker 的解析,希望大家喜欢。
小志 的博客地址:
前言https://www.jianshu.com/u/70a8f4edb323
今天我们来学习下微信的Tinker热修复,相比AndFix,Tinker的功能更加全面,更主要的是他支持gradle。他不仅做到了热修复更实现了“热更新”。既然他这么强大,下面我们就来了解他是如何使用的。
注意:本文有些长,但是理解起来很容易,非常适合没有接触过的小伙伴快速使用与分析
正文命令行生成补丁文件
在学习AndFix时由于它不自持Gradle,所以我们在生成补丁文件时是需要命令行去生成的。然而Tinker不仅支持Gradle同时也支持命令行生成补丁文件。不过在实际开发中,我们往往是使用Gradle去生成补丁文件,同时去配置一些需要的参数与属性。不过既然我们想详细了解它那么我们还是讲解下命令行生成补丁文件。
建议:无论学习什么技术,以官方文档为主,教程文章为辅。这样会好一些。
引入依赖
//注解库 用于生成application类 provided编译不打包 provided('com.tencent.tinker:tinker-android-anno:1.7.7'){ changing = true } //是否将依赖关系标记为正在改变 //tinker的核心库 compile编译并打包 compile('com.tencent.tinker:tinker-android-lib:1.7.7'){ changing = true }
命令行相对简单。首先我们要引入两个依赖。
创建ApplicationLike代理类
这里我们创建TinkerManager来实现对Tinker的管理。
TinkerManager:
/** * 功能 :Tinker管理类 */public class TinkerManager { private static boolean isInstalled = false;//是否已经初始化标志位 private static ApplicationLike mApplicationLike; /** * 完成Tinker初始化 * * @param applicationLike */ public static void installedTinker(ApplicationLike applicationLike) { mApplicationLike = applicationLike; if (isInstalled) { return; } TinkerInstaller.install(mApplicationLike); isInstalled = true; } /** * 完成patch文件的加载 * * @param path 补丁文件路径 */ public static void loadPatch(String path) { if (Tinker.isTinkerInstalled()) {//是否已经安装过 TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), path); } } /** * 利用Tinker代理Application 获取应用全局的上下文 * @return 全局的上下文 */ private static Context getApplicationContext() { if (mApplicationLike != null) return mApplicationLike.getApplication().getApplicationContext(); return null; }}
这里我们不是自己创建Application。而是使用Tinker为我们提供的ApplicationLike(也可以继承DefaultApplicationLike),作用已经有注释了。同时必须实现它的构造方法。并且我们重写了onBaseContextAttached这个方法并在里面初始化Tinker。完成这个后我们需要同步,然后就会生成MyTinkerApplication这个类(同步后没有出现这个类可以rebuild项目)。代码如下:
/** * 功能 :ApplicationLike为Tinker生成Context对象 官方建议 而不是继承我们自己的Application * 作用 :使用这个ApplicationLike这个类作为Application的委托代理是因为,Tinker需要监听Application * 的生命周期并针对不同的生命周期来做相应的初始化与处理,这样就减少使用者需要自己处理。 */@DefaultLifeCycle(application = ".MyTinkerApplication" , flags = ShareConstants.TINKER_ENABLE_ALL, loadVerifyFlag = false)//都是官方要求这么写的public class CustomTinkerLike extends ApplicationLike { public CustomTinkerLike(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); TinkerManager.installedTinker(this); }}
在Manifest.xml中配置
<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.ggxiaozhi.tinker"> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <application android:name=".tinker.MyTinkerApplication" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> <!-- 这个标签开判断我们生成的patch的.apk文件中的tinker_id_XXX 与我们的版本号tinker_id_XXX比较。相同合法,不同则不会进行更新 --> <meta-data android:name="TINKER_ID" android:value="tinker_id_19940208"/> </application></manifest>
首先我们加上必要的权限。然设置我们的MyTinkerApplication。同时我们还需要配置TINKER_ID这个属性,value值的数字部分一般为我们的versionCode。
生成差异apk文件
在完成配置后我们需要生成一个old.apk(也就是需要修复的apk)。代码如
MainActivity.xml:
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context="com.example.ggxiaozhi.tinker.MainActivity"> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:onClick="loadPatch" android:text="修复BUG"/></LinearLayout>
MainActivity:
public class MainActivity extends AppCompatActivity { private static final String FILE_END = ".apk";//文件后缀 private String FILEDIR;//文件路径 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // /storage/emulated/0/Android/data/com.example.ggxiaozhi.tinker/cache/tpatch/ FILEDIR = getExternalCacheDir().getAbsolutePath() + "/tpatch/"; //创建路径对应的文件夹 File file = new File(FILEDIR); if (!file.exists()) file.mkdir(); } public void loadPatch(View view) { TinkerManager.loadPatch(getPatchName()); } public String getPatchName() { return FILEDIR.concat("tinker").concat(FILE_END); }}
这是old.apk中的代码。布局与代码也非常简单就是创建补丁文件的路径,在点击按钮时加载补丁文件。然后我就开始打包带签名文件的old.apk。这里我就不带大家打包了。打包完成后,我们修改下布局:
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context="com.example.ggxiaozhi.tinker.MainActivity"> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:onClick="loadPatch" android:text="修复BUG"/> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:text="测试按钮"/></LinearLayout>
这里我们其他代码不动,只是增加了一个按钮。同时我们在打包一个新的new.apk文件出来。并将两个文件和签名文件。同时copy到命令行工具中。
命令行生成补丁文件
首先我们利用Tinker官方为我们提供的命令行工具目录如下:
将上面我们生成的两个apk文件重命名并将签名文件copy到该目录下。(注意.keystore是eclipse的签名文件.jks是AndroidStudio的签名文件,可以直接修改后缀名,并不影响)然后我们输入一下命令:
java -jar tinker-patch-cli.jar -old old.apk -new new.apk -config tinker_config.xml -out output_path
生成补丁文件。具体如下:
output为我们补丁文件的输出文件夹,不存在会自动创建。输入完命令后output文件夹如下:
patch_signed.apk文件就是我们的补丁文件。然后我们安装old.apk并将这个补丁文件通过命令或是拷贝我们之前创建的指定文件下并重命名成我们代码中写的tinker.apk。这样点击按钮就会完成修复。
注意,在点击后会杀到当前进程,需要重新进入后才能看到效果。官方建议我们去监听手机的广播,比如锁屏的广播,点击HOME键等。来去重新启动,这个问题后面我们再去优化
gradle生成补丁文件
文章开始我们就说过在实际中,我们是通常是以gradle生成补丁文件较多。当然网上也有很多配置教程,基本上大同小异。下面我们来在上面代码的基础上修改,来完成gradle生成补丁文件。首先我们先修改下Manifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.ggxiaozhi.tinker"> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> ... <!-- 这个标签开判断我们生成的patch的.apk文件中的tinker_id_ 与我们的版本号tinker_id_比较。相同合法,不同则不会进行更新 --> <!--<meta-data android:name="TINKER_ID" android:value="tinker_id_19940208"/>--></manifest>
这里我们将tinker_id注释掉,因为我们会在gradle中去配置。然后在最外面的gradle中添加插件。
// Top-level build file where you can add configuration options common to all sub-projects/modules.buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:2.3.3' //加入Tinker的插件 里面包含gradle脚本 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 }}
其他不同修改,只需加上插件即可。然后我们就可以配置gradle了。关于gradle配置网上有很多,基本上都懂小异。我把我的gradler配置粘贴出来,供大家参考:
apply plugin: 'com.android.application'/*================================常量块中的引用常量====================================*/def javaVersion=JavaVersion.VERSION_1_7//这个目录是基于项目的目录:Tinker/app/build/bakApk目录下存放oldApk//buildDir : Tinker/app/build/def bakPath = file("${buildDir}/bakApk/")//指定基准文件(oldApk)存放位置android { compileSdkVersion 25 buildToolsVersion "25.0.3" defaultConfig { applicationId "com.example.ggxiaozhi.tinker" minSdkVersion 19 targetSdkVersion 25 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" multiDexEnabled true } //排除目录下不需要编译的包 sourceSets { main { jniLibs.srcDirs = ['libs'] } } //java版本 compileOptions { sourceCompatibility javaVersion targetCompatibility javaVersion } //建议 recommend Tinker相关配置 dexOptions { //启动矩形模式 jumboMode = true } signingConfigs { release { try { storeFile file("release.jks")//目录位置app/release.jks storePassword "gg199402" keyAlias "gg199402" keyPassword "gg199402" } catch (ex) { throw new InvalidUserDataException(ex.toString()) } } } buildTypes { release { //是否进行混淆 minifyEnabled true // 混淆文件的位置 proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' signingConfig signingConfigs.release } }<!-- //真正的多渠道脚本支持 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(dir: 'libs', include: ['*.jar']) androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) //可选,用于生成application类 provided编译不打包 provided("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true } //是否将依赖关系标记为正在改变 //tinker的核心库 compile编译并打包 compile("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true } compile "com.android.support:multidex:1.0.1"//分包 compile 'com.android.support:appcompat-v7:25.3.1' compile 'com.android.support.constraint:constraint-layout:1.0.2' testCompile 'junit:junit:4.12'}ext { tinkerEnabled = true //是否启用Tinker的标志位 tinkerOldApkPath = "${bakPath}/"//oldApk 文件路径 tinkerID = "1.0"//与版本号一致 tinkerApplyMappingPath = "${bakPath}/" //混淆文件路径 tinkerApplyResourcePath = "${bakPath}/" //资源路径<!-- tinkerBuildFlavorDirectory = "${bakPath}/" //多渠道路径-->}/*================================方法实现模块====================================*/def getOldApkPath() { return ext.tinkerOldApkPath}def getApplyMappingPath() { return ext.tinkerApplyMappingPath}def getApplyResourceMappingPath() { return ext.tinkerApplyResourcePath}def getTinkerIdValue() { return ext.tinkerID}def buildWithTinker() { return ext.tinkerEnabled}<!--def getTinkerBuildFlavorDirectory(){ return ext.tinkerBuildFlavorDirectory}-->if (buildWithTinker()) { //启用Tinker 引入相关Gradle方法 apply plugin: 'com.tencent.tinker.patch' //所有Tinker相关参数的配置 tinkerPatch { /*================================基本配置====================================*/ //指定old apk(即上一个版本的Apk) 的文件路径 oldApk = getOldApkPath() //是否忽略Tinker在产生patch文件时的错误警告并中断编译 false 不忽略 这样可以在生成patch文件时查看错误 具体哪些错误类型查考文档 ignoreWarning = false //patch是否需要签名 true为需要 防止恶意串改 useSign = true //是否启用tinker tinkerEnable = buildWithTinker() /*================================build配置====================================*/ buildConfig { //指定old apk打包时所使用的混淆文件 (因为patch文件也是需要混淆的 所以必须要与Apk的打包混淆文件一致) applyMapping = getApplyMappingPath() //指定old apk的资源文件 希望new apk与其保持一致(R.txt 文件保持ResId的分配) applyResourceMapping = getApplyResourceMappingPath() //指定TinkerID patch文件的唯一标识符 要与新旧Apk一致 tinkerId = getTinkerIdValue() //通常为false true会根据dex分包动态编译patch文件 keepDexApply = false } /*================================dex相关配置====================================*/ dex { //Tinker提供两种模式jar、raw //jar 适配到了api=14以下 而raw只能再14以上 //jar模式下 Tinker会对dex文件压缩成jar文件 在对jar进行处理 //raw模式下 Tinker直接对dex进行处理 //使用jar文件体积相对会小一些 在实际开发中用jar模式较多 dexMode = "jar" //指定dex目录 "assets/secondary-dex-?.jar"为Tinker官方Demo中建议参数 //在没有分包的情况下 "classes*.dex" 会匹配到应用中的所有dex文件 分包会是classes1,classes2.... pattern = ["classes*.dex", "assets/secondary-dex-?.jar"] //制定patch文件用到类 loader = ["com.example.ggxiaozhi.tinker.tinker.MyTinkerApplication"] } /*================================Tinker关于jar与.so文件的替换相关配置====================================*/ lib { pattern = ["libs/*/*.so"] } /*================================Tinker关于资源文件替换相关配置====================================*/ res { //指定Tinker可以修改的资源文件路径 // resources.arcs :AndroidReSourCe也就是与Android资源相关的一种文件格式。 // 具体角色是提供资源ID到资源文件路径的映射关系, // 具体来说就是R.layout.activity_main(0x7f030000)到res/layout/activity_main.xml的映射关系 // 其中R.layout.activity_main就是应用开发过程中所使用的资源ID pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"] //在编译时会忽略该文件的新增、删除与修改 即使修改了文件 也不会patch文件生效 ignoreChange = ["assets/sample_meta.txt"] //对于修改的资源,如果大于largeModSize,我们将使用bsdiff算法。 // 默认大小为100kb largeModSize = 100 } /*=============附加说明字段 配置 说明本次Patch文件的相关信息 非必须 packageConfig(官方:用于生成补丁包中的'package_meta.txt'文件)=================*/ packageConfig { /*configField("key","value") 键值对 用于说明 当客户端使用patch文件修复成功 可以通过代码获取下面patch相关信息*/ configField("patchMessage", "fix the version's bugs") configField("patchVersion", "1.0") } //sevenZip ...... sevenZip { /** * 例如"com.tencent.mm:SevenZip:1.1.10",将自动根据机器属性获得对应的7za运行文件,推荐使用 */ zipArtifact = "com.tencent.mm:SevenZip:1.1.10" } } /*================================备份脚本 用来将生成的APK的制定文件备份到制定目录====================================*/ //多渠道相关遍历 List<String> flavors = new ArrayList<>(); project.android.productFlavors.each { flavor -> flavors.add(flavor.name) } //如果是多渠道 则size()>0 为true boolean hasFlavors = flavors.size() > 0 /** * bak apk and mapping 备份pak与mapping(配置文件) */ 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}" //destPath为备份的目录 没有没有多渠道打包那么hasFlavors为false destPath=bakPath bakPath即最上面定义的基础目录 def destPath = hasFlavors ? file("${bakPath}/${project.name}-${date}/${variant.flavorName}") : bakPath from variant.outputs.outputFile into destPath //备份.apk文件 rename { String fileName -> fileName.replace("${fileNamePrefix}.apk", "${newFileNamePrefix}.apk") } from "${buildDir}/outputs/mapping/${variant.dirName}/mapping.txt" into destPath //备份mapping.txt文件 rename { String fileName -> fileName.replace("mapping.txt", "${newFileNamePrefix}-mapping.txt") } from "${buildDir}/intermediates/symbols/${variant.dirName}/R.txt" into destPath //备份R.txt文件 即用于映射的资源ID rename { String fileName -> fileName.replace("R.txt", "${newFileNamePrefix}-R.txt") } } } } } } /* Tinker多渠道打包文件拼凑脚本*/ <!-- 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" } } } //Debug签名多渠道打包基本 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" } } } } }-->}
这里有关多渠道打包的部分我先注释掉,我们先从简单看起。里面注释应该是比较详细的了,在使用中这些配置也基本满足需求。关于参数与配置也可以参考官方文档。sample中的app/build.gradle以及gradle参数详解。
做完这些Tinker的gradle接入就完成了。还是之前的代码我们先打一个包含一个button的带签名的正式包。
首先我们点击1.生成基准(oldApk)签名包。2.是用来生成补丁文件的。然后我们修改代码,在加入一个Button,也可以同时给加上点击事件Toast。生成apk文件后目录如下:
首先我们在app/build/outputs/apk/app-release.apk生成签名文件apk,并备份到在app/build/bakApk/下,并以时间重命名文件。这三个文件分别是基准包(oldApk)、混淆文件、资源文件。然后我们分别将这个文件名写入到我们的gradle中,如下:
ext { tinkerEnabled = true //是否启用Tinker的标志位 tinkerOldApkPath = "${bakPath}/app-release-0201-16-15-06.apk"//oldApk 文件路径 tinkerID = "1.0"//与版本号一致 tinkerApplyMappingPath = "${bakPath}/app-release-0201-16-15-06-mapping.txt" //混淆文件路径 tinkerApplyResourcePath = "${bakPath}/app-release-0201-16-15-06-R.txt" //资源路径}
只需要修改ext部分其他不变。然后我们点击2.部分生成补丁文件。补丁文件目录文件如下:
目录中的参数作用,可以参考下表:
然后我们就将基准包安装到手机中,并将补丁文件copy到我们代码中指定的文件夹下并重命名。就可以完成动态更新。我亲测有效。所以就不发动图了。
功能扩展
在扩展功能之前我们要先来了解下。我们可以扩展那些功能。下面我们重Tinker的初始化函数入手。修改TinkerManager代码如下:
/** * 完成Tinker初始化 * * @param applicationLike */ public static void installedTinker(ApplicationLike applicationLike) { mApplicationLike = applicationLike; if (isInstalled) { return; }// TinkerInstaller.install(mApplicationLike); mPatchListener = new DefaultPatchListener(getApplicationContext());//一些补丁文件的校验工作 //这两个是监听patch文件安装的日志上报结果 也就是补丁文件安装监听 LoadReporter loadReporter = new DefaultLoadReporter(getApplicationContext());//一些在加载补丁文件时的回调 PatchReporter patchReporter = new DefaultPatchReporter(getApplicationContext());//补丁文件在合成时一些事件的回调 AbstractPatch abstractPatch = new UpgradePatch();//决定patch文件安装策略 不会去修改与自定义 TinkerInstaller.install(mApplicationLike, loadReporter, patchReporter, mPatchListener, CustomResultService.class,//我们自定义的 abstractPatch); isInstalled = true; }
可以看到我们把上面的初始化函数注释掉,而是采用6个参数的注册方法。这些参数的作用在官方文档中都非常的详细自定义扩展。我这里全都是使用的默认的。这里根据实际开发区决定要自定义那些内容。我就不过多介绍了。不过我重写了CustomResultService类。我们看下:
/** * 功能 :决定在patch安装以后的后续操作 默认实现是杀死进程 */public class CustomResultService extends DefaultTinkerResultService { private static final String TAG = "Tinker.DefaultTinkerResultService"; @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)); <!-- if (checkIfNeedKill(result)) { android.os.Process.killProcess(android.os.Process.myPid()); } else { TinkerLog.i(TAG, "I have already install the newly patch version!"); }--> } }}
我们知道,Tinker在补丁文件安装完成后默认是杀死当前进程的,显然这样做的效果不是很好。所以我们重写了DefaultTinkerResultService,并将原本杀死进程的代码注释掉,这样我们就可以在用户无感知的情况下完成补丁的安装,并且在用户下次启动的时候生效。其他的代码不变,重新走一遍流程。就会看到效果的生成
多渠道打包修复
还记得我们在第一篇中我们将所有关于多渠道打包的代码都注释了,现在我们将注释的代码放开,然后按照步骤。
首先先打出多渠道签名文件包
第二步配置gradle脚本。
可以看到多渠道打包后,基准包路径的配置还是有些不同的。(这里我没有开启混淆。不过是没有影响的)
第三步就是修改代码,然后生成补丁文件
生成后的目录:
这样就针对两个渠道的签名包,生成补丁文件,剩下的流程就与之前一样了。
从源码的角度分析流程
Tinker的源码是比较的复杂的尤其的它的Dexdiff算法。本身我还是个菜鸟,所以只能够源码角度理清下流程。有人可能问,一个框架,能使用有效果,问题能排查就好啦。确实我个人觉得一个技术或是框架日新月异。总有些新的技术出现并且我也不是大神,感觉有些浪费时间。不过我还是逼自己去看源码。从中我认为最好的好处有两个:1.能更好的定位为题,和自定义扩展功能 2.能学习优秀框架的代码格式书写。使自己写出高质量的代码,这也是我认为最重要的。
从Tinker的注册方法可以看到,他用了外观模式。所以我们也从 TinkerInstaller.install()进去
public class TinkerInstaller { private static final String TAG = "Tinker.TinkerInstaller"; /** * install tinker with default config, you must install tinker before you use their api * or you can just use {@link TinkerApplicationHelper}'s api * * @param applicationLike */ public static Tinker install(ApplicationLike applicationLike) { Tinker tinker = new Tinker.Builder(applicationLike.getApplication()).build(); Tinker.create(tinker); tinker.install(applicationLike.getTinkerResultIntent()); return tinker; } /** * install tinker with custom config, you must install tinker before you use their api * or you can just use {@link TinkerApplicationHelper}'s api * * @param applicationLike * @param loadReporter * @param patchReporter * @param listener * @param resultServiceClass * @param upgradePatchProcessor */ public static Tinker install(ApplicationLike applicationLike, LoadReporter loadReporter, PatchReporter patchReporter, PatchListener listener, Class<? extends AbstractResultService> resultServiceClass, AbstractPatch upgradePatchProcessor) { Tinker tinker = new Tinker.Builder(applicationLike.getApplication()) .tinkerFlags(applicationLike.getTinkerFlags()) .loadReport(loadReporter) .listener(listener) .patchReporter(patchReporter) .tinkerLoadVerifyFlag(applicationLike.getTinkerLoadVerifyFlag()).build(); Tinker.create(tinker); tinker.install(applicationLike.getTinkerResultIntent(), resultServiceClass, upgradePatchProcessor); return tinker; } /** * clean all patch files! * * @param context */ public static void cleanPatch(Context context) { Tinker.with(context).cleanPatch(); } /** * new patch file to install, try install them with :patch process * * @param context * @param patchLocation */ public static void onReceiveUpgradePatch(Context context, String patchLocation) { Tinker.with(context).getPatchListener().onPatchReceived(patchLocation); } /** * set logIml for TinkerLog * * @param imp */ public static void setLogIml(TinkerLog.TinkerLogImp imp) { TinkerLog.setTinkerLogImp(imp); }}
这个类也比较简单,就是2个注册的方法,前面我们也都用到了,还有就是加载补丁的方法和清除补丁的方法。可以看到Tinker处理的核心类是Tinker类。Tinker类中就是利用单例和构建者模式创建Tinker,并将我们传入的参数利用构建者进行初始化。下面我们就看下加载补丁的方法onReceiveUpgradePatch。这里调用 Tinker.with(context).getPatchListener().onPatchReceived(patchLocation);方法并将我们补丁文件的路径出入进去。点击去发现是一个接口。下面我们就来看下这个接口的实现类。还记得我们在参数中传入的DefaultPatchListener吗?这个就是实现这个方法的类。
public class DefaultPatchListener implements PatchListener { protected final Context context; public DefaultPatchListener(Context context) { this.context = context; } /** * when we receive a patch, what would we do? * you can overwrite it * * @param path * @return */ @Override public int onPatchReceived(String path) { //对补丁文件的校验 int returnCode = patchCheck(path); if (returnCode == ShareConstants.ERROR_PATCH_OK) { //启动加载补丁文件并修复BUG的服务 TinkerPatchService.runPatchService(context, path); } else { Tinker.with(context).getLoadReporter().onLoadPatchListenerReceiveFail(new File(path), returnCode); } return returnCode; } //对补丁文件的校验 我们可以重写这个方法,去实现我们自己的校验,比如MD5检验等 也可以重写ShareConstants来实现我们自己的错误码 protected int patchCheck(String path) { Tinker manager = Tinker.with(context); //check SharePreferences also if (!manager.isTinkerEnabled() || !ShareTinkerInternals.isTinkerEnableWithSharedPreferences(context)) { return ShareConstants.ERROR_PATCH_DISABLE; } File file = new File(path); if (!SharePatchFileUtil.isLegalFile(file)) { return ShareConstants.ERROR_PATCH_NOTEXIST; } //patch service can not send request if (manager.isPatchProcess()) { return ShareConstants.ERROR_PATCH_INSERVICE; } //if the patch service is running, pending if (TinkerServiceInternals.isTinkerPatchServiceRunning(context)) { return ShareConstants.ERROR_PATCH_RUNNING; } return ShareConstants.ERROR_PATCH_OK; }}
在这个onPatchReceived方法中启动了一个服务我们继续跟踪TinkerPatchService.runPatchService(context, path);这个方法:
public class TinkerPatchService extends IntentService { ...... public static void runPatchService(Context context, String path) { try { Intent intent = new Intent(context, TinkerPatchService.class); intent.putExtra(PATCH_PATH_EXTRA, path); intent.putExtra(RESULT_CLASS_EXTRA, resultServiceClass.getName()); context.startService(intent); } catch (Throwable throwable) { TinkerLog.e(TAG, "start patch service fail, exception:" + throwable); } } ...... @Override protected void onHandleIntent(Intent intent) { final Context context = getApplicationContext(); Tinker tinker = Tinker.with(context); tinker.getPatchReporter().onPatchServiceStart(intent); if (intent == null) { TinkerLog.e(TAG, "TinkerPatchService received a null intent, ignoring."); return; } String path = getPatchPathExtra(intent); if (path == null) { TinkerLog.e(TAG, "TinkerPatchService can't get the path extra, ignoring."); return; } File patchFile = new File(path); long begin = SystemClock.elapsedRealtime(); boolean result; long cost; Throwable e = null; increasingPriority(); PatchResult patchResult = new PatchResult(); try { if (upgradePatchProcessor == null) { throw new TinkerRuntimeException("upgradePatchProcessor is null."); } //处理补丁文件核心方法 result = upgradePatchProcessor.tryPatch(context, path, patchResult); } catch (Throwable throwable) { e = throwable; result = false; //将处理的结果通知到我们传入的DefaultPatchListener中 tinker.getPatchReporter().onPatchException(patchFile, e); } cost = SystemClock.elapsedRealtime() - begin; tinker.getPatchReporter(). onPatchResult(patchFile, result, cost); patchResult.isSuccess = result; patchResult.rawPatchFilePath = path; patchResult.costTime = cost; patchResult.e = e; //开启加载补丁完成后服务,这就是我们上面重写安装补丁后的如何自定义行为的service类,默认是杀死当前进程。 AbstractResultService.runResultService(context, patchResult, getPatchResultExtra(intent)); }....}
可以看到TinkerPatchService是一个自带子线程的服务开启这个服务后,就会走到onHandleIntent方法中,里面做了一些异常的判断。处理补丁文件的方法就是upgradePatchProcessor.tryPatch(context, path, patchResult);这个方法。点击入也是一个抽象的方法。我们来看他的默认的实现类UpgradePatch:
public class UpgradePatch extends AbstractPatch { private static final String TAG = "Tinker.UpgradePatch"; ..... //we use destPatchFile instead of patchFile, because patchFile may be deleted during the patch process //修复dex文件的核心方法入口 if (!DexDiffPatchInternal.tryRecoverDexFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) { TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch dex failed"); return false; } //修复lib文件的核心方法入口 if (!BsDiffPatchInternal.tryRecoverLibraryFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) { TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch library failed"); return false; } //修复资源文件的核心方法入口 if (!ResDiffPatchInternal.tryRecoverResourceFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) { TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch resource failed"); return false; }}
这个类的核心方法就是上面提到的三个方面的修复,再往里走下去就比较底层,涉及到Dexdiff算法,有兴趣的可以看下鸿洋大神关于diff算法(https://blog.csdn.net/lmj623565791/article/details/60874334 )的文章这个就不分析了。下面我们来用流程图总结下:
大致的流程就是这样,具体的大家也可以自行研究下。
引入Tinker后的代码管理
在引入Tinker可以在git分支上专门创建一个hotFix分支专门是用来修改bug的分支,与开发分支(如dev)和主分支(master)区分开。具体情况以自己实际情况为主。
结语经过本篇文章的学习,相信大家对Tinker已经有了一定的认识。Tinker在使用中感觉还是有不少坑的,但是相对于AndFix,Tinker支持的比较全,并且支持在微信上也在使用。同时现在又支持Android热更新服务平台与Bulgy,也更加方便。
欢迎长按下图 -> 识别图中二维码
或者 扫一扫 关注我的公众号
