作者
大家好,我叫小嘉;
本人20年本科毕业于广东工业大学,于2020年6月加入37手游安卓团队;目前工作是国内游戏发行安卓相关开发。
本文目录
一、背景
二、分析及解决
1、multidex 相关介绍
2、分主dex调研
(1)gradle 分主dex关键代码
(2)gradle 3.0 之后代码变化
3、测试(Android Gradle Plugin 3.3.0 版本)
(1)mainfest_keep.txt 规则检测
(2)maindexlist.txt 规则检测
三、分 dex 方案(gradle 3.3.0 版本)
四、实战
一、背景
在游戏发行行业,目前我们对于多个渠道sdk 都需要接同一个游戏包的业务场景下,目前采用的方案是:
用 apktool 反编译游戏包 跟 用 apktool 反编译渠道sdk demo资源合并后重新回编译打成 apk 包 。
但是,有时代码量较多,在进行回编译 smali 文件夹有时候会出现 方法数超 65535 的错误,我们便打不了包。对于方法数超的问题,谷歌推出了官方的解决方案: multiDex。
难点
multidex 的触发时机是在回编译时 gradle 插件触发的,但无法适用于我们目前的业务场景,为了兼容低版本手机,不能采取随意分包的方式。
二、分析及解决
multidex 相关介绍
multidex 使用的官方文档:
developer.android.google.cn/studio/buil…
在 art 虚拟机中,也就是 Android 5.0 版本之后,官方已经自动支持 multiDex 操作,因为 art 虚拟机运行的的 oat 文件已经把多个 dex 文件集合在一个 oat 文件了。 但为了兼容低版本手机,需要针对 Multidex 进行配置:
1 在项目的 build.gradle 文件里面设置 multiDexEnabled 为true
2 添加关于 multidex 的依赖,清单文件中声明的 Application(二选一) :
(1) 继承 MultiDexApplication.
(2)在 attachBaseContext() 中调用 Multidex.install().
对于步骤 1 而言,是 android 的 gradle 插件所做的,使其每一个dex 方法数目不会超过 65535,并且能兼容低版本手机。
对于步骤 2 而言, 依赖的是一个 jar 包,后面所做的步骤其实就是怎么让多个 dex 能在 davilk 虚拟机上运行。其实就是反射了一个 dexElements, 将其他 dex 的路径放在这个 dexElements 里面,这时 classloader 在进行加载 class 的时候便不会找不到类。
原理可查看相关博客:
Android分包MultiDex源码分析 – HansChen – 博客园 (cnblogs.com)
分析
所以为了兼容低版本手机,我们需要做的是:
1 仿制 google 官方执行 multideDexEnadble 这个过程,即 google 是怎么分包的?
2 执行 multidex 的处理,添加 multidex 支持库,以及相应的处理操作。
为什么需要分主 dex 和其他dex,主dex 和其他 dex 有什么区别吗?
multidex.install() 方法是在 application 的 attachBaseContext 里执行,假如某些类没有在 主 dex , 但是执行时机在 application 的 attachBaseContext 之前,那这个时候就是出错了,所以一些特定的类需要放在 主 dex。
分主dex调研:
网上的有很多 gradle 分主 dex 相关的博客解析:
gradle 分主dex关键代码:
public void transform(TransformInvocation invocation) throws IOException, TransformException, InterruptedException {LoggingManager loggingManager = invocation.getContext().getLogging();loggingManager.captureStandardOutput(LogLevel.INFO);loggingManager.captureStandardError(LogLevel.WARN);try {File input = verifyInputs(invocation.getReferencedInputs());this.shrinkWithProguard(input);this.computeList(input);} catch (ProcessException | ParseException var4) {throw new TransformException(var4);}}private void shrinkWithProguard(File input) throws IOException, ParseException {this.configuration.obfuscate = false;this.configuration.optimize = false;this.configuration.preverify = false;this.dontwarn();this.dontnote();this.forceprocessing();this.applyConfigurationFile(this.manifestKeepListProguardFile);if (this.userMainDexKeepProguard != null) {this.applyConfigurationFile(this.userMainDexKeepProguard);}this.keep("public class * extends android.app.Instrumentation { <init>(); }");this.keep("public class * extends android.app.Application { <init>(); void attachBaseContext(android.content.Context);}");this.keep("public class * extends android.app.backup.BackupAgent { <init>(); }");this.keep("public class * extends java.lang.annotation.Annotation { *;}");this.keep("class com.android.tools.ir.** {*;}");this.libraryJar(this.findShrinkedAndroidJar());this.inJar(input, (List)null);this.outJar(this.variantScope.getProguardComponentsJarFile());this.printconfiguration(this.configFileOut);this.runProguard();}public void transform(TransformInvocation invocation) throws IOException, TransformException, InterruptedException { LoggingManager loggingManager = invocation.getContext().getLogging(); loggingManager.captureStandardOutput(LogLevel.INFO); loggingManager.captureStandardError(LogLevel.WARN); try { File input = verifyInputs(invocation.getReferencedInputs()); this.shrinkWithProguard(input); this.computeList(input); } catch (ProcessException | ParseException var4) { throw new TransformException(var4); } } private void shrinkWithProguard(File input) throws IOException, ParseException { this.configuration.obfuscate = false; this.configuration.optimize = false; this.configuration.preverify = false; this.dontwarn(); this.dontnote(); this.forceprocessing(); this.applyConfigurationFile(this.manifestKeepListProguardFile); if (this.userMainDexKeepProguard != null) { this.applyConfigurationFile(this.userMainDexKeepProguard); } this.keep("public class * extends android.app.Instrumentation { <init>(); }"); this.keep("public class * extends android.app.Application { <init>(); void attachBaseContext(android.content.Context);}"); this.keep("public class * extends android.app.backup.BackupAgent { <init>(); }"); this.keep("public class * extends java.lang.annotation.Annotation { *;}"); this.keep("class com.android.tools.ir.** {*;}"); this.libraryJar(this.findShrinkedAndroidJar()); this.inJar(input, (List)null); this.outJar(this.variantScope.getProguardComponentsJarFile()); this.printconfiguration(this.configFileOut); this.runProguard(); }public void transform(TransformInvocation invocation) throws IOException, TransformException, InterruptedException { LoggingManager loggingManager = invocation.getContext().getLogging(); loggingManager.captureStandardOutput(LogLevel.INFO); loggingManager.captureStandardError(LogLevel.WARN); try { File input = verifyInputs(invocation.getReferencedInputs()); this.shrinkWithProguard(input); this.computeList(input); } catch (ProcessException | ParseException var4) { throw new TransformException(var4); } } private void shrinkWithProguard(File input) throws IOException, ParseException { this.configuration.obfuscate = false; this.configuration.optimize = false; this.configuration.preverify = false; this.dontwarn(); this.dontnote(); this.forceprocessing(); this.applyConfigurationFile(this.manifestKeepListProguardFile); if (this.userMainDexKeepProguard != null) { this.applyConfigurationFile(this.userMainDexKeepProguard); } this.keep("public class * extends android.app.Instrumentation { <init>(); }"); this.keep("public class * extends android.app.Application { <init>(); void attachBaseContext(android.content.Context);}"); this.keep("public class * extends android.app.backup.BackupAgent { <init>(); }"); this.keep("public class * extends java.lang.annotation.Annotation { *;}"); this.keep("class com.android.tools.ir.** {*;}"); this.libraryJar(this.findShrinkedAndroidJar()); this.inJar(input, (List)null); this.outJar(this.variantScope.getProguardComponentsJarFile()); this.printconfiguration(this.configFileOut); this.runProguard(); }
分主 dex 的过程主要由 shrinkWithProguard() 和 computeList() 构成。
从 shrinkWithProguard() 的代码中看出:
1.找出 manifest_keep.txt,multidex-config.txt (自定义配置), multidex-config.pro (自定义配置) 中的类。
2.找到 Instrumentation , Application ,BackupAgent , Annotation
对这些类调用 shrinkedAndroid.jar 包 解析得到一些类。
computeList() 用来找出类的引用。
multidex-config.txt 和 multidex-config.pro 的来由(来自 multidex 使用的官方文档):
在 gradle2.2.4 版本中, CreateManifestKeepList 是用来生成 manifest_keep.txt 文件的。这个是用来解析 AndroidMainfest 清单文件的,其中最明显的一条是:大意就是用来解析四大组件。
private static final Map<String, String> KEEP_SPECS = ImmutableMap.builder().put("application", "{\n <init>();\n void attachBaseContext(android.content.Context);\n}").put("activity", "{ <init>(); }").put("service", "{ <init>(); }").put("receiver", "{ <init>(); }").put("provider", "{ <init>(); }").put("instrumentation", "{ <init>(); }").build();private static final Map<String, String> KEEP_SPECS = ImmutableMap.builder().put("application", "{\n <init>();\n void attachBaseContext(android.content.Context);\n}").put("activity", "{ <init>(); }").put("service", "{ <init>(); }").put("receiver", "{ <init>(); }").put("provider", "{ <init>(); }").put("instrumentation", "{ <init>(); }").build();private static final Map<String, String> KEEP_SPECS = ImmutableMap.builder().put("application", "{\n <init>();\n void attachBaseContext(android.content.Context);\n}").put("activity", "{ <init>(); }").put("service", "{ <init>(); }").put("receiver", "{ <init>(); }").put("provider", "{ <init>(); }").put("instrumentation", "{ <init>(); }").build();
gradle 3.0 之后代码变化
但在 gradle 3.0 版本之后, CreateManifestKeepList 这个类不见了,说明 Android Gradle Plugin 在 分多 dex 策略上也是有在一直变化的。
对于 shrinkWithProguard() 和 computeList()相关代码中,似乎很多也很杂,直接看源码似乎不是一个好选择。但是我们可以通过一些测试来找到规律从而找到 multidex 规则。
以我们目前日常工程用的 Android Gradle Plugin 3.3.0 版本,我们来看一下到时是怎么分多 dex
我们配置了 multidex 后,进行 build 命令后看到工程目录下 build 文件夹有
multi-dex 这个目录,其中存放着 manifest_keep.txt 目录,和 maindexlist 目录。
manifest_keep.txt: 对 AndroidMainfest 的剪裁,保留需要 keep 的部分。
maindexlist.txt: 需要放在主 dex 的所有类,存放的是类的路径。
maindexlist.txt 中的类是怎么来的?
输入这些需要 keep 住的类后,调用了 shrinkedAndroid.jar,做了什么呢?computeList()又做了什么?调用了 shrinkedAndroid.jar + computeList() 做了什么才会算进 maindexlist.txt 中?由于 gradle 源码比较复杂且 调用了相关 jar 包,没办法直接从源码上看整个的流程逻辑,但可以从我们自己测试中去看结果。
测试(Android Gradle Plugin 3.3.0 版本)
mainfest_keep.txt 规则检测:
此时我们在清单文件里面声明四大组件,是否会放在 manifest_keep 里面呢?
运行结果:
发现只有 Application 会存放在 mainfest_keep.txt 中。
其实也可以理解:
调用流程: Application的attachBaseContext —> ContentProvider的onCreate —-> Application的onCreate —> Activity、Service等的onCreate(Activity和Service不分先后);
关于执行顺序,详细可看博客:
maindexlist.txt 规则检测:
根据前面的结论可得出 keep 规则的源头有以下:
1 mainfest_keep.txt
2 multidex-config.txt (自定义配置)
3 multidex-config.pro (自定义配置)
4 android.app.Instrumentation
5 android.app.Application
6 android.app.backup.BackupAgent
7 java.lang.annotation.Annotation
下面以 keep public class * extends java.lang.annotation.Annotation { *;} 为例子来看一下有哪些类会放在 maindexlist.txt 中:
我们需要验证六个点:
1 keep 的类 中 匿名内部类是否会算进去?
2 keep 的类中的直接引用是否会算进去?
3 keep 的类中的间接引用是否会算进去(即keep 类中所有引用的引用)?
4 keep 的子类中是否会算进去?
5 keep 的子类中 1 2 3 点是否会算进去?
6 对于其他类引用了该类是否会算进去?
验证代码:
//自定义注解public @interface MyAnnation {}// 实现类public class ImplementMyAnnation implements MyAnnation{@Overridepublic Class<? extends Annotation> annotationType() {return null;}//匿名内部类 验证第1点View.OnClickListener a = new View.OnClickListener() {@Overridepublic void onClick(View v) {int a =10 ;}};ImplementMyAnnationRefClass implementMyAnnationRefClass = new ImplementMyAnnationRefClass();}// 直接引用类 验证第2点public class ImplementMyAnnationRefClass {ImplementMyAnnationRefRefClass implementMyAnnationRefRefClass = new ImplementMyAnnationRefRefClass();}// 间接引用类 验证第 3点public class ImplementMyAnnationRefRefClass {ImplementMyAnnationRefRefRefClass implementMyAnnationRefRefRefClass = new ImplementMyAnnationRefRefRefClass();}// 间接引用类 验证第 3点public class ImplementMyAnnationRefRefRefClass {}// 子类 验证第 4点public class ExtendImplementMyAnnation extends ImplementMyAnnation {//匿名内部类 验证第 5点View.OnClickListener a = new View.OnClickListener() {@Overridepublic void onClick(View v) {int a =10 ;}};ExtendImplementMyAnnationRefClass extendImplementMyAnnationRefClass = new ExtendImplementMyAnnationRefClass();}// 子类直接引用类 验证第 5点public class ExtendImplementMyAnnationRefClass {ExtendImplementMyAnnationRefRefClass extendImplementMyAnnationRefRefClass = new ExtendImplementMyAnnationRefRefClass();}// 子类间接引用该类 验证第 5点public class ExtendImplementMyAnnationRefRefClass {}//其他类引用了 ImplementMyAnnation 验证第 6点public class ClassRefImplementMyAnnation {ImplementMyAnnation implementMyAnnation = new ImplementMyAnnation();}//自定义注解 public @interface MyAnnation { } // 实现类 public class ImplementMyAnnation implements MyAnnation{ @Override public Class<? extends Annotation> annotationType() { return null; } //匿名内部类 验证第1点 View.OnClickListener a = new View.OnClickListener() { @Override public void onClick(View v) { int a =10 ; } }; ImplementMyAnnationRefClass implementMyAnnationRefClass = new ImplementMyAnnationRefClass(); } // 直接引用类 验证第2点 public class ImplementMyAnnationRefClass { ImplementMyAnnationRefRefClass implementMyAnnationRefRefClass = new ImplementMyAnnationRefRefClass(); } // 间接引用类 验证第 3点 public class ImplementMyAnnationRefRefClass { ImplementMyAnnationRefRefRefClass implementMyAnnationRefRefRefClass = new ImplementMyAnnationRefRefRefClass(); } // 间接引用类 验证第 3点 public class ImplementMyAnnationRefRefRefClass { } // 子类 验证第 4点 public class ExtendImplementMyAnnation extends ImplementMyAnnation { //匿名内部类 验证第 5点 View.OnClickListener a = new View.OnClickListener() { @Override public void onClick(View v) { int a =10 ; } }; ExtendImplementMyAnnationRefClass extendImplementMyAnnationRefClass = new ExtendImplementMyAnnationRefClass(); } // 子类直接引用类 验证第 5点 public class ExtendImplementMyAnnationRefClass { ExtendImplementMyAnnationRefRefClass extendImplementMyAnnationRefRefClass = new ExtendImplementMyAnnationRefRefClass(); } // 子类间接引用该类 验证第 5点 public class ExtendImplementMyAnnationRefRefClass { } //其他类引用了 ImplementMyAnnation 验证第 6点 public class ClassRefImplementMyAnnation { ImplementMyAnnation implementMyAnnation = new ImplementMyAnnation(); }//自定义注解 public @interface MyAnnation { } // 实现类 public class ImplementMyAnnation implements MyAnnation{ @Override public Class<? extends Annotation> annotationType() { return null; } //匿名内部类 验证第1点 View.OnClickListener a = new View.OnClickListener() { @Override public void onClick(View v) { int a =10 ; } }; ImplementMyAnnationRefClass implementMyAnnationRefClass = new ImplementMyAnnationRefClass(); } // 直接引用类 验证第2点 public class ImplementMyAnnationRefClass { ImplementMyAnnationRefRefClass implementMyAnnationRefRefClass = new ImplementMyAnnationRefRefClass(); } // 间接引用类 验证第 3点 public class ImplementMyAnnationRefRefClass { ImplementMyAnnationRefRefRefClass implementMyAnnationRefRefRefClass = new ImplementMyAnnationRefRefRefClass(); } // 间接引用类 验证第 3点 public class ImplementMyAnnationRefRefRefClass { } // 子类 验证第 4点 public class ExtendImplementMyAnnation extends ImplementMyAnnation { //匿名内部类 验证第 5点 View.OnClickListener a = new View.OnClickListener() { @Override public void onClick(View v) { int a =10 ; } }; ExtendImplementMyAnnationRefClass extendImplementMyAnnationRefClass = new ExtendImplementMyAnnationRefClass(); } // 子类直接引用类 验证第 5点 public class ExtendImplementMyAnnationRefClass { ExtendImplementMyAnnationRefRefClass extendImplementMyAnnationRefRefClass = new ExtendImplementMyAnnationRefRefClass(); } // 子类间接引用该类 验证第 5点 public class ExtendImplementMyAnnationRefRefClass { } //其他类引用了 ImplementMyAnnation 验证第 6点 public class ClassRefImplementMyAnnation { ImplementMyAnnation implementMyAnnation = new ImplementMyAnnation(); }
运行后在 maindexlist 中的内容为:
可得出结论(还验证了一些其他的类,没贴出来):
1 keep 的类 中 匿名内部类会算进去。
2 keep 的类中的直接引用是会算进去。
3 keep 的类中的间接引用是会算进去(即keep 类中所有引用的引用)。
4 keep 的子类中是会算进去。
5 keep 的子类中 1 2 3 点是会算进去。
6 对于其他类引用了该类是不会算进去。
注:Annation 的用法比较特殊(分自定义注解类,实现注解类,继承注解类):对于规则也都合适
三、分 dex 方案(gradle 3.3.0 版本)
1 找出 清单文件中的 MainApplication 类。
2 找出 multidex-config.txt (自定义配置), multidex-config.pro (自定义配置) 中需要的类
3 找出来 android.app.Instrumentation , android.app.Application,android.app.backup.BackupAgent ,java.lang.annotation.Annotation 这些类
对 1 2 3中的类分别执行操作:
(1) 找出各个类的所有引用(递归所有引用) 加入到 maindexlist
(2) 找出所有子类,同时找出各个子类的所有引用(递归所有引用) 加入到 maindexlist
(3) 找出各个类的匿名内部类加入 加入到 maindexlist
四、实战
由于工作性质决定,需要对 apk 进行反编译操作,从中间加一些代码进去,有时会导致 65535 问题,所以multidex 操作 是在 smali 层进行。
放入 maindexlist 中的类
1 定义 subClassList 中的类:
(1) 清单文件中的Application, 主 Activity, provider 标签的类 (触发时机比较早)
(2) android.app.Instrumentation , android.app.Application , android.app.backup.BackupAgent, java.lang.annotation.Annotation 类
2 获取 subClassList 中的直接引用类和依赖引用类放进 maindexlist 类中
3 获取 subClassList 中所有子类的直接引用类和依赖引用类放进 maindexlist 类中
4 对 maindexlist 中类 存在的的匿名内部类放进 maindexlist 中。
注:(multidex-config.txt , multidex-config.pro 中没有去找,很少需要自定义配置)
最重要的是如何获得一个 类的所有引用,这里介绍一个很好用的工具:Javassist
Javassist 是一个开源的分析、编辑和创建Java字节码的类库。
里面的 api 用法可详看: www.javassist.org/html/javass…
其中有一个 getRefClasses() 方法 能够获取该类的所有引用。大概是原理是解析了 class 文件信息,从而获取相应信息,有兴趣了解 class 文件的同学自行学习。
主要步骤
(1) 由于 javassist 的操作对象是 class 文件或者 jar 包,所以需要进行将 smali 转为 jar 包。
这里采用的路线是: smali –> dex –>jar ,分别用到工具 smali.jar 和 dex2jar 这个两个工具。
注:smali.jar 是用来将 smali 转为 dex 的,但是 假如 smali 文件夹 方法数超 65535 ,则会失败,所以可分多几个 smali 文件夹来确定每一个包都不会超过 65535 。
在处理 smali 文件过程中,如何确定是否可以还是比较每一个 smali 文件夹中方法数目不会超过 65535?这里试了很多次的之后,发现 smali 文件数目不超过 6000 的话,是一个很安全的范围,通常情况下都能打成 dex 成功。
所以这里写了一个 计算文件数目的方法 + 对每个文件进行数目平分后移动,并循环操作到 每个文件夹中 smali 文件数目 都不会超过 6000。
/*** 将 path 中 smali 文件夹 超过 num 数量 的 smali 文件夹进行平分操作* @param path*/public static void splitSmali(String path,int num){System.out.println("splitSmali:---> " + "path: " + path);File file = new File(path);int originalCount = 0;int smaliCount = 0;for (File f:file.listFiles()){if (f.getName().startsWith("smali")) // 判断当前目录下有几个 smali 文件夹originalCount ++ ;}smaliCount = originalCount;System.out.println("originalCount: " + originalCount);Comparator<Map.Entry<String, Integer>> valCmp = new Comparator<Map.Entry<String,Integer>>() {@Overridepublic int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {return o2.getValue()-o1.getValue();}};for (int i=1;i<=originalCount;i++){String suffix = i==1?"smali":"smali_classes" + i;String smaliPath = path + File.separator + suffix;System.out.println("smaliPath:--> " + smaliPath);ArrayList<String> list = averageSmali(smaliPath,valCmp,num); // 平分操作,如果当前 smali 文件夹 数目操作,则进行平分操作,返回需要移 动的文件夹if (list!=null){smaliCount ++;for (String originalPath: list){String targetPath = originalPath.replaceAll(suffix,"smali_classes" + smaliCount);// 给移动后的文件夹命名File targetFile = new File(targetPath);File originalFile = new File(originalPath);FileUtil.checkParentFile(targetPath);// 如果没有父目录就创建父目录boolean isMoveSuccess = originalFile.renameTo(targetFile);if (isMoveSuccess){System.out.println("originalPath:" + originalPath + " 移动到: " + targetPath);}}}}File file1 = new File(path);for (File f1:file1.listFiles()) {System.out.println(f1.getAbsolutePath());if (f1.getName().startsWith("smali")) {File srcFile = new File(f1.getAbsolutePath());int sum = FileUtil.getFilesCount(srcFile, "smali");System.out.println("sum" + "-" + sum);if (sum>6000){System.out.println("包太大,需要重新分包!!");splitSmali(path,num); // 如果当前还有 smali 文件夹文件数目超过 6000,则再进行分割操作。}}}}/** * 将 path 中 smali 文件夹 超过 num 数量 的 smali 文件夹进行平分操作 * @param path */ public static void splitSmali(String path,int num){ System.out.println("splitSmali:---> " + "path: " + path); File file = new File(path); int originalCount = 0; int smaliCount = 0; for (File f:file.listFiles()){ if (f.getName().startsWith("smali")) // 判断当前目录下有几个 smali 文件夹 originalCount ++ ; } smaliCount = originalCount; System.out.println("originalCount: " + originalCount); Comparator<Map.Entry<String, Integer>> valCmp = new Comparator<Map.Entry<String,Integer>>() { @Override public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) { return o2.getValue()-o1.getValue(); } }; for (int i=1;i<=originalCount;i++){ String suffix = i==1?"smali":"smali_classes" + i; String smaliPath = path + File.separator + suffix; System.out.println("smaliPath:--> " + smaliPath); ArrayList<String> list = averageSmali(smaliPath,valCmp,num); // 平分操作,如果当前 smali 文件夹 数目操作,则进行平分操作,返回需要移 动的文件夹 if (list!=null){ smaliCount ++; for (String originalPath: list){ String targetPath = originalPath.replaceAll(suffix,"smali_classes" + smaliCount);// 给移动后的文件夹命名 File targetFile = new File(targetPath); File originalFile = new File(originalPath); FileUtil.checkParentFile(targetPath);// 如果没有父目录就创建父目录 boolean isMoveSuccess = originalFile.renameTo(targetFile); if (isMoveSuccess){ System.out.println("originalPath:" + originalPath + " 移动到: " + targetPath); } } } } File file1 = new File(path); for (File f1:file1.listFiles()) { System.out.println(f1.getAbsolutePath()); if (f1.getName().startsWith("smali")) { File srcFile = new File(f1.getAbsolutePath()); int sum = FileUtil.getFilesCount(srcFile, "smali"); System.out.println("sum" + "-" + sum); if (sum>6000){ System.out.println("包太大,需要重新分包!!"); splitSmali(path,num); // 如果当前还有 smali 文件夹文件数目超过 6000,则再进行分割操作。 } } } }/** * 将 path 中 smali 文件夹 超过 num 数量 的 smali 文件夹进行平分操作 * @param path */ public static void splitSmali(String path,int num){ System.out.println("splitSmali:---> " + "path: " + path); File file = new File(path); int originalCount = 0; int smaliCount = 0; for (File f:file.listFiles()){ if (f.getName().startsWith("smali")) // 判断当前目录下有几个 smali 文件夹 originalCount ++ ; } smaliCount = originalCount; System.out.println("originalCount: " + originalCount); Comparator<Map.Entry<String, Integer>> valCmp = new Comparator<Map.Entry<String,Integer>>() { @Override public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) { return o2.getValue()-o1.getValue(); } }; for (int i=1;i<=originalCount;i++){ String suffix = i==1?"smali":"smali_classes" + i; String smaliPath = path + File.separator + suffix; System.out.println("smaliPath:--> " + smaliPath); ArrayList<String> list = averageSmali(smaliPath,valCmp,num); // 平分操作,如果当前 smali 文件夹 数目操作,则进行平分操作,返回需要移 动的文件夹 if (list!=null){ smaliCount ++; for (String originalPath: list){ String targetPath = originalPath.replaceAll(suffix,"smali_classes" + smaliCount);// 给移动后的文件夹命名 File targetFile = new File(targetPath); File originalFile = new File(originalPath); FileUtil.checkParentFile(targetPath);// 如果没有父目录就创建父目录 boolean isMoveSuccess = originalFile.renameTo(targetFile); if (isMoveSuccess){ System.out.println("originalPath:" + originalPath + " 移动到: " + targetPath); } } } } File file1 = new File(path); for (File f1:file1.listFiles()) { System.out.println(f1.getAbsolutePath()); if (f1.getName().startsWith("smali")) { File srcFile = new File(f1.getAbsolutePath()); int sum = FileUtil.getFilesCount(srcFile, "smali"); System.out.println("sum" + "-" + sum); if (sum>6000){ System.out.println("包太大,需要重新分包!!"); splitSmali(path,num); // 如果当前还有 smali 文件夹文件数目超过 6000,则再进行分割操作。 } } } }
(2) javassist 在设置 classLoader 的时候,需要把 android.jar 包放进去,并将用 dex2jar 生成的 jar 包也放进去. (android.jar 包是 虚拟机自带的,但是可能 android.jar 包中的某个类 作为引用,引用到 了用 dex2jar 生成的jar 包中的类)
(3) 递归所有引用的写法:主要思想就是 DFS.
这里写出来的 maindexlist 有一些是系统类 但是没有关系,只要能全部覆盖smali 中的类就行。
/*** 找出 className 类 ,递归该类所有引用并添加到 mainDexList DFS 算法* @param className 类名* @param pool*/public static void getRefClass(String className, ClassPool pool,HashSet<String> mainDexClassList) {if (className == null) return; // 不需要遍历if (className.contains("java.") || className.contains("android.os.")) return;// jdk 和 Android 一些系统类,不需要遍历CtClass cc = null;try {cc = pool.get(className); // 获得 CtClass 实例} catch (NotFoundException e) {return;}mainDexClassList.add(className); // 将该 类添加进 maindexclasslist ,比较已经访问for (String name : cc.getRefClasses()) { // 获取该类的引用类if (!name.contains("java.")) {if (!mainDexClassList.contains(name)) { // 如果引用类中还有没访问过的,递归调用mainDexClassList.add(name);getRefClass(name, pool,mainDexClassList);}}}}/** * 找出 className 类 ,递归该类所有引用并添加到 mainDexList DFS 算法 * @param className 类名 * @param pool */ public static void getRefClass(String className, ClassPool pool,HashSet<String> mainDexClassList) { if (className == null) return; // 不需要遍历 if (className.contains("java.") || className.contains("android.os.")) return; // jdk 和 Android 一些系统类,不需要遍历 CtClass cc = null; try { cc = pool.get(className); // 获得 CtClass 实例 } catch (NotFoundException e) { return; } mainDexClassList.add(className); // 将该 类添加进 maindexclasslist ,比较已经访问 for (String name : cc.getRefClasses()) { // 获取该类的引用类 if (!name.contains("java.")) { if (!mainDexClassList.contains(name)) { // 如果引用类中还有没访问过的,递归调用 mainDexClassList.add(name); getRefClass(name, pool,mainDexClassList); } } } }/** * 找出 className 类 ,递归该类所有引用并添加到 mainDexList DFS 算法 * @param className 类名 * @param pool */ public static void getRefClass(String className, ClassPool pool,HashSet<String> mainDexClassList) { if (className == null) return; // 不需要遍历 if (className.contains("java.") || className.contains("android.os.")) return; // jdk 和 Android 一些系统类,不需要遍历 CtClass cc = null; try { cc = pool.get(className); // 获得 CtClass 实例 } catch (NotFoundException e) { return; } mainDexClassList.add(className); // 将该 类添加进 maindexclasslist ,比较已经访问 for (String name : cc.getRefClasses()) { // 获取该类的引用类 if (!name.contains("java.")) { if (!mainDexClassList.contains(name)) { // 如果引用类中还有没访问过的,递归调用 mainDexClassList.add(name); getRefClass(name, pool,mainDexClassList); } } } }
(4) 获取 subClassList 中所有子类的直接引用类和依赖引用类放进 maindexlist 类中
/*** 判断 currentClass 是否是 subClassList 列表中的类 的子类* 是的话 将 currentClass 的直接引用加入 mainDexList,并将 currentClass 加入 subClassList* @param pool classPool 对象* @param mainDexList mainDexList 所有应该放在主 dex 的类* @param currentClass 当前类* @param subClassList* @return* @throws NotFoundException*/private static boolean isHaveParentClass(ClassPool pool, Set<String> mainDexList, String currentClass, Set<String> subClassList) throws NotFoundException {if (subClassList.contains(currentClass)) { // currentClass已经包含 currentClassreturn true;}CtClass ctClass = null;CtClass superClass = null;try {ctClass = pool.getCtClass(currentClass);superClass = ctClass.getSuperclass(); // 获得 currentClass 的父类} catch (NotFoundException e) {return false;}if (superClass != null) {if (isHaveParentClass(pool, mainDexList, superClass.getName(), subClassList)) {// 递归判断父类是否是 currentClass 中的类,是的话 将 currentClass 加上,并递归引用类subClassList.add(currentClass);getRefClass(currentClass,pool,mainDexList);return true;}}return false;}/** * 判断 currentClass 是否是 subClassList 列表中的类 的子类 * 是的话 将 currentClass 的直接引用加入 mainDexList,并将 currentClass 加入 subClassList * @param pool classPool 对象 * @param mainDexList mainDexList 所有应该放在主 dex 的类 * @param currentClass 当前类 * @param subClassList * @return * @throws NotFoundException */ private static boolean isHaveParentClass(ClassPool pool, Set<String> mainDexList, String currentClass, Set<String> subClassList) throws NotFoundException { if (subClassList.contains(currentClass)) { // currentClass已经包含 currentClass return true; } CtClass ctClass = null; CtClass superClass = null; try { ctClass = pool.getCtClass(currentClass); superClass = ctClass.getSuperclass(); // 获得 currentClass 的父类 } catch (NotFoundException e) { return false; } if (superClass != null) { if (isHaveParentClass(pool, mainDexList, superClass.getName(), subClassList)) {// 递归判断父类是否是 currentClass 中的类,是的话 将 currentClass 加上,并递归引用类 subClassList.add(currentClass); getRefClass(currentClass,pool,mainDexList); return true; } } return false; }/** * 判断 currentClass 是否是 subClassList 列表中的类 的子类 * 是的话 将 currentClass 的直接引用加入 mainDexList,并将 currentClass 加入 subClassList * @param pool classPool 对象 * @param mainDexList mainDexList 所有应该放在主 dex 的类 * @param currentClass 当前类 * @param subClassList * @return * @throws NotFoundException */ private static boolean isHaveParentClass(ClassPool pool, Set<String> mainDexList, String currentClass, Set<String> subClassList) throws NotFoundException { if (subClassList.contains(currentClass)) { // currentClass已经包含 currentClass return true; } CtClass ctClass = null; CtClass superClass = null; try { ctClass = pool.getCtClass(currentClass); superClass = ctClass.getSuperclass(); // 获得 currentClass 的父类 } catch (NotFoundException e) { return false; } if (superClass != null) { if (isHaveParentClass(pool, mainDexList, superClass.getName(), subClassList)) {// 递归判断父类是否是 currentClass 中的类,是的话 将 currentClass 加上,并递归引用类 subClassList.add(currentClass); getRefClass(currentClass,pool,mainDexList); return true; } } return false; }
(5) 对 maindexlist中的类判断是否有匿名内部类,有的话也加上。
/*** 获取 mainDexClassList 中所有类的匿名内部类 并添加到 resultList, 同时将 mainDexClassList 添加到 resultList*/public static void getNestedClassList(HashSet<String> mainDexClassList ,HashSet<String> resultList) {ArrayList<String> smaliListPathList = SmaliPreProcessHelper.getSmaliListPath(SmaliDataPath);resultList.addAll(mainDexClassList);for (String name : mainDexClassList) {String lastName = name.substring(name.lastIndexOf(".") + 1);String namePath = name.replaceAll("\\.", quoteReplacement(File.separator));for (String path : smaliListPathList) {File file = new File(path + File.separator + namePath + ".smali");if (file.exists()) {File parentFile = file.getParentFile();for (File f : parentFile.listFiles()) {if (f.getName().startsWith(lastName + "$")) { // 判断当前该类是否有匿名内部类String nestClassName = name.substring(0, name.lastIndexOf(".") + 1) + f.getName().replace(".smali", "");resultList.add(nestClassName);}}}}}}/** * 获取 mainDexClassList 中所有类的匿名内部类 并添加到 resultList, 同时将 mainDexClassList 添加到 resultList */ public static void getNestedClassList(HashSet<String> mainDexClassList ,HashSet<String> resultList) { ArrayList<String> smaliListPathList = SmaliPreProcessHelper.getSmaliListPath(SmaliDataPath); resultList.addAll(mainDexClassList); for (String name : mainDexClassList) { String lastName = name.substring(name.lastIndexOf(".") + 1); String namePath = name.replaceAll("\\.", quoteReplacement(File.separator)); for (String path : smaliListPathList) { File file = new File(path + File.separator + namePath + ".smali"); if (file.exists()) { File parentFile = file.getParentFile(); for (File f : parentFile.listFiles()) { if (f.getName().startsWith(lastName + "$")) { // 判断当前该类是否有匿名内部类 String nestClassName = name.substring(0, name.lastIndexOf(".") + 1) + f.getName().replace(".smali", ""); resultList.add(nestClassName); } } } } } }/** * 获取 mainDexClassList 中所有类的匿名内部类 并添加到 resultList, 同时将 mainDexClassList 添加到 resultList */ public static void getNestedClassList(HashSet<String> mainDexClassList ,HashSet<String> resultList) { ArrayList<String> smaliListPathList = SmaliPreProcessHelper.getSmaliListPath(SmaliDataPath); resultList.addAll(mainDexClassList); for (String name : mainDexClassList) { String lastName = name.substring(name.lastIndexOf(".") + 1); String namePath = name.replaceAll("\\.", quoteReplacement(File.separator)); for (String path : smaliListPathList) { File file = new File(path + File.separator + namePath + ".smali"); if (file.exists()) { File parentFile = file.getParentFile(); for (File f : parentFile.listFiles()) { if (f.getName().startsWith(lastName + "$")) { // 判断当前该类是否有匿名内部类 String nestClassName = name.substring(0, name.lastIndexOf(".") + 1) + f.getName().replace(".smali", ""); resultList.add(nestClassName); } } } } } }
(6) 根据 maindexlist 中的类从smali 文件夹中进行移动并作为主 smali, 其他 smali 文件作为其他 smali
/*** 根据 mainDexList 从原先的所有smali 文件夹中 移动到 主 smali ,其余作为 副 smali* @param smaliPathList 原先所有 smali 文件夹路径* @param mainDexList 放在主 dex 的 类 的 list* @param parentOriginalPath 移动到的新文件夹路径*/public static void moveMainSmali(ArrayList<String> smaliPathList, String[] mainDexList, String parentOriginalPath) {// 作为主 smaliString smaliMainFilePath = parentOriginalPath + File.separator + "smali";for (String name : mainDexList) {name = name.replaceAll("\\.", quoteReplacement(File.separator));for (String parentPath : smaliPathList) {String smaliFilePath = parentPath + File.separator + name + ".smali";File file = new File(smaliFilePath);if (file.exists()) {File targetFile = new File(smaliMainFilePath + File.separator + name + ".smali");FileUtil.checkParentFile(smaliMainFilePath + File.separator + name + ".smali");//判断targetFile 是否有父目录,没有的话创建file.renameTo(targetFile);// 根据 mainDexList 中的目录进行移动break;}}}// 其他 smali 文件夹 命名为 smali_classesxx 并进行复制到 跟主 smali 同一个父目录下for (int i = smaliPathList.size() + 1; i >= 2; i--) {String path = smaliPathList.get(i - 2);String newFilePath = parentOriginalPath + File.separator + "smali_classes" + i;LogUtil.d(path);LogUtil.d(path + " 复制到:" + parentOriginalPath + File.separator + "smali_classes" + i);FileUtil.copyDirectiory(path, newFilePath,false);}/** * 根据 mainDexList 从原先的所有smali 文件夹中 移动到 主 smali ,其余作为 副 smali * @param smaliPathList 原先所有 smali 文件夹路径 * @param mainDexList 放在主 dex 的 类 的 list * @param parentOriginalPath 移动到的新文件夹路径 */ public static void moveMainSmali(ArrayList<String> smaliPathList, String[] mainDexList, String parentOriginalPath) { // 作为主 smali String smaliMainFilePath = parentOriginalPath + File.separator + "smali"; for (String name : mainDexList) { name = name.replaceAll("\\.", quoteReplacement(File.separator)); for (String parentPath : smaliPathList) { String smaliFilePath = parentPath + File.separator + name + ".smali"; File file = new File(smaliFilePath); if (file.exists()) { File targetFile = new File(smaliMainFilePath + File.separator + name + ".smali"); FileUtil.checkParentFile(smaliMainFilePath + File.separator + name + ".smali");//判断targetFile 是否有父目录,没有的话创建 file.renameTo(targetFile);// 根据 mainDexList 中的目录进行移动 break; } } } // 其他 smali 文件夹 命名为 smali_classesxx 并进行复制到 跟主 smali 同一个父目录下 for (int i = smaliPathList.size() + 1; i >= 2; i--) { String path = smaliPathList.get(i - 2); String newFilePath = parentOriginalPath + File.separator + "smali_classes" + i; LogUtil.d(path); LogUtil.d(path + " 复制到:" + parentOriginalPath + File.separator + "smali_classes" + i); FileUtil.copyDirectiory(path, newFilePath,false); }/** * 根据 mainDexList 从原先的所有smali 文件夹中 移动到 主 smali ,其余作为 副 smali * @param smaliPathList 原先所有 smali 文件夹路径 * @param mainDexList 放在主 dex 的 类 的 list * @param parentOriginalPath 移动到的新文件夹路径 */ public static void moveMainSmali(ArrayList<String> smaliPathList, String[] mainDexList, String parentOriginalPath) { // 作为主 smali String smaliMainFilePath = parentOriginalPath + File.separator + "smali"; for (String name : mainDexList) { name = name.replaceAll("\\.", quoteReplacement(File.separator)); for (String parentPath : smaliPathList) { String smaliFilePath = parentPath + File.separator + name + ".smali"; File file = new File(smaliFilePath); if (file.exists()) { File targetFile = new File(smaliMainFilePath + File.separator + name + ".smali"); FileUtil.checkParentFile(smaliMainFilePath + File.separator + name + ".smali");//判断targetFile 是否有父目录,没有的话创建 file.renameTo(targetFile);// 根据 mainDexList 中的目录进行移动 break; } } } // 其他 smali 文件夹 命名为 smali_classesxx 并进行复制到 跟主 smali 同一个父目录下 for (int i = smaliPathList.size() + 1; i >= 2; i--) { String path = smaliPathList.get(i - 2); String newFilePath = parentOriginalPath + File.separator + "smali_classes" + i; LogUtil.d(path); LogUtil.d(path + " 复制到:" + parentOriginalPath + File.separator + "smali_classes" + i); FileUtil.copyDirectiory(path, newFilePath,false); }
(7) 判断 multidex 使用 ,判断当前 smali 是否有使用 multidex,没有的话自己加上。
对于multidex 的支持库而言,可以找一个依赖了 multidex 的 apk 反编译后,找到 smali/android/support/multidex ,这个目录下就是 multidex 支持库下的 smali 代码,将这份代码复制到自己的 smali 对应目录下就可以了。
// 检测是否已经使用了 multidexprivate static boolean checkIsUseMultiDex(String path, String AndroidManifestFilePath, ClassPool classPool) throws DocumentException, NotFoundException {//1 判断是否有继承 MultiDexApplicationManifestProcessor manifestProcessor = new ManifestProcessor(AndroidManifestFilePath);String ApplicationName = manifestProcessor.getAppProcessor().getApplicationName();CtClass ctClass = classPool.getCtClass(ApplicationName);//while (!ctClass.getName().equals("android.app.Application")) {System.out.println(ctClass.getName());ctClass = ctClass.getSuperclass();if (ctClass.getName().equals("android.support.multidex.MultiDexApplication")) {System.out.println("存在继承 MultiDexApplication 的 Application 类");return true;}}// 2 是否有调用 MultiDex.install()String namePath = path + File.separator + ApplicationName.replaceAll("\\.", quoteReplacement(File.separator)) + ".smali";System.out.println(namePath);File applicationFile = new File(namePath);if (applicationFile.exists()) {String content = FileUtil.read(namePath);System.out.println(content);// MultiDex.install() --》smali 写法if (content.contains("Landroid/support/multidex/MultiDex;->install(Landroid/content/Context;)V")) {System.out.println("主 Application 调用了 multidex.install() 方法");return true;}}return false;}// 检测是否已经使用了 multidex private static boolean checkIsUseMultiDex(String path, String AndroidManifestFilePath, ClassPool classPool) throws DocumentException, NotFoundException { //1 判断是否有继承 MultiDexApplication ManifestProcessor manifestProcessor = new ManifestProcessor(AndroidManifestFilePath); String ApplicationName = manifestProcessor.getAppProcessor().getApplicationName(); CtClass ctClass = classPool.getCtClass(ApplicationName); // while (!ctClass.getName().equals("android.app.Application")) { System.out.println(ctClass.getName()); ctClass = ctClass.getSuperclass(); if (ctClass.getName().equals("android.support.multidex.MultiDexApplication")) { System.out.println("存在继承 MultiDexApplication 的 Application 类"); return true; } } // 2 是否有调用 MultiDex.install() String namePath = path + File.separator + ApplicationName.replaceAll("\\.", quoteReplacement(File.separator)) + ".smali"; System.out.println(namePath); File applicationFile = new File(namePath); if (applicationFile.exists()) { String content = FileUtil.read(namePath); System.out.println(content); // MultiDex.install() --》smali 写法 if (content.contains("Landroid/support/multidex/MultiDex;->install(Landroid/content/Context;)V")) { System.out.println("主 Application 调用了 multidex.install() 方法"); return true; } } return false; }// 检测是否已经使用了 multidex private static boolean checkIsUseMultiDex(String path, String AndroidManifestFilePath, ClassPool classPool) throws DocumentException, NotFoundException { //1 判断是否有继承 MultiDexApplication ManifestProcessor manifestProcessor = new ManifestProcessor(AndroidManifestFilePath); String ApplicationName = manifestProcessor.getAppProcessor().getApplicationName(); CtClass ctClass = classPool.getCtClass(ApplicationName); // while (!ctClass.getName().equals("android.app.Application")) { System.out.println(ctClass.getName()); ctClass = ctClass.getSuperclass(); if (ctClass.getName().equals("android.support.multidex.MultiDexApplication")) { System.out.println("存在继承 MultiDexApplication 的 Application 类"); return true; } } // 2 是否有调用 MultiDex.install() String namePath = path + File.separator + ApplicationName.replaceAll("\\.", quoteReplacement(File.separator)) + ".smali"; System.out.println(namePath); File applicationFile = new File(namePath); if (applicationFile.exists()) { String content = FileUtil.read(namePath); System.out.println(content); // MultiDex.install() --》smali 写法 if (content.contains("Landroid/support/multidex/MultiDex;->install(Landroid/content/Context;)V")) { System.out.println("主 Application 调用了 multidex.install() 方法"); return true; } } return false; }
(8) 回编译并 检查低版本手机运行情况
注:有时候对于通过反射来实例化的类没办法知道,所以只能自己手动加上去。
一些运行的日志截图:
方法数超 65535 打包失败进行自动分多 dex:
重新打包成功: