前言
做前端開發的都知道,當我們項目做完了以后,都會把應用上傳到應用市場上供用戶下載使用,比如上傳到應用寶啊,應用匯啊,360啊,小米,華為,魅族啊,等等
但是,有時候我們會經常遇到一些很扯淡的事情,剛剛熬夜加班將項目發到應用市場上,第二天,又發現一個嚴重bug,難道是開會研究看看是否能在下一版解決?還是將bug解決了,
在給測試進行測試,然后再發版?如果從新發版,這個成本太高了,那怎么辦呢,今天我們就來談一談熱修復——Tinker!
現在市場上的熱修復工具或者框架有很多很多,比較出名的有阿里的 AndFix、美團的 Robust 以及 QZone 的超級補丁方案。這個方案各有利弊,但是今天我們不說這幾個方案,我們來聊一聊另一個補丁方案——Tinker。首先我們上一張圖:
Tinker熱補丁方案·不僅支持類、So以及資源的替換,它還是2.X-7.X的全平台支持。
在官網上,介紹這個Tinker,用了一句話:Tinker 已運行在微信的數億 Android 設備上,那么為什么你不使用 Tinker 呢? ps:別人幾億的用戶量在用Tinker,都不擔心出現什么問題,
你們幾個用戶量的應用,還整天瞎擔心。
這篇文章,主要是和大家聊一下怎么集成這個Tinker,並不會設計很多原理的東西,如果需要了解更深入的原理性的東西,可以到官網看看。點擊官網http://tinkerpatch.com/Docs/intro
集成SDK
第一步 添加 gradle 插件依賴
在項目的Gradle添加遠程依賴
1 buildscript { 2 repositories { 3 jcenter() 4 } 5 dependencies { 6 classpath 'com.android.tools.build:gradle:2.3.2' 7 // TinkerPatch 插件 8 classpath "com.tinkerpatch.sdk:tinkerpatch-gradle-plugin:1.1.8" 9 // NOTE: Do not place your application dependencies here; they belong 10 // in the individual module build.gradle files 11 } 12 }
第二步 集成 TinkerPatch SDK
在app中的gradle添加denpendencies 依賴,注意:這兩個gradle不是同一個gradle,上面的那個build gradle 是整個項目的,下面這個build gradle是在app里面的,注意區分
1 dependencies { 2 // 若使用annotation需要單獨引用,對於tinker的其他庫都無需再引用 3 provided("com.tinkerpatch.tinker:tinker-android-anno:1.8.0") 4 compile("com.tinkerpatch.sdk:tinkerpatch-android-sdk:1.1.8") 5 }
注意,若使用 annotation 自動生成 Application, 需要單獨引入 Tinker 的 tinker-android-anno 庫。除此之外,我們無需再單獨引入 tinker 的其他庫。
為了簡單方便,我們將 TinkerPatch 相關的配置都放於 tinkerpatch.gradle 中, 我們需要將其引入:
1 apply from: 'tinkerpatch.gradle'
第三步 配置 tinkerpatchSupport 參數
好了,接下來我們就在app目錄下創建這個tinkerpatch.gradle,如圖所示的目錄
打開tinkerpatch.gradle,將 TinkerPatch 相關的配置都放於tinkerpatch.gradle中。
1 apply plugin: 'tinkerpatch-support' 2 3 /** 4 * TODO: 請按自己的需求修改為適應自己工程的參數 5 */ 6 7 //基包路徑 8 def bakPath = file("${buildDir}/bakApk/") 9 //基包文件夾名(打補丁包的時候,需要修改) 10 def baseInfo = "app-1.0.1-1018-17-29-31" 11 //版本名稱 12 def variantName = "debug" 13 14 /** 15 * 對於插件各參數的詳細解析請參考 16 * 17 */ 18 tinkerpatchSupport { 19 //可以在debug的時候關閉 tinkerPatch 20 tinkerEnable = true 21 //是否使用一鍵接入功能 默認為false 是否反射 Application 實現一鍵接入; 22 // 一般來說,接入 Tinker 我們需要改造我們的 Application, 若這里為 true, 即我們無需對應用做任何改造即可接入。 23 reflectApplication = true 24 //將每次編譯產生的 apk/mapping.txt/R.txt 歸檔存儲的位置 25 autoBackupApkPath = "${bakPath}" 26 appKey = "這里的appKey修改成你自己的appkey"// 注意!!! 需要修改成你的appkey 27 28 /** 注意: 若發布新的全量包, appVersion一定要更新 **/ 29 appVersion = "1.0.1" 30 31 def pathPrefix = "${bakPath}/${baseInfo}/${variantName}/" 32 def name = "${project.name}-${variantName}" 33 /** 34 * 基准包的文件路徑, 對應 tinker 插件中的 oldApk 參數;編譯補丁包時, 35 * 必需指定基准版本的 apk,默認值為空,則表示不是進行補丁包的編譯 36 */ 37 baseApkFile = "${pathPrefix}/${name}.apk" 38 39 /** 40 * 基准包的 Proguard mapping.txt 文件路徑, 對應 tinker 插件 applyMapping 參數;在編譯新的 apk 時候, 41 * 我們希望通過保持基准 apk 的 proguard 混淆方式, 42 * 從而減少補丁包的大小。這是強烈推薦的,編譯補丁包時,我們推薦輸入基准 apk 生成的 mapping.txt 文件。 43 */ 44 baseProguardMappingFile = "${pathPrefix}/${name}-mapping.txt" 45 /** 46 * 基准包的資源 R.txt 文件路徑, 對應 tinker 插件 applyResourceMapping 參數;在編譯新的apk時候, 47 * 我們希望通基准 apk 的 R.txt 文件來保持 Resource Id 的分配,這樣不僅可以減少補丁包的大小, 48 * 同時也避免由於 Resource Id 改變導致 remote view 異常 49 */ 50 baseResourceRFile = "${pathPrefix}/${name}-R.txt" 51 /** 52 * 若有編譯多flavors需求, 可以參照: https://github.com/TinkerPatch/tinkerpatch-flavors-sample 53 * 注意: 除非你不同的flavor代碼是不一樣的,不然建議采用zip comment或者文件方式生成渠道信息(相關工具:walle 或者 packer-ng) 54 **/ 55 } 56 57 /** 58 * 用於用戶在代碼中判斷tinkerPatch是否被使能 59 */ 60 android { 61 defaultConfig { 62 buildConfigField "boolean", "TINKER_ENABLE", "${tinkerpatchSupport.tinkerEnable}" 63 } 64 } 65 /** 66 * 一般來說,我們無需對下面的參數做任何的修改 67 * 對於各參數的詳細介紹請參考: 68 * https://github.com/Tencent/tinker/wiki/Tinker-%E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97 69 */ 70 tinkerPatch { 71 ignoreWarning = false 72 useSign = true //是否需要簽名,打正式包如果這里是true,則要配置簽名,否則會編譯不過去 73 dex { 74 dexMode = "jar" 75 pattern = ["classes*.dex"] 76 loader = [] 77 } 78 lib { 79 pattern = ["lib/*/*.so"] 80 } 81 82 res { 83 pattern = ["res/*", "r/*", "assets/*", "resources.arsc", "AndroidManifest.xml"] 84 ignoreChange = [] 85 largeModSize = 100 86 } 87 packageConfig { 88 } 89 sevenZip { 90 zipArtifact = "com.tencent.mm:SevenZip:1.1.10" 91 // path = "/usr/local/bin/7za" 92 } 93 buildConfig { 94 keepDexApply = false 95 } 96 }
簡單的說一下這里的參數配置:如圖
這里還需要注意一個地方,就是appKey,在我們登錄tinker的官網,並且添加一個app版本以后,都會生成一個appkey,我們要把自己的appkey填到上面的配置中,附上一張圖
第四步 初始化 TinkerPatch SDK
官方給了我們兩種方式來初始化TinkerPatch SDK,第一種是reflectApplication = true 的情況,另一種reflectApplication = false的情況,今天我們說reflectApplication = true這種情況,另一種情況,大家可以到官網中看看
簡單來說一下這兩種情況的區別啊,當reflectApplication = true這種情況是不需要更改我們項目的Application類,而reflectApplication = false的情況是需要改動Application這個類。
初始化TinkerPatch SDK
1 package com.example.administrator.tinkerdemotest; 2 3 import android.app.Application; 4 5 import com.tencent.tinker.loader.app.ApplicationLike; 6 import com.tinkerpatch.sdk.TinkerPatch; 7 import com.tinkerpatch.sdk.loader.TinkerPatchApplicationLike; 8 9 /** 10 * Created by Administrator on 2017/10/18 0018. 11 * 12 * reflectApplication = true 時 13 */ 14 15 public class tinkerApplication extends Application { 16 17 private ApplicationLike tinkerApplicationLike; 18 @Override 19 public void onCreate() { 20 super.onCreate(); 21 22 if (BuildConfig.TINKER_ENABLE) { 23 // 我們可以從這里獲得Tinker加載過程的信息 24 tinkerApplicationLike = TinkerPatchApplicationLike.getTinkerPatchApplicationLike(); 25 26 // 初始化TinkerPatch SDK, 更多配置可參照API章節中的,初始化SDK 27 TinkerPatch.init(tinkerApplicationLike) 28 .reflectPatchLibrary() 29 .setPatchRollbackOnScreenOff(true) 30 .setPatchRestartOnSrceenOff(true); 31 32 // 每隔3個小時去訪問后台時候有更新,通過handler實現輪訓的效果 33 new FetchPatchHandler().fetchPatchWithInterval(3); 34 } 35 } 36 }
最后在加上FetchPatchHandler這個類
1 package com.example.administrator.tinkerdemotest; 2 3 4 import android.os.Handler; 5 import android.os.Message; 6 7 import com.tinkerpatch.sdk.TinkerPatch; 8 9 /** 10 * Created by Administrator on 2017/10/18 0018. 11 */ 12 13 public class FetchPatchHandler extends Handler { 14 15 public static final long HOUR_INTERVAL = 3600 * 1000; 16 private long checkInterval; 17 18 19 /** 20 * 通過handler, 達到按照時間間隔輪訓的效果 21 */ 22 public void fetchPatchWithInterval(int hour) { 23 //設置TinkerPatch的時間間隔 24 TinkerPatch.with().setFetchPatchIntervalByHours(hour); 25 checkInterval = hour * HOUR_INTERVAL; 26 //立刻嘗試去訪問,檢查是否有更新 27 sendEmptyMessage(0); 28 } 29 30 31 @Override 32 public void handleMessage(Message msg) { 33 super.handleMessage(msg); 34 //這里使用false即可 35 TinkerPatch.with().fetchPatchUpdate(false); 36 //每隔一段時間都去訪問后台, 增加10分鍾的buffer時間 37 sendEmptyMessageDelayed(0, checkInterval + 10 * 60 * 1000); 38 } 39 }
最后將AndroidManifest.xml中的添加上相應的網絡和SD的權限,還要在application中加上 android:name=".tinkerApplication",附上代碼:
1 <?xml version="1.0" encoding="utf-8"?> 2 <manifest xmlns:android="http://schemas.android.com/apk/res/android" 3 package="com.example.administrator.tinkerdemotest"> 4 5 <uses-permission android:name="android.permission.INTERNET"/> 6 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> 7 <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> 8 9 10 <application 11 android:allowBackup="true" 12 android:icon="@mipmap/ic_launcher" 13 android:label="@string/app_name" 14 android:roundIcon="@mipmap/ic_launcher_round" 15 android:name=".tinkerApplication" 16 android:supportsRtl="true" 17 android:theme="@style/AppTheme"> 18 <activity android:name=".MainActivity"> 19 <intent-filter> 20 <action android:name="android.intent.action.MAIN" /> 21 22 <category android:name="android.intent.category.LAUNCHER" /> 23 </intent-filter> 24 </activity> 25 </application> 26 27 </manifest>
好了,到這里,集成Tinker的代碼基本上已經完成了,現在運行看看如圖
雙擊assembleDebug就可以運行項目了,結果如圖:
然后在app-->build下有個app-debug.apk的包,我們將這個包安裝到手機上,就相當於是我們本地的應用,在里面我加了一句話,如圖
接着我們打有補丁的包,打補丁的包之前,我們的改兩個地方,在tinkerpatch.gradle中找到如圖中的代碼,我們需要將baseInfo,和variantName改成和自己的一直如圖:
改完以后,我們就可以打補丁的包了,如圖
將baseInfo的值改為 右圖中1所對應的值,variantName的值改為右圖2對應的值。改完以后,我們就可以打補丁的包了,如圖
然后在build --> outputs --> tinkerPatch 中就可以找到我們的補丁的包了,如圖
最后將我們的補丁包上傳到tinker官網,就可以使用了,如圖
最后附上一條動態的效果圖。注意:一定要把app進程殺死,才會有效果。
源碼鏈接:https://github.com/343661629/Tinker