Tinker 熱補丁接入過程中的坑!!!
===============
Tinker 介紹
gradle 接入
gradle是推薦的接入方式,在gradle插件tinker-patch-gradle-plugin中我們幫你完成proguard、multiDex以及Manifest處理等工作。
添加gradle依賴
在項目的根目錄build.gradle中,添加tinker-patch-gradle-plugin的依賴
引入tinker 核心庫
然后在baseUI-lib文件的build.gradle,我們需要添加tinker的庫依賴以及apply tinker的gradle插件.
在APP/build.gradle 下面添加tinker 的配置文件
keep_in_main_dex.txt 文件內容就是指定你要放置到主DEX 中的類
-keep public class * implements com.tencent.tinker.loader.app.ApplicationLifeCycle {
*;
}
-keep public class * extends com.tencent.tinker.loader.TinkerLoader {
*;
}
-keep public class * extends com.tencent.tinker.loader.app.TinkerApplication {
*
}
-keep class com.tencent.tinker.loader.** {
*;
}
-keep class com.anzogame.corelib.GameApplication {
*;
}
加入tinker EXT提供配置文件
tinkerEnabled: tinker 的開關
tinkerOldApkPath : 生補丁包的基礎apk,線上包版本
tinkerApplyMappingPath : 線上包混淆文件生成mapping 文件
tinkerApplyResourcePath : 線上包的資源id 文件
以上版本是我們每一個正式版本需要保留的生成熱補丁的基礎信息
tinkerBuildFlavorDirectory : 多渠道用到,我們不用,下面說到我們怎么處理多渠道打包
加入 TinkerPatch 配置
if (buildWithTinker()) {
apply plugin: 'com.tencent.tinker.patch'
tinkerPatch {
/**
* necessary,default 'null'
* the old apk path, use to diff with the new apk to build
* add apk from the build/bakApk
*/
oldApk = getOldApkPath()
/**
* optional,default 'false'
* there are some cases we may get some warnings
* if ignoreWarning is true, we would just assert the patch process
* case 1: minSdkVersion is below 14, but you are using dexMode with raw.
* it must be crash when load.
* case 2: newly added Android Component in AndroidManifest.xml,
* it must be crash when load.
* case 3: loader classes in dex.loader{} are not keep in the main dex,
* it must be let tinker not work.
* case 4: loader classes in dex.loader{} changes,
* loader classes is ues to load patch dex. it is useless to change them.
* it won't crash, but these changes can't effect. you may ignore it
* case 5: resources.arsc has changed, but we don't use applyResourceMapping to build
*/
ignoreWarning = false
/**
* optional,default 'true'
* whether sign the patch file
* if not, you must do yourself. otherwise it can't check success during the patch loading
* we will use the sign config with your build type
*/
useSign = true
/**
* Warning, applyMapping will affect the normal android build!
*/
buildConfig {
/**
* optional,default 'null'
* if we use tinkerPatch to build the patch apk, you'd better to apply the old
* apk mapping file if minifyEnabled is enable!
* Warning:
* you must be careful that it will affect the normal assemble build!
*/
applyMapping = getApplyMappingPath()
/**
* optional,default 'null'
* It is nice to keep the resource id from R.txt file to reduce java changes
*/
applyResourceMapping = getApplyResourceMappingPath()
/**
* necessary,default 'null'
* because we don't want to check the base apk with md5 in the runtime(it is slow)
* tinkerId is use to identify the unique base apk when the patch is tried to apply.
* we can use git rev, svn rev or simply versionCode.
* we will gen the tinkerId in your manifest automatic
*/
tinkerId = "1.2"
}
dex {
/**
* optional,default 'jar'
* only can be 'raw' or 'jar'. for raw, we would keep its original format
* for jar, we would repack dexes with zip format.
* if you want to support below 14, you must use jar
* or you want to save rom or check quicker, you can use raw mode also
*/
dexMode = "jar"
/**
* optional,default 'false'
* if usePreGeneratedPatchDex is true, tinker framework will generate auxiliary class
* and insert auxiliary instruction when compiling base package using
* assemble{Debug/Release} task to prevent class pre-verified issue in dvm.
* Besides, a real dex file contains necessary class will be generated and packed into
* patch package instead of any patch info files.
*
* Use this mode if you have to use any dex encryption solutions.
*
* Notice: If you change this value, please trigger clean task
* and regenerate base package.
*/
usePreGeneratedPatchDex = false
/**
* necessary,default '[]'
* what dexes in apk are expected to deal with tinkerPatch
* it support * or ? pattern.
*/
pattern = ["classes*.dex",
"assets/secondary-dex-?.jar"]
/**
* necessary,default '[]'
* Warning, it is very very important, loader classes can't change with patch.
* thus, they will be removed from patch dexes.
* you must put the following class into main dex.
* Simply, you should add your own application {@code tinker.sample.android.SampleApplication}
* own tinkerLoader, and the classes you use in them
*
*/
loader = ["com.tencent.tinker.loader.*",
//warning, you must change it with your application
"com.anzogame.corelib.GameApplication",
"com.anzogame.corelib.BuildConfig.BaseBuildInfo"
]
}
lib {
/**
* optional,default '[]'
* what library in apk are expected to deal with tinkerPatch
* it support * or ? pattern.
* for library in assets, we would just recover them in the patch directory
* you can get them in TinkerLoadResult with Tinker
*/
pattern = ["lib/armeabi/*.so"]
}
res {
/**
* optional,default '[]'
* what resource in apk are expected to deal with tinkerPatch
* it support * or ? pattern.
* you must include all your resources in apk here,
* otherwise, they won't repack in the new apk resources.
*/
pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
/**
* optional,default '[]'
* the resource file exclude patterns, ignore add, delete or modify resource change
* it support * or ? pattern.
* Warning, we can only use for files no relative with resources.arsc
*/
// ignoreChange = ["*.png"]
ignoreChange = ["assets/sample_meta.txt"]
/**
* default 100kb
* for modify resource, if it is larger than 'largeModSize'
* we would like to use bsdiff algorithm to reduce patch file size
*/
largeModSize = 100
}
packageConfig {
/**
* optional,default 'TINKER_ID, TINKER_ID_VALUE' 'NEW_TINKER_ID, NEW_TINKER_ID_VALUE'
* package meta file gen. path is assets/package_meta.txt in patch file
* you can use securityCheck.getPackageProperties() in your ownPackageCheck method
* or TinkerLoadResult.getPackageConfigByName
* we will get the TINKER_ID from the old apk manifest for you automatic,
* other config files (such as patchMessage below)is not necessary
*/
configField("patchMessage", "tinker is sample to use")
/**
* just a sample case, you can use such as sdkVersion, brand, channel...
* you can parse it in the SamplePatchListener.
* Then you can use patch conditional!
*/
configField("platform", "all")
/**
* patch version via packageConfig
*/
configField("patchVersion", "1.0")
}
//or you can add config filed outside, or get meta value from old apk
//project.tinkerPatch.packageConfig.configField("test1", project.tinkerPatch.packageConfig.getMetaDataFromOldApk("Test"))
//project.tinkerPatch.packageConfig.configField("test2", "sample")
/**
* if you don't use zipArtifact or path, we just use 7za to try
*/
sevenZip {
/**
* optional,default '7za'
* the 7zip artifact path, it will use the right 7za with your platform
*/
zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
/**
* optional,default '7za'
* you can specify the 7za path yourself, it will overwrite the zipArtifact value
*/
// path = "/usr/local/bin/7za"
}
}
List<String> flavors = new ArrayList<>();
project.android.productFlavors.each { flavor ->
flavors.add(flavor.name)
}
boolean hasFlavors = flavors.size() > 0
/**
* bak apk and 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")
}
}
}
}
}
}
project.afterEvaluate {
//sample use for build all flavor for one time
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"
}
}
}
}
}
}
詳細參數參看文檔 傳送門
配置就到這里,然后執行assembleDebug,安裝apk,千萬不要 RUN ,RUN方式生成是補丁打補丁的,編譯不通過 彩蛋....(Too many classes in --main-dex-list, main dex capacity exceeded)
為什么會這樣子呢 ? 我們已經采用了GOOGLE 的方案多DEX ,從報錯上來看應該是主DEX 的類太多了,超過了限制,但是這個哪些類放到主DEX 不是我們決定的啊,很操蛋, 那么我們來看系統是如何分包的.
在項目中,可以直接運行 gradle 的 task 。
collect{flavor}{buildType}MultiDexComponents Task 。這個 task 是獲取 AndroidManifest.xml 中 Application 、Activity 、Service 、 Receiver 、 Provider 等相關類,以及 Annotation ,之后將內容寫到 build/intermediates/multi-dex/{flavor}/{buildType}/maindexlist.txt 文件中去。
packageAll{flavor}DebugClassesForMultiDex Task 。該 task 是將所有類打包成 jar 文件存在 build/intermediates/multi-dex/{flavor}/debug/allclasses.jar 。 當 BuildType 為 Release 的時候,執行的是 proguard{flavor}Release Task,該 task 將 proguard 混淆后的類打包成 jar 文件存在 build/intermediates/classes-proguard/{flavor}/release/classes.jar
shrink{flavor}{buildType}MultiDexComponents Task 。該 task 會根據 maindexlist.txt 生成 componentClasses.jar ,該 jar 包里面就只有 maindexlist.txt 里面的類,該 jar 包的位置在 build/intermediates/multi-dex/{flavor}/{buildType}/componentClasses.jar
create{flavor}{buildType}MainDexClassList Task 。該 task 會根據生成的 componentClasses.jar 去找這里面的所有的 class 中直接依賴的 class ,然后將內容寫到 build/intermediates/multi-dex/{flavor}/{buildType}/maindexlist.txt 中。最終這個文件里面列出來的類都會被分配到第一個 dex 里面。
通過上面的流程我們可以得出 ,我們主DEX 中的類取決于build/intermediates/multi-dex/{flavor}/{buildType}/maindexlist.txt 中的內容 ,那么我們在執行MultiDexComponents task 時候做些攔截,把Activity 從主DEX中移除,這里面的移除不是全部移除,如果Activity中包含有子類,那么我們的移除是無效,還是會被放入到主DEX,另外,如果你 Application 、Service 、 Receiver 、 Provider 中的直接引用類還是會被放到第一個主DEX中。
當我們采用多DEX 的時候,應用啟動的首先回加載主DEX ,其他的 dex 需要我們在應用啟動后進行動態加載安裝, 通過MultiDex.install(getApplication());加載其他DEX. 這樣的方法雖然可行,我們把 很多 Activity 放到其他的DEX 中了 ,可是生成包發現DEX 還是有9.2M 這種方式如果隨著代碼增加還是有可能導致主DEX 超限的問題,這時候我們可以采用dexnkife 通過配置形式把Application 中相關的類和一些必要的首頁類放置到主DEX 中,可以準確控制哪些放入到主DEX,這樣也可以解決問題。
那么Google 官方Multidex是如何加載的呢?
Google 官方支持 Multidex 的 jar 包是 android-support-multidex.jar,該 jar 包從 build tools 21.1 開始支持。這個 jar 加載 apk 中的從 dex 流程如下:
[圖片上傳失敗...(image-14a17f-1530665215929)]
此處主要的工作就是從 apk 中提取出所有的從 dex(classes2.dex,classes3.dex,…),然后通過反射依次安裝加載從 dex 并合并 DexPathList 的 Element 數組。
為什么API 21 以上就沒有主DEX 過大的問題呢?
- 這是為了5.0以上系統在安裝過程中的art階段就將所有的classes(..N).dex合并到一個單獨的oat文件(5.0以下只能苦逼的啟動時加載 對于Art相關知識,可以參考老羅的系列文章 傳送門
DEX類分包的規則
我們開啟多DEX支持一般是指定了multiDexEnabled,系統其實它利用的是Android sdk build tool中的mainDexClasses腳本,這在版本21以上才會有。使用方法非常很簡單:
mainDexClasses [--output <output file>] <application path>
該腳本要求輸入一個文件組(包含編譯后的目錄或jar包),然后分析文件組中的類并寫入到–output所指定的文件中。實現原理也不復雜,主要分為三步:
a. 環境檢查,包括傳入參數合法性檢查,路徑檢查以及proguard環境檢測等。
b. 使用mainDexClasses.rules規則,通過Proguard的shrink功能,裁剪無關類,生成一個tmp.jar包。
c. 通過生成的tmp jar包,調用MainDexListBuilder類生成主dex的文件列表。
這里只是簡單的得到所有入口類(即rules中的Instrumentation、application、Activity、Annotation等等)的直接引入類。何為直接引用類?在init過程,會在校驗階段去resolve它各個方法、變量引用到的類,這些類統稱為某個類的直接引用類。舉個栗子:
public class MainActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
DirectReferenceClass test = new DirectReferenceClass();
}
}
public class DirectReferenceClass {
public DirectReferenceClass() {
InDirectReferenceClass test = new InDirectReferenceClass();
}
}
public class InDirectReferenceClass {
public InDirectReferenceClass() {
}
}
上面有MainActivity、DirectReferenceClass、InDirectReferenceClass三個類,其中DirectReferenceClass是MainActivity的直接引用類,InDirectReferenceClass是DirectReferenceClass的直接引用類。而InDirectReferenceClass是MainActivity的間接引用類(即直接引用類的所有直接引用類)。
對于5.0以下的系統,我們需要在啟動時手動加載其他的dex。而我們并沒有要求得到所有的間接引用類,這是因為我們在attachBaseContext的時候,已將其他dex加載。
事實上,若我們在attachBaseContext中調用Multidex.install,我們只需引入Application的直接引用類即可,mainDexClasses將Activity、ContentProvider、Service等的直接引用類也引入,主要是滿足需要在非attachBaseContent加載多dex的需求。另一方面,若存在以下代碼,將出現NoClassDefFoundError錯誤。
public class HelloMultiDexApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
DirectReferenceClass test = new DirectReferenceClass();
MultiDex.install(this);
}
}
這是因為在實際運行過程中,DirectReferenceClass需要的InDirectReferenceClass并不一定在主dex。解決方法是手動將該類放于dx的-main-dex-list參數中:
afterEvaluate {
tasks.matching {
it.name.startsWith('dex')
}.each { dx ->
if (dx.additionalParameters == null) {
dx.additionalParameters = []
}
dx.additionalParameters += '--multi-dex'
dx.additionalParameters += "--main-dex-list=$projectDir/<filename>".toString()
}
}
LinearAlloc 是什么
- LinearAlloc 主要用來管理 Dalvik 中 class 加載時的內存,就是讓 App 在執行時減少系統內存的占用。在 App 的安裝過程中,系統會運行一個名為 dexopt 的程序為該應用在當前機型中運行做準備。dexopt 使用 LinearAlloc 來存儲應用的方法信息。App 在執行前會將 class 讀進 LinearAlloc 這個 buffer 中,這個 LinearAlloc 在 Android 2.3 之前是 4M 或 5M ,到 4.0 之后變為 8M 或 16M。因為 5M 實在是太小了,可能還沒有 65536 就已經超過 5M 了,什么意思呢,就是只有一個包的情況下也有可能出現 INSTALL_FAILED_DEXOPT ,原因就在于 LinearAlloc。
解決 LinearAlloc
DEXOPT && DEX2OAT 是什么?
dexopt
當 Android 系統安裝一個應用的時候,有一步是對 Dex 進行優化,這個過程有一個專門的工具來處理,叫 DexOpt。DexOpt 是在第一次加載 Dex 文件的時候執行的,將 dex 的依賴庫文件和一些輔助數據打包成 odex 文件,即 Optimised Dex,存放在 cache/dalvik_cache 目錄下。保存格式為 apk路徑 @ apk名 @ classes.dex 。執行 ODEX 的效率會比直接執行 Dex 文件的效率要高很多。
更多可查看 Dalvik Optimization and Verification With dexopt 。
dex2oat
Android Runtime 的 dex2oat 是將 dex 文件編譯成 oat 文件。而 oat 文件是 elf 文件,是可以在本地執行的文件,而 Android Runtime 替換掉了虛擬機讀取的字節碼轉而用本地可執行代碼,這就被叫做 AOT(ahead-of-time)。dex2oat 對所有 apk 進行編譯并保存在 dalvik-cache 目錄里。PackageManagerService 會持續掃描安裝目錄,如果有新的 App 安裝則馬上調用 dex2oat 進行編譯。
更多可查看 Android運行時ART簡要介紹和學習計劃 。
Application Not Responding
因為第一次運行(包括清除數據之后)的時候需要 dexopt ,然而 dexopt 是一個比較耗時的操作,同時 MultiDex.install() 操作是在 Application.attachBaseContext() 中進行的,占用的是UI線程。那么問題來了,當我的第二個包、第三個包很大的時候,程序就阻塞在 MultiDex.install() 這個地方了,一旦超過規定時間,那就 ANR 了。那怎么辦?放子線程?如果 Application 有一些初始化操作,到初始化操作的地方的時候都還沒有完成 install + dexopt 的話,那又會 NoClassDefFoundError 了嗎?同時 ClassLoader 放在哪個線程都讓主線程掛起。
微信/手Q加載方案
對于微信來說,我們一共有111052個方法。以線性內存3355444(限制5m,給系統預留部分)、方法數64K為限制,即當滿足任意一個條件時,將拆分dex。由此微信將得到一個主dex,兩個子dex,若微信采用Android方案,在首次啟動時將長期無響應(沒有出現黑屏時因為默認皮膚的原因),這對處女座的我來說是無法接受的。應該如何去做?微信與手Q的方案是類似的,將首次加載放于地球中,并用線程去加載(但是5.0之前加載dex時還是會掛起主線程)。
Dex形式
暫時我們還是放于assets下,以assets/secondary-program-dex-jars/secondary-N.dex.jar命名。為什么不以classes(..N).dex?這是因為一來覺得以Android的推廣速度,5.0用戶增長應該是遙遙無期的,二來加載Dex的代碼,傳進去的是zip,在加載前我們需要驗證MD5,確保所加載的Dex沒有被篡改(Android官方沒有驗證,主要是只有root才能更改吧)。
/**
- Makes an array of dex/resource path elements, one per element of
- the given array.
*/
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
ArrayList<IOException> suppressedExceptions) {
事實上,應該傳進去的是dex也是應該可以的,這塊在下一個版本將采用classes(..N).dex。但是如果我們使用了線程加載,并且彈出提示界面,對用戶來說并不是無法接受。
Dex類分包的規則
分包規則即將所有Application、ContentProvider以及所有export的Activity、Service、Receiver的間接依賴集都必須放在主dex。對于微信現在來說,這部分大約有41306個方法,每次通過掃描AndroidMifest計算耗時大約為20s不到。怎么計算?可以參考buck或者mainDexClasses的做法。
public MainDexListBuilder(String rootJar, String pathString) throws IOException {
path = new Path(pathString);
ClassReferenceListBuilder mainListBuilder=new ClassReferenceListBuilder(path);
加載Dex的方式
加載邏輯這邊主要判斷是否已經dexopt,若已經dexopt,即放在attachBaseContext加載,反之放于地球中用線程加載。怎么判斷?其實很低級,因為在微信中,若判斷revision改變,即將dex以及dexopt目錄清空。只需簡單判斷兩個目錄dex名稱、數量是否與配置文件的一致。
(name md5 校驗是否加載成功類)
secondary-1.dex.jar 63e5240eac9bdb5101fc35bd40a98679 secondary.dex01.Canary
secondary-2.dex.jar e7d2a4a181f579784a4286193feaf457 secondary.dex02.Canary
總的來說,這種方案用戶體驗較好,缺點在于太過復雜,每次都需重新掃描依賴集,而且使用的是比較大的間接依賴集(要真正運行到,直接依賴集是不行的)。當前微信必要的依賴集已經41306個方法,說不定哪一天就爆了。
FaceBook加載方案
那是否存在一種加載方式它的依賴集很小,但卻不會像官方方案一樣造成明顯的卡頓?逆過不少app,發現facebook的思路還是挺不錯的,下面作一個簡單的說明:
Dex形式
微信與facebook的dex形式是完全一致的,這是因為我們也是使用facebook開源工具buck編譯的。但是我們做了一個自動生成buck腳本的工作,即開發人員無須關心buck腳本如何編寫。
Dex類分包的規則
facebook將加載Dex的邏輯放于單獨的nodex進程,這是一個非常簡單、輕量級的進程。它沒有任何的ContentProvider,只有有限的幾個Activity、Service。
<activity android:exported="false" android:process=":nodex"
android:name="com.facebook.nodex.startup.splashscreen.NodexSplashActivity">
所以依賴集為Application、NodexSplashActivity的間接依賴集即可,而且這部分邏輯應該相對穩定,我們無須做動態掃描。這就實現了一個非常輕量級的依賴集方案。
加載Dex的方式
加載dex邏輯也非常簡單,由于NodexSplashActivity的intent-filter指定為Main與LAUNCHER。首先拉起nodex進程,然后初始化NodexSplashActivityActivity,若此時Dex已經初始化過,即直接跳轉到主頁面。
這種方式好處在于依賴集非常簡單,同時首次加載Dex時也不會卡死。但是它的缺點也很明顯,即每次啟動主進程時,都需先啟動nodex進程。盡管nodex進程邏輯非常簡單,這也需100ms以上。若微信對啟動時間非常敏感,很難會去采用這個方案。
美團加載方案
在 gradle 生成 dex 文件的這步中,自定義一個 task 來干預 dex 的生產過程,從而產生多個 dex 。
tasks.whenTaskAdded { task ->
if (task.name.startsWith('proguard') && (task.name.endsWith('Debug') || task.name.endsWith('Release'))) {
task.doLast {
makeDexFileAfterProguardJar();
}
task.doFirst {
delete "${project.buildDir}/intermediates/classes-proguard";
String flavor = task.name.substring('proguard'.length(), task.name.lastIndexOf(task.name.endsWith('Debug') ? "Debug" : "Release"));
generateMainIndexKeepList(flavor.toLowerCase());
}
} else if (task.name.startsWith('zipalign') && (task.name.endsWith('Debug') || task.name.endsWith('Release'))) {
task.doFirst {
ensureMultiDexInApk();
}
}
}
把 Service、Receiver、Provider 涉及到的代碼都放到主 dex 中,而把 Activity 涉及到的代碼進行了一定的拆分,把首頁 Activity、Laucher Activity 、歡迎頁的 Activity 、城市列表頁 Activity 等所依賴的 class 放到了主 dex 中,把二級、三級頁面的 Activity 以及業務頻道的代碼放到了第二個 dex 中,為了減少人工分析 class 的依賴所帶了的不可維護性和高風險性,美團編寫了一個能夠自動分析 class 依賴的腳本, 從而能夠保證?主 dex 包含 class 以及他們所依賴的所有 class 都在其內,這樣這個腳本就會在打包之前自動分析出啟動到主 dex 所涉及的所有代碼,保證主 dex 運行正常。
加載 dex 的方式
通過分析 Activity 的啟動過程,發現 Activity 是由 ActivityThread 通過 Instrumentation 來啟動的,那么是否可以在 Instrumentation 中做一定的手腳呢?通過分析代碼 ActivityThread 和 Instrumentation 發現,Instrumentation 有關 Activity 啟動相關的方法大概有:execStartActivity、 newActivity 等等,這樣就可以在這些方法中添加代碼邏輯進行判斷這個 class 是否加載了,如果加載則直接啟動這個 Activity,如果沒有加載完成則啟動一個等待的 Activity 顯示給用戶,然后在這個 Activity 中等待后臺第二個 dex 加載完成,完成后自動跳轉到用戶實際要跳轉的 Activity;這樣在代碼充分解耦合,以及每個業務代碼能夠做到顆粒化的前提下,就做到第二個 dex 的按需加載了。
美團的這種方式對主 dex 的要求非常高,因為第二個 dex 是等到需要的時候再去加載。重寫Instrumentation 的 execStartActivity 方法,hook 跳轉 Activity 的總入口做判斷,如果當前第二個 dex 還沒有加載完成,就彈一個 loading Activity等待加載完成。
引入dexnkife 核心庫
dexnkife 項目地址: DexKnifePlugin.
dexnkife 幫助我們劃分類到主DEX ,使DEX 劃分通過配置形式來完成
使用 # 進行注釋, 當行起始加上 #, 這行配置被禁用.
# 全局過濾, 如果沒設置 -filter-suggest 并不會應用到 建議的maindexlist.
# 如果你想要某個包路徑在maindex中,則使用 -keep 選項,即使他已經在分包的路徑中.
-keep android.support.v4.view.**
# 這條配置可以指定這個包下類在第二dex中.
android.support.v?.**
# 使用.class后綴,代表單個類.
-keep android.support.v7.app.AppCompatDialogFragment.class
# 不包含Android gradle 插件自動生成的miandex列表.
-donot-use-suggest
# 將 全局過濾配置應用到 建議的maindexlist中, 但 -donot-use-suggest 要關閉.
-filter-suggest
# 不進行dex分包, 直到 dex 的id數量超過 65536.
-auto-maindex
# dex 擴展參數, 例如 --set-max-idx-number=50000
# 如果出現 DexException: Too many classes in --main-dex-list, main dex capacity exceeded,則需要調大數值
-dex-param --set-max-idx-number=50000
# 顯示miandex的日志.
-log-mainlist
# 如果你只想過濾 建議的maindexlist, 使用 -suggest-split 和 -suggest-keep.
# 如果同時啟用 -filter-suggest, 全局過濾會合并到它們中.
-suggest-split **.MainActivity2.class
-suggest-keep android.support.multidex.**
搞定分DEX ,繼續集成Tinker
生成基礎版本
首先執行gradle 任務中的assableRelease,release 環境下打包會輸出到/home/anzogame/release/包名/版本/release/,同時會在release同級目錄下生成bakApk/app-1209-15-34-25/_test/ 生成apk ,mapping ,R.txt 文件,該文件就是基礎版本
生成補丁包
在build.grale 中將tinkerOldApkPath,tinkerApplyMappingPath,tinkerApplyResourcePath 替換生新生成apk,maping和R.txt 文件,
然后修改自己代碼,注意要修改一行CoreApplicationLike 中的代碼 ,可以加一個日志,因為1.7.5的tinker 有一個bug ,最好修改一下,然后執行
gradle 任務中的,tinkerPatchRelease 生成補丁文件,最后生成的補丁文件
輸出文件詳解
然后將生成補丁patch_signed_7zip.apk 重名 patch_signed_7zip.so 通過后臺CMS 傳到SERVER ,客戶端安裝基礎版本,啟動查看補丁是否安裝成功。
/Tinker.DexDiffPatchInternal: success recover dex file: /data/user/0/com.anzogame.lol/tinker/patch-3ce34da5/dex/classes.dex.jar, use time: 4118
12-09 16:21:34.900 12198-12228/? I/Tinker.DexDiffPatchInternal: try Extracting /data/user/0/com.anzogame.lol/tinker/patch-3ce34da5/dex/classes2.dex.jar
12-09 16:21:36.416 12198-12228/? I/Tinker.DexDiffPatchInternal: isExtractionSuccessful: true
12-09 16:21:36.526 12198-12228/? I/Tinker.DexDiffPatchInternal: try Extracting /data/user/0/com.anzogame.lol/tinker/patch-3ce34da5/dex/classes3.dex.jar
12-09 16:21:36.902 12198-12228/? I/Tinker.DexDiffPatchInternal: isExtractionSuccessful: true
12-09 16:21:36.932 12198-12228/? I/Tinker.DexDiffPatchInternal: try Extracting /data/user/0/com.anzogame.lol/tinker/patch-3ce34da5/dex/classes4.dex.jar
12-09 16:21:36.976 12198-12228/? I/Tinker.DexDiffPatchInternal: isExtractionSuccessful: true
12-09 16:21:36.982 12198-12228/? I/Tinker.DexDiffPatchInternal: try Extracting /data/user/0/com.anzogame.lol/tinker/patch-3ce34da5/dex/test.dex.jar
12-09 16:21:36.985 12198-12228/? I/Tinker.DexDiffPatchInternal: isExtractionSuccessful: true
12-09 16:21:49.737 12198-12228/? I/Tinker.DexDiffPatchInternal: recover dex result:true, cost:19018, isUpgradePatch:true
12-09 16:21:49.739 12198-12228/? W/Tinker.BsDiffPatchInternal: patch recover, library is not contained
12-09 16:21:49.746 12198-12228/? I/Tinker.ResDiffPatchInternal: res dir: /data/user/0/com.anzogame.lol/tinker/patch-3ce34da5/res/, meta: resArscMd5:91dfea65855052958dc4cd3abd408550
arscBaseCrc:2718315645
pattern:res/.*
pattern:resources\.arsc
pattern:assets/.*
addedSet:assets/only_use_to_test_tinker_resource.txt
modifiedSet:res/layout/activity_patch.xml
12-09 16:21:49.797 12198-12228/? I/Tinker.ResDiffPatchInternal: no large modify resources, just return
12-09 16:21:53.353 12198-12228/? I/Tinker.ResDiffPatchInternal: final new resource file:/data/user/0/com.anzogame.lol/tinker/patch-3ce34da5/res/resources.apk, entry count:7596, size:39259384
12-09 16:21:53.353 12198-12228/? I/Tinker.ResDiffPatchInternal: recover resource result:true, cost:3613, isNewPatch:true
12-09 16:21:53.366 12198-12228/? W/Tinker.UpgradePatch: UpgradePatch tryPatch: done, it is ok
12-09 16:21:53.366 12198-12228/? I/Tinker.DefaultPatchReporter: patchReporter: patch all result path:/storage/emulated/0/AnZoLOL/patch/patch_signed_7zip.so, success:true, cost:22752, isUpgrade:true
12-09 16:21:53.367 12198-12228/? I/Tinker.PatchFileUtil: safeDeleteFile, try to delete path: /data/user/0/com.anzogame.lol/tinker/patch.retry
12-09 16:21:53.368 12198-12228/? I/Tinker.PatchFileUtil: safeDeleteFile, try to delete path: /data/user/0/com.anzogame.lol/tinker/temp.apk
看到如下日志,表示補丁合成成功,殺進程,重啟應用,查看日志
TinkerLoader: tryLoadPatchFiles:isEnabledForResource:true
12-09 16:24:00.771 12420-12420/? D/Tinker.TinkerInternals: same fingerprint:Xiaomi/gemini/gemini:6.0.1/MXB48T/V8.1.1.0.MAACNDI:user/release-keys
12-09 16:24:00.775 12420-12420/? W/Tinker.TinkerLoader: tinker safe mode preferName:tinker_own_config_com.anzogame.lol count:0
12-09 16:24:00.780 12420-12420/? W/Tinker.TinkerLoader: after tinker safe mode count:1
12-09 16:24:00.780 12420-12420/? I/Tinker.TinkerDexLoader: classloader: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.anzogame.lol-1/base.apk"],nativeLibraryDirectories=[/data/app/com.anzogame.lol-1/lib/arm, /data/app/com.anzogame.lol-1/base.apk!/lib/armeabi-v7a, /vendor/lib, /system/lib]]]
12-09 16:24:00.806 12420-12420/? W/Tinker.ClassLoaderAdder: checkDexInstall result:true
12-09 16:24:00.807 12420-12420/? I/Tinker.TinkerDexLoader: after loaded classloader: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/user/0/com.anzogame.lol/tinker/patch-3ce34da5/dex/classes.dex.jar", zip file "/data/user/0/com.anzogame.lol/tinker/patch-3ce34da5/dex/classes2.dex.jar", zip file "/data/user/0/com.anzogame.lol/tinker/patch-3ce34da5/dex/classes3.dex.jar", zip file "/data/user/0/com.anzogame.lol/tinker/patch-3ce34da5/dex/classes4.dex.jar", zip file "/data/user/0/com.anzogame.lol/tinker/patch-3ce34da5/dex/test.dex.jar", zip file "/data/app/com.anzogame.lol-1/base.apk"],nativeLibraryDirectories=[/data/app/com.anzogame.lol-1/lib/arm, /data/app/com.anzogame.lol-1/base.apk!/lib/armeabi-v7a, /vendor/lib, /system/lib]]]
12-09 16:24:00.812 12420-12420/? E/Tinker.ResourcePatcher: checkResUpdate success, found test resource assets file only_use_to_test_tinker_resource.txt
12-09 16:24:00.813 12420-12420/? I/Tinker.ResourceLoader: monkeyPatchExistingResources resource file:/data/user/0/com.anzogame.lol/tinker/patch-3ce34da5/res/resources.apk, use time: 6
12-09 16:24:00.813 12420-12420/? I/Tinker.TinkerLoader: tryLoadPatchFiles: load end, ok!
12-09 16:24:00.845 12420-12420/? D/Tinker.DefaultAppLike: onBaseContextAttached:
12-09 16:24:00.853 12420-12420/? I/Tinker.SamplePatchListener: application maxMemory:256
12-09 16:24:00.858 12420-12420/? W/Tinker.Tinker: tinker patch directory: /data/user/0/com.anzogame.lol/tinker
12-09 16:24:00.863 12420-12420/? I/Tinker.TinkerLoadResult: parseTinkerResult loadCode:0
12-09 16:24:00.863 12420-12420/? I/Tinker.TinkerLoadResult: parseTinkerResult oldVersion:, newVersion:3ce34da54e4e3ec20c9f8868a8970db0, current:3ce34da54e4e3ec20c9f8868a8970db0
12-09 16:24:00.863 12420-12420/? I/Tinker.TinkerLoadResult: oh yeah, tinker load all success
12-09 16:24:00.863 12420-12420/? I/Tinker.DefaultLoadReporter: patch version change from to 3ce34da54e4e3ec20c9f8868a8970db0
12-09 16:24:00.863 12420-12420/? I/Tinker.DefaultLoadReporter: try kill all other process
12-09 16:24:00.865 12420-12420/? I/Tinker.DefaultLoadReporter: patch load result, path:/data/user/0/com.anzogame.lol/tinker, code:0, cost:81
oh yeah, tinker load all success
補丁合成加載成功。
tinker 多渠道打包怎么處理?
tinker 本身是支持flavor 打包的:
加入上面的配置,執行assembleRelease task, 會在app/build/bakApk/目錄下面生成所有flavor 中的渠道包.
接著修改代碼和資源文件,執行tinkerPatchAllFlavorRelease 生成所有渠道的補丁包
然在后在手機上執行通渠道的補丁升級,可以正常升級,如果你用tencent渠道的包升級test 渠道的補丁包,就會失敗,什么原因呢?查看tinker文檔
額。。。。 實際使用中不可能對不同渠道進行補丁包的管理,多個渠道需要使用一個補丁包,那么我們就需要對我們現在有的打包方式進行修改,tinker 的建議方式原理和美團的快速打包方案類似,那么我們來看下美團的打包方案。
美團打包方案
第一步:
修改我們的多渠道flavors 打包方式去掉所有渠道,只剩下一個test渠道做為基礎渠道,然后在啟動APP的時候動態設置渠道值,例如可以用友盟提供的方式AnalyticsConfig.setChannel(ChannelUtil.getChannel(getCurrentActivity()));動態設置渠道值
獲取渠道代碼
第二步:
替換我們之前的獲取渠道名稱代碼 AndroidApiUtils.getUmengChannel(Context context) , 改為
友盟提供的方式AnalyticsConfig.getChannel(getApplicationContext()) ,因為我們以前的代碼是直接讀取的
meta 中的與友盟渠道號
,查看友盟的代碼,AnalyticsConfig.getChannel 是在渠道號不為空的情況下才會去讀取meta 中的,我們打包方式是不會對AndroidManifest.xml 中的渠道號做替換的,只是內存中的channel 替換。
第三步:
打包生成apk ,在把生成APK 放到腳本同級的目錄下面,進入目錄執行python MultiChannelBuildTool.py
生成apk, 不到一分鐘,所有渠道的APK 已經生成好了,channel.txt 是所有渠道的渠道列表。
下面是打包腳本:
按照美團的打包方式生成基礎APK 多渠道APK 補丁包,經驗證補丁可以正常運行。
以后發版本打包的流程, 執行gradle clean assembleRelease -PDEV_PACKET=false 任務,release 目錄下面生成基礎版本的APK ,然后在當前目錄下面實行python MultiChannelBuildTool.py ,同時在release 同級目錄下面會生成 bakApk/.../ 備份的apk ,mapping.txt, R.txt 文件。
Tinker 常用API
Tinker 自定義擴展
[傳送門](file:///Users/zhulizhi/Downloads/Tinker-%E8%87%AA%E5%AE%9A%E4%B9%89%E6%89%A9%E5%B1%95.html)
Tinker 常見問題
熱補丁流程錯誤碼定義:
1,下載失敗上報
2,接口請求錯誤上報
3,MD5文件校驗失敗錯誤上報
5,文件格式校驗錯誤上報
6,補丁合成失敗上報
7,補丁合成成功上報
熱補丁相關疑問?
tinker 熱補丁和DroidPlug插件有什么區別?
tinker 是熱更新工具 目前補丁不支持新增四大組件
DroidPlug 核心思想是hook 系統流程,占坑實現插件。