/Android-Easy-MultiDex

Android,multidex,splitdex,分包插件

Primary LanguageJava

#Android傻瓜式分包插件 注1:不想看前半部分的话可以直接跳过到最下面配置部分。
注2:本插件是基于DexKnifePlugin 1.5.6优化改造而来,感谢ceabie的无私奉献。

##填坑之路
###坑1:65536 ,So easy!
**原因:**Dalvik 的 invoke-kind 指令集中,method reference index 只留了 16 bits,最多能引用 65535 个方法。
参考=>由Android 65K方法数限制引发的思考.

解决:

dependencies { 
	compile 'com.android.support:MultiDex:1.0.1'
}

继承 Application ,重写 attachBaseContext(Context)

@Override 
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    MultiDex.install(this);
}

###坑2:Too many classes in –main-dex-list ,what?
**原因:**通过上面的官方分包,已经把原Dex分为1主Dex加多从Dex。主Dex包含所有4大组件,Application,Annotation,multidex等及其必要的直接依赖。由于我们方法数已达到16W之巨,上百个Activity全部塞进主Dex,又成功的把主Dex撑爆了。

解决: gradle

afterEvaluate { 
  tasks.matching { 
    it.name.startsWith('dex') 
  }.each { dx -> 
    if (dx.additionalParameters == null) { 
      dx.additionalParameters = []
    }  
    dx.additionalParameters += '--set-max-idx-number=48000' 
  } 
}

参考=>Android Dex分包之旅

###坑3:gradle 1.5.0之后不支持这种写法 ,what the fuck?
**原因:**官方解释Gralde1.5.0以上已经将(jacoco, progard, multi-dex)统一移到Transform API里,然而Transform API并没有想象的那么简单好用,翻遍Google终于找到一个兼容Gradle 1.5.0以上的分包插件DexKnifePlugin
扩展=>这篇Android 热修复使用Gradle Plugin1.5改造Nuwa插件比较好的介绍了Transform API的使用。

###坑4:NoClassDefFoundError ,are you kiding me?
**原因:**通过插件手动指定main dex中要保留的类,虽然分包成功,但是main dex中的类及其直接引用类很难通过手动的方式指定。

解决方式:
美团Android DEX自动拆包及动态加载简介,他们是通过编写了一个能够自动分析Class依赖的脚本去算出主Dex需要包含的所有必要依赖。看来写脚本是跑不掉了。

###坑5:自定义脚本 ,read the fuck source!
**问题一:**哪些类是需要放入主Dex中?
查看sdk\build-tools\platform-version\mainDexClasses.rules发现放入主Dex相关类有Instrumentation,Application,Activity,Service,ContentProvider,BroadcastReceiver,BackupAgent的所有子类。

**问题二:**gradle是在哪里算出主Dex依赖?
查看Gradle编译任务发现有如下3个编译任务:

运行collect任务,发现会在build/multi-dex目录下单独生成manifest_keep.txt文件,该文件其实就是通过上述规则扫描AndroidManifest生成。manifest_keep.txt保留的是所有需要放入主Dex里的类。还没完,接下来transformClassesWithMultidexlist任务会根据manifest_keep.txt生成必要依赖列表maindexlist.txt,这里面所有类才是真正放入主Dex里的。bingo,现在非常清楚,我们只需要控制进入manifest_keep.txt中的类即可,最终其类的依赖关系由系统帮我们生成即可,安全绿色可靠!

**问题三:**在哪里控制maindexlist.txt的大小?
由问题一我们知道生成manifest_keep.txt的规则,对于绝大部分工程来说,manifest_keep.txt中80%是Activity,其实我们并不需要把全部的Activity放入主Dex,只需要保留必要的Activity即可,如首页 Activity、Laucher Activity 、欢迎页的 Activity 等启动时必要的Activity就OK了。

下图是Gradle的工作流程:
来源:深入理解Android之Gradle

我们只需要在完成任务向量图之后,执行任务之前Hook一下collect任务,过滤掉不必要的activity就OK了。添加Gradle:

//需要加入主dex的Activity列表
def mainDexListActivity = ['WelcomeActivity', 'MainFunctionActivity']
afterEvaluate {
    project.tasks.each { task ->
        if (task.name.startsWith('collect') && task.name.endsWith('MultiDexComponents')) {
            println "main-dex-filter: found task $task.name"
            task.filter { name, attrs ->
                String componentName = attrs.get('android:name')
                if ('activity'.equals(name)) {
                    def result = mainDexListActivity.find {
                        componentName.endsWith("${it}")
                    }
                    return result != null
                } else {
                    return true
                }
            }
        }
    }
}

###坑6:主dex依然爆表,shit again!
其实上面那段脚本已经成功筛选出我们想要放入主Dex的manifest_keep列表maindexlist列表,但是在打包的时候还是把所有类打进主Dex(已无语)。这个时候就需要跟DexKnifePlugin插件配合使用,首先在gradle中加上上述脚本,然后使用插件时在配置文件中加上 -split **.**#-donot-use-suggest。DexKnifePlugin插件运行原理很简单,在生成Dex任务之前首先读取自己的配置文件(包含前面我们通过Gradle脚本生成的maindexlist列表),然后扫描combined.jar(包含工程中所有.class文件)匹配出我们自定义的maindexlist.txt,再替换掉build/multi-dex/maindexlist.txt,和build实例。这样分包的时候就会基于我们的规则生成主Dex。

###坑7:ANR,HAHAHA!
我们最低API=16,测试并未发现ANR问题,所以暂时没考虑景上添花,这个问题比较好解决。
参考=>Android Dex分包之旅

###Congratulation 恭喜,填坑终于结束,不过还有点不爽的是需要同时维护Gradle脚本和插件的配置。 于是乎就将Gradle脚本整合进了插件,这样只需维护一个配置文件就行了。读者可以根据自己需求自行选择分开配置还是整合配置。通过这种方式我们把主Dex的方法数维持在15000左右,从此再也不用担心方法数问题了!!!

##配置部分 第一步:添加根目录Gradle

buildscript {
    dependencies {
        classpath 'com.library.tangxiaolv:dexknife-plus:1.0.1'
    }
}

第二步:在你的App模块的build.gradle添加插件

apply plugin: 'dexknifePlus'

第三步:配置参数

dexKnife{
    //必选参数
    enabled true //if false,禁用分包插件
    //可选参数
    //1.如果没有可选参数,将根据enabled决定是否分包。
    //2.如果有可选参数,需满足必选参数和可选参数的条件才允许分包
    productFlavor 'mock'
    buildType 'debug'

    /*
    *eg:当前productFlavors = dev,buildType = debug,
    *参数组合1:enabled = true,productFlavor = dev,buildType = debug 分包
    *参数组合2:enabled = true,productFlavor = mock,buildType = debug 不分包
    *参数组合1:enabled = true,buildType = debug 所有buildType = debug分包
    *参数组合1:enabled = true,productFlavor = dev 所有productFlavor = dev分包
    * */
}

第四步:在你的App模块目录下新建dexknife.txt,并自定义配置

#为注释符

#-----------主Dex中必要依赖的脚本配置-----------
#默认保留四大组件中其他三大组件(并计算其依赖树),Activity组件选择性保留(使用-just activity 选项),若为空不保留任何Activity
-just activity com.ceabie.demo.MainActivity

#-----------附加类-----------
# 如果你想要某个包路径在maindex中,则使用 -keep 选项,即使他已经在分包的路径中.若为空,不保留任意类
#-keep com.ceabie.demo.**

# 保留单个类.
#-keep android.support.v7.app.AppCompatDialogFragment.class

# 这条配置可以指定这个包下类在第二及其他dex中.
#-split android.support.v?.**
#将全部类移出主Dex
-split **.**

# 不包含Android gradle 插件自动生成的miandex列表.(不使用建议的依赖树,注释掉表示使用,否则-just activity无效)
#-donot-use-suggest

# (分割每个dex包的方法数上限) 扩展参数:例如 --set-max-idx-number=50000
# 如果出现 DexException: Too many classes in --main-dex-list, main dex capacity exceeded:
# 表明限制的方法数小于main dex的必要方法数,调大到合适数值即可
-dex-param --set-max-idx-number=4000

# 不进行dex分包, 直到 dex 的id数量超过 65536.(设置自动执行分包策略)
#-auto-maindex

# 显示miandex的日志.
#-log-mainlist

第五步:在 defaultConfig 或者 buildTypes中打开 multiDexEnabled true,否则不起作用

##已知错误

注:分包的时候如果发现一些莫名的错误,可以关掉instant run,一般都能解决

错误1: (已修复)

Error:Execution failed for task ':Toon:transformClassesWithDexForDebug'.> java.lang.NullPointerException (no error message)

发生此错误只要切换一次Gradle版本就OK了,比如1.5.0

错误2:(已修复)

Unsupported major.minor version 52.0

由于插件中使用到了JDK1.8的一些API,所以将JDK升级到1.8就可以了

错误3: (已修复)

Error:Execution failed for task ':app:transformClassesWithDexForDebug'.
> DexKnife Warnning: Main dex is EMPTY ! Check your config and project!

gradle 切到 1.5.0,目前就发现gradle 2.1.2有这问题。