##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 {
*;
}
运行APP 彩蛋….(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 .
Google 官方方案是如何加载的呢?
Google 官方支持 Multidex 的 jar 包是 android-support-multidex.jar,该 jar 包从 build tools 21.1 开始支持。这个 jar 加载 apk 中的从 dex 流程如下:
此处主要的工作就是从 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
这里只是简单的得到所有入口类(即rules中的Instrumentation、application、Activity、Annotation等等)的直接引入类。何为直接引用类?在init过程,会在校验阶段去resolve它各个方法、变量引用到的类,这些类统称为某个类的直接引用类。举个栗子:1
2
3
4
5public 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/
}
}
##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 放在哪个线程都让主线程挂起。
引入dexnkife 核心库
dexnkife 项目地址: DexKnifePlugin.
dexnkife 帮助我们划分类到主DEX
首先在APP/目录下面新建dexknife.txt文件,用于配置dexknife
##在APP层添加Tinker的配置文件 ,配置文件如下:
tinkerEnabled: tinker 的开关
###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 热补丁和DroidPlug插件有什么区别?
tinker 是热更新工具 目前补丁不支持新增四大组件
DroidPlug 核心思想是hook 系统流程,占坑实现插件。
###tinker 的资源是怎么修复的?
###tinker 的DEX是怎么修复的?
###tinker 的so是怎么修复的?
###Android 中是怎么确认哪些类放到主DEX中的呢?
##Tinker 相关文章
微信Tinker的一切都在这里,包括源码(一)