Android開發之Tinker的集成和使用介紹。
對於熱修復我相信很多小夥伴都已經知道它們普遍的操作套路,Tinker主要是依賴自己的gradlePlugin生成拆分包,所以其拆分包的生成就由Gradle來完成,當然也可以通過命令行的方式,這裡就不對命令行做講解,Tinker接入指南
項目結構
Tinker介紹
來自Tinker官方
1、優點
2、缺點
Tinker不支持修改AndroidManifest.xml,Tinker不支持新增四大組件(1.9.0支持新增非export的Activity); 由於Google Play的開發者條款限制,不建議在GP渠道動態更新代碼; 在Android N上,補丁對應用啟動時間有輕微的影響; 不支持部分三星android-21機型,加載補丁時會主動拋出”TinkerRuntimeException:checkDexInstall failed”; 對於資源替換,不支持修改remoteView。例如transition動畫,notification icon以及桌面圖標。
Tinker集成
1、在項目的build.gradle中,添加依賴
buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:2.3.3' // Tinker classpath ("com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}") } }
這裡的TINKER_VERSION寫在項目gradle.properties中
TINKER_VERSION=1.7.7
2、在app的build.gradle文件,添加依賴
provided("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") compile("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") compile "com.android.support:multidex:1.0.1"
添加依賴以後,我們在gradle文件中做以下配置
開啟Multidex、配置Java編譯的版本 配置簽名文件,為瞭後面打包方便調試 引入另一個gradle文件專門來對Tinker生成拆分包的配置(由於多渠道要用到gradle的參數,所以將引入放在末尾)
apply plugin: 'com.android.application' android { compileSdkVersion 25 buildToolsVersion "25.0.3" defaultConfig { applicationId "com.handsome.thinker" minSdkVersion 16 targetSdkVersion 25 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" multiDexEnabled true } compileOptions { sourceCompatibility JavaVersion.VERSION_1_7 targetCompatibility JavaVersion.VERSION_1_7 } dexOptions { jumboMode = true } signingConfigs { debug { keyAlias 'hensen' keyPassword '123456' storeFile file("../Hensen.jks") storePassword '123456' } release { keyAlias 'hensen' keyPassword '123456' storeFile file("../Hensen.jks") storePassword '123456' } } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' signingConfig signingConfigs.release } debug { minifyEnabled false signingConfig signingConfigs.debug } } } // 加入Tinker生成補丁包的gradle apply from: 'buildTinker.gradle'
3、buildTinker.gradle是專門為Tinker配置和生成拆分包而寫的,具體可以參考官方gradle
//指定生成apk文件的存放位置 def bakPath = file("${buildDir}/bakApk/") //參數配置 ext { //開啟Tinker tinkerEnable = true //舊的apk位置,需要我們手動指定 tinkerOldApkPath = "${bakPath}/" //舊的混淆映射位置,如果開啟瞭混淆,則需要我們手動指定 tinkerApplyMappingPath = "${bakPath}/" //舊的resource位置,需要我們手動指定 tinkerApplyResourcePath = "${bakPath}/" tinkerID = "1.0" } def buildWithTinker() { return ext.tinkerEnable } def getOldApkPath() { return ext.tinkerOldApkPath } def getApplyMappingPath() { return ext.tinkerApplyMappingPath } def getApplyResourceMappingPath(){ return ext.tinkerApplyResourcePath } def getTinkerIdValue(){ return ext.tinkerID } if (buildWithTinker()) { apply plugin: 'com.tencent.tinker.patch' tinkerPatch { oldApk = getOldApkPath() //指定old apk文件路徑 ignoreWarning = false //不忽略tinker警告,出現警告則中止patch文件生成 useSign = true //patch文件必須是簽名後的 tinkerEnable = buildWithTinker() //指定是否啟用tinker buildConfig { applyMapping = getApplyMappingPath() //指定old apk打包時所使用的混淆文件 applyResourceMapping = getApplyResourceMappingPath() //指定old apk的資源文件 tinkerId = getTinkerIdValue() //指定TinkerID keepDexApply = false } dex { dexMode = "jar" //jar、raw pattern = ["classes*.dex", "assets/secondary-dex-?.jar"] //指定dex文件目錄 loader = ["com.handsome.thinker.AppLike.MyTinkerApplication"] //指定加載patch文件時用到的類 } lib { pattern = ["libs/*/*.so"] //指定so文件目錄 } res { pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"] //指定資源文件目錄 ignoreChange = ["assets/sample_meta.txt"] //指定不受影響的資源路徑 largeModSize = 100 //資源修改大小默認值 } packageConfig { configField("patchMessage", "fix the 1.0 version's bugs") configField("patchVersion", "1.0") } } /** * 是否配置瞭多渠道 */ List flavors = new ArrayList<>(); project.android.productFlavors.each { flavor -> flavors.add(flavor.name) } boolean hasFlavors = flavors.size() > 0 /** * 復制apk包和其它必須文件到指定目錄 */ android.applicationVariants.all { variant -> /** * task type, you want to bak */ def taskName = variant.name def date = new Date().format("yyyy-MM-dd-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") } } } } } } }
4、記得開啟Manifest權限,否則生成拆分包的時候有奇怪錯誤
Tinker封裝
我們提供兩個方法來初始化Tinker
默認的方式 自定義模塊的方式
public class TinkerManager { private static boolean isInstalled = false; // 這裡的ApplicationLike可以理解為Application的載體 private static ApplicationLike mAppLike; private static CustomPatchListener mPatchListener; /** * 默認初始化Tinker * * @param applicationLike */ public static void installTinker(ApplicationLike applicationLike) { mAppLike = applicationLike; if (isInstalled) { return; } TinkerInstaller.install(mAppLike); isInstalled = true; } /** * 初始化Tinker,帶有自定義模塊 *
* 1、CustomPatchListener * 2、CustomResultService * * @param applicationLike * @param md5Value 服務器下發的md5 */ public static void installTinker(ApplicationLike applicationLike, String md5Value) { mAppLike = applicationLike; if (isInstalled) { return; } mPatchListener = new CustomPatchListener(getApplicationContext()); mPatchListener.setCurrentMD5(md5Value); // Load補丁包時候的監聽 LoadReporter loadReporter = new DefaultLoadReporter(getApplicationContext()); // 補丁包加載時候的監聽 PatchReporter patchReporter = new DefaultPatchReporter(getApplicationContext()); AbstractPatch upgradePatchProcessor = new UpgradePatch(); TinkerInstaller.install(applicationLike, loadReporter, patchReporter, mPatchListener, CustomResultService.class, upgradePatchProcessor); isInstalled = true; } /** * 增加補丁包 * * @param path */ public static void addPatch(String path) { if (Tinker.isTinkerInstalled()) { TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), path); } } /** * 獲取上下文 * * @return */ private static Context getApplicationContext() { if (mAppLike != null) { return mAppLike.getApplication().getApplicationContext(); } return null; } }
由於Tinker默認Patch檢查是沒有對文件做Md5校驗,我們可以重寫其檢驗的方法,加上我們自己的檢驗邏輯(需要自定義模塊的方式初始化Tinker)
CustomPatchListener.java
public class CustomPatchListener extends DefaultPatchListener { private String currentMD5; public void setCurrentMD5(String md5Value) { this.currentMD5 = md5Value; } public CustomPatchListener(Context context) { super(context); } /** * patch的檢測 * * @param path * @return */ @Override protected int patchCheck(String path) { //MD5校驗的工具可以網上查找 //這裡要求我們在初始化Tinker的時候加上MD5的參數 //增加patch文件的md5較驗 if (!MD5Utils.isFileMD5Matched(path, currentMD5)) { return ShareConstants.ERROR_PATCH_DISABLE; } return super.patchCheck(path); } }
由於Tinker默認安裝完補丁包之後是刪除補丁包,然後殺掉進程的方式,我們可以修改殺掉進程的行為(需要自定義模塊的方式初始化Tinker)
CustomResultService.java
public class CustomResultService extends DefaultTinkerResultService { private static final String TAG = "Tinker.SampleResultService"; /** * patch文件的最終安裝結果,默認是安裝完成後殺掉自己進程 * 此段代碼主要是復制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) { //刪除patch包 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使用
1、Tinker的使用需要ApplicationLike來生成我們的Application,然後初始化Multidex和Tinker
@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); MultiDex.install(base); TinkerManager.installTinker(this); } }
2、編譯項目自動生成Application,然後在Manifest中指定我們的生成的Application
3、在主頁面按鈕的點擊事件,來加載放在緩存目錄下的補丁包
public class MainActivity extends AppCompatActivity { private String mPath; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mPath = getExternalCacheDir().getAbsolutePath() + File.separatorChar; } /** * 加載Tinker補丁 * * @param view */ public void Fix(View view) { File patchFile = new File(mPath, "patch_signed.apk"); if (patchFile.exists()) { TinkerManager.addPatch(patchFile.getAbsolutePath()); Toast.makeText(this, "File Exists,Please wait a moment ", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(this, "File No Exists", Toast.LENGTH_SHORT).show(); } } }Tinker測試
完成Tinker的所有準備工作後,我們通過默認的初始化Tinker方式測試我們的補丁包
1、找到gradle工具欄,點擊生成Release包,作為1.0版本的程序
2、將生成的Release包Push到手機上,安裝,運行程序
生成apk的目錄在build的bakApk目錄下
運行程序
3、在項目中,對主界面添加加載圖片的按鈕,同時添加一個drawable文件
public class MainActivity extends AppCompatActivity { private String mPath; private ImageView iv; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); iv = (ImageView) findViewById(R.id.iv); mPath = getExternalCacheDir().getAbsolutePath() + File.separatorChar; } /** * 加載Tinker補丁 * * @param view */ public void Fix(View view) { File patchFile = new File(mPath, "patch_signed.apk"); if (patchFile.exists()) { TinkerManager.addPatch(patchFile.getAbsolutePath()); Toast.makeText(this, "File Exists,Please wait a moment ", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(this, "File No Exists", Toast.LENGTH_SHORT).show(); } } /** * 新增的按鈕點擊事件 * * @param view */ public void Load(View view) { iv.setImageResource(R.drawable.bg_content_header); } }4、同時記得修改buildTinker.gradle的old安裝包的路徑,Tinker需要比對前後安裝包然後生成補丁包
//參數配置 ext { //開啟Tinker tinkerEnable = true //舊的apk位置,需要我們手動指定 tinkerOldApkPath = "${bakPath}/app-release-2017-11-19-18-34-12.apk" //舊的混淆映射位置,如果開啟瞭混淆,則需要我們手動指定 tinkerApplyMappingPath = "${bakPath}/" //舊的resource位置,需要我們手動指定 tinkerApplyResourcePath = "${bakPath}/app-release-2017-11-19-18-34-12-R.txt" tinkerID = "1.0" }5、找到gradle工具欄,點擊thinker生成Release補丁包,作為1.0版本的補丁
6、將生成的Release補丁包Push到手機的緩存目錄上,運行程序點擊修復補丁包,稍等數秒程序會被殺掉,重啟點擊加載圖片按鈕
生成的補丁包
記得將補丁放到緩存目錄下,修復補丁後的程序
Tinker多渠道打包
1、Tinker支持多渠道打包,我們采用友盟的打包方式,下載友盟SDK,將jar包增加到項目上
2、初始化友盟SDK(新版本的SDK似乎不用初始化瞭,找不到初始化入口)
3、Manifest增加友盟的AppKey配置和渠道配置
4、在app的build.gradle中增加多渠道打包信息
/** * 配置多渠道 */ productFlavors { googleplayer { manifestPlaceholders = [UMENG_CHANNEL_VALUE: "googleplayer"] } baidu { manifestPlaceholders = [UMENG_CHANNEL_VALUE: "baidu"] } wangdoujia { manifestPlaceholders = [UMENG_CHANNEL_VALUE: "wangdoujia"] } productFlavors.all { flavor -> flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name] } }5、在buildTinker.gradle增加配置多渠道補丁包的生成規則
/** * 生成多渠道補丁包 */ 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" } } } 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" } } } } }6、找到gradle工具欄,點擊生成Release包,作為1.0版本的程序
7、同時記得修改buildTinker.gradle的old安裝包的路徑,Tinker需要比對前後安裝包然後生成補丁包
//參數配置 ext { //開啟Tinker tinkerEnable = true //舊的apk位置,需要我們手動指定 tinkerOldApkPath = "${bakPath}/app-2017-11-19-20-35-23" //舊的混淆映射位置,如果開啟瞭混淆,則需要我們手動指定 tinkerApplyMappingPath = "${bakPath}/app-2017-11-19-20-35-23" //舊的resource位置,需要我們手動指定 tinkerApplyResourcePath = "${bakPath}/app-2017-11-19-20-35-23" //舊的多渠道位置,需要我們手動指定 tinkerBuildFlavorDirectory = "${bakPath}/app-2017-11-19-20-35-23" tinkerID = "1.0" }8、找到gradle工具欄,點擊thinker生成Release補丁包,作為1.0版本的補丁
這裡對程序的修改就省略瞭
後面的測試更上面一樣,也就省略瞭。