前言
之前研究Sophix的,之後爲了對比又想着接入微信的Tinker,所以有了本篇文章。Tinker是微信官方的Android熱補丁解決方案,它支持動態下發代碼、So庫以及資源,讓應用能夠在不需要重新安裝的情況下實現更新。當然,你也可以使用Tinker來更新你的插件。本文就對Tinker的接入做簡單的介紹,具體使用情況需參考公司具體業務。另一篇關於接入Sophix如下,作爲對比使用。
Android——Sophix熱修復接入
Tinker修復優缺點
1.1 優點
1.2 缺點
由於原理與系統限制,Tinker有以下已知問題:
- 修復部分冷啓動方能生效
- Tinker不支持修改AndroidManifest.xml,Tinker不支持新增四大組件
- 由於Google Play的開發者條款限制,不建議在GP渠道動態更新代碼;
- 在Android N上,補丁對應用啓動時間有輕微的影響;
- 不支持部分三星android-21機型,加載補丁時會主動拋出"TinkerRuntimeException:checkDexInstall failed";
- 對於資源替換,不支持修改remoteView。例如transition動畫,notification icon以及桌面圖標。
上面的介紹摘選自Tinker官方文檔,如需要更加詳細文檔,訪問:Tinker官方地址。在對Tinker有個簡單瞭解後,下面我們就開始在項目中一步步集成Tinker了。
創建項目
進入Tinker熱修復官網,登錄賬號之後點擊上部的tab進入我的app欄目下,如下點擊創建項目即可,創建項目的包名要與使用Tinker熱修復項目一致。
項目接入Tinker
2.1 Project的build.gradle配置
dependencies {
classpath 'com.android.tools.build:gradle:2.3.0'
classpath "com.tinkerpatch.sdk:tinkerpatch-gradle-plugin:1.2.9"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
2.2 module級別build.gradle配置
module下build.gradle文件添加版本依賴:
dependencies {
// 若使用annotation需要單獨引用,對於tinker的其他庫都無需再引用
// compileOnly("com.tinkerpatch.tinker:tinker-android-anno:1.9.9")
implementation("com.tinkerpatch.sdk:tinkerpatch-android-sdk:1.2.9")
}
2.3權限配置
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
說明:
- 聯網權限是用於拉取阿里服務器補丁
2.4 Patch配置
複製官網的tinkerpatch.gradle文件到當前app下面,有些參數需要修改(更多的設置需查看官網文檔),並且當前app的build.gradle下添加:
apply from: 'tinkerpatch.gradle'
tinkerpatch.gradle文件具體如下:
apply plugin: 'tinkerpatch-support'
/**
* TODO: 請按自己的需求修改爲適應自己工程的參數
*/
def bakPath = file("${buildDir}/bakApk/")
def baseInfo = "app-1.0.2-1225-14-34-18"
def variantName = "release"
/**
* 對於插件各參數的詳細解析請參考
* http://tinkerpatch.com/Docs/SDK
*/
tinkerpatchSupport {
/** 可以在debug的時候關閉 tinkerPatch, isRelease() 可以判斷BuildType是否爲Release **/
tinkerEnable = true
reflectApplication = true
/**
* 是否開啓加固模式,只能在APK將要進行加固時使用,否則會patch失敗。
* 如果只在某個渠道使用了加固,可使用多flavors配置
**/
protectedApp = false
/**
* 實驗功能
* 補丁是否支持新增 Activity (新增Activity的exported屬性必須爲false)
**/
supportComponent = true
autoBackupApkPath = "${bakPath}"
appKey = "c2305b76bdc4e9b3"
/** 注意: 若發佈新的全量包, appVersion一定要更新 **/
appVersion = "1.0.2"
def pathPrefix = "${bakPath}/${baseInfo}/${variantName}/"
def name = "${project.name}-${variantName}"
baseApkFile = "${pathPrefix}/${name}.apk"
baseProguardMappingFile = "${pathPrefix}/${name}-mapping.txt"
baseResourceRFile = "${pathPrefix}/${name}-R.txt"
backupFileNameFormat = '${appName}-${variantName}'
}
/**
* 用於用戶在代碼中判斷tinkerPatch是否被使能
*/
android {
defaultConfig {
buildConfigField "boolean", "TINKER_ENABLE", "${tinkerpatchSupport.tinkerEnable}"
}
}
/**
* 一般來說,我們無需對下面的參數做任何的修改
* 對於各參數的詳細介紹請參考:
* https://github.com/Tencent/tinker/wiki/Tinker-%E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97
*/
tinkerPatch {
ignoreWarning = false
useSign = true
dex {
dexMode = "jar"
pattern = ["classes*.dex"]
loader = []
}
lib {
pattern = ["lib/*/*.so"]
}
res {
pattern = ["res/*", "r/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
ignoreChange = []
largeModSize = 100
}
packageConfig {
}
sevenZip {
zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
// path = "/usr/local/bin/7za"
}
buildConfig {
keepDexApply = false
}
}
說明:
- def bakPath 基包文件路徑,在app目錄下的build文件夾新創建一個bakApk文件夾下
- def baseInfo 基包文件名(打補丁包的時候,需要修改)後面帶着的是生成的時間(爲了確保唯一性)
- tinkerEnable 控制是release還是debug,處於debug狀態的時候就是關閉補丁狀態(默認爲true)
- reflectApplication 是否使用反射接入無需修改application(默認是true)
- protectedApp tinker對於加固兼容性不好(默認爲false)
- supportComponent 是否支持動態新增Activity,並且新增Activity的exported屬性必須爲false,否則不生效
- appKey 申請的appkey
- appVersion 每次打新的補丁一定要修改,否者服務器判斷此值是否需要下發補丁,儘量與versionName保持一致(便於之後版本管理)
- 如果簡單接入測試,只需配置上面appKey 和appVersion 即可
2.5 配置SDK參數
由於我們在tinkerpatch.gradle配置了reflectApplication =true,所以我們無需對application改造就可接入Tinker,如下:
public class SampleApplication extends Application {
private static final String TAG = "Tinker";
private ApplicationLike tinkerApplicationLike;
public SampleApplication() {
}
@Override
public void attachBaseContext(Context base) {
super.attachBaseContext(base);
//you must install multiDex whatever tinker is installed!
//MultiDex.install(base);
}
/**
* 由於在onCreate替換真正的Application,
* 我們建議在onCreate初始化TinkerPatch,而不是attachBaseContext
*/
@Override
public void onCreate() {
super.onCreate();
initTinkerPatch();
}
/**
* 我們需要確保至少對主進程跟patch進程初始化 TinkerPatch
*/
private void initTinkerPatch() {
// 我們可以從這裏獲得Tinker加載過程的信息
if (BuildConfig.TINKER_ENABLE) {
tinkerApplicationLike = TinkerPatchApplicationLike.getTinkerPatchApplicationLike();
// 初始化TinkerPatch SDK
TinkerPatch.init(
tinkerApplicationLike
// new TinkerPatch.Builder(tinkerApplicationLike)
// .requestLoader(new OkHttp3Loader())
// .build()
)
.reflectPatchLibrary()
.setPatchRollbackOnScreenOff(true)
.setPatchRestartOnSrceenOff(true)
.setFetchPatchIntervalByHours(3)
;
// 獲取當前的補丁版本
Log.e(TAG, "Current patch version is " + TinkerPatch.with().getPatchVersion());
// fetchPatchUpdateAndPollWithInterval 與 fetchPatchUpdate(false)
// 不同的是,會通過handler的方式去輪詢
TinkerPatch.with().fetchPatchUpdateAndPollWithInterval();
}
}
/**
* 在這裏給出TinkerPatch的所有接口解釋
* 更詳細的解釋請參考:http://tinkerpatch.com/Docs/api
*/
private void useSample() {
TinkerPatch.init(tinkerApplicationLike)
//是否自動反射Library路徑,無須手動加載補丁中的So文件
//注意,調用在反射接口之後才能生效,你也可以使用Tinker的方式加載Library
.reflectPatchLibrary()
//向後臺獲取是否有補丁包更新,默認的訪問間隔爲3個小時
//若參數爲true,即每次調用都會真正的訪問後臺配置
.fetchPatchUpdate(false)
//設置訪問後臺補丁包更新配置的時間間隔,默認爲3個小時
.setFetchPatchIntervalByHours(3)
//向後臺獲得動態配置,默認的訪問間隔爲3個小時
//若參數爲true,即每次調用都會真正的訪問後臺配置
.fetchDynamicConfig(new ConfigRequestCallback() {
@Override
public void onSuccess(HashMap<String, String> hashMap) {
}
@Override
public void onFail(Exception e) {
}
}, false)
//設置訪問後臺動態配置的時間間隔,默認爲3個小時
.setFetchDynamicConfigIntervalByHours(3)
//設置當前渠道號,對於某些渠道我們可能會想屏蔽補丁功能
//設置渠道後,我們就可以使用後臺的條件控制渠道更新
.setAppChannel("default")
//屏蔽部分渠道的補丁功能
.addIgnoreAppChannel("googleplay")
//設置tinkerpatch平臺的條件下發參數
.setPatchCondition("test", "1")
//設置補丁合成成功後,鎖屏重啓程序
//默認是等應用自然重啓
.setPatchRestartOnSrceenOff(true)
//我們可以通過ResultCallBack設置對合成後的回調
//例如彈框什麼
//注意,setPatchResultCallback 的回調是運行在 intentService 的線程中
.setPatchResultCallback(new ResultCallBack() {
@Override
public void onPatchResult(PatchResult patchResult) {
Log.i(TAG, "onPatchResult callback here");
}
})
//設置收到後臺回退要求時,鎖屏清除補丁
//默認是等主進程重啓時自動清除
.setPatchRollbackOnScreenOff(true)
//我們可以通過RollbackCallBack設置對回退時的回調
.setPatchRollBackCallback(new RollbackCallBack() {
@Override
public void onPatchRollback() {
Log.i(TAG, "onPatchRollback callback here");
}
});
}
/**
* 自定義Tinker類的高級用法, 使用更靈活,但是需要對tinker有更進一步的瞭解
* 更詳細的解釋請參考:http://tinkerpatch.com/Docs/api
*/
private void complexSample() {
//修改tinker的構造函數,自定義類
TinkerPatch.Builder builder = new TinkerPatch.Builder(tinkerApplicationLike)
.listener(new DefaultPatchListener(this))
.loadReporter(new DefaultLoadReporter(this))
.patchReporter(new DefaultPatchReporter(this))
.resultServiceClass(TinkerServerResultService.class)
.upgradePatch(new UpgradePatch())
.patchRequestCallback(new TinkerPatchRequestCallback());
//.requestLoader(new OkHttpLoader());
TinkerPatch.init(builder.build());
}
}
測試Tinker補丁
3.1 生成基包
每次開發完成後,打開Studio右側的Gradle,選擇assemableRelease打正式包,這個過程是打基包,基包路徑在之前的tinkerpatch.gradle配置的,生成的基包安裝在手機上。
說明:
-
記得簽名和打開混淆
3.2 生成補丁
這裏修復的內容就是修改textview內容以及文字顏色,然後生成的基包名稱賦值到tinkerpatch.gradle文件夾下的baseInfo,接着打開Gradle,選擇tinkerPatchRelease進行打補丁
進過gradle之後生成的補丁包位於 build/outputs/tinkerPatch 下,這裏只需要用到patch_signed_7zip.apk
3.3 測試補丁
3.3.1 添加版本
注意此處的版本號一定要與tinkerpatch.gradle文件的appversion一致
3.3.2 發佈測試補丁
注意如果是正式項目,一定要做測試發佈,防止出現未可知的問題,如下圖,選擇補丁文件和描述,然後記得選擇開發預覽之後再提交當前補丁
3.3.3 Tinker調試工具調試補丁
由於 Tinker 與代碼相關,我們不能通過在代碼設置是否爲 debug 模式。這裏我們提供了 debug 調試工具,它的 Github 地址爲 tinkerpatch-debug-tool。 我們也可以通過點擊此鏈接下載。
說明:
- 當測試手機已經安裝基包和發佈測試補丁時打開開關即可完成測試補丁
3.3.4 調試結果
- 如果測試通過,則可以再次通過編輯當前版本測試不同的發佈測試
注意事項
- 最新Tinker支持新增Activity,需要tinkerpatch.gradle修改supportComponent = true,並且新增Activity的exported屬性必須爲false
- 修復補丁之後必須冷啓動才能生效
- Tinker對於Android N版本存在兼容性問題
- 對於使用assemble方式生成基包,儘量選擇關閉Instant run方式編譯
- 具體更多接入細節請參考Tinker接入文檔