目录
我的学习手册 - 热更新了解了一下下
我的学习手册 - Glide了解了一下下
我的学习手册 - 进程保活了解了一下下
我的学习手册 - EventBus了解了一下下
我的学习手册 - ARouter了解了一下下
热修复(Tinker)
一、这个是什么东西
正常开发流程
版本1.0上线
-> 发现Bug -> 修复Bug -> 发布版本1.1 -> 用户下载安装
-> 发现Bug -> 修复Bug -> 发布版本1.2 -> 用户下载安装
-> 发现Bug -> 修复Bug -> 发布版本1.3 -> 用户下载安装
-> 应用更新 -> 发布版本1.4
热修复开发流程
版本1.0上线
-> 发现Bug -> 修复Bug -> 发布补丁 -> 应用自动修复
-> 发现Bug -> 修复Bug -> 发布补丁 -> 应用自动修复
-> 发现Bug -> 修复Bug -> 发布补丁 -> 应用自动修复
-> 应用更新 -> 发布版本1.1
二、原理
App中所有的类都是从dex中获取的,通过BaseDexClassLoader的findClass(String name)方法获取,dex可以通过AndroidStudio分析Apk看到,也可以解压apk看到。
我们可以从下面两段代码中看到和一些命名中可以很容易的看出App找一个类的时候是从dex列表中一个一个的遍历如果找到了就返回这个类,没有找到会抛出ClassNotFoundExcepton。而Tinker热修复的原理就是通过添加一个补丁Dex来让找类的时候先通过查找这个补丁Dex中的类,如果找到类那么就直接返回找到的类而不会继续向下寻找后面的出现Bug的Dex,这就是插桩,再来一张我画的图吧就更容易理解了
然后还有一点,我个人认为热修复和Windows的补丁(开启自动修复)类似,首先你需要开机,然后请求微软的某个接口,知道现在需要打补丁了,然后开始下载,下载完成后进行更新。热更新也是同样的,进入App之后请求后台,发现当前版本有一个补丁,之后下载补丁到本地,热更新工具发现本地补丁文件夹下面有一个补丁,那么就会在初始化的时候把这个补丁dex拿过来插入pathList中,来进行修复bug的功能实现
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;
}
...
}
插桩.jpg
三、代码演示
这里我们先创建一个App,将一些主要文件放入classes.dex,将bug文件放入classes2.dex,然后在其中创建一个bug,打包安装,此时装入手机的是Bug包,出现bug的文件类在classes2.dex中。
然后我们修复bug,buildApk,将apk解压拿到classess2.dex,这个就类似补丁包,实际上的补丁包会更小,只是本地测试使用,将这个补丁包复制到App的私有目录中模拟从服务器中下载。
点击修复按钮,将文件复制,然后退出应用(杀死进程),重新打开App就可以看到修复后的效果
一、我们需要App有多个dex包,至少有一个主包,一个bug包,先进行分包处理
App
class App:Application(){
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
MultiDex.install(this)
//加载补丁包
HotfixUtils.loadFixedDex(this)
}
}
build.gradle中对主包进行一些配置
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、创建一个出现Bug的Activity,后续方便修改
BugActivity
override fun initialize() {
ActivityCompat.requestPermissions(mActivity, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 1)
//模拟bug 点击不做任何操作
mBtnShow.setOnClickListener {
//这里模拟有bug
mTvResult.text = "这里有Bug"
//mTvResult.text = "Bug修复了"
}
//点击修复bug
mBtnFix.setOnClickListener { fixBug() }
}
private fun fixBug() {
//将补丁包patch.dex放到手机的根目录下面 然后将文件从本地复制到私有目录下面
//实际上是从服务器下载到本地目录下面
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()) {
//复制 模拟服务器下载
FileUtils.copy(from, to)
showToast("补丁加载成功")
LogUtils.i("copy成功${to.exists()}")
from.delete()
} else {
showToast("补丁包不存在")
}
}
3、划重点,我们在App中进行加载补丁包重点就是为了插桩,先获取补丁patchDexElement,在获取原有的oriDexElement,然后合并生成心得dexElement(顺序为补丁在前,原有在后),然后赋值给App的pathList里的dexElement,当然需要使用反射
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进行解压
val optDirPath: String = patchDexDir.absolutePath + File.separator + Constant.DEX_OPT
//创建
val optDir = File(optDirPath)
if (!optDir.exists()) {
optDir.mkdirs()
}
for (dex in patchDexSet) {
//自己创建一个补丁DexClassLoader
val patchClassLoader = DexClassLoader(dex.absolutePath, optDirPath, null, mContext.classLoader)
//每次获取一个补丁文件,需要插桩一次
//⚠️⚠️⚠️!!!最重要的环节!!!⚠️⚠️⚠️
hotFix(patchClassLoader, mContext)
}
}
/**
* ⚠️⚠️⚠️!!!最重要的环节!!!⚠️⚠️⚠️
* ⚠️⚠️⚠️!!!最重要的环节!!!⚠️⚠️⚠️
* ⚠️⚠️⚠️!!!最重要的环节!!!⚠️⚠️⚠️
*/
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、实际效果
show.gif5、结合实际使用一下Tinker,这里使用的Bugly
基本按照官网的示例文档来就好来
1.创建TinkerApplicationLike,进行一些详细的配置
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()
// 这里实现SDK初始化,appId替换成你的在Bugly平台申请的appId
// 调试时,将第三个参数改为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
//提示用户需要重启 内部进行来杀死进程操作
Beta.canNotifyUserRestart = true
//补丁的监听
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, "补丁应用成功", Toast.LENGTH_SHORT).show()
}
override fun onApplyFailure(msg: String) {
Toast.makeText(application, "补丁应用失败", Toast.LENGTH_SHORT).show()
}
override fun onPatchRollback() {
Toast.makeText(application, "补丁回滚", Toast.LENGTH_SHORT).show()
}
}
}
}
2.对于Tinker Gradle的配置
每次发线上包的时候需要把包作为基准包进行留存,这个很重要,如果没有基准包就没有办法打补丁包了,切记切记
每次打补丁包的时候要把基准包加进来注意文件夹和文件名称要和gradle中的配置一样 仔细检查 例如我的
def baseApkDir = "base"
baseApk = "${bakPath}/${baseApkDir}/app-debug.apk"
那么就是这样子的
base.png
// 主要修改的地方注意下面的名称
// 构建基准包和补丁包都要指定不同的tinkerId,并且必须保证唯一性
//base-(版本号)
tinkerId = "base-1.0.4"
//patch-(版本号)-(补丁版本号)
//tinkerId = "patch-1.0.4-1"
// 编译补丁包时,必需指定基线版本的apk,默认值为空
// 如果为空,则表示不是进行补丁包的编译
// @{link tinkerPatch.oldApk }
baseApk = "${bakPath}/${baseApkDir}/app-debug.apk"
// 对应tinker插件applyResourceMapping
baseApkResourceMapping = "${bakPath}/${baseApkDir}/app-debug-R.txt"
apply plugin: 'com.tencent.bugly.tinker-support'
def bakPath = file("${buildDir}/bakApk/")
/**
* 此处填写每次构建生成的基准包目录
*/
def baseApkDir = "base"
/**
* 对于插件各参数的详细解析请参考
*/
tinkerSupport {
// 构建基准包和补丁包都要指定不同的tinkerId,并且必须保证唯一性
//base-(版本号)
tinkerId = "base-1.0.4"
//patch-(版本号)-(补丁版本号)
//tinkerId = "patch-1.0.4-1"
// 编译补丁包时,必需指定基线版本的apk,默认值为空
// 如果为空,则表示不是进行补丁包的编译
// @{link tinkerPatch.oldApk }
baseApk = "${bakPath}/${baseApkDir}/app-debug.apk"
// 对应tinker插件applyResourceMapping
baseApkResourceMapping = "${bakPath}/${baseApkDir}/app-debug-R.txt"
// 对应tinker插件applyMapping
baseApkProguardMapping = "${bakPath}/${baseApkDir}/app-debug-mapping.txt"
// 开启tinker-support插件,默认值true
enable = true
// 指定归档目录,默认值当前module的子目录tinker
autoBackupApkDir = "${bakPath}"
// 是否启用覆盖tinkerPatch配置功能,默认值false
// 开启后tinkerPatch配置不生效,即无需添加tinkerPatch
overrideTinkerPatchConfiguration = true
// 构建多渠道补丁时使用
// buildAllFlavorsDir = "${bakPath}/${baseApkDir}"
// 是否启用加固模式,默认为false.(tinker-spport 1.0.7起支持)
isProtectedApp = true
// 是否开启反射Application模式
enableProxyApplication = false
// 是否支持新增非export的Activity(注意:设置为true才能修改AndroidManifest文件)
supportHotplugComponent = true
}
/**
* 一般来说,我们无需对下面的参数做任何的修改
* 对于各参数的详细介绍请参考:
* 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
}
}
网友评论