目錄
我的學(xué)習(xí)手冊 - 熱更新了解了一下下
我的學(xué)習(xí)手冊 - Glide了解了一下下
我的學(xué)習(xí)手冊 - 進(jìn)程弊馄活了解了一下下
我的學(xué)習(xí)手冊 - EventBus了解了一下下
我的學(xué)習(xí)手冊 - ARouter了解了一下下
熱修復(fù)(Tinker)
一义辕、這個是什么東西
正常開發(fā)流程
版本1.0上線
-> 發(fā)現(xiàn)Bug -> 修復(fù)Bug -> 發(fā)布版本1.1 -> 用戶下載安裝
-> 發(fā)現(xiàn)Bug -> 修復(fù)Bug -> 發(fā)布版本1.2 -> 用戶下載安裝
-> 發(fā)現(xiàn)Bug -> 修復(fù)Bug -> 發(fā)布版本1.3 -> 用戶下載安裝
-> 應(yīng)用更新 -> 發(fā)布版本1.4
熱修復(fù)開發(fā)流程
版本1.0上線
-> 發(fā)現(xiàn)Bug -> 修復(fù)Bug -> 發(fā)布補丁 -> 應(yīng)用自動修復(fù)
-> 發(fā)現(xiàn)Bug -> 修復(fù)Bug -> 發(fā)布補丁 -> 應(yīng)用自動修復(fù)
-> 發(fā)現(xiàn)Bug -> 修復(fù)Bug -> 發(fā)布補丁 -> 應(yīng)用自動修復(fù)
-> 應(yīng)用更新 -> 發(fā)布版本1.1
二、原理
App中所有的類都是從dex中獲取的宰译,通過BaseDexClassLoader的findClass(String name)方法獲取,dex可以通過AndroidStudio分析Apk看到泡一,也可以解壓apk看到兰吟。
我們可以從下面兩段代碼中看到和一些命名中可以很容易的看出App找一個類的時候是從dex列表中一個一個的遍歷如果找到了就返回這個類,沒有找到會拋出ClassNotFoundExcepton仙逻。而Tinker熱修復(fù)的原理就是通過添加一個補丁Dex來讓找類的時候先通過查找這個補丁Dex中的類驰吓,如果找到類那么就直接返回找到的類而不會繼續(xù)向下尋找后面的出現(xiàn)Bug的Dex,這就是插樁系奉,再來一張我畫的圖吧就更容易理解了
然后還有一點檬贰,我個人認(rèn)為熱修復(fù)和Windows的補丁(開啟自動修復(fù))類似缺亮,首先你需要開機翁涤,然后請求微軟的某個接口,知道現(xiàn)在需要打補丁了萌踱,然后開始下載葵礼,下載完成后進(jìn)行更新。熱更新也是同樣的并鸵,進(jìn)入App之后請求后臺章咧,發(fā)現(xiàn)當(dāng)前版本有一個補丁,之后下載補丁到本地能真,熱更新工具發(fā)現(xiàn)本地補丁文件夾下面有一個補丁,那么就會在初始化的時候把這個補丁dex拿過來插入pathList中,來進(jìn)行修復(fù)bug的功能實現(xiàn)
public class BaseDexClassLoader extends ClassLoader {
//這個就是存放所有的dex
private final DexPathList pathList;
...
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
...
}
final class DexPathList {
...
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
...
}
三粉铐、代碼演示
這里我們先創(chuàng)建一個App疼约,將一些主要文件放入classes.dex,將bug文件放入classes2.dex蝙泼,然后在其中創(chuàng)建一個bug程剥,打包安裝,此時裝入手機的是Bug包汤踏,出現(xiàn)bug的文件類在classes2.dex中织鲸。
然后我們修復(fù)bug,buildApk溪胶,將apk解壓拿到classess2.dex搂擦,這個就類似補丁包,實際上的補丁包會更小哗脖,只是本地測試使用瀑踢,將這個補丁包復(fù)制到App的私有目錄中模擬從服務(wù)器中下載。
點擊修復(fù)按鈕才避,將文件復(fù)制橱夭,然后退出應(yīng)用(殺死進(jìn)程),重新打開App就可以看到修復(fù)后的效果
一桑逝、我們需要App有多個dex包棘劣,至少有一個主包,一個bug包楞遏,先進(jìn)行分包處理
App
class App:Application(){
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
MultiDex.install(this)
//加載補丁包
HotfixUtils.loadFixedDex(this)
}
}
build.gradle中對主包進(jìn)行一些配置
multiDexEnabled true
multiDexKeepFile file('MultiDexKeep.txt')
dexOptions{
javaMaxHeapSize "4g"
preDexLibraries = false
additionalParameters = [
'--multi-dex',
'--set-max-idx-number=50000',
'--main-dex-list='+'/MultiDexKeep.txt',
'--minimal-main-dex'
]
}
MultiDexKeep.txt 把一些不會出錯的文件keep住 放到主包里
com/memo/hotfix/App.class
com/memo/hotfix/BaseActivity.class
com/memo/hotfix/utils/ArrayUtils.class
com/memo/hotfix/utils/Constant.class
com/memo/hotfix/utils/FileUtils.class
com/memo/hotfix/utils/HotfixUtils.class
com/memo/hotfix/utils/LogUtils.class
com/memo/hotfix/utils/ReflectUtils.class
2茬暇、創(chuàng)建一個出現(xiàn)Bug的Activity,后續(xù)方便修改
BugActivity
override fun initialize() {
ActivityCompat.requestPermissions(mActivity, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 1)
//模擬bug 點擊不做任何操作
mBtnShow.setOnClickListener {
//這里模擬有bug
mTvResult.text = "這里有Bug"
//mTvResult.text = "Bug修復(fù)了"
}
//點擊修復(fù)bug
mBtnFix.setOnClickListener { fixBug() }
}
private fun fixBug() {
//將補丁包patch.dex放到手機的根目錄下面 然后將文件從本地復(fù)制到私有目錄下面
//實際上是從服務(wù)器下載到本地目錄下面
val from: File = File(Environment.getExternalStorageDirectory(), Constant.PATCH_DEX)
val to: File =
File(getDir(Constant.DEX_DIR, Context.MODE_PRIVATE).absolutePath + File.separator + Constant.PATCH_DEX)
if (to.exists()) {
//如果之前的補丁包存在 就刪除
val isDelete = to.delete()
LogUtils.i("補丁包刪除$isDelete")
}
if (from.exists()) {
//復(fù)制 模擬服務(wù)器下載
FileUtils.copy(from, to)
showToast("補丁加載成功")
LogUtils.i("copy成功${to.exists()}")
from.delete()
} else {
showToast("補丁包不存在")
}
}
3橱健、劃重點而钞,我們在App中進(jìn)行加載補丁包重點就是為了插樁,先獲取補丁patchDexElement拘荡,在獲取原有的oriDexElement臼节,然后合并生成心得dexElement(順序為補丁在前,原有在后)珊皿,然后賦值給App的pathList里的dexElement网缝,當(dāng)然需要使用反射
object HotfixUtils {
/*** 補丁包集合 ***/
private val patchDexSet: HashSet<File> by lazy { HashSet<File>() }
fun loadFixedDex(mContext: Context) {
//先清空
patchDexSet.clear()
//獲取補丁包目錄
val patchDexDir: File = mContext.getDir(Constant.DEX_DIR, Context.MODE_PRIVATE)
//遍歷這個補丁包下面的所有的文件
val listFiles: Array<File> = patchDexDir.listFiles()
for (file in listFiles) {
if (file.name.endsWith(Constant.DEX_SUFFIX) && Constant.MAIN_DEX != file.name) {
//找到文件夾下面的補丁包 放入自己的補丁包集合中
patchDexSet.add(file)
}
}
if (patchDexSet.size > 0) {
//類加載器加載
createDexClassLoader(mContext, patchDexDir)
}
}
private fun createDexClassLoader(mContext: Context, patchDexDir: File) {
//臨時dex解壓目錄 因為類加載器加載的是類而不是dex 所以需要將dex進(jìn)行解壓
val optDirPath: String = patchDexDir.absolutePath + File.separator + Constant.DEX_OPT
//創(chuàng)建
val optDir = File(optDirPath)
if (!optDir.exists()) {
optDir.mkdirs()
}
for (dex in patchDexSet) {
//自己創(chuàng)建一個補丁DexClassLoader
val patchClassLoader = DexClassLoader(dex.absolutePath, optDirPath, null, mContext.classLoader)
//每次獲取一個補丁文件,需要插樁一次
//??????sāD酆!庇楞!最重要的環(huán)節(jié)!h九!??????
hotFix(patchClassLoader, mContext)
}
}
/**
* ??????A蛱怠!!最重要的環(huán)節(jié)3酆蟆!矗愧!??????
* ??????T钪ァ!唉韭!最重要的環(huán)節(jié)R固椤!属愤!??????
* ??????E鳌!春塌!最重要的環(huán)節(jié)O堋!只壳!??????
*/
private fun hotFix(patchClassLoader: DexClassLoader, mContext: Context) {
//這里分為6步
//1.獲取原有的PathClassLoader
val pathClassLoader: PathClassLoader = mContext.classLoader as PathClassLoader
try {
//2.獲取補丁包列表 dexElement
val patchDexElement = ReflectUtils.getDexElement(ReflectUtils.getPathList(patchClassLoader))
//3.獲取原有的pathList
val oriPathList = ReflectUtils.getPathList(pathClassLoader)
//4.獲取原有包列表 dexElement
val oriDexElement = ReflectUtils.getDexElement(oriPathList)
//5.合并成為一個新的 補丁包在前 原有包在后的dexElement
val finalDexElement = ArrayUtils.combineArray(patchDexElement, oriDexElement)
//6.用合成后的dexElement重新賦值原有的pathList里面的dexElement屬性
ReflectUtils.setDexElement(oriPathList, oriPathList.javaClass, finalDexElement)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
4俏拱、實際效果
5、結(jié)合實際使用一下Tinker吼句,這里使用的Bugly
基本按照官網(wǎng)的示例文檔來就好來
1.創(chuàng)建TinkerApplicationLike锅必,進(jìn)行一些詳細(xì)的配置
class TinkerApplicationLike(
application: Application, tinkerFlags: Int,
tinkerLoadVerifyFlag: Boolean, applicationStartElapsedTime: Long,
applicationStartMillisTime: Long, tinkerResultIntent: Intent
) : DefaultApplicationLike(
application,
tinkerFlags,
tinkerLoadVerifyFlag,
applicationStartElapsedTime,
applicationStartMillisTime,
tinkerResultIntent
) {
override fun onCreate() {
super.onCreate()
// 這里實現(xiàn)SDK初始化,appId替換成你的在Bugly平臺申請的appId
// 調(diào)試時惕艳,將第三個參數(shù)改為true
Bugly.init(application, "(Bugly 申請的 appId)", true)
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
override fun onBaseContextAttached(base: Context) {
super.onBaseContextAttached(base)
// you must install multiDex whatever tinker is installed!
MultiDex.install(base)
// 安裝tinker
Beta.installTinker(this)
Beta.checkUpgrade()
//自動下載
Beta.canAutoDownloadPatch = true
Beta.canAutoPatch = true
//提示用戶需要重啟 內(nèi)部進(jìn)行來殺死進(jìn)程操作
Beta.canNotifyUserRestart = true
//補丁的監(jiān)聽
Beta.betaPatchListener = object : BetaPatchListener {
override fun onPatchReceived(patchFile: String) {
Toast.makeText(application, "補丁下載地址$patchFile", Toast.LENGTH_SHORT).show()
}
override fun onDownloadReceived(savedLength: Long, totalLength: Long) {
Toast.makeText(
application,
String.format(
Locale.getDefault(), "%s %d%%",
Beta.strNotificationDownloading,
(if (totalLength == 0L) 0 else savedLength * 100 / totalLength).toInt()
),
Toast.LENGTH_SHORT
).show()
}
override fun onDownloadSuccess(msg: String) {
Toast.makeText(application, "補丁下載成功", Toast.LENGTH_SHORT).show()
}
override fun onDownloadFailure(msg: String) {
Toast.makeText(application, "補丁下載失敗", Toast.LENGTH_SHORT).show()
}
override fun onApplySuccess(msg: String) {
Toast.makeText(application, "補丁應(yīng)用成功", Toast.LENGTH_SHORT).show()
}
override fun onApplyFailure(msg: String) {
Toast.makeText(application, "補丁應(yīng)用失敗", Toast.LENGTH_SHORT).show()
}
override fun onPatchRollback() {
Toast.makeText(application, "補丁回滾", Toast.LENGTH_SHORT).show()
}
}
}
}
2.對于Tinker Gradle的配置
每次發(fā)線上包的時候需要把包作為基準(zhǔn)包進(jìn)行留存搞隐,這個很重要,如果沒有基準(zhǔn)包就沒有辦法打補丁包了远搪,切記切記
每次打補丁包的時候要把基準(zhǔn)包加進(jìn)來注意文件夾和文件名稱要和gradle中的配置一樣 仔細(xì)檢查 例如我的
def baseApkDir = "base"
baseApk = "${bakPath}/${baseApkDir}/app-debug.apk"
那么就是這樣子的
// 主要修改的地方注意下面的名稱
// 構(gòu)建基準(zhǔn)包和補丁包都要指定不同的tinkerId劣纲,并且必須保證唯一性
//base-(版本號)
tinkerId = "base-1.0.4"
//patch-(版本號)-(補丁版本號)
//tinkerId = "patch-1.0.4-1"
// 編譯補丁包時,必需指定基線版本的apk谁鳍,默認(rèn)值為空
// 如果為空癞季,則表示不是進(jìn)行補丁包的編譯
// @{link tinkerPatch.oldApk }
baseApk = "${bakPath}/${baseApkDir}/app-debug.apk"
// 對應(yīng)tinker插件applyResourceMapping
baseApkResourceMapping = "${bakPath}/${baseApkDir}/app-debug-R.txt"
apply plugin: 'com.tencent.bugly.tinker-support'
def bakPath = file("${buildDir}/bakApk/")
/**
* 此處填寫每次構(gòu)建生成的基準(zhǔn)包目錄
*/
def baseApkDir = "base"
/**
* 對于插件各參數(shù)的詳細(xì)解析請參考
*/
tinkerSupport {
// 構(gòu)建基準(zhǔn)包和補丁包都要指定不同的tinkerId,并且必須保證唯一性
//base-(版本號)
tinkerId = "base-1.0.4"
//patch-(版本號)-(補丁版本號)
//tinkerId = "patch-1.0.4-1"
// 編譯補丁包時倘潜,必需指定基線版本的apk绷柒,默認(rèn)值為空
// 如果為空,則表示不是進(jìn)行補丁包的編譯
// @{link tinkerPatch.oldApk }
baseApk = "${bakPath}/${baseApkDir}/app-debug.apk"
// 對應(yīng)tinker插件applyResourceMapping
baseApkResourceMapping = "${bakPath}/${baseApkDir}/app-debug-R.txt"
// 對應(yīng)tinker插件applyMapping
baseApkProguardMapping = "${bakPath}/${baseApkDir}/app-debug-mapping.txt"
// 開啟tinker-support插件涮因,默認(rèn)值true
enable = true
// 指定歸檔目錄废睦,默認(rèn)值當(dāng)前module的子目錄tinker
autoBackupApkDir = "${bakPath}"
// 是否啟用覆蓋tinkerPatch配置功能,默認(rèn)值false
// 開啟后tinkerPatch配置不生效养泡,即無需添加tinkerPatch
overrideTinkerPatchConfiguration = true
// 構(gòu)建多渠道補丁時使用
// buildAllFlavorsDir = "${bakPath}/${baseApkDir}"
// 是否啟用加固模式嗜湃,默認(rèn)為false.(tinker-spport 1.0.7起支持)
isProtectedApp = true
// 是否開啟反射Application模式
enableProxyApplication = false
// 是否支持新增非export的Activity(注意:設(shè)置為true才能修改AndroidManifest文件)
supportHotplugComponent = true
}
/**
* 一般來說,我們無需對下面的參數(shù)做任何的修改
* 對于各參數(shù)的詳細(xì)介紹請參考:
* 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"
}
buildConfig {
keepDexApply = false
}
}