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 {
*;
}

运行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 ]
该脚本要求输入一个文件组(包含编译后的目录或jar包),然后分析文件组中的类并写入到–output所指定的文件中。实现原理也不复杂,主要分为三步:
a. 环境检查,包括传入参数合法性检查,路径检查以及proguard环境检测等。
b. 使用mainDexClasses.rules规则,通过Proguard的shrink功能,裁剪无关类,生成一个tmp.jar包。
c. 通过生成的tmp jar包,调用MainDexListBuilder类生成主dex的文件列表。

这里只是简单的得到所有入口类(即rules中的Instrumentation、application、Activity、Annotation等等)的直接引入类。何为直接引用类?在init过程,会在校验阶段去resolve它各个方法、变量引用到的类,这些类统称为某个类的直接引用类。举个栗子:

1
2
3
4
5
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/“.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 放在哪个线程都让主线程挂起。

引入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 的建议方式原理和美团的快速打包方案类似,那么我们来看下美团的打包方案。

###美团打包方案
传送门1
传送门2

###第一步:
修改我们的多渠道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的一切都在这里,包括源码(一)

Android_N混合编译与对热补丁影响解析

微信Android热补丁实践演进之路