背景
众所周知,Activity、View、Service 等出现在 xml 里的相关 Java 类默认是不会被混淆的,Mess 是饿了么早期开源的 Gradle Plugin ,目的就是为了做到全面混淆。项目地址:https://github.com/eleme/Mess
菜比成长记
众所周知,Activity、View、Service 等出现在 xml 里的相关 Java 类默认是不会被混淆的,Mess 是饿了么早期开源的 Gradle Plugin ,目的就是为了做到全面混淆。项目地址:https://github.com/eleme/Mess
MultiDex 代码比较少,关键类就三个。
|
|
显然,关键得看 Multidex#install。
|
|
当虚拟机支持 MultiDex,直接打个 log 知会你一声,那么怎么判断当前虚拟机是否支持 MultiDex 呢?来看 IS_VM_MULTIDEX_CAPABLE:
|
|
静态代码块里拿到虚拟机版本号,正则判断版本是否大于 2.1,如果大于 2.1 就表明当前虚拟机默认支持 MultiDex。
那么有几个问题:
第一个问题,ART 虚拟机默认支持 MultiDex,为什么?会在 ART 和 DAVILK 虚拟机的区别中解释。
第二个问题,来看看官方文档:如何判断当前是否是 ART 虚拟机
文档说,当 java.vm.version >= 2.0.0 则表明是 ART 虚拟机,可是 MultiDex 的代码里写的是 >= 2.1,为什么?应该是文档没更新。。。。
那么 java.vm.version 又是在哪里被设置的呢?来看看 java.lang.System:
MultiDex 最低支持 Android 1.6
这部分是 MultiDex 的工作,仔细梳理。
|
|
其实和调试 gradle plugin 一个套路。
1、新建一个 Module,命名 buildSrc(buildsrc)
2、将 build.gralde 里的内容修改为:
|
|
3、删除 Module 文件下除了 build.gradle 外的所有文件,然后 sync
4、点击 Edit configurations
5、新建 Remote
按照默认配置点击 OK
6、输入打包命令,末尾加上 -Dorg.gradle.debug=true --no-daemon
如:./gradlew assDebug -Dorg.gradle.debug=true --no-daemon
7、点击 debug
按照以后步骤做后就可以愉快的调试了,来试验下。
打开 TaskManager.java,在判断是否需要使用 MultiDex 的地方打个断点。
然后执行步骤中的 6、7,然后等到这里被执行时就会断住了。
可能还有更好的方案。
当我们在 gradle 中将 multiDexEnabled 设为 true 后,编译 app 的过程中 Terminal 会多出一行: :app:transformClassesWithMultidexlistForDebug
显然 MultiDex 相关操作也是通过 Transform Api 完成了,自然我们查看 MultiDexTransform 源码,直接看 #transform 方法:
|
|
哟吼,核心代码好少啊,一个 shrinkWithProguard, 一个 computeList。
当我看到了方法名叫 shrinkWithProguard ,感觉很亲切啊,这不就是混淆器嘛,然后联想起 app 编译过程中输出的 app/build/intermediates/multi-dex/debug/ 下的那几个文件了:
其中 manifest_keep.txt 里的内容:
|
|
我的乖乖,shrinkWithProguard 方法势必和混淆器有扯不断的关系咯,来看看 shrinkWithProguard 具体的实现:
|
|
有点长,但是结构很清晰,我把上面代码块分为了7个部分:
第一部分是干嘛的?我以第一个 dont 方法为例,dontobfuscate:
|
|
configuration 是 proguard 里的一个配置类,换言之,这样写的效果等同于我们在给 app 做混淆的时候在 proguard-rules.pro 写:
|
|
好的,第一部分代码其实就是对混淆进行了配置。
那接下来的第二部分就太好理解了,applyConfigurationFile(manifestKeepListProguardFile); :
|
|
manifestKeepListProguardFile 就是之前提到的 manifest_keep.txt,等于把 manifest_keep.txt 里的 keep 规则也加了进来。
那第三部分和第二部分也是一样的咯,第三部分相当于是给开发人员的外部拓展入口,在 build.gradle 中配置:
|
|
第四部分就是一大堆 keep 规则,包括 keep Application 、Annotation 啦。
以上四部分就是把 keep 规则搞好了,继续看第五步,比较重要,先看 findShrinkedAndroidJar
|
|
返回的是 Android SDK 的 build-tools 里的 shrinkedAndroid.jar
那很明显了,第五部分就是把 shrinkedAndroid.jar 和刚刚的 input 文件都加入 classpath 里。
第六部分则是定义了一下相关输出文件。
第七部分运行混淆器。
从以上流程我们能得知,shrinkWithProguard 就是将我们的原来编译好的 jar 文件在使用 proguard 后输出了一个满足规则的 jar ,这个 jar 在哪?下图里的 componentClasses.jar 就是了,并且 components.flags 就是 shrinkWithProguard 中前四步所生成的 keep 规则。
来看源码:
|
|
先看看 callDx :
|
|
再看 createMainDexList:
|
|
从上面的代码很明显能得知 createMainDexList 中调用了 com.android.multidex.ClassReferenceListBuilder 的 main 方法,然后将所得的 Set 进行返回,那么 ClassReferenceListBuilder 的 main 方法执行了啥?
|
|
将参数按顺序又实例化了一个 MainDexListBuilder,然后通过这个对象调用 getMainDexList() 取出 MainDexList,最后再做输出,那么看看 MainDexListBuilder :
|
|
filesToKeep 变量最终的结果就是在 computeList 中的 mainDexClasses 的结果,那么在这个类里有两处地方调用了 filesToKeep.add,一处是 keepAnnotated 里,当存在运行时可见注解时会添加进来,另外一种就是遍历 mainListBuilder.getClassNames(),来看看这个又从哪来的?
首先用 allClassesJarFile 的 path 实例化 ClassReferenceListBuilder,然后将 jarOfRoots(这个 jar 文件就是我们执行 shrinkWithProguard 后生成的 componentClasss.jar) addRoots 到 ClassReferenceListBuilder 中,来看看 addRoots:
|
|
可以看到 classNames 变量是收集符合要求后的 classes 的集合,同时更应该看到这里的 keep 包括了两部分,一个是 jarOfRoots 文件的 root class,另一个是这个 root class 的直接引用,关于 keep 住 root class 的引用部分涉及到常量池,需要单开一篇文章做讲解,这里只要知道他 keep 住了这个 root class 的直接引用,以防运行这个 dex 时找不到类或方法。
到此,我们总算分析出了 callDx 干了啥,简单说就是通过 shrinkWithProguard 后生成的 componentClasss.jar 找出了所有应该在 mainDex 中出现的 class。
那么 callDx 下方还有段代码,很简单了,通过在 build.gradle 中配置需要加在 mainDex 的方法,如 multiDexKeepFile file(‘./main_dex_list.txt’)
最后会把所有在 mainDex 里的 class 输出在 maindexlist.txt 中:
以上终于把 MultiDexTransform 讲完了,一句话总结,其实我们就是弄清楚了 mainDex 是如何得来的。那么这还不够啊,搞了半天才输出了一个 maindexlist.txt,所以继续搞起。
|
|
在 app 编译过程中,在 MultiDex 后面后面执行的 Task 可谓是相当重要了,众所周知,将 class 文件转成 dex 文件就是这个 Task 做的了,那么先来看看 DexTrasnform 的构造函数:
|
|
其中重要的变量大家肯定一眼就看出来了,一个是 multiDex 的 boolean,一个是 mainDexListFile 的 File,来看看是在哪里实例化的:
|
|
可以看到,在 MultiDexTransform 实例化之后就去实例化了 DexTransform,实际上是将是否开启了 multidex 和 MultiDexTransform 生成的 maindexlist.txt 传给了 DexTransform,拿了参数做了啥?来看看 DexTransform 的 transform 方法:
|
|
继续往,由于调用链比较深,需要重点关注的我再单独贴代码:
|
|
这里判断了是否需要运行 MultiDex,如果需要则执行 com.android.dx.command.dexer.Main 的 runMultiDex 方法,这个 Main 类相当重要,也比较复杂,建议自行阅读,我只把 runMultiDex 方法执行的意义说一下:
|
|
一共分成五个部分:
将 MultiDexTransform 生成的 maindexlist.txt 里的内容转成 classesInMainDex Set 集合。
创建线程池,默认大小为 4 ,之后 每个 dex 的生成都会在单独线程去执行。
这一步是核心步骤,将所有 classes 打成 mainDex 和 其他 dex,待会再看。
将每个线程生成的 dex 字节流加入 dexOutputArrays 集合中。
依次输出 classes.dex、classes2.dex …
刚刚第三部分留着没讲,现在来看看:
|
|
可以看到,先是强行将 maindexlist.txt 里的 class 打进 mainDex,再去处理其他的 dex,关于其他的 dex 是根据什么规则产生的,有兴趣的可以自行去研究。
我在之前的一篇文章提到了使用系统 TabLayout 的一个Bug,详情见这篇文章,使用了系统TabLayout的app来领bug啦
系统 TabLayout 和 ViewPager 配合使用时有个 Bug,当切换 Tab 的时候,Tab 会整体往左抖一下,这个抖动速度很快,大家稍微注意点能看到,那么笔者在公司做业务时也有使用到系统的 TabLayout ,进行视觉校验的时候没逃过设计师的法眼,设计师要求高是件好事,但这个时候重新写一个也不现实,那么该怎么办呢?
先来看看直接使用系统 TabLayout 而出现问题的一些知名 App:
在分析问题之前,我们先回顾下这个 Bug 复现的场景:先选中一个靠后的 Tab,然后滑动 TabLayout 到最左边,点击第一个 Tab,会发现整个 TabLayout 往左抖了一下,速度很快,但无法忽视
那么我们要解决的就是快速抖动的问题。
想解决这个问题,TabLayout 的源码还是得分析的,TabLayout 直接继承的 HorizontalScrollView,不难推测,抖动的产生其实就是被执行了 scroll。
我们回想下,让 TabLayout 发生 scroll 行为的场景会有哪些?
我们发现的 Bug 出现的场景是点击 Tab 发生的,那么我们点击了 Tab 后符合上面说的场景一,那么 Tab 切换后会导致 ViewPager 滑动,那也会触发 scroll,擦,难道就是因为这样,导致了闪了一下?只能看看源码了:
首先看看点击 Tab 触发的方法
|
|
再看 TabLayout#animateToTab
|
|
再看看 ensureScrollAnimator 做了啥:
|
|
可以看到点击 Tab 最终会使 TabLayout 发生 scroll 行为。
继续顺着刚才说的点击 Tab 的时候也会触发 ViewPager 的滑动,我们看看 ViewPager 滑动方法里做了啥:
|
|
再看看 setScrollPosition :
|
|
看到了吧,这里也调用了 scrollTo。
那么之前抖动的问题就很显然了,点击 Tab 的时候会触发 TabLayout 的scrollTo,而点击 Tab 会触发ViewPager 滑动,ViewPager的滑动也特么触发了 scrollTo,这 ViewPager 滑动导致的 scrollTo 就是我们闪烁的原因!
分析完毕,如何解决呢?
其实解决方案很简单,我们只要使点击 Tab 的时候不触发 ViewPager 滑动的那个 scrollTo 就行了。
How?
我们在自己滑动 ViewPager 的时候 scrollTo 还是要走的,那么自己滑动和点击 Tab 触发的 ViewPager 滑动有啥区别呢?当然有!pageScrollState 不同!自己滑动的时候是 SCROLL_STATE_DRAGGING,而点击 Tab 时是 SCROLL_STATE_IDLE。
那么显而易见了,通过 pageScrollState 来区分下就行了。
我们需要对刚刚分析的 TabLayoutOnPageChangeListener 类的实现做点改变:
|
|
只有 pageScrollState 是 SCROLL_STATE_DRAGGING 的时候才触发 TabLayoutOnPageChangeListener 的 onPageScrolled。
但是 TabLayoutOnPageChangeListener 是 TabLayout 的 mPageChangeListener 变量,我们需要替换它,那只能反射了。
|
|
这样一来,就完成了,看看效果:
即使是官方的东西但难免也会有点小问题,重视细节,再解决它,这个过程还是不错的。
写了个小 demo,地址:https://github.com/JeasonWong/FixedTabLayout
项目地址:https://github.com/JeasonWong/R2Assistant
在子 module 中使用 ButterKnife 时,如果想使用 ButterKnife 提供的编译期注解,那么就得使用 ButterKnife 的 gradle 插件所生成的R2.java,比如 @BindView( R2.id.xxx )
,关于更多 R.java 与 R2.java 的资料可以看我这篇文章 R.java、R2.java是时候懂了。
当我们在子 module 中新增资源 id 时使用 R2.id.xxx 会报红,报红的原因是 R2.java 是依赖 R.java 生成的,必须重新 build project 生成全新的 R2.java,但这样耗时太久了,大点的工程基本需要四五分钟,报红又使强迫症实在看不下去,那么 R2Assistant 就是来解决这个问题的,使用这个插件可以快速生成 R2.java 中还不存在的 fileds,从而提高工作效率。
|
|
如果你想对所有的子 module 生效,执行 ./gradlew sweepR2
如果你只想对指定的子 module 生效,执行 ./gradlew sweepR2 -PmoduleName=${subModuleName}
原理其实很简单,基本利用正则表达式。
1、写出 @BindView( R2.id.xxx )
的正则 R2\.id\.([\w]*
2、遍历 /src/main/java 下的所有 java 文件,并找出所有匹配 1 中正则的资源名:
|
|
3、写出子 module 对应的R2.java 中 id 的正则 ,如 @IdRes
public static final int action_bar = 0x7f0a004f;
,对应的正则是:@IdRes[\s]*public static final int ([\w]*) = *[\w]*;
4、找出子 module 对应的R2.java 中 符合 2 中正则的资源名:
|
|
5、找出 /src/main/java 下的新增资源:
|
|
6、在 R2.java 中生成新的 filed ,新增 filed 的值可以随便撸,反正运行时用不着:
|
|
7、简单吧。
实现这个功能其实有很多方案,我的这种并不是最好的,我目前想的一个不错的方案是监听 xml 里的变化,如果有新增资源 id ,而这个 id 在 R2.java 中又不存在,那么自动添加这个 field,而不用现在这样执行一个task,感兴趣的同学可以做做。
项目地址:https://github.com/JeasonWong/MainDexWrapper
场景很多,先说两种:
当使用了multidex进行分包后,为了优化app首次启动
需要代理Application时,如InstantRun和其他hotfix框架
问题1:子module里的R.java为何不是常量?
问题2:ButterKnife是怎么解决的?
问题3:由于ButterKnife的R2.java存在,导致java compile替换了注解中的常量,为何实际运行时没出现问题?
先上demo地址:https://github.com/JeasonWong/CostTime
实际业务开发中有很多需要不改变原业务代码,而需额外增加一些包括各种统计的需求,如APM、无数据埋点等,也就是耳熟能详的AOP,本文以统计方法耗时为例,不使用Aspectj,采用原生态的方式进行实践。
使用者所需要做的就是对所需要统计耗时的方法头部加指定注解@Cost就可以使用了。