Tinker热修复之 —— Gradle接入

来源:互联网 发布:新路由器有信号没网络 编辑:程序博客网 时间:2024/04/30 02:08



今日科技快讯


10月17日,据路透社报道,中国智能手机制造商华为公司发布了一款新的旗舰智能手机Mate 10系列,其改进功能甚至可与苹果或三星的高端手机相媲美,而性价比更高。华为的目标是通过技术进步来凸显自己,这将使其在高端市场竞争中处于有利位置,进而提高盈利能力。


作者简介


本篇来自 BigSweetee 的投稿,讲解了如何用 gradle接入热修复tinker,希望对大家有所帮助!

BigSweetee 的博客地址:

http://blog.csdn.net/qq_15527709/


前言


今天研究了一天的热修复,热修复,简单的来讲就是在不需要发包的情况下,修改你线上应用的bug,接入使用后对于我这种小白来说还是很神奇的,同时也考虑了一下,要不要接入我们的项目中,这样就不用因为一个小BUG而去再次发包了,不过,就算要接入项目中,也还有很多坑需要踩,tinker有俩种接入方式,一种命令行接入,一种是gradle接入,本篇只讲 gradle接入,下篇我在补充命令行,主要用于自己做个记录,把踩得坑和感想写下来. 


正文


将这个 C/C++编译链接生成二进制文件的这个过程是谁做的?

首先,基本的配置 

在 project 的 build中配置如下

dependencies {         classpath 'com.android.tools.build:gradle:2.2.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     }

TINKER_VERSION 需要在 gradle.properties 中进行配置

TINKER_VERSION=1.7.7

这里这样写的目的是把资源统一管理,接下来是配置app的build,官方文档推荐我们使用这个,地址为:

https://github.com/Tencent/tinker/blob/master/tinker-sample-android/app/build.gradle 

于是我全部 copy 了过来,不过里面的几个地方还是需要修改的 。

这里因为源码太多,有兴趣的同学可以点击下方阅读全文,去原文中进行查看学习。

主要修改了下面几个地方

 buildConfigField "String", "TINKER_ID", "\"${getTinkerIdValue()}\""

官方文档默认是将git提交的记录作为think_id记录下来,这里我不需要,所以我改成下图

buildConfigField "String", "TINKER_ID", "\"1.0\""

我直接写死,需要git提交记录作为tinker_id的也可以按照官方文档推荐的。同时,这些地方也可以相应的替换掉

//废弃 /*def getTinkerIdValue() {     return hasProperty("TINKER_ID") ? TINKER_ID : gitSha() }*/ tinkerId = "1.0"/*getTinkerIdValue()*/

接下来修改自己的签名文件,我这里配置的文件路径是自己电脑上面的,这个位置大家需要修改为自己的签名文件路径,也可以按照我的去生成一个签名文件,我相信这个还是很简单的。

//配置自己的签名文件,签名文件的生成和导入可以去百度,本篇不讲解 signingConfigs {     release {          try {              keyAlias 'china'              keyPassword '123456'              storeFile file('D:/work/release.jks')              storePassword '123456'          } catch (ex) {              throw new InvalidUserDataException(ex.toString())          } 
   }    debug {         storeFile file('D:/work/debug.jks')         keyAlias 'china'         keyPassword '123456'         storePassword '123456'    } }
buildTypes {      release {          minifyEnabled true          signingConfig signingConfigs.release          proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'      }      debug {          debuggable true          minifyEnabled false          signingConfig signingConfigs.debug          proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'      }  }

官网文档没有设置 debug 的 proguardFiles getDefaultProguardFile(‘proguard-android.txt’), ‘proguard-rules.pro’ 也就是说没有应用混淆文件,这样是不会生成 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}"                         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")                         }                     }                 }             }         }

这下面主要是设置生成的文件所在的目录是什么。配置完上面之后,build就配置好了,接下来配置 application, 先上代码:

package com.anlaiye.swt.gradletest; import android.app.Application; import android.content.Context; import android.content.Intent; import android.support.multidex.MultiDex; import com.tencent.tinker.anno.DefaultLifeCycle; import com.tencent.tinker.lib.tinker.TinkerInstaller; import com.tencent.tinker.loader.app.ApplicationLike; import com.tencent.tinker.loader.shareutil.ShareConstants; @DefaultLifeCycle(application = ".SimpleTinkerInApplication",         flags = ShareConstants.TINKER_ENABLE_ALL,         loadVerifyFlag =true) public class SimpleTinkerInApplicationLike extends ApplicationLike {     public SimpleTinkerInApplicationLike(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);         TinkerInstaller.install(this);     }     @Override     public void onCreate() {         super.onCreate();     } }

这个 application 官网有提供的,也可以copy我的,ApplicationLike 并不是一个application,真正的 application 是 @DefaultLifeCycle(application = “.SimpleTinkerInApplication” 这个,所以在 androidmanifest 中配置 applica的 name:

<application     android:name=".SimpleTinkerInApplication"     android:allowBackup="true"     android:icon="@mipmap/ic_launcher"     android:label="@string/app_name"     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> 
</application>

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

同时配置读取sd卡的权限,接下来测试:

<?xml version="1.0" encoding="utf-8"?> 
<RelativeLayout    xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools"    android:id="@+id/activity_main"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:paddingBottom="@dimen/activity_vertical_margin"    android:paddingLeft="@dimen/activity_horizontal_margin"    android:paddingRight="@dimen/activity_horizontal_margin"    android:paddingTop="@dimen/activity_vertical_margin"    tools:context="com.anlaiye.swt.gradletest.MainActivity">    <TextView        android:id="@+id/tv"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:text="111"/>    <Button        android:layout_below="@+id/tv"        android:onClick="loadPath"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:text="更新"/>
</RelativeLayout>

第一次我设置 textview 的值是111,然后设置按钮调用 onclick:

package com.anlaiye.swt.gradletest; import android.os.Bundle; import android.os.Environment; import android.support.v7.app.AppCompatActivity; import android.view.View; import android.widget.Toast; import com.tencent.tinker.lib.tinker.TinkerInstaller; import java.io.File; public class MainActivity extends AppCompatActivity {     @Override     protected void onCreate(Bundle savedInstanceState) {         super.onCreate(savedInstanceState);         setContentView(R.layout.activity_main);     }     //加载补丁     public void loadPath(View view) {         String path = Environment.getExternalStorageDirectory().getAbsolutePath()+"/patch_signed_7zip.apk";         File file = new File(path);         if (file.exists()){             Toast.makeText(this, "补丁已经存在", Toast.LENGTH_SHORT).show();             TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), path);         }else {             Toast.makeText(this, "补丁已经不存在", Toast.LENGTH_SHORT).show();         }     } }

然后我运行APP,会生成如下的目录结构: 

 

同时界面效果 

 

接下来做如下修改:

//老版本的文件所在的位置,大家也可以动态配置,不用每次都在这里修改 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-release-0313-16-49-55.apk"     //proguard mapping file to build patch apk     tinkerApplyMappingPath = "${bakPath}/app-release-0313-16-49-55-mapping.txt"     //resource R.txt to build patch apk, must input if there is resource changed     tinkerApplyResourcePath = "${bakPath}/app-release-0313-16-49-55-R.txt"     //only use for build all flavor, if not, just ignore this field     tinkerBuildFlavorDirectory = "${bakPath}/app-1018-17-32-47" }

注意这里是 release 版本,不能 debug 版本,release 版本混着来,比如 oldapk 是 debug 版本,新的 apk 是 release版本,这样是不行的,统一用 release 版本。

这里我将对应路径改为 bakapk 中第一次运行生成的apk文件名字,比如第一次生成的 apk文件名为 app-release-0313-16-49-55.apk,所以我把 tinkerOldApkPath 的路径后面的名字修改为对应的 app-release-0313-16-49-55.apk,mapping 文件和 R 文件路径也要对应修改,最后一项 tinkerBuildFlavorDirectory 可以忽略,我一直觉得这里太死板,应该像设置tinker_id 那样对这里进行动态的配置,不用每次都来修改。

接下来我把 textview 改成2222,用来和第一次作区别。然后点击: 

生成目录如下

 

注意箭头所指的文件 ,我们在 onclick 中执行的文件名就是这个,这个文件就是我们需要的最终的文件,然后将这个文件导入SD卡的最外层目录,点击更新按钮,点击后如下 :

 

因为模拟器不好导入patch的apk所以直接截图了,成功。

本篇只是简单的演示了整个流程,整理了一下容易踩得坑,可以优化的地方还有很多,大家可以在这个基础上自行发挥。


总结


需要准备的工作:

总结一下自己碰到的坑, 

  • 首先没有在gradle.properties中配置tinker_id,会提示错误tinker_id not set!!! 

  • debug 没有配置 proguardFiles getDefaultProguardFile(‘proguard-android.txt’), ‘proguard-rules.pro’ 

导入生成的apk一直没有mapping文件,找了半天才发现。

最后说下热修复的大概流程 

最终我们需要的是一个patch.apk文件,这个文件是通过老apk和新apk对比生成的, 具体怎么对比和生成,是tinker控制的,所以我们需要导入他的依赖包,新APK和老APK的mapping 必须是一致的,所以我们需要把老APK的mapping保存起来,方便新APK与他对比。 这样就能得到最终的patch,然后调用 sdk 的TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), path),这样就实现了热修复的目的。

  • 本篇下载地址

csdn下载地址为:

http://download.csdn.net/download/qq_15527709/10017530 

考虑到现在csdn改版下载代码都需要分了,所以我重新上传了一份到了github,地址如下:

https://github.com/BigSweet/TinkerGradleDemo


欢迎长按下图 -> 识别图中二维码

或者 扫一扫 关注我的公众号

原创粉丝点击